0. イントロ:月曜7:30の“冷や汗”をテストで潰す
ある月曜の朝、役員のiPadで業務SaaSに入れないと連絡。前週金曜に条件付きアクセス(CA)の手直しをマージしたのを思い出し、ダッシュでサインインログとポリシーを洗う。結果は単純、例外の条件に“場所”の詰めが甘く社外回線+モバイルアプリがブロックされていた。
この手の事故は人の記憶と勘に依存している限り、何度でも起きる。
そこで、ポータルの What If をAPIで回す。プルリク時に「このサインイン文脈なら、このポリシーが適用される/されない」を機械的に照合し、差分があればCIを落とす。以降、月曜7:30の冷や汗は消えた。
1. 仕組みの要点(現場視点の言い換え)
What If は、サインインを“作文”するイメージ。
「誰が(ユーザーorサービスプリンシパル)」「どのアプリに」「どこから(IP/国)」「どのクライアント種別(ブラウザ/モバイル/デスクトップ)」「デバイスは準拠か」「リスクがどうか」等をJSONで指定し、テナント内の全CAに適用/非適用を機械判定させる。
APIの返り値は各ポリシーごとの判定(適用されるか、なぜ外れるか、求められるコントロールは何か)。
要するに、サインインログの「CAタブ」を“未来に対して”取る感じ。テスト前に落とし穴が見える。
権限は読み取り(Policy.Read.ConditionalAccess)で足りる。
設定を壊さず、“覗くだけ”で判定ができるのがありがたい。
注意点はひとつ。入れた条件しか評価されない。
例えば場所やリスク条件をCAで使っているのに、リクエストにipAddress/countryやsignInRiskLevel/userRiskLevelを書かないと、“評価不能(もしくは不一致)”で外れる。テストケースの作り込みが品質を決める。
2. サインイン“作文”の雛形(コピペして書き換え)
2-1. 「ユーザー → 特定アプリ、ブラウザ、社外IP、高サインインリスク、デバイス準拠」
{
"signInIdentity": {
"@odata.type": "#microsoft.graph.userSignIn",
"userId": "<GUID-of-user>"
},
"signInContext": {
"@odata.type": "#microsoft.graph.applicationContext",
"includeApplications": [
"<appId-of-target-resource>"
]
},
"signInConditions": {
"clientAppType": "browser",
"devicePlatform": "windows",
"country": "JP",
"ipAddress": "203.0.113.10",
"signInRiskLevel": "high",
"userRiskLevel": "low",
"deviceInfo": { "isCompliant": true }
},
"appliedPoliciesOnly": false
}
## 2-2. 「認証コンテキスト(機密操作)を要求するケース」
{
"signInIdentity": {
"@odata.type": "#microsoft.graph.userSignIn",
"userId": "<GUID-of-user>"
},
"signInContext": {
"@odata.type": "#microsoft.graph.authContext",
"authenticationContextValue": "<c1〜c99等>"
},
"signInConditions": {
"clientAppType": "mobileAppsAndDesktopClients",
"devicePlatform": "macOS",
"userRiskLevel": "high"
}
}
2-3. 「サービス プリンシパル(Workload ID)での評価」
{
"signInIdentity": {
"@odata.type": "#microsoft.graph.servicePrincipalSignIn",
"servicePrincipalId": "<GUID-of-service-principal>"
},
"signInContext": {
"@odata.type": "#microsoft.graph.applicationContext",
"includeApplications": ["<appId-of-target-resource>"]
},
"signInConditions": {
"servicePrincipalRiskLevel": "high",
"country": "JP"
}
}
迷ったらまず「使っているCA条件」を全部、入力JSONに載せる(場所/プラットフォーム/クライアント種別/リスク/デバイス準拠…)。“作文の精度=結果の精度”です。
3. PowerShellの実装(単発 → 回帰)
3-1. 単発評価 Invoke-GraphWhatIf.ps1
(ローカルは az login 済/CIは後述の azure/login 済を前提)
param(
[Parameter(Mandatory=$true)] [string] $InputJsonPath,
[switch] $AppliedOnly, # 適用ポリシーだけ見たいとき
[string] $GraphBase = "https://graph.microsoft.com"
)
1) Graph用のアクセストークン(Az CLI)
$token = (az account get-access-token --resource-type ms-graph | ConvertFrom-Json).accessToken
2) 入力JSONを読み込み(appliedPoliciesOnly を任意で上書き)
$body = Get-Content -Raw -Path $InputJsonPath | ConvertFrom-Json
if ($AppliedOnly) { $body.appliedPoliciesOnly = $true }
$payload = $body | ConvertTo-Json -Depth 20
3) What If 実行
$uri = "$GraphBase/v1.0/identity/conditionalAccess/evaluate"
$headers = @{ "Authorization" = "Bearer $token"; "Content-Type" = "application/json" }
$response = Invoke-RestMethod -Method POST -Uri $uri -Headers $headers -Body $payload
4) 見やすく整形
$rows = @()
foreach ($p in $response.value) {
$rows += [pscustomobject]@{
PolicyName = $p.displayName
PolicyId = $p.id
State = $p.state # enabled / report-only 等
Applies = $p.policyApplies # 適用/非適用
Reason = $p.analysisReasons # 外れた主因のヒント
GrantControls = ($p.grantControls.builtInControls -join ",")
}
}
$rows | Sort-Object Applies -Descending, PolicyName | Format-Table -AutoSize
return $rows # CI側での判定に使う
メモ:PowerShell派なら Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/" でもOK。Azモジュールのバージョン差異には注意。
3-2. 回帰テスト(期待との突き合わせでジョブを落とす)
宣言的に「このシナリオではAとBは適用、Cは非適用」を書き、ずれたら終了コード≠0でCIを失敗させます。
tests/ca-whatif.cases.json
[
{
"id": "user-web-highrisk-compliant",
"expect": {
"mustApplyByName": ["Require MFA for External", "Block Legacy Auth"],
"mustNotApplyByName": ["Privileged Admin Stronger MFA"]
},
"whatIf": {
"signInIdentity": { "@odata.type": "#microsoft.graph.userSignIn", "userId": "<GUID>" },
"signInContext": { "@odata.type": "#microsoft.graph.applicationContext", "includeApplications": ["<appId>"] },
"signInConditions": {
"clientAppType": "browser", "devicePlatform": "windows",
"country": "JP", "ipAddress": "203.0.113.10",
"signInRiskLevel": "high", "deviceInfo": { "isCompliant": true }
},
"appliedPoliciesOnly": false
}
}
]
Invoke-GraphWhatIf.Tests.ps1
param([string]$CaseFile = "tests/ca-whatif.cases.json")
$cases = Get-Content -Raw $CaseFile | ConvertFrom-Json
$allFailed = @()
foreach ($case in $cases) {
$tmp = New-TemporaryFile
$case.whatIf | ConvertTo-Json -Depth 20 | Set-Content -Path $tmp
$rows = & ./Invoke-GraphWhatIf.ps1 -InputJsonPath $tmp
$applies = $rows | Where-Object { $_.Applies } | Select-Object -ExpandProperty PolicyName
$fails = @()
foreach ($must in $case.expect.mustApplyByName) {
if ($applies -notcontains $must) { $fails += "Expected APPLY: '$must' but not applied." }
}
foreach ($deny in $case.expect.mustNotApplyByName) {
if ($applies -contains $deny) { $fails += "Expected NOT APPLY: '$deny' but applied." }
}
if ($fails.Count) {
Write-Error "[CASE:$($case.id)] FAILED:`n - $(($fails -join "`n - "))"
$allFailed += $fails
} else {
Write-Host "[CASE:$($case.id)] OK"
}
}
if ($allFailed.Count) { exit 1 } else { exit 0 }
ポリシー名は人が変えるので、本番はIDで判定するのが安全。ID↔名前はGraphで一覧取得し、ローカルにマッピングしておくと運用が楽です(後述の「おまけ」参照)。
4. GitHub Actionsに載せる(シークレットレス/OIDC)
4-1. 事前準備(初回だけ)
アプリ登録(サービスプリンシパル)を作成し、アプリ権限に Policy.Read.ConditionalAccess を付与 → 管理者同意。
アプリ登録のFederated credentialにGitHub OIDCを追加(組織/リポジトリ/ブランチ等で絞る)。
リポジトリ側は長期シークレット不要。ワークフローで azure/login を使い、Az CLI経由でGraphトークンを発行してAPIを叩く。
4-2. ワークフロー例 /.github/workflows/ca-whatif.yml
on:
pull_request:
paths: ["infra/entra/ca/**", "tests/ca-whatif.cases.json", ".github/workflows/ca-whatif.yml"]
workflow_dispatch: {}
permissions:
id-token: write # OIDC
contents: read
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # 任意
jobs:
whatif:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
- name: Setup PowerShell modules
run: pwsh -NoLogo -Command "Install-Module Az.Accounts -Scope CurrentUser -Force"
- name: Run Conditional Access WhatIf tests
shell: pwsh
run: |
./scripts/Invoke-GraphWhatIf.ps1 -InputJsonPath tests/sample.single.json | Out-Host
./scripts/Invoke-GraphWhatIf.Tests.ps1 -CaseFile tests/ca-whatif.cases.json
現場メモ:azure/login 後は az account get-access-token --resource-type ms-graph が素直。Connect-MgGraph を無理に使うより、REST直叩きの方がCIでは安定でした。
5. “現場で効いた”運用ノウハウ(泥臭さあり)
「入力が足りない」問題をテストで可視化
What If の判定理由は“なぜ外れたか”まで見える。場所/リスクの欠落、クライアント種別の取り違え(ブラウザ vs モバイル)、プラットフォーム未指定など、人が忘れがちな条件はまず落ちる。ケースを増やして潰すのが王道。
appliedPoliciesOnly の使い分け
設計レビュー中はfalseで非適用ポリシーの理由も拾い、CIの判定はtrueで適用されるものだけに絞ると、誤検知が減る。
Report-only の扱いはルール化
状態(enabled / report-only / disabled)を判定に混ぜるとノイズになる。report-onlyは情報としてログに出しつつ、CIの合否判定から除外するのが安定。
“名前で合否”は壊れやすい
ポリシーのリネームでテストが赤になるあるある。ID基準に切り替えるか、ID→名前のマップをCI前に取っておく。
業務アプリは“体感”まで再現
役員や営業が使う“モバイル+社外IP+準拠デバイス”は最優先でケース化。レガシークライアント(IMAP/POP/旧Office)はブロックの回帰を最低1本入れておくと安心。
Workload ID(サービスプリンシパル)も忘れずに
夜間バッチやCI/CDの止めたくない系は、Workloadリスクやアプリ制限のCAが効くかを必ずテスト。人のサインインだけ合格して機械が落ちる事故をよく見る。
6. おまけ:ポリシーIDの取得ワンライナー
$h = @{Authorization = "Bearer $token"}
$policies = Invoke-RestMethod -Headers $h -Uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"
$policies.value | Select-Object displayName,id,state | Sort-Object displayName
7. チェックリスト
□テストケースは人(ユーザー)と機械(SPN)の両方がある
□場所/IP、クライアント種別、プラットフォーム、デバイス準拠、
(ユーザー/サインイン/Workload)リスクを入力JSONに明示
□業務クリティカル(役員アプリ、SFA、会計)の通過シナリオが最低1本
□ブロック系(レガシー/社外/高リスク)の拒否シナリオが最低1本
□report-onlyは判定から除外(ログに残す)
□ID基準の期待値(名前はログ用)
□CIはPRで走る(main直コミットを許さない)
8. まとめ
CAの事故は「想定外の組合せ」で起きる。
サインイン文脈をJSONで“作文”し、Graphの What If をCIで回すだけで、人間の盲点をかなり潰せる。
最初は2~3シナリオから。運用で学んだ例外をテストに還元して、月曜7:30の冷や汗をなくしていこう。
参考リンク(一次情報・公式中心)
What If 評価API(v1.0):エンドポイント、権限、リクエスト/レスポンス、例。
https://learn.microsoft.com/en-us/graph/api/conditionalaccessroot-evaluate?view=graph-rest-1.0
whatIfAnalysisResult リソース:返り値の構造(適用可否・理由・要求コントロール等)。
https://learn.microsoft.com/en-us/graph/api/resources/whatifanalysisresult?view=graph-rest-1.0
ポータルの What If ツール(概念・使い方):UIでのシミュレーションの考え方。
https://learn.microsoft.com/en-us/entra/identity/conditional-access/what-if-tool
Az CLI のGraphトークン取得:az account get-access-token --resource-type ms-graph。
https://learn.microsoft.com/en-us/cli/azure/account?view=azure-cli-latest#az-account-get-access-token
PowerShellでのアクセストークン取得:Get-AzAccessToken の解説。
https://learn.microsoft.com/en-us/powershell/module/az.accounts/get-azaccesstoken?view=azps-14.4.0
ポリシー一覧(ID取得):GET /identity/conditionalAccess/policies。
https://learn.microsoft.com/en-us/graph/api/conditionalaccessroot-list-policies?view=graph-rest-1.0
GitHub ActionsのOIDC(Azure Login):アプリ側のFederated credential設定とワークフローの構成。
https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect
補足:GitHub Actions×OIDCの全体解説
https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure
補足:Conditional Accessの周辺一次情報
https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview