-
Notifications
You must be signed in to change notification settings - Fork 0
AccessCheck
A standalone PowerShell module for auditing identity posture, authentication methods, Conditional Access Policy (CAP) coverage, and resource access in Microsoft Entra ID / Microsoft 365 environments. It operates against the MS Graph API using an existing access token and optionally integrates with GraphRunner for token refresh resilience.
Unlike the Resume- scripts in this repository, this module is not a supplement to a specific GraphRunner function — it provides access-checking capability that GraphRunner does not have at all.
Once a token has been obtained in a pentesting engagement, the immediate question is: what does this token actually allow, and how is that access controlled? Answering it typically requires correlating several sources of information that are spread across different API endpoints and permission boundaries.
Invoke-AccessCheck brings these into a single run:
- Who is this identity? Decodes the current token's claims and resolves the target user or service principal's profile, group memberships, and active / PIM-eligible directory roles.
- How is MFA enforced? Enumerates every registered authentication method, checks the legacy per-user MFA state, and identifies gaps (no methods registered, TAP active, only SMS/email registered).
-
What Conditional Access Policies exist and do they apply? Enumerates all CAPs via the MS Graph path, with an optional fallback to the
graph.windows.net/policies?api-version=1.61-internalundocumented endpoint used by ROADrecon. For each enabled policy it performs an offline applicability analysis: given the target user's resolved groups and roles, which policies include them, which exclude them, and what controls are required? -
What can this token reach? Optionally probes a list of common Azure/M365 resource URIs via refresh token exchange to determine which services issue tokens and which are blocked by a CAP. Error codes on failed probes (e.g.
AADSTS53003) are informative even when no token is returned. - What are the significant access findings? Compiles a severity-tagged findings list (Critical / High / Medium / Info) covering MFA gaps, CAP coverage gaps, privileged roles, over-privileged exclusions, and more.
- Windows PowerShell 5.1 or PowerShell 7+
- A valid access token (and ideally a refresh token) for the target tenant — any standard method of token acquisition
-
GraphRunner dot-sourced in the same session if token refresh is needed (provides
Invoke-RefreshGraphTokens). If the token does not expire during the run, GraphRunner is not strictly required.
# With GraphRunner (recommended for refresh token resilience)
. .\GraphRunner\GraphRunner.ps1
. .\Invoke-AccessCheck.ps1
# Without GraphRunner (token refresh will silently fail if the token expires)
. .\Invoke-AccessCheck.ps1Encoding note: The file is pure ASCII. If you edit it, save as ASCII or UTF-8 with BOM. Windows PowerShell 5.1 reads files without a BOM as Windows-1252 by default, which silently corrupts multi-byte characters and causes parse errors.
| Section | What it covers | Typical permission needed |
|---|---|---|
| 1. Token claims |
oid, upn, scp, roles, wids, expiry |
None (local decode) |
| 2. Identity profile |
userType, accountEnabled, last sign-in, licenses |
User.Read (self) |
| 3. Group & role memberships | Transitive groups, active directory roles, PIM eligible roles |
User.Read (self); Directory.Read.All for others |
| 4. Auth methods & MFA state | Registered methods, TAP, per-user MFA state, sign-in preferences |
UserAuthenticationMethod.Read (self) / .Read.All (others) |
| 4. SP credentials | Certificate and client secret expiry (SP mode) | Application.Read.All |
| 5. CAP enumeration | All Conditional Access Policies + state breakdown |
Policy.Read.All + Security/Global Reader role |
| 6. CAP analysis | Offline include/exclude matching against target's groups and roles | Uses data from sections 3 & 5 — no additional calls |
| 7. CAP What-If | Simulation of a specific sign-in scenario |
Policy.Read.ConditionalAccess + Security Reader role |
| 8. App roles & grants | App role assignments, OAuth2 delegated permission grants |
User.Read (self) |
| 9. Identity risk | Risky user level and state | IdentityRiskyUser.Read.All |
| 10. Resource probing | Token acquisition per resource URI via refresh token | Refresh token required; one sign-in log entry per probe |
| 11. SP permissions | App roles, delegated grants, owners, directory roles (SP mode) | Application.Read.All |
| 12. Findings summary | Severity-tagged findings list compiled from all sections | — |
Sections that require elevated permissions fail gracefully with a [*] note — they do not abort the run.
The function accepts any token object with access_token and refresh_token properties. Populate $global:tokens beforehand using any acquisition method, or pass the object explicitly via -Tokens.
The ClientID is auto-detected from the appid claim in the access token JWT, matching the behaviour of the Resume- scripts. This ensures that token refresh calls use the same client the refresh token was originally issued to.
# Using a token already in $global:tokens
Invoke-AccessCheck -Tokens $tokens
# Explicit token object
$myTokens = [pscustomobject]@{
access_token = "eyJ0..."
refresh_token = "0.A..."
}
Invoke-AccessCheck -Tokens $myTokens| Parameter | Required | Default | Description |
|---|---|---|---|
-Tokens |
No | $global:tokens |
Token object with access_token and refresh_token
|
-TargetUser |
No | Token's oid
|
User UPN or object ID (GUID), or SP object ID / appId |
-TargetType |
No | User |
User or ServicePrincipal
|
-ResourceId |
No | Single resource URI to probe for token acquisition | |
-CheckAllResources |
No | switch | Probe all common resource URIs. Generates sign-in log entries. |
-SkipElevated |
No | switch | Skip checks that require Security/Global Reader roles (CAP enumeration, risk state, per-user MFA requirements) |
-RunWhatIf |
No | switch | Run the CAP What-If simulation. Prompts for application ID and optional IP address. |
-OutputPath |
No | Folder to write CSV exports: ConditionalAccessPolicies.csv, ResourceProbeResults.csv, AccessFindings.csv
|
|
-tenantid |
No | $global:tenantid |
Tenant ID for token refresh |
-ClientID |
No | Auto-detected | Override the client ID used for token refresh |
-RefreshInterval |
No | 300 |
Seconds between proactive token refreshes (not currently used; refresh is reactive on 401) |
-Device |
No | Windows |
Device string for Invoke-ForgeUserAgent
|
-Browser |
No | Edge |
Browser string for Invoke-ForgeUserAgent
|
Decodes the access token JWT locally (no network call). Prints oid, upn, tid, aud, appid, expiry, delegated scopes (scp), application permissions (roles), and directory role template IDs (wids) with friendly name lookups for common roles.
The wids claim is a snapshot of the roles the caller held at token issuance time and is used as a fallback source of role data in the CAP analysis when live role assignment enumeration is unavailable.
Fetches the target's core profile from /me (self) or /users/{id} (other user) or /servicePrincipals/{id} (SP mode).
For users: resolves userType (Member vs Guest), accountEnabled, onPremisesSyncEnabled, assignedLicenses, jobTitle, department, and last interactive / non-interactive sign-in timestamps (beta, requires AuditLog.Read.All).
Findings flagged:
-
High— account is disabled -
Medium— account is a Guest -
Info— no licenses assigned -
Info— last interactive sign-in more than 90 days ago (dormant account)
Enumerates transitive group memberships via /transitiveMemberOf, active directory role assignments via /roleManagement/directory/roleAssignments, and PIM-eligible roles via /roleManagement/directory/roleEligibilitySchedules.
The resolved group IDs and role definition IDs are passed directly into the CAP applicability analysis in Section 6.
Findings flagged:
-
High— any active directory role assignment -
High— any PIM-eligible role (not currently active) -
Medium— member of a role-assignable group
Calls three endpoints for the target user:
| Endpoint | What it returns |
|---|---|
/authentication/methods |
All registered methods (Authenticator, FIDO2, phone, OATH, TAP, Windows Hello, email) |
/authentication/signInPreferences (beta) |
System-preferred MFA method and user preference |
/authentication/requirements (beta) |
Legacy per-user MFA state: enforced / enabled / disabled
|
Findings flagged:
-
Critical— no authentication methods registered -
High— Temporary Access Pass (TAP) is active (bypasses MFA) -
High— only email authentication method registered -
Medium— no phishing-resistant method registered (no Authenticator app, FIDO2, or Windows Hello) -
Medium— only SMS/voice phone registered (SIM-swap / SS7 risk) -
Medium— legacy per-user MFA state isdisabled(relies solely on CAPs)
Checks keyCredentials (certificates) and passwordCredentials (client secrets) on the service principal and flags any that have already expired.
Attempts to retrieve all CAPs via two paths, trying in order:
Path A — MS Graph v1.0
GET https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies
Requires Policy.Read.All delegated permission and a privileged directory role (Security Reader, Conditional Access Administrator, Global Reader, etc.).
Path B — graph.windows.net 1.61-internal fallback
GET https://graph.windows.net/{tenant}/policies?api-version=1.61-internal
The undocumented endpoint used by ROADrecon. Requires only a valid Azure AD Graph token (any authenticated user); does not require a privileged role. Only attempted if Path A fails.
Note:
graph.windows.netentered active retirement in September 2024 and has been progressively blocked since February 2025. Path B is likely inaccessible in most tenants as of mid-2025, but is worth attempting as the attempt itself is low-noise and the error is informative.
If -OutputPath is specified, all retrieved policies are written to ConditionalAccessPolicies.csv with key fields expanded: included/excluded users, groups, roles, applications, client app types, grant controls, authentication strength, and sign-in frequency.
Performs an offline analysis using data already collected — no additional API calls. For each enabled policy retrieved in Section 5, evaluates whether the target user is in scope:
-
Inclusion:
includeUserscontains"All", or matches the target's object ID / UPN, or intersects with the target's group IDs or role definition IDs. -
Exclusion: Same matching logic against the corresponding
exclude*conditions. - Result: Included and not excluded → policy applies. Included and excluded → does not apply (exclusion wins). Not included → does not apply.
For each applicable policy, the grant controls are noted: mfa, compliantDevice, domainJoinedDevice, authentication strength, client app type restrictions, sign-in risk thresholds.
Findings flagged:
-
Critical— target is excluded from all enabled CAPs (potential break-glass / over-privileged exclusion) -
Critical— MFA is required by a CAP but the user has no registered MFA methods -
High— no applicable enabled CAP enforces MFA (relying on legacy per-user MFA or no enforcement at all) -
High— target is explicitly excluded from a specific named policy -
Medium— no applicable CAP restricts legacy authentication client types (EAS, IMAP, SMTP AUTH may bypass MFA)
Calls POST /identity/conditionalAccess/evaluate with configurable sign-in conditions. Prompts interactively for:
- Application ID (GUID,
"All", or Enter for MS Graph) - IP address (optional)
Returns which policies would apply to the simulated sign-in and why others would not. Requires Policy.Read.ConditionalAccess or Policy.Read.All plus a Security Reader role.
This is a POST operation and is considered moderate-noise relative to the GET-only sections. It does not trigger an actual sign-in.
| Endpoint | What it returns |
|---|---|
/users/{id}/appRoleAssignments |
App roles the user has been assigned in enterprise applications |
/oauth2PermissionGrants?$filter=principalId eq '{id}' |
Delegated permission grants — applications that can act on behalf of this user |
Findings flagged:
-
Medium— admin-consented (AllPrincipals) OAuth2 grant; any user in the tenant is covered
Calls GET /identityProtection/riskyUsers/{id}. Returns riskLevel (none / low / medium / high), riskState, and riskDetail. Requires IdentityRiskyUser.Read.All — fails gracefully with a [*] note if the permission is absent.
Findings flagged:
-
High— risk level ishighormedium
Attempts to exchange the refresh token for each resource URI via a direct POST to the v1 token endpoint. Does not touch $global:tokens — the existing token state is preserved regardless of probe results.
Warning: Each probe (successful or failed) generates one sign-in log entry in the tenant's Azure AD sign-in logs.
Common resources probed:
| Resource | URI |
|---|---|
| MS Graph | https://graph.microsoft.com/ |
| Azure Resource Manager | https://management.azure.com/ |
| Azure Core Management | https://management.core.windows.net/ |
| SharePoint Online |
https://{tenant}.sharepoint.com/ (derived from token UPN) |
| Exchange Online | https://outlook.office365.com/ |
| Teams | https://api.spaces.skype.com/ |
| Intune | https://api.manage.microsoft.com/ |
| Office 365 Management API | https://manage.office.com/ |
| Key Vault | https://vault.azure.net/ |
| Power BI | https://analysis.windows.net/powerbi/api |
| AAD Graph (legacy) | https://graph.windows.net/ |
AADSTS error codes on failed probes are interpreted and displayed:
| Error code | Meaning |
|---|---|
AADSTS53003 |
Blocked by a Conditional Access policy |
AADSTS50076 |
MFA required to acquire a token for this resource |
AADSTS65001 |
Resource not consented (app not authorised in tenant) |
AADSTS500011 |
Resource principal not found in tenant |
AADSTS50013 |
Refresh token expired or revoked |
Findings flagged:
-
Info—AADSTS53003on a resource; a CAP is explicitly blocking token acquisition for it
If -OutputPath is specified, probe results are written to ResourceProbeResults.csv.
| Endpoint | What it returns |
|---|---|
/servicePrincipals/{id}/appRoleAssignments |
App roles granted to this SP on other resources |
/servicePrincipals/{id}/oauth2PermissionGrants |
Admin-consented delegated grants |
/servicePrincipals/{id}/owners |
Users or SPs that own this service principal |
/roleManagement/directory/roleAssignments?$filter=principalId eq '{id}' |
Directory roles assigned to the SP |
Findings flagged:
-
High— active directory role assignment on the SP
Aggregates all findings collected during the run, groups them by severity, and prints a colour-coded summary:
| Severity | Colour | Tag |
|---|---|---|
| Critical | Red | [!!!] |
| High | Red | [!] |
| Medium | Yellow | [~] |
| Info | Cyan | [i] |
If -OutputPath is specified, findings are written to AccessFindings.csv with Severity, Category, Finding, and Detail columns.
When -OutputPath is specified, the following CSVs are written:
| File | Produced by | Description |
|---|---|---|
ConditionalAccessPolicies.csv |
Section 5 | All retrieved CAPs with key fields expanded |
ResourceProbeResults.csv |
Section 10 | Per-resource probe result: token acquired, error code, hint |
AccessFindings.csv |
Section 12 | All findings with severity, category, and detail |
# Check the current token holder — all checks, interactive output only
Invoke-AccessCheck -Tokens $tokens
# Check a specific user, export results
Invoke-AccessCheck -Tokens $tokens `
-TargetUser "jsmith@contoso.com" `
-OutputPath .\access-output
# Check a specific user by object ID
Invoke-AccessCheck -Tokens $tokens -TargetUser "00000000-0000-0000-0000-000000000000"
# Probe all common resource URIs (generates sign-in log entries)
Invoke-AccessCheck -Tokens $tokens -CheckAllResources
# Probe a single custom resource only
Invoke-AccessCheck -Tokens $tokens -ResourceId "https://database.windows.net/"
# Run CAP What-If simulation (prompts for app ID and IP)
Invoke-AccessCheck -Tokens $tokens -RunWhatIf
# Skip elevated-only checks for a faster, lower-noise run
Invoke-AccessCheck -Tokens $tokens -SkipElevated
# Check a service principal
Invoke-AccessCheck -Tokens $tokens `
-TargetUser "00000000-0000-0000-0000-000000000000" `
-TargetType ServicePrincipal
# Full check on a target user with all options, exported
Invoke-AccessCheck -Tokens $tokens `
-TargetUser "jsmith@contoso.com" `
-CheckAllResources `
-RunWhatIf `
-OutputPath .\access-output| Activity | Azure AD Audit Log | MS Graph Activity Log | Sign-in Log |
|---|---|---|---|
| Token JWT decode (local) | No | No | No |
/me, /users/{id} reads |
No | If enabled | No |
| Group / role reads | No | If enabled | No |
| Auth method reads | No | If enabled | No |
| CAP reads (MS Graph) | No | If enabled | No |
CAP reads (graph.windows.net) |
No | No (different service) | Token acq only |
CAP What-If (POST /evaluate) |
No | If enabled | No |
| Resource token probing | No | No | Yes — one entry per resource |
| Identity risk reads | No | If enabled | No |
| Any write operation | Yes | If enabled | No |
MS Graph Activity Logs are not enabled by default — they require a Diagnostic Setting configured in Azure Monitor with a P1/P2 licence. When they are enabled, bulk enumeration patterns (rapid GET requests across /users, /groups, /servicePrincipals) are detectable via KQL.
All read-only calls in sections 1–9 and 11 generate no sign-in log entries as they use the existing token. Resource probing in section 10 is the only section that triggers new token acquisitions and therefore new sign-in log entries.
Invoke-AccessCheck is a standalone module. It is not a supplement to a specific GraphRunner function and does not extend GraphRunner's enumeration or search capabilities. It uses Invoke-RefreshGraphTokens from GraphRunner purely for token refresh resilience (reactive refresh on 401 and 429 handling) — if GraphRunner is not loaded, the module will still run but will not attempt to refresh an expired token.
GraphRunner is authored by Beau Bullock (@dafthack) and licensed under MIT.