2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Graphで“回帰テスト化”するConditional AccessのWhat If —— /identity/conditionalAccess/evaluate をPowerShell+GitHub Actionsで回す

Last updated at Posted at 2025-10-25

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

CA WhatIf Regression
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の取得ワンライナー

$token = (az account get-access-token --resource-type ms-graph | ConvertFrom-Json).accessToken
$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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?