ENCA — Conditional Access Baseline Tools
Sign in with your Microsoft Entra account to browse, document, analyze and back up your Conditional Access baseline — including the newest policy settings.
Pop-up not working? Sign in without one →
⚙ Your organisation's ENCA login
🪪 No registration of your own yet?
Register ENCA in your own tenant — which route is open depends on the registration named above.
Runs entirely in your browser — no policy data leaves your session.
Reading your tenant…
Fetching Conditional Access policies
A clear view. A deliberate change.
Inspect policies, review the evidence and prepare your next rollout from one workspace.
Tools
⏳ 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. read-only
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
Entra device cleanup BETA NEW
Workspace 03 · Entra Tools. Stale directory devices in two deliberate steps: disable after 30 days without contact, delete after 90 — and only what is already disabled. Every write reads the device again first and back after; a device that woke up is skipped, one re-enabled since is never deleted, and ↩ Re-enable takes a disable back. A Markdown report of everything done. Ported from TUNO T25. writes to tenant
App access review BETA NEW
Workspace 03 · Entra Tools. Every app that can reach this tenant’s data — what it can do, who let it in, whether it is still used — one row per app with the verdict in plain words. App governance for tenants without Defender for Cloud Apps (Business Premium): Tier 0 and endpoint apps first, unverified publishers, unused apps and client secrets; who can do what (assign roles, push to every device, reach every mailbox); the consent settings; and per app approve, restrict, reduce, quarantine or remove — each with its impact and way back first, the decision written on the app. Markdown report and CSV. writes to tenant
Securing M365 BETA NEW
Workspace 03 · Entra Tools. The tenant's Microsoft 365 security settings read against a hardening baseline — Entra user and group settings, authentication methods, devices, apps and consent, guests, privileged access, SharePoint, mail DNS (SPF, DKIM, DMARC) and Conditional Access — every one a browser can reach. What Microsoft Graph can change is fixed from here: tick the failures, read each change's impact and way back, type FIX; a snapshot makes ↩ Undo possible. What only PowerShell can read (Exchange, Defender for Office, Purview, Teams) comes from a read-only script whose result you load back; portal-only settings are attested, and the attestation is kept in the tenant. Markdown report. writes to tenant
Egress & location impact BETA NEW
Workspace 03 · Entra Tools. What happens to Conditional Access when sign-ins leave through a different public IP — a cloud proxy such as Netskope or Zscaler, a new office line, a VPN. Paste the egress IPs: every policy with a location condition is judged today (from the trusted office ranges) and via the egress, side by side — over IPv4 and IPv6 as separate paths — who would be blocked, who gets an extra prompt, which policy loses its protection — with the country looked up in the sign-in log, an If trusted comparison and a Markdown export. Here until the egress decision (P-2716) is made. reads only
🗂 Explore & document
Read your baseline and turn it into something shareable.
User impact brief
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.
Policies UPDATED
Browse all Conditional Access policies as cards, a list or a settings matrix — grouped by persona. One screen, and the action bar does the rest: 📄 Create documentation (Word, PDF, PNG or a PNG bundle, with auditor-ready pages for the restricted units protecting exclusion groups), 🗄 Backup (JSON) to a zip with its dependencies, 🔍 Gap analyse, 👥 Assign groups or roles, 🎚 Set policy state and ⧉ Clone in builder — the three reads falling back to every visible policy when nothing is selected, the writes asking for an explicit pick. Open a card and ✎ Edit this policy — first in the card's action row since build 322: every box a form, a diff of what changes, whole-section writes. On a baseline tenant the document and the backup are scoped to the baseline catalog. writes to tenant
🔍 Analyse & simulate
Work out what your policies actually do, and to whom.
Gap analyse UPDATED
Who your policies actually cover, as a funnel: every user, then the ones a policy targets, then the ones an exclusion takes back out, then the ones holding the licence their own policies oblige. The last stage has its own tab — 🎫 Licences — because the obligation is the interesting half: Microsoft's licence usage blade counts who triggered a policy last month, while every user your policies target needs Entra ID P1, and risk conditions need P2.
What-If UPDATED
Two ways round the same question, one evaluator behind both. 🧪 One sign-in — describe a sign-in (user, app, platform, client, location, device state, risk) and see which policies would apply with the controls required, and which would not and why. ⚡ Every simulation — turn it around: per policy, every sign-in the policy implies and the control each one should or should not enforce, optionally narrowed to one user or persona group. Ported from Jasper Baes' CA Validator.
Who is … to CA BETA UPDATED
Pick the subject; the picture is the same one. 🕵 A user — 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, her sign-ins, her risk, what goes live next. 🌊 A group — the same for a whole wave: who is in it and how, which policies target it, how many members an exclusion takes back out, and every member's forecast. ⚖ Compare users — two or more side by side, where Conditional Access treats them differently and which group or role membership is behind it. One subject resolver and one policy ladder under all three.
Deployment dashboard BETA
The rollout as a picture, the way T28 draws it in TUNO. Every enabled user in the tenant against the CAD-SEC-U-DG-* deploy groups: who is in which group and how (direct, nested through a region group, by hand), who is left out of every group and why, which policies reach each group and in what state, and how many members an exclusion takes back out — a pie, by what. Per group, 🌊's go-live readiness on request. One page, exportable as HTML for people without ENCA. Reads only.
Apps with no service principal BETA UPDATED
Apps that signed in over the last 30 days and have no service principal in this tenant. Such an app is not in the Conditional Access app picker — it can be neither included nor excluded by name; only a policy on All resources reaches it. One Graph call lists them; for each one: which policies would apply the moment it exists, whether a policy already excludes the id (a phantom exclusion), and what Entra recorded on its newest sign-in.
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 UPDATED
Every exclusion across all policies — users, groups (expanded to members, with the nested groups they came through), roles, guest types, apps and locations — in an exclusion × policy matrix, with a risk review that flags an exclusion group fed by nesting.
Changes UPDATED
What moved, from either source. 🕓 Audit log — who changed which Conditional Access resource, when, and exactly what changed: a field-level diff of every policy, named location and authentication strength edit, as far back as your licence keeps (about 30 days). 📉 Snapshot file — take a snapshot today, compare a later run against it, and see what moved with no retention limit at all, because the history is a file you keep. One diff engine behind both, ranked by severity.
Sign-in log UPDATED
One read of the sign-in window, three questions of it. 🚦 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, CSV export and a one-click replay in What-If. 🎚 Report-only impact — what happens the day a staged policy goes live, per policy and per user. 🛂 Session controls — what a session control actually did, from Defender. 🤖 Workload identities BETA — the service-principal policies against the service principal sign-ins: who they reach, what they blocked, and who no policy can reach. Tabs over one window and one consent, so switching costs no second read.
🧬 Compare against a baseline
Measure this tenant against a reference set of policies.
Checks UPDATED
What is wrong with this baseline, judged against four references — each its own tab, each asking for its own permissions on its own run. 🛡 Bypass & Swiss cheese — known Conditional Access bypasses and the holes a stack of policies leaves between them: MFA coverage, FOCI token sharing, break-glass coverage, bypass apps, the persona × control matrix. 📘 Microsoft Learn — the exclusions and limitations Microsoft documents, with buildable fixes. 📐 CIS 5.2.2 — the 17 automated Conditional Access recommendations of the CIS Microsoft 365 Foundations Benchmark, per control. 🖥 Intune reality — the other half of a require-compliant-device grant: whether the policy's scope is actually assigned an Intune compliance policy at all. 🔑 Passkeys BETA — the policies that require a passkey against the Passkey (FIDO2) method: can the people in scope register and use one — and a Configure panel for the method. 🎫 CAE & token protection BETA — which personas have token protection, on which supported resources and platforms, and where continuous access evaluation is switched off or strict. 📏 Naming BETA — the CA-number convention checked: unnumbered policies, one number used twice, numbers outside every persona range, and names that say one persona while the number or the assignment says another.
Baseline UPDATED
Match this tenant against a Conditional Access baseline: which baseline policies are present, outdated or missing — then import the gap. Three catalogs, one picker in the toolbar — 🧬 CloudFellows R26.6 (v3.x), 🏢 SMB Basic BETA (security defaults as Conditional Access, plus Global Secure Access, stricter admins and break-glass — carried by ENCA) and 🧩 Joey Verlinden, the community baseline read live from his repository (latest release, with the commit it read) with the bundled 2026.6.1 snapshot as the fallback. The ★ active baseline for this tenant — what every group, vault and exclusion-restore tool works against — is chosen here, after a dry run shows what the switch would change. A tenant on security defaults is recognised — that is why it has no policies — and offered the way across: deploy a baseline in Report-only, read what it would do, then ⇄ switch security defaults off and the policies that replace it on in one run, with ↩ back. A 📖 Deployment guide tab gives the order to deploy it in with the reason for each step, and reads the tenant to say what is missing before each one.
✍️ Manage the tenant
The tools that write. Each one confirms before it changes anything.
Conditional Access groups UPDATED
Scope by workload (6.0): Used by CA policies or Used by GSA, picked by the workspace you come from — 01, or 03 where it is called Groups and opens on All groups. One read, then everything is a row: every group the baseline expects or a policy references, with its status, members (direct and nested), the policies that use it and its protection — and a detail panel for the one you click: members as a tree with add and remove, policies, protection, history. Create, assign, protect, migrate, import and compare from the rows you tick. writes to tenant
Protect exclusions UPDATED
Two locks per CA exclusion group on one row: a restricted management administrative unit so only AU-scoped roles can change the members, and nesting disabled so no group can be put inside. The break-glass group's accounts go into the unit too, and every unit is checked for a scoped Groups and User Administrator. writes to tenant
Policy building blocks UPDATED
🏗 Builder BETA — one guided screen for a policy, blank, from a baseline template, or from a policy selected elsewhere, preflighted in What-If and born report-only. The four kinds of object a policy references, each on its own tab, plus the bin they land in when deleted. 🌐 Locations — IP ranges and countries, with what is wrong with them: dangling references, empty country locations, overly broad ranges — and, in the vs. sign-ins view BETA, which trusted or policy-used location nobody signs in from any more. 💪 Strengths and 🎫 Contexts — the authentication strengths a policy grants and the contexts it is scoped to. 📜 Terms of use — the agreements it requires. ♻ Deleted — everything still inside the 30-day restore window, policies and locations, with one-click restore. Every tab says which policies use each row, from one shared lookup, and refuses a delete that would leave a policy pointing at nothing. writes to tenant
Restricted AUs
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.
Global Secure Access BETA NEW
Workspace 03 · Entra Tools. Make stolen tokens useless: every link Global Secure Access needs before a compliant-network-only Conditional Access policy can go on — licence, the tenant activated, the Microsoft traffic profile on and assigned, signalling, the compliant network location, the client on devices (Windows and macOS each in its own lane, ready only when Global Secure Access has seen the devices), the policy, and the proof from the sign-in log — on one board; once it is ready, 📈 Monitor watches the traffic: devices, the traffic log, Global Secure Access alerts and sign-ins that went around the tunnel. Each missing link that lives in Entra or Intune has its fix, described with its impact and its way back before it runs. Internet Access and Private Access come later. writes to tenant
Guided rollout NEW
CloudFellows from a backup ZIP, Joey via Fetch latest, SMB Basic from ENCA's own copy. Review the import, observed impact and enforcement with the existing confirmations.
Import UPDATED
Restore a backup or a baseline (zip, folder or Joey's repository): groups created and attached by name, dependencies first, policies staged Off and verified — landing Off or Report-only; re-attach earlier imports; the six E-Admins created from ENCA's own copy. 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.
Permissions in this session
View granted permissions, request consent and inspect revocation guidance.
Help
How every tool works and what to expect from each option — opens in its own tab, jump back to any tool anytime.
About ENCA, permissions and credits
ENCA (Entra Conditional Access) is a toolset for reviewing and managing policies 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 uses ordinary security groups with nesting disabled; dynamic groups retain their rule. This requests Group.ReadWrite.All and Group-NestingSupport.ReadWrite.All. Scoped role grants are a separate privileged operation.
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.
- M365 Security Toolkit — by Charlie Delmotte. The checks behind the Cross-tenant tab of 🛡 Checks: default and partner cross-tenant access, trusted external MFA and devices, cross-tenant sync, automatic redemption and dead partner entries.
- ⬇ Export baseline NEW (build 322) — on the reference tenant only. The same export 🗄 Backup gives on the Policies screen, opened from here and with a persona chooser: only the personas this tenant holds, each with its count, and the policy and dependency totals move as you tick. Dependencies — groups, authentication strengths, named locations, authentication contexts, terms of use, in per-category folders with the migration table — are ticked when it opens, because a baseline taken elsewhere without them is not taken. With nothing ticked it says zero and will not download.
- 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 tenant data stays in browser memory and is exchanged directly with the relevant Microsoft API; downloaded exports are files you keep.
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.
The ⚙ connection control on the sign-in card chooses which app registration and which tenant that sign-in uses — the registration this copy ships with, or one of your own, named by its client ID.
It sets only those two things (never the permissions asked for, nor which Graph endpoint is called), the choice is kept in this browser and sent nowhere, and whichever registration you name must carry this page's address as a SPA redirect URI.
Graph tokens are attached only to graph.microsoft.com; optional Azure RBAC reads use a separate token for management.azure.com (both hostname-validated); 🛡 Securing M365's mail-DNS check asks Cloudflare's public DNS-over-HTTPS resolver (cloudflare-dns.com) for your mail domains' SPF, DKIM and DMARC records — public names only, never a token; 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.
Policies
An exclusion group is a Conditional Access bypass, and two things widen it without the policy being touched: a tenant-wide admin adding a member, and somebody nesting a group inside it. Two locks, one row per group: a restricted management administrative unit for the first (only AU-scoped roles may change the members), disableNesting for the second (no group can be added as a member). The table says which lock each group still lacks; nothing is ticked for you. The break-glass group gets a third lock — its accounts go into the unit too — and 🔑 Who can manage the units checks that each restricted unit has a scoped Groups and User Administrator.
Same engines as 👥 CA groups → ⑥ Protect and ⑧ Disable nesting; nothing here recreates a group. Needs Privileged Role Administrator for the units, an Entra ID P1 licence for administrative-unit administrators, and a directory that has the nesting property — where it does not, the screen says so once and nesting stays in sight instead.
Guided rollout
CloudFellows from ZIP. Joey from Fetch latest. SMB Basic carried by ENCA. Review before applying.
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.
📋 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
Everything live is under Shipped before, at the bottom of this page — 59 items, newest first. What is moving now is In beta today, below.
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.
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).
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.
S04 ⏱ A collector that reads ahead of you planned
R58 still needs someone to open the tool for a read to happen, and Microsoft still keeps only 30 days. A self-hosted instance could run a small collector next to the container — its own identity (application permissions on your registration, a certificate or a managed identity, never a secret in the browser), the same daily bucket query every hour, the rows in a SQLite file on a volume that outlives every image update (schema kept by PRAGMA user_version, forward-only migrations, a nightly VACUUM INTO backup) — and the app reading /api/ on its own origin, so the week is already there when the tool opens and the retention is yours: ninety days, a year. Three things make it a different product and are why it is S-numbered: an unattended, tenant-wide read on a service principal; an API that has to authorise its callers with an exposed scope and a role check, because a Graph token cannot; and a second process behind nginx. On Azure Container Apps the store cannot be SQLite on Azure Files (Microsoft says not to put a database on an SMB mount) and the hourly run wants a Container Apps Job, so the storage layer has two backends — SQLite for compose and a VM, a hosted table store for ACA. Single-tenant self-hosters only; the shared registration never gets application permissions. Design note: review/SIGNIN-STORE-DESIGN.md.
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
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.
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
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.
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.
Shipped before delivered, and no longer news
Everything that is live, newest first. A card lands here when it goes live, and a later build ages any other shipped card here too. They are kept rather than deleted — a roadmap that forgets what it delivered is only a wish list — but they stop competing with what is happening now.
R92 🛡 Securing M365 — the tenant's M365 settings, checked and fixed from the browser live · build 330 · v2.0.8
T61 in Workspace 03, a new 🛡 Tenant hardening group: every recommendation of the CIS Microsoft 365 Foundations Benchmark v7.0.0 placed by how a browser reaches it — 43 fixed through Graph with impact, way back, typed FIX and undo; 9 checked; 17 from the Conditional Access engine; 3 mail-DNS lookups; 16 portal settings attested in the tenant; 61 from a generated read-only PowerShell script loaded back; 12 Fabric settings for phase 2. CIS numbers on the publisher tenant only.
R91 🏢 SMB Basic — a third Conditional Access baseline live · build 328 · v2.0.6
Security defaults rebuilt as Conditional Access for Business Premium tenants, plus Global Secure Access per platform, stricter admins (phishing-resistant MFA, 4-hour sessions, the compliant network, allowed countries) and a FIDO2-only break-glass policy. An exclusion group per policy and a restricted unit per section, the break-glass accounts in their own unit. A first-class catalog like CloudFellows and Joey Verlinden: the Baseline picker, the security-defaults card, Import, Guided rollout, CA groups, Protect exclusions and Restricted AUs all read it.
R90 🔑 App access review — app governance for Business Premium live · build 328 · v2.0.6
T60 in Workspace 03, a new 🔌 Applications group: every app that can reach the tenant's data — what it can do, who let it in, whether it is still used — judged per app instead of per permission name, Tier 0 and endpoint apps first. Who can do what as a matrix, users' own consents, the consent settings with the admin consent requests switched on from here, and per app approve, restrict, reduce, quarantine or remove, each with its impact and way back, the decision written on the app's Notes. Markdown report and CSV. For tenants without Defender for Cloud Apps app governance; replaces the community Export-MsIdAppConsentGrantReport workbook.
R88 🧹 Entra device cleanup — stale devices out in two steps live · build 328 · v2.0.6
T59 in Workspace 03, ported from TUNO T25: disable after 30 days without contact, delete after 90 and only what is already disabled, every write read again and read back, re-enable as the way back, and one Markdown report.
R89 🔐 Sign-in through your own registration only — no shared multi-tenant app live · build 327 · v2.0.5
Builds on R14, narrows R59. A copy of ENCA ships no login; a login is always a client ID and the tenant it lives in, and shared endpoints are refused in the code, the ⚙ panel and the container. After sign-in ENCA checks that the registration belongs to the tenant you signed in to and signs you out if it does not. The first registration in a tenant comes from Azure Cloud Shell (PowerShell, no storage account needed): the sign-in card gives the command, checked by its SHA-256. Next: a separate onboarding page, and permissions per workspace.
R85 🌐 Global Secure Access — make stolen tokens useless, the Microsoft traffic profile first live · build 326 · v2.0.4
T56 in Workspace 03: the readiness board for a compliant-network-only Conditional Access policy — licence, activation, the Microsoft traffic profile and its assignment, signalling, the compliant network location, the client on Windows and macOS (written to Intune from the browser), the policy and the sign-in proof — with the fixes that live in Entra and Intune. It never switches a policy on and never edits its exclusions. Next: Internet Access, then Private Access.
R84 🧭 Workspace 03 · Entra Tools — groups by the workload that uses them live · build 326 · v2.0.4
A second workspace beside Conditional Access for the directory objects every workload leans on, opening on its tools overview: Groups (T12, opened on All groups, its scope naming the workload — Used by CA policies or Used by GSA — beside Baseline + templates and All groups), 🌐 Global Secure Access, and the temporary tools (🛰 Egress & location impact, 📵 SMS & voice, 🧷 memberOf). 02 · PIM-buddy is listed in the workspace menu as beta: it is on the beta channel only.
R81 📊 Deployment dashboard — the T28 rollout picture for Conditional Access live · build 325 · v2.0.3
Every enabled user against the deploy groups: covered or left out of every one, route in, the policies on each group by state, and who an exclusion takes back out — a pie, by what. One row per deploy group with 🌊's go-live readiness on request, a left-out list, every other count a drill-through into 🌊, 🕵 or 👥. Export HTML for people without ENCA. Reads the tenant only — nothing from 🌍 Fill from regions. T54, reads only.
R76 🌍 Fill the deploy groups from the region groups live · build 325 · v2.0.3
The deploy groups filled from the country groups the way Tuno fills a wave: tick regions, read the users, see per group who comes in and who goes out and why, then write with a backup and an undo. Guests, external users, second accounts and users without an Intune licence each land in their own group.
R25 📏 Naming-convention linter live · build 325 · v2.0.3
A Naming tab in 🛡 Checks holds the CA-number persona ranges every tool here assumes against the policies loaded now: policies with no number, one number carried by two different policies (version pairs excepted — those are Housekeeping's), numbers outside every persona range, names that say one persona while the number says another, and — where the baseline names persona groups — a policy that includes another persona's group. The free numbers inside each range are listed as information. Reads nothing, writes nothing.
R63 📰 Microsoft Learn, watched every week live · build 325 · v2.0.3
The MS Learn checks were only as current as the last time someone read their pages. A weekly run now reads Microsoft's documentation repository for Conditional Access, flags every check whose page changed after it was last read, lists new pages and what's-new entries, and keeps one GitHub issue open while anything is undecided. Each item is decided on the tab, and 📋 Work order turns the decisions into work.
R46 🤖 Workload identity Conditional Access live · build 324 · v2.0.2
A Workload identities tab in 🚦 Sign-in log holds the policies that target service principals against the service principal sign-ins: which principals each policy reaches, which of them sign in, what was blocked and — from the Entra log — what a report-only policy would have blocked, and the principals signing in from outside every named location that no policy can reach because they are managed identities, Microsoft or multi-tenant apps. Its own source switch: the Entra log for per-policy results, Defender hunting (EntraIdSpnSignInEvents) for 30 days of volume without Conditional Access columns. Read-only.
R19 🌐 Named locations vs. the sign-in log live · build 324 · v2.0.2
A third view in 🧩 Policy building blocks → Locations, beside Cards and Table: every named location against 1, 7 or 30 days of the sign-in window the sign-in tools already share — how many sign-ins and users each one matched and when it was last seen, trusted ranges nobody signs in from any more, locations policies name that no sign-in came from, locations seen that no policy uses, and the countries sign-ins came from that no country location names. It reads Entra's own location match where the source carries one and matches IP ranges (IPv4 and IPv6) and countries itself otherwise. Read-only.
R21 🎫 CAE and token protection coverage live · build 324 · v2.0.2
A CAE & tokens tab in 🛡 Checks lays the two newer session controls out per persona: which personas have token protection, on which of the resources that support it and on which platforms, which are only in report-only, and where continuous access evaluation is switched off or made strict. The supported-resource list is dated against Microsoft Learn. Per-policy mistakes stay in 📘 Microsoft Learn, where they have a Fix — this tab counts them and points there. Reads nothing, writes nothing.
R66 🔑 Passkeys: required, and actually set up live · build 323 · v2.0.1
A Passkeys tab in 🛡 Checks starts from the policies that require a passkey and asks the Passkey (FIDO2) method whether the same people can get one — enabled, targeted, not excluded, self-service on, a passkey profile that lets through a key the strength accepts — counting the people by group and role membership. ✎ Configure changes the whole method (state, targets, self-service, passkey profiles) with a diff, what stops working and what it clears shown before Save, and ↩ Restore. Graduates once it has been read and written on a few real tenants, including one not yet opted in to passkey profiles.
R65 🤝 Cross-tenant access, read next to the policies live · build 323 · v2.0.1
A Cross-tenant tab in 🛡 Checks runs the checks of the open-source m365-security-toolkit (Charlie Delmotte, MIT): the default policy and every partner entry, trusted MFA and devices, cross-tenant sync, automatic redemption, dead partner tenants and entries that override nothing. ENCA adds where the trust lands — which policies a trusted MFA or device claim from another tenant satisfies. Read-only. Graduates once it has been read on a few real tenants with partner entries, including one with cross-tenant sync and one where a partner tenant no longer exists or the exists check was confirmed to degrade to not checked.
R20 🤝 Cross-tenant access settings delivered as R65 · build 323 · v2.0.1
Inbound and outbound B2B collaboration and whether the tenant accepts a partner's MFA and device claims, read next to Conditional Access. Built as R65, the 🤝 Cross-tenant tab of 🛡 Checks, which also follows every accepted claim into the policies it satisfies. Closed on 24 September 2026; the reference is kept so older notes still resolve.
R64 🏅 Identity Secure Score, next to the policies live · build 323 · v2.0.1
Microsoft Entra's Identity Secure Score on the home page and as a tab of 🛡 Checks: the percentage and its trend, the recommendations behind it, and for every recommendation Conditional Access answers, what ENCA sees — the policy that does it and whether it is On, report-only, built but Off, or missing. Read-only. Graduates once it has been read on a few real tenants, including one where the account lacks the reader role.
R62 🧰 From finding to catalog, in the baseline tenant live · build 323 · v2.0.1
A Microsoft Learn finding in the CloudFellows tenant used to mean: open Suggested fixes, apply everything or download JSON, find the changed policies again on the Baseline screen, tick them in 🧱 Update the catalog, and write the reason down yourself. Now each finding carries 🧰 Fix, the policy is changed in place with its version bumped, and the changed policies arrive in 🧱 Update the catalog already ticked, with the check and its Learn page as the revision note. Findings with no mechanical fix can be accepted with a reason. Graduates once a Fix has run against the real reference tenant and its catalog source has been committed.
R60 🧭 Workspaces: one layout for every tool live · build 322 · v2.0.0
The 2.0 interface. A left rail with the tools in work order, a branded header with the logo on its medallion, and every list tool drawn the same way — the list on the left, the selected item in an inspector on the right, the open row kept highlighted, both panes scrolling on their own and handing over to the page at their end. Each tool's head folds away with the ▾ at the end of its title, remembered per tool; flat icons, a contrasting active tool and tab, and tab strips inside the tools that host several. Nothing about what a tool reads or writes changed — only where it sits and how you move between them.
R61 🏠 An overview on the home page live · build 322 · v2.0.0
The home page became a dashboard. A snapshot header saying when the policies were read, the policy context (report-only, recently modified, baseline match, unique exclusion references), Worth a look first — findings with their evidence state and a policy filter that carries into 🗂 Policies — your own check runs, a configuration map of what the tenant holds, what changed since the previous refresh, and upcoming Microsoft changes. It says only what it knows: an unread source is Unknown, never zero, and nothing is scored provisionally.
R59 🪪 Your own registration, from the browser live · build 322 · v2.0.0
Builds on R14. Narrowed by R89 (production 327): the before-sign-in route needed the shared registration and is gone with it — the first registration in a tenant comes from the Cloud Shell command on the sign-in card; the wizard stays as the check and as + Add a separate registration once you are signed in through your own. The PowerShell route registers ENCA in your own tenant; this does the same from the page. Before sign-in, the sign-in card offers it on every host but the publisher's own: sign in once with the shipped registration, and a four-step wizard creates a single-tenant SPA registration in your directory with the same delegated permissions, its service principal, optionally assignment required with you assigned first, and optionally admin consent for the organisation — each step read back, impact and recovery stated before the write. After sign-in, a band on the home page (and a row in the account menu) offers it to a Global, Privileged Role, Application or Cloud Application Administrator while the copy still signs in through an app another directory owns. The result is the default sign-in: for this browser at once, for an Azure Container App deployment with one button that writes ENCA_CLIENT_ID and ENCA_TENANT_ID onto the container app with your own rights, and for any other host as the js/authConfig.local.js block and the docker -e lines to copy.
R23 🏗 Policy builder live · build 322 · v2.0.0
Shipped as T41, the first tab of 🧩 Policy building blocks — creation and Clone; editing an existing policy is R17, on its card — the tenant's locations, strengths, contexts and terms as the pickers, hints computed over the loaded policies, a What-If preflight, a change-plan export, and a policy born in report-only. 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.
R17 ✏️ Editable Conditional Access policies live · build 322 · v2.0.0
Shipped on the policy card in 2.0 — ✎ Edit turns the card's six boxes into forms, the name into a field, and a bar counts the settings that change, shows the ± Diff and the preflight, and saves whole sections, only the changed ones, read back after the write. The 🏗 builder keeps creation and Clone. 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.
R58 💾 Keep sign-ins on this device live · build 315
A report-only review is a cadence — an hour after staging, four hours, the next morning, a week — and every look used to read the whole window again. With your consent, per tenant, the browser now keeps the report-only verdicts the hunting sources bring back (per day, policy, verdict, user and app — the buckets of T26 2.0) and a later look asks Microsoft only for the days it does not hold yet, plus the current day, which is re-read every time because sign-ins reach the hunting table late. Opening the tool shows what the device holds first, then the new days land on top. Off by default, in words: the button next to the sign-in source says what is kept, where (this browser, this device, nowhere else), who can read it (anyone in this browser profile), and Forget deletes it at once; everything ages out after the days you choose. Tokens stay out of it, the Entra-log source is not stored, and the hosted site itself still keeps nothing anywhere. Next: whole sign-in rows for 🕵 🌊 🛂, behind a second, larger consent, once the buckets have earned their keep.
R51 🫥 Apps with no service principal live · build 312
Conditional Access can only name what exists. An app that signs in but has no service principal here is not in the app picker — neither includable nor excludable, reached only by a policy on All resources. One Graph call (signInEventsAppSummary, 30 days) diffed against the tenant's service principals; per app the 🧪 What-If verdict for the moment it exists, the phantom exclusions already naming it, and the newest sign-in as evidence. Idea from Jhope188's CA Policy Analyzer v1.17.0, rebuilt on ENCA's own What-If engine and sign-in window — and without its generated PowerShell.
R57 🧱 One frame for every tool screen live · build 312
Thirty-three tool screens, built one at a time over two years, had grown ten different orders for the same six kinds of control — find, scope, filter, window, the tool's own extras, the actions. No screen was wrong; what was wrong is that a control moved when you switched tools. The order is now one order, written into the stylesheet and enforced by tools/check-toolbar-order.js, and the spacing that 96 hand-typed inline margins used to hold together belongs to .screen.tool. The line each screen opens with came next: one title and one set of chips per tool, in the registry beside its number, after seven tools were found dropping a chip while they worked. The last four screens that were not like the others followed: the two form tools and 🔗 Group usage keep their controls in a toolbar like everybody else, every toolbar is addressable, and the one screen without one says why on itself. Then the part people actually noticed: the strip naming the tools inside a host moved above the head card and stays pinned, because under four paragraphs of 🛡 Checks nobody found it. Consistency between tools is the same programme, all of it in build 312: the verdict tile that six modules had each typed out by hand is one implementation in js/verdict.js, and 🔗 User or Group analyzer — the tool that surface was made for and the last big one not using it — is its first caller. Still open, and deliberately not done yet: whether the head card should collapse into the toolbar on the table-heavy tools so the result owns the whole window — worth it on 🗂 🚦 🕓 🕵, wrong on the tools whose prose is the tool.
R05 📖 Baseline usage guide live · build 311
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 live · build 311
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 live · build 311
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 live · build 311
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.
R48 👥 Conditional Access groups as one list live · build 311
Seven tabs that each started their own scan become one read, then everything is a row: a list of every group with its status, members, policies and protection, filter chips for what needs attention, an actions bar for the rows you tick, and a detail drawer that follows the row you click — members as a tree with add and remove (nested groups included), policies, protection, history. The engines underneath are unchanged; the list routes into them with the selection carried across.
R49 🚪 Exclusions: nesting in sight live · build 311
The exclusion analyzer shows how a user got into an exclusion group, not only that they are in it: direct or through a named nested group, per member, per matrix cell and in the head count — and the risk review flags an exclusion group fed by nested groups as the bypass it is. Same nesting read 👥 Conditional Access groups uses, so the two tools agree.
R50 ✅ One run ledger for every write live · build 311
Every tool that walks a list and writes — 139 policies, 6 groups, 4 ticks — shows the same box: the whole list before the first write, the row being written marked, ✓ or ✗ as each lands with the reason inline, the box scrolling to keep the working row in view, a count and a clock, Stop between writes. In 🎯 Assign, 🎚 Set policy state, and 👥 Migrate, Protect, Create, Import CSV, Archived groups and the Compare ticks.
R52 🗂 One door per room live · build 311
39 tiles, far fewer questions. Four of them were a second door onto a screen that already had one: 📄 Create documentation, 🗄 Backup (JSON) and 🎚 Set Policy state all opened 🗂 List Policies with one action pre-selected and a toast naming a button that was already on its action bar, and 🧩 Baseline (Joey Verlinden) was openBaseline("joey") against the same screen the catalog picker already switches. Folded: 39 tiles to 35, nothing removed, every deep link still landing. T02, T04, T05 and T11 keep their numbers, keep their Help sections under their new hosts, and the mapping is published in 🔢 Tool numbers. Phase 1 of the tool consolidation review; the tabbed hosts (🚦 Sign-in log, 🕒 Changes, 🧩 Policy building blocks, 🛡 Checks) are phase 2.
R53 🚦 One tool per data source live · build 311
Phase 2 of the consolidation review: where several tools read the SAME thing, they become tabs over one read. First the sign-in window — 🚦 Sign-in failures, 🎚 Report-only impact and 🛂 Session controls were three tiles over one readSignInWindow, one cache and one consent, and the only sign of it was a line saying the window had been re-used. Now 🚦 Sign-in log with three tabs. The tab strip is mounted into each member's own toolbar rather than moving three screens into one panel, so every screen keeps its scroll memory, its history entry and the ids other tools deep-link to; a beta-only member is a guarded tab instead of a deleted file. 🕓 Changes, 🧩 Policy building blocks and 🛡 Checks follow.
R54 🕓 What moved, and how to deploy it live · build 311
Two more hosts. 🕓 Change audit and 📉 Drift watch asked one question — what moved — of two sources over one diff engine, so they are 🕓 Changes with an Audit log tab and a Snapshot file tab. And 📖 Baseline guide, which only ever answered how do I deploy this baseline and linked seven other tools to do it, is the Deployment guide tab of 🧬 Baseline, where the baseline it describes already lives. Both bodies stay as they are: the fold removes a tile, not an answer.
R55 🧩 Policy building blocks live · build 311
A policy references four kinds of object managed elsewhere — named locations, authentication strengths, authentication contexts, terms-of-use agreements — and each had a tile over the same screen: a list, which policies use each row, create / edit / delete. Five tabs now, with ♻ Deleted as the fifth because the restore window holds deleted locations as well as deleted policies. Underneath, the four private copies of which policies use this became one implementation, so the All-trusted-locations case that only the locations tool knew about is available to every kind.
R56 🛡 Checks — four references, one tool live · build 311
The last of the four phase-2 hosts. Bypass patterns, Microsoft's documented limits, CIS 5.2.2 and the Intune compliance policy behind a require-compliant-device grant are rule packs as tabs, each asking for its own permissions on its own run. Not done, and said so rather than half done: one findings table across the four. They present a persona × control matrix, buildable fixes, a four-tier score with an L1/L2 filter and a per-platform grid — different shapes, and one table with a Source column would lose what each of them says. Phase 2 closes at 24 tiles, from 39.
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
Since R89 (production 327) this is the only way ENCA signs in — the shared multi-tenant registration it describes below is gone. Until then every tenant signed 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 could 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 (42 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.
Permissions in this session
Inspect the permissions granted to the current connection.
❓ How to use ENCA
A review and management toolset for your Microsoft Entra Conditional Access baseline. Sign in gives read-only Graph access; tools that can change your tenant are marked writes to tenant and ask for extra permission only when you run them. Imports are staged Off and verified. Replacements can then retain the previous state; as-is imports can retain the source state. Deletes need a typed confirmation.
🪪 Your own registration BETA writes to tenant
The browser companion of New-EncaAppRegistration.ps1 (R59, narrowed by R89 in 327). ENCA signs in only through an app registration owned by the tenant you sign in to — there is no shared, multi-tenant registration any more, and a copy of ENCA ships no login of its own. That ownership is the trust decision: your application ID, your consent record, revocable by you alone. The sign-in flow is unchanged: a public SPA client with authorization code + PKCE and no secret.
- Before sign-in — the first registration in a tenant. A web page cannot sign you in to a tenant through a registration that tenant cannot reach, so the in-page route that used the shared registration is gone. The ⚙ panel on the sign-in card, under 🪪 No registration of your own yet?, gives the one route that never needed one, as four steps: Open Azure Cloud Shell (a new tab, straight into PowerShell — choose No storage account required if it asks), the name of the registration (what your admins see in Entra; the same name updates it on a re-run), the command to paste, and + Add. The command downloads
New-EncaAppRegistration.ps1from this copy of ENCA, refuses it unless its SHA-256 is the one this build shipped, and uses the Cloud Shell session's own sign-in — no device-code prompt. It creates a single-tenant registration with this page as redirect URI, assigns you first and requires assignment, grants admin consent, and prints the client ID and tenant ID. Cloud Shell needs an Azure subscription in the tenant; without one, run the same script in PowerShell 7 with the Microsoft.Graph module. - After sign-in — ENCA first reads who owns the registration it signed in with, by the service principal's owner tenant, never the display name. A registration owned by another tenant — a multi-tenant app of another organisation that your tenant once consented to — is refused: ENCA signs you out and the sign-in card says which app and which owner. The account menu then has 🪪 App registration: check this one, or add my own…, which opens the wizard as a check of your own registration or as + Add a separate registration.
- When the registration is already yours the wizard opens as a check of the registration this copy signs in with, found by its application ID rather than its name. Running it adds this copy's SPA redirect URI if it is missing and keeps every redirect URI the registration already has, sets ENCA's delegated Graph permissions, and makes sure of the service principal and, if ticked, assignment and organisation-wide consent. Name, audience and secrets are not touched and no new connection is saved. + Add a separate registration instead switches to creating a new one in the same tenant.
- The four steps. ① What changes: the name (the handle the registration is found by — the same name updates it, a new name makes a second app), the exact SPA redirect URI this copy is served from,
localhost:8080if you want it, assignment required (you are assigned first, so it can never seal you out), admin consent for the organisation. ② The plan: what will be written, the likely impact, the recovery, and what ENCA needs from you now — consent, for you only, toApplication.ReadWrite.All,DelegatedPermissionGrant.ReadWrite.Alland, with assignment,AppRoleAssignment.ReadWrite.All, asked on the Create click. ③ The run, each step read back. ④ The default sign-in. - Default sign-in. This browser: saved as a ⚙ connection on the sign-in card and selected — Default there restores the shipped one. This deployment, on an Azure Container App: one button writes
ENCA_CLIENT_IDandENCA_TENANT_IDonto the container app with your own Azure rights, the way Branding settings writesENCA_BRANDING; a new revision rolls and every visitor signs in with your registration. Any other host: copy thejs/authConfig.local.jsblock, thedocker -elines or the Azure template parameters — SINGLE-TENANT.md step 3. - Recovery. Delete the registration (it sits in the deleted applications list for 30 days), or ⚙ Forget the connection and pick another. Nothing here can lock you out of the tenant. Demo mode simulates every step and writes nothing.
🏠 Overview NEW
The home page once a tenant is loaded. It reads nothing beyond what sign-in already read: everything on it is either pure over the loaded policy set, the baseline comparison that already runs at sign-in, or the last result a tool published.
- Flat dashboard — one statistics strip, findings beside policy context on desktop, and checks separated by simple lines. The dashboard uses line icons, no raised cards or shadows. It reads top to bottom: the snapshot header, then Worth a look first with Policy context beside it, then Your checks, the Configuration map and Upcoming changes. Evidence opens within its finding; About this assessment, the configuration map and the upcoming changes expand on request. On narrow screens the same information stacks in that order.
- Snapshot header — under the Conditional Access overview heading, the tenant and a line saying when the policies were read (details come from each check’s own run), then Policies loaded / Enabled / Report-only / Off; the four counts stay on every screen size and each opens 🗂 Policies filtered to that state. A line under them says when there is something to say: Reading policies… (the earlier snapshot stays on screen until the read completes), 0 policies loaded (a real answer, with Read again / Start from a baseline / Import), or could not be re-read with the failure, a Retry button and the snapshot still shown. The tool library sits below the Overview, closed until you press Show (the choice is remembered) — the rail and the header keep All tools one press away.
- Policy context — the tiles beside the findings. Report-only with the policies last modified 30+ days ago (the app knows when a policy was last modified, not when it entered report-only, and the tile says so — a policy without a date is counted apart as date unavailable, never as “no change”); Modified in 30 days with the newest change; the Baseline match with missing, outdated and conflicting policies; the Unique exclusion references by kind, every kind, so the breakdown adds up to the number. Configured is the word: who a policy actually reaches needs a membership read, and that stays behind the 🚪 Run button. The two policy tiles open 🗂 Policies (report-only, or the full list), with a smaller link beside each — validate with sign-ins into 🚦 and who changed what into 🕓; the baseline and exclusion tiles open 🧬 and 🚪.
- Upcoming changes — folded at the foot of the page, its summary line carrying the dates: the two dated retirements, each with its cohorts and dates (Microsoft-provided SMS & voice delivery: most users including internal guests on 1 February 2027, Global Administrators and external users on 1 July 2027; memberOf on 3 November 2026), the Microsoft source and the day it was checked. A published date says nothing about this tenant: the tenant impact reads not assessed with an Assess button until 📵 or 🧷 has run, and then shows what that run found, with Open.
- Worth a look first — the highest-severity findings the loaded policy set shows, ranked (severity, then tool); three show, View all keeps the rest one press away. Each line carries its evidence state — Policy snapshot (computed from the loaded policies, with the time), Partial context (a read the check needed was not made; such a finding can be understated, never invented), Run needed, or Previous snapshot (the result predates the last reload) — and opens in place to show what was observed, the policies it names with their original names, scope, grant logic (AND / OR is spelled out: OR means any one control satisfies the policy), exclusions and modification date, the suggested next step, a button into the tool (a 🛡 finding opens Checks on its Bypass tab, a 📐 line the CIS tab), and Show these policies, which opens 🗂 Policies filtered to exactly those policies. The filter shows there as a From the Overview chip with its count; it stays while you move between Home and 🗂 Policies and comes off with its ✕, or when you pick one of the four state counts in the header. A policy count on the tiles opens the policies it counts; sign-in validation and audit history are separate links. The 🛡 gap checks run over the loaded policies at once, on what sign-in already read: the named locations, authentication strengths and authentication contexts are kept from the sign-in read (with when they were read and whether they completed — a failed read stays failed, nothing retries and nothing widens the sign-in scope), so the provisional pass uses them and names only what is still unread — the Conditional Access settings, and any read that failed. Without a 🛡 run the findings still read Partial context. 📐 CIS runs provisionally only when both reads completed, with its licence read named as unread; otherwise it offers the run. The 🚪 app-exclusion comparison (which other enforcing policy still reaches an excluded app) runs over the policy set too. A provisional pass shows configuration checks — partial and no number, and About this assessment under the findings says what it used and what is still unread; the configuration score appears only from a 🛡 run whose context was read in full, with the run time, and is a count of findings, not effective protection.
- Unknown is never zero — a Licences result whose measures were not all read says Unknown · incomplete read, and an exclusion result without a state count says the bypass count is not established, rather than the length of a list.
- Configuration map — folded under the checks, open on request (the choice is remembered): the report-only review queue (oldest modification first — a review prompt, not proof of a forgotten rollout), recently modified policies (names and dates; who changed what is audit data), controls referenced by policies by state (policy counts that overlap and count no protected people), the snapshot context (the selected baseline with its release, source and how it came to be active; requirements matched, missing, outdated and in conflict; exclusion references as unique / occurrences / policies; effective user impact not checked until 🚪 has run; what sign-in read — named locations, authentication strengths and authentication contexts with their counts and read time, or read failed at sign-in with the error — and the Conditional Access settings and licence SKUs, which read not read until 🛡 Checks or 🎫 Licences reads them), and since your previous refresh — added, modified and removed definitions between the two reads of this session, compared by id and normalised definition with dates left out (No earlier snapshot on the first read; cleared on sign-out, tenant or demo/live change). Every section has a way out: Open the report-only list and Validate with sign-ins under the review queue, Show these policies and Who changed what under recent changes, Inspect policies and grant logic under the controls, Open the baseline and Open exclusions under the context, Show the changed policies and Audit history under the diff.
- Your checks — one row per on-demand tool: 🚪 Exclusion analyzer (effective bypasses), 🔍 Gap analyse (risky bypasses, and how many are unresolved) and 🎫 Licences (P1 seats short, or entitlements to assign with no purchase). Each reads Not run this session with Run check, Previous snapshot when the policies were reloaded after the run, or the headline with the run number, time and completeness (a partial read is marked), with Open and Run again / Rescan. Results are session-scoped and are dropped on a reload, a tenant change or a sign-out.
- What it deliberately does not do — no tenant-wide read at sign-in, no score without its evidence, and no work on the sign-in path it can avoid: the page paints once per snapshot (an unchanged page is not redrawn), the baseline comparison and exclusion collection are cached on the snapshot, and the configuration checks run after the first paint and are dropped if the snapshot moved under them. A dashboard that reads the tenant fourteen times before it shows anything is a different product.
Read-only. Draws only from what is already loaded.
🗂 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) — and act on what you have picked without leaving the screen.
One door. Until build 311 there were four tiles onto this one screen: 🗂 List Policies, 📄 Create documentation, 🗄 Backup (JSON) and 🎚 Set Policy state. Each of them opened this view, pre-selected one action and showed a toast naming a button that was already on the action bar. They are one tile now, and the three verbs are below as sub-sections — T02, T04 and T05 keep their numbers (see 🔢 Tool numbers). Nothing was removed and no route changed: the action bar, the per-card quick actions and every deep link from another tool work exactly as before.
- 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.
- Persona filter NEW (build 322) — the row of chips under the toolbar, one per persona this tenant actually has: Global, Admins, Internals, Externals, Guest users, Guest admins, the service-account ranges, and Other / unnumbered for policies whose name carries no CA number. Personas come from the CA number in the name, the same reading the grouped view and the search already use. Several can be on at once — Admins and E-Admins is a real question — and a chip you click again turns itself off; All personas clears them. Only the personas the tenant has get a chip, and the counts follow the state filter and the search, so a chip says how many you would get by ticking it rather than how many exist. A persona you have ticked keeps its chip even when the count falls to zero, so the filter never disappears under its own empty result. It narrows what the action bar acts on: Documentation, Backup and Gap analyse fall back to everything in view, and Select all ticks what is in view — so a persona filter is also a way to scope those to one persona.
- Select policies (checkboxes) — a green action bar appears: send the selection straight to Documentation, Backup, Assign groups or roles, Set state, Delete — or ⧉ Clone in builder: a new policy from the selected one in the 🏗 Policy builder, with the next free number in its range, in report-only (build 322).
- ✎ Edit on the card (build 322, R17). Open a policy and press ✎ Edit this policy — the first button in the card's action row at the bottom, ahead of What-if flow, Documentation and the rest: the six boxes — users, target resources, network, conditions, grant, session — become forms in the layout you were reading, the name a field under the title. A bar under the boxes counts the settings that change against the policy in the tenant, marks the boxes they are in, holds the state (switching to On needs a typed ON), and offers ± View diff and 🧪 Preflight. Save sends the changed sections whole and only those — a clause you were not looking at cannot be dropped — reads the policy back, re-reads the set and reopens the card; 🕓 Changes shows the same rows. Cancel discards; closing the card ends the edit. Users are edited inline for one policy here; 👥 Assign groups or roles stays the tool for many at once.
- Per-card quick actions — ✎ Edit this policy, ⑃ What-if flow, Documentation, Backup, Assign groups or roles, and Policy state, scoped to that one policy.
- 👥 Assign groups or roles — besides groups and directory roles, the wizard can aim at guests & external users: the external user types and the tenants they come from, set, added to or removed from a policy's include or exclude, for a selection or the whole tenant. A policy already scoped to different external tenants is left untouched with the reason rather than quietly rescoped. A directory-role change keeps a policy's guest and external user clause as it was. The full description is under 👥 Conditional Access groups.
- 🗑 Delete — the selected policies, after a JSON backup (ticked by default; if the download fails nothing is deleted), a typed
DELETE, and an extra tick when any of them is On, because deleting it stops enforcement at once. The run shows one row per policy as it goes — deleted, or why it was refused — with a Stop that halts before the next delete, and ends with a report listing every deleted policy with its id. The dialog stays open on the outcome. A deleted policy can be restored with its original id for 30 days in ♻️ Recycle bin; after that only the backup can recreate it, with a new id. - Clear (build 322) — takes away everything narrowing the view at once: the selection, an From the Overview filter you arrived through, the persona chips, the search box and the state filter. It greys out when there is nothing to clear, and hovering it names exactly what it would remove. Each filter still has its own control if you want to drop just one — the Overview chip's ✕, a second click on a persona chip, All on the state chips.
- 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).
- 👯 Duplicates NEW — the same policy twice. 🧹 Housekeeping reads VERSIONS and needs one in the name; a baseline whose names carry none (Joey Verlinden's) leaves a second import sitting beside the first, which is how Courseware ended up with
CA004-Global-…-AuthenticationFlowsas CA018 (Off, on the deploy group) and CA019 (Report-only, All users). A duplicate set is one name carried by two or more policies, the staging prefix aside. The version is part of the name, so v1.0 beside v3.0 is not a duplicate — that is Housekeeping's case; the same CA number under two different names is a number clash, which 🧬 Baseline reports. Each set is compared with the same engine as 🔍 Compare and gets a verdict: identical (nothing but the state differs), assignment differs (only who they reach), or needs review (conditions, controls, or a guest or workload scope a merge cannot express — never merged here). Each row says when that copy was created — to the minute, in your own timezone — and which of them is the newer one, because two runs of the same import land on the same day and the date alone answers nothing. A copy changed after it was made says that too. Merge is: pick the copy to KEEP, tick what to bring across from the other (include groups, exclusions, directory roles, the state — each tick says what it changes, and whether it widens who the policy reaches), and the rest are deleted. The copy that is On is never the one deleted while the kept one is not: that set is refused with the reason on it. A needs review set carries a Reviewed tick — the tool refuses to decide a security question for you, not to be overruled on one you have looked at: tick it and the set can be selected and merged like any other, with the reason it was held recorded in the report. The page behind the popup does not scroll while it is open, the list keeps its place when you tick something, and closing a comparison comes back to the set it was opened from. Then a plan — what is patched, what is deleted — a JSON backup of what goes, the typed DELETE, the run ledger and a Markdown report. A deleted policy is restorable for 30 days in ♻️ Recycle bin. - 🧹 Housekeeping — lists older versions when a higher version of the same CA number exists, including On and Report-only policies. Status, assignment and configuration differences are shown as review points. Compare opens a read-only, field-by-field view of both versions, with additions and removals highlighted; turn off Differences only to see matching settings too. Every row is a setting: a block one version leaves unset is compared setting by setting against the other (build 315), an authentication strength is shown as its name and id rather than as the strength's own combinations, and Graph's
@odataannotations — which name the policy's own id and made two policies on the same strength look different — are not settings and are left out. A cleanup candidate — an Off version with matching configuration and a newer On successor, or (build 330) an Off version beside the version the active baseline ships, whatever else differs, since an Off policy enforces nothing — can be selected straight away; any other row needs the Reviewed tick below first. Either way the delete goes through the same confirmation as 🗑 Delete (JSON backup + typedDELETE, a row per policy, Stop and a report), and the list behind it refreshes afterwards. Nothing is selected automatically. The button reads 🧹 Housekeeping (n), or (n · ⇄ m) when m version changes are waiting to be switched over. Names are compared without the version, written-v1.0.3orv1.0.3. ⇄ Switch over (at the top, when it applies) finishes a version change that stopped half-way — a newer version still Off while an older one is On or Report-only, as an import leaves it when Entra is slow to show the new policy. Tick the pairs: each new version takes its older version's state (or Report-only first, if you tick that), is read back, and only then is the older version switched Off. A pair whose names or scope differ can only be ticked after you have compared it. It also finishes half a switch: a newer version already On beside an older one still On only needs the older one Off. TypedSWITCH, a run ledger and a report; afterwards the older versions are Off and show up here as cleanup candidates. Reviewed: a row that differs from its newer version only in ways you meant — an exclusion added, a name tidied — has its own Reviewed tick, and ✓ Mark all reviewed ticks them all; a reviewed row can be selected for deletion, and ☑ Select all to delete selects every one that can. Still blocked, whatever you tick: an older version that is On or Report-only (switch it over first), incomplete details, or several policies at the highest version. Every delete still goes through the JSON backup and the typedDELETE. - 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.
Licence check before every policy write. Whatever writes a policy — ✎ Edit on the card, the 🏗 Policy builder, 👥 Assign, 🎚 Policy state, 📥 Import, a Checks fix, a restore from ♻️ Recycle bin — the tenant's active subscriptions are read first, and the write stops when the policy needs a product that cannot be shown to be there: Entra ID P2 for user or sign-in risk and Require password change, Workload ID for a policy on service principals, Intune for app protection, Defender for Cloud Apps for Conditional Access App Control, and Purview Adaptive Protection for an insider-risk condition. Adaptive Protection is recognised from the Insider Risk Management plans and from the suites that carry it — the Purview suites (Business Premium, enterprise, frontline), E5 Compliance, Microsoft 365 E5 and F5 Compliance. An edit is checked only for what it adds: a condition Entra already accepted on the policy is not held against it, so adding an exclusion to an insider-risk policy just saves. When a licence cannot be confirmed, the message lists the subscriptions that were read, so a mismatch is visible; nothing is removed from the policy to make it fit. Switching a policy Off is never held back, and a new policy is refused when the tenant has no free slot of the 240.
Browsing is read-only. The writing actions — ✎ Edit, 👥 Assign groups or roles, 🎚 Policy state, 🗑 Delete, ⇄ Switch over, 👯 Merge — each confirm before they touch the tenant.
📄 Create documentation folded into this tool — build 311
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.
🗄 Backup (JSON) folded into this tool — build 311
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.
🎚 Set Policy state writes to tenant folded into this tool — build 311
Switch selected policies between On (enabled), Report-only (logged, not enforced) and Off (disabled).
Writes the new state to the tenant after you confirm.
🗣 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
The funnel and its last stage. 🎫 Licence gap computed the same obligation this funnel's final stage already reports — every user a policy targets needs Entra ID P1, a risk condition needs P2 — and then said how to close it. It is that stage opened up rather than a second tool, so from build 311 it is the 🎫 Licences tab; T03 and T31 keep their numbers (see 🔢 Tool numbers) and 🎫 keeps its section below. One thing to know about where the strip lives: this tool IS 🗂 Policies' screen in its analyze view, so the strip is taken back out when you open 🗂 Policies — a tab offering to switch a tool you are not in would be worse than no tab.
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 scope → targeted 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.
- Targeting, not enforcement UPDATED — the stages say targeted by an enabled policy and targeted by an enabled MFA policy, because that is what an identity assignment proves. A policy that demands MFA of one app at high sign-in risk targets the user; whether it fires on a given sign-in depends on its own conditions, which is a 🧪 What-If question. The word enforced is reserved for scenario or sign-in evidence.
- Every bypass carries a verdict UPDATED — expanding a user shows, per bypassed policy, whether another enabled policy gives that person the same protection: ✔ Equivalent coverage, ◐ Partial coverage with the missing requirement named, ✖ No equivalent coverage, or ? Not established. Sharing one grant control is no longer enough to call a bypass closed — a policy requiring MFA and a compliant device is not replaced by one requiring MFA alone, and the row says which half is missing. Anything short of established equivalence counts as risky, because “could not tell” is not a reason to stop showing a bypass.
- 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 policy targets them” counts everyone no MFA policy targets 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.
- Cohorts NEW — users whose state is identical across every policy in the run are one row, the same way the Exclusion analyzer collapses identical exclusion patterns. A tenant of thousands reads as a handful of cohorts plus its outliers; findings sort first, so a cohort of one user with a risky bypass is at the top and the quiet crowd of 400 is one line at the bottom. Open a cohort to list the people in it. The cohorts follow the filters, so a search or a group narrows them too.
- The run strip and the exported header UPDATED — the result carries the run that made it: tenant, policy snapshot time, states, population, options, completeness and a run number. Reloading the policies, signing out, or switching tenant or between demo and live drops the result and its export, so nothing from the previous snapshot stays on screen; a snapshot that moves while a result is open turns the strip amber. The exported HTML report takes its header from that descriptor, so changing the scope without rerunning no longer rewrites the description of a file whose contents have not changed; stopping a run says Stopped, and a second run cannot start over the first.
- 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 (sign-in, user or insider risk) 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.
- Policies are kept apart by id — results are keyed on the policy id, not its display name, so two policies that share a name stay two columns in the Matrix, each header then carrying the start of its id, and each opens its own policy card. An included policy and an excluded one can no longer merge into one cell.
- 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.
🎫 Licence gap folded into this tool — build 311
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.
- Three measures, never one UPDATED — estimated demand (the identities your policies target), seats purchased (what the tenant owns) and assigned in scope (how many of those targeted identities actually hold the entitlement today). The bar draws purchased capacity against demand with a marker at the assigned count, because they answer different questions: a purchasing shortfall is something to buy, and targeted identities without an assignment are something to assign — often from seats already paid for. Before build 322 the bar drew seats and labelled them “targeted users licensed”, so it could read 100% directly above a list of unlicensed users.
- What you see — tiles for member users, estimated P1 demand, P1 seats purchased, P1 assigned in scope and the P1 gap (and the same four for P2 where a risk-based policy exists or P2 seats are owned). Under the bar the verdict keeps the two actions apart: a Licence gap of N users to buy for, or No P1 purchasing shortfall followed, where it applies, by how many targeted identities do not hold the entitlement — an assignment to make, not a purchase. The Markdown table carries the same four columns: estimated demand, seats purchased, assigned in scope and purchasing shortfall.
- The head states the population — member users tenant-wide, guests excluded, On and Report-only policies, insider risk counted as a P2 condition — so a Licences result and a scoped 🔍 Coverage run say plainly when they are answering the question about different populations.
- Bound to its run — the same run strip as 🔍: tenant, policy snapshot time, states, population, completeness and a run number. A policy reload, sign-out or tenant switch drops the count and its export, and a snapshot that moves under an open result turns the strip amber with rescan before quoting these numbers.
- One population for every number UPDATED — exclusions are resolved from groups and roles that can hold guests and service principals, so they are intersected with the member users before being subtracted from a member count. A tenant whose exclusion groups contain guests no longer reports fewer users in scope than it has gap users.
- Mailbox states say what was read UPDATED — a returned
userPurposeis named as such, with the note that a shared mailbox can still need licensing for size, archive or litigation hold and should stay sign-in blocked. A denied mailbox read is reported as exactly that — purpose not established, verify in the Exchange admin center — rather than being turned into “likely shared, never licensed”. No mailbox classification suggests removing Conditional Access from an account. - Seats are matched on service plans, not SKU names: any subscription carrying the
AAD_PREMIUM/AAD_PREMIUM_P2plan counts (EMS, M365 E3/E5, Business Premium…), a P2 seat counts for the P1 obligation too, and suspended or cancelled subscriptions are excluded bycapabilityStatus. 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, and an empty gap list over such a read is exported as no gap found in the scope that could be resolved, marked incomplete — not as everyone licensed. 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.
🛡 Checks
Three references, three tabs. 🛡 Best-practice & bypass checks, 📘 MS Learn checks and 🖥 Device reality check all answer one question — what is wrong with this baseline, judged against a reference — and each was its own tile with its own Refresh and its own Export MD. One tool with rule packs as tabs from build 311; T08, T07 and T30 keep their numbers (see 🔢 Tool numbers) and their sections below. A fourth pack, 📐 CIS Benchmark (T21), runs on the beta channel only for now and is not shown here. Each pack asks for its own permissions on its own ▶ run, never on opening the tool, so reaching 🛡 Checks never prompts for Intune scopes you were not going to use.
What is deliberately not here: one findings table across the packs. They present genuinely different shapes — a persona × control matrix, findings with buildable fixes, a four-tier CIS score with an L1/L2 filter, a per-policy-per-platform grid — and flattening them into one table with a Source column would lose information rather than share it. If that changes it will be its own release, not something that arrived behind a tab strip.
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.
- Under 🏢 SMB Basic the rows are its sections NEW — Base, Admins, Compliant network and Break-glass instead of the CloudFellows personas, each asked for what that section is for: Base MFA and the legacy block; Admins MFA, phishing-resistant MFA, sign-in frequency, the country block and the compliant network (never a compliant device — SMB tenants do not enrol admin devices); Compliant network the block outside it; Break-glass a phishing-resistant method. 🗂 Policies groups SMB policies under the same section names.
- Each finding has a severity and an explanation; a deployed-but-Off policy is flagged as not yet enforcing.
- ▶ Run checks — the tab waits until you ask, and the result stays until you refresh. A finding you opened stays open when you change the severity filter.
- Coverage is only counted when it is real. MFA for all users needs an All-users policy on All resources; one that asks MFA for selected apps only is its own high finding. Legacy authentication counts as blocked tenant-wide only when the block covers everyone, All resources and both legacy client types; a narrower block is reported with its scope.
- Grant “OR” is judged by what it lets through. MFA OR a compliant or hybrid-joined device is Microsoft's own template and reads low; MFA OR app protection or password change reads medium, because those can be met without a second factor; an OR with terms of use reads high — accepting terms is a click, not a factor. An authentication strength counts as the MFA side. An OR between device and app-protection controls only (MDM-or-MAM) is an accepted pattern.
- Guest authentication strength — raised only when every combination of the strength needs a method a guest can complete only at home (phishing-resistant, passwordless). A guest MFA policy on Require multifactor authentication is fine: an untrusted guest simply registers MFA here.
- Security info registration — no enabled policy protects the Register security information user action with MFA or a block, so whoever has a user's password can register their own authenticator for that account (medium; low when the policy exists but is not On). The remediation is Microsoft's template, with a Temporary Access Pass for new users.
- Device code flow — no enabled block on the device code flow for everyone on All resources (medium; low when the block is narrower or not On). The remediation names the Teams Android devices that still need it.
- Duplicate group names (Group Hygiene) — two or more groups referenced by Conditional Access that share a display name, each listed with the policies that use it: one persona's MFA policy can sit on one copy while an exclusion sits on the other.
- Allow-list blocks — an unconditional block on All users and All resources that excludes three or more groups is an allow-list. It is reported when it misses a group another active policy includes, or a group two or more other All-users policies exclude (the shared-device accounts, for instance), and when it blocks service provider, B2B direct connect and other external users, who can never be in a group.
- The persona matrix's Sign-in frequency column counts sign-in frequency only — a persistent-browser setting alone no longer marks a persona covered. Recommendations are written as where / change / why.
- Export MD — the findings as Markdown for a ticket or review.
Read-only.
📘 MS Learn checks folded into this tool — build 311
Checks policies against documented Microsoft Learn exclusions and limitations — break-glass, token protection, Teams Rooms, service providers, retired features.
- ▶ Run checks — the tab waits until you ask, like Bypass & Swiss cheese, and says what it will read: authentication strengths, cross-tenant access settings (the default and every partner), the Conditional Access settings, the authentication methods policy, the break-glass and shared-device groups, and how many guests sit in each group a policy includes. The result then stays when you switch to another tab and back — same severity filter, same open findings, same sub-tab. ⟳ Refresh re-reads the tenant and runs again; ticking Include Off re-runs; another tenant always starts fresh.
- Findings lists what conflicts with the docs; Suggested fixes holds the fixes they come with. Severity chips run from Critical to Info, Low included.
- 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.
- Trust is read from the default as well as the partners. Every guest answer that depends on cross-tenant trust — MFA claims, compliant-device claims, hybrid-joined claims — reads the tenant's default inbound trust and every partner row; a partner that sets nothing inherits the default. A tenant with no partner at all is judged on its default, so a compliant-device policy reaching every B2B guest while the default does not trust device claims is reported (and the matrix cell reads trust, not ok). Each finding says whether the missing trust is in the default or in a named partner.
- Guests & external users NEW (build 322) — six checks and a matrix, from Microsoft's own two tables for external identities. What a guest cannot satisfy at all: Require approved client app, Require app protection policy and Require password change are documented as not supported for B2B collaboration and B2B direct connect — the first two need a device registered in this tenant, and a guest has no password here. What needs cross-tenant trust: compliant device and hybrid joined device, because a device is managed only by its owner's home tenant. The authentication-strength trap: FIDO2 / passkeys, Windows Hello for Business, certificate-based authentication, Authenticator phone sign-in and OATH hardware tokens can only be completed in the guest's home tenant — in yours they have SMS, voice, Authenticator push and OATH software tokens and nothing else. So the built-in Phishing-resistant MFA and Passwordless MFA strengths are unsatisfiable for a guest unless inbound MFA trust is on and their home tenant has deployed the method; the check says which of the two you are missing, and never confuses "not configured" with "not read". Also: an authentication strength does not reach email one-time passcode, SAML/WS-Fed, Google or MSA guests at all, so for them the policy imposes no MFA requirement; a user-risk policy blocks guests it can never remediate, since the password reset it demands is impossible in your directory; and terms of use, sign-in frequency, persistent browser, app enforced restrictions and Conditional Access App Control are unsupported for B2B direct connect while working normally for collaboration guests.
Local guest users are deliberately left out of all of it — that type is an account in your own directory with UserType Guest, so it has your credentials and your management, and none of the cross-tenant limits apply. - The guest matrix NEW (build 322) — above the findings: the external user types this tenant's policies actually reach, against the controls those policies actually demand. Each cell is ok (satisfiable as configured), trust (only with inbound cross-tenant trust, which is not in place), blocked (documented as unsatisfiable — an exclusion is the remedy) or n/a (the control does not reach that type); a control nothing asks of a type is left blank, because "nobody demands it" is not the same as "fine". Click a cell to open the policies behind it in 🗂 Policies. Built from the policies already loaded and the cross-tenant settings the service provider checks already read — no extra permission and no extra call; when those settings could not be read the band says every trust answer is unverified rather than guessing.
- The shared-device matrix NEW (build 322) — below the guest matrix: Teams Rooms on Windows, Teams Rooms on Android / phones / panels, and Surface Hub, against the controls the policies that reach their resource accounts demand, read from Microsoft's own support tables. A policy reaches them through All users (unless it excludes the shared-device group) or by including that group, and only when it targets All resources or an app the devices use; a platform condition decides the family. ok supported · prompts supported but someone has to complete it on the device (MFA on Android devices) · blocked not supported — the device stops signing in · ? not in that device's documentation (Surface Hub documents only the grant controls). Sign-in frequency, persistent browser, token protection, terms of use and every session control are unsupported on Teams devices; blocking device code flow is fine for Windows rooms and breaks Android ones. Click a cell for the policies behind it. The card names the shared-device group it read (or says none was found, and then counts every policy on All users as reaching them).
Every blocked cell has a finding below it, per policy, naming the device families and the controls they cannot meet, and the fix depends on how the policy reaches them: through All users → exclude the shared-device group (🧰 Fix adds the exclusion); by including that group → it is the devices' own policy, so take the unsupported control out of it — excluding them from their own policy would leave them with none, so no exclusion fix is offered 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
- 🧰 Fix where the finding is NEW (build 323) — baseline tenant only. A finding with a fix carries 🧰 Fix N on its head and in its detail; the shared-device matrix carries 🧰 Fix these, which takes every shared-device finding at once; 🧰 Fix all sits in the toolbar. A finding's button applies that finding's change and nothing else. You confirm the list first. Each policy is changed in place — same policy, same ID, same state; only the listed change and the version in its name move — and a companion policy is created Off beside its original. The one exception: a policy still carrying Require approved client app is read-only in Entra, so it is rebuilt — created Off at the bumped version, then the frozen original deleted — and the confirm says so. Nothing else is deleted. The report that opens afterwards keeps every changed section as it was, which is the way back (✎ Edit on the card, or a PATCH of that JSON). The policies are re-read and the checks run again, so what was fixed drops out of the list.
- The shared devices' own policy (build 323) — a policy that includes the shared-device group is theirs, so the Fix takes the unsupported control out rather than excluding them — but only when that group is its only include, and only grant and session controls. A condition (insider risk, the device-code block) is never removed, and a control that is the point of the policy (compliant device, which Surface Hub cannot meet) is named and left for you to decide.
- 📰 Learn changes NEW (build 325) — the third tab; it needs no scan and reads nothing from the tenant. Once a week a GitHub Action reads Microsoft's own documentation repository (MicrosoftDocs/entra-docs) and lists what changed for Conditional Access. Your part is only to decide what each change means for ENCA, in three steps that the How this works card at the top of the tab repeats:
- Read — open an item and click see the diff for exactly which lines Microsoft changed.
- Decide — answer the one question on the item with a button. The answer is kept in this browser, and the status bar counts it as decided, not handed over yet; undo takes it back.
- Hand over — 📋 Make work order in the status bar gives you a file to hand to Claude in a session. Claude records your answers — then they leave the tab on every device and the GitHub issue — and builds the check changes you asked for.
- What needs an answer — sections marked answer needed: ⚠ A check may be out of date (a Learn page one of ENCA's checks is based on changed after the check was last read — is the check still right? ✓ Still correct or ✎ The check needs a change), New pages and What's new (should ENCA do something with it? + Make it a new check, ↗ Add to an existing check, ✓ Already covered or – Not relevant). Sections marked for reading — changed pages no check depends on, typo and link fixes, other services' notes — need nothing. A choice that orders or dismisses work asks for a note: what changed, which check, or why not.
- What each answer leads to — ✓ Still correct, ✓ Already covered and – Not relevant are no work: once handed over the item is recorded and leaves the list, and it comes back only if Microsoft changes the page again. ✎ The check needs a change, + Make it a new check and ↗ Add to an existing check are work: the work order carries each with its Learn page, the diff and your note, and it is built as a beta build with a queue item and a test.
- On the finding itself — a check whose page changed after it was last read carries ⚠ Learn changed on its head in Findings, and a line at the top of its detail linking the change; the advice below may be out of date until the page is read again.
- When — when the GitHub issue 📰 Learn changes (label learn-feed, in the ENCA repository) gets a new comment, or on a weekly round. The issue lists everything still undecided, is rewritten by every weekly run, gets a comment only when something new appears, and closes by itself when everything is recorded. If GitHub cannot be reached the tab says so and shows the copy shipped with the build. Pages outside that repository (Teams Rooms, Surface Hub, Defender, Partner Center, Zero Trust) are listed as not watched and are still checked by hand.
- Ready for the catalog (build 323) — the policies a Fix changed are marked ready for 🧱 Update the catalog on the Baseline screen. A band on this tab counts them and opens the catalog panel with them ticked, each saying which check it answers; the generated catalog source carries the checks, their Learn pages and the changes as its revision note. Clear empties the list; an entry also leaves by itself once the catalog has caught up with that policy.
- ✓ Accept with a reason (build 323) — a finding with nothing to fix automatically, such as the baseline-scopes one, can be accepted with a reason. It folds into Accepted under the findings, kept for this tenant; it reopens by itself when a different set of policies matches, and Reopen puts it back.
- Which way the policy is wrong NEW (build 322) — the external-identity findings come in two kinds and they need opposite fixes, so each one says which it is. 🚫 Blocks them: the control cannot be met by an identity this tenant does not manage, so those users are denied access — the policy is too strict, and the remedy takes them out of its scope. 🕳 Misses them: the control is not applied to them at all, so the policy asks them for nothing — too loose, and the remedy brings a requirement that reaches them, since excluding people a policy already asks nothing of changes nothing. That is why four of these findings begin “exclude the guest and external user types” and two begin “exclude nothing”. A finding that is genuinely neither carries no badge rather than a guessed one.
- Two verdicts on one strength NEW (build 322) — an authentication strength is never applied to guests who sign in with an email one-time passcode, a SAML/WS-Fed provider, Google or a Microsoft account, but what to DO about it depends on the strength. If it asks more than Require multifactor authentication does (phishing-resistant, passwordless), swapping the control would weaken the policy for everyone else, so the requirement is added beside it. If it asks no more — Microsoft's own table calls the built-in Multifactor authentication strength the same set of combinations that satisfies Require multifactor authentication, which is why the two cannot share a policy — then the answer is one swap on the policy you already have, and no second policy at all. The tool reads the strength's own allowed combinations to tell the two apart, and says nothing if the strengths were not read.
- Companion policies NEW (build 322) — one finding's answer is not an edit to the policy it names but a second policy beside it: an authentication strength does not reach guests who sign in with an email one-time passcode, a SAML/WS-Fed provider, Google or a Microsoft account, and Entra refuses Require multifactor authentication and Require authentication strength in the same policy. Its remediation says both halves outright — Exclude: nothing (no condition names the identity provider a guest used, so those identities cannot be carved out of the original, and the strength does not block them anyway) and Create: one policy, the same scope with the plain control — and 🧰 Fix builds it: the next free CA number in the original's range rather than a version bump, since it lives beside the original rather than replacing it. The original is never modified. Marked companion in the Suggested fixes tab, which counts how many fixes go beside their policy. The finding's remediation is laid out in three labelled parts — Exclude, Create and Why two — the last because Entra will not hold both controls in one policy.
- Guests reached through a group. A B2B guest can sit in any group, so a policy that includes a group holding guests — a guest-admin persona, a partner group — reaches them as surely as one naming the user type. For each group a policy includes (when the policy does not already reach every user), the run counts the members with userType Guest, and the guest checks and the guest matrix treat that policy as reaching B2B collaboration guests, naming the group and the count. Excluding the group on the policy removes it again. The summary says how many included groups hold guests; when some counts could not be read, it says guests reached only through those groups were not evaluated. The count cannot tell a B2B guest from a local account created with userType Guest, so the finding says members with userType Guest.
- A Temporary Access Pass is not a guest method. Microsoft's external-user table lists it in neither the home nor the resource column, so a strength such as Phishing-resistant MFA + TAP is not satisfiable by a guest here. A strength with no combination a guest can complete anywhere reads blocked in the matrix — not here, and not through trust either — and the finding says a TAP does not work for guests.
- External user types that no MFA policy reaches (high) — a guest MFA policy names some external types, and the ones it leaves out are required MFA by no other policy on All resources (typically because the All-users MFA policy excludes every external type). The finding sits on the guest MFA policy, and 🧰 Fix adds only the missing types to it (a new version, Off). A type that an unconditional block on All resources shuts out already counts as covered and is named as such; a block with a location, platform, client-app, risk, device or authentication-flow condition does not count. B2B direct connect is left out while it is blocked inbound; when it is open, the remediation warns that requiring MFA of it without inbound MFA trust blocks it.
- MFA for B2B direct connect without inbound MFA trust — a direct connect user has no account here and can only bring MFA from home, so a policy requiring MFA or a strength of them blocks them from Teams shared channels unless inbound MFA trust is on. Silent when direct connect is blocked inbound or trust is on for every tenant. The fix is usually the trust setting; excluding them is the alternative.
- User-risk policies and guests — reported only when the policy grants Require password change or Require risk remediation, or blocks: those a guest can never clear in your directory. A user-risk policy that asks for MFA is not a guest lockout.
- Token protection. Windows Cloud Login is recognised as a supported target, alongside Exchange Online, SharePoint Online, Teams, Azure Virtual Desktop and Windows 365. The unsupported-device filter the fix writes excludes only Entra-joined Cloud PCs, Azure Virtual Desktop session hosts, Power Automate hosted machines and Azure VMs — each label paired with
device.trustType -eq "AzureAD"— because hybrid-joined ones are supported and should stay protected. A filter that names those labels without the trust type is reported as over-broad. If Entra rejects the rule, the fix tries simpler filters and says which one it applied. Autopilot self-deploying devices, Surface Hub and Teams Rooms are left for you to exclude by name. - Require approved client app is retired. Since 30 June 2026 a policy carrying it is read-only in Entra: still enforced while On, but it can no longer be edited, and no new policy can use it. That includes the transition pattern approved client app OR app protection. The finding is high, and 🧰 Fix builds a copy with Require app protection policy in place of the retired control, state Off, for you to run report-only, switch On, then switch the old one Off.
- Defender mobile exclusion — asked only of broad blocks the Defender mobile app actually meets: a block limited to legacy authentication, to an authentication flow, or to platforms that leave out both Android and iOS is no longer flagged.
Checks are read-only; applying a fix writes, and produces a change report.
🖥 Device reality check NEW folded into this tool — build 311
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.
🏅 Identity Secure Score BETA a tab of this tool — build 323
Microsoft Entra's own score for the identity setup of the tenant (Entra ID → Identity Secure Score in the admin center), and the recommendations behind it — beside what ENCA sees in the policies loaded now.
- ▶ Read Identity Secure Score — the tab waits until you ask. It reads the daily score history and the recommendations, one after the other, and tries each again a few times when Microsoft answers "try again later"; the result stays for the session until ⟳ Refresh. Microsoft recalculates once a day, so a policy switched on today counts tomorrow.
- When one part does not answer (build 323) — the tab says which. Without the score history the score is added up from the recommendations themselves and shown as ≈, without a trend; without the recommendations the score shows alone.
- The score — the percentage, the points behind it, the change over the last 30 days and a line of the daily history; then how many recommendations are open, how many of those Conditional Access answers, and how many of THOSE are already built in this tenant and simply not enforcing.
- What ENCA sees — for the recommendations Conditional Access answers (MFA for administrative roles, blocking legacy authentication, sign-in risk, user risk, insider risk, MFA for everyone), the policies that do it and their state: On, report-only, built but Off, or none. It is ENCA's reading, next to Microsoft's, not a correction of it: where a policy is On and Microsoft still counts the recommendation open, the detail says Microsoft's own check decides.
- Filters — To address (the default), Conditional Access, In the score, All, Completed, and Accepted / planned / dismissed. Open a row for Microsoft's reasoning, value, user impact and steps with their links.
- On the home page — the score is a fifth figure beside the policy counts, and Microsoft recommends lists the open recommendations, Conditional Access first. Before a read, both offer ▶ Read.
- Read-only. The API can mark a recommendation planned, dismissed or risk-accepted; this tool never does — status changes are made in the Entra admin center. 📄 Export MD writes the list for the review record.
Needs DirectoryRecommendations.Read.All (read-only, admin consent once), asked on the click, and a role that can read it — Reports Reader, Security Reader, Global Reader, Security Operator, Security Administrator, Application Administrator or Cloud Application Administrator. The recommendations API is Microsoft Graph beta.
🤝 Cross-tenant access BETA a tab of this tool — build 323
How other Microsoft Entra tenants reach into this one — the cross-tenant access default policy and every partner entry — and whose MFA and device decisions this tenant has agreed to trust. The checks are those of Get-CrossTenantAccessReview.ps1 by Charlie Delmotte (m365-security-toolkit, MIT), reimplemented in the browser.
- ▶ Read cross-tenant access — the tab waits until you ask. It reads the default policy, the partner entries and each partner's cross-tenant sync setting; the result stays for the session until ⟳ Refresh.
- Default policy — guest access and Teams shared-channel (B2B direct connect) access inbound, and whether MFA, compliant devices and hybrid-joined devices from ANY other tenant are trusted. What this trust does to your policies lists the enabled and report-only policies whose MFA, authentication strength or device requirement a B2B user from such a tenant satisfies with a claim made at home.
- Partners — per entry: whether the tenant still exists, guest access inbound, MFA and device trust (marked (default) where the entry inherits it — trust is inherited flag by flag), cross-tenant sync inbound, automatic redemption, CSP and multitenant-organization markers, and Reaches your CA: how many policies its trusted claims satisfy.
- Findings — High, Medium and Info as the script grades them, with two deliberate differences: default MFA or device trust is High only when an enabled policy relies on it today (Medium while nothing does), and guests being invitable from any tenant is Info, because it is Microsoft's default.
- Check partner tenants still exist (on by default) — asks Microsoft's public sign-in endpoint for each partner tenant ID, the same lookup any sign-in page makes; no permission needed. A tenant this browser could not check reads not checked, never gone.
- Show partner names (off by default) — looks up each partner's name and default domain, asking for
CrossTenantInformation.ReadBasic.Allonce. Without it a partner shows its tenant ID, or the name its sync configuration carries. - Not covered — the toolkit's second script, Get-CrossTenantInventory.ps1, reads Exchange organization relationships (cross-tenant Free/Busy) through the Exchange Online PowerShell module. Microsoft Graph has no API for them, so that stays a PowerShell job.
- Read-only. Nothing is changed; cross-tenant settings are edited in Entra ID → External Identities → Cross-tenant access settings. Export MD and Export CSV write the findings for the review record.
Needs Policy.Read.All (part of sign-in) and a role that can read cross-tenant access settings — Security Reader, Global Reader or Security Administrator; Teams and Global Secure Access administrators cannot read the sync setting. Optional: CrossTenantInformation.ReadBasic.All for partner names.
🔑 Passkeys BETA writes to tenant a tab of this tool — build 323
Your policies require a passkey — can the people they reach actually get one? The tab starts from the policies: a policy requires a passkey when its grant is an authentication strength whose every allowed combination is phishing-resistant (passkey, Windows Hello for Business, certificate multifactor) with the passkey among them, and nothing else is offered beside it under OR. A Temporary Access Pass beside them does not change that — it is the way in for the first registration, issued per person and time-limited, so "Phishing-resistant MFA + TAP" counts as requiring a passkey. A policy whose target resources are None is listed but applies to no sign-in, so it is not counted. Then it asks the Passkey (FIDO2) authentication method whether the same people can register and use one.
- ▶ Read the passkey setup — reads the method (Microsoft Graph v1.0, where passkey profiles live), the authentication strengths with their advanced options, and the transitive members of every group and directory role involved, so gaps are counted in people. Directory roles count active holders; PIM-eligible admins join when they activate.
- Blocks — the method is disabled; people in scope are in none of the method's include targets; people in scope are in an excluded group (an exclusion wins over every include — no registration, no sign-in); the strength accepts only certain passkeys (AAGUIDs) and the passkey profiles those people get let none of them through; self-service setup is off; an Allow list with nothing on it. A report-only policy makes the same finding a warning.
- Warnings and info — key restrictions without attestation (the AAGUID list is then a guide, not a control); a policy on Register security information that demands a passkey while Temporary Access Pass is off (nobody can register their first one); part of a scope that could not be read; guests in scope (judged in 📘 Microsoft Learn); not opted in to passkey profiles (the tenant-wide attestation and key-restriction properties are deprecated, removed October 2027); three of three profiles.
- Policies that require a passkey — per policy: the strength and its combinations, how many people it reaches, how many of them cannot get a passkey. Current settings — Enable and target, Exclude, self-service and every profile, laid out as in the portal.
- ✎ Configure — the whole method: Enabled or Disabled, Allow self-service set up, include targets (All users or groups) with the passkey profiles each one gets, exclusions, and the profiles themselves — name, device-bound and synced, attestation, key restrictions Allow or Block with Microsoft Authenticator, Windows Hello and YubiKey 5 presets or any AAGUID. A ✎ button on a finding opens it with that change already made. On the right, before Save: the diff, what stops working (a removed AAGUID or an unticked type stops keys already registered — Microsoft applies key restrictions to sign-in, not only registration), what changes for people, and which blocking findings the save clears or opens — the same checks re-run on the settings as they would be. Losses and the opt-in ask once more. Opting in to passkey profiles is its own button: Microsoft does not let a tenant opt out.
- ↩ Restore — before every save the method as read is kept in this browser, for this tenant. ↩ Restore loads it back into Configure, where you see the diff and press Save. ⬇ Export JSON downloads the settings as read and the PATCH body without writing anything.
- Not written here — Temporary Access Pass and certificate-based authentication are reported, not changed; authentication strengths are changed in 🧩 Policy building blocks → 💪 Strengths.
Reading needs Policy.Read.All and Directory.Read.All (part of sign-in) and a role that can read authentication methods — Global Reader, Security Reader or Authentication Policy Administrator. Saving needs Policy.ReadWrite.AuthenticationMethod (asked once, on Save) and the Authentication Policy Administrator role.
📏 Naming BETA a tab of this tool — build 325
Every tool in ENCA groups policies by the CA number in the name — CA000–099 Global, CA100–199 Admins, CA200–299 Internals and so on — and until this tab none of them checked that the convention holds. It reads nothing: it works on the policies already loaded, and redraws each time the tab is opened.
- Number used twice — one CA number carried by policies whose names differ beyond the version. A version pair (CA204 v1.0 beside CA204 v1.1) is not flagged: that is 🧹 Housekeeping's case, and the same name twice is 👯 Duplicates'.
- Name ≠ range — the name says one persona (Admins, Internals, Guests, service accounts, DevOps, emergency access …) and the number sits in another persona's hundred. Guests may sit in Externals or Guest users.
- Name ≠ assignment — the policy includes the persona group of another range, for example an Internals policy that includes CAB-SEC-U-Persona-Admins. Checked only where the active baseline names persona groups (CloudFellows today); elsewhere the tab says it was skipped.
- No persona range — the number falls in a hundred no persona owns (CA1400 and up today). Unnumbered — no CA number at all, so every tool files the policy under Other; where the name reads as a persona, the next free number in that range is suggested.
- Free numbers — the unused numbers below the highest one in each range, folded and labelled as information. They are never counted as findings.
- Read-only. Renaming goes through ✎ Edit on the policy card. Export MD writes the findings for the review record.
No extra permission and no read — the policies loaded at sign-in are all it needs.
🎫 CAE & token protection BETA a tab of this tool — build 324
Continuous access evaluation and token protection are the two newer session controls, and both are easy to leave half-done. 📘 Microsoft Learn already judges each token protection policy — its apps, platforms, client apps and device filter — and every policy that switches CAE off, with a Fix. This tab shows coverage instead: the same treatment the persona × control matrix gives the classic grant controls. It reads nothing and redraws each time the tab is opened.
- Per persona — for every persona with active (On or report-only) policies: the token protection policies and their state, the resources and platforms each one names, and any policy that customises CAE. A policy with findings in 📘 Microsoft Learn carries a count; the findings themselves are not repeated.
- Resources that support token protection — Exchange Online, SharePoint Online and Teams on Windows, macOS and iOS; Azure Virtual Desktop, Windows 365 and Windows Cloud Login on Windows; Azure Resource Manager as a browser preview for the Azure, Intune and Entra admin portals. Each row lists the personas protected On and in report-only. The list is dated and checked against Microsoft Learn.
- Findings — no token protection for the Admins persona (Medium) or another persona of people (Info); token protection only in report-only; a token protection policy with Microsoft Learn findings; CAE switched off (fixed in 📘); strict location enforcement where the persona has no location-based policy, so it changes nothing; strict location enforcement that is live (check the IP-mismatch workbook); a supported resource no token protection policy includes.
- Read-only. Export MD writes the matrix, the resource table and the findings.
No extra permission and no read — the policies loaded at sign-in are all it needs.
🧪 What-If
Two directions, one evaluator. 🧪 What-If evaluates one described sign-in against every policy; ⚡ CA validator turns it around and enumerates every sign-in a policy implies. They were two tiles, and the plan for folding them carried a condition: they must not be able to disagree. They cannot now — build 311 removed a dead second simulator so both go through WhatIfEval.evaluate, and gave them one scope check (CaScope.of). One tool with a mode strip from build 311; T13 and T14 keep their numbers (see 🔢 Tool numbers) and ⚡ keeps its section below.
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.
⚡ CA validator folded into this tool — build 311
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.
- Scope it cannot decide is not guessed — with a target, a policy scoped to specific external-user types resolves for a user from their account (an internal account is never a service provider or direct-connect user), exactly as in 🕵 and ⚖. Where the scope still cannot be decided — a guest whose home tenant is not knowable against a list of named partner tenants — the policy is not simulated and is named in the Not simulated line under the results instead of being counted in or out.
- 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.
🔗 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 device limits and enrolment configurations (platform restrictions, Enrollment Status Page, Windows Hello for Business, notifications, co-management authority), compliance policies, configuration profiles (device, settings catalog, ADMX), scripts and remediations, app protection, app configuration, app assignments, Autopilot profiles, Windows update profiles, Windows 365 (provisioning policies and Cloud PC user settings — build 315) and Intune role assignments (build 315), in both directions: a group that is a member of a role assignment can manage devices, a group in its scope is what those admins may act on.
- 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
404is 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.
🕵 Who is … to CA BETA
One question, three subjects. 🕵 Who is Anna to CA, 🌊 Who is the wave to CA and ⚖ Compare users were three tiles over one picture. The code always said so: 🌊 resolves every member of a group through 🕵's own policy ladder, and 🕵 resolves its user through ⚖'s resolver. Pick the subject from the strip — a user, a group, or several users side by side — from build 311; T36, T37 and T18 keep their numbers (see 🔢 Tool numbers) and their sections below. Since build 311 all three also share one scope check, so the three subjects cannot disagree about the same person and the same policy.
The question a service desk gets is never “which policies target group X” — it is why can't Anna sign in, why does she keep getting the Authenticator prompt, 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 — and everything on it is hers: her policies, her devices, her sign-ins, never a tenant-wide list.
- At a glance — five 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, what happens to her if everything in report-only went live — locked out, extra prompts, no change, nothing for her, or no data — and her identity risk. - Policy numbers are the baseline's —
CA012from “(NEW)CA012-BLOCK-…”, the number the person reading knows the policy by — never ENCA's own running number. A policy whose name carries no number is shown in full. - 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. Filter chips: reaches her, excluded, not targeted, all; and by state.
- Unknown scope (build 322) — a fourth answer, next to reaches / excluded / not targeted, for a policy whose scope this read cannot decide: an external-user type or home tenant that is not knowable from here, or a membership list that came back incomplete. It gets its own amber chip when there is one (and no chip at all when there is not, so the chips always add up to All), a count on the policies tile, a place above Not targeted in the table, and a callout saying which half could not be decided. It is deliberately not guessed in either direction — Microsoft's own What If in the Entra portal is what settles it. Before build 322 a policy scoped to specific external-user types, such as Guests & external users: Service provider users, landed here for everybody and had nowhere to be shown; those resolve now, because a principal holding an account in this tenant cannot be a service provider or direct-connect user — neither has one. The type is read from the account’s own user type and the
#EXT#marker in its UPN, so no extra read is made, and the same answer is given in ⚖ Compare users, ⚡ CA validator and 🔍 Gap analyse, which share this scope check. - 🛡 Identity risk — what Identity Protection says about her now (at risk and at which level, remediated, dismissed, never flagged), the risk detections of the last 30 days, the risky sign-ins in the window, and only the risk-based policies that reach her — user risk, sign-in risk, insider risk — with the one that fires now on her current level named. A risk policy that does not reach her (not targeted, or she is excluded) is counted in one line and stays in the policies table, not here. The user-risk read is optional (IdentityRiskyUser.Read.All + IdentityRiskEvent.Read.All), offered as a button, and needs Entra ID P2 to be populated. Nothing here changes the tenant — dismissing or confirming a risk is done in the Entra portal.
- 💻 Devices she signs in from — one row per device in the window with what Conditional Access saw of it on the last sign-in: Compliant, Managed, not compliant, Registered, not managed or Unmanaged; sign-in and stopped counts, the apps, when it was last seen. A callout says when a compliant-device grant reaches her and sign-ins came from devices that cannot meet it. State is per sign-in, not per device — a laptop made compliant yesterday shows compliant on the sign-ins since.
- 📵 Retired control: Require approved client app — Microsoft retired that grant on 30 June 2026: a policy carrying it is read-only (it enforces until disabled, cannot be edited — the policies table marks it) and the day it is replaced by Require app protection policy or the control is simply removed, whoever satisfied it through the approved app — a mobile app on a device that is not compliant — is blocked unless an Intune app protection policy already reaches them. The card lists, per retired policy that reaches her, the sign-ins it let through on that path (apps, devices, last), and gives the verdict: blocked the day the control goes (no app protection policy seen for her yet — assign one first), ready for the replacement (a sign-in already shows an app protection policy satisfied on a non-compliant device), or not affected (compliant device, or no mobile app in the window).
- 🔐 MFA on her sign-ins — the answer to “she keeps getting the prompt on app X — which policy is it?”. A prompt the user completes is a successful sign-in, so it never appears in the stopped list; this card does: per app, the applied policies that demanded MFA (result success with an MFA or authentication-strength grant), how many sign-ins required it, and — from the record's own authentication steps — how many were a fresh prompt versus a claim already in the token, from how many devices, with the last fresh prompt. Then it says why: fresh prompts spread over many devices (a non-persistent VDI pool is one prompt per host), a sign-in frequency or non-persistent browser session reaching her, or every requirement satisfied by the token — in which case the prompt she sees is per-user MFA, security defaults or the app, not Conditional Access. Hunting rows carry the requirement but not the steps and count as not known; the Entra log source tells them apart.
- Sign-ins Conditional Access stopped — her rows from the log the way 🚦 Sign-in failures shows them: when, app, client, device state and place; under each policy the control it demanded; under the result the error code in words — MFA demanded and not completed (50074), compliant device required and this one is not (53000), device authentication demanded (50097) — 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”). Three outcomes that look alike are told apart: out of scope on her sign-ins (the policy was evaluated on every one of her sign-ins and its conditions never matched — no legacy client, no risk, not that app — so going live changes nothing for her as she signs in today), no data (the policy appears on none of her records at all: created after the window, or the source dropped its verdicts — never safety), and a real verdict.
- 🔑 What she signed in with (build 315) — at the top of the 🔐 card: the methods she used across the window (Password, Microsoft Authenticator, a passkey, an MFA claim already in the token) and the authentication strengths she was held to, from each sign-in's authentication steps — the portal's Authentication Details tab. A sign-in that asked for MFA or a strength and got a password only is called out in red with the app and the newest case; when the strength was phishing-resistant and no passkey, FIDO2 key, Windows Hello or certificate appears in the window, the callout says so: the method is the fix, not a policy. The third tile reads Asked, not given for those sign-ins, and the stopped table prints 🔑 what was used under each result (the steps in the tooltip). Hunting rows carry the requirement and no steps, and say so.
- Every card folds on its heading — click to fold or unfold; the choice is remembered per card, so the next user or a rescan keeps your layout.
- Buttons out — ⚖ Compare (adds her, ready for a colleague), 🧪 What-If (user filled in), 🔗 Analyzer (everything outside Conditional Access that points at her), 🌊 the wave she is in, Markdown brief for the ticket with every card, CSV of the policy table.
Read-only. Memberships and policies come from what ENCA already holds — the same include/exclude resolution as ⚖ Compare users. Sign-in source: the segment in the toolbar is shared with 🚦 Sign-in failures, 🎚 Report-only impact and 🌊 the wave — Entra sign-in log (interactive, AuditLog.Read.All), Entra + non-interactive preview, Defender hunting, or Hunting + non-interactive (ThreatHunting.Read.All, Entra ID P2) — see 🚦 for the trade-offs. On the Entra source only this user's sign-ins are read (server-filtered, with a 5,000-event safety cap), or the window another tool already read is reused when it was not capped. Registered MFA methods and identity risk are optional extra reads, offered as buttons and never required. Licence comes from the same verdict 🎫 Licence gap uses.
🌊 Who is the wave to CA BETA folded into this tool — build 311
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. Policy numbers are the baseline's, as in 🕵.
- 📵 Retired control across the wave — the same check per member: how many satisfy a reaching approved-client-app policy through the app on a non-compliant device, how many of those already show an app protection policy working, and the red rows — the members who are blocked the day the control goes unless an Intune APP policy reaches them first. Every name opens 🕵.
- 🛡 Identity risk across the wave — on demand, the Read identity risk button in the head: Identity Protection's user risk for every member (one read each, twenty to a batch; IdentityRiskyUser.Read.All, Entra ID P2), joined to the risky sign-ins the members already have from the window and to the risk-based policies aimed at the wave. Three tiles — members at risk by level, members with a risky sign-in, risk policies firing now on whom — the risk policies with how many members each reaches, and a table of the members that are flagged, remediated or had a risky sign-in. Before the read, a callout already names the members with a risky sign-in in the window.
- 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.
- ■ Stop — the read can be stopped. Stopped while the group is being read, nothing partial is shown (a half member list would report exclusions that are not there) and the read is offered again; stopped during the sign-in window, the group half stays and the note says the log half is missing. A running Graph call cannot be cancelled, so the stop lands after the query in flight.
- Every card folds on its heading, remembered per card.
Read-only. Referenced group and role memberships are fully paged. Wave analysis reads up to 25,000 members, with a lower per-run limit when needed to stay within two million user-policy evaluations. The screen and exports identify partial membership or sign-in evidence; incomplete evidence cannot produce an activation recommendation. The sign-in window is shared with the other log tools, including the P1 non-interactive preview. Licence per member uses the same verdict as Licence gap.
⚖ Compare users folded into this tool — build 311
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), not targeted (·), or ? when the scope cannot be decided from what was read (the same fourth state as 🕵’s Unknown scope; a policy scoped to specific external-user types resolves for any account in this tenant). 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.
📊 Deployment dashboard BETA
T54. The TUNO T28 rollout dashboard carried over to Conditional Access: the waves there are the CAD-SEC-U-DG-* deploy groups here. One page, reads only, and the questions it answers top to bottom:
- Covered. Every enabled user in the tenant (members and guests, one read of
/users) against the deploy groups' members, transitive. The strip at the top says how many are in a deploy group and how many are left out of every one — the tenant total is what makes that a number. Disabled accounts are counted apart and never as left out. - Route in. Direct member, or nested through a group inside the deploy group.
- Policies on the deploy groups. Every policy that includes a deploy group, by state — enforced, report-only, off. Report-only on a live group is the go-live question.
- Exclusions, a pie. The deploy-group members an exclusion takes back out of at least one enforced or report-only policy on their group, by what: BreakGlass, a
CA*-Exclusiongroup, another group, a direct user exclusion, a directory role. A person with several routes counts once, by the first in that order. Off policies' exclusions are not a hole. - Group by group. The same four side by side, one row per deploy group, with go-live readiness — 🌊 Who is the wave's verdict for the report-only policies on the group: locked out, prompted, unchanged, silent (no data, never safety) — and needs a look: in both INT and EXT, disabled members still in a group, a standing bypass through an exclusion group on an enforced policy. A group the tenant does not have is said; 👥 CA groups ② Create makes it.
- Left out, the one list this page makes itself: each left-out user, her UPN and type. Which group she belongs in is the deployment's own decision, made in 👥 CA groups — this page only says she is in none. Every other count is a drill-through: a member count, an excluded count or a readiness pill opens 🌊 Who is the wave with that group picked and the filter pre-set, a name opens 🕵 Who is Anna to CA, a group name opens its row in 👥 CA groups. The per-member answer already has a home; one list in three places would drift.
- Exclusion groups on the policies, folded under the table: every group the policies on the deploy groups exclude, its members, which policies use it, how many of its members are in a deploy group — the largest hole and any orphan (members, no policy) marked.
It reads the tenant and nothing else. The deploy groups are whatever the tenant calls CAD-SEC-U-DG-* (one read), plus the names the active catalog gives its deploy groups, shown as not in this tenant when they are missing. The page does not read 🌍 Fill from regions' region list, its ⚙ Settings or its rules — the two tools share the tenant and nothing else.
Readiness is behind a button. The users, groups and exclusions come back in seconds; 🌊's read is the shared sign-in window plus a member-by-policy evaluation per group, minutes on a large tenant with a cold cache. The page is useful without it, so 🌊 Read readiness runs it when you ask — for the groups that have a report-only policy on them — and the strip says when it is in. It asks for AuditLog.Read.All once, the same as 🚦.
Export HTML writes the whole page as one self-contained file — no script, no sign-in, light and dark by the reader's system, the left-out list included — for people who have no ENCA. It is a snapshot and it names users; share it like the tenant data it is. Export CSV gives one row per deploy group and one per left-out user. Reads: User.Read.All, Group.Read.All and Policy.Read.All (already granted); readiness adds AuditLog.Read.All. Nothing here writes.
🫥 Apps with no service principal BETA
Conditional Access can only name what exists. An app that signs in but has no service principal in the tenant — a multi-tenant SaaS nobody consented to, a Microsoft first-party client that was never registered here — is not in the app picker: it can be neither included nor excluded by name, and only a policy on All resources reaches it. Microsoft Graph gives one row per app for the past 30 days in a single call; this tool diffs that list against the tenant's service principals and reports what is left.
- Five tiles — apps with no service principal, how many have no Conditional Access at all (nothing enabled reaches them, not even All resources), how many depend on conditions, how many are reached the moment they exist (enforced or blocked), and how many are phantom exclusions.
- Folded rows (build 315) — one line per app: name, sign-ins, verdict, and the counts of what the policy columns hold (1 would · 9 may · 0 excluded by · newest sign-in). Click a row for the full detail — the would / may / excluded-by pills and the newest sign-in; ⊞ Expand all and ⊟ Collapse all above the table set the default for every row, and a click on a row is an exception to it. The state survives a filter, a search and a rescan.
- Once it exists — the 🧪 What-If verdict for an All-users sign-in to this app with nothing else known: would apply (the policy and its grant), may apply (a policy whose other conditions — location, risk, platform, client app, a group scope — the scenario cannot decide, with the reason), or nothing.
- Excluded by — a policy that already excludes the id. That exclusion protects nothing today and goes live the moment the app is consented; 🚪 Exclusion analyzer flags the same ids from the policy side.
- Newest sign-in — after 📖 Read evidence: when, who, the client, the Conditional Access status and the policies Entra recorded as applied. Evidence, never a prediction; where it disagrees with “would apply”, the sign-in is right.
- Names — the tenant has no service principal to name these apps by, so Microsoft's first-party ids resolve from ENCA's built-in map (tagged Microsoft); anything else is “Unknown app” with its id.
- Exports — Markdown for the report, CSV with one row per app id for whoever registers them. ENCA does not generate PowerShell: registering a service principal is a decision per app, made in the portal or by a script someone wrote on purpose.
Read-only. GET /beta/auditLogs/signInEventsAppSummary (delegated AuditLog.Read.All, asked once on the run click; Reports Reader or Security Reader; a fixed 30-day window, at most 1,000 apps — the tool says when it hit that cap) and GET /servicePrincipals. The sign-in evidence is the shared sign-in window. An app that signs in only non-interactively still appears in the summary; whether it appears in the evidence depends on the sign-in source chosen in 🚦.
🕓 Changes
Two sources, one question. 🕓 Change audit and 📉 Drift watch were two tiles asking what moved — of the Entra audit log and of a snapshot file you keep — over one diff engine (Audit.diff, which Drift watch already imported from here). They are one tool with two tabs from build 311; T16 and T28 keep their numbers (see 🔢 Tool numbers) and their sections below. The two bodies are deliberately not merged into one: a rolling 30-day timeline of who edited what and a two-point comparison against a file are different shapes of answer, and the tab strip is what says which you are reading.
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.
- Exclusion group membership — every add and removal on a group that a policy includes or excludes, read as the two exact activities (Add member to group, Remove member from group) for the last 30 days: who, when, which policies the group feeds, and flags — widens a bypass (an add to an exclusion group), by an app, off hours (outside 07:00-19:00 on a weekday, your clock). A change the directory refused, such as adding someone who is already a member, is shown as refused and does not count. The log keeps 30 days; whether the person who added is an owner of the group is not checked.
- Field-level diff — expand an entry and you get the fields that actually moved (
state: report-only → enabled, a group added toexcludeGroups), 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.
📉 Drift watch folded into this tool — build 311
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.
🛡 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-ExclusionsplusCAB-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-BreakGlassis 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 getsCAB-SEC-RMAU-BreakGlass, deliberately without an-Exclusionssuffix 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-EmergencyAccessis 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_Access1is 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-Exclusionsholds 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-ExceptionsandContractors-NoMFAmatch 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 a tenant's name — the baseline's own groups are the one exception, and they are not a guess:
CAB-SEC-U-Persona-AdminsandCAD-SEC-U-DG-INTname their persona in words the wayCA101names it in a number, and are read the same way. Everything else is left alone, 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-Exclusionis 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.
🧭 Workspaces & navigation
From 2.0, once you are signed in, ENCA is laid out as workspaces: a rail of shortcuts on the left, a branded header across the top, a strip of open tools under it, and the tool itself below. The tools, their data and their confirmation steps are the same as before; this is how you move between them.
- Workspaces 01 and 03 NEW (build 326) — the chip beside the wordmark says which side you are on: 01 · Conditional Access, every Conditional Access tool as before, or 03 · Entra Tools, the directory objects every workload leans on — 👥 Groups (the same tool as Conditional Access groups, opened on All groups, with a CA or GSA badge on every row it can place), 🌐 Global Secure Access and the ⏳ temporary tools (🛰 Egress & location impact, 📵 SMS & voice retirement, 🧷 memberOf retirement). Press the chip, the 01 ⇄ caption at the foot of the rail, ⌘⇧1 / ⌘⇧3, or ⌘K → Switch to. The header colour follows the ENCA mark: deep green with a gold line on 01, the beacon green with a pale line on 03. A tool opened from the other side switches sides with it; since build 330 UPDATED the tab strip is shared by every workspace: open tools stay in the strip wherever you are, a tab from the other workspace carries its number (01, 03), and clicking it takes you to that side. Since build 330 UPDATED the 03 overview is compact: three groups — 🧱 Directory & apps (Groups, Entra device cleanup, App access review), 🛡 Security posture (Securing M365, Global Secure Access) and ⏳ Temporary tools — as small cards three to a row, and Help as one row of chips, so the whole library fits on one screen.
?ws=entraopens on 03, and the last side used is remembered in this browser. - 02 · PIM-buddy BETA — listed in the chip's menu so the numbering reads 01 · 02 · 03, marked beta and not selectable: PIM-buddy is on the beta channel only and none of it is in this build.
- The rail — Home, then the tools in the order the work happens: Policies, Sign-ins and Who is… (what exists and who it reaches), Checks (what is wrong with it), then Baseline, CA groups and Building blocks — 🧩 Policy building blocks (what to change). Below the divider, All tools and Help. The tool you are in is highlighted. On a narrow window the rail becomes a bar along the bottom with fewer shortcuts; the rest stay under All tools.
- All tools — in the rail and in the header. Opens the tool library: every tool in its group, searchable by name, task or tool number (
T14). - The header — your logo on a round medallion hanging over the header's lower edge above the rail (a wide wordmark logo, or a narrow window, keeps the flat logo), the product name, and the account button naming the tenant, with Demo when you are on sample data. The account menu holds the connected app registration, Theme, Close all workspaces and sign-out. Header text switches between dark and light to stay readable on whatever colours the branding sets.
- Home — the 🏠 Overview, your Recent tools (the last five opened in this session; they reset when you sign out or switch tenant) and the full tool list, folded until you press Show (remembered in this browser).
- Open tools as tabs — every tool you open gets a tab in the strip under the header, after Overview, and keeps its filters and selection while you work elsewhere. Hover a tab for its × to close it. The browser's Back walks through the tools you visited instead of leaving the site, and closes an open dialog or full-screen panel first. Each tool keeps its own scroll position.
- Tabs inside a tool — tools that host others (🚦 Sign-in log, 🕓 Changes, 🔍 Gap analyse, 🕵 Who is … to CA, 🧪 What-If, 🛡 Checks, 🧩 Policy building blocks, 🧬 Baseline) show their own tab strip under the title, and coming back to one returns you to the tab you left. The active tool, the active tab and the matching rail shortcut have a solid, contrasting fill in both themes.
- Fold the head — every tool's title ends in ▾. Press it and the description underneath folds away; the title and its chips stay and the toolbar moves up. ▸ brings it back. The choice is remembered per tool in this browser.
- List and detail — 🗂 Policies in list view opens a policy in an inspector beside the table, with Settings, Assignments, Conditions, Controls and Definition tabs, Open wide & actions for the full detail and its actions, and Back to results. The row you opened stays highlighted while you tick, filter or search. 🕓 Changes, 🚦 Sign-in log, 🧩 Policy building blocks, 🚪 Exclusion analyzer and 🔗 User or Group analyzer use the same results list with a detail panel, a slider to resize the list, Open wide / Restore split view, and detail tabs where there is evidence to show.
- Scrolling — the list and the detail scroll on their own, pinned under the toolbar, so the list stays in view while you read a long detail; at the end of a pane the page takes over, so the rest is never out of reach. A newly picked item opens at its top. Below 900 pixels wide, list and detail become one column with Back to results.
- Resize any column NEW — every data table (🗂 Policies, 👥 CA groups, 🧬 Baseline, the Checks findings, sign-in and change lists, reports on screen …) has a grip on the right edge of each column header. Drag it to make the column wider or narrower; double-click it to fit the column to its widest entry on one line. Widths are remembered per table in this browser, and a resized table shows ↺ in its last header to go back to the tool's own layout. Matrix grids (persona × control, members × groups, the exclusion grid) keep their narrow columns and are not resizable.
- 🗂 Policies list — uses the full width: the policy name gets the largest column and stays on one line (the full name is in its tooltip, as are the complete Users, Target resources and Grant lists), State and Modified are as wide as their values. On a narrow window the table scrolls sideways instead of wrapping.
- Flat icons — the emoji in navigation, headings, buttons and chips are shown as matching line icons in the theme colour. Only the screen changes: policy names, exported files and reports keep their original text.
Presentation only — nothing here reads from or writes to the tenant. Recent tools last for the session; folded heads and the tool list on Home are remembered in this browser only.
⌨️ 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
gapfinds Gap analyse andbpbcfinds 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 just7, 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-number — T01 to T60 — 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
T01–T04are 🗂 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 —
T61is next.T48–T53,T55andT58belong to tools on the beta channel only. - Folded tools keep their number, and the mapping is published here. A rule that says a number is never reused is only worth something if you can still find out what the number means. Twenty-one numbers have no tile of their own:
T02📄 Create documentation → 🗂 Policies, action bar (build 311)T04🗄 Backup (JSON) → 🗂 Policies, action bar (build 311)T05🎚 Set Policy state → 🗂 Policies, action bar (build 311)T11🧩 Baseline (Joey Verlinden) → 🧬 Baseline, catalog picker (build 311)T26🎚 Report-only impact → 🚦 Sign-in log, Report-only impact tab (build 311)T38🛂 Session controls → 🚦 Sign-in log, Session controls tab (build 311)T28📉 Drift watch → 🕓 Changes, Snapshot file tab (build 311)T29📖 Baseline guide → 🧬 Baseline, Deployment guide tab (build 311)T22🎫 Authentication contexts → 🧩 Policy building blocks, Contexts tab (build 311)T23💪 Authentication strengths → 🧩 Policy building blocks, Strengths tab (build 311)T24♻ Recycle bin → 🧩 Policy building blocks, Deleted tab (build 311)T25📜 Terms of use → 🧩 Policy building blocks, Terms of use tab (build 311)T07📘 MS Learn checks → 🛡 Checks, Microsoft Learn tab (build 311)T21📐 CIS Benchmark → 🛡 Checks, CIS 5.2.2 tab — not part of this build at all: it runs on the beta channel only, and its number is listed here because a number is never reusedT30🖥 Device reality check → 🛡 Checks, Intune reality tab (build 311)T13⚡ CA validator → 🧪 What-If, Every simulation mode (build 311)T37🌊 Who is the wave to CA → 🕵 Who is … to CA, A group subject (build 311)T18⚖ Compare users → 🕵 Who is … to CA, Compare users subject (build 311)T31🎫 Licence gap → 🔍 Gap analyse, Licences tab (build 311)T35📞 Teams devices → 👥 Conditional Access groups, the 📞 Rule action on the TeamsSharedDevices row (build 311)T41🏗 Policy builder → 🧩 Policy building blocks, Builder tab (build 322 — born there, never a tile)T42🏅 Identity Secure Score → 🛡 Checks, Identity score tab (build 323 — born there, never a tile)T43🤝 Cross-tenant access → 🛡 Checks, Cross-tenant tab (build 323 — born there, never a tile)T44🔑 Passkeys → 🛡 Checks, Passkeys tab (build 323 — born there, never a tile)T45📏 Naming → 🛡 Checks, Naming tab (build 325 — born there, never a tile)T46🎫 CAE & token protection → 🛡 Checks, CAE & tokens tab (build 324 — born there, never a tile)T47🤖 Workload identities → 🚦 Sign-in log, Workload identities tab (build 324 — born there, never a tile)
- The newest two —
T40↗ Guided rollout is a tile of its own;T41🏗 Policy builder is the one number that never had a tile: it was born as the Builder tab of 🧩 Policy building blocks, and is listed above with the folded tools for that reason. - 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, 🗺 Roadmap and 🔑 Permissions in this session carry none, deliberately. They are the app describing itself and its session 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.
🚦 Sign-in log
One window, three questions. Until build 311 these were three tiles — 🚦 Sign-in failures, 🎚 Report-only impact and 🛂 Session controls — and the first two already read the SAME window through the same cache and the same consent, so the second tile you opened re-used what the first had read and said so in a line nobody should have needed to see. They are one tool with tabs now; T17, T26 and T38 keep their numbers (see 🔢 Tool numbers) and their sections below. The range picker, the sign-in source and the search box stay per tab, because the three ask different things of the same records.
Which sign-ins Conditional Access failed and which policy did it — read from the Entra sign-in log, grouped per policy.
Sign-in source. The log tools share a source selector. Entra sign-in log reads interactive user sign-ins through the stable Graph endpoint on P1/P2. Entra + non-interactive · preview explicitly includes background user sign-ins through the beta endpoint and uses the same AuditLog.Read.All permission. Defender hunting and Hunting + non-interactive remain optional: they need a populated Entra hunting table, P2 and the relevant Defender access. P2 alone does not establish Defender access. Each source has row/response limits; a capped or failed read is incomplete. The shared window is tied to tenant, source, period and policy snapshot. The screen shows the observed event interval separately from the requested period.
While reading. Progress starts before Microsoft's first response, keeps the elapsed time across phases and shows server retry waits. A page limit is never presented as a completion percentage. Report-only impact shows provisional results from the first useful page. Stop cancels supported reads or stops at the next safe read boundary; a stopped read cannot become a complete result. Larger analyses run in background workers. Coverage analysis accepts up to two million user-policy evaluations per run: choose a deployment group or named users for larger tenants.
P1 support and optional products. Policy and directory tools use P1 as the supported baseline. Identity risk, Defender evidence, Intune checks and Governance features keep their own product, consent, role and source requirements. Import preview identifies premium requirements; policy writes and restores recheck active subscription evidence and policy capacity. Unknown entitlement blocks the write without removing premium conditions. Subscription discovery does not prove that each affected person is correctly licensed. Actual P1/P2 tenant acceptance is recorded separately in Help → Waiting for production.
- 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.
- 🔑 Signed in with (build 315) — what the person authenticated with, from the record's authentication steps (the portal's Authentication Details tab): Password — Phishing-resistant MFA required, not provided (MFA required in Azure AD), Password + Microsoft Authenticator — MFA satisfied, or MFA by a claim already in the token. A password where MFA or an authentication strength was required is the gap that explains the failure without touching a policy: it is red on the card and the per-policy detail line, counted per policy and in the header (3 without the MFA asked), and it has its own filter chip. The sign-in card lists the steps one by one. The CSV carries
authRequired,authUsedandauthGap; the Markdown export a Signed in with column. Hunting rows carry the requirement and no steps, and say so. - What the browser can hold — the hunting sources have no server-side window cap, and on Hunting + non-interactive an enforced read on a large tenant is dominated by legacy-protocol blocks of service accounts retrying every few seconds: hundreds of thousands of sign-ins for one week. Every one of them was shaped into a record and kept, and the tab died. A read that keeps its rows now stops at 50 000 sign-ins and says so — window stopped at 50,000 sign-ins — more than this window holds in the browser. Read a shorter period, or switch the source off non-interactive. That is a partial window, labelled as one, in place of a lost tab; narrow the period or drop non-interactive for a complete answer. 🎚 Report-only impact on the hunting sources is not affected: its buckets are summarised by Microsoft and streamed, so it holds nothing of the window.
The ceiling is on the total the tab keeps for one window, on either hunting source, and it is the same for every tool that reads the shared window — 🕵 Who is … to CA, 🌊 the wave and 🛂 Session controls name a stopped window in the same words. It is not the other short-window reason: truncated — a day hit the hunting row cap means one slice of a day could not be read whole, and truncated at 10,000 sign-ins is the Entra sign-in log's own cap. Each says which it is, because each asks for a different fix. No query, permission, verdict or export changed; the newest sign-in is still first in the list and in the exports. - 🧪 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 folded into this tool — build 311
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
LowMediumUserRiskreporting “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.
- Sign-in source. The segment in the toolbar is shared with 🚦 Sign-in failures, 🕵 Who is Anna to CA and 🌊 the wave: the Entra sign-in log (interactive only, capped at 10,000, AuditLog.Read.All), Defender hunting (the same sign-ins from
EntraIdSignInEventsthrough advanced hunting on Graph — ThreatHunting.Read.All, no cap, 30 days, Entra ID P2 for the table to be populated) or Hunting + non-interactive, which adds the token refreshes and background client sign-ins the Graph list never returns. A forecast that counts non-interactive sign-ins is a different number — the header says which source the verdicts came from. The hunting read is one query per day, halved on size, and narrates every query; two tools asking for the same window at once share one read. Full detail under 🚦. - Require app protection policy cannot be evaluated in report-only. Microsoft documents that the control reports Report-only: Failure in report-only mode even where it passes once enforced, so a staged policy with that grant shows would-deny counts that are not denials. The tool says so under such a policy; enable it for a pilot group to get the real answer.
- 💾 Keep on this device (build 315, R58). The button after the sign-in source. Off, every read asks Microsoft for the whole window. On — per tenant, after a dialog that says exactly what is kept — the browser keeps the report-only verdict buckets the hunting sources bring back (per UTC day — per hour was 24× the rows and more than a tab could hold on a large tenant), and the next read asks only for the days it does not hold yet plus the current day, which is re-read every time because sign-ins reach the hunting table late. Opening the tool with 💾 on starts the read by itself and shows what the device holds first, under a “From this device” strip, then the new days land on top. The window starts at a day boundary when the store is in use — whole days, a little more evidence than asked, never less. The coverage line says how much came from the device and how much from Microsoft (“6 of 7 settled days from this device, 1 day and the current day from Microsoft just now”). What is kept: user principal names, IP addresses, locations, device state and verdicts of real sign-ins — in this browser's IndexedDB, on this device, readable by anyone using this browser profile and by nobody else; never tokens, never the Entra-log source's rows. Forget deletes it at once; it ages out after the days you choose (3 to 30, default 8). A capped day is shown but never kept, so a partial day is never believed whole later. ⟳ Rescan reads the whole window from Microsoft and rewrites what is held.
- On the hunting sources the verdicts are summarised before they leave Microsoft (build 315). Report-only impact never needed the sign-ins — it counts them per policy, verdict, user and app — so on Defender hunting and Hunting + non-interactive the query now asks for exactly that: one row per day × policy × verdict × user, with the device facts the “why it would say no” line is judged on (compliance, management, MFA requirement, both risk levels) and the set of apps the user hit; the per-app counts and the one real sample sign-in per user × policy that would be denied are small rows of their own (build 315 — grouping by app, client and OS as well was 24× the rows on a large tenant). The query runs once per report-only policy per day, so a day of a large tenant stays under the engine's row cap without being sliced into hours; the header says “N sign-ins summarised by Defender hunting in 35 queries (one per policy per day)”. Nothing of the window is held in the tab: every answer is folded into the running forecast as it lands and let go. The per-policy and per-user numbers are the same ones the row read produced — the offline test builds both from the same rows and compares. A tenant whose hunting engine refuses the summarised query is read row by row instead, with a note; the Entra sign-in log source always reads rows (Graph cannot summarise).
- The hunting read keeps the slice length that worked (build 315). Advanced hunting refuses a result over 50 MB, so a day of a large tenant's sign-ins is read in slices and a slice that is too big is halved. Halving used to start again from 24 hours for every day of the window — on a tenant that needs 22-minute slices that was 63 failed queries for the 64 that returned rows, every day, each one spending the tenant's hunting CPU allowance on nothing. The reader now starts the next slice at the length that last worked, lets it grow again when three slices in a row come back light, keeps it per tenant and source between sessions, and reads four days at a time. The progress line says how many sign-ins are already back and what the stride is, instead of "waiting for Microsoft's first response" for the whole first day.
- The forecast arrives as the days land. After every finished day (every five pages on the Entra log) the verdicts are rebuilt from what has been read so far and shown under a still reading strip, so a large tenant gives a first answer in a minute instead of after the whole window. ■ Stop keeps what was read and marks the result as a partial window — a verdict on a partial window is said to be one; ⟳ Rescan reads it whole.
- One read, five tools. 🚦 Sign-in failures in report-only mode, 🕵, 🌊 and 🛂 issue the same window read 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.
🛂 Session controls BETA folded into this tool — build 311
“Is CA308 blocking downloads?” has no answer in any log Entra keeps. A session control such as Conditional Access App Control only routes the browser session to Defender for Cloud Apps; what happens inside the session — a download blocked, a file labelled on the way out, a step-up authentication — is decided by a Defender session policy and written to Defender's activity log. This tool reads that log and joins it back to the routing policy.
- At a glance — policies with a session control, sessions routed to Defender in the window (from the sign-in log: an applied policy carrying App Control), downloads blocked, and the other actions: protected, step-ups, audited, logins.
- Per Conditional Access policy — state, which session control (App Control: Monitor only / Block downloads / Use custom policy; sign-in frequency; persistent browser; token protection; CAE; app-enforced restrictions; Global Secure Access), how many sessions it routed, what Defender did behind it, which Defender policies matched, and a verdict: Acting, Watching (routed and logged, nothing acted), Monitor only (routes, never blocks), Routed, nothing acted, Nothing routed, Off, Report-only (session controls are not applied in report-only).
- What Defender did — every session-control event: when, user, app and object, action (Blocked / Protected / Step-up / Login routed / Audited), the Defender policy, and the Conditional Access policy that routed the session. Filters per action and a search box; user names open 🕵 Who is Anna to CA.
- Defender session policies seen — every policy name the activities carried, with its counts and the CA policy that routed to it — or the warning that none did.
- Schema panel — the action types and raw field names the hunting rows carried. The words a blocked download uses differ per app and are documented nowhere; if a block shows as plain “Activity”, that list is what makes the classifier exact.
- Two halves that fail on their own. The Defender half is minutes of hunting; the routing half is the shared sign-in window. When only the sign-in window fails — a gateway hiccup, a refused scope — the activities are still shown, the note says routing cannot be said, and ↻ Read the sign-in window again re-reads the log alone and joins it to the activity already in hand, instead of a Rescan that repeats the hunt.
- ■ Stop — during the hunt, the slices already read are kept and shown as a partial window, named as such, without reading the sign-in log; during the sign-in read, the Defender half stays and routing is marked missing. The stop lands after the query in flight — a running hunting query cannot be cancelled.
Read-only. Defender activity comes from advanced hunting through Microsoft Graph — POST /security/runHuntingQuery over CloudAppEvents filtered to the session / access control audit source, delegated ThreatHunting.Read.All, the signed-in user needing Security Reader, Global Reader, Security Operator, Security Administrator or a Defender RBAC role with hunting access; 30-day retention; the window is read in 4-hour slices of up to 5,000 events each, a slice that takes more than 2 minutes is halved down to 30 minutes and then skipped with a note. Routing comes from the sign-in window 🚦 Sign-in failures, 🎚 Report-only impact, 🕵 and 🌊 share, from whichever sign-in source is chosen there — Entra sign-in log (AuditLog.Read.All) or Defender hunting. Join: same user, same app, newest routing sign-in within 8 hours; a user-only fallback is labelled; no match is shown as no match. Monitor only logs the Login activity only — a policy on Monitor only never blocks anything, and the tool says so.
🤖 Workload identities BETA a tab of this tool — build 324
Do the service-principal policies ever fire? Conditional Access for workload identities targets service principals, and its evidence is in a sign-in log of its own that the other tabs never read. This tab holds the policies that target workload identities against those sign-ins. It has its own source switch, separate from the user sign-in source of the other tabs:
- Entra log (default) — the service principal and managed identity sign-ins from the Entra sign-in log (
AuditLog.Read.All), capped at 10,000 and 5,000. Each sign-in names the policies that applied, so a block is attributed to its policy and a report-only policy shows what it would have done. - Defender hunting —
EntraIdSpnSignInEvents(the olderAADSpnSignInEventsBetauntil it retires on 19 October 2026) in one summarised query: 30 days of volume, IPs and countries cheaply. That table has no Conditional Access columns: a block is only error 53003, without the policy that did it, and there is no forecast. Needs Entra ID P2 and hunting access. - Service principals — every principal that signed in, and every one a policy names that did not: its kind (single-tenant app registered here, multi-tenant, managed identity, Microsoft app — read from the directory, with the application's sign-in audience for the ones registered here), sign-ins, how many came from outside every named location, the policies whose scope it is in, and a verdict: Blocked, Reachable, not targeted (a single-tenant app no policy scopes), Report-only only, Covered, applied, In scope if the attribute filter matches, or Cannot be targeted. Microsoft apps are hidden unless Show Microsoft apps is ticked.
- Per policy — who it names (all owned principals, a list, an attribute filter) and excludes, how many of those sign in, how often it blocked, and for a report-only policy what it would have blocked; named principals the policy can never act on are counted.
- What Microsoft does not enforce — only single-tenant service principals registered in this tenant can be targeted; managed identities, Microsoft and third-party SaaS apps and multi-tenant apps cannot, and a service principal reached through a group is not enforced at all — it must be assigned directly. Creating or changing such a policy needs Workload Identities Premium; existing ones keep working without it.
- Read-only. Export MD writes the principals and the per-policy results.
Needs AuditLog.Read.All for the Entra log or ThreatHunting.Read.All for hunting (each asked once), plus Directory.Read.All and Policy.Read.All from sign-in for the principals and the named locations.
🚪 Exclusion analyzer
Every exclusion across all policies in one place.
- Exclusions (default) UPDATED — the ranked list of what is excluded and where; Matrix — exclusions × policies, kept as the expert view because a grid is the only thing that shows the odd one out: where most policies carry an exclusion, the shared pattern is muted and the policies that do not carry it are marked ⚠ with a line naming them (“most” means at least 60% of the policies on screen, and at least two — an exclusion in only one or two policies has no pattern to deviate from and is never marked); Effective users — the same but expanded to the individual users a group exclusion covers; Risk review — policies scored for exclusion governance, worst first.
- Configured is not the same as bypassing UPDATED — a user listed in an exclusion is only bypassing when the policy would otherwise have included them. Each cell in Effective users is one of three: ✗ / ◐ / ↪ an effective bypass, ○ configured only (listed, but this policy never targets them — a policy scoped to Finance that also excludes the break-glass account never addressed that account), and ? not established, which is the honest answer when the include side names a group or role this scan did not read. The head counts all three, and the column header carries the policy state.
- Populations this list does not contain UPDATED — an excluded directory role is not expanded to its members, so those people are excluded and are not rows here; the head says how many roles were left unexpanded instead of showing a silent zero. Guest and external clauses are named the same way.
- The external clause is kept whole UPDATED — types, tenant kind and the named tenants. Two policies excluding B2B collaboration guests from two different partner tenants are two exclusions, each naming its tenants, and High “all guests and external users are excluded” is raised only when the clause really does cover every external user; a narrower one gets Medium with its scope spelled out.
- Risk review flags — High 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.
- Who counts as excluded: everyone Entra evaluates — each excluded group is expanded to its transitive members, so a user who is only in a group nested inside the excluded group is counted, exactly as Conditional Access evaluates them. The read is paged in full (no cap) and asks for users only, so nested groups are not rows of their own and devices or service principals sitting in the group are ignored — they hold no user scope. If a group's membership cannot be read the scan stops and says so, rather than publishing a smaller number that looks like a clean result. Two populations are deliberately not expanded and are named on the head instead of contributing a silent zero: an excluded directory role (its members are real exclusions and are not rows here) and a guest or external clause, which has no member list to read.
- Membership and path are different kinds of completeness — knowing who is in the group is one read; knowing how each person got there is another. The scan also reads each group's direct members and the members of its nested groups, so the matrix row says ↪ all through 9 nested groups, the member list names the group each member came through (direct first), and the effective-users grid shows ↪ for somebody who is excluded through nesting only. Both reads follow their continuation links; the first 40 nested groups per run are resolved by name, and anything past that — or any read that failed — says path not resolved rather than inventing a route. So a large group can legitimately read membership exact, some paths unresolved: the count is right, the ↪ attribution is not complete.
- Why nesting is a governance flag — Medium when an excluded group contains nested groups, High when an exclusion group is fed entirely by nesting. At that point whoever administers the child groups decides who bypasses your policy, without ever touching the exclusion group itself.
- Coverage verdict UPDATED — an app excluded from an All-resources policy is compared against every enabled policy that reaches it, and the answer is one of four: ✔ Equivalent coverage every compared requirement met; ◐ Partial coverage with the missing requirements named (the replacement excludes somebody, is scoped to one platform, or requires a different control); ✖ No equivalent coverage; ? Not established when a condition sits outside what the comparison models — a device filter, an authentication context, a resource collection such as Office 365. It is the same comparison 🔍 Gap analyse uses for bypasses (
js/coverage.js), so the two tools cannot disagree about what “covered” means. An app nothing reaches reads no equivalent coverage established by this analysis rather than the stronger claim that it has no Conditional Access at all. - Type chips — All, User, Group, Directory role, Guest/external, Application, Named location, Device platform.
- Click a cell for the evidence NEW — every marked cell in Matrix and Effective users is a button. Its card names who and why (named directly, member of which excluded group, through which nested group, or a route that could not be read), the policy with its state, and the verdict: effective bypass, configured only or not established for a user; the coverage verdict for an app; the odd-one-out sentence for a gap. From the card: Open policy, View members, Only this row, Only this policy. Escape closes it and puts focus back on the cell; a click outside or a scroll closes it too. A tap works the same on a touch screen — nothing depends on hovering.
- Members from the grid — a group row in the Matrix carries its own 👥 View N members button on a line under the group name; it opens the member dialog without also pinning the row. Full screen takes the page controls with it, so a paged Effective users grid can be paged inside the full-screen view.
- Keyboard NEW — Tab lands on the first policy header; the arrow keys walk headers, row labels and marked cells; Home and End jump along a row; Enter opens. One cell holds the tab stop at a time.
- Click to filter UPDATED — 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. Each pin is a removable chip in the toolbar beside the type chips, so one place says what is narrowing the view. With a column picked, the strip above the grid carries the full policy name, its state, its controls and its exclusion count, which is why the rotated headers can stay short. Pin one row and one policy and the tool shows the evidence card for that pair instead of a grid with a single mark in it. Below 700px the Matrix tab is not offered — the Exclusions list carries the same facts. A legend above each grid says what clicking does.
- The run strip UPDATED — above every result: which tenant, when the policies were read, how many and in which states, the population, how complete the read was, and a run number. Reloading the policies drops the result rather than leaving it on screen under a new snapshot (it used to survive a Refresh, exports included), and a snapshot that moves while a result is open turns the strip amber. Signing out, or switching tenant or between demo and live, drops it the same way. While a scan runs, reopening the tool shows only the progress panel — no run prompt, no toolbar, no export — and the new result appears whole when the scan finishes, never half-built over the previous one.
- Export — CSV or Markdown, with the three states written out in words (effective bypass, configured only, not established) and each policy’s state (On / Report-only / Off) in its column header; full-screen for wide grids.
Read-only.
🧬 Baseline
Matches this tenant against a baseline catalog and shows the gap. The catalogs live behind the picker in the toolbar — 🧬 CloudFellows R26.6, 🧩 Joey Verlinden and, since build 328, 🏢 SMB Basic — which is what T11 🧩 Baseline (Joey Verlinden) was a second tile for until build 311; it keeps its number and its section below. The CloudFellows catalog 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 filters — Missing (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.
- 🏢 SMB Basic BETA (R91) — a third catalog for small tenants on Microsoft 365 Business Premium: security defaults rebuilt as Conditional Access, with more on top. 20 policies in four sections, each section with its own restricted unit: Base CA001–CA010 (MFA for every user, block legacy authentication, block the device code flow, MFA for the 16 admin roles security defaults names, MFA for the Microsoft admin portals and Azure management, registering security info needs MFA or a Temporary Access Pass, 1.0.2: registering or joining a device needs MFA or a TAP, and a Windows PC or Mac that is not Entra joined — hybrid joined counts as unmanaged, a Mac counts as joined once Platform SSO registered it — gets a 4-hour session that is never kept in the browser; 1.0.4: CA009 — on iOS and Android only apps under an Intune app protection policy reach company data, guests, shared devices, the Defender mobile apps and — 1.0.5, as every CloudFellows app protection policy — Azure Credential Configuration Endpoint Service left out, so a passkey can still be created in Microsoft Authenticator; that resource is also left out of CA103, CA203 and CA204, the policies a phone meets; 1.0.6: CA010 blocks every internal user outside the 27 member states of the European Union, Iceland, Liechtenstein, Norway, the United Kingdom and Switzerland — the named location
SMB - Allowed EU countries, looked up from the IP address, guests and the two Defender mobile apps left out; 1.0.7: through the Global Secure Access client (All Compliant Network locations) a user may sign in from anywhere — on a tenant without Global Secure Access signalling the import creates CA010 without that exception and says so — while CA104 keeps the admins onSMB - Allowed countries, compliant network or not; 1.0.8: CA103 and CA201–CA204 leave out Device Registration Service, so a Mac or PC being set up can Entra-join or register Platform SSO before Intune has installed the Global Secure Access client — registering still needs MFA through CA007) inCA-RMAU-SMB-BASE; Admins CA101–CA104 (phishing-resistant MFA, a 4-hour session, the compliant network instead of a compliant device, allowed countries) inCA-RMAU-SMB-ADM; Compliant network CA201–CA205 (Global Secure Access per platform, guests excluded, admins not; 1.0.3: CA205 blocks every platform that is not Android, iOS, Windows or macOS, so nobody steps around the per-platform policies) inCA-RMAU-SMB-CN; Break-glass CA901 (a passkey only — synced passkeys too when the Passkey (FIDO2) profile allows them) inCA-RMAU-BreakGlass, which also holds the break-glass accounts. Every policy has its own exclusion group,CAB-SEC-U-SMB-CAnnn-Exclusion, and every policy but CA901 excludesCAB-SEC-U-BreakGlass; the all-users policies that reach every resource (CA001–CA003, CA008, CA201–CA204) and CA005 also exclude the directory sync roles (Directory Synchronization Accounts, On Premises Directory Sync Account), as security defaults does — the sync server runs no Global Secure Access client. 1.0.3, the exclusions matrix, from 🛡 Bypass & Swiss cheese and 📘 MS Learn:CAB-SEC-U-TeamsSharedDevices(Teams Rooms, phones, panels, Surface Hub — created dynamic as in the CloudFellows baseline, on the service plans only device licences carry; 📞 Teams devices tunes the rule to the tenant) out of CA001, CA003, CA008 and CA201–CA204; Azure Windows VM Sign-In out of CA001; the two Defender mobile apps, which carry the Global Secure Access client on iOS and Android, out of CA103, CA203 and CA204; B2B direct connect out of CA008. ENCA's own checks on the catalog leave only the documented exclusions under the baseline-scopes change and the sync-role exclusion. In 🗂 Policies and the Checks matrix SMB policies are grouped by these sections, not by the CloudFellows personas. No personas, no deploy groups and no shared 🚨 E-Admins — its own admin and break-glass policies cover them. ENCA carries the catalog: Import baseline and the security-defaults card need no ZIP and no download. CA103 and CA201–CA204 land Off whatever the landing state, because they wait for 🌐 Global Secure Access; at the ⇄ switch CA001–CA005 go On. Every CA number held more than once is shown on its row — ⚠ also here, with what the other policy is (an older version, the same version again, another policy on the number) — and the summary counts them, pointing at 🧹 Housekeeping (build 330). - 🛡 Security defaults NEW writes to tenant — the security defaults setting is read with the policies (and the P1/P2 licences when the answer needs them). A tenant on security defaults has no Conditional Access policies, and the screen now says that instead of “No policies loaded.” — the table shows the whole catalog as Missing, and the card above it offers both baselines through three steps: ① deploy in Report-only (📥 Import opens with new policies landing Report-only; Entra will not turn one On while security defaults is on), ② read what it would do (🎚 Report-only impact), ③ ⇄ Switch from security defaults. The switch band lists what security defaults does — MFA for every user, MFA for admins, legacy authentication blocked, the device code flow blocked — next to the policy here that takes each part over, plus the break-glass group (it must exist, have members and be excluded by every policy that goes On). The switch stays off until each is ✓. Its confirmation names the policies, the order (security defaults Off and read back, the policies On one by one and read back, the setting read again), the impact and the recovery. A run in which no policy goes On turns security defaults straight back on. ↩ Back to security defaults stays on the band in this browser: the policies go back to the state they had, security defaults goes on — and if Entra refuses, the policies are switched On again at once, so the tenant is never left with neither. Without Entra ID P1 the catalog is a preview and deploying is off; with security defaults off and no policies the card says nothing enforces MFA and offers to turn security defaults on. The Overview shows the same card. Try it without a tenant:
?demo=sd(on, no policies) and?demo=sdro(after step ①). - ★ 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 are created from ENCA's own copy, 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.
- ⬇ Newer than baseline — the fifth status: a policy in this tenant ahead of the catalog. What counts as newer depends on the tenant. On the baseline tenant (where the catalog is authored) it is a newer version in the name or the same version with a definition edited in place — or renamed — because that is exactly what the catalog has not caught up with yet; the summary line splits the count into those kinds. Everywhere else the CA number and the version decide. A policy at the catalog's number and version is Up to date whatever its definition, and the tenant's own assignments — the groups it includes, the exclusions it adds, the name — are never a difference. Where the resources, conditions, grant or session controls differ at that version, the row carries a differs · … tag naming what, and the summary counts those separately; they are shown for reading, not counted as newer.
- Catalog revised 2026-09-22 from the CloudFellows.dev reference tenant, taken with 🧱 Update the catalog below. The main change: B2B direct connect users come out of the session policies — sign-in frequency, persistent browser and app-enforced restrictions are documented as not supported for them, so the session policies that reach every external type now exclude direct connect, and the guest includes of CA400 and CA402 drop it. CA000 now excludes every guest and external type, and CA400 (guest MFA) grants Require multifactor authentication instead of the MFA strength, which is the only control an external who did not authenticate with Entra can satisfy. A number of names, exclusions (Teams shared devices, service provider users) and device filters followed. A tenant still on an earlier version of these policies now reads Outdated, and Import baseline proposes the new version. Recorded with the catalog, not changed by it: after this revision B2B direct connect and other external users are reached by no MFA grant in the catalog — the catalog says what CloudFellows publishes. 🗣 User impact brief warns until its rules have been checked against this revision.
- ⬇ Export baseline — on the baseline tenant only (never in demo), beside 🧱. The same export as 🗄 Backup (JSON) on 🗂 Policies — one zip with
MigrationTable.jsonand the same dependency read — opened from the page where the baseline is worked on, with a per-persona chooser: only the personas this tenant holds are offered, each with its count, plus a row for policies without a CA number. Dependencies is ticked on open, and the dependency folders hold only what the chosen policies reference — the groups, named locations, strengths, contexts and terms of use — not the whole tenant's. Untick a persona and the policy and dependency counts drop; untick all and Download is disabled. As with every backup on the baseline tenant, only the persona baseline policies are in scope. 🗄 Backup on 🗂 Policies is unchanged. - 🧰 Ready from Fix NEW (build 323) — policies changed by the MS Learn checks' 🧰 Fix arrive here ticked, each marked 🧰 from Fix with the check it answers; the button says how many are waiting, Select the N from Fix picks them again, and the generated source opens with a note listing the checks, their Learn pages and the changes. An entry leaves once the catalog no longer differs from that policy.
- 🧱 Update the catalog from this tenant NEW — baseline tenants only, because the baseline tenant is where the catalog is authored. It runs the comparison the other way round: not is this tenant behind the catalog, but what has the catalog not caught up with.
It compares definitions, not version numbers. The ordinary comparison reads the version out of the policy's name — so a policy you edit and save without bumping it reads as up to date. This one compares the assignment, resources, network, conditions, grant and session controls themselves, and it knows what is not a change: group order, condition order and the catalog's own markup are ignored, which is most of what made the last hand-regeneration slow.
Take it, or hold it with a reason. The reason is kept and travels into the generated revision note, the way the catalog's header already records its departures — so what you held last time stays held instead of being decided again. A hold is tied to the difference it was made about: if that policy later changes in some other way it reopens and says so, because nobody has looked at that one. A policy the tenant has and the catalog does not is offered as new; one the catalog has and the tenant no longer does is reported and never removed — deleting a baseline policy is not a decision this makes for you.
The panel counts what the chip counts. The changed policies are grouped the way the Newer than baseline chip and the summary line count them — 🆙 newer version in the name, ✏️ edited in place (definition differs, version not bumped), 🏷 renamed (only the name differs) — then 🆕 new here, so the chip's number equals the first three groups together. A staging prefix such as(NEW)or a capital letter is not a rename, and a policy on another policy's CA number is never offered. Each card says which kind it is; a policy at an older version whose definition differs is listed too, and counted as outdated. ☑ Select the N newer / edited in place / renamed ticks one group, ☑ Select all every changed and new-here card, ☐ Deselect all clears the ticks, and the Generate button counts what is ticked.
What comes out is the catalog source: the entries you took, in the file's own shape and keeping their number, version and tag, plus a revision note listing what was taken and what was held, and the reminder to re-check 🗣 User impact brief's rules. It opens as a report to download and read. Nothing is written to your tenant, and nothing to the repository — you editjs/baselineData.jsyourself.
It also says when its own wording has drifted: every policy it calls unchanged is a free test of the writer, and if those would come out phrased differently from the catalog's existing prose it tells you how many, before you paste any of them. - 🧷 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) folded into this tool — build 311
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, and a group every baseline shares — an E-Admins group, a
CAD-SEC-U-DG-*deploy group — is not listed at all: it serves the new baseline as it served the old one. 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. The groups every baseline shares are routed by the same exact rule:
Emergency_Access1,Emergency_Access2andCAB-SEC-U-BreakGlassto CA-RMAU-BreakGlass, aCAD-SEC-U-DG-<CODE>deploy group to the unit of the persona its code names when his catalog has that persona (ADM, INT, SA, GUESTUSERS, GLO, AGENTS) — never a guess for any other code. - 📡 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. Each of those files is then compared against a policy of its OWN in the tenant: release 2026.6.1 numbers both the AnyPlatform and the iOS/Android copy CA005, and a tenant holding both gets two matched rows rather than one match and one policy reported as new while it sits two lines above.
Read-only itself; the writes it steers are the other tools', with their own confirmations.
📖 Baseline guide BETA folded into this tool — build 311
The deployment order, with the reason for each step rather than just the sequence — and read against your tenant, so each step says what is missing before you run it instead of after. Six steps: understand the model, groups and their vaults, named locations / strengths / contexts, Terms of use, policies imported Off, then go live.
- Order is not arbitrary. Groups and their restricted units come before the policies that target them, because a policy referencing a group that does not exist cannot be created. Named locations, authentication strengths and contexts come before the policies that reference them for the same reason. Terms of use sits on its own because it has no Graph create API — it must be made in the portal first, and a policy granting one imports without that control until it exists.
- Readiness, not a checklist. Each step is evaluated against the tenant as it is now: what exists, what is missing, and what that means for the step after. A step you cannot do yet says so with the reason, which is the difference between a guide and a list of instructions.
- It ends where 🎚 Report-only impact begins — policies land Off, are switched to report-only, and are enforced once the forecast is boring. That is the whole of "go live the boring way".
- 📄 Export MD — the plan and the current readiness as Markdown, for the change record or to hand to whoever runs the deployment.
Read-only. It reads groups, administrative units, named locations, strengths, contexts and policies to work out where you are; it writes nothing and never acts on your behalf — the tools it points at do that, and each asks for its own permission on the click.
🌐 Global Secure Access BETA writes to tenant
Workspace 03 · Entra Tools, T56. A stolen session token or cookie is replayed from the attacker's machine. A Conditional Access policy that blocks every location except the compliant network makes it useless there — but only once every link Global Secure Access needs is in place, or the policy blocks your own people instead. This tool puts the links on one board. It covers the Microsoft traffic profile; Internet Access and Private Access come later.
- The eight links — ① Licence: Entra ID P1 or P2 seats (the Microsoft traffic profile, the compliant network and source IP restoration come with P1). ② Tenant: Global Secure Access activated. ③ Microsoft traffic profile: enabled, every traffic policy in it enabled, and assigned — to groups (a pilot first) or all users. ④ Conditional Access signalling (source IP restoration). ⑤ Compliant network named location — Entra makes it when signalling is switched on (its id is the same in every tenant,
3d46dbda-…); the fix reads first and only tries a create, which Microsoft does not document, when it stays missing. ⑥ Client on devices: an Intune remediation, platform script or app for Windows; for macOS a shell script and the three profiles the client cannot run without (system extensions, transparent proxy, client settings). ⑦ The policy: every Conditional Access policy that names the compliant network, with its state, who it covers, whether the Microsoft Intune and Intune Enrollment apps are out (without them a new device cannot enrol to get the client) and whether admin roles are excluded. ⑧ Proof: on request, the last 7 days of sign-ins (first 5,000) — how many came through Global Secure Access, and per policy how many a report-only policy would block and for how many users. - The fixes — Activate, Enable profile, Assign (a pilot group by name, or all users), Enable signalling, Create the compliant network location, Create Windows install remediation (0.15.0 install only — detection checks the client every hour or every day, remediation installs it from Microsoft when missing, never restarts; the client settings are Create Windows configuration, their own deployment; named
Win - DHS - Device Security - D - Install Global Secure Access Client - R26.10 - v1.0; its results — without issue, remediated, failing — show on the client link; Microsoft lists Windows Enterprise E3/E5, Education A3/A5 or VDA for remediations) and Create the macOS script and profiles UPDATED — both made in Intune and named the way the baseline names its scripts (Win - PSH - Device Security - D - Install Global Secure Access Client - R26.10 - v1.0,MACOS - SH - Software Installation - D - Install Global Secure Access Client - R26.10 - v1.0; release and version editable), assigned to a device group —INT-SEC-D-GSA-WIN-M365/INT-SEC-D-GSA-MAC-M365by default, made empty when missing; Windows lock-down on by default, prefer IPv4 optional. Each opens a panel that says what it changes, who it affects and how to undo it, and asks for its permission only when you press Apply; the board reads the tenant again afterwards. The script can be downloaded as it will be uploaded. macOS also needs three profiles — system extensions, transparent proxy, client settings — and UPDATED (0.8.0) the macOS workspace makes them with the script: the system extension policy (settings catalog,com.microsoft.globalsecureaccess.tunnelandcom.microsoft.globalsecureaccessunder teamUBF8T346G9), the transparent proxy profile (Learn's plist, device channel) and the client settings (preference filecom.microsoft.globalsecureaccess; Pause and Disable shown by default, Lock the client down hides all four), each ticked when missing or unassigned, one already in Intune reused and only assigned, namedMACOS - DCP - Device Configuration - D - Allow GSA System Extensions / GSA Transparent Proxy / U - GSA System settings - R26.10 - v1.0. With the script there and a profile missing or unassigned the step offers Create macOS profiles… instead — the same workspace, the script left alone. A baseline that pre-approves the GSA extensions in one consolidated System Extensions policy counts as found and is marked as the baseline's. UPDATED (0.9.0) The macOS chain table under step 6 checks the script and the three profiles four ways: found by what they contain (any name — a preference domain with the.plistslip too), content against Learn (the script: Microsoft signature check, upgrade, waits for both approvals; the proxy: TransparentProxy, the tunnel provider, app-proxy, team UBF8T346G9, device channel; the settings: the exact domain and four booleans), reach — do they reach the Macs the script reaches (exclusions, All devices, filters) — and landed, what Intune reports. It also says how many Intune-managed Macs the script's group holds. One fix per row: Repair makes the corrected next version, assigns it like the old one and marks the old one[DEPRECATED](never deleted); Assign adds the script's group; when another policy already allows system extensions on the Macs the GSA ids are added to it (backup first) rather than a second, conflicting policy. The macOS script is the CloudFellows baseline v1.2 (filescr-app-109, weekly), and the client settings are named with D — a preference file is a device payload. - The flow — the eight links are one flow in five lanes (Foundation, Traffic path, Endpoint, Enforce, Verify); the step that blocks opens under it, any step opens on a click, with what ENCA read and what you can do. A fix opens as a workspace below the step — several can be open at once, newest first, each with what it sends (the script as uploaded, or the Graph request; Copy and Download), its options, impact, way back, Apply and the log. One Apply runs at a time. (an assignment whose remediation Graph reports as off is NOT treated as detect only — Graph said off in devcf while Intune showed it on.)
- Go-live checks UPDATED (0.10.0, build 328, from the go-live recheck of 18 Oct) — the verdict is green only when a policy that blocks every resource outside the compliant network is on, every link before it is ready — the client included, and everybody it includes gets the Microsoft profile; otherwise it says the policy blocks people, not tokens. A policy that only requires MFA on the compliant network, or blocks outside it for one app, is listed but not counted. Who must pass against who can: the policy's include is compared with the profile's assignment (groups by id, All users, users named directly; nested membership is not expanded) and with the devices that carry the client — a gap is a warning in report-only and red when on. Windows is judged like macOS: the deployment's groups are read (Windows devices by join type — an Entra registered device gets Private Access only — and users) and its run summary decides (has not run, failing, found without the client and not remediated). Remediations need Windows Enterprise E3/E5, Education A3/A5 or VDA: the licences are read, and on Business Premium (Windows Business) the fix offers the platform script, with a remediation beside it. UPDATED (0.13.1, build 328) Intune runs a remediation on Business Premium when it is switched on, so one that is deployed there is a warning, not a block: the Windows lane's Licence stage is amber and says that using it outside the licence is your choice, the lane is decided by Connected, and Replace… stays on offer as the route inside the licence. A remediation step 6 found — any name, any release — is the one the fix assigns, and its other assignments are written back with remediation on. No Intune-managed Mac and no macOS script: macOS is not in scope and step 6 can be green on Windows alone. The proof reads interactive and non-interactive sign-ins (up to 5,000 each); a sign-in the policy did not apply to only because of the compliant network counts as passed through GSA, and the share is over the sign-ins in the policies' scope. Create it in report-only pre-fills Windows and macOS and the profile's groups. The policy card shows other apps a policy leaves out — each one a resource a stolen token still reaches. Adding the GSA ids to the baseline's System Extensions policy keeps its name; a deprecated Extensions profile that also sets system extensions is named.
- Step 6 in two lanes UPDATED (0.11.0, build 328) — the client on devices is two lanes, Windows and macOS, each with its own actions that stay whatever you do on the other platform. A lane shows what was deployed, whom it reaches, whether it may run (a remediation needs Windows Enterprise E3/E5, Education A3/A5 or VDA), what Intune reports and — the stage that decides — Connected: Global Secure Access itself has seen the device in the last 7 days, in its device report or in a sign-in marked through Global Secure Access. Step 6 is ready when every device in the deployment's groups is seen; Intune's run state explains why one is not there yet, and macOS no longer turns ready on assignments alone. A Devices in scope table lists every device with its group and when it was last seen. Replace… (Windows, optional, on a tenant whose licence does not include remediations) hands the remediation's groups to the platform script — a JSON backup of its assignments first, the script assigned next, and only then the remediation unassigned (it stays in Intune). Not in scope… takes a platform out of step 6 with a reason, kept in the tenant as the ENCA settings record (the open extension
nl.limonit.encaon the organization object, Organization.ReadWrite.All on Apply), so every browser and colleague sees it; Back in scope… undoes it. A tenant without an Intune-managed Mac and without a macOS script counts macOS as not in scope by itself. Check a Mac gives three read-only Terminal commands: the app, its system extension and the install log. - 📈 Monitor NEW (0.12.0, build 328) — the board has two modes, Setup (the eight links) and Monitor, and opens on Monitor once steps 1–7 are ready; the switch is on top of both. Monitor is read-only and reads, for 24 hours, 7 or 30 days, Global Secure Access's own records — the traffic log, the device report, its alerts and the transactions per time bucket — and the sign-in log. Six tiles: devices through GSA against the GSA groups; sign-ins through GSA (of the guard policies' scope, else of the Windows and macOS sign-ins); around the tunnel — successful Windows or macOS sign-ins not through GSA by people who also used it (a phone never tunnels and is not counted); blocked by the policy (and would be, in report-only); Token / device inconsistency, Global Secure Access's own alert that a token was used from a device it was not issued to — any value above 0 is critical; and the client versions (a Windows client below 2.31.125 does not upgrade through Windows Update). Then the traffic heartbeat — each bucket compared with the same time a week earlier; a red bar is below a third of it, Microsoft's tamper signal for a profile switched off or a tenant offboarded, which leaves no audit entry — Needs attention, most serious first, the devices (connected in the last day, silent, not seen, or seen outside the GSA groups, with user and client version) and the traffic log: newest first, 1,000 a page with Load more, a filter on user, device, destination or process, traffic type and action, and ⬇ CSV of what is loaded. Each source is read on its own: a refused one is named, never counted. Global Secure Access keeps its traffic log for 30 days; for longer, send NetworkAccessTrafficLogs and NetworkAccessAlerts to Log Analytics or Sentinel in Entra's diagnostic settings — this tool does not set that.
- My Apps NEW (0.13.0, build 328) — everybody assigned to the Microsoft traffic profile, directly or through a group, sees its enterprise app as a tile in My Apps and the Microsoft 365 app launcher, and the tile opens nothing useful. Step 3 reads the app's tags and says whether the tile shows; the node carries an in My Apps badge while it does, and the step's state is not changed by it. Hide from My Apps… sets Visible to users? to No — the
HideApptag on the app's service principal, its other tags kept — and leaves the assignment, the traffic and the client as they are; Show again… undoes it. Assign… hides it in the same run for a pilot group (ticked by default; with All users nobody is assigned and no tile shows), and Monitor's Needs attention carries the same button while the tile shows. Needs Application.ReadWrite.All and Cloud Application Administrator or Application Administrator. Not yet verified on a live tenant: Microsoft documents the Graph route for non-Microsoft apps, and the profile's app is Microsoft's — if Graph refuses, the log names the portal setting. - The Windows chain and Redeploy NEW (0.14.0, build 328) — step 6 shows a Windows chain under the lanes, like the macOS one, found by content and checked against Learn: Install client (the remediation or platform script, its download, how many devices installed it and which client version), Client settings (the four values the install script writes — pilot, locked down or custom; changed through the install script), Secure DNS off in Edge and Chrome (Learn: the client must see the DNS requests to tunnel by name — Create… makes one Windows settings catalog policy with only the settings no policy sets yet and assigns it to the install groups; a policy that sets them differently is named, never changed) and IPv4 preferred (optional). The Windows lane reads each device: Installed (Intune's run state), Policy fetched and Tunnel (the client's own sign-ins) — a device that is installed and fetched a policy but got no tunnel token shows Disabled by your organization, and the lane lists the checks. Redeploy… runs the install remediation now on the devices you tick, with a Sync first (Run now… on a device row does one); a platform script gets a new version instead, because Intune only runs it again when it changes. Devices in scope has a Client column.
- The client's own registry key is kept UPDATED (0.14.1, build 328) — the Windows install scripts wrote the client settings after creating
HKLM\SOFTWARE\Microsoft\Global Secure Access ClientwithNew-Item -Force, which replaces an existing key with an empty one and wiped the values the client keeps there; the client could then sign out with a configuration issue. The scripts now create the key only when it is missing. A script deployed before this build is red in the Windows chain and Redeploy is held back; replace it, then reinstall the client on the devices it ran on. - Install and configuration split NEW (0.15.0, build 328) — on Windows the client is installed by one deployment and configured by another, each created and assigned on its own. Create Windows install remediation… (every hour by default) or install script… installs the client and nothing else. Create the Windows configuration… makes
Win - DHS - Device Security - D - Configure Global Secure Access Client - R… - v…(or theWin - PSHplatform script): the four client settings, pilot or locked down, and IPv4 preferred when ticked, daily by default, on the install's targets or a group you name. As a remediation it first checks that the client is installed and reports nothing to configure yet until it is, so it can go before, with or after the install; as a platform script it writes the settings straight away, because a platform script runs once. One configuration only — a second one assigned is red. A script that installs and configures in one (made before this build) shows Split…: a backup, the install as its next major version (v1.0 → v2.0) and the configuration with the same settings on the same groups, both read back, then the old one assigned to nobody and renamed[DEPRECATED]— kept, never deleted. - What it never does — switch a policy on, or change a policy's users, exclusions or controls. A policy's state is changed in 🗂 Policies or 🎚 Set policy state when the proof says nobody is blocked who should not be; its exclusions stay as they are (the admin-role question is in the admin handover).
- Policies that name the compliant network — a card of its own under the board: every policy loaded in 🗂 Policies that names the compliant network, on either side — excluded (the policy applies outside it, the token-replay shape) or included (it applies on it) — with its state, the grant, who it covers and whether the Intune and Intune Enrollment apps are out. Load 🗂 Policies first; the card says so when they are not.
- Intune policies and scripts for the client — a card under the policies: every remediation, platform script, app, macOS shell script and macOS profile with Global Secure Access or GSA in its name — where it sits in Intune, whether ENCA made it, and the groups it is assigned to and excluded from. Duplicates are marked, a Windows client deployed more than one way is called out, and a missing macOS profile is named. Read only.
- Who Global Secure Access reaches UPDATED (0.9.1) — replaces the groups card with three questions: who gets the Microsoft traffic profile (users: All users when Assignment required is off on its app — then no group is needed — or the assigned groups), who must sign in through it (users: what the compliant-network policies include) and which devices get the client (devices: the Intune groups of the remediation, scripts and macOS profiles, per macOS and Windows, with how many Macs Intune manages). One line on top says whether anybody's traffic goes through yet. These groups are the Used by GSA scope of 👥 Groups (T12). Step 7 is never a dead end: with the policies unread it offers Read the policies and comes back; with none naming the compliant network, Create it in report-only opens 🏗 Policy builder pre-filled — you choose the exclusions.
- 🚧 Tenant restrictions v2 NEW (0.7.0) — optional (0.10.0: folded by default, marked optional, never part of the chain or the verdict; Enforce is refused while no tenant is allowed, on this browser's list or in the tenant, and the tenant's own allow-all partners are named on the card) — a card of its own under the selected step, not a ninth link: it decides which other tenants' accounts may be used on devices whose traffic goes through Global Secure Access, so company data cannot be carried to a personal Microsoft account or a foreign tenant from a managed device. Conditional Access cannot do this, because those sign-ins happen in the other tenant. This tenant's own accounts and its B2B guests are never restricted. The rule to remember: the tenant that owns the device's GSA client decides. Set up opens a workspace: an allowed list kept in this browser per tenant (look a domain up, or import a CSV of domain,tenantId,name; the tenant itself is refused), partners that inherit the default with a tick to keep them allowed, a switch for personal Microsoft accounts (off by default) and an Enforce tick. Apply writes in a fixed order — a partner entry allowing all users and applications for each allowed tenant, then the default blocking all external users and applications, then, only when ticked, universal tenant restrictions on — and a step whose predecessor failed is skipped, so nothing is blocked before its allows land. A backup of exactly what the run touches is downloaded first; ↩ Put back reads it and undoes in reverse, enforcement off first. Without Enforce nothing is blocked. With it, only devices with the client in the Microsoft traffic profile's assigned group are affected, and there is no report-only mode: keep the profile on a pilot group. In the Entra admin center the same lives under External Identities → Cross-tenant access settings (Default settings and Organizational settings → Tenant restrictions) and Global Secure Access → Settings → Session management → Universal tenant restrictions. If the admin center says Access denied for a partner tenant once it is on, Microsoft's workaround is to add ?feature.msaljs=true&exp.msaljsexp=true to its address.
- What the board cannot see (from a lab deployment) — the Microsoft profile's device platforms are set on its Assignments page in the portal (no documented Graph call): a profile has User and device assignments AND Device platform assignments, and a device gets it only when it matches both — All users and devices with no platform ticked means nobody (step 3 says so, and shows assignmentRules when Graph returns any). On Windows, secure DNS (DNS over HTTPS) must be off in Edge and Chrome or the client cannot tunnel by name. The portal makes a
_GSA_<guid>backing group per profile — the groups table marks it; do not use it in Intune or Conditional Access. The tenant link mentions, as information only, when the ZTNA Policy Service Client app is not in the tenant — Microsoft does not require it; create it with the optional button only if clients stay Partially Connected. The Windows script writes pilot client settings unless Lock down is ticked; the macOS script installs only on macOS 14 or later and only a package signed by Microsoft. - Caveats — the client tunnels IPv4 only: a device that reaches Entra over IPv6 goes around it and is blocked by the policy. A platform script runs once and installs whatever version Microsoft serves then; from November 2026 Windows Update upgrades clients at 2.31.125 or later. The Graph routes for activation, the profile, signalling and the location are Microsoft's beta network access API.
Reads: networkAccess tenant status, forwarding profiles and settings (NetworkAccessPolicy.Read.All, NetworkAccess.Read.All), the profile's service principal and assignments (Application.Read.All), named locations, Intune scripts and apps (DeviceManagementConfiguration.Read.All, DeviceManagementApps.Read.All), subscribed SKUs, cross-tenant access settings (Policy.Read.All) and universal tenant restrictions; the sign-ins marked through Global Secure Access (step 6 and the proof) AuditLog.Read.All, the device report NetworkAccessPolicy.Read.All, the ENCA settings record the organization object, a domain look-up CrossTenantInformation.ReadBasic.All; the Windows chain reads the install script's content and each device's run state (DeviceManagementManagedDevices.Read.All), the client's own sign-ins (AuditLog.Read.All) and the Windows settings catalog policies. Monitor adds NetworkAccess-Reports.Read.All (the traffic log and the alerts; the person needs Global Secure Access Log Reader, Security Administrator or Global Secure Access Administrator), asked on the click. Writes ask for their own scope on Apply (tenant restrictions: Policy.ReadWrite.CrossTenantAccess and NetworkAccess.ReadWrite.All; Not in scope: Organization.ReadWrite.All; Hide from My Apps and Show again: Application.ReadWrite.All; Redeploy of a remediation: DeviceManagementManagedDevices.PrivilegedOperations.All; Redeploy of a platform script: DeviceManagementScripts.ReadWrite.All; Secure DNS off: DeviceManagementConfiguration.ReadWrite.All).
👥 Conditional Access groups writes to tenant
📞 Teams devices is a row action here, not a tile. ② Create already makes CAB-SEC-U-TeamsSharedDevices as a dynamic group with the Teams rule; T35 rebuilds that rule from the device licences the tenant actually holds, and replaces it. One action on ONE row, so from build 311 it is offered where the group is — the 📞 Rule button on that row — and nowhere else, because on any other row it would mean nothing. Which row gets it is decided by the tool's own alias list, so a tenant that named the group CAB-SEC-U-SharedDevices still gets the action. T35 keeps its number (see 🔢 Tool numbers) and its section below, ⌘K still finds it by name and number, and its screen carries a ← Groups button back to the list it came from.
The group side of the baseline, in one tool. One read, then everything is a row. On open the tool reads the tenant once — the groups your policies reference (scope Used by CA policies), plus everything the ★ active baseline expects (Baseline + templates), or every security group in the tenant, first 5,000 (All groups) — with their policies, protection, role-assignable state and nested groups — and shows them as a list. Members and nesting are read per group when you first look at it, kept for the session, and a write re-reads only the group it touched.
- Scope by workload UPDATED (6.0, build 326) — the scope says which groups: Used by CA policies, Used by GSA (the groups 🌐 Global Secure Access assigns its traffic profile to — live once that tool has read the tenant), Baseline + templates or All groups. The workspace you come from picks the first one — 01 opens on Used by CA policies, 03 (where the tool is called Groups) on All groups — and a scope you pick by hand is kept for that workspace until the page reloads. Every row carries a badge for the workloads that use it: CA when a Conditional Access policy names it, GSA when Global Secure Access assigns to it.
- The list — group, status (present / missing / referenced but gone), members (count, direct vs nested, or a read button), the policies that use it with their state, protection (which restricted unit, or not protected). Filter chips: needs attention, missing, gone, empty, not protected, role-assignable, ↪ has nested groups (always shown — a nested group inside an exclusion or break-glass group is needs attention, because whoever manages the nested group decides who bypasses the policy), not in the baseline; the search matches group names, object IDs and members. Sorted attention first; click a column header to sort by it, again to flip, a third time to go back. Each row offers what applies to it — + Create on a missing group, 🔁 Restore on a dangling reference, 🌊 on a deploy group, ⋯ for everything else.
- Groups every baseline shares UPDATED — a group counts as the baseline's (present, not not in the baseline) when the ★ active baseline defines it, and also when it belongs to every baseline: the 🚨 E-Admins groups —
Emergency_Access1,Emergency_Access2,CAB-SEC-U-BreakGlass, and any group an E-Admins policy includes — and the 🚀 deploy groupsCAD-SEC-U-DG-*that a Deployment groups import and 🌊 waves use under any baseline. Under Joey Verlinden's baseline a group also counts when his repository names it that way (CA403-Guests-… - Exclude), when it is his example include group (APP_Microsoft365_E5), or when it is the exclusion group his naming rule gives a policy this tenant has under an older release's name (CA005-…-AppEnforcedRestrictions - Exclude). The row says why — every baseline, 🚨 E-Admins, every baseline, the baseline's naming rule, the repository's group name — and so does Export MD. Under his baseline, Baseline + templates also expects the Emergency_Access pair (the shared E-Admins policies name them; his own break-glass group does the rest) and ② Create makes them from the CloudFellows template; deploy groups are recognised but never reported missing. The ⚠ hint that this tenant carries another baseline's policy names no longer counts the six E-Admins policies, which every baseline expects. Only a group none of this explains stays under not in the baseline. - 🌍 Fill from regions BETA UPDATED writes to tenant (toolbar) — puts the users of a region list's member groups into the seven deploy groups (default
CAD-SEC-U-DG-*), in three steps; ℹ How it works in the dialog walks through them with this tenant's names. Nothing about a customer is built in: the region list is a CSV you import once per tenant — headerregion,group,label,ticked,mode, one row per source group; the group is a display name (any case), an object id, or a name ending in*for every group starting with it; mode is empty (the rules),sa-only(its users go to SA only) orignore(listed, never read). ⬇ Download template gives a sample. The import is checked against the tenant before it is saved: found (with members and dynamic), 0 members, not in this tenant (kept, shown as not in this tenant), a duplicate, a name two groups carry, or a deploy group named as its own source (left out). Where it is kept: in this browser, per tenant (enca-dgfill:<tenant id>) with its file name and date — ⬇ Export CSV gives it back, ⬆ Replace imports a new one, Clear removes it; another browser or machine imports it once. ⚙ Settings (kept the same way): the main account domain (default the tenant's default domain), the second account domains (empty turns rules 0a, 0b and 5 off), the external marker (an extensionAttribute 1–15 and the text it starts with; empty turns rule 2 off), an optional prefix that also lists the tenant's other groups starting with it, unticked, and the name of each deploy group with whether it is there. ① Sources: the list's regions in CSV order, the ticks from the CSV; plus all guests and every second account, read directly. ② Preview: per deploy group +adds and −removes, the policies that name it, and per user the rule and why. The rules: a second account (guests excluded, paired wherever it was read — the Second accounts tick or a ticked group) with a main-domain account of the same name before the @ → ADM only, without one → SA only; a user only in sa-only sources → SA only; a guest → GUESTUSERS; the external marker starting with the external text (any case) → EXT; anybody else → INT; all three → GLO; the main account of a second account → DevOps. Add + fix (default) also takes a user read from a ticked source out of a deploy group the rules do not give her — never somebody in no ticked source, never out of DevOps, never a member through another group. ③ Apply: impact and recovery, a JSON backup of every deploy group's direct members, the tenant's domain typed, adds before removals in one run ledger, a read-back, a report — and ↩ Undo this run, which reverses exactly what the run changed. A dynamic or missing deploy group is refused, never written. - 🔀 Duplicate names writes to tenant (toolbar, only when it applies) — two or more groups with one display name look like one group everywhere, so one policy can include one and another exclude the other without anybody seeing it. The button appears after a scan finds such a name among the groups in the current scope (names compared ignoring case and surrounding spaces) and counts them.
What it reads. The direct members of every group involved, and the Conditional Access policies already loaded. One card per name lists each group by object id — assigned or dynamic, its rule, its direct member count, the policies that name it (include and exclude) — with a keep radio; the suggestion is the group more policies point at, then the one with more members, then an assigned group over a dynamic one. The foot of the card says how many members move, how many policies are repointed (and which already name both, so the duplicate reference is dropped), and how many policies name the kept group afterwards.
What it changes, in this order. ① The other group's direct members are added to the kept group (someone already there is skipped). ② Every policy that names the other group is read fresh and its users block is written back with the kept group in its place — includes and exclusions alike — then read back until the change shows. ③ Only when every member and every policy landed is the other group renamed aside as “(merged YYYY-MM-DD)” — the default — or deleted, if you choose that. A run that fails or is stopped part-way never removes the other group, because a policy may still name it; the run ledger says what landed and the report lists the failed steps.
Confirmations. A review step shows the plan per name before anything is written; running it takes a typedMERGE(rename aside) orDELETE(delete). A JSON backup — the groups, their members and every affected policy's users block as it is now — downloads first unless you untick it, and a failed download stops the run. Permissions are asked for on the click.
Refusals and warnings, on the card. It refuses when the kept group is dynamic and members of the other group are not in it (nobody can be added to a dynamic group by hand — keep the other one, or widen the rule first), when a policy includes one and excludes the other (after the merge the exclusion would win and the policy would apply to nobody in the group — decide what that policy means first), or when members could not be read. Members taken from a dynamic group become fixed members of the kept one, and a retired dynamic group with its own rule is flagged: people that rule would add later are not reached by the policies.
What it never does. It never removes a member from the kept group, never touches a policy that names neither group, never changes anything but the group references in a policy, and never moves anything outside Conditional Access — app assignments, licences, access packages or Intune assignments that use the other group are not repointed. That is why rename aside is the default: 🧹 Archived groups lists the renamed group, checks its other uses and deletes it when you are satisfied. Demo mode simulates the run. NeedsGroup.ReadWrite.AllandPolicy.ReadWrite.ConditionalAccess; the consent asked on the click also namesRoleManagement.ReadWrite.Directory, which it shares with the other member-moving writes here. - 🧹 Archived groups (toolbar) — the groups a migration renamed aside. Delete leaves nothing behind: a ticked group still named by a policy is taken out of those policies first (live read, removal, read-back) and deleted only when no policy names it. A policy that refuses the change (the unpatchable ones every tenant seems to have) does not fail the group: the row reads ◐ partly done — taken out of 36 of 39, still named by the three that refused, not deleted so no policy points at nothing — and the report says which policies to fix before running again. 🔗 Check other uses runs the User or Group analyzer's sources over the ticked groups — app assignments, Intune, licensing, Teams, admin units — and shows the hits per group before you delete, because a recreate moves Conditional Access for you and nothing else.
- The actions bar — shows as soon as a row is open, and acts on that row until you tick more. Tick rows and act on them: read members, ⊞ compare selected (the members × groups matrix, only for what you ticked, with the nesting view), assign to policies, protect in a restricted unit, migrate off role-assignable, import members from CSV, create the missing ones. The writes — Create, Assign, Import, Protect, Migrate — open in a dialog over the list with your selection carried across; close it (✕ or Esc) and the ticks and the open row are where you left them. ⊞ Compare opens as a sheet rising from the bar with two views — Members (the members × groups matrix, with nesting) and Policies (groups × policies: ● in, ✗ ex, · not named; rows where the picked groups differ come first, and each group gets a line saying how many inclusions and exclusions it is missing against the others — after a migration, that is what the new group still lacks; every missing cell is a tick, and one button adds the ticked groups to those policies the way the other group already is on them) — full height by default; ▁ Half in its header drops it to half the window so the list stays visible above it and the matrix follows your ticks. Protect and Migrate check only the groups you came with on arrival, and offer the full check as a button.
- The detail drawer — click a row, or tick it: the drawer follows the tick, and unticking the open row opens the last one still ticked. Members: direct members first, then each nested group with its own members underneath; a find box filters them as you type (nested groups open on a match) and a segment orders them as read / by name / by UPN; when the group is bigger than the 500 read, 🔎 or Enter searches the whole group in the tenant — the answer is either “not a member” or the person, direct or placed under the nested group she came through, with her ×; add inline; × removes from the group the member actually sits in — a member under a nested group is removed from that nested group, and the confirmation names what else that group feeds. Policies: include / exclude with state, names open the policy card. Protection: the unit it is in, or why it is not protected and the button that fixes it. History: the group's last 30 days from the directory audit log — who added or removed whom, refused changes marked, apps marked. Show when each member was added (Members tab) runs that read once and puts the add date and the adder on every direct member; a member with no add in the window was added earlier or by the membership rule.
The run ledger. Every write that walks a list — Migrate, Protect, Create, Import CSV, Archived groups, the Compare ticks, and 🎯 Assign and 🎚 Set policy state elsewhere — shows the same box: the whole list before the first write, the row being written marked amber, ✓ green or ✗ red as each lands with the reason on the row, the box scrolling to keep the working row in view, a count and a clock in the header, and ■ Stop after this one, which finishes the write in flight and reports the rest as not reached. A run with failures keeps its dialog open so the ✗ rows are read where they happened.
Below: the engine screens the list opens in its dialog and sheet. They are the same flows as before, one step shorter because the groups arrive pre-selected.
- 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
isAssignableToRoleis immutable — so it is replaced instead: the old group is renamed-static-YYYYMMDDand 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 getsCA201-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 guests & external users NEW (build 322) — the wizard's third target, the portal's Guests or external users choice under Include and its matching tick under Exclude. Pick the types — local guests, B2B collaboration guests, B2B collaboration members, B2B direct connect users, other external users, service provider (CSP/GDAP) users — and the tenants they come from: all external tenants, or a list of tenant IDs. Aimed at a selection or at every policy in the tenant, like the other two.
It is one clause, not a list of ids, and that changes what the actions mean. Set writes the types and the tenant scope, replacing whatever the clause held. ADD unions the types — but the tenant scope is a single value that cannot merge, so a policy already scoped to different external tenants is left untouched and says why, rather than being quietly rescoped; re-run those with Set if replacing the scope is what you meant. REMOVE subtracts types, and a policy left with none loses the clause entirely, because an empty type list is not a valid clause. The step-1 panel lists what the policies in scope carry today, and a REMOVE offers only the types they actually have, pre-ticked — the question there is which ones to keep.
On the include side this is a choice like any other: a policy currently on All users switches to covering only these guest types, and the wizard says so before it writes. On the exclude side it widens the policy — whoever those types cover stops being subject to it. Every outcome reaches the run ledger and the change report, including left untouched with its reason. - 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
disableNestingon neither the v1.0 nor the betagroupresource, and a tenant that lacks it answers400 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 betadisableNestingproperty 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. NeedsGroup-NestingSupport.ReadWrite.AllandGroup.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). NeedsAdministrativeUnit.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
disableNestingautomatically. 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 betagroupresource, and a directory that has not been given the feature refuses the field outright withRequest_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 waits for the directory, and finishes what an earlier run left half-way UPDATED. Entra is eventually consistent: a group created a moment ago can answer “does not exist” to the very next write naming it. On a tenant on 27 October seven of eight groups failed exactly there — created, then every member add refused — and the only one that went through had no members to copy. The run now reads the new group back until the directory answers (about 20 s at most), every member add, member read and unit placement retries a not-here-yet answer, and a copy that stops part-way reports how many members did land. The check after the copy waits as well: a read that does not show a member the directory has just accepted is repeated on the same schedule instead of failing the run, and a member still missing after it is named. The other half: a run that stops after the create leaves a pair — the archived
(migrated …)original, still role-assignable and still on every policy, and the plain replacement under the live name. The rescan used to offer the archive for migration again, which would have stacked a second suffix and created a permanent group calledX (migrated …). It is now recognised and offered as finish an earlier run: no rename, no create — members copied (the ones already there left alone), policies repointed, unit placement — and any other archived group is refused with the reason. The change report says per failed row where it stopped, why, where things stand in the tenant and, in so many words, that the archived original is still the live group: do not delete it, do not take it out of its policies. - ⑦ 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
DELETEconfirmation still gates the button.
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 / 🔀 Merge duplicate names, each behind a confirmation.
📞 Teams devices BETA writes to tenant folded into this tool — build 311
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-startsWithclause — 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.
🔒 Protect exclusions UPDATED writes to tenant
An exclusion group is exposed in two ways, and one lock does not cover the other: who can change its members (a restricted management administrative unit — the vault) and whether a group can be nested into it (the group's disableNesting setting — otherwise anyone who can edit some other group can put that group inside the exclusion and walk through the policy). This tool shows both locks per group in one table and applies either or both in one run. It is the standalone version of Conditional Access groups → ⑥ Protect and shares that scan and selection.
- One row per group, two locks. 🔒 Members — restricted unit: in which vault it sits, or unprotected with where it would go. 🚫 Nesting: disabled, allowed (with the number of nested groups already inside), never for a dynamic-membership group (its rule picks users or devices; a group can never be added), impossible for a role-assignable one, or not returned by the directory. Chips filter by state: not fully protected, vault only, nesting only, open, fully protected, cannot here.
- Nothing is pre-ticked. A tick is something you did: a row, the header box (ticks every lock that is still open), or a selection carried over from the CA groups list. The bar counts exactly the ticks.
- A greyed tick says why, on the tick itself — no vault to put it in — map it, or pick a fallback unit in Settings; CAB-SEC-RMAU-INT-Exclusions does not exist yet — create it in 🛡 Restricted AUs; blocked by 2 nested groups (empty it first — 👥 CA groups shows them); already; never nests (dynamic); state not returned by the directory. The row tick explains itself on hover.
- Each group goes to its own persona vault. The destination is read from the group's name — the CA number (
CA001toCAB-SEC-RMAU-GLO-Exclusions,CA101to ADM,CA1001to DevOps), or, for the baseline's own groups, the persona in the name (CAB-SEC-U-Persona-Admins,CAD-SEC-U-DG-INT) and break-glass by intent (BreakGlass,Emergency_Access1) — or from what you said once in 🛡 Restricted AUs → 🏷 Group personas. Under Joey Verlinden's baseline the groups every baseline shares are placed too: the E-Admins groups in CA-RMAU-BreakGlass, a deploy group in its persona's unit when his catalog has that persona. Shown per row before you write. A tenant's own group that names no persona stays unmapped and says so; the fallback unit in Settings is used only for those, 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. Nothing is filed by proximity. - 👤 The break-glass accounts are a third lock UPDATED — the group in a vault guards who is in it, not the accounts: a tenant-wide User, Authentication or Privileged Authentication Administrator can still change their methods, disable or delete them. Under the break-glass row a panel lists every user in the group (read through nested groups) with its shape (cloud-only, an onmicrosoft.com sign-in name, enabled), its Global Administrator assignment (active; permanent, time-bound, activated or eligible once PIM is read) and the restricted unit it sits in. An open account is ticked into the unit the group sits in (or the break-glass vault it would go to) — one tick per account, or vault N accounts on the group row — and read back through the account's own memberOf. An account in the unit but no longer in the group is shown as in the unit · not in the group, with a take out of the unit tick that is never ticked for you. A second acknowledgement is required: a Global Administrator inside a restricted unit can have its password and methods reset by nobody — no unit-scoped role may change a Global Administrator — so recovery is the other break-glass account taking it out of the unit (audited), fixing it and putting it back. The 👤 Break-glass accounts tile counts them, and a group whose accounts are open is 👤 Accounts open, not fully protected.
- 🔑 Who can manage the units UPDATED — a restricted unit blocks every tenant-wide role, Global Administrator included, so each restricted unit is checked for a role scoped to it: Groups Administrator where it holds groups, User Administrator where it holds users. Verdicts: correct, role missing, nobody (no scoped role at all), empty unit. Active assignments come from the unit's scoped role members; PIM-eligible ones are added (tagged eligible) once 🔑 Read PIM has granted
RoleManagement.Read.Directory. A User Administrator on the break-glass unit manages its ordinary users only — the Global Administrators stay out of its reach, and the row says so. - Break-glass groups are candidates on any reference — included by the break-glass policy and excluded by none, they used to be missed; the group holding the accounts that bypass every policy belongs in the break-glass vault whatever it is called.
- Role-assignable groups are refused — their membership is already limited to Global Administrator / Privileged Role Administrator, and a restricted AU blocks those same two, so a group with both has nobody who can edit its members; ⑦ Migrate it first is offered on the row. 🧊 frozen — a group that already has both (inherited from before the guard, or from the portal) is a dead end and is called out wherever it appears: take it out of the unit to restore the two roles, then migrate.
- The run — the run ledger with both locks as notes per group: added to the vault (or already there), nesting disabled and read back — a tenant whose directory does not return the property is told so once, tenant-wide, rather than per row. Scoped administrators are granted on every unit the run wrote to, each grant reported separately. Explicit acknowledgement before anything is written; Markdown change report after.
- Groups no policy references are listed too, marked not referenced, never pre-selected and not swept up by select-all: a baseline exclusion group can sit in a unit, or be frozen inside one, long after its policy was retired. Protection is read in one call for the whole tenant; if that read fails the column says unknown, never “unprotected”.
- Search — the box narrows the groups already found; select-all applies to what is visible.
📘 MS Learn shows the same break-glass verdict as a band above its findings — accounts outside the restricted unit (High), or the unit's missing scoped roles — with Fix in 🔒 Protect exclusions; the fix is only ever made here. Needs AdministrativeUnit.ReadWrite.All and Group.ReadWrite.All (on demand), the Privileged Role Administrator role for the unit and Entra ID P1 for administrative-unit administrators. Nesting is read and written on Graph v1.0 — some tenants answer without the property on beta. See the CA groups section above for the migration and the vault design.
🧩 Policy building blocks writes to tenant
Four kinds of reference, one tool. 🌐 Named locations, 💪 Authentication strengths, 🎫 Authentication contexts and 📜 Terms of use were four tiles over four copies of the same screen — a list, which policies use each row, create / edit / delete — and four private copies of the same query, one per module. ♻ Recycle bin is the fifth tab because the restore window holds deleted named LOCATIONS as well as deleted policies. They are one tool with five tabs from build 311; T15, T22, T23, T24 and T25 keep their numbers (see 🔢 Tool numbers) and their sections below. The four which policies use this lookups are now one implementation in js/causes.js, so the All-trusted-locations case — a policy that never names a location but reaches it through All trusted locations, and so changes behaviour the moment you flip the trusted flag — is available to every kind rather than to locations alone.
🏗 Builder is the first tab, before 🌐 Locations: the policy the other five tabs are building blocks of. It creates new policies and clones existing ones (see 🏗 Policy builder below); changing an existing policy is done on the policy's own card in 🗂 Policies — ✎ Edit this policy, first in its action row. The 🧩 tile and its deep links still open on Locations, and the strip remembers the last tab you opened in this session.
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.
- vs. sign-ins BETA build 324 — the third view beside Cards and Table: every named location against 1, 7 or 30 days of sign-ins, read on ▶ Read the sign-ins from the same window 🚦 Sign-in log, 🎚 Report-only impact and 🕵 share (the same source setting, the same cap, a window one of them already read is reused). Per location: how many sign-ins matched, from how many users, when it was last seen, and a finding — Trusted, nobody signs in from it (High when a policy relies on it), Used by policy, not seen, Seen, no policy uses it, and for a country location with no sign-ins, a note that this is expected for a list of blocked countries. Below it, the countries sign-ins came from that no country location names. A sign-in matches when Entra's own record names the location (networkLocationDetails, Entra log sources) or when its IP falls in one of the ranges (IPv4 and IPv6) or its country is on the list; a country location looked up by Authenticator GPS only counts Entra's own match. A capped window says so, because a location missing from a capped read may just be past the cap. Export MD in the view writes the table and the findings. Read-only.
- ⇄ 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 folded into this tool — build 311
The named step-up requirements, c1–c99. 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.
c1is 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
isAvailableflag, 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 folded into this tool — build 311
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 folded into this tool — build 311
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 folded into this tool — build 311
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.
🏗 Policy builder BETA writes to tenant the first tab — T41
One guided screen for a policy. Seven steps — name and persona, who, what, when, then, session, state — with the tenant's own building blocks as the pickers: the named locations of the 🌐 tab, the contexts of 🎫, the strengths of 💪 and the terms of 📜. The CA number is the next free one in the persona's range and the name follows the baseline convention (CAnnn-GRANT|BLOCK|SESSION-Persona-words-vX.Y) as you choose; the kind follows the controls. Hints on the way are computed from the loaded policies and blocks, never read from the tenant again: the break-glass group not excluded, legacy authentication already blocked tenant-wide, an IP location not marked trusted behind “All trusted locations”, an empty country location, an enabled policy that already targets the same people and resources.
- Three starts. ✨ Blank. 🧬 A baseline template — when the tenant holds that CA number the builder starts from the tenant's policy (structured); otherwise from the template's number, persona, version, group names resolved against the tenant's groups, and its own sentences carried into step 1 as “the template says”. 🗂 A policy from this tenant — every row offers ✎ Edit this policy (that policy itself: same number, same name, Save writes only the sections that differ) and ⧉ Clone into a new policy (a new one at the next free number, report-only). Also reached as ⧉ Clone in builder from a policy card and from 🗂 Policies' action bar, and from every block's detail pane (New policy with this location / strength / context / terms).
- Editing a policy belongs on its card. The builder is for new policies and clones; to change one that exists, open it in 🗂 Policies and press ✎ Edit this policy, first in the card's action row: the card's boxes become the same forms the builder uses, a bar counts what changes and shows the diff, the name is editable, and Save writes the same way the builder does. Edit from the builder's 🗂 start is the same edit reached from here: the toolbar reads Editing <name> with Discard, and Save stays disabled until something changes.
- Save validates first. A draft that Entra would refuse is stopped before the round-trip — Save greys out and says why: nobody included, nothing enforced, no target resource, a guests clause limited to chosen tenants with none chosen, the retired approved-client-app control — and an authentication strength together with Require multifactor authentication in one grant, which Entra does not allow. The built-in Multifactor authentication strength is the same set of combinations as the control, so pick one: the strength, or the control with the strength set to none. Switching a policy from a strength to Require MFA clears the strength on save, so the two never end up on the policy together. A Block policy is not affected.
- Open folds stay open. The folded panels inside a step — include and exclude guests and external users, exclude selected locations, exclude platforms — stay open while you tick inside them, in the builder and on the card edit alike; one you closed stays closed.
- Preview. 🗂 Card is the policy card it will be; { } JSON the exact body Graph receives (a PATCH body when editing); 🧪 Preflight evaluates the draft with What-If against synthetic sign-ins — a member of what it includes from outside and from inside a trusted location, a member of what it excludes, a B2B guest — beside the loaded policies, so each row also names the other policies that apply; ± Diff compares the tenant's policy with the draft when editing. 📋 Change plan exports the op list, the step summaries, the hints and the body as Markdown, to hand to a reviewer before applying it here (R22).
- The write. Create POSTs the policy report-only, reads it back with the same settle schedule 📥 Import uses, re-reads the policy set and opens 🗂 Policies filtered to it. Off is allowed; On needs a typed ON. The policy's own exclusion group (the baseline's naming rule,
CAB-SEC-U-CAnnn-Exclusionfor CloudFellows) is created first, empty, so the policy never names a group that failed to exist — untick it to use existing groups only. An edit PATCHes whole sections — conditions, grant controls, session controls — and only the ones that changed, so a clause nobody was looking at (a guest exclusion, a device filter) cannot be dropped. A staging prefix such as(NEW)stays on an edit. - Recovery. A created policy is restorable for 30 days from ♻ Deleted; a created group can be deleted in 👥 CA groups; an edit can be reverted from its 🕓 Changes entry.
📵 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, and a Methods registered column lists every MFA method the registration report holds for the account — the default first, bold and starred, phones in the retirement colour, the raw Graph name on hover — so a verdict can be checked against the record it was made from. The same list is in the Markdown report and, as two columns (raw values and labels), in the CSV.
- Countdown, chips, exports — both dates, per-verdict filter chips, a capped on-screen table, Markdown report and full CSV.
Reads only; no tenant settings are changed. 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 documenteduser.memberof/device.memberofshape; 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.
🧹 Entra device cleanup BETA writes to tenant
Workspace 03 · Entra Tools, T59 — ported from TUNO T25. Stale directory devices, taken out in two deliberate steps: disable after 30 days without contact, delete after 90 — and only what is already disabled. A machine goes dark before it goes away, and the gap between the two is where somebody notices the laptop in the drawer still matters.
- The read — the Entra device objects (
/devices), not the Intune enrolment:approximateLastSignInDateTimeis the directory's own last contact, and its name says approximate. No new permission:Directory.Read.All, held from sign-in, covers it. Both thresholds can be changed in the toolbar; the buckets follow at once. - The lifecycle row UPDATED (0.2.0) — the stages are the navigation: Active, Stale and enabled (the disable candidates — a device past the delete line that is still enabled is STILL only a disable candidate), Disabled and waiting, Delete candidates (disabled and past the delete line), Never seen (no last sign-in — unknown is not stale, so no action is offered, a pill beside the row). The 30-day and 90-day lines are drawn between the stages; click one to open its card. An empty stage says what is true instead of nothing.
- ① Disable · ② Delete · ↩ Re-enable — tick devices and the bar appears. Every write reads the device again first: one that signed in since the read is skipped (it woke up), one re-enabled since is refused at delete (somebody wants it back). After the write it is read back; only a verified outcome moves a device between tabs. Delete must be armed by typing DELETE: a deleted device object takes its BitLocker recovery keys with it, and Graph refuses Autopilot-registered devices until they leave Autopilot. A device marked managed is enrolled in Intune — removing the directory object does not unenrol it; retire it in Intune first.
- Results — the run ledger, as on every batch write: a row per device turning disabled, re-enabled, deleted, skipped, refused or failed, with ■ Stop between rows. 📝 Report writes one Markdown file: the summary, every action with its outcome, and what is left.
Writes need Device.ReadWrite.All — asked for at the click, and new in New-EncaAppRegistration.ps1 at this build, so a customer tenant gives admin consent once more. Consent is not enough on its own: Graph lets only a directory role that may change devices write them — Cloud Device Administrator or Intune Administrator. A 403 after consent is that role gate, and the row says so.
🔑 App access review BETA writes to tenant
Workspace 03 · Entra Tools, T60. App governance for tenants without Defender for Cloud Apps — Microsoft 365 Business Premium, Entra ID P1. Every app that can reach this tenant's data, one row per app: what it can do, who let it in, whether it is still used, and a verdict in plain words. It replaces the community Export-MsIdAppConsentGrantReport workbook: same data, read correctly, judged per app instead of per permission name, and with a decision you can act on and find again.
- The read — the service principals with their application permissions, every delegated grant (
oauth2PermissionGrants), the app registrations (owners, secrets, certificates), the directory roles held by apps, and the consent settings. Nothing new is needed to read: the read permissions held from sign-in cover it. Graph returns at most 20 application permissions per app in this read and says nothing about the rest; an app that comes back with 20 is read again on its own, so an app with 88 shows 88. ⌚ Last use & who consented adds the last sign-in of every app and the consent events of the last 30 days (AuditLog.Read.All, asked once, plus Reports Reader, Security Reader or Security Administrator). - The verdict — a tier per app, the worst of what it holds: TIER 0 (can give itself or others more: assign roles, grant permissions, change authentication or Conditional Access), ENDPOINT (can push to every device), HIGH, MEDIUM, LOW, INFO. A delegated permission tops out at HIGH: it acts as the signed-in user and never more. The origin is said too — your organisation, a partner (Limon-IT, Dovilo, CloudFellows; ⚙ Partners edits the list), Microsoft, or a third party with its verified publisher or the word unverified. Microsoft's admin tools (Graph CLI, Graph Explorer, Azure CLI and PowerShell, the Exchange, SharePoint and Teams shells) are judged like any third-party app, because a consent to them is a consent to everything they can ask for. 📥 Your table loads your own classification in the community format (Type, Permission, Privilege, Reason) and it wins over the built-in rules.
- The tabs — Needs a decision (Tier 0 and endpoint first, then unverified, unused for 90 days, client secrets, consented for every user), All apps, Who can do what (a matrix: who can assign roles, change Conditional Access, push to devices, read every mailbox, reach every file — one click opens the app), Users' own consents, Consent settings and Decisions. Click a row for the app's card: the plain verdict, the facts, every permission with why it matters, and the five rungs. UPDATED (0.1.1) A certificate that other registrations hold too is named on the card: its name, end date and thumbprint, and every app that holds it, each one click away. The report lists the shared certificates in one table.
- Five rungs, mildest first, each with its impact and its way back before the button. ✓ Approve changes nothing. 👥 Restrict assigns one group and switches assignment required on — anyone outside is refused at sign-in (direct members only). ✂ Reduce revokes the permissions you tick. 🧊 Quarantine disables sign-in for this tenant; ↻ Enable again undoes it with nothing lost. 🗑 Remove revokes every grant and deletes the service principal, and only after a quarantine. Every decision is written to the app's own Notes field as one line —
ENCA T60 · Quarantined 2026-10-18 · by · review 2027-01-18 · reason— so it is in Entra, not in a spreadsheet, and the next read shows it. ↩ Grant back and ↩ Restore (within 30 days) sit on Decisions; this browser keeps what was taken. ENCA's own registration is refused for every rung but Approve, and Microsoft's own services for Revoke and Remove. - Consent settings — who may consent (users, to what), the admin consent requests (admin consent workflow) and the low-impact permission classification, each with Microsoft's recommendation. Switch admin consent requests on with a group as reviewers and the days a request stays open: users then ask instead of being stopped cold, and the request reaches the group.
- Report — 📝 Report writes one Markdown file (the summary, the queue with the reason for each app, the matrix, the settings and the decisions). ⬇ CSV has one row per app; ⬇ Permissions CSV one row per app and permission.
Writes are asked for at the click and are new in New-EncaAppRegistration.ps1 at this build, so a customer tenant gives admin consent once more: Application.ReadWrite.All (Notes, sign-in on or off, delete and restore), AppRoleAssignment.ReadWrite.All (Restrict, and taking an application permission away or back), DelegatedPermissionGrant.ReadWrite.All (taking delegated permissions or a user's consent away or back) and Policy.ReadWrite.ConsentRequest (the admin consent requests). Roles: Cloud Application Administrator or Application Administrator for the apps and for the admin consent requests; Privileged Role Administrator to give a Microsoft Graph application permission back.
🛡 Securing M365 BETA writes to tenant
Workspace 03 · Entra Tools, T61. The tenant's Microsoft 365 security settings read against a hardening baseline, and fixed from the browser where Microsoft Graph can change them. On the publisher tenant every row carries its CIS Microsoft 365 Foundations Benchmark v7.0.0 number and L1/L2 profile; on every other tenant the rows are ENCA's own wording and the numbers stay out (the CIS terms of use). Each row says how it is reached:
- Fix in browser — Graph reads it and can change it: the authorization policy (app registration, tenant and security-group creation, BitLocker keys, user consent, guest access and invitations), device registration (who may join, the quota, local admins, LAPS), the default app management policy (no secrets, 180-day lifetimes), the admin consent workflow, authentication methods (Authenticator number and location context, companion apps, SMS and voice, email OTP, system-preferred MFA, per-user MFA), password rules (banned words, on-premises protection, smart lockout), Microsoft 365 groups creation, public groups, shared-mailbox sign-in, password expiry, the idle-session timeout, the Office Store and trials, Forms phishing, third-party storage, Intune's no-policy default and personal enrolment, PIM approval for Global Administrator and Privileged Role Administrator, and SharePoint's legacy auth, external sharing, resharing and domain list.
- Check only — read and judged, changed elsewhere: synced or licensed admins, the number of Global Administrators, standing privileged assignments (→ 02 PIM-buddy), MFA registration, password hash sync, invitation domains, access reviews.
- CA engine — the 17 Conditional Access controls judged by the same engine as 🛡 Checks → CIS 5.2.2; built in 🏗 Policy builder.
- Mail DNS — SPF, DKIM selectors and DMARC per mail domain, looked up from the browser through Cloudflare's DNS-over-HTTPS (public names only, no token).
- Portal — attest — settings with no API (SSPR, LinkedIn, the keep-me-signed-in prompt, My Groups, the admin center restriction, Sway…): the portal link and the value, then ✓ Attest with a reason.
- PowerShell script — Exchange, Defender for Office, Purview, Teams and the SharePoint settings Graph does not expose: 📜 Audit script downloads a read-only script (Get-* only; ExchangeOnlineManagement, Microsoft.Online.SharePoint.PowerShell, MicrosoftTeams) that writes one JSON file; 📥 Load script result fills the rows. A result for another tenant is refused.
- Fabric (phase 2) — the Fabric tenant settings need a second API permission and are not read yet.
- Fixing — tick failing rows (or 🔧 Fix this… on one) and ✎ Review the change plan: every setting with its impact and way back, the inputs a fix needs (a group for join or approvers, banned words, allowed domains, a number), and the writes exactly as sent. Two fixes on one object are one write — the device registration policy is always written whole, because a write without the quota resets it to 0. Type FIX; the run ledger shows each write, and every setting is read back. The values it changed are kept as a snapshot in this browser (⬇ downloadable): ↩ Undo writes them back after a typed UNDO.
- ✓ Accept with a reason keeps a failure on purpose; ✓ Attest confirms a portal setting. Both are written to the tenant — the open extension
nl.limonit.enca.hardeningon the organization (2 KB, so reasons are 60 characters) — so every admin and every browser sees them. - On demand — 🔐 Read per-user MFA (one read per member, batched by 20) and 📬 Find shared mailboxes (unlicensed accounts with a mailbox, their mailbox type read one by one; a token may only read mailboxes you can open, and the row says so).
Reading uses the sign-in permissions; areas that need more (SharePoint settings, Microsoft 365 org settings, Forms, Intune, the MFA report, PIM, access reviews, directory sync) are read after one click on 🔑 Read more areas, read-only. Each fix asks for its own write permission at the click — e.g. Policy.ReadWrite.Authorization, Policy.ReadWrite.DeviceConfiguration, Policy.ReadWrite.ApplicationConfiguration, Policy.ReadWrite.AuthenticationMethod, Directory.ReadWrite.All (password rules, group creation), SharePointTenantSettings.ReadWrite.All, RoleManagementPolicy.ReadWrite.Directory, Organization.ReadWrite.All (the record). They are not added to New-EncaAppRegistration.ps1: an admin consents at the first click. The role still decides — the inspector names it per row.
🛰 Egress & location impact BETA
A temporary tool in Workspace 03 · Entra Tools (T57), here until the egress decision of P-2716 is made. Conditional Access evaluates the public IP that reaches Entra: behind a cloud proxy (Netskope, Zscaler) that is the proxy's egress, not the office line — the X-Forwarded-For header is ignored — and the country is looked up from that same IP. A policy that excludes the office, or all trusted locations, then treats everyone as outside.
- Input — the egress IPs or ranges (IPv4 and IPv6, CIDR allowed). ⌕ Look up in the sign-in log reads 30 days of sign-ins (interactive first, then non-interactive;
AuditLog.Read.All, asked once) for the country Entra recorded for the egress — the country Conditional Access uses — and for the office range. An address is looked up in Entra's own spelling (an IPv6 address written out in full is still found), and a range by its prefix — an IPv6 /64 or /48, an IPv4 /24 — keeping only the sign-ins inside it, so an office that is only known by its IPv6 prefix gets a country too. With no sign-in from that IP, pick the country by hand; an unknown country is reported, never guessed. - Today vs via the egress — every policy with a location condition, judged twice: from inside every trusted IP location (in the office country), and from the egress. Where the location rule does not match, the cell reads passes — the sign-in gets through that policy — with why: the place is trusted, is in a location the policy excludes, or is not in its locations. Impact: everyone blocked and break-glass blocked (lockout risk), a group or workload identities blocked, an extra prompt or session control, protection lost, no change, or cannot judge (a GPS country location, the legacy MFA trusted IPs, an unknown country). A policy that targets no resources, or names All locations with no exclusion, is called out as not depending on the location. Off and report-only policies are judged as they would act once on.
- IPv4 and IPv6 — a sign-in reaches Entra over IPv4 or IPv6, never both. An egress list holding both families is judged as two paths: the table shows Via egress IPv4 and Via egress IPv6, each placed in the named locations on its own, and the impact is the worse of the two, with which family under it when they differ (over IPv6 only). A location that holds only one family no longer makes the list read partly inside. Paste the proxy's IPv6 egress too when it has one — a v6 path that misses the trusted locations is where a dual-stack client gets blocked.
- If trusted (option A) — the egress judged as if it were in a NEW named location marked trusted. Policies on all trusted clear; policies that name the office location directly do not, and a finding says which locations to add the egress to instead.
- Findings — lockouts, break-glass behind a location block, a policy that targets nothing, the policies on all trusted, CAE strict location enforcement (a proxy that sees a different IP than the resource blocks the session; standard mode gives a one-hour token), an egress list only partly inside a location, IPv6 ranges on the office and none in the egress.
- The options — A: dedicated egress IPs from the proxy vendor in a named location marked trusted (never the vendor's shared ranges: other customers egress from them too). B: a steering bypass for the Microsoft sign-in endpoints, so Entra sees the real IP. C: pin the proxy to a PoP in the allowed country. D: device and authentication trust (compliant device, phishing-resistant MFA) instead of the network for people; keep IP rules for service accounts and workload identities. Break-glass never depends on a location.
Reads only. Markdown export for the change record: a narrow verdict table (policy, state, today, via the egress, impact), then who, excluded, target, rule and control per policy — it fits a Markdown preview without scrolling sideways, and follows the table's Changes only / All / On only. Lives in Workspace 03 under ⏳ Temporary tools and is removed once the egress decision has been made.
📥 Import writes to tenant
Restore a backup (zip or extracted folder). Dependencies are created first (create-if-missing), then the policies — created Off, read back, then set to the state chosen under Land new policies as (Off, or Report-only), unless a replacement inherits an existing state.
- Groups are attached by name UPDATED — the group ids in a file belong to the tenant it was exported from and are never written into a policy. Every group a policy names is created here, or found when one with that name exists, and the policy gets this tenant's group. A group that cannot be created holds back the policies that name it (they fail with the reason) instead of letting them land on an id this tenant does not have — an exclusion naming a group that does not exist excludes nobody, break-glass included. The 👥 panel counts the groups and names every correction: one source id exported under two names (Joey's CA005 and CA006) gives each policy the group that carries its own name, and a policy's own exclusion group filed under another name (Joey's CA403 and CA404) is created under the name the baseline gives that policy. A backup restored into the tenant it came from binds to the same group objects. Ids no file explains — a user, a group, a named location — must exist here, or the policy is not created.
- Assignment mode — 🧩 As shipped NEW: the policies keep the includes and exclusions the file ships, on the groups created or found by name. Nothing already in the tenant is touched and the policies land Off. Pre-selected for a baseline that is not CloudFellows (Joey Verlinden's), which has no
CAD-SEC-U-DG-*deploy groups of its own. - 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 CloudFellowsCAB-SEC-U-CAnnn-Exclusionset), 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. - 🏢 SMB Basic from ENCA's own copy NEW — 📥 Import baseline on 🧬 Baseline (SMB Basic), the security-defaults card and Prepare import in ↗ Guided rollout load the catalog ENCA carries: 19 policies, their 19 exclusion groups, the shared-device group, the break-glass group, the named location
SMB - Allowed countries(the Netherlands — adjust it in 📍 Locations) and a custom strengthSMB - Passkey (FIDO2), all created by name, assignment as shipped. Choosing it makes SMB Basic the tenant's active baseline, so the restricted-unit step offers its four units and files each exclusion group into its section's unit. CA103 and CA201–CA204 always land Off; Entra knows the compliant network location only once Global Secure Access signalling is on, so before that it may refuse them — the import then reports those as failed and the rest lands; import again once 🌐 Global Secure Access is green. - From the repository — 📥 Import baseline on 🧬 Baseline (Joey Verlinden) and Fetch latest in ↗ Guided rollout need no zip: the live read of his repository ships the policy files,
Config/GroupsandConfig/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. A downloaded copy of the repository imports too (its files are UTF-16). - 🚨 E-Admins from a CloudFellows backup — the emergency-access policies (CA1100–CA1105) are expected under every baseline. Besides + Create (ENCA's own copy, above), the 🚨 panel still takes a newer copy from a CloudFellows backup ZIP (or an extracted folder): it takes only the E-Admins policies and the groups, locations and authentication strengths they use into this import, renames the break-glass group to the target baseline's (
CAB-SEC-U-BreakGlass→CA-BreakGlassAccounts - Excludefor Joey), files the groups they name in the break-glass restricted unit (never the Admins one, and never a deploy group), and lands them Off whatever the backup says — an emergency-access policy switched On in a tenant without the trusted locations or the phishing-resistant methods it expects locks the emergency accounts out. - 🛫 Pre-flight NEW — at the top of every import, read from the tenant before anything is written: a policy this file would create that is already here by name (read on /beta, so the compliant-network policies v1.0 leaves out are seen) is skipped and never created twice — nor is one this page created earlier in the session, even when the policy list has not shown it yet; this baseline's policies that are here and Off while the run lands Report-only, with a tick to set them Report-only too (each read back); a policy that is in the tenant twice; room for the new ones; each custom authentication strength — found, reused when one with exactly the same methods exists under another name, or created — and the limit of 15; the named locations; Global Secure Access signalling for the compliant network location; the break-glass group and its members; the Passkey (FIDO2) method when a policy asks for a passkey only; the device setting Require MFA to register or join devices (it must be No for a register-or-join policy such as SMB CA007 to be enforced); and, for a policy that treats every Windows PC and Mac that is not Entra joined as unmanaged (SMB CA008), how many enabled devices that is — hybrid joined, Macs without Platform SSO, registered only; for an app protection policy (SMB CA009), whether Intune app protection policies for iOS and Android are assigned; a group the file creates dynamic (SMB's shared-device group) that already exists as an assigned group; the applications the policies name that have no service principal here yet (created first by the run); security defaults. A ✗ holds Import until it is fixed; ⟳ Check again reads it all once more. During the run a state Conditional Access answers “does not exist” for — a policy created seconds earlier — is tried again, and once more at the end of the run, before it is left Off; a strength or location that could not be created stops only the policy that needs it, before its create; a policy that names a service principal the run has just created (Azure Windows VM Sign-In, the Defender mobile apps) — or, since build 331, a named location or authentication strength the run has just created — waits up to about 1.5 minutes for Conditional Access to know it, instead of failing with a bare 400.
- Land new policies as NEW — Off (the default) or Report-only. A new policy is still created Off and read back first; only then is the chosen state written. On is never offered here: a policy goes On after you have read what it would do. 🚨 E-Admins taken into an import from another file always land Off, and a replacement keeps the state of the policy it takes over. The 🛡 security defaults card opens the import on Report-only, because step ② reads what the policies would do — and if Entra refuses Report-only while security defaults is on, the policy stays as created, Off and verified, and the report says so.
- 🚨 + Create the 6 E-Admins policies NEW — ENCA carries its own copy of CA1100–CA1105, read from cloudfellows.dev on 18 Oct 2026, with the groups (Emergency_Access1, Emergency_Access2, the break-glass group) and the Phishing-resistant MFA + TAP strength they use. A file without them shows Create as the button; a CloudFellows backup ZIP or folder is only the way to bring a newer copy. A Joey Verlinden import from the repository — 📥 Import baseline, ↗ Guided rollout's Fetch latest, the security defaults card — adds them by itself, ticked, the break-glass group becoming his (
CA-BreakGlassAccounts - Exclude), landing Off. - Read back, patiently UPDATED — Conditional Access is eventually consistent: a policy created a moment ago can answer “does not exist” to a read by its own id. Every read after a write is repeated with a short backoff (about 20 seconds for a missing policy) before it counts. A policy Entra is slow to show is finished at the end of the run, not failed: its row reads created · checked again at the end of the run, the import carries on with the rest, and when the other policies are done every such policy is read again with a longer wait (up to about a minute). Once it shows, it is finished exactly as planned — verified, given its state, and in ♻️ Match & replace the old version switched Off — and its row and the report mark it as done on the second check. Only a policy still invisible after both checks is reported as failed, with its id: it is almost certainly there, Off, and a re-run skips it by name, so finish it with 🧹 Housekeeping → ⇄ Switch over or by hand. The version it was meant to replace is never touched until the new one is verified. Stop while policies are waiting for their second check leaves them created, Off and unverified, with the old versions unchanged. Authentication contexts, locations, strengths and terms of use come along only when a chosen policy names them exactly.
- 🔧 Re-attach NEW writes to tenant — policies already in the tenant that point at group ids from the loaded file (an import before build 315 left them when a group could not be created; 👥 CA groups lists them as referenced but gone) are listed with the group each id meant. One click creates or finds those groups by name, swaps only those ids, and reads every policy back; state, conditions and the other assignments are untouched. A separate click from Import, with its own ledger and report.
- 🤖 Agent policies (preview) UPDATED — Joey's CA5xx block targets agent identities and agents' user accounts. His files are what a read returns, and Graph refused all five as written (a bare 400), so an agent policy is now written in the shape Graph documents for a create: agent identities without the read-only
users: Noneblock and empty service-principal list; agents' user accounts as usersAllAgentIdUsersinstead of theagentsobject; no OData annotations. The agent execution environment (CA503's “sessions initiated from endpoints”) is documented nowhere — if Graph refuses it, the policy is created once more without it and the row and report say that it then covers every session of the agents' user accounts. An agent filter, or users and agents in one policy, cannot be written that way and is refused with the reason. The tenant still needs Microsoft Entra Agent ID (Entra ID P1 or P2 with a Microsoft Agent 365 licence), and agent risk needs ID Protection. CA505 excludes Microsoft's compliant network location, which is referenced by its fixed id and never created — it applies once Global Secure Access signaling is enabled for Conditional Access. - 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 not created and the row names the missing app — dropping an exclusion would widen the policy.
- 🔒 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. The warning only appears for a file that has workload-identity policies.
- Licences are checked per policy before it is written — the same check every policy write goes through (see 🗂 Policies): a policy that needs Entra ID P2, Intune, Defender for Cloud Apps or Purview Adaptive Protection is refused, with the reason on its row, when the tenant's subscriptions cannot show it. Insider-risk policies are accepted in tenants with Insider Risk Management — the Purview suites, E5 Compliance or Microsoft 365 E5 — and a refusal lists the subscriptions that were read.
- 📜 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 and 🔧 Re-attach are separate clicks from Import; units need AdministrativeUnit.ReadWrite.All plus Privileged Role Administrator, groups Group.ReadWrite.All.
↗ Guided rollout NEW
A four-step path from “which policies” to “switch them on”, built on the tools that already do each part. The rollout screen itself reads and writes nothing: every step opens the existing tool, and every tenant change goes through that tool's own review, permission check and confirmation.
- The steps — a numbered strip across the top: 1 Scope, 2 Plan, 3 Observed impact, 4 Go live. Click any step to jump to it, or Continue → to take the next. A line above the steps names the tenant (or Demo — writes are simulated) and how many policies are selected.
- Scope — the rollout works on the policies ticked in 🗂 Policies, listed here with their current state. Choose policies opens the list to tick them; Compare a baseline opens 🧬 Baseline; Check deployment prerequisites opens the baseline's Deployment guide.
- Plan — Back up selected policies runs 🗄 Backup from the 🗂 Policies action bar, Prepare an import opens 📥 Import, Review groups & exclusions opens 👥 Conditional Access groups. The backup is a file you keep; ENCA does not check that you saved it or that your rollback works.
- Start from a baseline (on the first two steps) — CloudFellows: Choose ZIP opens 📥 Import and the file picker for your baseline backup ZIP. Joey Verlinden: Fetch latest reads the newest release of his repository — policies, groups and named locations — and opens it in 📥 Import with the release and commit named. If the release or any of its dependencies could not be read completely, nothing is opened (never an older copy) and you are asked to retry. Either way you land in the import review: assignment mode, groups created or found by name, and + E-Admins from a CloudFellows backup ZIP for the shared emergency-access policies. Imported policies arrive Off.
- Observed impact — shows sign-ins in the loaded window, users with a predicted denial and users with a predicted prompt, taken from a completed 🎚 Report-only impact read of this tenant that is newer than the policy snapshot. A capped, running or older read is not used, and the step says impact is not checked yet. The totals describe that report, not necessarily your selection. Open report-only impact, Review configuration findings (🛡 Checks) and Test a sign-in scenario (🧪 What-If) are one click away. A window with no matching sign-ins is no evidence that the rollout is safe.
- Go live — three acknowledgements you tick yourself: the selection, exclusions and emergency access are reviewed; the current definitions are saved with a rollback procedure; the sign-in impact is reviewed. They are your statements, not an automated validation. With all three ticked and at least one policy selected, Review policy state change → opens 🎚 Policy state from the 🗂 Policies action bar for the selection, where each proposed state is named and confirmed as usual.
- Nothing is kept — the acknowledgements live in this session only. Refreshing the policies, changing the selection or switching tenant clears them.
No permissions of its own. The import and the state change it leads to write to the tenant under 📥 Import's and 🗂 Policies' permissions and confirmations; Fetch latest reads the public repository on GitHub. Back to policies in the toolbar returns to 🗂 Policies.
General
- Tabs — tools open in browser-style tabs under the header; Overview returns home, a tab's × closes it, and Close all workspaces in the account menu closes every tab at once. See 🧭 Workspaces & navigation.
- 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=1to 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.
- Version — from 2.0 the label counts releases within the major version:
v2.0.0,v2.0.1… The build number underneath keeps counting and is shown in each release's header on 📋 What's new. The beta channel readsv2.0.1-beta.N: the release it is a beta of. - 📋 What's new — every release, newest first. From 2.0 the page shows the 2.0 notes open and folds all of 1.x into one 📦 Archive — version 1.x, closed, with its release count and date range — still there and one click away. The overlay after sign-in lists what changed since your last visit within the current major version only, so someone last here on 1.x is told about 2.0 rather than handed the tail of the 1.x log.
- 🗺 Roadmap — where ENCA is heading: shipped and shipping, in beta today, next, self-hosted, later and on the horizon. Each item carries a permanent reference (
R33), and a shipped item moves to Now with its reference intact rather than being deleted. - 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.