← cd ~/tools
// tools/scripts
Script library
All 44 scripts and queries from the posts on this site, in one place. Search by what you want to do, filter by language and copy. Each one links back to the post that explains it.
-
3. Review what's already grantedLock down app consent without blocking your usersPowerShell
Connect-MgGraph -Scopes "Directory.Read.All" Get-MgOauth2PermissionGrant -All | Where-Object { $_.Scope -match "Mail\.|Files\.|Sites\.|Directory\." } | ForEach-Object { [pscustomobject]@{ App = (Get-MgServicePrincipal -ServicePrincipalId $_.ClientId).DisplayName ConsentType = $_.ConsentType Scope = $_.Scope } } | Sort-Object App | Format-Table -AutoSize -
1. Create a certificateUnattended Graph PowerShell scripts with certificate authenticationPowerShell
$cert = New-SelfSignedCertificate -Subject "CN=Graph-UserReport" ` -CertStoreLocation "Cert:\LocalMachine\My" ` -KeyExportPolicy NonExportable -KeySpec Signature ` -NotAfter (Get-Date).AddYears(1) Export-Certificate -Cert $cert -FilePath C:\Temp\Graph-UserReport.cer -
3. Connect in the scriptUnattended Graph PowerShell scripts with certificate authenticationPowerShell
Connect-MgGraph -ClientId "<app-id>" -TenantId "<tenant-id>" ` -CertificateThumbprint "<thumbprint>" -NoWelcome Get-MgUser -All -Property DisplayName,UserPrincipalName,AccountEnabled | Export-Csv C:\Reports\users.csv -NoTypeInformation -
Graph HTTP
PATCH https://graph.microsoft.com/beta/policies/authenticationmethodspolicy Content-Type: application/json { "optOutSettings": { "passkeyDynamicMigration": true } } -
CLI / shell
az consumption budget create \ --budget-name monthly-dev \ --amount 500 \ --category cost \ --time-grain monthly \ --start-date 2026-09-01 \ --end-date 2027-08-31 -
CLI / shell
nslookup stdata.blob.core.windows.net -
PowerShell
# PowerShell equivalent, showing the CNAME chain Resolve-DnsName stdata.blob.core.windows.net | Format-Table Name, Type, IPAddress, NameHost -
Switching overKey Vault: move from access policies to Azure RBACCLI / shell
az keyvault update --name kv-app-prod --resource-group rg-app --enable-rbac-authorization true -
Finding vaults still on access policiesKey Vault: move from access policies to Azure RBACKQL
// Azure Resource Graph resources | where type == "microsoft.keyvault/vaults" | extend rbac = tobool(properties.enableRbacAuthorization) | where rbac != true | project name, resourceGroup, subscriptionId -
A safer stale-device processSoft delete for Entra device objects: a safety net for device clean-upsPowerShell
Connect-MgGraph -Scopes "Device.Read.All" $cutoff = (Get-Date).AddDays(-90) Get-MgDevice -All -Property DisplayName,OperatingSystem,ApproximateLastSignInDateTime,AccountEnabled | Where-Object { $_.ApproximateLastSignInDateTime -lt $cutoff } | Sort-Object ApproximateLastSignInDateTime | Select-Object DisplayName, OperatingSystem, ApproximateLastSignInDateTime, AccountEnabled -
PowerShell
Connect-MgGraph -Scopes "Device.ReadWrite.All" $cutoff = (Get-Date).AddDays(-90) $stale = Get-MgDevice -All -Property Id,DisplayName,ApproximateLastSignInDateTime,AccountEnabled | Where-Object { $_.AccountEnabled -and $_.ApproximateLastSignInDateTime -lt $cutoff } $stale | Export-Csv stale-devices.csv -NoTypeInformation # keep a record $stale | ForEach-Object { Update-MgDevice -DeviceId $_.Id -AccountEnabled:$false } -
CLI / shell
IntuneWinAppUtil.exe -c C:\Packages\7zip -s 7z-x64.msi -o C:\Packages\Output -
Install and uninstall commandsPackaging Win32 apps for Intune: detection rules and return codesCLI / shell
msiexec /i "7z-x64.msi" /qn /norestart msiexec /x {23170F69-40C1-2702-0000-000001000000} /qn /norestart -
Example: clear a full temp folderIntune Remediations: find and fix problems before users noticePowerShell
$sizeGB = (Get-ChildItem "$env:SystemRoot\Temp" -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1GB if ($sizeGB -gt 5) { Write-Output "Temp is $([math]::Round($sizeGB,1)) GB"; exit 1 } Write-Output "Temp OK"; exit 0 -
Example: clear a full temp folderIntune Remediations: find and fix problems before users noticePowerShell
Get-ChildItem "$env:SystemRoot\Temp" -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue Write-Output "Cleaned temp folder"; exit 0 -
1. Top sign-in failuresSix KQL queries for Entra sign-in logs every admin should keepKQL
SigninLogs | where TimeGenerated > ago(7d) and ResultType != "0" | summarize Count = count() by ResultType, ResultDescription | top 20 by Count -
2. One user's recent sign-insSix KQL queries for Entra sign-in logs every admin should keepKQL
SigninLogs | where TimeGenerated > ago(3d) | where UserPrincipalName =~ "jane.doe@contoso.com" | project TimeGenerated, AppDisplayName, ResultType, ResultDescription, IPAddress, Location = tostring(LocationDetails.countryOrRegion), ConditionalAccessStatus | order by TimeGenerated desc -
3. Sign-ins blocked by Conditional AccessSix KQL queries for Entra sign-in logs every admin should keepKQL
SigninLogs | where TimeGenerated > ago(1d) and ConditionalAccessStatus == "failure" | summarize Count = count() by UserPrincipalName, AppDisplayName | order by Count desc -
4. Successful sign-ins from new countriesSix KQL queries for Entra sign-in logs every admin should keepKQL
let known = SigninLogs | where TimeGenerated between (ago(30d) .. ago(1d)) and ResultType == "0" | distinct UserPrincipalName, Country = tostring(LocationDetails.countryOrRegion); SigninLogs | where TimeGenerated > ago(1d) and ResultType == "0" | extend Country = tostring(LocationDetails.countryOrRegion) | join kind=leftanti known on UserPrincipalName, Country | project TimeGenerated, UserPrincipalName, Country, IPAddress, AppDisplayName -
5. Password spray patternSix KQL queries for Entra sign-in logs every admin should keepKQL
SigninLogs | where TimeGenerated > ago(1h) and ResultType == "50126" | summarize Users = dcount(UserPrincipalName) by IPAddress | where Users > 10 | order by Users desc -
6. Legacy authentication still in useSix KQL queries for Entra sign-in logs every admin should keepKQL
SigninLogs | where TimeGenerated > ago(14d) | where ClientAppUsed !in ("Browser", "Mobile Apps and Desktop clients") | summarize Count = count() by ClientAppUsed, UserPrincipalName, AppDisplayName | order by Count desc -
7. Non-interactive sign-ins by appSix KQL queries for Entra sign-in logs every admin should keepKQL
AADNonInteractiveUserSignInLogs | where TimeGenerated > ago(1d) | summarize Count = count(), Failures = countif(ResultType != "0") by AppDisplayName | order by Count desc -
8. Service principal sign-ins from unexpected IPsSix KQL queries for Entra sign-in logs every admin should keepKQL
AADServicePrincipalSignInLogs | where TimeGenerated > ago(7d) and ResultType == "0" | summarize IPs = make_set(IPAddress, 20), Count = count() by ServicePrincipalName | where array_length(IPs) > 3 | order by Count desc -
The two lock typesResource locks: a cheap insurance policy against the wrong clickCLI / shell
az lock create --name do-not-delete \ --lock-type CanNotDelete \ --resource-group rg-core-networking \ --notes "Hub networking. Raise a change before removing." -
Managing locks at scaleResource locks: a cheap insurance policy against the wrong clickCLI / shell
# Every lock in the current subscription az lock list --output table # Remove one (needs Microsoft.Authorization/locks/delete) az lock delete --name do-not-delete --resource-group rg-core-networking -
Where errors show upGroup-based licensing: finding and fixing assignment errorsPowerShell
Connect-MgGraph -Scopes "Group.Read.All" Get-MgGroup -All -Filter "hasMembersWithLicenseErrors eq true" | Select-Object DisplayName, Id -
Reprocessing after a fixGroup-based licensing: finding and fixing assignment errorsPowerShell
Connect-MgGraph -Scopes "User.ReadWrite.All" $g = Get-MgGroup -Filter "displayName eq 'LIC-M365-E5'" Get-MgGroupMember -GroupId $g.Id -All | ForEach-Object { Invoke-MgLicenseUser -UserId $_.Id } -
Roll out with the right effectAzure Policy guardrails every subscription should haveCLI / shell
# Assign Allowed locations at a management group, UK regions only az policy assignment create \ --name allowed-locations \ --display-name "Allowed locations: UK" \ --scope "/providers/Microsoft.Management/managementGroups/mg-landingzones" \ --policy "e56962a6-4747-49cd-b67b-bf8b01975c4c" \ --params '{ "listOfAllowedLocations": { "value": ["uksouth","ukwest"] } }' -
How evaluation worksAzure Policy guardrails every subscription should haveCLI / shell
# Run a compliance scan now rather than waiting az policy state trigger-scan --resource-group rg-app-prod # What's non-compliant at a management group? az policy state summarize --management-group mg-landingzones -
3. Update the workflowDeploy from GitHub Actions to Azure without storing a single secretYAML
permissions: id-token: write contents: read jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - run: az group show --name rg-app -
PowerShell
# Give a Function App's system-assigned identity read access to a storage account $principalId = az functionapp identity assign -g rg-app -n func-app --query principalId -o tsv az role assignment create --assignee $principalId \ --role "Storage Blob Data Reader" \ --scope "/subscriptions/<sub-id>/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stdata" -
Building a country block safelyNamed locations: getting IP ranges and countries right in Conditional AccessKQL
SigninLogs | where TimeGenerated > ago(30d) and ResultType == "0" | summarize Users = dcount(UserPrincipalName), SignIns = count() by Country = tostring(LocationDetails.countryOrRegion) | order by SignIns desc -
One subscription by CLITurning on Defender CSPM across a landing zoneCLI / shell
az account set --subscription "<subscription-id>" az security pricing create --name CloudPosture --tier Standard az security pricing show --name CloudPosture -
Checking coverageTurning on Defender CSPM across a landing zoneKQL
// Azure Resource Graph: Defender CSPM status per subscription securityresources | where type == "microsoft.security/pricings" and name == "CloudPosture" | project subscriptionId, tier = tostring(properties.pricingTier) | order by tier asc -
Reviewing the impactTest Conditional Access safely with report-only mode and What IfKQL
SigninLogs | where TimeGenerated > ago(7d) | mv-expand ConditionalAccessPolicies | where ConditionalAccessPolicies.displayName == "Require compliant device" | where ConditionalAccessPolicies.result == "reportOnlyFailure" | summarize Failures = count() by UserPrincipalName, AppDisplayName | order by Failures desc -
MonitoringBreak-glass accounts done rightKQL
SigninLogs | where UserPrincipalName in~ ("bg-admin1@contoso.onmicrosoft.com", "bg-admin2@contoso.onmicrosoft.com") | project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, ResultType -
Connect with the right scopesActivate PIM roles from PowerShell with Microsoft GraphPowerShell
Connect-MgGraph -Scopes "RoleEligibilitySchedule.Read.Directory", "RoleAssignmentSchedule.ReadWrite.Directory" $me = Get-MgUser -UserId (Get-MgContext).Account -
List your eligible rolesActivate PIM roles from PowerShell with Microsoft GraphPowerShell
$eligible = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -Filter "principalId eq '$($me.Id)'" -ExpandProperty RoleDefinition $eligible | Select-Object @{n="Role";e={$_.RoleDefinition.DisplayName}}, DirectoryScopeId, EndDateTime -
PowerShell
$role = $eligible | Where-Object { $_.RoleDefinition.DisplayName -eq "Exchange Administrator" } $params = @{ Action = "selfActivate" PrincipalId = $me.Id RoleDefinitionId = $role.RoleDefinitionId DirectoryScopeId = $role.DirectoryScopeId Justification = "Investigating mail flow issue" ScheduleInfo = @{ StartDateTime = Get-Date Expiration = @{ Type = "AfterDuration"; Duration = "PT2H" } } } New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params -
Check the result, extend or end earlyActivate PIM roles from PowerShell with Microsoft GraphPowerShell
# Requests you've made, newest first Get-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -Filter "principalId eq '$($me.Id)'" | Sort-Object CreatedDateTime -Descending | Select-Object -First 5 Action, Status, RoleDefinitionId, CreatedDateTime # Finished early? Deactivate rather than leave it running $params.Action = "selfDeactivate" $params.Remove("ScheduleInfo"); $params.Remove("Justification") New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params -
Wrap it in a functionActivate PIM roles from PowerShell with Microsoft GraphPowerShell
function Enable-PimRole { param([Parameter(Mandatory)][string]$Role, [string]$Reason = "Planned admin work", [int]$Hours = 1) $me = Get-MgUser -UserId (Get-MgContext).Account $r = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -Filter "principalId eq '$($me.Id)'" -ExpandProperty RoleDefinition | Where-Object { $_.RoleDefinition.DisplayName -eq $Role } if (-not $r) { throw "Not eligible for $Role" } New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter @{ Action = "selfActivate"; PrincipalId = $me.Id RoleDefinitionId = $r.RoleDefinitionId; DirectoryScopeId = $r.DirectoryScopeId Justification = $Reason ScheduleInfo = @{ StartDateTime = Get-Date; Expiration = @{ Type = "AfterDuration"; Duration = "PT$($Hours)H" } } } } Enable-PimRole -Role "Intune Administrator" -Reason "Deploying compliance policy" -Hours 2 -
PowerShell
Connect-MgGraph -Scopes "Application.Read.All" $days = 60 $now = Get-Date Get-MgApplication -All -Property DisplayName,AppId,PasswordCredentials,KeyCredentials | ForEach-Object { $app = $_ $creds = @( $app.PasswordCredentials | ForEach-Object { [pscustomobject]@{ Type="Secret"; Name=$_.DisplayName; End=$_.EndDateTime } } $app.KeyCredentials | ForEach-Object { [pscustomobject]@{ Type="Certificate"; Name=$_.DisplayName; End=$_.EndDateTime } } ) foreach ($c in $creds) { [pscustomobject]@{ App = $app.DisplayName AppId = $app.AppId Type = $c.Type Name = $c.Name Expires = $c.End DaysLeft = [int]($c.End - $now).TotalDays } } } | Where-Object DaysLeft -le $days | Sort-Object DaysLeft | Format-Table -AutoSize -
Quick inventory with GraphRolling a SAML signing certificate without an outagePowerShell
Connect-MgGraph -Scopes "Application.Read.All" Get-MgServicePrincipal -All -Property DisplayName,KeyCredentials,PreferredSingleSignOnMode | Where-Object PreferredSingleSignOnMode -eq "saml" | ForEach-Object { foreach ($k in $_.KeyCredentials | Where-Object Usage -eq "Verify") { [pscustomobject]@{ App = $_.DisplayName Expires = $k.EndDateTime DaysLeft = [int]($k.EndDateTime - (Get-Date)).TotalDays } } } | Sort-Object DaysLeft | Format-Table -
Issue one with PowerShellTemporary Access Pass: onboarding users without a passwordPowerShell
Connect-MgGraph -Scopes "UserAuthenticationMethod.ReadWrite.All" $tap = New-MgUserAuthenticationTemporaryAccessPassMethod -UserId "new.starter@contoso.com" -BodyParameter @{ lifetimeInMinutes = 60 isUsableOnce = $true } $tap.TemporaryAccessPass
No scripts match. Try a shorter word.
Read the post before running anything in production. Scripts use example names like contoso.com and need the scopes or roles described in each post.