[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80716":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":11,"openIssues":12,"contributorsCount":12,"subscribersCount":12,"size":12,"stars1d":12,"stars7d":12,"stars30d":13,"stars90d":12,"forks30d":12,"starsTrendScore":12,"compositeScore":14,"rankGlobal":8,"rankLanguage":8,"license":15,"archived":16,"fork":16,"defaultBranch":17,"hasWiki":18,"hasPages":18,"topics":19,"createdAt":8,"pushedAt":8,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":12,"starSnapshotCount":12,"syncStatus":23,"lastSyncTime":24,"discoverSource":25},80716,"ConditionalAccessBaseline-Hardened","Teuftis\u002FConditionalAccessBaseline-Hardened","Teuftis",null,"Python",45,3,0,1,38.91,"MIT License",false,"main",true,[],"2026-06-12 04:01:29","# Mirage CA Baseline\n\nA Conditional Access baseline for Microsoft 365 plus a browser-based deployer. 41 policies covering users, admins, applications, service accounts, guests, and workload identities, with 11 supporting groups and 4 named locations.\n\nDeployer: https:\u002F\u002Fteuftis.github.io\u002FConditionalAccessBaseline-Hardened\u002F\n\nClick the badge, sign in as a CA admin, deploy. The deployer is a single-page app using PKCE - no client secret, no backend, no GitHub Actions, no Cloud Shell. Your delegated Graph token does the work and dies when the tab closes.\n\n[![Open deploy app](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FOpen-deploy%20app-0078D4?logo=microsoftazure&logoColor=white&style=for-the-badge&logoWidth=28)](https:\u002F\u002Fteuftis.github.io\u002FConditionalAccessBaseline-Hardened\u002F)\n\nQuick links: [deployer](https:\u002F\u002Fteuftis.github.io\u002FConditionalAccessBaseline-Hardened\u002F) · [policy catalog (filterable)](https:\u002F\u002Fteuftis.github.io\u002FConditionalAccessBaseline-Hardened\u002Finventory.html) · [POLICY_INVENTORY.md](.\u002FPOLICY_INVENTORY.md) · [SECURITY.md](.\u002FSECURITY.md)\n\n## What gets created\n\n41 Conditional Access policies, 11 groups, and 4 named locations. Stored as intent JSON under [`baseline\u002F`](.\u002Fbaseline\u002F), not as raw Graph exports - the deployer in [`docs\u002F`](.\u002Fdocs\u002F) resolves display names to object IDs at write time.\n\nThe policy catalog and group descriptions are at the bottom of this README. Full per-policy detail lives in [`baseline\u002Fpolicies\u002F`](.\u002Fbaseline\u002Fpolicies\u002F).\n\n### Why CA304 - Require Compliant Linux\n\nAdded to close the Linux User-Agent spoof gap: the CA platform condition is self-reported (from User-Agent), so without CA304, an attacker can present `User-Agent: Linux` and skip the Windows\u002FmacOS\u002Fmobile compliance gates. CA304 requires compliant Linux devices, completing the platform coverage. If you don't run managed Linux endpoints, drop `linux` from CA105's exclude list to block it outright instead.\n\n## What state policies deploy in\n\nMost land in **Report-only**. Eight specific policies deploy **disabled** because Report-only either doesn't apply to them cleanly or misrepresents what they'd do once enforced - those are `CA111`, `CA202`, `CA204`, `CA302`, `CA303`, `CA603`, `CA606`, `CAA01`. The list lives in `POLICY_IDS_DEPLOY_DISABLED_BY_DEFAULT` in [`docs\u002Ftranslate.js`](docs\u002Ftranslate.js) and `ALLOWED_DEPLOY_STATES` in [`docs\u002Fconfig.js`](docs\u002Fconfig.js) blocks the SPA from creating anything in the On state regardless of intent. You enable manually from the Entra admin center after reviewing telemetry.\n\nIf a policy with the same display name already exists in the tenant, deploy leaves it alone. There is no PATCH path. Same for groups and named locations: created if missing, never overwritten.\n\nYou can override the default state per-policy in a fork by adding `deploymentState` (or `deployState`) to a policy intent file. That only affects the first POST when no policy with that name exists yet - it doesn't unlock PATCH.\n\n## Deploying\n\nYou need one of: Conditional Access Administrator, Security Administrator, Groups Administrator, or Global Admin. The first three combined cover all the writes the deployer makes; Global covers everything if you'd rather not split roles.\n\nThe deploy [`docs\u002Fdeploy.js`](docs\u002Fdeploy.js) uses MSAL **in-memory** token cache (`cacheLocation: \"memory\"`): refreshing or closing the tab drops sign-in until you authenticate again.\n\n1. Open the deployer.\n2. Sign in. Accept the delegated Graph scopes.\n3. Run with **Dry run** enabled the first time. No writes happen, but you'll see exactly what would.\n4. Clear Dry run and run again to actually deploy.\n5. Read the activity log. Outcomes are `created`, `unchanged` (display name already matches), `skipped` (missing prerequisite - usually a third-party service principal that doesn't exist in your tenant yet), or `error`.\n\nAfter deploy, in the Entra admin center:\n\n- Populate group memberships (break-glass, exclusions, pilot groups, automation groups).\n- Fill in named location IP ranges and country lists. The placeholders are empty.\n- Review CA insights and sign-in logs against the Report-only policies.\n- Move policies to On in phases when telemetry says it's safe.\n\nThe Microsoft Sentinel queries at the bottom of this README are what I use to triage Report-only and hard-failure outcomes.\n\n## Auth model\n\nWhatever lands on `main` and gets published to GitHub Pages is what runs in the admin's browser. Delegated tokens are cached in-memory by MSAL for the lifetime of that tab session, not in `sessionStorage` or `localStorage`. Branch protection on `main` and code review on anything under [`docs\u002F`](.\u002Fdocs\u002F) or [`.github\u002Fworkflows\u002Fdeploy-pages.yml`](.github\u002Fworkflows\u002Fdeploy-pages.yml) are the controls that count. Treat them as such if you fork.\n\n## Forking it for your own MSP\n\nYou'll want your own multitenant SPA registration and your own GitHub Pages site so customers see your branding, not mine.\n\n1. Register a multitenant single-page application in your tenant. Add `https:\u002F\u002F\u003Cyour-org>.github.io\u002F\u003Crepo>\u002F` as a redirect URI. No secret. Delegated scopes: `Policy.ReadWrite.ConditionalAccess`, `Policy.Read.All`, `Group.ReadWrite.All`, `Directory.Read.All`, `Application.Read.All`, `User.Read`.\n2. Fork this repo. Edit [`docs\u002Fconfig.js`](docs\u002Fconfig.js): replace `clientId` and the redirect URI list. `GRAPH_SCOPES` is in the same file.\n3. Settings → Pages → GitHub Actions. The included [`deploy-pages.yml`](.github\u002Fworkflows\u002Fdeploy-pages.yml) builds and publishes on push to `main`.\n4. Put your own deploy badge in your README.\n\nTo change the actual policies, edit the `POLICIES` and `GROUPS` dicts in [`scripts\u002Fgenerate-baseline.py`](scripts\u002Fgenerate-baseline.py), then run `python scripts\u002Fgenerate-baseline.py`. The generator rewrites everything under `baseline\u002F`, mirrors it to `docs\u002Fbaseline\u002F` for same-origin fetch from the SPA, and regenerates `POLICY_INVENTORY.md` plus the appendix tables in this README. Commit all of those together.\n\nFor local hacking: `python -m http.server` from the repo root, open `\u002Fdocs\u002F`, and add a localhost redirect URI alongside your production one on the SPA registration.\n\n## GitHub Pages\n\nThe browser bundle lives under [`docs\u002F`](.\u002Fdocs\u002F) and rebuilds via [`deploy-pages.yml`](.github\u002Fworkflows\u002Fdeploy-pages.yml) on every push to `main`. Configure Pages once under Settings → Pages → GitHub Actions (older \"publish from branch `\u002Fdocs`\" works too).\n\nThe workflow runs `rm -rf docs\u002Fbaseline && cp -r baseline docs\u002Fbaseline` so `fetch(\".\u002Fbaseline\u002Fmanifest.json\")` stays same-origin with the SPA. `generate-baseline.py` does the same locally. If you fork an older copy that lacks this step, set `window.MIRAGE_BASELINE_URL` or fall back to raw.githubusercontent.com URLs (subject to CORS).\n\nIf the deployer loads blank or 404s on the manifest, the Pages workflow probably hasn't completed yet. Check Actions, then hard refresh.\n\n## Caveats\n\n- Not a Microsoft product. Nothing here is supported by them.\n- Risk-based policies need Entra ID P2.\n- Graph and CA schemas drift. If a POST returns 400, the schema probably moved - open an issue with the response body.\n- Policy **display names** look like `CA101 - Require MFA` (intent JSON may use a Unicode em dash or ASCII hyphen). The deployer normalizes those variants when matching, but policies you've already created with a different convention won't be touched.\n- Terms of Use objects are tenant-owned by Microsoft's design. The deployer can't create them.\n- Use a non-prod tenant for first runs if you have one, and confirm at least one break-glass account is excluded from everything before flipping any policy to On.\n\n## Issues and contributions\n\nIssues are the right place to start. A reproduction with the deployer's activity log helps most. PRs require collaboration access on this repo - open an issue first and we can sort it from there.\n\n## Repo layout\n\n```\nbaseline\u002F                  intent JSON: policies, groups, namedLocations\ndocs\u002F                      the deployer (GitHub Pages source)\nscripts\u002F                   generate-baseline.py - rebuilds baseline\u002F from dicts\nreference\u002F                 authoring spreadsheets, not read at runtime\n.github\u002Fworkflows\u002F         deploy-pages.yml publishes Pages on main\nPOLICY_INVENTORY.md        markdown mirror of the policy table below\nSECURITY.md                prefer GitHub Security → Report a vulnerability\nLICENSE                    MIT\n```\n\n---\n\n## Policy catalog\n\nAuto-generated from [`baseline\u002Fpolicies\u002F`](.\u002Fbaseline\u002Fpolicies\u002F) via `python scripts\u002Fgenerate-baseline.py`. The filterable version is at [inventory.html](https:\u002F\u002Fteuftis.github.io\u002FConditionalAccessBaseline-Hardened\u002Finventory.html).\n\n\u003C!-- policy-catalog:start -->\n| ID | Policy | Persona | Criticality |\n| --- | --- | --- | --- |\n| CA101 | Require MFA | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA102 | User Risk - Require MFA + Password Change | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA103 | Sign-In Risk - Require MFA | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA104 | Block Legacy Authentication | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA105 | Block Unknown Platforms | All users | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA106 | Block Outside Trusted Countries | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA107 | Session Controls | All users | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA108 | Block Cross-Device Auth Flows | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA109 | Require MFA for Azure Management | All users | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA110 | Block Malicious IPs | All users | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA111 | Continuous Access Evaluation - Standard | All users | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA112 | MFA on Device Register or Join | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA113 | Require Token Protection (Pilot) | All users | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA114 | Terms of Use | All users | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA201 | Intune Enrolling - Require MFA | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA202 | Require App Protection (Mobile) | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA204 | Require Compliant Mobile (Optional MDM track) | All users | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA301 | Require Compliant Windows | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA302 | Require Compliant macOS | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA303 | Limited Browser Access on Unmanaged Devices | All users | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA304 | Require Compliant Linux | All users | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA601 | Phishing-Resistant MFA for Admins | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA602 | Admin Session Controls | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA603 | Admin CAE - Strict | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA604 | Admin Block High User Risk | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA605 | Admin Block High Sign-In Risk | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA606 | Admin Require Compliant or Joined Device | Admins | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA701 | App - FortiClient - MFA | Application | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA702 | App - Salesforce - MFA | Application | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA801 | Service - Require MFA (Interactive) | Service | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA802 | Service - Block Outside Trusted IPs | Service | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA803 | Service - Block Legacy Auth | Service | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA804 | Service - Block Non-M365 Apps | Service | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA901 | Guest - Require MFA | Guest | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA902 | Guest - Block High Sign-In Risk | Guest | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA903 | Guest - Block Legacy Auth | Guest | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CA904 | Guest - Block Outside Trusted Countries | Guest | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA905 | Guest - Block Non-Collaboration Apps | Guest | ![Critical](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Critical&color=c62828&style=flat-square) |\n| CA906 | Guest - Terms of Use | Guest | ![Optional](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Optional&color=757575&style=flat-square) |\n| CA907 | Guest - Session Controls | Guest | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n| CAA01 | Agent - Block High Risk | Agent | ![Recommended](https:\u002F\u002Fimg.shields.io\u002Fstatic\u002Fv1?label=&message=Recommended&color=1565c0&style=flat-square) |\n\u003C!-- policy-catalog:end -->\n\n## Groups\n\nEach group below has a JSON file under [`baseline\u002Fgroups\u002F`](.\u002Fbaseline\u002Fgroups\u002F). Policies reference groups by display name; the deployer binds them to object IDs at write time. The `mailNickname` values are there to satisfy Graph's uniqueness constraint at creation.\n\n`tier` values: **Required** is foundation, **Service track** is the automation split that lets CA801 hit interactive service logons without breaking unattended ones, **Exception** is short-lived allowances, **Pilot** is narrow experiments.\n\n\u003C!-- group-catalog:start -->\n- **BG_BreakGlass** - Required - mailNickname `bg-breakglass`\n  Break-glass and other emergency administrator accounts that must remain reachable if Conditional Access misconfiguration locks out normal admins. Keep membership empty until accounts exist; remove members when not actively needed. Excluded from nearly all CA policies so use only for documented recovery procedures.\n- **CA_ExcludedFromCA** - Required - mailNickname `ca-excludedfromca`\n  Catch-all exclusion for identities that must never be evaluated by user-facing CA (for example certain directory sync or legacy integration principals your vendor documents as CA-exempt). Treat membership as highly privileged-every account here bypasses most workforce controls.\n- **CA_ServiceAccount** - Required - mailNickname `ca-serviceaccount`\n  Parent group for non-human and automation accounts. Policies that target all users exclude this group so background jobs are not forced through interactive MFA. Nest members into the interactive vs non-interactive child groups so CA801 can target only human-driven service logons.\n- **CA_ServiceAccount_Interactive** - Service track - mailNickname `ca-serviceaccount-interactive`\n  Service principals or managed identities that sometimes sign in through a browser or device-code style flow. CA801 requires MFA for this population while leaving pure client-credential automation in the non-interactive sibling group.\n- **CA_ServiceAccount_NonInteractive** - Service track - mailNickname `ca-serviceaccount-noninteractive`\n  Automation identities that only use client credentials, managed identity, or other non-interactive OAuth flows. Excluded from CA801 so scheduled jobs are not blocked; pair with CA802-CA804 for network and app restrictions.\n- **CA_TravelException** - Exception - mailNickname `ca-travelexception`\n  Short-lived membership for employees who must sign in from outside TRUSTED_COUNTRIES during approved travel. CA106 excludes this group from the country condition so the geofence still applies to everyone else; expire memberships when the trip ends.\n- **CA_DeviceCodeApproved** - Exception - mailNickname `ca-devicecodeapproved`\n  Rare allowance for CA108's block on device-code and authentication-transfer flows (for example controlled kiosk or DevOps scenarios). Add only fully trusted principals; every member is a phishing surface.\n- **CA_TokenProtection_Pilot** - Pilot - mailNickname `ca-tokenprotection-pilot`\n  Users or devices included in the CA113 Windows token-protection pilot. Start with a small population, collect sign-in and help-desk telemetry, then expand membership as your estate supports the feature.\n- **CA_ExcludedAgents** - Exception - mailNickname `ca-excludedagents`\n  Workload agent or service principal objects that must not be blocked by CAA01 when Identity Protection flags them high risk (for example monitored automation with known false positives). Keep the group tiny and review quarterly.\n- **CA_MSP_PartnerUsers** - Exception - mailNickname `ca-msp-partnerusers`\n  Delegated administrator or partner accounts that need access to Microsoft 365 admin experiences blocked for standard guests in CA905. Requires explicit lifecycle: remove access when the engagement ends.\n- **AUTOPILOT_DevicePrep** - Exception - mailNickname `autopilot-deviceprep`\n  Device objects undergoing Windows Autopilot pre-provisioning so they can complete join\u002Fenrollment without triggering CA112 MFA-on-join or CA201 enrollment MFA prematurely. Clean up stale device members after deployment finishes.\n\u003C!-- group-catalog:end -->\n\n## Microsoft Sentinel queries\n\nTwo KQL queries I use to triage outcomes from the Report-only and enforced policies. They assume `SigninLogs` and `AADNonInteractiveUserSignInLogs` (or equivalents) are in the workspace.\n\n### Report-only and hard failures, 5-day lookback\n\nTriages Report-only outcomes (`reportOnlyFailure`, `reportOnlyInterrupted`) alongside production hard fails (`failure`, `interrupted`). Includes legacy auth and device posture context.\n\n```kusto\nlet lookback = 5d;\nunion isfuzzy=true\n    (SigninLogs | extend SignInType = \"Interactive\"),\n    (AADNonInteractiveUserSignInLogs | extend SignInType = \"NonInteractive\")\n| where TimeGenerated > ago(lookback)\n| extend CAPolicies = coalesce(\n    todynamic(column_ifexists(\"ConditionalAccessPolicies_string\", \"\")),\n    column_ifexists(\"ConditionalAccessPolicies_dynamic\", dynamic(null)),\n    column_ifexists(\"ConditionalAccessPolicies\", dynamic(null)))\n| extend StatusObj = coalesce(\n    todynamic(column_ifexists(\"Status_string\", \"\")),\n    column_ifexists(\"Status_dynamic\", dynamic(null)),\n    column_ifexists(\"Status\", dynamic(null)))\n| extend DeviceObj = coalesce(\n    todynamic(column_ifexists(\"DeviceDetail_string\", \"\")),\n    column_ifexists(\"DeviceDetail_dynamic\", dynamic(null)),\n    column_ifexists(\"DeviceDetail\", dynamic(null)))\n| extend LocationObj = coalesce(\n    todynamic(column_ifexists(\"LocationDetails_string\", \"\")),\n    column_ifexists(\"LocationDetails_dynamic\", dynamic(null)),\n    column_ifexists(\"LocationDetails\", dynamic(null)))\n| where isnotempty(CAPolicies) and tostring(CAPolicies) != \"[]\"\n| mv-expand CAPolicies\n| extend\n    PolicyName    = tostring(CAPolicies.displayName),\n    PolicyResult  = tostring(CAPolicies.result),\n    GrantControls = tostring(CAPolicies.enforcedGrantControls)\n| where PolicyResult in (\"failure\", \"reportOnlyFailure\", \"interrupted\", \"reportOnlyInterrupted\")\n| extend\n    ErrorCode     = tostring(StatusObj.errorCode),\n    FailureReason = tostring(StatusObj.failureReason),\n    IsCompliant   = tobool(DeviceObj.isCompliant),\n    IsManaged     = tobool(DeviceObj.isManaged),\n    Country       = tostring(LocationObj.countryOrRegion)\n| summarize\n    HardFailures          = countif(PolicyResult == \"failure\"),\n    ReportOnlyFailures    = countif(PolicyResult == \"reportOnlyFailure\"),\n    Interrupted           = countif(PolicyResult == \"interrupted\"),\n    ReportOnlyInterrupted = countif(PolicyResult == \"reportOnlyInterrupted\"),\n    DistinctIPs           = dcount(IPAddress),\n    Apps                  = make_set(AppDisplayName, 10),\n    ClientApps            = make_set(ClientAppUsed, 10),\n    GrantControlsHit      = make_set(GrantControls, 10),\n    ErrorCodes            = make_set(ErrorCode, 10),\n    FailureReasons        = make_set(FailureReason, 5),\n    Countries             = make_set(Country, 5),\n    LegacyAuthSeen        = countif(ClientAppUsed in (\"Other clients\", \"IMAP\", \"POP\", \"SMTP\", \"Exchange ActiveSync\", \"Authenticated SMTP\", \"Exchange Web Services\")),\n    NonCompliantDevice    = countif(IsCompliant == false),\n    UnmanagedDevice       = countif(IsManaged == false),\n    LastSeen              = max(TimeGenerated)\n    by UserPrincipalName, PolicyName, SignInType\n| extend Severity = case(\n    HardFailures > 0 and LegacyAuthSeen > 0, \"High - hard fail + legacy auth\",\n    HardFailures > 0, \"Medium - hard fail\",\n    ReportOnlyFailures > 0, \"Tuning - report-only fail\",\n    \"Low - interrupt only\")\n| order by HardFailures desc, ReportOnlyFailures desc, Interrupted desc, UserPrincipalName asc\n```\n\n### Enforced (On) policies, hard failures only, 1-day lookback\n\nSame shape, narrower window, drops Report-only outcomes. Use this for ops triage of policies you've already moved to On.\n\n```kusto\nlet lookback = 1d;\nunion isfuzzy=true\n    (SigninLogs | extend SignInType = \"Interactive\"),\n    (AADNonInteractiveUserSignInLogs | extend SignInType = \"NonInteractive\")\n| where TimeGenerated > ago(lookback)\n| extend CAPolicies = coalesce(\n    todynamic(column_ifexists(\"ConditionalAccessPolicies_string\", \"\")),\n    column_ifexists(\"ConditionalAccessPolicies_dynamic\", dynamic(null)),\n    column_ifexists(\"ConditionalAccessPolicies\", dynamic(null)))\n| extend StatusObj = coalesce(\n    todynamic(column_ifexists(\"Status_string\", \"\")),\n    column_ifexists(\"Status_dynamic\", dynamic(null)),\n    column_ifexists(\"Status\", dynamic(null)))\n| extend DeviceObj = coalesce(\n    todynamic(column_ifexists(\"DeviceDetail_string\", \"\")),\n    column_ifexists(\"DeviceDetail_dynamic\", dynamic(null)),\n    column_ifexists(\"DeviceDetail\", dynamic(null)))\n| extend LocationObj = coalesce(\n    todynamic(column_ifexists(\"LocationDetails_string\", \"\")),\n    column_ifexists(\"LocationDetails_dynamic\", dynamic(null)),\n    column_ifexists(\"LocationDetails\", dynamic(null)))\n| where isnotempty(CAPolicies) and tostring(CAPolicies) != \"[]\"\n| mv-expand CAPolicies\n| extend\n    PolicyName    = tostring(CAPolicies.displayName),\n    PolicyResult  = tostring(CAPolicies.result),\n    GrantControls = tostring(CAPolicies.enforcedGrantControls)\n| where PolicyResult == \"failure\"\n| extend\n    ErrorCode     = tostring(StatusObj.errorCode),\n    FailureReason = tostring(StatusObj.failureReason),\n    IsCompliant   = tobool(DeviceObj.isCompliant),\n    IsManaged     = tobool(DeviceObj.isManaged),\n    DeviceOS      = tostring(DeviceObj.operatingSystem),\n    Country       = tostring(LocationObj.countryOrRegion),\n    City          = tostring(LocationObj.city)\n| summarize\n    Failures         = count(),\n    DistinctIPs      = dcount(IPAddress),\n    DistinctApps     = dcount(AppDisplayName),\n    Apps             = make_set(AppDisplayName, 10),\n    ClientApps       = make_set(ClientAppUsed, 10),\n    GrantControlsHit = make_set(GrantControls, 10),\n    ErrorCodes       = make_set(ErrorCode, 10),\n    FailureReasons   = make_set(FailureReason, 5),\n    Countries        = make_set(Country, 5),\n    Cities           = make_set(City, 5),\n    DeviceOSes       = make_set(DeviceOS, 5),\n    LegacyAuthHits   = countif(ClientAppUsed in (\"Other clients\", \"IMAP\", \"POP\", \"SMTP\", \"Exchange ActiveSync\", \"Authenticated SMTP\", \"Exchange Web Services\")),\n    NonCompliantHits = countif(IsCompliant == false),\n    UnmanagedHits    = countif(IsManaged == false),\n    FirstSeen        = min(TimeGenerated),\n    LastSeen         = max(TimeGenerated)\n    by PolicyName, UserPrincipalName, SignInType\n| extend Triage = case(\n    LegacyAuthHits > 0, \"Legacy auth - check for stale app config or attack\",\n    NonCompliantHits == Failures, \"Device compliance - Intune posture issue\",\n    UnmanagedHits == Failures, \"Unmanaged device - enrollment gap\",\n    GrantControlsHit has \"block\", \"Block policy fired - verify intent\",\n    GrantControlsHit has \"mfa\", \"MFA failure - user couldn't complete challenge\",\n    \"Investigate\")\n| order by Failures desc, LastSeen desc\n```\n\n## Further reading\n\n- [Conditional Access overview](https:\u002F\u002Flearn.microsoft.com\u002Fentra\u002Fidentity\u002Fconditional-access\u002Foverview)\n- [Policies and assignments](https:\u002F\u002Flearn.microsoft.com\u002Fentra\u002Fidentity\u002Fconditional-access\u002Fconcept-conditional-access-policies)\n- [Named locations](https:\u002F\u002Flearn.microsoft.com\u002Fentra\u002Fidentity\u002Fconditional-access\u002Fconcept-assignment-network)\n- Microsoft's Zero Trust \u002F CA reference materials at [microsoft\u002FConditionalAccessforZeroTrustResources](https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FConditionalAccessforZeroTrustResources).\n\n## License\n\nMIT. See [LICENSE](.\u002FLICENSE).\n\n## Security\n\nSee [SECURITY.md](.\u002FSECURITY.md). For vulnerability reports, prefer GitHub Security → Report a vulnerability over public issues.\n\nThe `reference\u002F` spreadsheets are authoring companions, not read by the deployer. If you edit them, keep group display names and `mailNickname` strings aligned with [`baseline\u002Fgroups\u002F`](.\u002Fbaseline\u002Fgroups\u002F) (`CA_` prefix on display names, `ca-` on nicknames), then regenerate via `scripts\u002Fgenerate-baseline.py`.\n","Mirage CA Baseline 是一个用于 Microsoft 365 的条件访问基线项目，包含41个策略、11个支持组和4个命名位置。该项目使用Python编写，提供了一个基于浏览器的部署工具，采用PKCE认证机制，无需客户端密钥或后端服务，通过用户的Graph令牌完成操作，确保了安全性和便捷性。此项目适用于需要加强Microsoft 365环境安全性的企业或组织，特别是那些希望简化条件访问策略配置过程的场景。通过该项目，管理员可以快速部署一套全面的安全策略，覆盖用户、管理员、应用程序等多个方面，从而有效提升整体安全性。",2,"2026-06-11 04:01:46","CREATED_QUERY"]