ENCA

What do you want to do?

⏳ Temporary tools

Deadline-driven tools for a dated migration or retirement. Each one names the date it stops mattering — and leaves once that date has passed.

📵

SMS & voice retirement BETA UPDATED

Microsoft retires its SMS and voice MFA delivery on 1 February 2027 — and from 1 September 2026 everyone still enabled for them is auto-enabled for passkeys and nudged at sign-in. Who is still in the SMS/Voice policy scope, who actually has a phone method registered, and who would be locked out — with Markdown and CSV export. Also reads and, if you want, pauses Microsoft's September rollout — the Graph-only opt-out with no control in the portal — and now says who changed that setting and when, from the directory audit log. writes to tenant

🧷

memberOf retirement BETA

The memberOf dynamic rule operator leaves preview on 3 November 2026. Rules still using it don't fail — they stop updating and keep handing out whatever membership they held that day. Scans all three surfaces Microsoft names — dynamic groups, dynamic administrative units and entitlement-management auto-assignment policies — then says what it costs: which Conditional Access policies point at each group, whether it's an include that quietly stops covering joiners or an exclude that becomes a permanent bypass, which groups carry licensing, and which access packages stop being handed out. Owner notification, Markdown and CSV export. reads only

🗂 Explore & document

Read your baseline and turn it into something shareable.

🗣

User impact brief UPDATED

The rollout email, written by the tenant itself: what people will notice and what will deliberately no longer be possible, derived from the Conditional Access policies actually deployed — with what is live today kept apart from what changes at go-live. Filter by audience, export as Markdown or a Word document for the communications team.

📉

Drift watch

Take a snapshot of the Conditional Access configuration today, compare a later run against it, and see exactly what moved — policies added, removed, switched Off, exclusions widened, locations retrusted. The history is a file you keep, so it has no 30-day limit, and no infrastructure is needed. Ranked by severity, because a widened exclusion matters more than a rename.

🗂

List Policies UPDATED

Browse all Conditional Access policies as cards, a list or a settings matrix — grouped by persona.

📄

Create documentation UPDATED

Select policies (or take all) and generate documentation — Word, PDF, PNG or a PNG bundle, with optional auditor-ready pages for the restricted units protecting exclusion groups. On a baseline tenant it documents the baseline catalog only.

🗄

Backup (JSON)

Select policies (or take all) and download their raw JSON definitions in a zip. On a baseline tenant it backs up the baseline catalog only.

🔍 Analyse & simulate

Work out what your policies actually do, and to whom.

🔍

Gap analyse UPDATED

How many of your users Conditional Access actually reaches — a coverage flow from everyone in scope down to targeted, enforced, MFA and licensed — then the users × policies matrix: who bypasses what and why.

CA validator

Per policy, the sign-in simulations it implies — user, app, client, location, platform, risk — and the control each one should (or should not) enforce. Ported from Jasper Baes' CA Validator.

🧪

What-If

Simulate a sign-in — user, app, platform, client, location, device state and risk — and see which policies would apply, with the controls required, and which would not and why.

Compare users

Two or more users side by side: which policies include, exclude or ignore each of them, the group and role memberships behind the differences, and optionally one What-If sign-in run for every user.

🕵

Who is Anna to CA BETA

One user, the whole picture: which deployment group she is in and how she got there (direct, or nested via which parent), every policy that reaches her — or misses her, and why — the sign-ins Conditional Access actually stopped, and what happens to her the day report-only goes live. The answer to “why can't Anna sign in” and “is Anna in the wave yet” on one screen.

🌊

Who is the wave to CA BETA

The same picture for a whole deployment group: who is in the wave and how they got in, which policies target it and how many members an exclusion takes back out, what the sign-in log did to its members, and — per report-only policy — whether it can go live for this wave without locking somebody out. The members that need a look, each one a click into 🕵 Who is Anna to CA.

🔗

User or Group analyzer UPDATED

Where is a group actually used? One group or user checked against Conditional Access, Entra roles, apps and licensing, Intune, Microsoft 365 and Azure RBAC — or sweep every group and find the ones nothing references. extra permissions

🚪

Exclusion analyzer

Every exclusion across all policies — users, groups (expanded to members), roles, guest types, apps and locations — in an exclusion × policy matrix.

🖥

Device reality check UPDATED

A grant demanding a compliant device is only worth what Intune's compliance policies are worth — and nothing checks the other half. Per CA policy and per platform: is the scope actually assigned a compliance policy, or does it fall through to the tenant default? Same check for app protection behind “require approved client app”. extra permissions

🎫

Licence gap UPDATED

Microsoft's licence usage blade counts who triggered a policy last month — the obligation is on who your policies target. Every targeted user needs Entra ID P1, risk-based policies need P2. This counts the real number, compares it with the seats you own, and lays out the ways to close a gap.

📞

Teams devices BETA

Is the shared-device exclusion group (CAB-SEC-U-TeamsSharedDevices) catching every Teams Room, Shared Space phone and Phone resource account — and nobody else? Builds the membership rule from the device licences this tenant actually has, previews who it matches, flags Teams groups whose rule catches people, and replaces the rule after you confirm. writes to tenant

🕓

Change audit

Who changed which Conditional Access resource, when, and exactly what changed — a field-level diff of every policy, named location and authentication strength edit from the Entra audit log.

🚦

Sign-in failures

Which sign-ins Conditional Access failed or interrupted and which policy did it — per policy: who, on which app, from where, with the controls that weren't met. Enforced and report-only, with CSV export and a one-click replay in What-If.

🎚

Report-only impact UPDATED

What happens the day a report-only policy goes live — per policy: who would be denied, who just gets an extra prompt, who passes unchanged; per user: the combined effect of everything in report-only. The evidence for the go-live decision, from the sign-in log.

🧬 Compare against a baseline

Measure this tenant against a reference set of policies.

🛡

Best-practice & bypass checks

Check the baseline against known CA bypasses and the Swiss-cheese model — MFA coverage, FOCI token sharing, break-glass coverage, known bypass apps, persona × control matrix.

🧬

Baseline Policies UPDATED

Match this tenant against the CloudFellows R26.6 (v3.x) baseline: which baseline policies are present, outdated or missing — then import the gap. The ★ active baseline for this tenant — what every group and vault tool works against — is chosen here, after a dry run shows what the switch would change.

🧩

Baseline (Joey Verlinden) UPDATED

Match this tenant against the community Conditional Access Baseline by Joey Verlinden — read live from his repository (latest release, with the commit it read), the bundled 2026.6.1 snapshot as the fallback. Make it the ★ active baseline and the group checks, group creation, persona vaults and exclusion restore all work against it.

📘

MS Learn checks

Check policies against documented Microsoft Learn exclusions & limitations — break-glass, token protection, Teams Rooms, retirements.

✍️ Manage the tenant

The tools that write. Each one confirms before it changes anything.

👥

Conditional Access groups UPDATED

Check the active baseline's groups against this tenant — CloudFellows or Joey Verlinden, your choice — create the missing ones, analyze their members in a matrix, and assign them to policies. writes to tenant

🔒

Protect exclusions UPDATED

Place the CA exclusion groups in a restricted management administrative unit, so only AU-scoped roles can change their membership — closing the "an admin adds themselves to an exclusion group" bypass. writes to tenant

🌐

Named locations

View, create, edit and delete Conditional Access named locations — IP ranges and countries — see which policies use each one, and what is wrong with them: dangling policy references, empty country locations, overly broad ranges. writes to tenant

🎫

Authentication contexts

The step-up requirements apps and Protected Actions can ask for (c1–c99): create, rename, publish or unpublish each context and see which Conditional Access policies enforce it. writes to tenant

💪

Authentication strengths

The method combinations a policy can require: the three built-in strengths read-only, custom strengths created, re-combined, renamed and deleted — with the policies granting each one. writes to tenant

📜

Terms of use BETA

The terms-of-use agreements Conditional Access can require: every agreement with its behaviour settings, PDFs and the policies that require it — create with a PDF upload, edit behaviour, check acceptances, delete when unreferenced. writes to tenant

Recycle bin

Recently deleted Conditional Access policies and named locations — everything still inside the 30-day restore window, with what it did, when it went, and one-click restore. A policy that was On comes back enforcing. writes to tenant

🛡

Restricted AUs UPDATED

Manage restricted management administrative units — the vaults that shield CA exclusion groups from tenant-wide admins. One vault per persona of the active baseline (CloudFellows or Joey Verlinden). List them with members and scoped role grants, edit, add or remove members, grant scoped administrators, create and delete. The restricted flag itself is immutable, and the tool says so.

🎚

Set Policy state

Switch selected policies between On, Report-only and Off. writes to tenant

📥

Import UPDATED

Restore a backup (zip or folder): dependencies first, policies imported Off, includes remapped to deploy persona groups. writes to tenant

❓ Help

📋

What's new

Everything added or changed in ENCA, newest first — the full changelog behind the overlay you get after signing in.

🗺

Roadmap UPDATED

Where ENCA is heading next — the ideas being worked toward, in the open, so you know what to expect and can say what matters most.

Help

How every tool works and what to expect from each option — opens in its own tab, jump back to any tool anytime.

📖 About these tools

ENCA (Entra Conditional Access) is a read-only toolset for your Microsoft Entra Conditional Access baseline. List Policies shows every policy as cards, a list or a settings matrix, grouped by persona (CA number ranges). Create documentation turns all or selected policies into shareable documentation — Word, PDF, PNG or a PNG bundle. Gap analyse builds a users × policies impact matrix: which policies apply to whom, who bypasses a policy via exclusions (and why), whether a bypass is covered elsewhere, and who gets no MFA from any policy. Best-practice & bypass checks checks the baseline against known CA bypasses and the Swiss-cheese defense model — MFA coverage, FOCI token sharing, resource-exclusion scope leaks, break-glass coverage, known bypass apps, and a persona × control coverage matrix. Baseline Policies matches this tenant against the CloudFellows R26.6 (v3.x) baseline on the CA number and compares versions, so you see at a glance which baseline policies are present, outdated or missing — with a Markdown gap report and a hand-off to Import. MS Learn checks compares your policies against exclusions, limitations and upcoming behavior changes documented on learn.microsoft.com — missing break-glass exclusions, token protection limits, Teams Rooms / Surface Hub impact, required app exclusions and control retirements — and can build the corrected policy for you (new version, state Off, downloadable; never written to your tenant). Backup downloads the raw JSON definitions of your policies in one zip. Conditional Access groups is the one place the groups live: it checks the tenant against the expected baseline set, creates the missing ones, reads their members into a members × groups matrix, and assigns them to policies (replace, add, or set to All Users) — always behind an explicit review step.

Permissions

The app uses delegated, read-only Microsoft Graph permissions for its base tools, consented once per tenant by an administrator — see the permission overview above for the exact scopes and their live status. The read-only tools never write to your tenant, and the signed-in user only needs a reader role such as Security Reader or Global Reader. The Conditional Access groups tool additionally requests Policy.ReadWrite.ConditionalAccess on demand (incremental consent) and requires a role that can edit Conditional Access, such as Conditional Access Administrator or Security Administrator. Creating missing groups (assigned groups always role-assignable; dynamic groups keep their membership rule) additionally requests Group.ReadWrite.All and RoleManagement.ReadWrite.Directory and requires the Privileged Role Administrator role.

Credits & thanks

These community projects shaped the tools, and all deserve the credit:

  • idPowerToys — by Merill Fernando. The Conditional Access documenter that started all of this: the idea that a CA baseline should be readable as a document rather than clicked through in the portal. These tools are the web successor to that documenter, extended to the newest CA settings.
  • Conditional Access Impact Matrix — by Jasper Baes. The users × policies impact model behind the Gap analyse tool: working out which policies actually apply to a given user, who bypasses a policy through an exclusion, and whether that bypass is covered somewhere else.
  • Conditional Access Validator — by Jasper Baes, part of his Conditional Access Blueprint. The simulation generator behind the CA validator tool: deriving, per policy, the sign-in scenarios it implies and the control each one should (or should not) enforce. Reimplemented here as a report only (no Maester test-code generation). Licensed CC BY-NC-SA 4.0.
  • Conditional Access Baseline — by Joey Verlinden. A minimised community baseline on top of the Microsoft Conditional Access framework, included here as a second catalog in Baseline Policies so a tenant can be measured against it the same way. The policy set and naming convention are his; these tools only read and compare.
  • CA Policy Analyzer — by jhope188. The inspiration for the Best-practice & bypass checks set: best-practice and known-bypass checks laid out against the Swiss-cheese layered-defense model.
  • Audit Conditional Access Exclusions with PowerShell — by Tiago S. Carvalho. The governance flag patterns behind the Risk review in the Exclusion analyzer: privileged roles excluded, all-guest exclusions, direct user exclusions, oversized exclusion lists, stale disabled accounts and report-only exclusions.
  • Microsoft Entra Conditional Access blog series — by Sebastian F. Markdanner (Chance of Security). A deep, practical walk-through of designing a persona-based Conditional Access framework; several of his policy designs also informed the CloudFellows baseline bundled here.

All are independently reimplemented here in browser-side JavaScript against Microsoft Graph — no code was copied. Thanks also to the Conditional Access community whose baselines this tool is designed to compare against: Kenneth van Surksum, Joey Verlinden, Sebastian F. Markdanner and Claus Jespersen (Zero Trust persona framework).

Security

Everything runs in your browser — there is no backend and your policy data is never stored or sent anywhere except Microsoft Graph. Sign-in uses Microsoft Entra (MSAL, authorization code flow with PKCE, no client secret); tokens live in session storage and are gone when the tab closes. Access tokens are only ever attached to graph.microsoft.com (hostname-validated), a strict Content-Security-Policy is enforced, and all JavaScript libraries are self-hosted — nothing loads from third-party CDNs at runtime. Exports are generated locally on your machine and carry your tenant's branding, not this tool's.

0 policies selected
Matrix

🔒 Protect exclusions NEW writes to tenant

Membership of a CA exclusion group is a Conditional Access bypass — and any tenant-level Groups/User Administrator can hand it out. This tool places those groups in a restricted management administrative unit, after which only roles scoped to that administrative unit can change their members.

The same workflow also lives in Conditional Access groups → ⑥ Protect. Needs the Privileged Role Administrator role; consents AdministrativeUnit.ReadWrite.All on demand.

Only groups used by Conditional Access takes the include and exclude groups off every policy already loaded — enabled, report-only and Off alike. No /groups enumeration at all, so it is the fastest scope and the one that matches what this tool is for. An id a policy names but the directory no longer has is kept and flagged as a dangling reference: that assignment targets nobody.

The name filter narrows which groups are scanned, so a prefix like CAB-SEC- keeps a sweep of a 20 000-group tenant to the set you actually care about. It runs server-side where Graph supports it and falls back to filtering locally if not. “Groups in scope” then counts matches, not raw groups.

A sweep reads each service once and matches every group against it, so the cost barely grows with the number of groups. The exceptions are the per-group lookups — nesting, administrative units, app assignments, licensing and Teams — which are asked object by object, batched 20 at a time. Untick them for a fast first pass on a large tenant.

Where to look

📋 What's new

Everything added or changed in ENCA, newest first. The overlay after sign-in shows you only what landed since your last visit — this is the full record.

🗺 Roadmap

Where ENCA is heading, laid out in time. Order and scope may change — real-tenant feedback decides what lands first. Suggestions are welcome as GitHub issues.
Each item carries a reference (Rnn) so it can be pointed at without quoting the title. A reference belongs to that item permanently: it is never reused once the item ships, and the numbers are not a priority order. An item that ships moves from Next to Now with its reference intact — it is not deleted, because a roadmap that forgets what it delivered is only a wish list.

Now shipped & shipping

R40 🕵 Who is Anna to CA live · build 309

One user, the whole Conditional Access picture on one screen — the answer to why can't Anna sign in and is Anna in the wave yet, which used to take four tools. Her deployment stage (which of the baseline's CAD-SEC-U-DG-* groups, and how: direct or nested via which parent), every policy that reaches her and via what — or excludes her and by what — the sign-ins Conditional Access actually stopped for her, and what happens to her the day report-only goes live. A standing bypass through an exclusion group is named as such. Same include/exclude resolution as ⚖ Compare users, so the two cannot disagree.

R41 🌊 Who is the wave to CA live · build 309

The same picture for a whole deployment group. The question before a report-only policy is switched On is can this wave take it — and the answer existed per policy (🎚) or per user (🕵), never per wave. Pick a deploy or persona group and get, per report-only policy that targets it, a go-live readiness verdict: locked out (named), prompted, unchanged, silent — with silent counted as no data, never as safety. Plus who is in the wave and how they got in, the policies that target it, and the members that need a look, each a click into 🕵.

R42 👥 Remove a member from a group, where you see it live · build 309

③ Members could add but not remove. Now a − Remove button next to + Add, and an × on every ● in the matrix — both ask first, and the question says what the group is used for: taking someone out of an exclusion group puts them back inside the policy. Dynamic groups are not offered; a user not read as a member is refused rather than deleted blind.

R15 🍴 Fork detection and update-from-upstream live · build 298

A high-assurance tenant is encouraged to fork, review and serve its own pinned copy — and the moment it does, it stops hearing about fixes. Now shipped: the app notices it is running as a fork (its origin is not the canonical host and the build it carries is older than upstream), says so plainly in a strip under the header, and shows what has changed since the pinned build — the What's-new headline of every build between here and there — with the commands to update a Docker instance and to pull main in and re-review the diff. The point is not auto-updating, which would defeat the reason for forking; it is making “you are 14 builds behind, and here is what those builds changed” impossible to miss. If upstream cannot be read — offline, air-gapped, a hardened CSP — it says nothing rather than something wrong. Pairs with 📋 What's new and the security document's fork-review-pin guidance.

R06 🐳 Self-hosting with Docker live · build 305

ENCA is static files and a browser — no server, no database, nothing stored anywhere. That makes it trivially self-hostable, and some tenants will require it: a security tool that reads Conditional Access is exactly the kind of thing an organisation wants served from its own infrastructure rather than from someone else's GitHub Pages, whatever the code does.

And with a host of your own, somewhere to keep things. Today nothing survives the tab closing, by design — there is no server to keep it on, so a Drift snapshot is a file you download and a report is a file you save. A self-hosted instance changes what is possible: an optional SQLite store alongside the container could hold drift snapshots, generated reports and per-tenant preferences, so a baseline signed off in March can be compared against in November without anyone having had to keep track of a JSON file. Strictly opt-in and self-hosted only — the hosted site keeps storing nothing, because "nothing is kept anywhere" is a promise worth more than the convenience. Tokens would stay out of it regardless: they belong in the browser session and nowhere else.

First cut shipped: a published image — ghcr.io/nurejev/enca, nginx over these files, built by CI from main (:latest) and the beta channel (:beta) — plus a docker compose example, one-command install scripts for Mac and Windows that check Docker, pull, run and open the browser, and a Deploy to Azure button that stands up an Azure Container App with scale-to-zero, so an idle instance costs close to nothing. SELF-HOSTING.md in the repo ties it together and leads with the one step nothing can automate: the redirect URI — every host you serve from must be registered as a SPA redirect URI on the app registration, and it is the step that produces the confusing sign-in error when missed. It pairs with the single-tenant app registration already available: your own registration, your own host, no dependency on anything of ours at run time. A self-hosted instance also gets its own branding without forking — Branding settings, in the account menu — and its own roadmap: the Self-hosted section below, where the SQLite idea above now lives as S01.

R38 🔁 Restore the exclusion group a policy has lost live · build 293

The baseline gives most numbered policies their own exclusion group, and that reference is the first thing to go missing: a policy gets rebuilt, an older version is imported, somebody tidies an exclusion list, and the group is left in the directory with nothing pointing at it. Nothing notices — a policy that lost its exclusion group looks exactly like one that never had it, until the day the exception it existed for is needed and the bypass is not there.

👥 Assign groups or roles gains an eighth action, and the only one that aims a different group at every policy: CA200 gets CA200-Exclusion, CA201 gets CA201-Exclusion, read from the CA number in the name and confirmed against the baseline catalog. Step 2 is a drift report — how many are already right, how many lost the reference, how many name a group that no longer exists — with only the repairs pre-ticked and missing groups creatable from their templates in the same run. Point it at a selection to fix one, or at every policy in the tenant for the sweep. Additive only, and the tenant-wide run asks for the typed ALL, because an exclusion widens a policy rather than narrowing it.

R27 📄 Documentation for the restricted units live · build 283

📄 Create documentation can now add the restricted administrative units to the same document as the Conditional Access policies. Each unit gets an auditor-ready page naming its persona, the groups it protects, every policy that includes or excludes those groups, the people scoped to it and whether their role can change group membership.

The review starts with the questions that decide whether the control is real: who could widen an exclusion, what would happen if they did, whether a unit has nobody scoped to it, and whether it holds a frozen role-assignable group. The integration is optional and fail-soft: Create documentation reads the units for itself; an absent, denied or broken restricted-unit read is reported and the policy document still exports.

R14 🏢 Your own single-tenant app registration live · build 273

Every tenant signs in through one multi-tenant app registration owned by Limon-IT by default — the same application ID, and therefore an application outside your directory holding a delegated grant on your data. You can run ENCA on your own instead: a single-tenant SPA (AzureADMyOrg) registered inside your tenant, with your own client ID, consent record, redirect URIs and audit trail — nothing to trust but the code you reviewed. The sign-in mechanism stays authorization code + PKCE with no client secret; only the owner changes.

Shipped: ./New-EncaAppRegistration.ps1 -SingleTenant creates the registration and prints what to paste; js/authConfig.local.js points your copy at it without editing a file upstream also changes. -RequireAssignment can restrict the enterprise application after assigning you first, so the script cannot seal you out.
📖 Read the setup manual — prerequisites, the five steps, verification, and keeping a pinned copy current. Also in Security & risk.

R11 🖥 Compliant-device reality check live · build 282

A grant control demanding a compliant device is only worth what Intune's compliance policies are worth — and until now nothing checked the other half. The tool reads the compliance policies with their assignments and flags, per CA policy and per platform, every scope that no compliance policy actually covers — with the tenant default for policy-less devices read first, because that single Intune toggle decides whether a gap means silently unprotected (marked Compliant) or users blocked (marked Not compliant). The same check runs for app-protection policies behind “require approved client app”, which is flagged as unsatisfiable outside iOS and Android. OR-alternatives are named, coverage is proven first from assignments and then from transitive group-membership overlap where names alone cannot prove it, incomplete reads stay honestly not proven, and the verdicts export as Markdown.

R29 🔍 Gap analyse — scan only who you asked about live · build 280

Gap analyse builds a users × policies matrix for every user it can read. On a tenant of any size that is a long read producing a wide answer, when the question was usually narrow: is this contractor covered, what does the finance team bypass, did that one exclusion group leave anybody exposed. The matrix is the right output; the scope is not.

It names users or groups before the scan and judge only those — the same policies, the same verdicts, fewer rows and a fraction of the reads. Two things it has to get right: a group means its members, including nested ones, or the answer is about a group rather than about people; and the report must say what was scoped as prominently as what it found, because a clean matrix over eleven users looks exactly like a clean matrix over the tenant, and only one of those is reassuring.

R30 🧪 What-If — filter the policies that did not apply live · build 279

A run against a real tenant answers with two policies that apply and 107 that do not, each with its reason. That list is the honest answer and it is also unreadable: the question behind it is almost never “show me everything”, it is why did none of the Admins policies apply to this admin — a persona question — or what happened to CA103, a number question.

It filters that list by persona and by CA number, the same grouping 🗂 List Policies already uses, so the ranges stay one idea across the toolset rather than two. Worth adding at the same time: a count per reason — “94 out of scope, 8 wrong platform, 5 excluded” — because when a policy you expected is missing, the reason is what you are hunting for, and filtering to it beats scrolling for it. The reasons must stay per policy either way; a summary that replaced them would answer a different question.

R31 🧪 What-If — search the tenant's apps, not their GUIDs live · build 279

Target resource offers the well-known apps by name and then Other — enter an App ID, which asks for a raw GUID. Nobody knows their apps by GUID, so answering it means leaving the tool, finding the enterprise application in the portal, copying the id back — and a mistyped GUID does not fail, it silently describes a sign-in to an app nobody has, which is the same class of mistake as a wrong country code.

The plan is a type-ahead over the tenant's own service principals, matching on display name and filling the id in, the way 🌐 Named locations will do for countries. A pasted GUID must keep working: a policy can reference an application that has no service principal here — 📥 Import already has to deal with exactly that — so an id ENCA cannot name is still a legitimate answer, and should be marked not found in this tenant rather than rejected.

R08 🌐 Type the country, get the code live · build 269

A country named location takes two-letter ISO 3166-1 codes, typed by hand into a free-text box. The failure mode is silent and total: a wrong code is still a valid code, so the policy saves, looks right in the portal, and quietly covers the wrong country until somebody is blocked or — worse — is not. Ireland is IE; IR is Iran. Austria is AT, Australia AU. Sweden is SE, Switzerland CH.

It types the name and fills the code in — a type-ahead over the full ISO list, matching on country name, adding the code as a chip you can see and remove. Codes typed directly keep working, but anything that is not a real ISO code is rejected at entry rather than accepted and discovered later. Worth doing for the same reason the CA-number routing exists: the tool knows the right answer, so the human should not have to be the one who remembers it.

🎚 Report-only impact (the go-live forecast) · 💪 Authentication strength advanced options (AAGUIDs, certificate restrictions) · 🛡 Restricted AUs, 📉 Drift watch and the ⌨️ command palette (all out of BETA) · 🔒 the security & risk documentation, readable before sign-in · one progress visual for every long read. The full record lives in 📋 What's new.

R02 📉 Drift watch live · build 267

Does the tenant still look like it did when we signed it off? Snapshot the Conditional Access configuration to a file you keep, and compare a later run against it — added, removed, changed, ranked so a widened exclusion or a policy switched Off outranks a rename. No server and no 30-day limit, because the history is the file rather than Microsoft's audit log. Now watches the restricted administrative units too — a group leaving one widens the bypass surface as much as editing the policy would. Next for it: exclusion-group membership drift (an exclusion group quietly gaining members is the same risk as widening the exclusion itself) and comparing two snapshots to each other without re-reading the tenant.

R03 ⌨️ Command palette live · build 267

Past twenty-odd tools, a tile grid stops being the fastest way in. Ctrl/Cmd + K anywhere opens a search box: type a few letters and land in the tool, or type a CA number or policy name and land on the policy card. Matching takes initials as well as substrings, so gap finds Gap analyse and bbc finds Best-practice & bypass checks.

R09 🔝 Flagged tiles first in a collapsed section live · build 268 · 281

A collapsed home section shows four tiles, and anything flagged NEW, BETA or UPDATED claims those slots ahead of the rest — but it claims them in page order, so a flagged tile that sits ninth is visible while still appearing after unflagged ones, and with five flagged tiles in a section one of them drops below the fold. Flagged tiles now lift to the top of the collapsed view, newest first — the ranking is read from the changelog, the build number of the newest entry naming that tool, so a tool changed in this build outranks a BETA tag that has been sitting there for weeks. Before that the tie was broken by page order, which buried the most recent change under the oldest badge. Expanding restores the normal order, because the grid's grouping is meaningful and shuffling it permanently would cost more than it gains.

R10 🎛 Group actions where you are already looking live · build 269

Clicking a group in 👥 CA groups ① Check opens a card that reads: object id, type, the policies that include or exclude it, its members. Everything you might then want to do lives on a different numbered tab, so you close the card, find the right step, and hunt for the group again. List Policies already solves this — every policy card carries its own Documentation, Backup, Assign and Policy-state buttons.
Selecting a group now shows the same treatment for a group: create it if it is missing (②), read or add members (③), assign it to policies (④), import members from CSV (⑤), protect it in a restricted AU (⑥), migrate it off role-assignable (⑦), disable nesting, or open it in 🔗 User or Group analyzer to see everything outside Conditional Access that points at it — each offered only when it actually applies, so a role-assignable group is not offered ⑥ Protect and a group already in a restricted AU is not offered it twice.

R07 👤 Bulk grant scoped administrators live · build 269

Members can be added to a persona vault in bulk; the people who may manage them still cannot. A scoped role is granted one administrator, on one unit, at a time — so a four-person team across the eleven baseline units is 44 separate grants, and the failure mode is not the tedium but the gap: miss one unit and that persona has no administrator, which in a restricted unit means nobody can change its members.

Pick the administrators once and the units once, and the grid is applied — with each unit × administrator reported as its own outcome, because a partial success has to be readable rather than rounded to “done”. It should also show the current grants across all units together, since the question people actually ask is “who can manage the Externals exclusions?” and answering it today means opening eleven cards.

R04 🧹 Retire role-assignable groups everywhere live · build 287

The baseline is moving off role-assignable groups: the flag was only ever used for a side effect — membership reachable by Global Administrator or Privileged Role Administrator only — and a restricted management administrative unit does that better, names who may manage the group, and can be undone. It also cannot be combined with one: a group carrying both has nobody who can change its members.
Done: new groups are ordinary security groups (assigned or dynamic) with an optional restricted-AU placement at creation — in production since build 254, so the role-assignable checkbox and the ↻ Recreate-role-assignable action are gone from both channels; the ① Check drift flag is inverted, so a plain group is correct and a role-assignable one is what gets flagged; ⑦ Migrate moves existing ones across; and the bundled group templates and the baseline catalogs no longer ask for the flag anywhere.
The last gap, closed in beta 25149: 📥 Import no longer walks past the groups it reuses. A created group gets a place in its persona vault; a reused one got neither that nor its nesting looked at, and naming that in beta 25129 only made the exposure visible — it still sent you to 🔒 Protect exclusions and ⑧ Disable nesting to do anything about it. The preflight now offers both, per group and ticked by hand, and applies them after the policies import. Nothing is pre-ticked and membership is never touched: an existing group may hold members and sit somewhere on purpose, so the operator decides, not the importer.

Neither write is destructive, which is what makes offering them here defensible rather than reckless: Entra refuses the nesting change when a group already holds nested groups instead of forcing it, and administrative-unit membership can be undone. The one route that is destructive — recreating a group Entra refuses — stays in ⑧ Disable nesting behind its own typed confirmation, and converting a role-assignable group stays in ⑦ Migrate. One implementation each, as before.

What this still does not do, and cannot: the flag only truly leaves a tenant once ⑦ Migrate has been run there. Nothing retires a group this app did not create, and every protection path goes on refusing a role-assignable group until it is converted. That is a fact about the tenant rather than a gap in the tools.

Shipped across beta 25128–25129 · production 285 — find the role-assignable groups a file would build on, and say what a reused group inherits — and beta 25149 · production 287, which acts on it.

R35 🧬 Rename the bundled baseline to CloudFellows live · build 287

The baseline shipped here was called Limon-IT baseline — R26.6 (v3.x) and is now CloudFellows baseline — R26.6 (v3.x). The release designation and the catalog itself did not change; this is the name it is published under.

A rename is not one string, and all of them moved together: the catalog's own label and author — which is what the 🧬 Baseline Policies header, the on-screen summary line and the Markdown gap report all read, so none of the three could drift from the others — the tile, the home overview, the Help section and the code comments that explain why a rule exists (📘 MS Learn's exclusion-group convention refers to the baseline by name).

Two things deliberately untouched. The catalog id is still limonit: saved state and 📉 Drift watch snapshots key on it, so changing it would silently orphan every comparison stored before today — an identifier that appears in no interface has no reason to follow a display name. And the CAB-SEC / CAD-SEC group names are the tenant's own objects, not ours to rename; nothing in the 99-policy catalog, the group templates or the persona routing moved. Display name everywhere, identifiers nowhere.

R37 🌐 Named locations report what is wrong with them live · build 287

🌐 Named locations already knew all of this. It resolved which policies use each location and how, it counted the ones reachable only through All trusted locations, and it warned while editing that flipping the trusted flag moves every policy consuming it — and none of it became a finding. It was a reporting gap, not a reading gap, which is why it went ahead of anything needing new Graph calls: the four checks cost no extra read, they run over the location list and the policies already in memory.

Four checks. A dangling reference — a policy naming a location id this tenant no longer has, the same failure ① Check reports for groups; high while an enforced policy still names it, because an exclusion built on it silently stopped applying while the policy went on reading as configured. An empty country location, which matches nothing, so the block scoped to it blocks nobody — and a location that includes unknown regions is deliberately not flagged, because that one does match something. An overly broad IP range: a /0 is the whole internet and a /8 is 16.7 million addresses, neither of which is an office, and a trusted location carrying one is raised a level because everything using All trusted locations inherits it without naming it. And the untrusted IP location.

The fourth check is the one that had to be careful. The trusted flag only changes behaviour when something actually consumes All trusted locations, so in a tenant where nothing does it says nothing at all. Where a Block policy names the location, or the name says so itself, nothing is raised either — _Blocked IPs is untrusted on purpose and marking it trusted would be the bug. Everywhere else it is raised as low and the finding text names the deliberate block list as a valid reason to leave it exactly as it is. A check that cries wolf about the normal case is worse than no check. The context-aware form follows ca-policy-analyzer v1.16.3, which shipped the naive version first and then had to narrow it for exactly this case.

Where they surface, and where they deliberately do not. A panel above the list, a ⚠ badge on each row, the per-location report and both Markdown exports — with findings ahead of the inventory table, because the findings are why the file was opened. They are not in 🔍 Gap analyse: that tool reads policies and this reads objects, and a first non-policy finding there would need its own answer to “what is a finding about”. The open question is still open, now with an implementation to argue from.

R28 🏷 Custom groups in the persona vaults live · build 290

Everything that routed a group to a vault read the CA number in its name. That worked for the baseline and for nothing else: a tenant's own exclusion group — SEC-VIP-Exceptions, Contractors-NoMFA — matched no persona, so ⑥ Protect skipped it unless a fallback unit was chosen by hand, + Bulk add never offered it, and the break-glass case had to be special-cased by name to work at all. Most tenants have groups that predate the baseline, and telling them the naming is wrong is not a feature.

Now there is a per-tenant mapping: say once in 🛡 Restricted AUs → 🏷 Group personas that Contractors-NoMFA belongs to Externals, and every tool routes it there afterwards. One function decides — CaMap.codeOf, this tenant's mapping first and the CA number second — and ⑥ Protect, + Bulk add, the persona chips, 📥 Import, the 🛡 Markdown report and the restricted-unit pages in 📄 Create documentation all go through it, so none of them can hold a different opinion about where a group belongs.

The two things it must not do, built in rather than promised. It does not guess: matching is the group's object id or its display name compared case-insensitively, and nothing is inferred from a prefix or a word — a mapping nobody stated is how a group ends up in the wrong vault silently, and a vault is an authorisation boundary, so the cost of a wrong guess is one persona's scoped administrator holding another's exclusions. And it does not hide the unmapped: 🔎 Find the unmapped groups lists every group the policies reference that nothing places, with a persona one click away, and ⑥ Protect says unmapped on the row rather than skipping it in silence.

Held back from production 287, while the seven items around it went: it is the only one of them that changes where a write puts a group object, and a persona vault is an authorisation boundary — file a group in the wrong one and that persona's scoped administrator can edit another persona's exclusions. An empty mapping leaves every existing routing decision byte-for-byte as it was, so the risk is entirely in what an operator states rather than in what the code assumes. It reached production 290, one release behind its neighbours. Queue item 67.

Kept with the tenant, not in the code — the browser's own storage under the tenant id, so it survives a release and never becomes our list of everybody's group names. Nothing is written to the directory and nothing leaves the machine, which also means it does not follow you to another browser: ⭳ Export / ⭱ Import is the way round that, and it is why filing a group from the unmapped list records the object id as well as the name — an id survives a rename in Entra, a typed name does not.

R33 🔢 Tool reference numbers, in order of introduction live · build 287

The roadmap items carry Rnn references so one can be pointed at without quoting its title; the tools went by name and emoji, which drift — tools get renamed, retitled and, once 🇳🇱 R26 lands, translated, at which point every English note about a tool stops being findable. Each tool now carries a permanent T-number, shown in the tile corner and in the tool's own header beside the per-tool version, and searchable in the ⌨️ command palette: type T07, or just 7, and you land on 📘 MS Learn checks.

T01–T31 were reconstructed from git, not assigned by preference: the first commit that put each tile in index.html, ordered by commit time, with tools that arrived together ordered as they were written into the page. That makes the order a fact about the repository — checkable rather than arguable — and it puts 🗂 List Policies, 📄 Create documentation, 🔍 Gap analyse and 🗄 Backup at T01–T04, which is the day this app existed at all.

Never reused — not when a tool is retired, not when it is folded into another, because a recycled number makes every older note about it wrong, which is the exact failure the number exists to prevent. A new tool takes the next free number (34 next) in the same commit that adds its tile, alongside the changelog entry and the promotion-queue row.

Three things are deliberately not numbered: ❓ Help, 📋 What's new and 🗺 Roadmap. They are the app describing itself rather than tools that read a tenant, report on it or write to it — which is also why they carry no version. They go by name, and that is written down so nobody later "fixes" the gap by numbering them and shifting everything.

R36 🧩 Joey Verlinden baseline as a first-class baseline live · build 306

Until beta 25243 (finished in 25246), 🧩 Baseline (Joey Verlinden) only compared: it told you which of his 36 policies a tenant had and what differed, while group checks, group creation, persona checks, restricted-AU creation and placement all stopped at the CloudFellows catalog. Now there is an ★ active baseline per tenant — chosen by a button in the Baseline tool, never by merely looking at a comparison — and every tool downstream works against the one you picked. The switch is preceded by a dry run that reads the tenant and shows what would change — expected groups, expected vaults, which groups stop or start being routed, which conventions move — before offering the ★ Switch button, and a 🧹 leftovers list afterwards that reads what the previous baseline had created and deletes only what has stopped doing anything: ① Check and ② Create build his “<policy name> - Exclude” groups, 🛡 Restricted AUs offers his personas as vaults, ⑥ Protect and + Bulk add route his groups into them, the exclusion-restore action names his groups, and the guide's readiness checks count his objects. Each of those screens says which baseline it is working against.

Read live. The catalog is now fetched from github.com/j0eyv/ConditionalAccessBaseline once per session — the latest release, its Config/ConditionalAccess listing and every policy JSON — and the summary card says which release and commit it read. The hand-checked snapshot (2026.6.1 at 38469a4) stays as the fallback, and the first live read already showed why: the repository ships 38 files at that tag where the snapshot lists 36.

The three things that made it more than plumbing, and how each landed. His exclusion groups are named after the policy, not the CA number, so the routing rule for his baseline is an exact match against a policy name the catalog knows — the shape R28 established: one function deciding, an exact match, the unmapped left visible. His personas (Global / Admins / Internals / ServiceAccounts / Guests / Agents) are a separate vault list under his prefix — CA-RMAU-<Persona>-Exclusions, ENCA's convention because his baseline defines no units — sharing a code with a CloudFellows persona only where it means the same thing, so the R28 mapping is kept per baseline. And the live fetch is a trust boundary: unauthenticated GitHub is rate-limited and a release can be malformed, so every file is validated before it is believed, a read that fails leaves the snapshot in place and states the reason on the card, and the two new hosts are the only widening of the content-security policy, written down beside it.

R36.1 (beta 25249) — deployed on, not chosen. A tenant that never chose a baseline now gets the one it holds most of by name as the active one for the session — the card says the counts, 📌 Keep saves it, a saved choice is never overridden — instead of defaulting to CloudFellows while 26 policies carried his names. The CloudFellows 🚨 E-Admins policies are expected under every catalog as a shared section. And the live read now brings Config/Groups and Config/NamedLocations with the policies, so 📥 Import baseline takes the repository read directly, no zip, with a third assignment mode — 🔀 Switch baseline — that creates his groups and copies the members of the CloudFellows counterparts across (break-glass, same-CA-number exclusion, persona include) before the policies land Off; the old groups stay as the rollback and surface in 🧹 leftovers.

In beta today running on the beta channel, not yet in production

R01 📐 CIS Benchmark alignment in beta today

Score the Conditional Access policies against the CIS Microsoft 365 Foundations Benchmark v7.0.0 — the 17 automated recommendations of section 5.2.2, with per-control pass / report-only / configured-but-Off / fail, licence awareness, the nearest policy for every gap, and a Markdown compliance report. Running on the beta channel now; graduates to production once it has proven itself against enough real tenants.

R05 📖 Baseline usage guide in beta today

The toolset could build the baseline; it could not tell you what the baseline is or what order to do it in. That knowledge lived in whoever has done it before, which is the least durable place to keep it. Now it is a tool: the deployment order as six steps, each with the reason it sits where it does — the persona model and its CA ranges, groups and restricted units before the policies that target them, named locations, authentication strengths and contexts before the policies that reference them, Terms of use (which has no create API) before import, policies imported Off, then Off → report-only → 🎚 forecast → enforced, Global last. 🔎 Read the tenant turns every step into a readiness check that names what is missing before the step is run instead of after. Graduates to production once the step texts have proven themselves against enough real deployments.

R43 🛂 Session controls in beta today

What a session control actually did. The sign-in log stops at “App Control applied”; the blocked download, the protected file and the step-up are written by Defender for Cloud Apps. Read through Defender advanced hunting on Microsoft Graph (ThreatHunting.Read.All — no second portal, no API token) and joined back to the Conditional Access policy that routed the session. Says when a policy routes but never acts (Monitor only) and when a Defender policy has no router. The activity schema is undocumented and read tolerantly; a schema panel lists what the tenant's events actually carry, which is what graduates it.

R44 🔭 Sign-in source: Entra log, Defender hunting, or hunting with non-interactive in beta today

One segment shared by 🚦, 🎚, 🕵 and 🌊. The Graph sign-in list is interactive only and capped at 10,000; Defender hunting has the same sign-ins with no cap and 30 days — and, on the third setting, the non-interactive ones the Graph list never returns: token refreshes and background client sign-ins, where sign-in-frequency re-prompts at night, legacy-protocol blocks of service accounts and token-protection failures live. Read in explicit datetime slices that halve themselves on a large tenant. Needs Entra ID P2 for the hunting table to be populated.

R45 👥 Show nesting in the members matrix in beta today

Every member read also reads the group's direct list and its nested member groups, so the matrix can say how as well as who: ● direct, ◐ through a nested group with the child named, a Nested groups panel with each child's members, hide-empty-groups, and ◐ cells that are not removable because the membership lives in the child. On the same build the matrix finally got the grid every other matrix has — it had been bare rows since build 97.

Next being worked toward

R12 ↩️ Undo for writes planned

Every write tool already produces a change report of exactly what it did. Make it replayable in reverse: one action that puts back the state before the run, with the same typed-confirmation discipline as the original write. It lowers the cost of every write tool already shipped, and it is the natural safety net under Editable Conditional Access policies further down this map.

R13 🔐 Improve security planned

Implementing our own advice: the hardening recommendations from the security documentation turned into defaults — a tighter Content-Security-Policy, a documented review routine for the self-hosted libraries in vendor/, and an even smoother fork-review-pin path for high-assurance tenants that want to serve a reviewed copy from their own origin.

R16 🔓 Revoke permissions when done partially done

End every session least-privileged: one click that drops the consented write scopes when the work is finished, so a delegated grant collected for an afternoon of changes does not linger on the account afterwards. The natural closing move of the consent-on-the-click model the tool already uses. First step shipped: the Permissions panel's 🔒 How to revoke guide documents the three revocation routes with ready-to-run PowerShell and their consequences — what remains is doing it in one click, which needs careful design (see the guide for why the app deliberately holds no revoke rights itself).

R46 🤖 Workload identity Conditional Access planned

Do the service-principal policies ever fire? Conditional Access for workload identities has its own sign-in log (AADSpnSignInEventsBeta in Defender hunting) and nothing in ENCA reads it. The plan: the WorkloadIDs policies next to that log — which principals each targets, which of those actually sign in, what was blocked, the report-only forecast per policy, and the principals signing in from outside every named location that no policy can reach because they are managed identities or multi-tenant apps. Same hunting scope as R44; needs Workload Identities Premium for the policies to apply at all.

R47 🖥 Device reality check — the Defender half planned

🖥 checks the Intune half: is the scope of a compliant-device policy actually assigned a compliance policy. The Defender half is the fleet as Defender for Endpoint sees it (DeviceInfo): join type, sensor health, onboarding, exposure — held against every compliant / hybrid-device policy. The findings only the fleet side can give: devices that can never be compliant because they are not joined, a hybrid-join grant that permanently excludes cloud-native laptops, and “compliant” devices with high exposure. A second tab on the existing tool.

Self-hosted for the instance you run yourself — see SELF-HOSTING.md in the repo

S01 💾 Keep things between visits planned

The hosted site stores nothing anywhere, by design, and keeps that promise. A host of your own changes what is possible: an optional SQLite store alongside the container — a volume, nothing more — holding Drift watch snapshots, generated reports and per-tenant preferences, so a baseline signed off in March can be compared against in November without anyone having kept track of a JSON file. Strictly opt-in and self-hosted only; tokens stay out of it regardless — they belong in the browser session and nowhere else. Drift is the first customer: a snapshot that survives the tab closing is the difference between a drift check and a drift watch.

S02 🎨 Branding without a fork live · build 297

Branding settings, in the account menu behind your initials, on any host: product and organisation names, logos, login text and light/dark identity colours, through the same override mechanism as the hosted per-audience looks — chrome only, exports keep the neutral product credit. Apply keeps the look in this browser; Download produces selfhost-branding.json, Import reads one back into the form for review — so a look made elsewhere or handed over by a customer never has to be retyped — and serving that file next to index.html (the compose file and install scripts mount it) gives every visitor the branding — and softens the red BETA ribbon to a neutral SELF-HOSTED one, because a deliberately configured instance is not a test site. What the dialog will never configure is the canonical host: that drives the production check and the export credit, and a settings dialog must not be able to make a copy claim to be production.

S03 📦 Update channel choice planned

The image ships as :latest (production) and :beta today, and the fork notice (R15) says when you are behind — what is missing is making the choice a stated, visible thing: the instance saying which channel it follows, guidance for unattended updates (a watchtower-style puller for those who want them, and the reasons a reviewed fork should not), and a pinned-digest example for the high-assurance case where even :latest is too much trust.

Later after the above

R17 ✏️ Editable Conditional Access policies planned

Edit an existing policy from its card — not just state and group assignments but the conditions, grant and session controls themselves, with a field-level diff of exactly what will change and validation before the PATCH. The Change audit tool then shows the same diff after the fact, so intent and record match.

R18 🏢 Several tenants at once planned

For anyone who looks after more than one tenant: sign into each in turn and hold the results side by side — baseline coverage, CIS score, gap count, drift since last visit, one row per customer. One screen that answers “which of my tenants is worst off this month” without opening thirteen browser tabs. Pairs with Drift watch: a snapshot per tenant, reviewed on a schedule you keep rather than infrastructure you run.

R19 🌐 Named locations vs. the sign-in log planned

The trusted IP ranges in a tenant are usually older than the offices they describe. Cross the named locations with the sign-in log already read for Report-only impact and Sign-in failures: ranges nobody has signed in from for months, sign-ins from countries no location names, and trusted ranges that no longer match reality — the quiet way a location-based exception outlives its reason.

R20 🤝 Cross-tenant access settings planned

Inbound and outbound B2B collaboration, and whether the tenant accepts a partner's MFA and device claims. It sits directly next to Conditional Access — accepting a partner's MFA claim decides whether your own MFA requirement is satisfied by someone else's authentication — and it is invisible in every tool here today.

R21 🎫 CAE and token protection coverage planned

Continuous access evaluation and token protection are the newer session controls, and both are easy to leave half-configured: which policies opt in, which silently do not, and which resources support it. The same treatment the persona × control matrix gives the classic grant controls.

R22 📋 Change plan export planned

Export what a write would do as a reviewable file, hand it to a colleague, then apply it. The obvious companion to a toolset whose whole pitch is that nothing changes until you say so — and the practical form of a two-person rule for tenants that need one.

On the horizon builds on policy editing

R23 🏗 Policy builder planned

Build a new policy from scratch, guided: pick the persona, the resources, the conditions and the controls with best-practice hints along the way, preflight the result in What-If, and create it in report-only by default — so every new policy is born with the evidence trail the Report-only impact tool needs before go-live.

R24 🚀 Go-live checklist planned

Report-only impact forecasts one policy at a time; a checklist tracks the whole report-only backlog toward enforcement — what is staged, what has enough traffic to judge, what is ready to switch on, with the forecast attached to each. The paperwork of a go-live, kept in the tool that produced the evidence.

R25 📏 Naming-convention linter planned

The CA-number persona ranges are a convention every tool here assumes and none of them validates: numbers out of range for their persona, duplicates, gaps, policies whose name says one persona while their assignment says another. Cheap to check, and it keeps the convention that makes everything else legible from quietly rotting.

R26 🇳🇱 Dutch interface planned

The exported documentation is the part non-administrators actually read, and for a Dutch organisation that reads better in Dutch. The tool is built for it — one labels file, no build step — so the work is translation and keeping it honest, not plumbing.

R32 🧩 Every tool runs on its own planned

Today most tools quietly lean on the rest of the app having run first: the policy list a tool judges against was loaded by sign-in, the group names it prints were resolved by another tool's pass, the shared wiring holds state a tool assumes is there. It mostly works because the app loads everything up front — which means the dependency is invisible until the day it is not (a deep link straight into a tool, a tool opened before a slow read finished, a tool lifted out of this app entirely).

The rule this item works toward: every tool must be able to run with nothing but a signed-in session. Whatever its core result needs, it reads itself — or states plainly that it is waiting, instead of rendering from state that happens to be lying around. Connections to other ENCA tools — shared results, deep links and one-click hand-offs — are welcome conveniences, never prerequisites. Each connection is capability-checked and fails soft: if the other tool is absent, unfinished, denied or broken, the core tool still performs its own read and produces its result; only the shortcut or enrichment may be unavailable. Independence is also the precondition for R34 — a tool that cannot run alone cannot move alone.

R34 📦 Every tool easy to port to other applications planned

The newer tools already follow a pattern worth making a rule: the logic lives in its own file as pure functions over a plain data contract — no DOM, no globals, no Graph calls — and the app contributes only a thin wiring layer that fills the contract and renders the result. That split is why the Restricted-AUs checks, the guide and the device check can be tested with node alone, and it is exactly what makes a tool liftable: another application (a customer's own portal, a CLI in a pipeline, a scheduled report) reimplements the small wiring and takes the logic file as-is.

This item makes that the standard for every tool: each one a logic module with a documented contract (what goes in, what comes out, which reads the host must supply), thin host wiring, and optional integrations behind adapters outside the core contract. A host may supply another tool's result or expose a hand-off, but it may omit every integration and the module must still work. Tests run the core with integrations absent and throwing: an integration failure may remove convenience, never break the main tool. Portability is the honest test of R32.

R39 🤖 A second opinion, without growing a backend planned

Bring your own key, and let a model read a policy with you — starting with one 🤖 Second opinion button on the expanded policy card, next to the per-policy 🧪 What-If flow, because that is already the corner where you interrogate a single policy. The name is the promise: advisory, and labelled as such. Later the same panel takes a selection — two or five policies you suspect are interfering, which is where conflict and shadowing analysis actually lives — and later still the whole set. One policy first is not timidity; it is the only scope where the answer can be checked against tools that already know the answer.

No server, and it does not need one. The obvious build is a small backend holding the key and forwarding the calls. ENCA is not going to grow one: everything here is static files in a browser, nothing stored anywhere, and that promise is worth more than any feature. The Anthropic API can be called directly from the browser — CORS is enabled for it, opted into with the anthropic-dangerous-direct-browser-access request header. The key is pasted per session and held in memory, never in localStorage where it would outlive the tab and sit in a profile backup; it goes to api.anthropic.com and nowhere else; the whole feature is off until somebody types one in. Tools that reach for a proxy here are solving a problem this architecture does not have.

It reads the findings, not just the JSON — and it says where each line came from. A policy alone is the one thing ENCA is already strongest at: 📘 MS Learn, 📐 CIS, ✅ CA validator and the dependency inspector all judge it deterministically, and a model paraphrasing them adds nothing but a chance to be subtly wrong. So it is handed what those tools found — 🔍 Gap analyse already tags every finding with a policy id, so the per-policy slice is free — and every line it produces must be one of two things: a relayed finding, linked back to the tool that made it and styled like ENCA's own, or its own reading, marked as opinion and never dressed up as a check.

Disagreement is the output, not a bug to hide. When the model contradicts a deterministic result — “📐 CIS scores this a pass; I think the scope makes it ineffective because…” — that is shown, loudly. Either the model is wrong or a check has a blind spot, and both are worth knowing; neither survives being quietly resolved in favour of one side. And it fails soft, per R32: it works with nothing but the policy, and states what it did not see — “🔍 Gap analyse had not run, so nothing about who bypasses this was in what I read” — which tells you the answer is thinner than it could be and exactly which button makes it better.

Redaction, and the sharp edge is not the one you would expect. Replacing every name with a placeholder would destroy the most useful signal there is: CA101-GRANT-Admins-AnyApp-MFA is the stated intent, and intent-versus-implementation is precisely what a model is good at. So it is three tiers, not one switch. Always replaced: tenant name and domain, user UPNs, object GUIDs. Kept: the baseline convention — CA numbers, CAB-SEC-*, CAD-SEC-* — which is published documentation, not a secret, and carries the meaning. Replaced, with the mapping held in the browser so a finding reads back to real names: the tenant's own group and location names, SEC-VIP-Exceptions and the like, which are facts about that organisation. Conveniently that is a split ENCA can already evaluate, because it is the same convention-or-not question R28's group mapping answers.

Two things it will not do. It will not generate PowerShell: ENCA ships scripts written and tested deliberately, and a model improvising one that writes to a tenant is exactly the wrong shape for a toolset whose pitch is that it cannot do much harm. And its output never reaches 📄 Create documentation alongside the deterministic findings — in one document a model's opinion and a CIS control result look equally authoritative, and they are not. The connect-src in the Content-Security-Policy has to name api.anthropic.com: a deliberate, reviewable one-line change, not something to slip in ahead of the rest.

→ and further, steered by what you ask for

❓ How to use ENCA

A read-only toolset for your Microsoft Entra Conditional Access baseline. Sign in gives read-only Graph access; the four tools that can change your tenant are marked writes to tenant and ask for extra permission only when you run them. Every write lands safely — imported policies come in Off, deletes need a typed confirmation.

🗂 List Policies

Browse every Conditional Access policy, grouped by persona (the CA-number range in the name: Global 000–099, Admins 100–199, Internals 200–299, and so on).

  • Cards / List / Settings matrix — Cards show each policy in full; List is a compact one-line-per-policy view; the Settings matrix is a policies × settings grid for spotting differences at a glance. The matrix has a full-screen button.
  • Search — filter by CA number, policy name or persona.
  • Select policies (checkboxes) — a green action bar appears: send the selection straight to Documentation, Backup, Assign groups or roles, Set state or Delete.
  • Per-card quick actions — Documentation, Backup, Assign groups or roles, and Policy state, scoped to that one policy.
  • Apply flow (per persona) — a visual of what actually applies to a persona, including Global policies in scope and honouring exclusions (an excluded persona group drops the policy from the flow).
  • 🧹 Housekeeping — appears only when there is something to clean: policies that are Off while a higher version of the same CA number also exists, i.e. the old versions a match & replace import left behind as your rollback. It lists each one next to the policy that superseded it; the ones you tick go through the normal delete flow (JSON backup + typed DELETE).
  • The policy ID is on the expanded card too. The compact card carried it and the expanded one did not, so opening a policy to look at it properly took away the one string that identifies it — on the card that gets exported, screenshotted and pasted into a ticket. Two policies can share a name, a version and a persona; the portal is reached by id. One click selects the whole GUID.

Read-only. Nothing changes until you open a write tool from the action bar.

📄 Create documentation

Turn all or selected policies into shareable documentation carrying your tenant's branding. For a full control hand-off, the same document can include the restricted administrative units that protect the exclusion groups.

  • Word (.docx) — an editable document, one section per policy.
  • PDF — the same, print-ready.
  • PNG — a single image of the selection.
  • PNG bundle — a zip with one image per policy.
  • Include restricted administrative units — available for Word, PDF, Markdown and the PNG bundle. It adds an overview plus one page per restricted unit: persona and CA range, protected groups, every tenant policy that includes or excludes them, scoped administrators and whether the role definition can change group membership. Findings name a unit with nobody scoped, an empty unit, a frozen role-assignable group, and anything that could not be read.
  • The integration is optional and fails soft. Create documentation reads the units itself; it never depends on the Restricted AUs screen having run. If the unit list, one unit, scoped roles or role definitions are unavailable, the unknown part is labelled rather than guessed. The Conditional Access policy document still exports — an optional appendix may lose detail, but it cannot break the main tool.

On a baseline tenant (the 🧪 badge in the header) this documents the baseline catalog, not the tenant — the same rule 🗄 Backup follows, through the same code. The export is scoped to the persona CAxxx policies, the modal says how many of the tenant's own are being left out, and the scope is carried into the file itself: a line on the PDF cover, a quote under the Markdown header, the Word title, a SCOPE.txt beside the images in the PNG bundle. A document is read by somebody who was not in the room when it was made, and a policy that is absent leaves no trace of its absence.

The restricted-unit appendix is not filtered there: on a baseline tenant those units are the baseline's own persona vaults, so they are documented as they stand — scoped administrators included. The modal says so at the point of choosing, rather than leaving you to work out why one half of the document is narrowed and the other is not. PNG is the exception: loose images have no cover, header or companion file, so they cannot state their own scope — the modal warns when you pick it on a baseline tenant, and the PNG bundle is the one to reach for instead.

Read-only and produces a download. Nothing is written to the tenant. Restricted-unit pages use all loaded tenant policies for their impact references, even when only a subset of policy pages is selected, so the document cannot understate what widening an exclusion would affect.

🗣 User impact brief

The rollout communication, derived from the policies this tenant actually has: what people will notice, and what will deliberately no longer be possible. It is the one document in ENCA written for the people the baseline happens to, rather than for the person deploying it.

  • Every statement names its policies. Click one and the policy card opens — a sentence the communications team is about to send to the whole company can be checked against the configuration it claims to describe, which is the only thing that makes a generated brief safe to forward.
  • What is live is kept apart from what changes at go-live. The timing comes from the policy states, not from a guess: statements backed by enforced policies are marked live now and collected under Already live today; anything backed only by report-only or Off policies reads as at go-live. A statement resting solely on Off policies can never claim to be live.
  • Audience filter — everyone, employees, external consultants, guests, guest admins, developers, factory workers, administrators, whichever of them this tenant's policies actually address. Service accounts, workload identities and the break-glass lockdowns are deliberately not audiences and appear under no filter: nobody writes a rollout email to a service principal.
  • The persona baseline speaks first. Policies carrying a CAxxx number are analyzed first and carry the brief; policies without one are analyzed last, in their own trailing section, marked as possibly temporary — a hand-made policy that has been sitting in the tenant for a week should not put a sentence in the middle of a company-wide email. On a tenant with none, there is no trailing section and no mention of it.
  • Export as Markdown or Word for the communications team, with an appendix naming the same policies as the cards on screen.

Read-only, and it reads nothing at all: the analysis runs over the policies already loaded in this session, so there are no Graph calls and no extra consent. In BETA — the risk here is wrong words rather than wrong writes, so read the brief against what you know the tenant enforces before anybody sends it.

🔍 Gap analyse

A users × policies impact matrix: who is in scope for what, who bypasses a policy and why.

  • Coverage NEW — the funnel above the results, and usually the first thing worth reading. Five stages, each a subset of the one above: everyone in scopetargeted by at least one active policy → covered by one that is actually enforced → required to do MFA → holding the licence their own policies oblige. Every bar is drawn against the same denominator, so the shape of the drop is readable without doing arithmetic on five percentages.
  • The drops are the finding, not the totals. A tenant with 900 targeted users and 40 covered by an enforced policy has a report-only backlog, not a coverage problem — and those two look identical in any single number. Users reached by no active policy at all and users targeted only by report-only policies are called out by name in the notes under the funnel, because for the first group Conditional Access is not weak, it is absent.
  • Click a row to list exactly who did not get that far, and click it again to clear. These are deliberately not the same sets as the summary cards below: “No MFA from CA” counts everyone without MFA including the users no policy reaches, while the funnel’s MFA drop counts only those a policy does reach and still does not ask. A row with no drop is not clickable rather than being a link that does nothing.
  • The licence stage reads its own data — the assigned licences ride along on the user read the tool was doing anyway, and the subscribed SKUs are one more call, both covered by permissions it already holds. It does not depend on 🎫 Licence gap having run, and it uses that tool's own verdict function, so the two cannot disagree about whether a user is licensed. P2 is required where a risk-based policy targets them, P1 otherwise; report-only counts, because the obligation follows targeting rather than enforcement.
  • Not read is not zero. If the licence read is refused or fails, the stage says not read and every other stage is unaffected — “we did not look” and “nobody is licensed” are opposite findings. Targeted users with no licence record at all (guests, or a capped read) are left out of the stage and counted separately, never rounded up to licensed.
  • Group filters — narrow the matrix columns to the policies that target chosen groups.
  • Apply flow — the same per-persona flow as in List Policies.
  • Its own toolbar, and one way out. Gap analyse renders inside the List Policies screen, so it used to sit under that screen’s Cards / List / Matrix picker — which switched away from Gap analyse rather than within it, with none of the three highlighted because none of them was where you were. Its Matrix also meant the policies × settings grid, while the Matrix tab in this tool’s own toolbar means users × policies: two buttons, one label, one screen. The picker is hidden here now and a single ← Back to policies button takes its place, returning you to whichever view you came from. The tool’s own Users and Matrix tabs appear once a run has produced results.
  • Export — a standalone HTML report you can open outside the tool.
  • Scan only who you asked about. Scope → Only these users or groups names principals before the run and judges those, with the same policies and the same verdicts. A group is expanded to its members, nested ones included — the scan judges people, not groups, because a group being in scope of a policy says nothing about a member another policy excludes. In this mode nothing is read tenant-wide, which is where the time goes on a large tenant. The result carries a banner saying what was scoped, and the exported report says it too: "no risky bypasses" over four named people and over the whole tenant are different answers that would otherwise look identical.

Read-only.

🛡 Best-practice & bypass checks

Checks the baseline against known Conditional Access bypasses and the Swiss-cheese model.

  • MFA coverage, FOCI token-sharing, break-glass coverage, known bypass apps, and a persona × control coverage matrix.
  • Each finding has a severity and an explanation; a deployed-but-Off policy is flagged as not yet enforcing.
  • Export MD — the findings as Markdown for a ticket or review.

Read-only.

⚡ CA validator

For each enabled policy, the sign-in simulations it implies — the combinations of user, application, client app, location, device platform and risk it targets — and, per grant control, the control each one should enforce.

  • Expected control — every simulation shows the control the policy would require (MFA, Block, Compliant device, …). When the principal, app, location or platform is on the excluded side, the expectation inverts to “no <control>” — proving the policy does not fire there.
  • Run against a target — optionally enter a user (UPN) or a persona group (name or ID) to scope the report to only the policies that actually apply to that principal (include minus exclude); for a user, their group and role memberships are evaluated. Leave empty to simulate every policy.
  • Include report-only — off by default; tick it to also simulate policies in report-only mode.
  • Filter & search — filter by expected control, or search by policy or simulation text; collapse/expand all policies.
  • Users are shown as representative placeholders (“all users”, “members of <group>”, “role: …”) — no individual accounts are sampled. Session-only policies are listed as having nothing to assert.

Read-only. Ported from Jasper Baes' Conditional Access Validator (CC BY-NC-SA 4.0); the Maester test-code generation is not included.

🧪 What-If

Simulate one sign-in and see exactly which policies would hit it — the same idea as the Entra portal's What If tool.

  • Required conditions — user, target resource, device platform and client app. Everything else (IP, country, device state, sign-in/user/insider risk, authentication flow) is optional.
  • Policies that apply — each with the grant controls (and/or) and session controls that must be satisfied. A verdict line at the top says whether access ends up blocked or granted-with-controls.
  • Policies that do not apply — each with the first condition that wasn't met (user excluded via a group, wrong client app, location out of scope, …).
  • Filter that list, because on a real tenant it is a hundred long. The honest answer to a run is two policies that apply and 107 that do not, which is unreadable as one block — and the question behind it is almost never "show me everything". It is a persona question (why did no Admins policy apply to this admin?) or a number question (what happened to CA103?), so the list filters by both, using the same CA ranges 🗂 List Policies groups by. A count per reason comes first — "94 user out of scope, 8 platform not in scope, 5 user excluded" — because when a policy you expected is missing, the reason is what you are hunting for. The reason counts travel into the Markdown export too.
  • Search this tenant's applications by name. Target resource → Other searches your own service principals and fills the id in, rather than asking for a raw GUID. A pasted App ID still works and is resolved to a name underneath the box so the choice is verifiable; an id with no service principal here is reported, not refused, because a policy can legitimately name one — what is refused is a typed word matching no application, since that is neither a name nor an id.
  • Unspecified conditions — if a policy scopes something your scenario leaves blank (say sign-in risk), it can't be evaluated and so does not apply. Fill the field in to test it, exactly as the Microsoft evaluation API behaves.
  • App groups don't match — a policy targeting the Office 365 group won't match by design; pick the specific app. Only enabled and report-only policies are evaluated; Off policies are listed separately.
  • The verdict names the policy that said no. "Access would be blocked" with ten applying policies leaves you to work out which one did it; the blockers are named in the verdict and clickable straight through to the policy, and marked in the list below.
  • Report-only never counts toward the verdict. A report-only policy records what it would have done and changes nothing today, so a report-only block is reported separately — would block once enforced — rather than as a block. Counting it produced "access would be blocked" for a sign-in that in fact succeeds, which is the opposite of the answer you asked for. The same applies to grant controls: what you must satisfy comes from the enforced policies, with the report-only ones listed as what would additionally be required.
  • Export MD — the scenario and both lists as Markdown.

Read-only. Like the Microsoft tool it does not follow Conditional Access service dependencies, so a policy on a downstream service isn't pulled in.

🔗 User or Group analyzer

An Entra group is a shared handle. One admin scopes a Conditional Access policy to it, another targets an Intune compliance policy at it, a third grants it Contributor on a subscription — and nobody sees the whole picture, so adding a member has consequences that are invisible at the moment of the change. This tool reads that picture back out of the tenant. After Jasper Baes' Microsoft Cloud Group Analyzer, re-implemented here against delegated permissions.

  • One group or user — paste a group name, a UPN or an object ID. For a user, every group they are a member of is taken along, so you see what the person is actually subject to.
  • Parents count too — if the group is nested inside another group, anything targeting the parent reaches these members. Those hits are reported with via parent group “…” in the Matched via column, so you can tell a direct assignment from an inherited one.
  • Sweep every group — runs the same checks across the tenant and gives a group × service count table, plus the list of groups nothing references. Each service is read once and matched against every group, so a sweep costs little more than a single lookup.
  • Only groups used by Conditional Access — the default scope, and the one that matches what ENCA is for. It takes the include and exclude groups off every policy already in memory (enabled, report-only and Off), so it needs no directory enumeration and is bounded by your baseline rather than by tenant size. Any id a policy names that the directory cannot resolve is kept and flagged as a dangling reference — the policy still points at it, but the group is gone, so that assignment targets nobody. Worth more attention than a group that is merely unused.
  • Narrow the sweep by name — a prefix (CAB-SEC-), a suffix or a substring. Naming conventions live at both ends of a name, so this is usually all it takes to cut a 20 000-group tenant down to the set you care about. It runs server-side where Graph supports it and falls back to filtering locally where it does not, and “Groups in scope” then counts matches rather than raw groups.
  • Click a group in the sweep — a popup opens with that group's references, filtered out of what the sweep already read. No second scan, so it is instant however large the tenant. A sweep matches each group only against itself, so the popup shows direct references; ⤢ Deep analyze re-reads that one group with its parent groups expanded. Markdown, HTML and CSV export for that group alone sit in the popup footer. Deep analyze does not throw the sweep away — ← Back to the sweep appears above the result and puts it straight back, filters and all, at no cost.
  • Where to look — Entra ID, Intune, Microsoft 365 and Azure are independent. Tick only what you need; each area asks for its own permissions when it first runs, and Azure needs a separate Azure sign-in (management.azure.com), which is a different token from Graph.
  • Entra — group nesting, administrative units, directory roles (active and PIM-eligible), enterprise application assignments, group-based licensing, the authentication methods policy, Conditional Access, access packages, admin-consent reviewers, and — for a user — what they have actually registered.
  • Intune — enrolment restrictions (device limit and platform), compliance policies, configuration profiles (device, settings catalog, ADMX), scripts and remediations, app protection, app configuration, app assignments, Autopilot profiles and Windows update profiles.
  • Microsoft 365 & Azure — whether the group is a Microsoft 365 group with a team provisioned on it, the Purview sensitivity label on the group (the container label that governs the team's and site's privacy, guest access and external sharing), and every Azure RBAC role assignment across the subscriptions and management groups you can read, down to resource scope.
  • What Purview cannot tell you here — the container label above is the only part of Purview exposed to Microsoft Graph. DLP policies, retention policies, sensitivity-label publishing policies, insider risk and communication compliance have no Graph API at all: they live behind Security & Compliance PowerShell, which a static site cannot call. If a group is used to scope one of those, this tool will not see it — check the Purview portal separately.
  • The summary chips are controls — in a sweep, groups clears the filters, with no usage found toggles the unused-only list, services read opens the receipt (every service, what it found, how long it took) and not read jumps to the failures. On a single group, each area chip jumps to its section.
  • Not read — any service that failed, was only partly read, or was never granted is listed by name with the reason, the permission the call needs and the role the signed-in user needs on top of it. Those are two different things: a consented scope with no matching Entra or Intune role still returns 403. Read that list before concluding a group is unused; “nothing found” only means “nothing found in what was actually read”. A 404 is normal for a workload the tenant does not use.
  • Export MD / HTML / CSV — the full report as Markdown, a standalone HTML report that opens on any machine without access to the tenant (the artefact to attach to a change request), or one CSV line per reference for a spreadsheet.

Read-only — it never writes. A Conditional Access policy name opens its card, as in List Policies.

⚖ Compare users

Why does it work for her but not for him? Put two or more users side by side and see where Conditional Access treats them differently.

  • Policy assignment — per enabled or report-only policy, whether each user is included (✓), excluded (✗ — hover for the group, role or direct exclusion behind it) or not targeted (·). Rows where the users differ are flagged; Differences only (default) hides the rest.
  • Memberships — the groups and directory roles each user holds, same ✓/· grid. Assignment differences almost always trace back to a row here.
  • What-If scenario (optional) — describe one sign-in (resource, platform, client, IP/country, device state, risk) and it is evaluated once per user with the What-If engine: a verdict per user, then the per-policy matrix of who it would actually hit.
  • Export MD — the whole comparison as Markdown tables, differences marked .

Read-only. Resolution is per user (a few Graph calls each), so it stays fast on any tenant size. Assignment looks at user scoping only — conditions like location or platform only enter through the scenario run.

🕵 Who is Anna to CA BETA

The question a service desk gets is never “which policies target group X” — it is why can't Anna sign in, or is Anna in the wave yet. Answering it used to take four tools. This one takes a UPN and puts the whole Conditional Access picture for that one person on a single screen.

  • At a glance — four tiles: her deployment stage (which of the active baseline's CAD-SEC-U-DG-* groups she is in), how many policies reach her (enforced / report-only / off, and how many exclude her), how many sign-ins Conditional Access stopped in the window, and what happens to her if everything in report-only went live — locked out, extra prompts, no change, or no data.
  • Deployment groups — every deploy group and persona group of the ★ active baseline with In / Not in / Not in this tenant, and for each membership how: direct, or nested via which parent group. The exclusion groups she is in are listed with the policies they take her out of; one that bypasses an enforced policy is called a standing bypass. Who put her there is a question for ⚖ Compare users (the membership next to a colleague's) or 🕓 Change audit.
  • Policies — every policy, including the Off ones: state, reaches her via (All users, a group with its path, a role, guest type — or EXCLUDED and by what), the controls, this user's blocked / interrupted counts from the log, and for report-only policies the forecast (would block / prompts / no change, or no data). Filter chips: reaches her, excluded, not targeted, all.
  • Sign-ins Conditional Access stopped — her rows from the log: when, app and client, the policy, blocked or interrupted with the reason and error code, and a one-click 🧪 Replay in What-If.
  • If everything in report-only went live — worst case first, per policy, from the report-only verdicts on her own sign-ins, with the reason a denial would happen (“needs a compliant device — device NOT compliant”). A report-only policy that reaches her but evaluated none of her sign-ins is shown as no data, never as safe.
  • Buttons out — ⚖ Compare (adds her, ready for a colleague), 🧪 What-If (user filled in), 🔗 Analyzer (everything outside Conditional Access that points at her), Markdown brief for the ticket, CSV of the policy table.

Read-only. Memberships and policies come from what ENCA already holds — the same include/exclude resolution as ⚖ Compare users. The sign-in half asks for AuditLog.Read.All once, on the click, and reads only this user's sign-ins (server-filtered, so no record cap) — or reuses the window 🚦 Sign-in failures and 🎚 Report-only impact already read when it was not capped. Registered MFA methods are an optional extra read (UserAuthenticationMethod.Read.All), offered as a button and never required. Licence comes from the same verdict 🎫 Licence gap uses.

🌊 Who is the wave to CA BETA

A rollout goes wave by wave — CAD-SEC-U-DG-GLO, then -INT, then -ADM. The question before the next policy is switched from report-only to On is can this wave take it, and until now the answer was per policy (🎚 Report-only impact) or per user (🕵 Who is Anna to CA), never per wave. Pick a deployment group — the active baseline's deploy and persona groups are offered with their member counts, or type any group — and get:

  • At a glance — how many policies target the group (enforced / report-only / off, and how many reach members some other way), the sign-ins Conditional Access stopped for its members in the window, how many members would be locked out if everything in report-only went live, and the quiet ones — signed in, nothing stopped, no forecast change.
  • Go-live readiness per report-only policy — for each report-only policy that targets the wave: members with traffic, how many would be locked out (named), prompted, unchanged, and how many were silent. Verdict: Not yet (somebody would be denied), Friction only (prompts, nobody denied), Ready (evaluated, nobody stopped) or No data. Silent members are counted as no data, never as safety.
  • Policies and how they reach the wave — targets via a direct include of the group or a parent group, All users, or “reaches N members another way” (a role, another group, guest type); how many members an exclusion takes back out and through which group; the controls; blocked / interrupted counts and distinct members from the log.
  • Members — every member with how they got in (direct, via which child group, dynamic rule) and flags: in an exclusion group while the policy is On (a standing bypass), in two waves at once, blocked in the window, would be locked out, no Entra ID P1, disabled. Filter chips per flag; every name opens 🕵 Who is Anna to CA for that person.
  • How the wave is built — direct members, each child group with its member count and dynamic rule, and callouts: two waves at once (both personas' policies apply, the stricter wins), standing bypasses, coverage (is every member also in the Global deploy group), disabled accounts.

Read-only. Members are read transitively, first 500. The reads are bounded by the groups the policies name, not by the members: every referenced group (first 999 members each) and the baseline's deploy / persona groups are read once, plus the directory roles, and every member is then resolved against every policy with the same rule 🕵 Who is Anna to CA uses — so the two tools cannot disagree about a member. The sign-in half asks for AuditLog.Read.All once and reuses the window 🚦 Sign-in failures and 🎚 Report-only impact read; a capped window is said so. Licence per member is the same verdict 🎫 Licence gap uses.

🕓 Change audit

Who changed which Conditional Access resource, when, and exactly what changed — read from the Entra directory audit log.

  • Covers policies, named locations, authentication strengths and contexts, and terms of use — add, update and delete.
  • Field-level diff — expand an entry and you get the fields that actually moved (state: report-only → enabled, a group added to excludeGroups), not a wall of JSON. Assignment lists are compared as sets, so you see the one group that changed rather than the whole array.
  • Who and from where — the user or application that made the change, with the source IP, plus the result if it failed.
  • Range — use the last 1 hour, 4 hours or 24 hours to verify a fresh rollout or investigate an incident without paging through a week of unrelated changes. The 7, 30 and 90-day windows remain for reviews; Entra returns only what its retention still holds.
  • Filters — by resource kind, plus search across resource, person and changed field name.
  • Export MD — the whole change set with per-entry diff tables, for a review or a ticket.

Read-only. Needs AuditLog.Read.All (requested when you run it) and a reader role such as Reports Reader, Security Reader or Security Administrator. Audit retention is licence-bound — roughly 30 days on Entra ID P1/P2, 7 days otherwise — so this is a rolling window, not full history.

🛡 Restricted AUs

Restricted management administrative units — the vaults that shield your Conditional Access exclusion groups from tenant-wide administrators. Members of a restricted AU answer only to roles scoped to that AU; a Global Administrator can read them but not change their membership.

  • The baseline check comes first. The tool opens with a panel that looks for the nine units the baseline expects — CAB-SEC-RMAU-GLO / ADM / INT / EXT / GUESTUSERS / GUESTAdmins / SA / WLI / DevOps / FW-Exclusions plus CAB-SEC-RMAU-BreakGlass — and marks each one present, missing, or a name clash. One unit per persona is the whole point: with a single shared vault, whoever may manage the Admins exclusions may equally manage the Externals exclusions, and the separation you built the personas for stops at the AU boundary.
  • Create the missing ones — tick them, or take all. Each is created with the restricted flag set, and you are granted Groups Administrator scoped to it in the same step. That second half is not a convenience: an administrative unit with no scoped administrator is a vault nobody can open, because the restricted flag is precisely what shuts out the tenant-wide roles. Creation and the role grant are reported separately, so a unit that exists but has no administrator is visible as the half-outcome it is rather than counted as a success.
  • A name clash is refused, not skipped — if an AU already carries an expected name but is not restricted, it is shown as name taken — not restricted and is left alone. The flag is immutable, so that unit cannot be upgraded into the one you want; rename it and create a restricted replacement, or give the persona a different name.
  • Break-glass gets its own vault. CAB-SEC-U-BreakGlass is excluded from very nearly every policy in the baseline, so it belongs to no single persona — whoever can edit it can walk through every policy at once. It gets CAB-SEC-RMAU-BreakGlass, deliberately without an -Exclusions suffix because it holds the emergency access group itself rather than a persona's exclusions. Keep this unit's list of scoped administrators shorter than any other's.
  • Break-glass groups are matched by intent, not by our spelling. Tenants name them locally — BreakGlass, Break-Glass, Emergency_Access1, BG-Admins — so + Bulk add on the break-glass unit looks for all of those rather than only the baseline's name. A group carrying a CA number keeps that number's persona even if its name also says "emergency": CAB-SEC-U-CA101-EmergencyAccess is an Admins exclusion group, filed deliberately, and a coincidence of wording should not overrule it.
  • Groups, not the accounts themselves. Bulk add offers security groups. If Emergency_Access1 is a user account rather than a group, do not add it here — a Global Administrator inside a restricted unit cannot have their password reset by anybody, which is the one thing you need to work during the incident these accounts exist for.
  • Workload identities get a vault that will often be empty — and that is correct rather than a mistake. CA900–CA999 policies target service principals, which cannot be members of an administrative unit at all; only users, groups and devices can. CAB-SEC-RMAU-WLI-Exclusions holds whatever exclusion groups the range uses, which is worth separating because those groups gate the automation that runs with nobody behind it.
  • 📄 Report — the eleven rows, their status and their scoped administrator as Markdown, for the design document or the hand-over.
  • Ceilings worth knowing — a tenant may hold at most 100 restricted management administrative units, so eleven is comfortable. Only security groups can be members: Microsoft 365, mail-enabled security and distribution groups cannot. Deleting a unit can take up to 30 minutes to lift the protection from its former members.
  • Open a card — click its header, or the 👤 Scoped admins button, to see members and scoped role grants. The ✎ Edit dialog is only the AU's own name and description; roles are not granted there.
  • + Bulk add <persona> groups — on a baseline persona unit, gathers this tenant's security groups whose CA number falls in that persona's range and adds the ticked ones in one run. Matched by the same rule ⑥ Protect routes by, so the two cannot disagree. Adding twenty groups one at a time through the type-ahead is the same decision twenty times, and the failure mode is not noticing you stopped at nineteen.
  • What cannot go in is shown as prominently as what can, because those rows need a different action rather than another attempt: a role-assignable group (would be frozen — convert it with ⑦ Migrate first), a Microsoft 365, mail-enabled security or distribution group (only cloud security groups may be members), one already in this unit, and one already in another restricted unit — that last is not a duplicate but a widening, since a scoped administrator on any unit an object belongs to can manage it.
  • Add a member — the box suggests your baseline group names before you type anything, since those are what the AU exists to protect, and folds in live directory results for any other group or user as you type.
  • 🏷 Group personas — this tenant NEW. Every tool here routes a group to its vault by reading the CA number in its name, which works for the baseline and for nothing else: SEC-VIP-Exceptions and Contractors-NoMFA match no persona, so ⑥ Protect skips them, + Bulk add never offers them, and break-glass had to be special-cased by name to work at all. Most tenants have groups that predate the baseline, and telling them their naming is wrong is not a feature. Say once where such a group belongs and every tool routes it there afterwards — ⑥ Protect files it, + Bulk add and the persona chips offer it, 📥 Import places a reused group with it, and both the Markdown report here and the restricted-unit pages in 📄 Create documentation state the mapping.
  • It does not guess. Matching is exact — the group's object id, or its display name compared case-insensitively. Nothing is inferred from a prefix or a word in the name, because a mapping nobody stated is how a group ends up in the wrong vault silently, and a vault is an authorisation boundary: the wrong one hands a persona's scoped administrator another persona's exclusions. What you state wins over the CA number — somebody typed it, against this group, in this tenant — and ⑥ Protect marks such a row mapped here so a group filed somewhere its name does not explain always says why.
  • And it does not hide the unmapped. 🔎 Find the unmapped groups lists every group your policies reference that nothing places — no CA number the baseline recognises, and nothing said here — with a one-click persona for each. A group with no persona stays visible as unmapped rather than quietly dropping out of every list, and ⑥ Protect says the same on the row instead of silently skipping it. Filing a group from that list records its object id, so it survives a rename in Entra; typing a name into the box can only record the name.
  • Where the mapping is kept. In this browser, under this tenant's id — nothing is written to your directory, nothing leaves the machine, and the hosted site goes on storing nothing server-side. Two consequences, stated rather than hidden: it does not follow you to another browser or another machine, and a private window keeps it for the session only. ⭳ Export JSON / ⭱ Import JSON is the way round both — the mapping is a few lines of JSON that can live in a repository next to the tenant's other configuration, and importing offers replace or merge rather than choosing for you.
  • 👤 Grant across units — one set of people, several units, applied as a grid. Each unit × each administrator is a separate grant and a separate outcome, because a partial success has to be readable rather than rounded to “done”. It is deliberately not an “apply to all”: who may reach the Admins exclusions is not automatically who may reach the Externals ones, and assuming otherwise would quietly undo the per-persona split. A unit with nobody scoped to it is flagged in the list — that is a unit whose members nobody can change.
  • Grant a scoped administrator — pick the role, then start typing a name: users are suggested from the directory. Without at least one scoped grant, nobody can manage these members at all.
  • Do not combine with role-assignable groups. A role-assignable group's membership can only be changed by Global Administrator or Privileged Role Administrator, and a restricted AU blocks exactly those two — neither can be assigned at AU scope. A group with both protections has nobody who can edit its members. The tool refuses to add role-assignable groups to a restricted AU for that reason. Pick one: for a CA exclusion group the restricted AU is usually better, because it lets you name who may manage it.
  • The restricted flag is immutable — set at creation, never changeable. Converting an existing AU means creating a new one and moving the members, and the tool says so rather than letting you discover it.
  • Put the group in, not the accounts. A restricted unit holding a group protects the group's membership. Adding the break-glass user accounts themselves is a different and much sharper act: a Global Administrator inside a restricted unit cannot have their password reset by anyone, because no role that resets a Global Administrator's password can be assigned at administrative-unit scope. Recovering one means removing the account from the unit first — which is the opposite of what you want at 3am.
  • A 403 is the shield working — if a member change is refused, that is the restriction doing its job, not a fault. You need a role scoped to that AU.
  • Granting scoped administrators in bulk is searchable too. 👤 Grant across restricted units took a comma-separated list of UPNs, which assumes you already know them — so it was faster to leave and look them up. Search a person by name, pick, and they join the list as a chip you can take back off. Pasting a list still works and both feed one list: the picker writes the same field the paste box holds, because two sources of truth for who is being granted is how somebody ends up with a role they had removed.
  • Adding a group works like granting a scoped administrator. A unit already knows its persona, so the groups whose CA number maps to it are offered by name, one click each — nobody should have to remember that CAB-SEC-U-CA101-Exclusion is the Admins one. Groups already in the unit are not offered again, and when there are none left it says so rather than showing an empty row. Any other group is still typed in below, and while you are typing there the suggestion list puts this persona's groups first and the rest of the baseline after — a flat alphabetical list of every group in the tenant makes the right answer exactly as hard to reach as the wrong one.
  • A group that cannot go in says so, instead of being offered. The persona chips were built from the group NAMES the baseline knows about, which offered two things it should not: groups this tenant does not have, and groups it has that cannot be protected. Clicking a role-assignable one produced exactly the frozen group this tool exists to prevent. The chips are now backed by the same bounded scan + Bulk add runs, through the same verdict function — so what can be added is a click, and what cannot is listed with the reason: role-assignable (with a route to ⑦ Migrate), a Microsoft 365 group, a mail-enabled security or distribution group, already in this unit, or already in another restricted unit — which widens who can reach it rather than narrowing it. The type-ahead below carries the same verdicts on its options.

Writes to the tenant, and asks for each permission on the click. Creating one needs Privileged Role Administrator; granting a scoped role also needs RoleManagement.ReadWrite.Directory. Directory writes are not read-your-writes consistent, so a unit you just created may take a moment to appear in the list below — the creation log above it is the record of what actually happened.

⌨️ Command palette

Press Ctrl + K (⌘ + K on a Mac) anywhere in the app. Type a few letters, press Enter.

  • Tools — always searchable, signed in or not. Matching takes initials as well as substrings, so gap finds Gap analyse and bpbc finds Best-practice & bypass checks.
  • Tool numbers — every tool carries a permanent T-number (shown in the tile corner and in the tool's own header, next to its version). Type T07, or just 7, and you land on 📘 MS Learn checks; an exact number match outranks everything else, because a query that is a tool number is not an accident.
  • Policies — once a tenant is loaded, type a CA number (203) or any part of the policy name to open its card directly, without going through List Policies first.
  • Keys↑ ↓ move, Enter opens, Esc closes, Ctrl/⌘ + K again closes. Clicking outside closes it too.

Navigation only — it opens things, it never changes anything.

🔢 Tool numbers

Each tool carries a permanent T-numberT01 to T31 — shown in the corner of its tile and in its own header beside the per-tool version. Quote it and it means one thing: across the beta and production channels, in every build, and in any future translation of the interface.

  • Assigned in the order the tool entered ENCA, reconstructed from the repository rather than by preference — the first commit that added each tile, in commit order. So T01T04 are 🗂 List Policies, 📄 Create documentation, 🔍 Gap analyse and 🗄 Backup: the day this app existed at all.
  • Never reused. Not when a tool is retired, not when it is folded into another. A recycled number would make every older note about it wrong, which is exactly what the number exists to prevent. A new tool takes the next free number.
  • The version beside it is the opposite kind of thing — the number never changes, the version always does. They sit together so they are read together: “T21 v0.5” says which tool and how far along it is.
  • ❓ Help, 📋 What's new and 🗺 Roadmap carry none, deliberately. They are the app describing itself rather than tools that read your tenant, report on it or write to it — which is also why they carry no version. They go by name.

A label, nothing more — it reads nothing and changes nothing.

📉 Drift watch

What moved in the Conditional Access configuration since a snapshot you took earlier. Change audit answers who changed this inside the selected audit window — from the last hour up to the history Entra still retains; Drift watch answers does the tenant still look like it did when we signed it off — with no time limit, because the history is a file you keep rather than a log Microsoft ages out.

  • 📸 Take snapshot — reads the policies, named locations, authentication strengths, authentication contexts and administrative units and downloads them as one JSON file. Store it with your review files. Nothing is uploaded and nothing is kept server-side.
  • 📂 Load snapshot & compare — pick that file back up days, months or a year later. The tenant is re-read and the two are compared object by object.
  • Restricted administrative units are watched too. They sit nowhere near the Conditional Access blade, but they are what stops a tenant-wide Groups Administrator adding somebody to an exclusion group — so a group quietly leaving one widens the bypass surface exactly as much as editing the policy would, and nothing else in this tool would notice. A member removed from a restricted unit ranks Critical, a scoped administrator added ranks High (somebody new can now edit the protected groups), and a unit deleted ranks Critical. Needs AdministrativeUnit.Read.All, asked for on the click; decline it and the area is reported as not captured rather than as clean.
  • Ranked by severity — a policy switched Off or to report-only, an exclusion widened, a location turned trusted rank Critical, because that is how protection disappears without anyone deleting a policy. A rename ranks Low. The highest severity present is shown up front.
  • Readable diffs — GUIDs resolve to group and location names, and fields that move on their own (modifiedDateTime) are ignored, so a policy nobody touched reads as unchanged rather than as noise.
  • Honest gaps — an area that could not be read is reported as not captured, never as “no drift”. A clean bill of health is only given for what was actually compared.
  • Who, where known — run 🕓 Change audit first and choose a window that reaches back before the suspected change: 1, 4 or 24 hours for fresh drift, longer for older drift. Each drifted object then names the person who changed it while that event is still inside Entra's audit retention. Older drift is shown without attribution rather than with a guess.
  • Export MD — the drift report for a review, a ticket or a customer's change file.

Read-only, and uses the permissions the session already has. The snapshot contains your Conditional Access configuration — treat the file with the same care as any policy export.

🚦 Sign-in failures

Which sign-ins Conditional Access failed and which policy did it — read from the Entra sign-in log, grouped per policy.

  • Enforced — sign-ins with conditionalAccessStatus = failure: a policy's controls were not satisfied and the sign-in was blocked or interrupted. Filtered by Graph, so the read is cheap. An abandoned MFA prompt lands here too — a failure is a prompt to look, not proof the policy is wrong.
  • Report-only — sign-ins a report-only policy would have failed. These sign-ins complete, so Graph cannot pre-filter them: the whole window is read and filtered in the browser, capped at 10 000 sign-ins. This is the number to check before flipping a policy from report-only to On.
  • Per policy — one row per failing policy: failure count, distinct users, most affected users, and the grant controls that weren't met. Expand a row for the individual sign-ins.
  • 🧪 Replay in What-If — one click prefills the What-If scenario with the sign-in's user, app, platform, client, IP, country and device state, so you can see exactly which policies apply and why.
  • Export CSV — one line per sign-in × failing policy, ready for a pivot table or a SIEM. Export MD — the per-policy summary with sign-in tables, for a review or a ticket.

Read-only. Needs AuditLog.Read.All (requested when you run it) and a reader role such as Reports Reader, Security Reader or Security Administrator, plus an Entra ID P1/P2 licence for the sign-in log. Retention is licence-bound (≈30 days), so this is a rolling window.

🎚 Report-only impact

The go-live forecast. Every policy sitting in report-only is judged against the sign-in log: who would be denied the day it is enforced, who is interrupted for an extra step, and who passes unchanged. Report-only writes a verdict into each sign-in without acting on it, so this is evidence rather than simulation.

  • Per policy — is it safe to switch THIS one on. Per user — what happens to THIS person when everything staged goes live at once, which is the question nobody can answer policy by policy.
  • Why a user was interrupted — a policy called LowMediumUserRisk reporting “3 interrupted” invites exactly one question, and the sign-in records behind the verdict carry the answer: the drill-down reads user risk low ×2, user risk medium ×1. Counted per level rather than summarised, because two lows and one medium is a different decision from three mediums. User risk and sign-in risk are shown separately — a policy can key off either, and merging them would be right about half the time. Where the tenant has no Entra ID P2 the value arrives as hidden, and is reported as hidden rather than as no risk: not knowing is not the same as nothing.
  • Why it would say no — not just why the policy looked at the user. The scope line explains the assignment; this explains the refusal: what the policy demands and what the sign-in actually brought — needs a compliant device — device NOT compliant, Azure AD joined, Windows. Stated as evidence rather than as a diagnosis, because Graph does not return a per-control verdict: a device with no compliance state on the record is reported as unregistered rather than as non-compliant, which is a different problem with a different fix. Only a denial gets one — an interruption was satisfied by doing the extra step, so calling it a refusal would be wrong — and a Block policy says it blocks outright, since nothing the user does would help.
  • The sign-ins behind the verdict, shown the way 🚦 Sign-in failures shows them: the controls the policy demanded, then the client, operating system, location and device state of the actual sign-in — up to three per user and policy, which is enough to see a pattern without holding the window in memory twice. Where the sign-in also failed for an enforced reason, the failure reason and error code are there too (Device authentication is required. (50097)). Where it does not, the row says the sign-in itself succeeded and this policy only recorded what it would have done — an absence that is information, not a gap.
  • A verdict is only as good as its window. The range and the number of sign-ins read are stated up front, and a truncated read says so — a policy with no evidence is called out rather than counted as safe, because "nobody was affected" and "nobody signed in" look identical in a summary and only one of them is a reason to go live.
  • One read, two tools. 🚦 Sign-in failures in report-only mode issues byte-for-byte the same Graph query as this tool — the whole window, because report-only verdicts cannot be filtered server-side. Whichever runs second reuses what the first read and says so, rather than spending another multi-minute pass over ten thousand records; ⟳ Rescan always re-reads the tenant. Enforced-failure mode keeps its own server-filtered read, since that is a different and much cheaper query.
  • Export MD — the forecast with every affected user, for the change record or the go-live approval.

Read-only. Needs AuditLog.Read.All (requested when you run it) and a reader role such as Reports Reader, Security Reader or Security Administrator. Audit retention is licence-bound — roughly 30 days on Entra ID P1/P2 — so the window is what Microsoft still holds, not all history.

🚪 Exclusion analyzer

Every exclusion across all policies in one place.

  • Matrix (default) — exclusions × policies; Effective users — the same but expanded to the individual users a group exclusion covers; Risk review — policies scored for exclusion governance, worst first.
  • Risk review flagsHigh a privileged role excluded, or all guests/external excluded; Medium direct user exclusions (no lifecycle), more than five excluded groups, or disabled accounts still in scope — directly or sitting inside an excluded group; Info a report-only policy whose exclusions go live the moment it is enforced. Each flag is a prompt to review, not a finding. Patterns follow Tiago S. Carvalho's CA exclusions audit.
  • Type chips — All, User, Group, Directory role, Guest/external, Application, Named location, Device platform.
  • Click to filter — click a user/group row to show only the policies excluding it, or a policy column to show only the exclusions in scope for it. The out-of-scope “·” cells drop away; a banner shows the filter with a one-click clear.
  • Export — CSV or Markdown; full-screen for wide grids.

Read-only.

🧬 Baseline Policies — CloudFellows R26.6

Matches this tenant against the CloudFellows baseline catalog and shows the gap. It was published as the Limon-IT baseline until beta 25151 (production 287); the release designation, the catalog and the CAB-SEC / CAD-SEC group names are unchanged — only the name it goes by.

  • Status filtersMissing (not in tenant), Outdated (older version present), Up to date, Not in baseline (in tenant but not the catalog). The count chips on the summary card are the same filter: click one to see those rows, click it again for all. An active baseline with missing policies opens on Missing, because what is still to do is the question the screen answers.
  • ★ Active by match — a tenant that never chose a baseline gets the one it holds most of, by name: 26 policies carrying Joey Verlinden's names and none carrying CloudFellows' makes his the active baseline for the session, and the card says so with the counts. It is not saved — the next read decides again — until 📌 Keep or an explicit ★ Switch writes the choice; a saved choice is never overridden by a match.
  • 🚨 E-Admins on every catalog — emergency access is not one baseline's idea, and only the CloudFellows catalog writes those policies out (CA1100–CA1105). So they are expected under every catalog: the Joey Verlinden table carries them as its own section marked shared, a tenant that has them no longer shows six “not in baseline” rows for doing the right thing, and the import count says they come from the CloudFellows backup, not his repository.
  • One table, both catalogs — a baseline comparison is a row per policy with a status, so that is what it renders, whichever catalog you pick. The Cards view was removed in beta 25157: the same screen answered the same question two different ways depending on which catalog you had clicked, which made the two baselines impossible to read side by side. Search and collapsible persona sections work as before.
  • Catalog switch — CloudFellows R26.6 or the community Joey Verlinden catalog. Switching what you LOOK at does not switch what the tools work against; the ★ button on the summary card does (R36, see the next section).
  • Export MD — a gap report; Refresh re-compares in place; Import baseline hands the missing/outdated set to the Import tool — for the Joey Verlinden catalog straight from the live repository read (policy files, groups and named locations, no zip), with the gap ticked and 🔀 Switch baseline offered when this tenant's policies use the other baseline's groups.
  • 🧷 Update pre-requirements — one click, before you change anything, for the three things you cannot capture afterwards. A configuration snapshot (JSON) is taken first, because it is the only one that can be diffed against a later read in 📉 Drift watch: it is what answers what did this change? Then a full policy backup with every dependency resolved — groups, named locations, authentication strengths and contexts, terms of use — since a policy backup without its dependencies restores to a policy pointing at ids the tenant may no longer have, which is not a backup. Then documentation as Markdown, for the change record and the reviewer.
  • It takes every policy, not your current selection — a pre-requirement that captured whatever happened to be ticked would be worse than none, because it would look complete. Each artefact is attempted independently, so one failing does not cost you the other two, and the run always ends in a report saying which of the three were captured. Anything that could not be read is named: an unreadable snapshot area, a dependency that failed to fetch. If any step failed the report says so and tells you not to start the update — a failed pre-requirement is not a formality, it is the copy you would have restored from.

Read-only comparison. Only Import (below) writes.

🧩 Baseline (Joey Verlinden)

The same comparison view against the community Conditional Access Baseline by Joey Verlinden — a different catalog, the same table. Since beta 25243 (roadmap R36) it is a first-class baseline: not only compared against, but the one every downstream tool can work against.

  • ★ Active baseline — the summary card says which of the two baselines this tenant works against. The button under it runs a dry run first: it reads the tenant's groups and administrative units and shows what the switch would change — how many groups ① Check / ② Create would expect and how many exist, which persona vaults 🛡 Restricted AUs would expect and which are present, which groups would stop or start being routed by ⑥ Protect and + Bulk add, and the conventions that change (exclusion-group shape, break-glass name, fallback unit). Only under that card is the ★ Switch button; nothing is written by either step. The choice is kept per tenant in the browser (like the R28 group personas), defaults to CloudFellows, and is never changed by merely looking at a comparison — looking at Joey's table must not change where a write puts a group.
  • What follows the choice — 👥 Conditional Access groups ① Check and ② Create (his “<policy name> - Exclude” groups plus CA-BreakGlassAccounts - Exclude and CA-ServiceAccounts, as templates), 🔒 Protect exclusions and 🛡 Restricted AUs (his personas as vaults: CA-RMAU-Global / Admins / Internals / ServiceAccounts / Guests / Agents-Exclusions and CA-RMAU-BreakGlass — ENCA's convention under his prefix, because his baseline defines no administrative units), + Bulk add and the persona chips, the exclusion-restore action in 🗂 Assign, the persona picker, the 📘 MS Learn break-glass fix, and 📖 Baseline guide's readiness checks. Each of those screens carries a Working against … chip with a link back here.
  • 🧹 What the other baseline left behind — the switch writes nothing, so the previous baseline's exclusion groups, persona units and policies are all still in the tenant afterwards. The second button on the non-active card reads them and lists them in three sections with a verdict each: a group is safe to delete only when no policy references it, it has no members and it is not inside a unit that stays; a unit only when every member is an old-baseline group that is itself safe; a policy only when it is Off. Everything else is listed with the reason and cannot be ticked — a referenced group would leave a policy pointing at nothing, a guarding unit would open a vault, a policy that is On is still enforcing — and names the tool that deals with it. Break-glass groups are never offered. Deletion runs behind a typed DELETE, units first, then groups, then policies, with a change report; permissions are asked for on the click.
  • The naming contract — his groups carry the policy NAME, not a CA number, so a group is routed to a persona by an exact, case-insensitive match against a policy name the catalog knows, and nothing else. A group that matches nothing stays visible as unmapped, and the tenant's own mapping (🏷 Group personas, which is kept per baseline) can place it. His personas do not map onto the CloudFellows CA ranges — his CA300 block is service accounts — so the vaults are his, not a rename of ours.
  • 📡 Live from the repository — the catalog is read from github.com/j0eyv/ConditionalAccessBaseline once per session: the latest release, its file listing and every policy JSON in Config/ConditionalAccess, and the card says which release and commit it read. The bundled snapshot (2026.6.1 at 38469a4) is the fallback: a read that fails — GitHub's unauthenticated rate limit, a moved folder, a malformed release — leaves it in place and says so on the card rather than falling back quietly. Every file is validated before it is believed, nothing from it is executed, and a CA number used by more than one file in the repository is shown as such rather than silently de-duplicated.

Read-only itself; the writes it steers are the other tools', with their own confirmations.

📘 MS Learn checks

Checks policies against documented Microsoft Learn exclusions and limitations — break-glass, token protection, Teams Rooms, service providers, retired features.

  • Findings tab lists what conflicts with the docs; Refresh re-runs after you fix something.
  • Service provider (CSP / GDAP) exclusions — five checks covering the partner who administers this tenant on your behalf. They are matched by the Service provider users external user type and by nothing else: a partner admin arriving through delegated privileges holds no account, no group membership and no device here, so a group exclusion or a named break-glass account never reaches them. The checks find an external-user exclusion that lists the other five types but not this one; a Block policy their sign-in falls into; a compliant or hybrid-joined device requirement they cannot meet unless cross-tenant access settings trust device claims from their tenant; grant controls documented as unsupported for external users (approved client app, app protection policy, password change); and a guest MFA policy that leaves out the one external identity holding admin roles here. Exclusions naming specific tenants are read as such — an exclusion for two partner tenants does not cover a third.
  • Read once, judged against your tenant. Cross-tenant access settings are read to see which partners are flagged as service providers and whether this tenant accepts their MFA and device claims. A partner whose compliant-device claims you already trust does not raise a device finding, and a tenant with no partner at all has the five checks skipped rather than answered — the summary says so. If the settings cannot be read the checks still run and say the trust configuration was not verified, which is not the same as saying there is nothing there.
  • Buildable fixes — where a documented fix exists, apply it in the tenant (creating a service principal or group if the fix needs one). writes to tenant

Checks are read-only; applying a fix writes, and produces a change report.

🗄 Backup (JSON)

Select policies (or take all) and download their raw JSON in a zip — dependencies (groups, named locations, auth strengths/contexts) and any Terms-of-use PDFs are included, so the backup is importable.

On a baseline tenant (the 🧪 badge in the header) this tool backs up the baseline catalog, not the tenant. That tenant is where the baseline is built, so the backup is scoped to the persona CAxxx policies and the dependencies those policies reference; the tenant's own Conditional Access, and anything only it depends on, is left out. The confirmation says how many policies were skipped, the zip is named ConditionalAccess-BASELINE-… instead of -JSON-, and it carries a BackupScope.json stating what was left out — a zip is read long after the tenant that produced it is out of reach. There is no “include everything” option: a customer import is the place a stray tenant policy would surface, and by then nobody is looking.

Read-only download.

👥 Conditional Access groups writes to tenant

The group side of the baseline, in one tool.

  • Check — baseline groups vs this tenant: present, missing, or drifted. Create a single missing group per row (the TeamsSharedDevices group is created as a dynamic group with the Teams-Rooms membership rule). A group that IS still role-assignable is flagged the other way round now — that flag is retired, so a plain group is correct and a role-assignable one is what needs attention, with a button into ⑦ Migrate to convert it and place it in a restricted administrative unit. Everything created here is a plain security group with nesting disabled.
  • ⟳ Make dynamic — appears when the template defines a membership rule but the tenant group is assigned. A plain security group is converted in place: same id, so every policy, app and role assignment that points at it is untouched. A role-assignable group cannot be dynamic at all — Entra makes the two mutually exclusive and isAssignableToRole is immutable — so it is replaced instead: the old group is renamed -static-YYYYMMDD and kept as your rollback, a dynamic group is created under the original name, added to the include/exclude of every policy that referenced the old one, and only then is the old one removed from those policies (so nothing is uncovered mid-flight). The replacement is not role-assignable — don’t convert a group that carries a directory role or PIM assignment. In both cases members who don’t match the rule stop being members.
  • Members — a members × groups matrix.
  • Assign — add a group to policies’ include or exclude lists (ADD is additive — it never removes what’s there). A final confirmation and a change report before anything is written.
  • 🔁 Restore convention exclusions NEW — the eighth action, and the only one that targets a different group for every policy: CA200 gets CAB-SEC-U-CA200-Exclusion, CA201 gets CA201-Exclusion, from the CA number in the policy name. That reference is the first thing to go missing — a policy is rebuilt, an older version imported, an exclusion list tidied — and nothing notices, because a policy that lost its exclusion group looks exactly like one that never had it, right up until the exception it existed for is needed.
    Step 2 is a drift report rather than a list to tick: how many policies are already correct, how many have lost the reference, and how many name a group that does not exist in the tenant at all. Only the ones needing repair arrive ticked. Scope it to your selection for a repair, or to all policies in this tenant for the sweep — the same question asked of everything, which is how you find out how far an estate has drifted.
    The group name comes from the baseline catalog where the catalog has that CA number (definitive — and 14 of the 99 policies legitimately have no exclusion group, so those are reported as nothing to do rather than invented). A policy carrying a CA number the catalog does not have gets the convention applied by inference, labelled derived so it can be checked before it is ticked. Groups that do not exist yet can be created first from their baseline templates in the same run.
    Additive only — the group is added to whatever the policy already excludes; nothing is replaced and nothing is removed. But an exclusion widens a policy: anyone already in one of these groups stops being covered by the policy it is added to, so a tenant-wide sweep asks for the typed ALL like every other tenant-wide write, and the change report records the policy → group pairing rather than one group list.
  • Assign directory roles NEW — the wizard can target a policy’s Directory roles include/exclude, the same assignment the portal offers, not only groups. Choose Directory roles at the top and the same actions apply to them (bar “All users”, which has no role equivalent). Quick picks give Microsoft’s privileged set — the fourteen roles Microsoft names as the minimum to require MFA on, resolved against this tenant’s own role templates rather than hard-coded IDs — or every built-in role. Conditional Access enforces built-in roles only: custom roles and administrative-unit-scoped assignments are not covered by a policy scoped this way, which the wizard says at the point of choosing, in the review and in the change report.
  • 🚫 Disable group nesting BETA — on any present, non-dynamic group in ① Check. Not generally available: Microsoft documents the permission (Group-NestingSupport.ReadWrite.All) but publishes disableNesting on neither the v1.0 nor the beta group resource, and a tenant that lacks it answers 400 Request_BadRequest — “Unexpected request made to property ‘disableNesting’”. ENCA sends this one request to v1.0 (the only version whose docs name it), and when the directory refuses it says so, stops offering the action for the rest of the session, and does not fall through to the recreate — that route sets the property at creation, which is the request that was just refused, so it would destroy and rebuild a group to reach the same error. A nested group is an invisible route into a Conditional Access assignment: someone adds a group to a group and a policy's scope widens without the policy being touched, and the extra members never show up in a review of the policy itself. Entra's beta disableNesting property closes that side door — with it set, no group can be added as a member.
    Two things make this awkward, and the tool is explicit about both. Reading it is not free: a plain read does not return the property at all, so ENCA asks for it separately and reports three states — disabled, allowed, or not reported (which honestly means "this tenant does not surface it yet", not "allowed"). Writing it is undocumented: Microsoft lists a dedicated permission for updating it but does not list the property as updatable, and in the field it currently only takes at creation. So ENCA tries the in-place change first — and verifies it by reading the value back, because Entra will accept a PATCH and silently ignore a property it does not know. Only if that genuinely fails does it offer to recreate the group: rename the current one aside, create it again with nesting disabled, move the user members and repoint every Conditional Access assignment. That path is behind its own typed confirmation, never the first one. When Microsoft finishes shipping this, the recreate simply stops being offered.
    It refuses to run on a group that already contains nested groups. A nesting-disabled group cannot hold them, so recreating would silently drop those memberships and every user who was only a member through them. The tool names the nested groups and stops, so you resolve it deliberately.
    A recreate gives the group a new object ID. Conditional Access is moved for you; app assignments, Intune, group-based licensing and Azure RBAC are not — run User or Group analyzer on the group first. The old group is renamed, not deleted, so it is recoverable. Needs Group-NestingSupport.ReadWrite.All and Group.ReadWrite.All (on demand). Background by Daniel Bradley ↗
  • ⑥ Protect NEW — place the CA exclusion groups in a restricted management administrative unit. Membership of an exclusion group is a CA bypass, and any tenant-level Groups/User Administrator can hand it out; with the groups in a restricted AU (isMemberManagementRestricted, immutable at creation) only roles scoped to that AU can change members — Global Administrator included. Shows which exclusion groups are already protected, pre-selects the unprotected assigned exclusion groups (dynamic groups are opt-in — their membership follows a rule), creates or reuses the restricted AU, and optionally grants one account Groups Administrator scoped to the AU so membership stays manageable (do this — otherwise member changes need a new scoped assignment first). Needs AdministrativeUnit.ReadWrite.All (on demand), the Privileged Role Administrator role and P1. Only cloud security groups can be added; note that ⑤ Import members will afterwards need an AU-scoped role for these groups. Role-assignable groups are refused here — their membership is already limited to Global Administrator / Privileged Role Administrator, and a restricted AU blocks those same two, so a group with both protections has nobody who can edit its members. Choose one or the other.
  • ⑤ Import members NEW — bulk-add deployment-test users from a CSV. A UPN column is enough; with a Persona column (multi-persona cells split on , ; | or spaces) each user is auto-routed to the mapped group, pre-matched against the tenant’s group names — abbreviated conventions included (internals → …-INT). Only baseline groups (templates + active catalogs) are offered by default; ad-hoc groups that policies merely reference need an explicit opt-in checkbox. Users are resolved and existing memberships pre-checked (already-members are skipped), the plan is shown per group for review, and only then are members added. Dynamic groups are excluded — their membership is rule-managed. Ends with a Markdown change report.
  • Protection column — ① Check now shows where each group actually lives: 🔒 <unit>, not in a unit, or 🧊 frozen in <unit> when the group is role-assignable and restricted at once, which means nobody can change its members. A dash means the administrative units could not be read, which is not the same as “not protected”.
  • Group nesting is offered, not applied. A nested group is an invisible route into a Conditional Access assignment — somebody adds a group to a group and a policy’s scope changes without the policy being touched — so every create path used to set disableNesting automatically. That is off by default as of beta 25166, because the property is not generally available: Microsoft documents the permission that governs it (Group-NestingSupport.ReadWrite.All — “read and write groups’ disableNesting property”) but publishes the property on neither the v1.0 nor the beta group resource, and a directory that has not been given the feature refuses the field outright with Request_BadRequest. Applying it by default meant a red failure line under every single group created in such a tenant, for a setting the panel had promised. It is now a tick in ① Create and ② Build a group manually, off unless you ask; the request goes to v1.0, which is the only version whose documentation names the property; a refusal is reported rather than assumed; and once a tenant has refused it the tool stops asking for the rest of the session. Until it ships, the protection that actually works is a restricted management administrative unit, which limits who can change the members at all.
  • It is asked for twice and then verified. The property goes in the create body and is confirmed by a PATCH afterwards, because field reports say it only takes at creation while Microsoft documents it as patchable — doing both means whichever is true in your tenant, the group ends up right. Nothing is allowed to cost you the group: a tenant that rejects the property on create has the create retried without it, an unrelated create error still fails loudly, and a group whose nesting could not be set is marked nesting still allowed with the reason rather than reported as done. Not sent for dynamic groups (members are rule-driven, and only users and devices can be members) or role-assignable ones (Entra already refuses group-in-group).
  • ⑦ Migrate keeps running when you navigate away, and now says so. The recreate renames the original group aside before building its replacement, so "did it stop halfway?" has real consequences — and the answer used to be invisible. The progress log and bar were bound to the panel's elements once, so leaving the tool and coming back destroyed them: the run carried on writing into nodes that were no longer on the page, and you returned to an empty panel. Progress now lives with the run, so the panel replays it however often it is rebuilt, and a badge in the corner shows "⑦ Migrating 3/12" from any screen and takes you back with one click. Closing the tab mid-run asks first, and ⟳ Rescan refuses while a migration is in flight rather than discarding it.
  • ⑦ Migrate removes the old group from every policy, and now proves it did. The repoint used the reference list from the scan, so a policy edited since — by somebody else, or by an earlier group in the same run — was missed. A missed removal is not cosmetic: the old group stays assigned, and when the archive is tidied up later that policy is left naming an object id the directory no longer has, which is what a dangling reference in ① Check actually is. References are now re-read from the live policies at the moment of the repoint, extra ones found are reported, and the removal is verified by reading the policies back. If the old group is still referenced, the run says which policies and warns not to delete the archived group yet; if the check itself fails, it says that too rather than reporting a clean migration.
  • Deleting from a list no longer jumps you back to the top. Working through a list of named locations, authentication contexts or strengths, terms of use or administrative units meant scrolling back down after every single delete, because the panel re-renders from scratch. The scroll position is now kept across the re-render — and the browser clamps it when the list got shorter, which is the right answer for deleting the last row.
  • 🧹 Archived groups has select all / deselect all — a tenant that has been through a few recreates can have ninety of them, and ticking those by hand is not a review, it is an endurance test. Select all only ticks what is safe to delete: a group still referenced by a policy is never included, because one careless click across a list that long is exactly how a policy ends up pointing at nothing. Those rows stay individually tickable on purpose. Deselect all clears everything, including a referenced row you ticked by hand. A live count says how many of the total are ticked and how many are being deliberately left out, and the typed DELETE confirmation still gates the button.
  • Add a member — the matrix carries an + Add bar: type two letters and the directory suggests users, pick one of the loaded groups, and the member is added and the group re-read so the matrix shows the result rather than merely claiming it. Adding someone who is already in the group says so instead of failing.
  • Remove a member — the same bar has a − Remove button, and every ● in the matrix shows an × on hover. Both ask first, and the question says what the group is used for: taking someone out of an exclusion group puts them back inside the policy, taking them out of an include group takes them out of its scope. Dynamic groups are not offered — their membership is the rule's to decide. The group is re-read afterwards so the matrix shows the result.
  • What you can do with it — click a group in ① Check and the overlay lists the actions that apply to that group, each carrying it across: read its members, assign it to policies, protect it, or convert it. Only what makes sense is offered — a group that does not exist yet cannot have members read, a role-assignable one cannot be protected, and one already in a vault is not offered protection again. Every one of these was reachable before by opening the right tab and finding the row a second time; the tab strip just never said which of the seven applied to the group in front of you.
  • ⑦ Migrate BETA — move the baseline off role-assignable groups and onto a restricted management administrative unit. Role-assignable was only ever used here for its side effect: membership reachable only by Global Administrator or Privileged Role Administrator. A restricted AU does that and lets you name who may manage the groups, and it drops the role-assignable costs (500-per-tenant cap, no dynamic membership, no nesting control). The two cannot be combined — a restricted AU blocks GA and PRA, so a role-assignable group inside one has nobody who can change its members.
    isAssignableToRole is immutable, so each group is recreated, in this order: rename the current group aside as (migrated YYYY-MM-DD), create a plain group under the original name (nesting disabled by default — the one property the role-assignable flag gave you for free), copy the members, add the new group to every policy, remove the old one, and only then place the new group in the restricted AU. Members move before the AU placement because afterwards only an AU-scoped role could add them; the new group joins each policy before the old one leaves, so no exclusion ever briefly vanishes.
    Skipped, with the reason shown: a group that holds a directory role (a plain group cannot carry one — deal with the role first), one already inside a restricted AU, one not in the tenant, and one already plain. A group whose role check fails is skipped rather than risked.
    Select all / deselect all above the list, and the restricted-AU placement is optional — untick it to convert in stages, verify the members, then place the groups from ⑥ Protect when ready (they are ordinary groups by then, so Protect handles them like any other). A converted-but-unplaced group is editable by any tenant-wide Groups Administrator until you do.
    The archived originals keep their members and stay role-assignable — they are your rollback until you delete them from 🧹 Archived groups. Needs Group.ReadWrite.All, RoleManagement.ReadWrite.Directory, AdministrativeUnit.ReadWrite.All and Policy.ReadWrite.ConditionalAccess, plus Privileged Role Administrator.
  • Writes only on Create / Recreate / Make dynamic / Assign / Import members / Protect / Migrate, each behind a confirmation.

    🔒 Protect exclusions NEW writes to tenant

    The exclusion-group protection workflow as its own tool — identical to Conditional Access groups → ⑥ Protect, just reachable directly from the home grid.

    • What it does — creates the restricted management administrative unit if it doesn't exist (or reuses an existing restricted one), shows which exclusion groups are already protected, adds the selected groups, and can grant one account Groups Administrator scoped to the AU. Explicit acknowledgement before anything is written; Markdown change report after. Role-assignable groups are refused here — their membership is already limited to Global Administrator / Privileged Role Administrator, and a restricted AU blocks those same two, so a group with both protections has nobody who can edit its members. Choose one or the other.
    • Shared state — it uses the same group scan and selection as CA groups ⑥, so switching between the two views keeps your work.
    • Each group goes to its own persona vault. The destination is read from the CA number in the group's name — CA001 to CAB-SEC-RMAU-GLO-Exclusions, CA101 to ADM, CA1001 to DevOps — and shown per row before you write. One unit for the whole run was the old behaviour, and it quietly undid the point of having a unit per persona: protect CA001 and CA101 together and the Admins exclusions land in the Global vault, giving the Global vault's scoped administrators control of policies they have nothing to do with.
    • Nothing is filed by proximity. If a group's persona unit does not exist yet, or its name carries no CA number the baseline recognises, it is skipped and named rather than placed in whichever unit was nearest. The fallback unit at the top is used only for the unrecognised ones, and only if you pick it — it is unset by default, because defaulting to the first unit in the list means Global on most tenants.
    • Scoped administrators are granted on every unit the run wrote to, not just one of them, with each grant reported separately.
    • 🧊 frozen — a group that is role-assignable and already in a restricted unit is a dead end: its members may only be changed by Global Administrator or Privileged Role Administrator, and the unit blocks both. Neither flag can be undone. Remove it from the unit to restore those two roles, then convert it with ⑦ Migrate. ENCA refuses to create this state, but it can be inherited from a tenant configured before that guard existed, or from the portal — so it is now detected and called out wherever the group appears.
    • Search — the box above the list narrows the exclusion groups already found, with type-ahead over their names. Select-all then applies to what is visible, not to the whole list behind the filter.
    • Groups no policy references are listed too. A baseline exclusion group can sit inside a unit — or be frozen inside one — long after the policy that used it was retired, and while the list only held currently referenced groups there was nowhere in the app that would tell you. They appear marked not referenced, are never pre-selected and are not swept up by select-all: they are here to be seen, not acted on by default.
    • Protection is read in one call for the whole tenant rather than one per group, so the wider list costs nothing extra. If that read fails the column says unknown, never “unprotected” — the reassuring answer is the one you must not guess.

    Needs AdministrativeUnit.ReadWrite.All (on demand), the Privileged Role Administrator role and Entra ID P1 for administrative-unit administrators. See the CA groups section above for the full detail and caveats.

    🌐 Named locations writes to tenant

    Manage the named locations your Conditional Access policies target — the IP-range and country locations — without leaving the tool.

    • Cards / Table — Cards tile at least two per row with the ranges, usage and the Edit/Delete buttons; Table is the same information one line per location, which is the readable option once a tenant has dozens.
    • View — every location with its ranges or country list, whether it's trusted, and which policies use it (included or excluded). Filter by IP / country / trusted / unused, or search.
    • + New location — create an IP ranges location (CIDR, one per line, IPv4 or IPv6, optionally marked trusted) or a countries/regions location (two-letter ISO codes, optionally including unknown countries, with client-IP or Authenticator-GPS lookup).
    • Edit — rename and change the ranges/countries. The type is fixed at creation: an IP location can't become a country location. If you change the trusted flag, you're warned how many policies use “All trusted locations” and would follow the change.
    • Delete — a location that no policy references deletes after a plain confirm; one that is still referenced lists those policies and needs a typed DELETE, because removing it drops that condition and usually widens the policy.
    • Click a location name — opens its report: every range or country, the trusted flag and what it means, the location id, and the full list of policies that name it plus the ones covering it through “All trusted locations” (each clickable through to the policy card). From there, 📄 Documentation (MD) writes that one location up on its own.
    • ⚠ Findings — four checks over what the tool has already read, so they cost no extra call to your tenant. A dangling reference (a policy names a location id this tenant no longer has — the exclusion stopped applying, and the policy still reads as configured), an empty country location (no countries and unknown regions not included, so it matches nobody while every policy using it looks scoped), an overly broad IP range (a /8 — 16.7 million addresses — or a /0; worse when the location is trusted, because everything using “All trusted locations” inherits it), and an untrusted IP location where policies do rely on “All trusted locations”. Findings appear in a panel above the list, as a ⚠ badge on the row, in the per-location report, and in Export MD. Nothing is written and nothing is stored.
    • The untrusted check stays quiet on purpose — the trusted flag only changes behaviour when something actually consumes “All trusted locations”, so in a tenant where nothing does, the check says nothing at all. And a deliberate block list is untrusted on purpose: where a Block policy names the location, or its name says so, nothing is raised; everywhere else the finding names that as a valid reason to leave it exactly as it is. A check that cries wolf about the normal case is worse than no check.
    • Export MD — findings first with the full text of each (what was found, why it matters, what closes it, which policies are involved), then the full inventory with usage.
    • ⭳ Export JSON — the configuration of every location as a snapshot file. Nothing is stored server-side, so this is how you keep a record of what the ranges looked like on a given day.
    • ⇄ Compare… — load an earlier JSON export and see what moved since: locations whose ranges, countries or trusted flag changed, ones in the file that are gone from the tenant, and ones created since. Matched by display name, because an id from another tenant means nothing here. A flipped trusted flag is the one worth looking at first — it silently changes which policies match.

    Reading is read-only; create, edit and delete write to the tenant and ask for Policy.ReadWrite.ConditionalAccess when you run them.

    🎫 Authentication contexts writes to tenant

    The named step-up requirements, c1c99. An app, a Protected Action or a Purview label asks for "c1", and whichever Conditional Access policy is scoped to c1 is what enforces it. Every defined context is listed with its publish state and the policies that consume it.

    • The id is the contract, and the tool treats it as one. c1 is what applications request and what the ACRS claim carries, so a context can be renamed and republished freely but its id can never change. Renaming is safe; renumbering is not possible, and nothing here pretends otherwise.
    • Publish / unpublish in one click — that is the isAvailable flag, which is what decides whether the context can be selected at all.
    • Delete follows Graph's own rules, surfaced before you try rather than as a raw error: an unpublished context can be deleted, a published one is refused with a 403, and one a policy still references is refused with a 400 whatever its publish state. Repoint or retire the policy first. The delete itself takes a typed confirmation.
    • Free slots are counted, because c1–c99 is a fixed range and running out is a thing that happens quietly.
    • Export MD — every context, its state and what enforces it.

    Writes need Policy.ReadWrite.ConditionalAccess, the same scope every other Conditional Access write uses, asked for on the click. Create and update are the same call — Graph treats it as an upsert on the id.

    💪 Authentication strengths writes to tenant

    A strength is a set of allowed authentication method combinations, and it is what a policy grants when it demands more than "MFA". The three built-in strengths — Multifactor, Passwordless MFA, Phishing-resistant MFA — are Microsoft-managed and read-only here; custom strengths are yours to create, rename, re-combine and delete.

    • Classified by their weakest combination — phishing-resistant, MFA, or allows-single-factor. A strength is only as strong as the easiest way to satisfy it, and that is the number worth seeing on the card rather than the name somebody gave it.
    • Advanced options, as in the portal — restrict Passkeys (FIDO2) to specific AAGUIDs (Microsoft Authenticator and Windows Hello presets, or any custom AAGUID) and certificate-based authentication to specific issuer SKIs and policy OIDs. Shown on the cards and in the report, because a strength with restrictions is a different control from one without.
    • Per-strength policy usage — which policies grant it, so you can see what a change would touch before making it.
    • Delete is blocked while any policy grants the strength, and typed-confirmed otherwise. A dangling grant control is a policy that cannot be evaluated.
    • Export MD — every strength with its combinations, restrictions and consumers.

    Writes need Policy.ReadWrite.ConditionalAccess. Changing the combinations is its own Graph action (updateAllowedCombinations) rather than part of the update — sending them in a normal edit is rejected — so the tool issues both calls and reports them separately. The valid-combination catalog is read live from Graph, with a bundled fallback when that read is refused.

    📜 Terms of use writes to tenant

    The agreements a Conditional Access policy can require before granting access. Each one is shown with its behaviour settings — view before accepting, per-device acceptance, re-accept schedule, expiration — its PDFs per language, and the policies that require it.

    • The PDFs are actually there — with a direct download per language, and the default marked. The list endpoint does not return file content, so each agreement is fetched individually to get them; a list that showed no languages was reporting the API's shape rather than the tenant's.
    • Create takes a PDF, uploaded inline. Behaviour settings that the create API does not accept are applied immediately afterwards, which is invisible unless it fails — and if it does, it says which half succeeded.
    • Acceptance summary on demand — accepted and declined counts with the most recent, per agreement. It is a separate permission (AgreementAcceptance.Read.All) and is therefore requested only when you ask for it, not at sign-in.
    • Delete is blocked while a policy requires the agreement. Deleting it anyway would leave that policy with a grant control pointing at nothing; repoint the policy first. Otherwise typed-confirmed.
    • Export MD — every agreement, its settings, languages and consumers.

    Reads need Agreement.Read.All, writes Agreement.ReadWrite.All, acceptances AgreementAcceptance.Read.All — all delegated-only, with Conditional Access Administrator or Security Administrator. Replacing a PDF or adding a language is deliberately not in scope: that is a new file localisation and belongs in the portal.

    ♻ Recycle bin writes to tenant

    Conditional Access has a soft-delete window: a deleted policy or named location sits for 30 days and can be restored, after which it is gone for good. Each item shows what it did, the state it was in when deleted, when it went and how many days remain.

    • A policy comes back in the state it was deleted in. One that was On when it went enforces again the moment it is restored — not after a review, not in report-only. That restore therefore demands a typed RESTORE, and the stored state is shown before you commit rather than discovered afterwards.
    • Name collisions are flagged against the live set, since a restore does not rename anything and two policies with one name is its own kind of confusion.
    • Restoring a trusted named location warns you that every policy using "All trusted locations" follows it again immediately — the location is restored with its trusted flag intact, so the blast radius is wider than the one object.
    • Expiring soon filter, because the useful question is usually "what am I about to lose".
    • After a policy is restored the live policy set is reloaded, so every other tool sees it without a manual refresh.
    • Export MD — the bin's contents with their deadlines.

    Reads and restores need Policy.ReadWrite.ConditionalAccess, asked for on the click. Nothing here can delete anything — the only write is a restore.

    📞 Teams devices BETA writes to tenant

    Teams Rooms, panels, common-area phones and the resource accounts behind auto attendants and call queues sign in as users — and cannot answer an MFA prompt, accept terms of use, survive a sign-in frequency or work without device code flow. The baseline keeps them out of those controls by excluding one dynamic group, CAB-SEC-U-TeamsSharedDevices, from every global policy. Whether that works depends entirely on the group's membership rule, and the rule shipped so far names three Teams Rooms plans: a tenant whose phones sit on Teams Shared Space (Microsoft Teams Shared Devices until April 2026) and whose call queues use Teams Phone Resource Account licences has none of them in the group.

    • Why the rule cannot just list the SKUs — dynamic membership sees service plans (user.assignedPlans), never a licence SKU, and a device SKU is mostly plans every E3/E5 user also holds: Teams Phone (MCOEV), Teams, Skype for Business, Intune, Entra P1. A rule naming any of those puts people in the exclusion group. The tool reads the tenant's subscribed SKUs, recognises the device SKUs by part number, and keeps only the plans that no other subscription in the tenant carries — the plans that can isolate device accounts here. A bundled catalog of Microsoft's device-only plans is the fallback when the SKUs cannot be read, and gives every referenced plan a name.
    • Licences — every device SKU with how many of its seats are assigned and which plan isolates it. A SKU whose plans are all shared with user SKUs is marked not isolatable: type the UPN prefix those accounts share (mtr-, cap-) in the box and rescan, and it is added to the rule as a -startsWith clause — or keep those accounts in an assigned group the same policies exclude. Teams Phone Standard and the calling plans are listed as user licences, left out: people hold them, and they keep MFA.
    • And not a person — a device match alone is not enough: a DECT handset or a Shared Space licence assigned to somebody's own account matches every device plan and would put that person in the exclusion group, out of MFA. So the rule has a second half, -and (user.assignedPlans -all (servicePlanId -ne … )): the account must hold none of the plans that mark a user suite — E1/E3/E5, F1/F3, A3/A5, Business. Those markers are derived the same way in reverse (plans the tenant's suites carry that no device SKU carries, the smallest set covering every suite), with the SharePoint plans as the static fallback since no device SKU has ever carried SharePoint. The accounts this keeps out are counted and named — a device licence on a person is a licensing problem the rule avoids but cannot fix.
    • Recommended rule — the rule text, each plan named, its length against Microsoft's 3,072-character limit, and a preview: how many accounts in the directory match it today, with the first few by name. The preview is the same filter the rule expresses, run as a Graph query, so the number is what Entra will produce — before anything is written.
    • Groups — every group that looks like a Teams device group: the canonical name and its aliases, any dynamic group whose rule names a Teams plan, anything called Teams / shared / rooms. Per group: dynamic or assigned, rule state, member count, which Conditional Access policies include or exclude it, and a verdict — current, update the rule (with the missing plans named), matches PEOPLE (the rule names a plan real users hold — the group is excluding humans from MFA), assigned (nothing keeps it current) or read by hand.
    • Conditional Access reach — which active policies reach the accounts in the target group (All users, or the group included, and the group not excluded), against Microsoft's not-supported list for Teams devices: MFA and authentication strength, hybrid join, approved client app, password change, terms of use, sign-in frequency, persistent browser, app-enforced restrictions, app control, token protection, customised CAE, blocked device code flow, insider risk. Excluding the group is one click per policy in 📘 MS Learn checks.
    • ✏️ Replace the rule — writes the recommended rule onto the target group (an assigned group is converted to dynamic membership by the same write) after a confirmation that shows the old and new rule side by side; the new rule can be edited in the dialog first. + Create makes the group with the rule when the tenant has none — and reminds you it is excluded from nothing until you do that next.
    • 📄 Export MD — licences, rule, groups and reach as Markdown for the change record.

    Reads are covered by the baseline Directory.Read.All. The write asks for Group.ReadWrite.All once, on the click, and needs a role that can edit the group (Groups Administrator, or the restricted unit's scoped role when the group is protected). Entra re-evaluates a changed rule in the background: accounts the old rule matched and the new one does not leave the group and lose its exclusions; new matches join. The catalog reflects Microsoft's licensing reference as read on 2 September 2026 — the tenant's own SKUs are always preferred over it.

    📵 SMS & voice retirement BETA

    A temporary tool. Microsoft retires its own SMS and voice MFA delivery on 1 February 2027; from 1 September 2026 users still enabled for SMS or voice are auto-enabled for passkeys and nudged at sign-in, and after the retirement a user whose only MFA method is a phone number gets a blocking passkey-registration prompt with no opt-out. This tool reads the sms and voice authentication-method configurations (state, registration campaign, include and exclude targets), expands the scope to the actual users, and joins the registration-details report so every user carries a verdict.

    • Verdicts — phone as the only MFA method (locked out after the retirement), phone plus another method (migrate), phishing-resistant already registered (ready), in scope with no phone method (clean). A Phone role column says whether the phone is the user's only method, their default, or a backup.
    • Passkey dynamic migration — the Graph-only opt-out from Microsoft's 1 September 2026 rollout, which has no control in the Entra admin center: 🔎 Check reads it, one button pauses or resumes it behind an acknowledgement that names what it does not do (the 2027 retirement does not move), and 🕓 Who changed this? reads the audit log for the person and time behind the current value. This is the tool's only write.
    • Countdown, chips, exports — both dates, per-verdict filter chips, a capped on-screen table, Markdown report and full CSV.

    Reads only, except the one opt-out toggle (Policy.ReadWrite.AuthenticationMethod, Authentication Policy Administrator, asked on the click). The registration report needs AuditLog.Read.All, asked once; a refusal degrades the run to scope-only. Lives in the Temporary tools section and is removed once the retirement has passed.

    ⏳ memberOf retirement BETA

    A temporary tool. The memberOf dynamic-rule operator has been in public preview since 2022 and Microsoft ends that preview on 3 November 2026. Rules using it do not fail on that date — they stop updating and stay in their last known state: a frozen group keeps handing out whatever it held that day, joiners are never covered and leavers never removed, and nothing on screen says so.

    • Three surfaces — dynamic groups, dynamic administrative units and entitlement-management auto-assignment policies (the third needs EntitlementManagement.Read.All, asked once; a refusal reports that surface as not read, never as clean). The operator is detected by its documented user.memberof / device.memberof shape; anything that says memberof without matching it is reported as read-by-hand rather than dropped.
    • Consequences, not a list — every affected group is crossed against the Conditional Access policies ENCA already holds: an exclusion that stops shrinking is a permanent bypass, an inclusion that stops growing is an enforcement gap, both named per policy with the policy's state. Group-based licensing, an already-paused rule and a stale source group are called out too.
    • Owner notification — owners of the affected groups are read for an email that asks each owner to replace the rule, convert to assigned, or say it can go; administrative units have no owner and the modal says so. Countdown, Markdown report and CSV.

    Reads only and asks for no extra consent beyond the optional entitlement-management scope — rewriting a membership rule changes who a Conditional Access policy hits, and that decision belongs to a person in the portal. Lives in the Temporary tools section and is removed once the retirement has passed.

    🎚 Set Policy state writes to tenant

    Switch selected policies between On (enabled), Report-only (logged, not enforced) and Off (disabled).

    Writes the new state to the tenant after you confirm.

    🖥 Device reality check NEW

    The other half of the require compliant device grant. The CA side names who must present a compliant device; the Intune side decides which devices can ever be one — and neither portal checks that the two halves meet. Per CA policy and per platform: is the policy's scope actually assigned an Intune compliance policy, or does it fall through to a tenant default almost nobody has read?

    The four verdicts — one per platform, and the policy card wears the worst of them:

    • ✅ covered — proof was found: a compliance / app-protection policy for that platform is assigned to All devices or All users, or the CA policy's include group is assigned directly, or every member of the CA scope was matched inside the assigned groups (the membership pass). Covered means the check has someone to answer for this scope — it says nothing about how strict that compliance policy is.
    • ⚠️ not proven — Intune policies exist for the platform, but coverage could not be proven, and the tool refuses to guess. The reasons it says so: the CA scope cannot be enumerated (All users, guests, roles — there is no member list to match); only some members of the scope matched (“n of m covered”); an assigned group is a device group, which cannot be matched against the users of the CA scope; or the included group is empty today. Not proven is not the same as broken — it is “nobody can show you this works”, which for an audit is usually the same problem.
    • ❌ uncovered — a proven gap as far as reads go: no Intune policy exists for the platform at all, or none of the members of the CA scope is in any assigned group. What happens next is the tenant default above: marked-Compliant means the CA grant passes silently for exactly the people the policy names; marked-Not-compliant means those people get blocked instead.
    • 🚫 n/a — the control cannot exist on this platform. Approved client app / app protection is an iOS-and-Android mechanism; a CA policy demanding it on Windows or macOS has written a condition no sign-in from those platforms can ever satisfy.
    • The tenant default comes first, because it decides what every gap means: Intune's “Mark devices with no compliance policy assigned as” toggle. Set to Compliant, an uncovered device passes the CA grant silently — the policy enforces nothing for that scope. Set to Not compliant, the same gap surfaces as blocked users instead: loud, but not silent.
    • Per platform, not per tenant — compliance policies are per-platform, so a CA policy scoped to “any platform” can be covered on Windows and wide open on macOS. Settings-catalog compliance policies are read too (Linux only exists there).
    • Coverage is proven from assignments first: All devices / All users is proof; a CA include group directly assigned a compliance policy is proof. Each Intune policy on a verdict is listed with the groups it is assigned to (and its exclusion groups), because which groups decides who falls through.
    • Then the members themselves — a different group name does not mean a user is not covered: the CA policy can include a persona group while Intune assigns a BYOD group, and if the same people are in both, the coverage is real. Where assignment names could not prove it, the tool expands the memberships (transitive, so nesting counts) of the CA include groups and the Intune-assigned groups and matches the users: all matched → covered by membership, some → “n of m covered”, none → uncovered. Per-policy Intune exclusions count against that policy only. Scopes that cannot be enumerated (All users, guests, roles) stay honestly “not proven”, an Intune policy assigned a device group is flagged as unmatchable to users rather than counted as a gap, and an unreadable or over-cap group falls back to assignment names instead of pretending to be empty.
    • OR-alternatives are named — a policy granting compliant device OR MFA does not block an uncovered device, it just lets the sign-in through on MFA. That is a gap in what the policy name promises, not in availability, and the card says which it is.
    • Same check for app protection behind “require approved client app / app protection policy” — an iOS-and-Android mechanism, so a CA policy demanding it on Windows or macOS is flagged as a control that can never be satisfied there.
    • 📄 Export MD — the verdicts as Markdown for the review record.

    Read-only. Needs DeviceManagementConfiguration.Read.All and DeviceManagementApps.Read.All, asked once on the click, plus an Intune RBAC role that can read the workload (e.g. Read Only Operator). Assignment targets are checked first; where names alone cannot prove overlap, the tool expands transitive group memberships and matches the users. An unreadable, unenumerable or over-cap scope stays not proven rather than being guessed empty or uncovered.

    🎫 Licence gap

    Microsoft has started showing a “Licensing overage” warning on the Conditional Access page, linked to the licence usage blade. The blade counts evaluated users — unique users who actually triggered a policy during the previous month. But that is not what Microsoft licenses on: every user targeted by a Conditional Access policy needs Entra ID P1, and every user targeted by a risk-based (sign-in / user risk) policy needs P2 — whether they signed in or not. A blade showing “2 of 25, no warning” can sit on a tenant whose broadest policy targets every one of its 121 users. This tool computes the targeted number — the obligation — and compares it with the seats the tenant owns.

    • Targeting is resolved on the actual members — an All-users policy is the member-user count minus its resolvable exclusions; a scoped policy is the union of its included users, transitive group members (nesting counts) and directory-role holders (a role held through a role-assignable group is expanded to its users; service principals holding a role are ignored — they hold no user licences), minus its exclusions. Per-policy counts overlap, so the obligation is the union across policies, computed on user ids — one user under five policies needs one licence, never five.
    • Seats are matched on service plans, not SKU names: any subscription carrying the AAD_PREMIUM / AAD_PREMIUM_P2 plan counts (EMS, M365 E3/E5, Business Premium…), a P2 seat counts for the P1 obligation too, and suspended or cancelled subscriptions are excluded by capabilityStatus. The comparison is against seats owned, not assigned — an unassigned seat still covers a targeted user.
    • The gap is named, in a pop-out — the on-screen card shows the breakdown; the full list opens in a searchable dialog (large tenants make long lists), and the complete list is always in the Markdown export. 🏷 Check mailbox types reads each gap user's mailbox purpose (MailboxSettings.Read, asked once on the click) and labels shared, room and equipment mailbox accounts — those are never licensed and should be disabled or excluded, not bought for. A delegated sign-in usually cannot read another mailbox's type directly, so the check also reads the failure: an unlicensed account whose mailbox read is denied has a mailbox without a licence — almost always shared/room/equipment — and is labelled likely shared/resource with a verify-in-Exchange note, while “no mailbox” confirms a regular unlicensed account.
    • Honest numbers — a count where a group was unreadable or over the read cap, or a role could not be read, is marked rather than presented as exact. Report-only policies count (they evaluate everyone in scope); disabled policies do not, but a disabled risk-based policy is named as a P2 obligation waiting to happen. Guests are deliberately not counted — guest licensing follows different rules.
    • Why gaps exist and how to close them — the result explains the usual cause (an All-users baseline sweeping in disabled accounts, stale synced users, service accounts and shared mailboxes) and lays out the options with their trade-offs: clean up and exclude before buying, map admin accounts to their owners (one person, one licence — the counts here are identities), narrow the targeting only as a deliberate decision because everyone outside a licensed-users group goes unprotected, Security defaults for small tenants (cannot coexist with CA), and finally buying the remainder.
    • 👑 Exclude admin-accounts groups — Microsoft licenses people: a second internal account (an adm- account next to a day-to-day account) needs no second licence. Pick the group or groups that hold those accounts, one after another — the field auto-fills with the groups your CA policies already reference and searches the directory as you type — and the union of their transitive user members drops out of every count and named list. Each group gets its own chip with its own ✕. The exclusion is stated on the result and in the export, with the caveat that it assumes every member maps to a licensed owner: document the mapping.
    • 📄 Export MD — the comparison, the per-policy table and the mitigation options as Markdown for the licensing conversation.

    Read-only. Covered by the baseline Policy.Read.All + Directory.Read.All; only the optional 🏷 mailbox-type check asks for MailboxSettings.Read, once, on that click. After Rudy Mens' Get-EntraLicenseGap.ps1 (lazyadmin.nl). The overage banner is a warning today, not enforcement — this tool exists so your number is ready before that changes.

    📥 Import writes to tenant

    Restore a backup (zip or extracted folder). Dependencies are created first (create-if-missing), then the policies — always in state Off unless a replacement inherits an existing state.

    • Assignment mode — 🚀 Deployment groups: new policies are scoped to this tenant’s deploy persona groups (CAD-SEC-U-DG-*). Staged and safe — nothing already in the tenant is touched.
    • Assignment mode — ♻️ Match & replace: for a policy whose CA number already exists at a different version, the new version keeps the current policy’s assignment and state, merges in any new exclusion groups the update adds (created if missing), and the old version is switched Off — a seamless swap.
    • Assignment mode — 🔀 Switch baseline BETA: for moving a tenant from one baseline's groups to the other's. Policies land with the baseline's own groups as shipped (created here — Joey Verlinden's “<policy name> - Exclude” and CA-BreakGlassAccounts - Exclude, or the CloudFellows CAB-SEC-U-CAnnn-Exclusion set), and the members of the other baseline's counterpart groups are copied across: break-glass to break-glass, the exclusion group of the same CA number, the persona include group. It is a copy — the old groups keep their members as the rollback and show in 🧹 What <baseline> left behind (🧬 Baseline Policies) once nothing references them. A same-policy-other-name it supersedes is switched Off. Offered only when the file matches a baseline the tenant is not on, and pre-selected when the tenant's policies visibly use the other baseline's groups.
    • From the repository — 📥 Import baseline on 🧩 Baseline (Joey Verlinden) needs no zip: the live read of his repository ships the policy files, Config/Groups and Config/NamedLocations — the same layout as a backup — so the import starts from that read with the gap ticked and the rest of the release listed unticked. The shared 🚨 E-Admins policies are only in the CloudFellows backup and are said so.
    • Import only — pick a persona to bring in just its policies, or use All / None. A policy with the same CA number and version is skipped.
    • Unknown application references — a policy can only name an app that has a service principal in this tenant, and Graph rejects the whole policy over one it cannot resolve. The importer instantiates the missing ones first (the baseline excludes Microsoft first-party apps like Defender for Endpoint and Defender for Mobile TVM that an unused tenant does not have). If one still cannot be created, the policy is imported without that reference rather than failing — the report names each drop, and a dropped exclusion widens the policy, so review it before switching it On.
    • 🔒 no Workload ID licence — a policy scoped to service principals (the CA900 range) needs the separately purchased Microsoft Entra Workload ID licence, which is not part of Entra ID P1/P2. The importer reads the tenant’s subscriptions first; without the licence those policies are shown greyed out and left out entirely, rather than attempted and rejected by Graph with a bare 400. Buy or trial the licence, then re-import.
    • 📜 needs ToU — a policy granting a Terms of use imports without that control (Terms of use has no create API); the report gives a checklist to create it in the portal, then re-import.
    • 🛡 Protection preflight BETA — before the import runs, the tool checks that a restricted administrative unit exists for each persona your selection actually touches, and offers to create the missing ones (restricted, with you scoped as Groups Administrator). Every group the import creates is then added to its persona's unit, so a tenant-wide Groups Administrator cannot afterwards widen a Conditional Access exclusion. It is optional and never blocks the import: skip it and the groups simply land unprotected.
    • Only groups this run created are placed — a group that already existed is left where it is, because it may be somewhere on purpose and that is not the import's call to make. A group used by two personas is not placed either, and the report says why: an object may sit in several restricted units and a scoped administrator on any of them can manage it, so filing a shared group under one persona hands that persona's admin control of the other's exclusions, and filing it under both hands it to either. Split it per persona, or place it by hand knowing who can reach it.
    • An unreadable check says so — if the tenant's administrative units cannot be read, the panel reports that it could not check rather than reporting everything present, and the import proceeds unprotected with each group named in the report.
    • Role-assignable groups in the file are found before the import, not after. A baseline exported from a tenant that still used the role-assignable flag brings those groups with it, and importing on top of them is not neutral: the flag is immutable, it forbids controlling nesting, and it cannot be combined with a restricted management administrative unit — a group carrying both has nobody who can change its members. So the policies would import cleanly and land on groups the baseline has deliberately moved away from, and the 🛡 protection step would silently skip exactly those. The preflight now names them and says what will happen either way. Converting means recreating each one — rename aside, plain security group, members copied, every referencing policy repointed — which runs in ⑦ Migrate behind its own typed confirmation and report, and is deliberately not reimplemented here: one destructive operation, one implementation. Import now and convert later is still a choice; the report lists them as unprotected.
    • A tenant that partly has the groups is the normal case, and the two halves are treated differently. Groups the file brings that already exist here are reused — the policies bind to them, nothing is duplicated, and nothing about them is changed. That is the safe default: an existing group may hold members and sit somewhere deliberately, and an import is not the place to overrule that. But it means a reused group inherits none of the hardening a created one gets: a created group has nesting disabled and is filed into its persona vault, a reused one keeps whatever it has, and the placement step skips it unless you say otherwise. That skip used to be invisible — a reused group appeared in the report as a bare name. The preflight now lists them with what is actually being inherited: nesting disabled, allowed, or not reported by this tenant; already in a restricted unit or unprotected; a Microsoft 365 group that can never go in one; and a shape mismatch, where the file expects a dynamic group with a membership rule and the tenant has an assigned group of that name — the rule is not applied, and that would otherwise pass unnoticed.
    • What a reused group is missing can now be finished here. Naming the gap was only half an answer: the panel said the group was unprotected and its nesting untouched, then sent you to two other tools. Each reusable row now carries a tick for what is actually still open on it — 🚫 disable nesting, 🔒 file it into its persona vault — and anything impossible says why on the row rather than offering a dead checkbox: role-assignable (⑦ Migrate first), dynamic, a Microsoft 365 group, already protected, a group two personas share, or a vault that does not exist yet. Nothing is pre-ticked, and the writes run after the policies import, so an import that fails leaves every existing group exactly as it found it. Members are never touched. Both are writes the create path already makes and neither is destructive — Entra refuses the nesting change rather than forcing it when a group holds nested groups, and unit membership can be undone — while the destructive recreate stays in ⑧ Disable nesting behind its own confirmation. Every outcome reaches the change report, a failure included: it is reported as still unprotected rather than rounded to done.

    Writes; a change report is shown on screen and downloadable. Creating administrative units is a separate click from Import, and needs AdministrativeUnit.ReadWrite.All plus Privileged Role Administrator.

    General

    • Tabs — tools open in browser-style tabs; the + opens another, the house button returns to the tool grid, and ✕ all closes every tab at once.
    • Permissions — sign-in is read-only; a write tool requests its extra scope only when you run it, with an up-front consent step so pop-ups aren’t blocked.
    • Demo — append ?demo=1 to explore with sample policies; writes are simulated.
    • Build — the number on the sign-in screen matches the asset version; hard-refresh if it lags what you expect.
    • Forking — everything identity-shaped (name, organisation, logos, colours, host) lives in a single file, js/branding.js; nothing else hard-codes it. See “Rebranding a fork” in the README.