Skip to main content
CMDashCybersecurity Maturity Dashboard
← Back to Microsoft Entra ID

Coming soon

Microsoft Entra ID — Integration Setup

Note: This guide covers Microsoft Entra ID (formerly Azure AD) as an identity posture source — MFA registration, privileged-account ratio, and dormant accounts, read through Microsoft Graph. It is a separate integration from Microsoft Defender for Endpoint (integration-setup-defender.md) and Microsoft Defender for Cloud (integration-setup-azure-defender.md), even though all of CMDash's Microsoft integrations authenticate through the same app registration.

How CMDash connects to Microsoft

A CMDash deployment runs its Microsoft integrations (Entra ID, Intune, Purview, Defender for Endpoint, Defender for Cloud, Sentinel) in one of two modes. The operator chooses the mode; you do not.

Per directory (AZURE_MULTI_TENANT=true)Single directory (the default)
Used byCMDash SaaS; self-hosted installs that serve several organisationsSelf-hosted installs that serve one organisation
Which directory is readThe one your administrator signs in and consents from, recorded on the connectionThe deployment's AZURE_TENANT_ID
Microsoft cloudsCommercial (includes GCC), GCC High, DoDCommercial
TokenApp-only (client credentials), minted at your directory's authorityApp-only (client credentials), minted at AZURE_TENANT_ID
App registrationCMDash's multi-tenant app, one per cloud (commercial; US Government)Your own, single-tenant

CMDash SaaS: available once CMDash's Microsoft apps are registered. The per-directory connect flow is built and switched on for SaaS, but CMDash has not yet registered its production Microsoft apps (see For the CMDash operator). Until it has, Connect answers that the Microsoft app registration is not set up on this deployment, and nothing is created.

Connecting on CMDash SaaS (per directory)

Before you start

  • An account that holds Global Administrator or Privileged Role Administrator in your directory, active at the time you connect (activate it first if you use Privileged Identity Management). Only these two roles can grant consent to application permissions for Microsoft Graph, so CMDash requires one of them and checks it.
  • An Entra ID P1/P2 licence if you want the dormant-account metric (the rest works without it).
  • Your Microsoft cloud: Commercial, US Government — GCC, US Government — GCC High, or US Government — DoD. If you are not sure, ask whoever administers your Microsoft 365 subscription. Choosing wrong is harmless: CMDash reads your directory's own published cloud and refuses a mismatch with a message saying so.

Steps

  1. In CMDash, go to Integrations → Microsoft Entra ID → Connect.
  2. Choose your Microsoft cloud (the picker lists only the clouds this deployment can serve).
  3. Microsoft's sign-in page opens. Choose your administrator account and sign in.
  4. Microsoft shows the permissions CMDash's app asks for. Review them and Accept on behalf of your organisation.
  5. You are returned to CMDash, and the connection runs its first health check. Open the integration's page: its Connected directory panel shows the directory (tenant) id and the Microsoft cloud it is now bound to. Check it is yours — this is how you notice having connected the wrong organisation's directory.

Finish within 10 minutes, in the same browser you started in: the handshake is bound to that browser by a short-lived cookie, and expires. The cookie is Secure, so CMDash must be served over HTTPS (http://localhost is the one exception browsers make, for development), with its API on the same host as the app (the /api path, which is how CMDash SaaS and the bundled Caddy configuration serve it). If the browser comes back without the cookie, nothing is connected and the Integrations page says so.

If the first health check fails right after you accept (the connection shows Unhealthy, typically HTTP 403), Microsoft has not finished applying the fresh consent yet. It recovers by itself: every 30 minutes CMDash re-checks an Unhealthy connection with a newly minted token and marks it Active as soon as the check passes, and scheduled syncs resume. To retry sooner, click Sync now on the integration's page; every retry uses a newly minted token.

What CMDash checks before it keeps anything

  • Your directory is proven, not asserted. CMDash reads your directory id from the ID token Microsoft returns to CMDash's server — never from a value in the browser's address bar.
  • The account is an administrator who could grant the consent: the ID token must list Global Administrator or Privileged Role Administrator.
  • The cloud matches. Your directory's own published metadata must name the Microsoft Graph of the cloud you chose (a GCC High directory connected as "DoD", or the reverse, is refused).
  • The consent comes from the same directory the sign-in proved.
  • Only CMDash's own server can finish the flow, and only for the browser that started it.

If any check fails, nothing is connected and the reason is shown on the Integrations page.

What is recorded, and how to change or revoke it

  • The connection records your directory id and cloud, and the object id of the administrator who consented (no name or e-mail). It stays bound to that directory: reconnecting it (after a failure, say) only accepts consent from the same directory. To connect a different directory, Disconnect first — the disconnect is audited and releases the binding.
  • CMDash stores no refresh token and no credential of yours. It holds a one-hour access token for your directory, minted from CMDash's own app registration and re-minted as it expires.
  • The binding does not outlive the administrator who made it. Each time CMDash re-mints the token, it first looks that administrator up in your directory (User.Read.All). If the account has been deleted or disabled, CMDash deletes its token, marks the connection Disconnected, and records why in the audit log (and, when a sync found it, in the connection's sync history); an active Global Administrator or Privileged Role Administrator of the same directory reconnects it. If the lookup itself cannot be answered (a Microsoft Graph outage), that attempt fails and is retried; nothing is disconnected on an unknown. The administrator who consents therefore needs an account of their own in the directory — a member, or a guest invited into it: a sign-in with no account there is looked up in vain, and the connection ends at its first token renewal.
  • To revoke CMDash's access entirely: Microsoft Entra admin center → Enterprise applications → CMDash → Delete (or set Enabled for users to sign in? to No). Every CMDash connection to your directory stops at its next token request.

What else uses this connection

  • SaaS discovery. When someone in your CMDash workspace runs SaaS discovery → Sync, CMDash reads up to 10,000 of your directory's sign-in log records from the last 30 days with this connection's token (auditLogs/signIns, AuditLog.Read.All), and keeps, per application and user, only the application name, the user's principal name, first and last seen, and a count.
  • Remediation write-backs are not available through it. CMDash's app holds read-only permissions, so an Entra ID account deactivation or reactivation (and an Intune configuration push) cannot go through a per-directory connection: CMDash disables those actions, with the reason, rather than let them fail after approval.

Permissions CMDash's app holds in your directory

Microsoft's consent screen lists every permission on CMDash's app registration, so one consent covers all of CMDash's Microsoft integrations, including ones you have not connected yet. All are application permissions and all are read-only:

APIPermissionUsed forClouds
Microsoft GraphAuditLog.Read.AllEntra ID: the MFA-registration report (reports/authenticationMethods/userRegistrationDetails), and users' sign-in activity (dormant accounts)Commercial, GCC High, DoD
Microsoft GraphUser.Read.AllEvery Microsoft connection: confirming, at each token re-mint, that the administrator who consented is still an enabled account. Entra ID: the user list that carries sign-in activity (dormant accounts)Commercial, GCC High, DoD
Microsoft GraphDeviceManagementManagedDevices.Read.AllIntune: managed-device inventoryCommercial, GCC High, DoD
Microsoft GraphSecurityAlert.Read.AllPurview: DLP alertsCommercial, GCC High, DoD
Microsoft GraphSecurityIncident.Read.AllSentinel: incidentsCommercial
WindowsDefenderATPMachine.Read.All, Vulnerability.Read.All, Alert.Read.AllDefender for EndpointCommercial
Microsoft Graphopenid, profile (delegated)The sign-in step only: proves who signed in and from which directory. No token from it is kept.Commercial, GCC High, DoD

CMDash never writes to your directory and requests no *.ReadWrite.* permission. Defender for Cloud and Sentinel's analytics rules read Azure Resource Manager, which is authorised by an Azure role you assign, not by consent — see their own setup guides.

US Government clouds (GCC, GCC High and DoD)

CloudSign-in and tokensMicrosoft GraphDefender for Endpoint API
Commercialhttps://login.microsoftonline.comhttps://graph.microsoft.comhttps://api.securitycenter.microsoft.com
US Government — GCChttps://login.microsoftonline.comhttps://graph.microsoft.comhttps://api-gcc.securitycenter.microsoft.us
US Government — GCC Highhttps://login.microsoftonline.ushttps://graph.microsoft.ushttps://api-gov.securitycenter.microsoft.us
US Government — DoDhttps://login.microsoftonline.ushttps://dod-graph.microsoft.ushttps://api-gov.securitycenter.microsoft.us

Sources: Microsoft Entra authentication and national clouds, Microsoft Graph national cloud deployments and, for the Defender host, Defender for Endpoint for US Government customers (all fetched 2026-10-07). GCC signs in and uses Graph exactly like Commercial; it is a separate choice because its Defender for Endpoint host differs. A government connection's token is only ever sent to its own cloud's hosts; CMDash refuses to follow any link that leaves them. In GCC High and DoD, Entra ID, Intune, Purview and Defender for Endpoint are available; Defender for Cloud and Sentinel (which also read Azure Resource Manager) are available in Commercial and GCC only. A connection made before GCC was offered is recorded as Commercial: a GCC customer using Defender for Endpoint disconnects and reconnects choosing GCC.

Self-hosted: single directory (the default)

This is the original setup, unchanged: one app registration in your own directory, named by AZURE_TENANT_ID.

Step 1: Register (or reuse) the CMDash app

CMDash's Microsoft integrations all authenticate through one app registration, identified by the server-side settings AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET. If you already connected Defender for Endpoint, Defender for Cloud, or Sentinel, that app registration already exists — add the Graph permissions below to it rather than creating a second one.

  1. Go to Azure Portal → App registrations → New registration.
  2. Name: CMDash Integration. Supported account types: Single tenant.
  3. Click Register, then copy the Application (client) ID and Directory (tenant) ID.
  4. Under Certificates & secrets → New client secret, create a secret and copy the Value immediately.
  5. Set AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET in the API's environment and restart. Leave AZURE_MULTI_TENANT unset.

Step 2: Grant the minimum Graph permissions

PermissionNeeded forRequired?
Reports.Read.All/v1.0/reports/credentialUserRegistrationDetails — MFA registration + admin flag per userYes — without it the sync fails outright
AuditLog.Read.All + User.Read.All/v1.0/users?$select=id,accountEnabled,signInActivity — dormant-account detection onlyOptional
  1. In the app registration, go to API permissions → Add a permission → Microsoft Graph → Application permissions.
  2. Add Reports.Read.All. Add AuditLog.Read.All and User.Read.All only if you want the dormant-account metric.
  3. Click Grant admin consent for <tenant>.

Do not grant more. CMDash never writes to Entra ID and never reads groups, devices, applications, sign-in logs, or directory objects through this integration. Directory.Read.All, Directory.ReadWrite.All, Policy.Read.All, and the *.ReadWrite.* family are not required.

Permission type: application. CMDash obtains its token with the OAuth client-credentials grant (the https://graph.microsoft.com/.default scope) — there is no signed-in user, so the permissions must be Application permissions with admin consent. Delegated permissions do nothing for this integration.

Step 3: Connect in CMDash

  1. Navigate to Integrations and find Microsoft Entra ID.
  2. Click Connect. CMDash requests a token from https://login.microsoftonline.com/<AZURE_TENANT_ID>/oauth2/v2.0/token with the configured client credentials, checks it against Graph, and marks the connection active. There is no sign-in screen.
  3. No connection Configuration JSON is needed; anything you put there is ignored by this adapter.

A self-hosted install that serves several organisations' directories can use the per-directory mode instead: set AZURE_MULTI_TENANT=true and make AZURE_CLIENT_ID/AZURE_CLIENT_SECRET a multi-tenant registration, following the operator steps below with your own URL. That mode needs CMDash served over HTTPS with the API under the app's own host (the consent handshake is bound to the browser by a Secure cookie); a callback URL on plain HTTP is refused when Connect is clicked, with the reason. Existing single-directory Microsoft connections must then be reconnected: in per-directory mode, a connection with no verified directory refuses to sync, and its stored single-directory token is no longer handed to anything (sync, health check, SaaS discovery, remediation). Its next sync fails, and the health check after that marks it Disconnected until an administrator reconnects it.

What Syncs

Data TypeMicrosoft Graph APISync Frequency
Identity/MFA coverage + privileged accountsPer directory: GET /v1.0/reports/authenticationMethods/userRegistrationDetails. Single directory: GET /v1.0/reports/credentialUserRegistrationDetails?$top=999Every 4 hours by default (configurable per connection)
Dormant accounts (optional)GET /v1.0/users?$select=id,accountEnabled,signInActivity&$top=999Every 4 hours by default (configurable per connection)

Microsoft deprecated credentialUserRegistrationDetails ("will stop returning data on June 30, 2024") and never offered it in the US Government clouds; its replacement, userRegistrationDetails, is available in every cloud and carries the same fields CMDash reads. Per-directory connections use the replacement. Single-directory connections keep the legacy report their operators granted Reports.Read.All for; in a tenant where it no longer returns data, switch the install to per-directory mode (which needs AuditLog.Read.All).

From the registration report CMDash computes the identity coverage summary: total accounts, how many have MFA registered (isMfaRegistered), how many are privileged (isAdmin, taken straight from the report — no title/userType guesswork), and how many privileged accounts have MFA. MFA coverage is reported as a 0–100 percentage and feeds the posture engine's MFA signal and the O.5 identity-posture rollup; it is deliberately not also written as a second, competing maturity observation.

Two identity-posture maturity observations are emitted:

  • entra-privileged-account-ratio — privileged accounts ÷ total accounts, mapped to CIS Controls v8 5.4.
  • entra-dormant-account-ratio — enabled accounts with no interactive or non-interactive sign-in in the last 45 days ÷ enabled accounts, mapped to CIS Controls v8 5.3. An enabled account with no sign-in activity at all counts as dormant.

Both fetches paginate through @odata.nextLink (the current report at Graph's default page size, up to 50 pages; the legacy report and the user list 999 rows a page, up to 20 pages). If a cap is reached while more data remains, the observation is flagged truncated rather than presented as a whole-directory figure.

The dormant fetch is intentionally fail-soft: if it 403s (no AuditLog.Read.All, or no premium licence), the MFA/privileged sync still succeeds and simply omits the dormant observation for that cycle.

Troubleshooting

The connection detail page shows the health check's message; the health check requests the same registration report sync reads, so it fails on exactly the conditions a sync fails on. A connect attempt that is refused comes back to the Integrations page with one of these reasons:

  • Microsoft app registration not set up on this deployment: the operator has not registered CMDash's app for the cloud you chose. On CMDash SaaS this is expected until CMDash registers its apps.
  • Administrator role required (admin_role_required): the account that signed in is not an active Global Administrator or Privileged Role Administrator. Activate the role (PIM) or sign in with an account that holds it.
  • Different directory (directory_mismatch): the consent or sign-in came from a directory other than the one the connection is bound to (or the one the sign-in proved). Sign in to the right directory, or Disconnect first to change directory.
  • Wrong cloud (cloud_mismatch): your directory lives in a different Microsoft cloud than the one you chose. Reconnect and choose the matching cloud.
  • Connect cookie missing (binding_cookie_missing): the browser that came back did not carry the short-lived cookie CMDash set when you clicked Connect — a different browser, or one that could not keep it: CMDash served over plain HTTP (other than http://localhost), or its API on a different site than the app. Finish in the browser you started in; if it keeps happening, fix the deployment: HTTPS, and the callback on the same host the browser calls the API on — the API under the app's own /api path, or INTEGRATION_OAUTH_REDIRECT_URI pointed at the callback on the API's host (and registered as the redirect URI).
  • Session mismatch (session_mismatch): the browser presented a connect cookie, but not the one this handshake was started with. Start again from the browser you will finish in.
  • Superseded (superseded): a newer Connect (or a Disconnect) for the same integration happened while you were signing in, so this attempt changed nothing; the integration shows the result of the latest one.
  • Link expired or already used (invalid_state): more than 10 minutes passed, or the page was reloaded. Start again.
  • Identity not verified (identity_unverified): the sign-in did not produce a valid work-account identity for this app (for example a personal Microsoft account). Sign in with a work account.
  • HTTP 401: the stored access token expired and could not be re-minted (the CMDash app was deleted from your directory, or — single directory — the client secret behind AZURE_CLIENT_SECRET expired).
  • HTTP 403: the registration-report permission (AuditLog.Read.All per directory, Reports.Read.All single directory) was never consented. Right after a fresh consent, Microsoft can take a few minutes to apply it: the connection shows Unhealthy, and CMDash re-checks it every 30 minutes with a newly minted token and marks it Active once the check passes (or click Sync now to retry sooner — each retry mints a new token). If a sync has already failed on it, it stays Unhealthy until a sync succeeds: use Sync now.
  • Disconnected, "the administrator who connected this integration no longer exists / is disabled": the account that granted the consent was deleted or disabled in your directory, so the binding ended (see What is recorded). An active Global Administrator or Privileged Role Administrator reconnects it.
  • Sync fails with "Could not verify the administrator who connected this integration": CMDash could not look the consenting administrator up (a Microsoft Graph outage, or User.Read.All removed from CMDash's enterprise application). The sync is retried; nothing is disconnected. Restore the permission if it was removed.
  • Sync log shows Entra ID Graph API error (<status>): <body>: the paginated fetch got a non-2xx response; the whole sync attempt fails and is retried.
  • Entra ID circuit open — provider recently failing repeatedly: the per-tenant circuit breaker tripped after 5 failures inside a 60-second window and is failing fast for a 30-second cool-off.
  • No dormant-account figure appears: expected when AuditLog.Read.All/User.Read.All are not granted or the tenant has no Entra ID P1/P2 licence.
  • A ratio is marked truncated: your directory is larger than the page cap, so the ratio was computed over a prefix of the directory. It is disclosed rather than silently presented as complete.
  • MFA coverage looks lower than expected: the report counts registration, not enforcement — an account covered by a conditional-access policy but with no registered method is correctly counted as not registered. Guest accounts are included in the denominator.
  • Privileged count is zero: isAdmin comes from the Graph report itself, so a genuine zero is trustworthy and is reported as-is.

Rate Limits & Sync Behavior

SettingValue
Sync frequencyEvery 4 hours by default (syncInterval, in seconds, is configurable per connection; default 14400)
Pagination capSee What Syncs; overflow disclosed as truncated
Rate limit handlingEvery request (including the health check) is paced through CMDash's shared token-bucket limiter at 1,500 requests/minute per tenant for this provider
Circuit breaker5 failures in 60 seconds trips the breaker OPEN for 30 seconds, then a single probe decides whether it closes
Dormant threshold45 days without an interactive or non-interactive sign-in
Observation freshnessIDENTITY_POSTURE observations decay on a 14-day half-life
Token renewalThe app-only access token is re-minted automatically when it is within 5 minutes of expiry (there is no refresh token), and after a failed health check or sync. Per directory, each re-mint first confirms the consenting administrator is still an enabled account
Unhealthy recoveryRe-checked every 30 minutes with a newly minted token; a passing check restores Active and scheduled syncs resume
Failed sync retryJob queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md

Credential Rotation

  • CMDash SaaS: there is nothing on your side to rotate. CMDash rotates its own app's client secret; your consent is unaffected.
  • Self-hosted: the client secret lives in the CMDash server's configuration (AZURE_CLIENT_SECRET, and AZURE_GOV_CLIENT_SECRET for a US Government registration), not on the connection. CMDash does not display an expiry warning for it — set a calendar reminder from the expiry you choose in Azure (24 months maximum). To rotate: App registrations → your app → Certificates & secrets → New client secret; update the variable; restart the API; delete the old secret once connections report healthy. The secret is shared by every Microsoft integration, so rotating it affects all of them at once.

Multiple Instances / Multi-Site

A connection is bound to one directory. If you operate more than one Entra ID tenant (after an acquisition, say), each needs its own CMDash workspace connection. On a per-directory deployment (CMDash SaaS) each workspace connects its own directory; on a single-directory self-hosted install, a second directory needs per-directory mode or its own deployment.

Compliance Framework Coverage

The Microsoft Entra ID integration provides evidence for the following controls. Coverage is automatically mapped when data is synced.

FrameworkControlDescription
CIS Controls v85.4Restrict administrator privileges — privileged-account ratio observation, stamped directly by the adapter
CIS Controls v85.3Disable dormant accounts — dormant-account ratio observation, stamped directly by the adapter (requires the optional permissions)
NIST CSF 2PR.AA-03Users, services, and hardware are authenticated — MFA coverage feeds the identity-posture model's MFA-enforcement dimension
CIS Controls v86.3 / 6.4 / 6.5MFA for externally-exposed, remote, and administrative access — the same MFA-coverage percentage feeds each of these mappings via the identity-posture maturity model

For the CMDash operator: registering the Microsoft apps

This is the one-time setup that makes per-directory Microsoft connections available on a deployment (for CMDash SaaS). Two registrations, one per Microsoft cloud; each is optional, and a cloud without one is simply not offered.

RegistrationWhereServesSets
CommercialA CMDash-owned tenant in the commercial cloud — entra.microsoft.comCommercial and GCC directoriesAZURE_CLIENT_ID, AZURE_CLIENT_SECRET
US GovernmentA CMDash-owned tenant in Azure Government — portal.azure.usGCC High and DoD directories (one Entra instance, login.microsoftonline.us)AZURE_GOV_CLIENT_ID, AZURE_GOV_CLIENT_SECRET

For each registration:

  1. App registrations → New registration. Name: CMDash. Supported account types: Accounts in any organizational directory (Any Microsoft Entra ID tenant — Multitenant).
  2. Redirect URI: platform Web, value <APP_URL>/api/integrations/oauth/callback — for CMDash SaaS, https://app.cmdash.net/api/integrations/oauth/callback (confirm against APP_URL on the hosts; set INTEGRATION_OAUTH_REDIRECT_URI only if the callback is served elsewhere). It must match exactly.
  3. API permissions → Add a permission, all Application permissions:
    • Microsoft Graph: AuditLog.Read.All, User.Read.All, DeviceManagementManagedDevices.Read.All, SecurityAlert.Read.All; commercial registration only: SecurityIncident.Read.All.
    • Both registrations — the Defender for Endpoint API (APIs my organization uses → WindowsDefenderATP; on the US Government registration the same API is listed under the government Defender for Endpoint resource, host api-gov.securitycenter.microsoft.us): Machine.Read.All, Vulnerability.Read.All, Alert.Read.All. Confirm the permission is offered in portal.azure.us before relying on it.
    • Remove the delegated Microsoft Graph User.Read a new registration starts with: nothing uses it. The sign-in step requests only the OpenID Connect scopes openid and profile, which need no entry here, and every token CMDash keeps is app-only. (Left in, it would be granted with the admin consent and appear on the consent screen for no purpose.)
    • Do not add Reports.Read.All (per-directory connections read the current report, which needs AuditLog.Read.All) or any *.ReadWrite.* permission.
    • Do not click Grant admin consent for CMDash's own tenant unless CMDash will connect its own directory: customers grant consent in theirs.
  4. Token configuration → Add groups claim → tick Directory roles, for the ID token. This puts the wids claim (the signed-in user's directory roles) in the ID token; without it every connect is refused with Administrator role required, because CMDash cannot see that the consenting account is an administrator.
  5. Certificates & secrets → New client secret. Copy the Value. Put the Application (client) ID and the secret value into the variables in the table above, on every API host, identically. Record the expiry (24 months maximum) and rotate before it.
  6. Branding & properties: set the publisher domain to a verified CMDash domain, and complete publisher verification for the commercial registration (Microsoft AI Cloud Partner Program ID) so the consent screen shows CMDash as a verified publisher. Microsoft does not support publisher verification in national clouds, so the US Government registration stays unverified.
  7. Leave AZURE_TENANT_ID empty on SaaS. AZURE_MULTI_TENANT defaults to true in docker-compose.saas.yml; do not override it — the deploy preflight refuses AZURE_MULTI_TENANT=false next to an AZURE_TENANT_ID.
  8. Redeploy (scripts/ops/stage-host-config.sh reports, per host, which of these are set), then connect a test directory in each cloud end to end.

Ready to see your posture?

Start a 30-day free trial of any paid plan — a card is required, and it converts unless you cancel — or sign up for Free with no card at all.