Topic Hub

PowerShell

Automation, Graph API integration, Active Directory administration, Intune and endpoint reporting, patch compliance, and safe scripting patterns for Windows and Microsoft 365 administrators.

Guides, scripts and reference

Overview

What PowerShell Is Used For in Windows and M365 Administration

PowerShell is the primary automation runtime for Microsoft environments. In production it runs everything from one-off AD queries to scheduled compliance reports — and with the Microsoft Graph PowerShell SDK, it now reaches every service in the Microsoft 365 stack.

Active Directory and on-premises automation

The ActiveDirectory module (RSAT) provides Get-ADUser, Get-ADComputer, Get-ADGroup, and set/move/disable equivalents. Bulk operations — disabling stale accounts, syncing attribute sets, generating OU membership reports — are standard daily-use cases.

Microsoft Graph API integration

The Microsoft.Graph module wraps Graph API endpoints as PowerShell cmdlets. Use it to query Intune device inventory, Entra ID users, group memberships, Teams, SharePoint, and Exchange Online without the admin portals.

Intune and endpoint management

Get-MgDeviceManagementManagedDevice and related Graph cmdlets export compliance state, OS version, last check-in, and primary user for every enrolled device. Proactive Remediation script pairs (detect + remediate) run on Intune-managed endpoints on a configurable schedule.

Patch and vulnerability reporting

Query the Windows Update Agent via COM API, WSUS database, or WUfB Reports in Log Analytics. Generate per-device patch lag reports, identify devices missing critical KBs, and produce HTML dashboards for weekly review.

Security baseline validation

Scripts like Invoke-WindowsHardening apply CIS Level 1/2 controls and generate a pre/post compliance delta report. Run as an Intune Proactive Remediation to continuously validate that baseline settings have not drifted from policy.

Infrastructure provisioning and lab work

Hyper-V management (New-VM, Checkpoint-VM), WinRM configuration for remoting, unattend.xml-driven OS deployments, and repeatable lab baseline scripts reduce setup time from hours to minutes and make environments reproducible.

Safe scripting

Safe Scripting Practices for Production Environments

Scripts running against production AD, Intune, or Exchange Online can cause outages or data loss if written carelessly. These practices separate scripts that are safe to schedule from ones that should never leave a test environment.

Use -WhatIf and -Confirm before destructive actions

Any cmdlet that modifies, removes, or moves objects should be tested with -WhatIf first to preview what would change. Build -WhatIf support into your own functions using [CmdletBinding(SupportsShouldProcess)] so callers can do the same. Never run bulk set/remove operations against production without a -WhatIf pass first.

Validate input at script boundaries

Use [Parameter(Mandatory)] with [ValidateNotNullOrEmpty()] and type constraints on all inputs. Fail fast with a meaningful error if inputs are outside expected range — before touching any real objects. Untrusted input (CSV files, API responses) should always be sanitised before being passed to cmdlets that accept pipeline input.

Never hardcode credentials

Credentials in script files get committed to version control, stored in scheduled task history, and logged in transcripts. Use Get-StoredCredential (CredentialManager module), certificate-based app registrations, managed identities, or Windows Credential Manager via [System.Net.NetworkCredential]. For service accounts, use gMSAs where available.

Scope to least privilege

Graph API app registrations should request only the permission scopes the script actually uses — never Directory.ReadWrite.All when User.Read.All is sufficient. AD scripts should run as a service account with delegation scoped to the specific OU or attribute. Audit permission scopes quarterly as scripts evolve.

Test against a non-production target

For Intune scripts, use a test Entra ID group with non-critical devices before assigning to production groups. For AD scripts, run against a test OU. For Exchange scripts, run against a mailbox you own. The cost of a test environment is always less than the cost of a production rollback.

Version control and peer review

Production scripts belong in source control (Git). Changes to scheduled scripts should go through a pull request review — the same bar you apply to application code. Comment the WHY of non-obvious logic, not the WHAT. Include a changelog block in the script header with author, date, and summary of each change.

Trust and policy

Execution Policy, Script Signing, and AMSI

Execution policy is not a security boundary — it is a speed bump. Actual script security in enterprise environments comes from code signing, constrained language mode, and AMSI integration with your AV or EDR stack.

Configuration, not a security control

Execution Policy

Restricted
Default on Windows clients — no scripts run. Interactive commands only. Enforce via GPO (Turn on Script Execution = Disabled) for kiosks and standard user machines.
RemoteSigned
Recommended baseline for managed admin machines — locally authored scripts run freely, scripts downloaded from the internet require a trusted signature. Set via GPO or Intune CSP.
AllSigned
Every script must be signed by a trusted code signing certificate. Appropriate for high-security environments. Requires a code signing CA and signing workflow for all team-authored scripts.
Bypass
No policy applied — used by automated pipelines running pre-validated signed scripts in a controlled runner context. Never set as the user-scope default on a managed workstation.
Scope precedence
MachinePolicy (GPO) > UserPolicy > Process > CurrentUser > LocalMachine. GPO wins — a user cannot override a machine policy set via Group Policy or Intune.

Enterprise signing workflow

Code Signing

Obtain a code signing cert
Issue from an internal PKI CA (recommended — free, trusted by domain members) or purchase an EV code signing certificate from a public CA. Self-signed certificates are only suitable for single-machine testing.
Sign a script
Set-AuthenticodeSignature -FilePath .\script.ps1 -Certificate (Get-Item Cert:\CurrentUser\My\<thumbprint>). The signature block is appended to the script file as a comment.
Trust the certificate
Add the signing CA to Trusted Publishers via GPO (Computer Configuration > Windows Settings > Security Settings > Public Key Policies > Trusted Publishers) or Intune certificate profile.
Signature invalidation
Any edit to a signed script — including whitespace changes — invalidates the signature. Re-sign after every change. Use a signing step in your CI/CD pipeline to enforce this.
Timestamp signing
Sign with a timestamp (Set-AuthenticodeSignature ... -TimestampServer http://timestamp.digicert.com) so the signature remains valid after the signing certificate expires.

Runtime enforcement

AMSI and Constrained Language Mode

What AMSI does
The Antimalware Scan Interface passes script content to the registered AV/EDR before execution. Defender and CrowdStrike both hook AMSI — obfuscated or malicious script content is caught at runtime regardless of execution policy.
Constrained Language Mode
CLM restricts which .NET types and methods PowerShell scripts can access — blocking most post-exploitation techniques. Enabled automatically when AppLocker or WDAC is enforcing Allow rules. Scripts in CLM cannot call arbitrary .NET methods or use Add-Type.
Script Block Logging
Enable via GPO (Turn on PowerShell Script Block Logging) or Intune. Logs the full de-obfuscated script content to the Microsoft-Windows-PowerShell/Operational event log (Event ID 4104) before execution — useful for incident response.
Module logging
Enable Turn on Module Logging via GPO to log all pipeline execution detail per module. Verbose but comprehensive — captures all cmdlet invocations including those from imported modules.
Transcript logging
Enable Turn on PowerShell Transcription via GPO to write a per-session transcript to a central share. Captures all input and output including interactive sessions — useful for SOC visibility into admin activity.

Tutorials & Guides

Exchange Online SMTP AUTH Basic Authentication 2026 Migration Planning

A practical operational guide for planning Exchange Online SMTP AUTH Basic Authentication and credential-based Exchange Online PowerShell automation migrations, covering inventory, EAC and Entra checks, mailbox and tenant settings, OAuth, High Volume Email, Azure Communication Services Email, relay caveats, app-only PowerShell, managed identity, rollback, and prevention controls.

32 min read · Advanced

Migrating AzureAD and MSOnline PowerShell Scripts to Microsoft Graph PowerShell SDK

A practical migration guide for replacing production AzureAD and MSOnline PowerShell scripts with Microsoft Graph PowerShell SDK, covering module strategy, delegated and app-only authentication, managed identity, permission discovery, cmdlet mapping, paging, OData filters, eventual consistency, throttling, beta endpoint risk, logging, rollback, and prevention checks.

34 min read · Advanced

Migrating Intune Administrative Templates to Settings Catalog Without Breaking Policy Behaviour

A practical migration guide for moving Intune Administrative Templates and older configuration profiles to Settings Catalog, covering inventory, duplicate settings, assignments, Graph PowerShell checks, conflict detection, pilot design, validation, reporting, rollback, and prevention controls.

26 min read · Advanced

Microsoft 365 Admin Centre Mandatory MFA Readiness for Admins

A practical operational guide for Microsoft 365 admin centre mandatory MFA readiness, covering affected admins, break-glass accounts, security defaults, Conditional Access, per-user MFA, phishing-resistant methods, Graph PowerShell audits, sign-in checks, service-style admin accounts, Phase 2 tooling impact, safe rollout, and recovery planning.

22 min read · Advanced

Graph API

Microsoft Graph Automation with PowerShell

The Microsoft.Graph module (v2+) is the recommended way to interact with Microsoft 365 services from PowerShell. It replaces the older AzureAD, MSOnline, and individual service modules — most of which are now deprecated.

Authentication: delegated vs application

Delegated authentication (Connect-MgGraph -Scopes) prompts for user sign-in and inherits user permissions. Use for interactive admin sessions. Application authentication uses a client ID and secret or certificate registered in Entra — required for scheduled scripts and pipelines where no user is present. Prefer certificate over client secret for non-interactive auth.

Requesting minimum scopes

Connect-MgGraph -Scopes "User.Read.All","DeviceManagementManagedDevices.Read.All" requests only what the session needs. The first connection with a new scope combination triggers admin consent. Check Find-MgGraphCommand -Command Get-MgUser to discover which scopes a specific cmdlet requires before building your app registration.

Handling pagination with -All

Graph API returns results in pages of 100 by default. Add -All to any Get-Mg* cmdlet to automatically follow @odata.nextLink until all results are returned. For large tenants (10k+ devices or users), pipe through Select-Object immediately to avoid holding the full result set in memory.

Throttling and retry logic

Graph API enforces per-tenant, per-app, and per-resource throttling limits. A 429 response includes a Retry-After header. The Microsoft.Graph module does not automatically retry throttled requests — wrap calls in a do/while loop that checks $_.Exception.Response.StatusCode -eq 429 and sleeps for the Retry-After value.

Filtering server-side with $filter

Always filter on the server when possible: Get-MgDeviceManagementManagedDevice -Filter "complianceState eq 'noncompliant'" instead of retrieving all devices and filtering in PowerShell. Server-side filtering reduces response size, latency, and throttle consumption — critical for large Intune tenants.

Batch requests for high-volume operations

Graph batch requests combine up to 20 individual API calls into a single HTTP request. Use Invoke-MgGraphRequest with a $batch body to run 20 GET requests in one round trip — effective for per-device lookups across a list of IDs. Batching reduces overall latency and throttle impact compared to 20 sequential calls.

Active Directory

Active Directory and Windows Server Automation

The ActiveDirectory PowerShell module (part of RSAT) covers the majority of daily AD administration tasks. For server management across multiple machines, PowerShell remoting and CIM sessions provide a consistent execution layer.

RSAT and the AD module

Install on Windows 10/11 via Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0. On Windows Server, Install-WindowsFeature RSAT-AD-PowerShell. The module loads automatically when you call an AD cmdlet — no explicit Import-Module required on domain-joined machines.

Querying AD objects efficiently

Use -Filter instead of -LDAPFilter for simpler queries; use -LDAPFilter for complex multi-attribute filters. Always specify -Properties to retrieve only the attributes you need — Get-ADUser -Properties * is expensive on large directories. Use Get-ADOrganizationalUnit with -SearchBase to scope queries to a specific container.

Bulk account operations

Get-ADUser -Filter {Enabled -eq $true} | Where-Object {$_.LastLogonDate -lt (Get-Date).AddDays(-90)} | Disable-ADAccount is the standard pattern for stale account cleanup. Always pipe to a report first, review the list, then run the disable pass with confirmation or a -WhatIf sweep.

PowerShell remoting (WinRM)

Enable-PSRemoting on target machines. Use Enter-PSSession for interactive remote sessions and Invoke-Command -ComputerName for scriptblock execution across one or many machines. Use -Credential with a service account — never pass plain-text credentials. For cross-domain or workgroup machines, configure TrustedHosts and use HTTPS transport.

CIM sessions for WMI queries

New-CimSession creates a persistent connection to a remote machine for WMI/CIM queries. Use Get-CimInstance -CimSession $session -ClassName Win32_OperatingSystem instead of deprecated Get-WmiObject. CIM sessions use WinRM by default (DCOM with -SessionOption as fallback for older machines).

Group Policy reporting from PowerShell

Get-GPResultantSetOfPolicy -ReportType HTML generates an RSoP report for a user/computer combination without requiring a logon. Get-GPO -All | Get-GPOReport -ReportType XML exports all GPO settings for offline analysis or version-controlled backup. Combine with Backup-GPO for disaster recovery.

Intune automation

Intune and Endpoint Reporting Scripts

The Graph API surfaces every piece of data visible in the Intune portal — and more. These are the most operationally useful queries for endpoint management at scale.

Device inventory and compliance export

Get-MgDeviceManagementManagedDevice -All -Filter "operatingSystem eq 'Windows'" | Select-Object DeviceName,ComplianceState,OsVersion,LastSyncDateTime | Export-Csv gives you the core fleet inventory. Add UserPrincipalName via the primaryUsers relationship for a per-user view. See Export-IntuneDeviceReport in the script library.

Stale device cleanup workflow

Get-StaleDevices cross-references Intune, Entra ID, and on-premises AD for devices that have not checked in within a configurable threshold. The script outputs a CSV with recommended actions (retire, delete, hybrid-join reconcile) rather than acting directly — review before any destructive step.

Proactive Remediation script pairs

Detection scripts return exit code 0 (compliant) or 1 (non-compliant). Remediation scripts run when detection returns 1. Use $env:COMPUTERNAME and $env:USERNAME for context. Write output to stdout — it appears in the Intune portal under device proactive remediation status. Keep each script under 200KB and avoid downloading additional files from within a remediation script.

App installation state queries

GET /deviceAppManagement/managedDevices/{id}/deviceAppStates returns per-app install state for a device. Run across your fleet to identify devices where a required app is in Pending or Failed state — the portal surfaces this per device but not as a fleet-wide exportable report.

Targeting with Entra ID dynamic groups

Dynamic group membership rules use deviceOSVersion, deviceModel, displayName, and enrollmentProfileName attributes — all settable via PowerShell. Use Get-MgGroup -Filter "displayName eq 'Ring0-Pilot'" | Get-MgGroupMember to verify group membership before using the group as an Intune assignment target.

Graph API vs. Intune module methods

The Microsoft.Graph module wraps Graph REST endpoints. For fine-grained control — custom $select/$expand/$filter, beta API endpoints, or operations the module does not yet expose — use Invoke-MgGraphRequest -Method GET -Uri "/v1.0/deviceManagement/managedDevices?$select=id,deviceName,complianceState&$filter=complianceState eq 'noncompliant'".

Patch reporting

Patch and Compliance Reporting Scripts

Automated patch compliance reporting closes the gap between what Intune or WSUS reports in the portal and what leadership or audit needs in an exportable format.

Windows Update Agent (COM API)

The Microsoft.Update.Session COM object exposes the local Windows Update client without requiring admin rights for read operations. $Searcher.Search("IsInstalled=0 and IsHidden=0") returns all missing updates with Title, KBID, and MsrcSeverity properties. Use in Get-PatchComplianceReport for per-device missing update lists.

WSUS database queries

WSUS stores update and client data in a SQL Server (or WID) database. Invoke-Sqlcmd against the SUSDB lets you build custom reports beyond what the WSUS console exposes — e.g., all computers that have not reported in 30 days, or all updates approved but not installed across the fleet. Requires db_datareader on SUSDB.

WUfB Reports via Log Analytics

Windows Update for Business Reports populates UCClient, UCDeviceAlert, and UCUpdateAlert tables in a Log Analytics workspace. Query via Invoke-AzOperationalInsightsQuery (Az.OperationalInsights module) or the REST API. Useful for fleet-wide deferral compliance across Intune-managed and hybrid-joined devices.

Graph-based update compliance

GET /deviceManagement/managedDevices?$select=deviceName,osVersion,complianceState,lastSyncDateTime returns OS version and compliance state per Intune device. Cross-reference osVersion against the minimum OS version in your compliance policy to identify devices on end-of-support or non-compliant builds.

Exporting to HTML dashboards

ConvertTo-Html with -PreContent and -PostContent generates styled HTML reports from PowerShell output. Embed CSS inline for portability. Schedule via Task Scheduler or Azure Automation to drop a dated report file to a network share or SharePoint document library each week.

Scheduled email delivery

Send-MgUserMail (Graph API) or Send-MailMessage (SMTP, deprecated in PS7) delivers reports to a distribution list on a schedule. Prefer Graph API for M365 tenants — it uses OAuth and does not require SMTP AUTH, which is increasingly blocked. Attach the CSV as a base64-encoded blob in the message body.

Error handling and logging

Logging, Transcript, and Error Handling Patterns

Scripts that run unattended need to communicate failure clearly — either by writing structured output, raising alerts, or producing logs that a human can read after the fact. Silent failure is the most dangerous pattern in scheduled automation.

Try / Catch / Finally structure

Wrap every operation that can fail in a try block. Set $ErrorActionPreference = "Stop" at the top of the script to ensure non-terminating errors (e.g., cmdlet warnings) are promoted to terminating exceptions that Catch can intercept. In the Catch block, log $_.Exception.Message with context — not just "an error occurred."

Start-Transcript for unattended scripts

Call Start-Transcript -Path "$LogPath\$(Get-Date -Format yyyyMMdd-HHmmss)-scriptname.log" -Append at the start of every scheduled script. Stop-Transcript in the Finally block ensures the log closes even if the script exits unexpectedly. Transcripts capture all output — useful for debugging after the fact without needing to reproduce the run.

Write-EventLog for Windows event integration

New-EventLog -LogName Application -Source "AdminSignal-Scripts" once (at setup time), then Write-EventLog -LogName Application -Source "AdminSignal-Scripts" -EventId 1000 -EntryType Error -Message $errorDetail writes structured events that IT monitoring and SIEM tools can alert on — without requiring file share access to the transcript.

$ErrorActionPreference and -ErrorAction

Set $ErrorActionPreference = "Stop" globally to catch all errors. Override per-cmdlet with -ErrorAction SilentlyContinue when you intentionally want to ignore a specific failure (e.g., Test-Path returning false). Never set SilentlyContinue globally — it masks real failures. Use -ErrorVariable to capture errors without stopping execution for non-critical steps.

Structured output over Write-Host

Write-Host writes to the host and cannot be captured or redirected. Use Write-Output for data, Write-Verbose for diagnostic detail (shown with -Verbose), Write-Warning for non-fatal issues, and Write-Error for failures. Scripts that return structured objects (not formatted strings) can be piped and composed by callers.

Exit codes for scheduled tasks and pipelines

Task Scheduler and CI/CD pipelines read the script exit code to determine success or failure. Use Exit 0 for success and Exit 1 (or a non-zero code) in your Catch block for failures — this triggers task scheduler failure notification or pipeline step failure. PowerShell scripts invoked via powershell.exe return $LASTEXITCODE to the caller; pwsh.exe (PS7) does the same.

Automation runners

Scheduled Task and Automation Runner Considerations

Where a script runs and what account it runs under determines what it can reach and what it leaves behind. These considerations separate a script that works on your workstation from one that runs reliably on a schedule in production.

Task Scheduler — action account

Run As: SYSTEM has local machine rights but no network credentials — useless for AD queries or Graph API calls. Run As: NETWORK SERVICE gets Kerberos tickets for network resources but has minimal local rights. For most admin scripts, create a dedicated service account with only the AD, Exchange, or Graph permissions required. Use "Run whether user is logged on or not" and store credentials in the task definition (encrypted by DPAPI to the machine key).

Group Managed Service Accounts (gMSA)

gMSAs remove the need to manage passwords for service accounts — AD rotates the password automatically and makes it available only to authorised servers. Configure with New-ADServiceAccount and Install-ADServiceAccount on each machine that will run the task. Grant the gMSA the specific AD permissions it needs rather than adding it to Domain Admins.

Azure Automation and Managed Identity

Azure Automation runbooks run PowerShell in Azure on a schedule without requiring an on-premises server. Assign a system-assigned managed identity to the Automation Account and grant it the Graph API application permissions needed — no client secret or certificate to manage. Hybrid Runbook Workers extend runbooks to on-premises machines for AD or WMI operations.

Execution environment: PS5.1 vs PS7

Windows PowerShell 5.1 (powershell.exe) ships with Windows and is the default for Task Scheduler actions. PowerShell 7 (pwsh.exe) must be installed separately and supports parallel execution (ForEach-Object -Parallel), ternary operators, and newer SDK features. The Microsoft.Graph module fully supports both. Specify the full path to pwsh.exe in Task Scheduler if you need PS7 features.

Module availability in the runner context

Modules installed in your user profile (Install-Module -Scope CurrentUser) are not available when a script runs as SYSTEM or a service account. Install modules machine-wide (Install-Module -Scope AllUsers) on any machine that will run the script, or include a #Requires -Modules block and handle installation in a setup script. Verify module versions match between dev and production.

Network path and firewall dependencies

Scripts that write to a UNC share, query a SQL database, or call a REST API need the firewall, DNS, and network path to be available at the time the task runs. Document these dependencies explicitly. Add a Test-NetConnection check at script start and fail gracefully with a logged error if connectivity is absent — rather than hanging until a timeout.

Common problems

Where PowerShell Scripts Fail in Production

Most PowerShell failures in scheduled or automated contexts are environment and account problems, not logic errors. These are the patterns that appear most often.

Module not installed in the runner context

The script works at the console but fails as a scheduled task. The Microsoft.Graph or ActiveDirectory module was installed for the current user, not AllUsers. Install-Module -Scope AllUsers on the runner machine, or add a setup step that installs missing modules before the script body runs.

Execution policy blocks the script

The task runs but the script is blocked by RemoteSigned policy because the file was downloaded from a network share and has the Zone.Identifier alternate data stream marking it as internet-origin. Unblock-File -Path .\script.ps1 removes the mark, or set the task to run with -ExecutionPolicy Bypass in the pwsh.exe arguments (not as a permanent policy change).

Graph API token expiry mid-script

Connect-MgGraph tokens expire after one hour. A script that runs longer than 60 minutes against large tenants will start receiving 401 errors mid-run. Break long-running scripts into smaller batches with a reconnect between batches, or use an app registration with a client certificate and handle token refresh explicitly.

Silent failure — no exit code, no log

The task shows "Last Run Result: 0x0" (success) but nothing happened. The script caught an error, logged nothing, and exited 0. Add $ErrorActionPreference = "Stop" and a Catch block that calls Exit 1 on failure. Task Scheduler and Azure Automation both interpret non-zero exit codes as task failure and can trigger email alerts.

Credential double-hop over WinRM

Invoke-Command -ComputerName ServerA -ScriptBlock { Get-ChildItem \\ServerB\Share } fails because Kerberos credentials cannot be delegated a second time over WinRM by default. Solutions: enable CredSSP (use cautiously — it delegates full credentials), use Kerberos constrained delegation, or run the script directly on ServerA rather than tunnelling through remoting.

Script runs as SYSTEM — no AD access

SYSTEM has no domain identity and cannot authenticate to AD, Exchange, or Graph API. Change the task to run as a service account or gMSA with the specific permissions the script needs, not Domain Admins.

Rate limiting and 429 errors from Graph API

A script looping through thousands of devices hits Graph throttling limits. The script does not handle the Retry-After header and retries immediately, making the throttling worse. Add a [int]$retryAfter = $_.Exception.Response.Headers["Retry-After"]; Start-Sleep -Seconds $retryAfter pattern in your catch block for 429 responses.

Working directory assumption

The script uses relative paths (Import-Csv .\data.csv) that work in the console but fail in a scheduled task where the working directory is System32. Always use $PSScriptRoot to build absolute paths: Join-Path $PSScriptRoot "data.csv". This works whether the script runs interactively, via a task, or from a different working directory.