Skip to content

AccessCheck

vaarg edited this page Jun 12, 2026 · 2 revisions

Invoke-AccessCheck.ps1

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.

Background

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-internal undocumented 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.

Requirements

  • 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.

Setup

# 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.ps1

Encoding 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.


What It Checks

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.


Authentication

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

Parameters

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 output files: AccessCheck_<timestamp>.txt (full console transcript), 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

Sections

Section 1 — Token Claims

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.


Section 2 — Identity Profile

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)

Section 3 — Group & Role Memberships

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

Section 4 — Authentication Methods & MFA State (User)

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 is disabled (relies solely on CAPs)

Section 4 — Service Principal Credentials (SP mode)

Checks keyCredentials (certificates) and passwordCredentials (client secrets) on the service principal and flags any that have already expired.


Section 5 — Conditional Access Policy Enumeration

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.net entered 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.


Section 6 — CAP Applicability Analysis

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:

  1. Inclusion: includeUsers contains "All", or matches the target's object ID / UPN, or intersects with the target's group IDs or role definition IDs.
  2. Exclusion: Same matching logic against the corresponding exclude* conditions.
  3. 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.

Policies that do not apply to the target are listed in a separate "Policies NOT applying to target" block with colour-coded reasons:

Colour Meaning
Yellow Not in scope (user/group/role conditions don't match)
Red + [EXCLUDED] Target is explicitly excluded from this policy
DarkGray Policy is disabled or report-only (not enforced)

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 (only fires if method enumeration succeeded; see Section 4 note below)
  • 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)

Section 4 note: If /authentication/methods returned 403 (token lacks UserAuthenticationMethod.Read.All), the "no methods registered" Critical finding is not raised — it is replaced by an Info finding noting that manual verification is needed. This prevents a false positive when enumeration simply failed.


Section 7 — CAP What-If Simulation (-RunWhatIf)

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.


Section 8 — App Role Assignments & OAuth2 Permission Grants (User)

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

Section 9 — Identity Risk State

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 is high or medium

Section 10 — Resource Token Probing (-CheckAllResources / -ResourceId)

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:

  • InfoAADSTS53003 on a resource; a CAP is explicitly blocking token acquisition for it

If -OutputPath is specified, probe results are written to ResourceProbeResults.csv.


Section 11 — Service Principal Permissions (SP mode)

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

Section 12 — Access Summary & Findings

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.


Output Files

When -OutputPath is specified, the following files are written:

File Produced by Description
AccessCheck_<timestamp>.txt Start of run Full console transcript (all STDOUT for the entire run)
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

The transcript is started before Section 1 and stopped at the end of the run (or on an early exit), so it captures everything including any initialisation messages.


Examples

# 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

Noise & Detection Considerations

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.


Relationship to GraphRunner

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.

Clone this wiki locally