Manage Microsoft 365 with the Microsoft.Graph PowerShell module through GraphWarden
Once connected through the quickstart, Microsoft.Graph cmdlets work against GraphWarden transparently. This page walks through common Microsoft 365 admin scenarios — users, groups, licences, directory audit — and shows how ruleset enforcement surfaces when a cmdlet is denied. Every snippet assumes the connection is already established; see the quickstart for the Connect-MgGraph setup.
Prerequisites
List users with selected properties
The Get-MgUser cmdlet wraps GET /v1.0/users. -Property maps to $select, -Top maps to $top, and -Filter maps to $filter. All three query shapes pass through the proxy byte-for-byte.
Get-MgUser `
-Top 10 `
-Property 'displayName','mail','userPrincipalName','accountEnabled'
| Format-Table displayName, mail, accountEnabled
If a ruleset Response Filter applies allow_properties or strip_properties to /v1.0/users, the missing keys will not appear in the deserialized objects — a user with mail stripped surfaces as $null.
Create a group
New-MgGroup issues POST /v1.0/groups. The example below creates a security group; adjust MailEnabled, SecurityEnabled, and GroupTypes for other shapes (Microsoft 365 group, distribution list, dynamic membership).
$group = New-MgGroup `
-DisplayName 'Test security group' `
-MailNickname 'test-sec-group' `
-MailEnabled:$false `
-SecurityEnabled:$true `
-GroupTypes @()
Write-Host "Created group: $($group.Id) - $($group.DisplayName)"
Group creation is a common ruleset gate — many policies limit which App Identities can create groups, or require a naming convention enforced by a rule's conditions block. If your request is denied, the response body will name the matched rule.
Assign a licence
License assignment uses Set-MgUserLicense, which issues POST /v1.0/users/{id}/assignLicense. You need the SKU Id of the plan you are assigning — fetch it with Get-MgSubscribedSku before calling assign.
# 1. Find the SKU you want to assign (e.g. Microsoft 365 E3).
$sku = Get-MgSubscribedSku `
| Where-Object SkuPartNumber -eq 'ENTERPRISEPACK' `
| Select-Object -First 1
# 2. Assign it to a user by UPN or object id.
Set-MgUserLicense `
-UserId 'alice@contoso.com' `
-AddLicenses @(@{ SkuId = $sku.SkuId }) `
-RemoveLicenses @()
License state is an audit-heavy area. Even with action: allow, every assign / remove surfaces as an audit event in GraphWarden with the matched rule id, the caller, and the payload summary.
Read audit log entries
Get-MgAuditLogDirectoryAudit returns tenant-level audit activities (admin actions, policy changes). The scope AuditLog.Read.All or Directory.Read.All must be granted on the underlying Azure AD app registration AND permitted by the ruleset — the App Identity needs to be permitted to call /v1.0/auditLogs/directoryAudits in GraphWarden.
Get-MgAuditLogDirectoryAudit `
-Top 10 `
-Filter "activityDisplayName eq 'Add user'"
| Select-Object ActivityDateTime, ActivityDisplayName, InitiatedBy
If the ruleset has not been configured to permit this endpoint, expect 403 Forbidden with a rule id in the response body (see the "Understanding ruleset denials in PowerShell" section below).
Delete a user (permission-gated)
Delete operations are high-impact and are typically denied by default in GraphWarden rulesets. The example below intentionally demonstrates a denied cmdlet — run it against an identity whose ruleset blocks user deletes to see the shape of the error.
try {
Remove-MgUser -UserId 'victim@contoso.com' -ErrorAction Stop
Write-Host "User removed."
}
catch {
# Expected: 403 Forbidden when the ruleset denies DELETE on /v1.0/users.
Write-Warning "Remove-MgUser failed: $($_.Exception.Message)"
Write-Host "Response body:" ($_.ErrorDetails.Message)
}
Expected output when the rule blocks the request (exact rule id will vary with your ruleset):
# WARNING: Remove-MgUser failed: Response status code does not indicate success: 403 (Forbidden).
# Response body: {"error":{"code":"Forbidden","message":"Blocked by GraphWarden rule 'deny-user-delete'."}}
Understanding ruleset denials in PowerShell
A blocked cmdlet raises a terminating error whose $_.Exception.Response.StatusCode is 403 and whose response body contains the matched rule id. There are two useful ways to inspect it.
# Option 1 — inspect $Error[0] after a failed cmdlet.
try {
Get-MgAuditLogSignIn -Top 1 -ErrorAction Stop
}
catch {
$response = $_.Exception.Response
Write-Host "Status: $([int]$response.StatusCode) $($response.StatusCode)"
Write-Host "Rule body: $($_.ErrorDetails.Message)"
}
# Option 2 — -ErrorVariable captures the error without terminating the script.
$failures = $null
Get-MgGroup -Top 1 -ErrorAction SilentlyContinue -ErrorVariable failures
if ($failures) {
foreach ($f in $failures) {
Write-Host "Failed: $($f.Exception.Message)"
}
}
The rule id in the response body is the same identifier shown in GraphWarden App Admin's audit log entry for the request; use it to locate the rule, review its match pattern and conditions, and decide whether to widen the rule or adjust the script.
Troubleshooting
The five failure modes below cover the common surprises when running Microsoft.Graph cmdlets against GraphWarden.
Connect-MgGraph succeeds but every cmdlet returns 401
The proxy JWT that was passed to Connect-MgGraph has expired, or the SDK is not re-reading MICROSOFT_GRAPH_API_ENDPOINT (set after the connection was established). Disconnect with Disconnect-MgGraph, reacquire a fresh proxy JWT, ensure the env var is set, and reconnect.
Cmdlet returns 403 Forbidden with no rule id in the body
The rule body format changes with proxy versions; on older proxies the body may be {"error":{"code":"Forbidden","message":"Access denied."}} with no rule id. Check the audit log in GraphWarden App Admin for the matching request — audit events always include the rule id regardless of response body shape.
Cmdlet hangs on first call
The proxy URL resolves by DNS but the host is unreachable. Test with Test-NetConnection -ComputerName <proxy-host> -Port 443. In on-prem deployments, verify the firewall permits outbound 443 from the scripting host to the proxy.
Invoke-MgGraphRequest ignores the env var
Invoke-MgGraphRequest is a lower-level cmdlet that composes against the endpoint fixed at Connect-MgGraph time. If you switched MICROSOFT_GRAPH_API_ENDPOINT after connecting, Invoke-MgGraphRequest will keep calling the previous endpoint. Disconnect and reconnect to pick up the change.
Pagination returns 0 results with a @odata.nextLink
Some ruleset Response Filters strip the @odata.nextLink property. If pagination appears broken, inspect the raw response with Invoke-MgGraphRequest -Method GET -Uri '/v1.0/users?$top=2' and check whether @odata.nextLink is present. If it was filtered, adjust the rule to keep navigation properties in the allow list.
Last reviewed: