Top 10 Essential Entra PowerShell Scripts

By Mark D. Albin, MS

The ten Microsoft Graph PowerShell scripts every Entra ID administrator should keep within arm's reach — for security reviews, cleanup, and the questions leadership always asks.

Every one of these scripts answers a question we get asked in real tenants: Who still doesn't have MFA? Which accounts are stale? Who is a Global Admin? Which app secrets are about to expire and break something? All of them use the modern Microsoft Graph PowerShell SDK — the old MSOnline and AzureAD modules are deprecated and should not be used for new work.

All scripts below are read-only reports except #9 and #10, which make changes. As always: test in a test environment before using in production, and connect with the least-privileged scopes each task needs.

Before You Start: Install and Connect

Install the Graph SDK once, then connect with only the scopes required for the script you're running. Delegated (interactive) sign-in is fine for ad-hoc admin work; for unattended automation, use certificate-based app-only authentication.

# One-time install (CurrentUser scope avoids needing local admin)
Install-Module Microsoft.Graph -Scope CurrentUser

# Connect with the scopes used across this article
Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All",
    "UserAuthenticationMethod.Read.All","Policy.Read.All",
    "Application.Read.All","RoleManagement.Read.Directory",
    "IdentityRiskyUser.Read.All","Group.Read.All"

# Confirm who you are and what you're allowed to do
Get-MgContext | Select-Object Account, TenantId, Scopes

What each script needs

#ScriptKey Graph scope
1MFA registration reportUserAuthenticationMethod.Read.All
2Inactive accounts (90+ days)AuditLog.Read.All + User.Read.All
3Backup Conditional Access policiesPolicy.Read.All
4Privileged role auditRoleManagement.Read.Directory
5Expiring app secrets & certificatesApplication.Read.All
6Stale guest accountsUser.Read.All + AuditLog.Read.All
7Risky users & risky sign-insIdentityRiskyUser.Read.All
8License assignment reportUser.Read.All + Organization.Read.All
9Bulk-disable accounts from CSVUser.ReadWrite.All
10Group membership snapshot & exportGroup.Read.All

1. Find Users Without MFA Registered

The single highest-value report in this list. Unregistered users are the accounts a password-spray attack will actually compromise.

# Users who have NOT registered any MFA method
Get-MgReportAuthenticationMethodUserRegistrationDetail -All |
    Where-Object { -not $_.IsMfaRegistered } |
    Select-Object UserPrincipalName, UserDisplayName, IsAdmin,
        @{n='DefaultMethod';e={$_.DefaultMfaMethod}} |
    Sort-Object IsAdmin -Descending |
    Export-Csv .\Users-Without-MFA.csv -NoTypeInformation

Write-Host "Report saved to Users-Without-MFA.csv"

Pay special attention to any row where IsAdmin is True — an unregistered admin account is a critical finding, not a to-do item.

2. Find Inactive Accounts (No Sign-In for 90+ Days)

Dormant accounts are free attack surface: nobody notices when they get compromised. Review, disable, then remove.

$cutoff = (Get-Date).AddDays(-90)

Get-MgUser -All -Property DisplayName,UserPrincipalName,AccountEnabled,SignInActivity,UserType |
    Where-Object {
        $_.AccountEnabled -and
        ($null -eq $_.SignInActivity.LastSignInDateTime -or
         $_.SignInActivity.LastSignInDateTime -lt $cutoff)
    } |
    Select-Object DisplayName, UserPrincipalName, UserType,
        @{n='LastSignIn';e={$_.SignInActivity.LastSignInDateTime}} |
    Sort-Object LastSignIn |
    Export-Csv .\Inactive-Users-90d.csv -NoTypeInformation

Note: SignInActivity requires an Entra ID P1 (or higher) license in the tenant, and a LastSignInDateTime of null usually means the account has never signed in at all — those deserve the closest look.

3. Back Up All Conditional Access Policies to JSON

Before anyone edits a Conditional Access policy, you want a point-in-time backup you can diff or restore from. This exports every policy to its own JSON file.

$backupDir = "C:\CA-Backup\$(Get-Date -Format 'yyyy-MM-dd')"
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null

Get-MgIdentityConditionalAccessPolicy -All | ForEach-Object {
    $name = ($_.DisplayName -replace '[\\/:*?"|]', '_')
    $_ | ConvertTo-Json -Depth 10 |
        Out-File -FilePath (Join-Path $backupDir "$name.json") -Encoding utf8
    Write-Host "Backed up: $($_.DisplayName)  [$($_.State)]"
}

Run this on a schedule and keep the folder in version control — it doubles as change history for your Conditional Access estate.

4. Audit Privileged Role Assignments

Answer "who is a Global Admin?" with evidence, not memory. This lists every directory role assignment with the member behind it.

$assignments = Get-MgRoleManagementDirectoryRoleAssignment -All -ExpandProperty Principal

$assignments | ForEach-Object {
    $role = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $_.RoleDefinitionId
    [pscustomobject]@{
        Role      = $role.DisplayName
        Principal = $_.Principal.AdditionalProperties['displayName']
        UPN       = $_.Principal.AdditionalProperties['userPrincipalName']
        Type      = ($_.Principal.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.','')
    }
} | Sort-Object Role | Export-Csv .\Privileged-Role-Audit.csv -NoTypeInformation

Healthy tenants keep two to four Global Administrators (including a break-glass account) and grant everything else through lesser roles — ideally as eligible assignments in Privileged Identity Management rather than standing access.

5. Find Expiring App Secrets and Certificates

An expired client secret is the classic 2 a.m. outage: integrations silently stop authenticating. This flags anything expiring in the next 60 days — and anything already expired.

$deadline = (Get-Date).AddDays(60)

Get-MgApplication -All | ForEach-Object {
    $app = $_
    ($app.PasswordCredentials + $app.KeyCredentials) | ForEach-Object {
        if ($_.EndDateTime -lt $deadline) {
            [pscustomobject]@{
                App        = $app.DisplayName
                AppId      = $app.AppId
                Credential = if ($_.Hint) { "Secret ($($_.Hint)...)" } else { "Certificate" }
                Expires    = $_.EndDateTime
                Status     = if ($_.EndDateTime -lt (Get-Date)) { "EXPIRED" } else { "Expiring soon" }
            }
        }
    }
} | Sort-Object Expires | Format-Table -AutoSize

Better yet: replace secrets with certificates entirely — we cover the full process in Replace Client Secrets with Certificate Authentication and how to rotate them without downtime.

6. Find Stale Guest Accounts

Guests accumulate forever unless someone looks. Old vendor and contractor accounts still have whatever access they were ever granted.

$cutoff = (Get-Date).AddDays(-90)

Get-MgUser -All -Filter "userType eq 'Guest'" `
    -Property DisplayName,Mail,AccountEnabled,CreatedDateTime,SignInActivity |
    Where-Object {
        $null -eq $_.SignInActivity.LastSignInDateTime -or
        $_.SignInActivity.LastSignInDateTime -lt $cutoff
    } |
    Select-Object DisplayName, Mail, AccountEnabled, CreatedDateTime,
        @{n='LastSignIn';e={$_.SignInActivity.LastSignInDateTime}} |
    Sort-Object LastSignIn |
    Export-Csv .\Stale-Guests.csv -NoTypeInformation

For ongoing hygiene, pair this report with Entra access reviews so guest access expires unless someone re-approves it.

7. Review Risky Users and Risky Sign-Ins

If you have Entra ID P2, Identity Protection is already scoring your users — this pulls anyone it currently considers at risk, plus the last week of risky sign-ins.

# Users currently flagged at risk
Get-MgRiskyUser -Filter "riskState eq 'atRisk'" |
    Select-Object UserPrincipalName, RiskLevel, RiskState, RiskLastUpdatedDateTime |
    Sort-Object RiskLevel

# Risky sign-ins from the last 7 days
$since = (Get-Date).AddDays(-7).ToString("yyyy-MM-ddTHH:mm:ssZ")
Get-MgRiskDetection -Filter "detectedDateTime ge $since" |
    Select-Object UserPrincipalName, RiskEventType, RiskLevel,
        DetectedDateTime, IPAddress |
    Sort-Object DetectedDateTime -Descending | Format-Table -AutoSize

Any high-risk user warrants an immediate password reset, session revocation, and a check of their mailbox rules and registered MFA methods.

8. License Assignment Report

Know what you're paying for. This maps every user to their assigned SKUs and shows totals so you can spot unused licenses.

# Tenant-wide license totals
Get-MgSubscribedSku |
    Select-Object SkuPartNumber,
        @{n='Purchased';e={$_.PrepaidUnits.Enabled}},
        @{n='Assigned';e={$_.ConsumedUnits}},
        @{n='Unused';e={$_.PrepaidUnits.Enabled - $_.ConsumedUnits}} |
    Format-Table -AutoSize

# Per-user assignments
$skuMap = @{}
Get-MgSubscribedSku | ForEach-Object { $skuMap[$_.SkuId] = $_.SkuPartNumber }

Get-MgUser -All -Property DisplayName,UserPrincipalName,AssignedLicenses |
    Where-Object { $_.AssignedLicenses.Count -gt 0 } |
    Select-Object DisplayName, UserPrincipalName,
        @{n='Licenses';e={($_.AssignedLicenses | ForEach-Object { $skuMap[$_.SkuId] }) -join '; '}} |
    Export-Csv .\License-Report.csv -NoTypeInformation

9. Bulk-Disable Accounts from a CSV (Offboarding)

The first script in this list that makes changes. Feed it a CSV with a UserPrincipalName column (for example, the inactive-accounts report from #2 after review) and it disables each account and revokes active sessions.

# Requires: Connect-MgGraph -Scopes "User.ReadWrite.All"
$users = Import-Csv .\To-Disable.csv

foreach ($u in $users) {
    try {
        Update-MgUser -UserId $u.UserPrincipalName -AccountEnabled:$false
        Revoke-MgUserSignInSession -UserId $u.UserPrincipalName | Out-Null
        Write-Host "Disabled + sessions revoked: $($u.UserPrincipalName)"
    }
    catch {
        Write-Warning "FAILED: $($u.UserPrincipalName) - $($_.Exception.Message)"
    }
}

Review the CSV before running. Disabling is reversible; the session revocation will force every device the user has to reauthenticate. Never point this at an unreviewed export.

10. Group Membership Snapshot and Export

Perfect for audits and change tickets: a point-in-time export of who is in which group, including whether membership is assigned or dynamic.

$report = foreach ($g in Get-MgGroup -All -Property Id,DisplayName,GroupTypes) {
    foreach ($m in Get-MgGroupMember -GroupId $g.Id -All) {
        [pscustomobject]@{
            Group      = $g.DisplayName
            Membership = if ($g.GroupTypes -contains 'DynamicMembership') { 'Dynamic' } else { 'Assigned' }
            Member     = $m.AdditionalProperties['displayName']
            UPN        = $m.AdditionalProperties['userPrincipalName']
        }
    }
}
$report | Export-Csv .\Group-Membership-$(Get-Date -Format 'yyyy-MM-dd').csv -NoTypeInformation
Write-Host "Exported $($report.Count) membership rows."

In a large tenant, scope this to the groups you care about (for example, groups used in Conditional Access or license assignment) by adding a -Filter to Get-MgGroup.

Make Them Yours

Save these as .ps1 files in a scripts repository, run the read-only reports on a monthly schedule, and store the CSV output where your team reviews it — a report nobody reads is the same as no report. When you're ready to run them unattended, switch from interactive sign-in to certificate-based app-only authentication so no credentials ever live in the script.

Want this level of visibility into your Microsoft 365 tenant without doing it yourself? That's exactly what our Managed IT Services and Microsoft 365 Services deliver — including scheduled security reporting and remediation.

References

Top 10 essential Entra PowerShell scripts | IT Master Services