RapidValueDocs RapidValue.eu
Docs/Connector reference

🆔 Microsoft Entra ID

Microsoft Entra ID (Azure AD) — directory + provisioning via Microsoft Graph.

This is the broadest connector in the set. It reads users, groups, directory roles, service principals, licences and app roles; it writes group membership and user attributes; and it feeds three signals that most of the platform depends on — MFA registration, SharePoint folder permissions, and sign-in activity. Those three are worth reading closely, because each of them can only tell the truth about part of your tenant, and the platform is deliberate about saying which part.

Identification

Supported auth methods

  • oauth2_client_credentials

The connector authenticates app-only: an app registration in your tenant, a client secret, and a set of Microsoft Graph application permissions. There is no user session behind it and no delegated consent at run time.

What the platform can see is exactly what you granted

Graph permissions are not a formality here — they are the boundary of what the connector is able to observe, and therefore the boundary of what the platform is willing to assert. Each capability has its own permission, and each one is optional:

Capability Graph permission Without it
Read users, groups, directory objects User.Read.All, Group.Read.All, Directory.Read.All The connector cannot sync at all
Write users and group membership User.ReadWrite.All, Group.ReadWrite.All Read-only; the system is a visibility-only target
App roles and Enterprise apps Application.Read.All, AppRoleAssignment.ReadWrite.All App-role entitlements are not discovered
MFA registration state AuditLog.Read.All + Reports.Read.All MFA state stays unknown — see below
Sign-in activity (usage data) AuditLog.Read.All No activity signal; dormancy is not asserted
SharePoint folder permissions Sites.Read.All + Files.Read.All No unstructured lens for this tenant
App-registration credential expiry Application.Read.All No secret/certificate expiry inventory

A missing optional permission degrades one capability. It never breaks the sync, and — the part that matters — it never turns into a confident answer about something nobody looked at.

Importable objects (6)

Object types the wizard offers to import out of the box.

Users

  • name: users
  • iga_target: identity
  • path: /users?$expand=manager($select=id,userPrincipalName,mail)&$select=id,userPrincipalName,displayName,givenName,surname,mail,mobilePhone,department,jobTitle,companyName,officeLocation,accountEnabled,employeeId,employeeHireDate,employeeLeaveDateTime,employeeOrgData

All users in the tenant. For reuse: filter via $filter=accountEnabled eq true

Default field mappings:

IGA field Expression Required
business_id {{ id }}
display_name {{ displayName }}
native_account_name {{ userPrincipalName }}
identity_business_id {{ mail|if_null(userPrincipalName)|lower }}
status {{ 'active' if accountEnabled else 'terminated' }}
is_privileged {{ false }}
identity_id {{ userPrincipalName }}
email {{ mail|if_null(userPrincipalName) }}
first_name {{ givenName }}
last_name {{ surname }}
department {{ department }}
function {{ jobTitle }}
manager_id {{ manager.userPrincipalName|if_null(manager.mail) }}
hire_date {{ employeeHireDate }}
end_date {{ employeeLeaveDateTime }}
location {{ officeLocation }}
company {{ companyName }}
cost_center {{ employeeOrgData.costCenter }}
employee_id {{ employeeId }}
entra_object_id {{ id }}

Security & M365 groups

  • name: groups
  • iga_target: entitlement
  • iga_subtype: security_group
  • path: /groups?$select=id,displayName,description,securityEnabled,mailEnabled,groupTypes,membershipRule

All groups — use for membership provisioning.

Default field mappings:

IGA field Expression Required
business_id {{ id }}
name {{ displayName }}
description {{ description }}
native_identifier {{ id }}
risk_level {{ 'high' if (('Privileged' in (displayName or '')) or ('Admin' in (displayName or ''))) else 'low' }}
privileged {{ ('Privileged' in (displayName or '')) or ('Admin' in (displayName or ''))|to_bool }}

Directory Roles (privileged)

  • name: directory_roles
  • iga_target: entitlement
  • iga_subtype: directory_role
  • path: /directoryRoles

Built-in Entra roles (Global Admin, User Admin, etc.).

Default field mappings:

IGA field Expression Required
business_id {{ id }}
name {{ displayName }}
description {{ description }}
native_identifier {{ id }}
risk_level 'critical'
privileged true

Service Principals / Enterprise apps

  • name: service_principals
  • iga_target: account
  • path: /servicePrincipals?$select=id,displayName,appId,servicePrincipalType,accountEnabled

Apps + service identities — often NHIs

Default field mappings:

IGA field Expression Required
business_id {{ id }}
native_account_name {{ appId }}
display_name {{ displayName }}
status {{ 'active' if accountEnabled else 'terminated' }}
is_privileged {{ false }}
identity_type 'service_account'

License SKUs

  • name: subscribed_skus
  • iga_target: entitlement
  • iga_subtype: license
  • path: /subscribedSkus

Available licenses (M365 E5, etc.) and consumption

Default field mappings:

IGA field Expression Required
business_id {{ skuId }}
name {{ skuPartNumber }}
native_identifier {{ skuId }}
description {{ skuPartNumber }} ({{ consumedUnits }}/{{ prepaidUnits.enabled }} consumed)
risk_level 'medium'
total {{ prepaidUnits.enabled }}
consumed {{ consumedUnits }}

App roles (per Enterprise app)

  • name: app_role_assignments
  • iga_target: entitlement
  • iga_subtype: app_role
  • path: /servicePrincipals?$select=id,displayName,appRoles

Which app roles does each Enterprise app offer? These are the role definitions (e.g. Salesforce.Admin / Tableau.Viewer) — the actual assignments to users are retrieved at runtime via /servicePrincipals/{id}/appRoleAssignedTo.

Default field mappings:

IGA field Expression Required
business_id {{ id }}
name {{ displayName }}
native_identifier {{ id }}
description Discovered app-role definitions for {{ displayName }}
risk_level 'medium'
source_app {{ displayName }}
role_list {{ appRoles }}

Account type templates (6)

Pre-filled account types the wizard's ACCOUNT_TYPES_REVIEW step shows.

👤 Personal accounts — 🟢 governance

  • business_id_suffix: personal
  • purpose_class: personal
  • default_identity_type: employee
  • native_filter: userType eq 'Member' and accountEnabled eq true

Regular employee accounts — fall into JML, certifications and access requests. If your tenant has separate admin or service-account naming conventions, enable the Privileged admin or Service accounts types below for stricter governance per subtype. Otherwise admin/service accounts fall into this catch-all type and you can split them out post-onboarding via Account Rules.

🤝 B2B Guest accounts — 🟡 visibility-only

  • business_id_suffix: guest
  • purpose_class: guest
  • default_identity_type: external
  • native_filter: userType eq 'Guest'

External collaboration via Entra business-to-business (B2B). Visible in IGA for audit + reporting, but NOT in joiner / mover / leaver (JML) or access-review workflows (no employer relationship).

🗄 Disabled / dormant accounts — 🟡 visibility-only

  • business_id_suffix: disabled
  • purpose_class: personal
  • native_filter: accountEnabled eq false

Accounts that are disabled in Entra (offboarded employees, obsolete service accounts). Visibility-only for cleanup audits and access-review sweeps that identify what can be removed.

🛡 Privileged admin accounts (personal) — 🟢 governance (opt-in)

  • business_id_suffix: admin-personal
  • purpose_class: administrative
  • default_identity_type: employee
  • native_filter: userType eq 'Member' and accountEnabled eq true and (startswith(userPrincipalName, 'a-') or startswith(userPrincipalName, 'adm-') or startswith(userPrincipalName, 'admin-'))

Secondary personal accounts for elevated/admin work. Stricter governance (shorter cert cycle, MFA required, JIT recommended). Identification via UPN naming convention (a-/adm-/admin- prefix); ownership via employeeId — the admin account is linked to its human owner by writing the identity's employee_id to the account's employeeId Graph attribute. This requires the provisioning mapping below to be active. If your tenant uses a different correlation field, uncheck the ownership rule and refine via Systems → Account Types → Rules post-onboarding.

⚙️ Service accounts (NHI) — 🟢 governance (opt-in)

  • business_id_suffix: service
  • purpose_class: service
  • default_identity_type: service_account
  • native_filter: userType eq 'Member' and accountEnabled eq true and (startswith(userPrincipalName, 'svc-') or startswith(userPrincipalName, 'svc_') or endswith(userPrincipalName, '.svc'))

Non-human identities: integration users, scheduled jobs, system accounts. Different lifecycle than human accounts — longer dormant threshold, NHI ownership review. Heuristic via UPN naming convention; refine the rule if your tenant uses different prefixes.

⚙ Microsoft-managed system accounts — ⚫ excluded (opt-in)

  • business_id_suffix: excluded-system
  • purpose_class: service
  • native_filter: startswith(userPrincipalName, 'Sync_') or startswith(userPrincipalName, 'ADToAADSyncServiceAccount')

Built-in tenant-system accounts (Directory Sync, On-Premises Directory Synchronization Service, etc.). Managed by Microsoft — IGA should NEVER include them in workflows.

Default write-mappings (13)

Surfaced in the wizard PROVISIONING_CONFIG_REVIEW step for activation. sync_mode and per-type scoping are tunable per mapping.

target_field operator source / expression sync_mode job_type applies_to
displayName passthrough identity.display_name enforce * all types
givenName passthrough identity.first_name enforce * all types
surname passthrough identity.last_name enforce * all types
mail passthrough identity.email enforce * all types
jobTitle passthrough identity.function enforce * all types
department passthrough identity.department enforce * all types
usageLocation passthrough identity.location enforce * all types
employeeId passthrough identity.employee_id set_once * all types
manager lookup find_user_by_upnidentity.manager_email enforce * all types
accountEnabled expression {identity.status|lower} enforce * all types
userPrincipalName passthrough identity.email set_once * account type personal
userPrincipalName expression admin.{identity.email|lower} set_once * account type admin
extensionAttribute1 constant admin set_once * account type admin

Two things about that table are easy to misread:

  • Scoped mappings are matched on the account type's exact business id. The last three rows are not general defaults — they apply only to accounts of the named type, resolved as <connector-id>-<suffix>. A scoped mapping whose account type is not activated simply never fires; it does not silently fall back to "all accounts". This is deliberate: a UPN rewrite that leaked onto every account would be a very expensive accident.
  • accountEnabled carries per-event overrides. The expression shown is what a modify evaluates; the template also declares a fixed true for a create event and a fixed false for a delete. The second of those is the one that matters on Entra: the platform disables rather than hard-deletes, so the audit trail and the account's history survive the leaver. (The create branch is not exercised here — see the limitations below.)

sync_mode distinguishes enforce (the platform keeps re-asserting the value, so an HR correction propagates) from set_once (written at creation and then left alone, because changing it later breaks downstream linkage — a UPN or an employee id is exactly that kind of field).

Note

Every write-mapping has a read-side counterpart, and the two can drift apart: a mapping that writes a value the account type's filter then excludes creates accounts the platform can never see again. That failure mode has its own detector and its own page — see Read/Write Parity.

Default lookups (3)

Helper lookups used by the write-mappings above (e.g. manager-FK resolution).

  • 🔍 find_user_by_upn — looks up users via userPrincipalName eq '{{ value }}' returning id (cache: per_job)

Find a user by user principal name (UPN) — for manager_id, owner refs, and cross-system identity correlation.

  • 🔍 find_group_by_name — looks up groups via displayName eq '{{ value }}' returning id (cache: per_job)

Find a group by displayName — for group-membership writes where the admin only knows the name.

  • 🔍 find_directoryrole_by_name — looks up directoryRoles via displayName eq '{{ value }}' returning id (cache: per_job)

Find a directory role by displayName (Global Administrator, Privileged Role Administrator, …).

MFA registration is a tri-state signal

Once per sync the connector asks Graph for the authentication-methods registration report — /reports/authenticationMethods/userRegistrationDetails, reading the single field isMfaRegistered, keyed by user object id. That report needs Reports.Read.All / AuditLog.Read.All. If the app registration does not have it, the call fails, the platform logs it, and account sync completes normally with MFA left unset. A missing report permission is not allowed to break your directory sync.

The value the platform stores per account has three states, not two:

What Entra reported for the account Stored What it means
isMfaRegistered: true true Registered for MFA
isMfaRegistered: false false Reported, and not registered — a real finding
account absent from the report (unset) Unknown. Nobody has looked at this account

The third row carries the whole point. An account missing from the report is not an account without MFA — it is an account the report did not cover, and those are different facts.

So coverage is computed over the accounts that were actually reported on. Take a tenant with 1,240 accounts, where the report covers 1,190 of them and 1,150 of those are registered. The platform writes a sentence that names its own denominator:

1,150 of 1,190 accounts with a reported MFA state are registered for MFA (97%).

Divide by the full 1,240 instead and you get 93% — a number that quietly invents fifty people without MFA, none of whom was ever assessed.

Why both roundings are lies, in opposite directions

The harder case is a tenant where nothing reports MFA — no Entra connector with the report permission, no Google Workspace. There are two tempting ways to fill that hole, and the platform takes neither.

Rounding unknown up — reporting 100%, or "no findings" — says the estate is protected when nobody has checked. It is the dangerous direction: it closes a question that was never opened. An auditor reading a green control reasonably concludes that someone looked and found nothing; here, nobody looked. The control would be evidence of a measurement that did not happen.

Rounding unknown down — reporting 0% — is the flattering direction, and it is no better. It manufactures a fleet of unprotected accounts out of an absent permission, and manufactured findings cost real money: a remediation project, an audit exception, an argument with a security team, all directed at a fact nobody established. It also makes the tool look vigilant while it is being precisely the opposite.

Both share one defect: they convert absence of evidence into evidence. So instead, an unmeasurable control is published as a visible gap that states its own reason ("no connected system reports MFA registration — connect Entra with Reports.Read.All, or Google Workspace, to evidence this control") and is excluded from the framework score rather than averaged into it as a nought. If no control in a framework can be measured, the framework shows no overall figure at all — not zero per cent.

💡 Tip

The same rule governs a second Entra signal. Sign-in activity for service principals is licence-gated behind Microsoft Entra Workload ID Premium. Without that licence Graph reports no workload sign-in activity, and the platform says "sign-in activity: not reported" on an agent or service identity — never "never used". See NHI & Ownership.

SharePoint permissions: the unstructured scan

The connector also reads a lens that has nothing to do with users and groups: who can reach the content in SharePoint. It walks four levels —

  1. /sites?search=* — the sites in the tenant
  2. /sites/{id}/drives — each site's document libraries
  3. /drives/{id}/root/children — the top-level folders in each library
  4. /drives/{id}/items/{item}/permissions — who has access to each folder

— and turns the result into folder records with a permission list. It needs Sites.Read.All and Files.Read.All, both read-only. The scan never writes. A site or library the app cannot read is skipped and logged; one inaccessible site does not abort the run.

Each permission entry is classified into one of three levels: Graph's owner and sp.full control roles become owner, write and sp.edit become write, and everything else — read, restricted view — becomes read. A single Graph permission may grant one principal or several (a sharing link often grants many), and both shapes are flattened into individual entries.

How external exposure is detected

Three independent signals mark a reader as external:

  • The #EXT# marker in a user principal name. This is Entra's own canonical mark for a B2B guest, applied by the directory itself, which makes it the most reliable cross-tenant signal available — it does not depend on anyone having configured anything.
  • An off-domain email address, if you have configured which email domains count as internal. This is opt-in precisely because guessing at it would produce false alarms in tenants that legitimately use several domains.
  • An anonymous sharing link — a link whose scope is "anyone", which is external by construction regardless of who holds it.

Scanning runs on a slow cadence and with a per-run folder budget: document libraries and their permission lists change slowly, and the Graph sites endpoint is rate-limited, so a fast sweep would buy nothing and cost your tenant's request budget.

Two honest limits. The scan covers the top-level folders of each document library, not a full recursive tree — a deeply nested folder with its own broken inheritance is not enumerated. And a reader the platform cannot resolve to a governed identity still counts towards the external and reader tallies without becoming an identity row; "someone we do not govern can read this" is the honest reading of that, and it is the reading the lens takes. The lens itself is described in Unstructured Data Visibility.

Activity data, and the evidence gate behind dormancy

Dormancy — "nobody has used this access in ninety days" — is one of the most actionable findings the platform produces and one of the easiest to get catastrophically wrong. The Entra connector supplies its input by reading /auditLogs/signIns for the lookback window, selecting only the user principal name and the timestamp, and aggregating per user into a last-used time and a count of sign-ins within the window. It needs AuditLog.Read.All.

Two deliberate choices in that read:

  • Only successful sign-ins count (status/errorCode eq 0). Counting failed attempts would make an account under a brute-force attack look like the busiest account in the tenant — exactly backwards for a signal whose job is to find the quiet ones.
  • The signal is account-level. An account that signed in this week is taken to be exercising the access it currently holds, so the observation propagates to every grant on that account rather than to nothing at all.

A last-used timestamp also never moves backwards. Re-ingesting an overlapping window cannot overwrite a more recent observation with an older one.

Capability is not evidence

This is the part a real false positive taught us. Being able to report activity is not the same as having reported it, and treating an empty last-used field as dormancy conflates two unrelated situations:

  • the connector observes activity and has looked — empty means "not used since we started watching", which is a genuine finding;
  • the connector cannot observe activity, or has never completed a run — empty means "we do not know", and reading that as dormancy once made every grant in a test tenant, thousands of them, appear dead on day one.

So a system counts as usage-tracked only when both conditions hold: its engine declares that it can report activity, and that specific connector has completed at least one real ingest cycle. Until both are true, dormancy is not asserted for anything on that system, and the risk score and the advisor inbox apply the identical test — a tenant must never see dormant findings in one place and a dormancy-driven score in another.

A cycle that returns zero events is a valid answer. "We polled and saw nothing new" is an observation, not a non-event, and it marks the connector as having looked. A cycle that hard-failed does not mark anything: there is no evidence in a failure. And the mark is monotonic — a later failure never erases it, because the grants an earlier cycle updated remain real observations regardless of what happened afterwards.

The practical consequence is worth stating plainly: a freshly connected Entra tenant shows no dormancy findings until its first ingest cycle completes. That is the intended behaviour, not a delay to work around.

App-registration credentials: references and dates, never values

With Application.Read.All, the connector also inventories the secrets and certificates on your app registrations — reading passwordCredentials and keyCredentials from /applications to answer "what expires when, and who owns it".

What it reads is deliberately thin: the credential's key id, display name, hint, and start and end dates. No secret value is read, because none is available — Graph returns a client secret's value once, at creation, and never again. A certificate thumbprint is readable and public, and is still deliberately not collected: expiry hygiene does not need it, and every extra field is one more thing that can end up in an audit payload. See NHI & Ownership and AI Agent Governance.

What this connector does not do

  • It writes membership and user attributes — not everything. The write path adds and removes group membership (idempotently: already-a-member and not-a-member both settle as success), disables and re-enables users, and updates user attributes. There is no hard-delete — a leaver's Entra account is disabled, never removed, so the history stays.
  • Creating a user in Entra is not implemented. The connector reads and governs accounts that already exist. If a policy or request would require a new Entra account to be provisioned, that attempt fails loudly rather than appearing to succeed; the account is created in Entra by your own process and the platform picks it up on the next sync. This is worth knowing before you plan a joiner flow around it.
  • It does not read what you did not grant. No mailbox or file content, no Conditional Access policy evaluation, no sign-in risk scores, no credential material. If a permission is absent, the corresponding capability reports as unavailable rather than as an empty result.
  • Group risk and privilege flags are name-based heuristics. The default mappings mark a group as privileged when its display name contains "Privileged" or "Admin". That is a starting point for triage, not a classification — refine it with your own rules once you can see the estate.
  • Directory roles are read, not assigned. They import as critical-risk entitlements so that holding one is visible in the governed model; the connector's write path covers group membership.

Onboarding setup checklist

Create an Entra app registration with Microsoft Graph permissions, collect tenant_id + client_id + secret, and paste them below. Takes ~5 minutes in a clean tenant.

Estimated time: ~5 min. The wizard exposes this checklist as a drawer (📋 button) during onboarding. See the Onboarding wizard walkthrough for context.


Further reading:

Did this answer your question?One click records the page; add detail by email if something is missing.

Try “tenant isolation”, “role mining” or “Entra”.