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.
Prerequisites
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.
Find service principals with admin consent
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: