Skip to main content
Tenant Auditing Runbook

Audit Microsoft 365 Without Entra ID P1, P2 or E5

By SysClaw Engineering Published Updated 8 min read

You can audit OAuth grants, mail forwarding, standing admin roles, and guest accounts with read-only Microsoft Graph and Exchange Online queries without relying on Entra ID P1, P2 or E5 reporting features. These four checks produce review candidates, not a complete security assessment; access still depends on your Microsoft subscription, API permissions, and administrator role.

On this page

Key Takeaways

  • Entra ID Free supports many directory reads, but API permissions, administrator roles, and endpoint-specific licensing still apply.
  • Audit OAuth exposure on both axes: delegated permission grants and application role assignments, with app role IDs resolved against the resource service principal.
  • External forwarding has four candidate layers: mailbox forwarding attributes, inbox rules (including hidden ones), transport rules, and outbound connectors.
  • Every check produces candidates for review, not verdicts — verify against accepted domains, owners, and approvals before changing anything.

This guide was reviewed against Microsoft’s published documentation on 17 October 2026. The sample commands passed offline syntax checks only — they were not executed against a live tenant. Verify them in a scoped pilot before relying on the output.

Prerequisites and Ground Rules

You need:

  • PowerShell 7 or later: the app-role helper uses the ?? operator, which Windows PowerShell 5.1 does not support (about_Operators).
  • Microsoft.Graph PowerShell SDK (Install-Module Microsoft.Graph) with admin consent for Directory.Read.All — the read-only scope that covers users, groups, directory roles, service principals, and OAuth grants. Add Policy.Read.All only if you also query authorization policy or similar policy objects; do not request scopes you do not use.
  • A directory role, not just Graph consent. Admin consent to a scope is not sufficient on its own — the signed-in account also needs an Entra role the endpoint supports; the oauth2PermissionGrant documentation lists Directory Readers and Global Reader as examples.
  • ExchangeOnlineManagement (Install-Module ExchangeOnlineManagement) and an account with at least view-only mailbox and transport configuration permissions (View-Only Organization Management covers it).
  • Entitlement caveat: access to an API is not identical across subscriptions. Some Graph reports and audit sources carry their own licensing requirements; when a call returns authorization or subscription errors, treat that as a boundary to document, not a permission to escalate.

Everything below is read-only. Findings are candidates for review — the correct output of an audit is a verified list with an owner-approved decision, not an automated change.

Attackers and shadow-IT alike leave durable access behind as consent grants. A delegated grant (oauth2PermissionGrants) lets an app act on behalf of a user; an app-only assignment (appRoleAssignments) lets a service principal act with tenant-wide application permissions like Mail.Read. Neither is revoked by resetting a user’s password.

Audit both, and resolve each granted role ID to its human-readable name on the resource service principal’s appRoles — a bare GUID tells a reviewer nothing.

Connect-MgGraph -Scopes "Directory.Read.All" -ErrorAction Stop

# Cache resource service principals so role-id -> name resolution doesn't
# re-fetch Microsoft Graph / Exchange for every grant.
$spCache = @{}
function Resolve-AppRoleName([string]$resourceId, [string]$roleId) {
  if (-not $spCache.ContainsKey($resourceId)) {
    # Errors are NOT swallowed: an unresolvable resource SP means the
    # finding is unresolved too — surface it.
    $spCache[$resourceId] = Get-MgServicePrincipal -ServicePrincipalId $resourceId -ErrorAction Stop
  }
  ($spCache[$resourceId].AppRoles | Where-Object Id -eq $roleId).Value ?? $roleId
}

# --- Delegated grants ---
Get-MgOauth2PermissionGrant -All -ErrorAction Stop | ForEach-Object {
  [PSCustomObject]@{
    Type        = 'Delegated'
    ClientId    = $_.ClientId
    Scope       = $_.Scope
    ConsentType = $_.ConsentType      # 'AllPrincipals' = tenant-wide consent
    PrincipalId = $_.PrincipalId
  }
}

# --- App-only assignments: every service principal, paged ---
Get-MgServicePrincipal -All -ErrorAction Stop | ForEach-Object {
  $sp = $_
  Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All -ErrorAction Stop | ForEach-Object {
    [PSCustomObject]@{
      Type          = 'AppOnly'
      ServicePrincipal = $sp.DisplayName
      Resource      = $_.ResourceDisplayName
      AppRole       = Resolve-AppRoleName $_.ResourceId $_.AppRoleId
      Created       = $_.CreatedDateTime
    }
  }
}

What to expect: output rows listing each grant. Interpretation: flag AllPrincipals delegated grants and app-only roles with broad scopes (*.Read.All, Mail.Read, Mail.Send, full_access_as_app) from publishers you cannot identify. False positives: Microsoft’s own first-party service principals hold large legitimate assignments — verify the app’s publisher and business purpose before treating it as rogue. A grant proves permission exists, not that it has been used; usage evidence lives in sign-in/audit logs.

Check 2: External Mail Forwarding — All Four Candidate Layers

A ForwardingSmtpAddress-only check does not cover inbox rules, transport rules, or connector routes. Enumerate all four layers (Get-Mailbox, Get-InboxRule, Get-TransportRule, Get-OutboundConnector) and compare every destination domain against Get-AcceptedDomain plus your list of approved external destinations.

Connect-ExchangeOnline -ErrorAction Stop

# Layer 1: mailbox forwarding attributes (SMTP and legacy/foreign address).
Get-Mailbox -ResultSize Unlimited -ErrorAction Stop |
  Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress } |
  Select-Object UserPrincipalName, ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward

# Layer 2: inbox rules, including rules hidden via MAPI.
$mailboxes = Get-Mailbox -RecipientTypeDetails UserMailbox,SharedMailbox -ResultSize Unlimited -ErrorAction Stop
foreach ($mbx in $mailboxes) {
  Get-InboxRule -Mailbox $mbx.UserPrincipalName -IncludeHidden -ErrorAction Stop |
    Where-Object { $_.ForwardTo -or $_.ForwardAsAttachmentTo -or $_.RedirectTo } |
    Select-Object MailboxOwnerId, Name, Enabled, ForwardTo, ForwardAsAttachmentTo, RedirectTo, DeleteMessage
}

# Layer 3: transport rules that copy or redirect mail.
Get-TransportRule -ErrorAction Stop |
  Where-Object { $_.BlindCopyTo -or $_.RedirectMessageTo -or $_.CopyTo } |
  Select-Object Name, State, BlindCopyTo, RedirectMessageTo, CopyTo

# Layer 4: outbound connectors (smart-host routes; also review journaling
# rules if journaling is in use). A connector is a *candidate* path — it does
# not by itself forward user mail without a rule or route using it.
Get-OutboundConnector -ErrorAction Stop |
  Select-Object Name, Enabled, SmartHosts, RecipientDomains

What to expect: rows naming mailboxes, rules, or connectors and their destinations. Interpretation: extract the domain from every forwarding target and RedirectTo/SmartHosts/RecipientDomains value; anything not in Get-AcceptedDomain and not an approved partner is a review candidate. Hidden rules (-IncludeHidden) named ./.. or paired with DeleteMessage deserve priority. False positives: shared mailboxes legitimately forward to ticketing systems, and hybrid routing uses outbound connectors normally — confirm the destination’s owner before acting.

Check 3: Standing Privileged Role Assignments

This check inventories current membership of activated directory roles; it does not reconstruct historical access or PIM eligibility. Enumerate each activated directory role’s members including groups and service principals (membership is not only users) and page with -All so large roles are not truncated (directoryRole members).

Connect-MgGraph -Scopes "Directory.Read.All" -ErrorAction Stop

Get-MgDirectoryRole -All -ErrorAction Stop | ForEach-Object {
  $role = $_
  Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All -ErrorAction Stop | ForEach-Object {
    [PSCustomObject]@{
      RoleName   = $role.DisplayName
      MemberType = $_.AdditionalProperties['@odata.type']
      DisplayName = $_.AdditionalProperties['displayName']
      UPN        = $_.AdditionalProperties['userPrincipalName']
      MemberId   = $_.Id
    }
  }
}

What to expect: a flat list of role → member rows. Interpretation: review Global Administrator and other high-privilege roles member by member; group members need expansion to see effective admins. False positives: break-glass accounts, dedicated admin accounts, and service principals holding roles by design are normal — a standing assignment is exposure to manage (verify it’s intended, MFA-protected, monitored), not proof of misuse or that the member is dormant.

Check 4: Review Guest Access Candidates

External guests accumulate in Teams, Microsoft 365 Groups, and SharePoint. On Entra ID Free there are no automated Access Reviews, so the practical output is a review list for owner attestation, not an “inactive guest” verdict: CreatedDateTime and ExternalUserState tell you when a guest was created and whether the invitation was accepted — neither measures activity, and signInActivity requires P1/P2 (user list reference).

Get-MgUser -Filter "userType eq 'Guest'" -All -ErrorAction Stop `
  -Property Id,DisplayName,Mail,UserPrincipalName,CreatedDateTime,ExternalUserState,AccountEnabled |
  Select-Object Id, UserPrincipalName, DisplayName, Mail, AccountEnabled, ExternalUserState, CreatedDateTime |
  Sort-Object CreatedDateTime

What to expect: guest rows with invitation state (PendingAcceptance vs Accepted) and creation date. Interpretation: route the list to the inviting team or resource owner — pending-acceptance guests of unknown origin and guests with no current sponsor are the usual first candidates. False positives: partner accounts with active contracts and guests on long-running projects are expected; an owner attestation or an approved access-review decision is the bar for removal. Do not disable or delete accounts off this list without that confirmation.

What Each Check Does and Does Not Prove

CheckSignal producedDoes not prove
OAuth consent grantsDelegated grants and app-only role assignmentsThat a grant was ever used
Mail forwardingForwarding destinations across all four layersThat a listed destination is unauthorized
Role membershipCurrent members of activated directory rolesHistorical access or PIM eligibility
Guest accessInvitation state and creation dateGuest activity or inactivity

Verify, Then Decide

Before treating any finding as actionable:

  • Re-run the check and confirm the row reproduces — transient state and caching produce stale positives.
  • Corroborate read-only: look up the object in the Entra or Exchange portal, check accepted domains/approved destinations, and pull sign-in or audit evidence where your licensing allows.
  • Get an owner decision. Remediation (revoking consent, removing a rule, disabling an account) is a change with a rollback plan, not a script step.

Limitations

  • This guide lists candidate-finding checks, not a complete security baseline — MFA posture, device state, and threat telemetry need additional sources.
  • Some APIs referenced carry their own licensing requirements; availability on your subscription should be verified per endpoint.
  • Sample scripts were reviewed against documented cmdlet and Graph behavior, not executed in a live tenant for this article — run them first in a test tenant or scoped pilot.

References

Frequently Asked Questions

Can I audit Microsoft 365 without E5?

Yes, within scope. The four read-only checks in this guide cover OAuth grants, mail forwarding, standing role membership, and guest access using standard Graph and Exchange Online queries with the required permissions. They do not unlock premium reports and are not a complete security baseline.

Can I check user sign-in activity on Entra ID Free?

The signInActivity property on user objects requires an Entra ID P1 or P2 license in the tenant, so it is not a reliable signal on Free. Unified audit log events can corroborate some sign-in activity, subject to their own retention limits, but they are not equivalent to a complete Entra sign-in history — do not treat guest creation date or invitation state as a proxy for inactivity.

Are all the APIs and cmdlets used here free?

No blanket claim is safe. Directory reads like oauth2PermissionGrants and directoryRoles work under standard Graph permissions, but some APIs and reports carry their own licensing or entitlement requirements. Check the entitlement notes for each Microsoft endpoint before assuming it is available on your subscription.

Is it safe to run these scripts in production?

Every command in this guide is read-only (Get-* and Graph GETs). None of them change configuration, disable accounts, or remove rules — treat the output as a review list and route any remediation through your normal change process.

More Microsoft 365 Auditing Guides

All guides →

Related Resources

Try it on your own tenant

SysClaw Community Edition runs these checks and keeps the results on your machine — free forever for one tenant and one operator.