PowerShell cookbook: common M365 admin tasks through GraphWarden

Copy-paste PowerShell snippets for common Microsoft 365 administration tasks routed through GraphWarden. Each snippet assumes you have already connected via Connect-MgGraph per the PowerShell quickstart. Snippets are reference-authored and should be validated on a non-production tenant before operational use.

Reference snippets — validate on a test tenant before production use

Prerequisites

- Completed the [PowerShell quickstart](./quickstart) — the connection must be established via `Connect-MgGraph -AccessToken $SecureToken` before any snippet below will route through GraphWarden - Appropriate ruleset permissions for the Graph endpoints each snippet calls (the per-snippet notes flag which endpoint is touched) - For write snippets (group create, license assign, guest invite, secret rotate): an App Identity whose ruleset permits the write action
These snippets are reference-authored from the official `Microsoft.Graph` module documentation and the GraphWarden recon notes. They have NOT been executed against a live M365 tenant from this writing. Validate each snippet on a non-production tenant before operational use; parameter names and output shapes change between module versions. Report accuracy issues via the Suggest-an-edit link in the page footer.

Generate a PowerShell snippet for a specific M365 admin task

Generate a PowerShell snippet for a specific Microsoft 365 admin task, routed through GraphWarden.

Assume my script has already called Connect-MgGraph through the GraphWarden proxy (see the PowerShell quickstart).

I need the snippet to:
1. Use Microsoft.Graph module v2.x cmdlets
2. Include the Graph endpoint the cmdlet calls (so I can verify my ruleset allows it)
3. Include a short "what to expect" note on the shape of the output

Reference documentation: https://graphwarden.com/llms.txt

Ask me for the admin task, any filter criteria, and the output format I want.

Reference: llms.txt

List users with selected properties

Return a compact directory listing with the fields you care about. -Property maps to $select, keeping the payload small and ruleset-filter-friendly.

Get-MgUser -Top 25 -Property displayName, mail, userPrincipalName, accountEnabled `
    | Format-Table displayName, mail, accountEnabled

Graph endpoint: GET /v1.0/users?$top=25&$select=displayName,mail,userPrincipalName,accountEnabled. What to expect: a table of up to 25 users with the four selected fields; any property a Response Filter strips will surface as $null.

Find inactive users (no sign-in in 90 days)

Useful for licence reclamation and lifecycle hygiene. Requires the AuditLog.Read.All scope on the App Identity's underlying registration AND a ruleset that permits /v1.0/users with the signInActivity property read.

$cutoff = (Get-Date).AddDays(-90).ToString('o')

Get-MgUser -Filter "signInActivity/lastSignInDateTime lt $cutoff" `
    -Property displayName, userPrincipalName, signInActivity `
    -Top 100 `
    | Select-Object displayName, userPrincipalName,
        @{ n = 'LastSignIn'; e = { $_.SignInActivity.LastSignInDateTime } }

Graph endpoint: GET /v1.0/users?$filter=.... What to expect: a list of up to 100 users whose last interactive sign-in was before the cutoff; signInActivity can be $null for never-signed-in accounts.

Create a security group

A new security group with no mail enablement and no group types (the shape most admin tooling creates).

$group = New-MgGroup `
    -DisplayName      'Finance-ReadOnly' `
    -MailNickname     'finance-readonly' `
    -MailEnabled:$false `
    -SecurityEnabled:$true `
    -GroupTypes       @()

Write-Host "Created group: $($group.Id)"

Graph endpoint: POST /v1.0/groups. What to expect: the new group id on $group.Id; GraphWarden audit entry records the matched rule and caller.

Add members to a group

Add a user to an existing group by directory object id. Accepts user ids, group ids, or service principal ids via the same cmdlet.

$groupId = '00000000-0000-0000-0000-000000000000'
$userId  = '11111111-1111-1111-1111-111111111111'

New-MgGroupMember `
    -GroupId         $groupId `
    -DirectoryObjectId $userId

Graph endpoint: POST /v1.0/groups/{groupId}/members/$ref. What to expect: a 204 on success; re-adding an existing member raises a terminating error with Request_BadRequest.

Assign a licence to a user

Look up the SKU and assign in two steps. The SKU lookup is a separate Graph call and must itself be permitted by the ruleset.

$sku = Get-MgSubscribedSku `
    | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK' `
    | Select-Object -First 1

Set-MgUserLicense `
    -UserId          'alice@contoso.com' `
    -AddLicenses     @(@{ SkuId = $sku.SkuId }) `
    -RemoveLicenses  @()

Graph endpoint: POST /v1.0/users/{id}/assignLicense. What to expect: the updated user object; the assignment shows as SkuPartNumber in Get-MgUserLicenseDetail on the next call.

Revoke a licence

The same Set-MgUserLicense cmdlet with an empty -AddLicenses and the SKU id in -RemoveLicenses.

$sku = Get-MgSubscribedSku `
    | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK' `
    | Select-Object -First 1

Set-MgUserLicense `
    -UserId          'alice@contoso.com' `
    -AddLicenses     @() `
    -RemoveLicenses  @($sku.SkuId)

Graph endpoint: POST /v1.0/users/{id}/assignLicense. What to expect: the user's AssignedLicenses collection no longer contains the removed SKU on subsequent reads.

List a user's assigned roles

Discover which directory roles a user holds. Useful for privileged-access reviews.

$userId = (Get-MgUser -UserId 'alice@contoso.com').Id

Get-MgUserMemberOf -UserId $userId `
    | Where-Object { $_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.directoryRole' } `
    | Select-Object @{ n = 'Role'; e = { $_.AdditionalProperties.displayName } }

Graph endpoint: GET /v1.0/users/{id}/memberOf. What to expect: a filtered list of directory-role display names the user holds; groups are filtered out by the @odata.type test.

Invite a guest user

Send an invitation to an external email address and optionally include a redirect URL.

$invitation = New-MgInvitation `
    -InvitedUserEmailAddress 'partner@supplier.example' `
    -InvitedUserDisplayName  'Partner, External' `
    -SendInvitationMessage:$true `
    -InviteRedirectUrl       'https://myapps.microsoft.com'

Write-Host "Invitation: $($invitation.InviteRedeemUrl)"

Graph endpoint: POST /v1.0/invitations. What to expect: an InviteRedeemUrl the guest can use to accept; the guest user appears in Get-MgUser once accepted.

List conditional access policies (read-only)

Read-only inventory of CA policies. Requires Policy.Read.All scope and ruleset coverage for the /identity/conditionalAccess namespace.

Get-MgIdentityConditionalAccessPolicy `
    | Select-Object DisplayName, State, CreatedDateTime `
    | Sort-Object CreatedDateTime -Descending

Graph endpoint: GET /v1.0/identity/conditionalAccess/policies. What to expect: a table of policy name, state (enabled / disabled / enabledForReportingButNotEnforced), and creation timestamp.

Query directory audit logs (last 24 hours)

Fetch recent directory audit events. Requires AuditLog.Read.All scope and ruleset coverage for /v1.0/auditLogs/directoryAudits.

$since = (Get-Date).AddHours(-24).ToString('o')

Get-MgAuditLogDirectoryAudit `
    -Filter "activityDateTime ge $since" `
    -Top    50 `
    | Select-Object ActivityDateTime, ActivityDisplayName,
        @{ n = 'InitiatedBy'; e = { $_.InitiatedBy.User.UserPrincipalName } }

Graph endpoint: GET /v1.0/auditLogs/directoryAudits?$filter=.... What to expect: up to 50 audit events in the last 24 hours; InitiatedBy.User.UserPrincipalName can be $null for app-initiated events (check InitiatedBy.App in that case).

List enterprise applications and their permissions

Inventory service principals and their granted permissions — a starting point for app-consent reviews.

Get-MgServicePrincipal -Top 100 -Property DisplayName, AppId, AppRoleAssignmentRequired `
    | Select-Object DisplayName, AppId, AppRoleAssignmentRequired

Graph endpoint: GET /v1.0/servicePrincipals. What to expect: up to 100 service principals; use -Filter "tags/any(t: t eq 'WindowsAzureActiveDirectoryIntegratedApp')" to narrow to user-consented enterprise apps.

Locate service principals whose permissions were granted via admin consent (not user consent). Useful for privileged-app audits.

Get-MgOauth2PermissionGrant `
    | Where-Object ConsentType -eq 'AllPrincipals' `
    | Select-Object ClientId, ResourceId, Scope `
    | Sort-Object ClientId

Graph endpoint: GET /v1.0/oauth2PermissionGrants. What to expect: delegated-permission grants where ConsentType is AllPrincipals (admin-consent scope); the ClientId is the service principal id of the consenting app.

Export group membership to CSV

Flatten every member of a given group to a CSV — useful for compliance evidence packs.

$groupId = '00000000-0000-0000-0000-000000000000'

Get-MgGroupMember -GroupId $groupId -All `
    | ForEach-Object {
        [pscustomobject]@{
            GroupId           = $groupId
            MemberId          = $_.Id
            MemberType        = $_.AdditionalProperties.'@odata.type'
            DisplayName       = $_.AdditionalProperties.displayName
            UserPrincipalName = $_.AdditionalProperties.userPrincipalName
        }
    } `
    | Export-Csv -Path '.\group-members.csv' -NoTypeInformation

Graph endpoint: GET /v1.0/groups/{id}/members (paginated under the hood via -All). What to expect: one CSV row per member; nested groups appear as member rows with MemberType #microsoft.graph.group.

Check MFA registration status for a user

Read the MFA registration detail for a single user from the authentication methods endpoint.

$userId = (Get-MgUser -UserId 'alice@contoso.com').Id

Get-MgReportAuthenticationMethodUserRegistrationDetail `
    -Filter "id eq '$userId'" `
    | Select-Object UserDisplayName, IsMfaRegistered, IsMfaCapable, MethodsRegistered

Graph endpoint: GET /v1.0/reports/authenticationMethods/userRegistrationDetails?$filter=.... What to expect: one record per user; MethodsRegistered is an array (for example, @('microsoftAuthenticatorPush', 'phone')).

Rotate an app registration secret

Rotate the client secret on an app registration. This is an admin operation — ruleset coverage and audit retention should be tight around this endpoint.

$appId    = '00000000-0000-0000-0000-000000000000'
$app      = Get-MgApplication -Filter "appId eq '$appId'"

$newSecret = Add-MgApplicationPassword `
    -ApplicationId $app.Id `
    -PasswordCredential @{
        DisplayName = "rotated-$(Get-Date -Format 'yyyyMMdd')"
        EndDateTime = (Get-Date).AddMonths(12)
    }

Write-Warning "Capture this secret now; Graph will not return it again: $($newSecret.SecretText)"

Graph endpoint: POST /v1.0/applications/{id}/addPassword. What to expect: $newSecret.SecretText holds the one-time cleartext; save it to your secret store immediately. Always pair with Remove-MgApplicationPassword on the old secret once the consuming workload is rotated.

Troubleshooting

The five failure modes below cover the common cookbook-snippet surprises.

Cmdlet returns 403 Forbidden naming a specific Graph scope

The App Identity's underlying Azure AD registration lacks the scope the cmdlet needs (for example, AuditLog.Read.All for audit-log cmdlets, Policy.Read.All for CA-policy cmdlets). Scope grants happen on the real registration, not in GraphWarden — add the scope in Azure AD and admin-consent it, then retry. A scope missing at the registration level produces a different error surface from a GraphWarden ruleset denial; the former names a scope, the latter names a rule.

Cmdlet returns 403 Forbidden naming a GraphWarden rule

A ruleset rule returned block on the endpoint the cmdlet called. The response body names the matched rule id. Use the What-If Simulator in GraphWarden App Admin to preview rule evaluation without hitting Graph, and widen or adjust the rule if the block was unintentional.

Pagination returns fewer results than expected

Many Graph endpoints default to pages of 100; cmdlets like Get-MgUser expose -All to follow @odata.nextLink automatically. Without -All, you get only the first page. Some ruleset Response Filters strip @odata.nextLink as part of allow_properties; if pagination appears broken, inspect the raw response with Invoke-MgGraphRequest -Method GET -Uri '/v1.0/users?$top=2' and widen the filter to include navigation properties.

Rate-limit (429 Too Many Requests) on bulk snippets

Large cookbook snippets (for example, export group membership on a 10,000-seat tenant) can trigger Microsoft Graph's per-app throttles, or a ruleset rate-limit rule in GraphWarden. The response body's Retry-After header indicates the backoff window. For scripted bulk work, add a Start-Sleep between batches or switch to $batch request batching.

Snippet result differs from tenant reality

The snippets on this page are reference-authored and not live-tested against every tenant shape. If a snippet's output does not match what you expect, re-run the underlying Graph call with Invoke-MgGraphRequest -Verbose to see the exact request URI and response, then file a correction via the Suggest-an-edit link in the page footer.

Last reviewed:

Last reviewed: .