Skip to content

Useful PowerShell scripts for Windows admin tasks

Andrew Pla
Andrew Pla|May 26, 2026
Powershell1 2026
Powershell1 2026

TL;DR: These PowerShell scripts help Windows admins quickly check endpoint health, security settings, and inventory details without digging through a dozen consoles. Use them to troubleshoot event logs, audit scheduled tasks, validate BitLocker and Defender status, review certificates, inspect extensions, and spot configuration drift before it becomes ticket confetti.

PowerShell scripts help sysadmins collect Windows health, security, and configuration data without clicking through endless consoles. The scripts below cover common endpoint checks like event logs, scheduled tasks, certificates, BitLocker, battery health, mapped drives, browser extensions, Defender status, and Secure Boot readiness.

These examples are built for practical troubleshooting and inventory workflows. Use them to gather signals quickly, then decide whether you need deeper remediation, reporting, or deployment automation.

Big thanks to the community members who shared some of these packages and made them available on GitHub.

PowerShell scripts for Windows endpoint checks

The following PowerShell scripts help Windows admins answer common endpoint management questions:

1. Troubleshoot with event logs when things go sideways

Event logs are usually the fastest place to start when Windows gets dramatic. This script pulls recent critical and error events from the System or Application log so you can review the most relevant failures without spelunking through Event Viewer.

[CmdletBinding()] param ( # Number of days worth of entries to collect [UInt32]$Days = 28, # Defaults to Four weeks # Limit the number of returned events [UInt32]$EventLimit = 30, # Defaults to 30 events [ValidateSet("System", "Application")] [String]$EventLog = "System", # Defaults to "System" # The level of events to gather [ValidateRange(0, 5)] [UInt32[]]$EventLevel = 1 # Critical ) # Set the start date to be $Days before now $StartDate = (Get-Date).AddDays(-$Days) # Collect and output all relevant events Get-WinEvent -FilterHashtable @{LogName = $EventLog; Level = $EventLevel; StartTime = $StartDate } -ErrorAction SilentlyContinue | Select-Object -Last $EventLimit | ForEach-Object { [PSCustomObject]@{ Id = $_.Id Provider = $_.ProviderName Message = $_.Message TimeCreated = [DateTime]$_.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") Level = $_.Level } }

2. Audit scheduled tasks

Scheduled tasks can be helpful, noisy, or deeply suspicious. This script filters enabled tasks and flags common risk indicators like hidden tasks, encoded commands, execution policy bypasses, user writable paths, web downloads, and suspicious LOLBins.

[CmdletBinding()] param () function Resolve-ActionPath { param ( [string]$Path ) if ([string]::IsNullOrWhiteSpace($Path)) { return $null } $Path = [Environment]::ExpandEnvironmentVariables($Path.Trim('"')) if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } $params = @{ Name = $Path ErrorAction = 'SilentlyContinue' } $Command = Get-Command @params if ($Command) { return $Command.Source } return $Path } function Get-TaskReason { param ( [object]$Task, [object]$Action, [string]$ResolvedPath ) $Reasons = @() $ActionText = "$($Action.Execute) $($Action.Arguments)" $IsMicrosoftTask = ( $Task.TaskPath -like '\Microsoft\Windows\*' -and $Task.Author -match 'Microsoft' -and $ResolvedPath -match '^C:\\Windows\\(System32|SysWOW64)\\' ) if ($IsMicrosoftTask) { return $null } if ($Task.TaskPath -notlike '\Microsoft\Windows\*') { $Reasons += 'NonMicrosoftPath' } if ($ResolvedPath -match '(?i)\\Users\\|\\AppData\\|\\Temp\\|\\Downloads\\|\\Public\\|\\ProgramData\\') { $Reasons += 'UserWritablePath' } if ($Task.Settings.Hidden) { $Reasons += 'Hidden' } if ($ActionText -match '(?i)-enc|-encodedcommand') { $Reasons += 'EncodedCommand' } if ($ActionText -match '(?i)executionpolicy\s+bypass|-ep\s+bypass') { $Reasons += 'ExecutionBypass' } if ($ActionText -match '(?i)-w\s+hidden|-windowstyle\s+hidden') { $Reasons += 'HiddenWindow' } if ($ActionText -match '(?i)https?://|downloadstring|invoke-webrequest|invoke-restmethod|\biwr\b|\birm\b') { $Reasons += 'WebDownload' } if ($ActionText -match '(?i)mshta|regsvr32|rundll32|wscript|cscript|bitsadmin|certutil') { $Reasons += 'SuspiciousLolbin' } if ($Reasons.Count -eq 0) { return $null } return ($Reasons | Sort-Object -Unique) -join ', ' } $params = @{ ErrorAction = 'SilentlyContinue' } $Tasks = Get-ScheduledTask @params foreach ($Task in $Tasks) { if (-not $Task.Settings.Enabled) { continue } foreach ($Action in @($Task.Actions)) { $resolveParams = @{ Path = $Action.Execute } $ResolvedPath = Resolve-ActionPath @resolveParams $reasonParams = @{ Task = $Task Action = $Action ResolvedPath = $ResolvedPath } $Reason = Get-TaskReason @reasonParams if (-not $Reason) { continue } [pscustomobject]@{ Reason = $Reason TaskName = $Task.TaskName TaskPath = $Task.TaskPath RunAs = $Task.Principal.UserId ActionExe = $Action.Execute ActionArgs = $Action.Arguments ResolvedPath = $ResolvedPath } } }

3. Check certificate health

Certificate problems love showing up as vague application errors, expired trust chains, or authentication weirdness. This certificate health check script inventories certificates from the CurrentUser or LocalMachine certificate stores and returns the properties you care about most.

[CmdletBinding()] param () function Resolve-ActionPath { param ( [string]$Path ) if ([string]::IsNullOrWhiteSpace($Path)) { return $null } $Path = [Environment]::ExpandEnvironmentVariables($Path.Trim('"')) if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } $params = @{ Name = $Path ErrorAction = 'SilentlyContinue' } $Command = Get-Command @params if ($Command) { return $Command.Source } return $Path } function Get-TaskReason { param ( [object]$Task, [object]$Action, [string]$ResolvedPath ) $Reasons = @() $ActionText = "$($Action.Execute) $($Action.Arguments)" $IsMicrosoftTask = ( $Task.TaskPath -like '\Microsoft\Windows\*' -and $Task.Author -match 'Microsoft' -and $ResolvedPath -match '^C:\\Windows\\(System32|SysWOW64)\\' ) if ($IsMicrosoftTask) { return $null } if ($Task.TaskPath -notlike '\Microsoft\Windows\*') { $Reasons += 'NonMicrosoftPath' } if ($ResolvedPath -match '(?i)\\Users\\|\\AppData\\|\\Temp\\|\\Downloads\\|\\Public\\|\\ProgramData\\') { $Reasons += 'UserWritablePath' } if ($Task.Settings.Hidden) { $Reasons += 'Hidden' } if ($ActionText -match '(?i)-enc|-encodedcommand') { $Reasons += 'EncodedCommand' } if ($ActionText -match '(?i)executionpolicy\s+bypass|-ep\s+bypass') { $Reasons += 'ExecutionBypass' } if ($ActionText -match '(?i)-w\s+hidden|-windowstyle\s+hidden') { $Reasons += 'HiddenWindow' } if ($ActionText -match '(?i)https?://|downloadstring|invoke-webrequest|invoke-restmethod|\biwr\b|\birm\b') { $Reasons += 'WebDownload' } if ($ActionText -match '(?i)mshta|regsvr32|rundll32|wscript|cscript|bitsadmin|certutil') { $Reasons += 'SuspiciousLolbin' } if ($Reasons.Count -eq 0) { return $null } return ($Reasons | Sort-Object -Unique) -join ', ' } $params = @{ ErrorAction = 'SilentlyContinue' } $Tasks = Get-ScheduledTask @params foreach ($Task in $Tasks) { if (-not $Task.Settings.Enabled) { continue } foreach ($Action in @($Task.Actions)) { $resolveParams = @{ Path = $Action.Execute } $ResolvedPath = Resolve-ActionPath @resolveParams $reasonParams = @{ Task = $Task Action = $Action ResolvedPath = $ResolvedPath } $Reason = Get-TaskReason @reasonParams if (-not $Reason) { continue } [pscustomobject]@{ Reason = $Reason TaskName = $Task.TaskName TaskPath = $Task.TaskPath RunAs = $Task.Principal.UserId ActionExe = $Action.Execute ActionArgs = $Action.Arguments ResolvedPath = $ResolvedPath } } }

4. Validate BitLocker status

BitLocker status is one of those checks you want to confirm before you need it, not after a device walks out the door. This command returns BitLocker volume details so you can verify protection state, encryption status, and volume information.

Get-BitLockerVolume

5. Monitor battery health

Laptop battery health can explain sudden shutdowns, user complaints, and the classic “it was working five minutes ago” ticket. This script compares designed capacity with full charge capacity and returns a simple health percentage.

$Batteries = (Get-WmiObject -Class "BatteryStatus" -Namespace "ROOT\WMI" -ErrorAction SilentlyContinue) if (-not $Batteries) { [PSCustomObject]@{ Name = $null DesignedCapacity = $null FullCharge = $null Health = $null HasBattery = $false } return } $AllBatteryData = Get-WmiObject -Class "BatteryStaticData" -Namespace "ROOT\WMI" $AllCharges = (Get-WmiObject -Class "BatteryFullChargedCapacity" -Namespace "ROOT\WMI").FullChargedCapacity $BatteryIndex = 0 Foreach ($Battery in $Batteries) { $BatteryName = $AllBatteryData.DeviceName.split(" ")[$BatteryIndex] $DesignedCapacity = $AllBatteryData.DesignedCapacity[$BatteryIndex] $FullCharge = $AllCharges[$BatteryIndex] # https://github.com/pdq/PowerShell-Scanners/issues/49 Try { $Health = [Math]::Round($FullCharge / $DesignedCapacity * 100, 2) } Catch { Write-Warning "DesignedCapacity is 0 or null" $Health = $null } [PSCustomObject]@{ Name = $BatteryName DesignedCapacity = $DesignedCapacity FullCharge = $FullCharge Health = $Health HasBattery = $true } $BatteryIndex++ }

6. Collect mapped drives

Mapped drives are easy to forget until a user loses access to the one share they use every day. This script checks user registry hives for mapped network drives and returns the username, drive letter, and remote path.

# Credit to Fortress for this $results = [System.Collections.Generic.List[PSCustomObject]]::new() function Get-UserMappedDrives { param([string]$RegistryPath, [string]$Username) $networkPath = "Registry::$RegistryPath\Network" if (-not (Test-Path $networkPath)) { return } Get-ChildItem $networkPath -ErrorAction SilentlyContinue | ForEach-Object { $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue [PSCustomObject]@{ Username = $Username DriveLetter = "$($_.PSChildName):" RemotePath = $props.RemotePath } } } $skipSids = @('S-1-5-18', 'S-1-5-19', 'S-1-5-20') Get-CimInstance Win32_UserProfile -ErrorAction SilentlyContinue | Where-Object { -not $_.Special -and $_.SID -notin $skipSids } | ForEach-Object { $sid = $_.SID $profilePath = $_.LocalPath $username = try { (New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount]).Value } catch { $sid } if (Test-Path "Registry::HKU\$sid") { # Hive already loaded — user is logged in Get-UserMappedDrives -RegistryPath "HKU\$sid" -Username $username | ForEach-Object { $results.Add($_) } } else { $ntUserDat = Join-Path $profilePath 'NTUSER.DAT' $tempKey = "HKLM\TempDrives_$($sid -replace '-', '_')" if (Test-Path $ntUserDat) { $null = & reg.exe load $tempKey $ntUserDat 2>&1 try { Get-UserMappedDrives -RegistryPath $tempKey -Username $username | ForEach-Object { $results.Add($_) } } finally { [GC]::Collect() [GC]::WaitForPendingFinalizers() $null = & reg.exe unload $tempKey 2>&1 } } } } if ($results.Count -eq 0) { [PSCustomObject]@{ Username = '(none)' DriveLetter = $null RemotePath = $null } } else { $results | Select-Object -First 100 }

7. Inventory web browsers and VS Code extensions

Browser and editor extensions can affect performance, security, and user experience. These scripts help inventory Chromium-based browser extensions and VS Code extensions so you can spot unexpected installs or standardize what belongs on managed machines.

Chromium

This script inventories extensions across supported Chromium-based browsers, including Brave, Chromium, Google Chrome, Microsoft Edge, Opera, and Vivaldi.

[CmdletBinding()] param ( [String[]]$Browsers, [Switch]$EnablePermissions, [Switch]$OnlyCurrentUser ) function ConvertFrom-InstallTime { [CmdletBinding()] param ( $RawInstallTime ) if ( $RawInstallTime ) { $TimeZone = [TimeZoneInfo]::Local $Epoch = Get-Date -Date '1970-01-01 00:00:00' # Convert install_time from Webkit format. $InstallTime = [Double]$RawInstallTime # Divide by 1,000,000 because we are going to add seconds on to the base date. $InstallTime = ($InstallTime - 11644473600000000) / 1000000 $UtcTime = $Epoch.AddSeconds($InstallTime) [TimeZoneInfo]::ConvertTimeFromUtc($UtcTime, $TimeZone) } } $Template = @{ 'AppData' = 'Local' 'LastVersion' = 'last_chrome_version' # 'Default*' is intentionally a wildcard to prevent errors if it is missing. # https://github.com/pdq/PowerShell-Scanners/pull/54#discussion_r626112183 'ProfileNames' = 'Default*', 'Profile*' 'Settings' = 'settings' } $BrowserTable = @{ 'Brave' = $Template.Clone() 'Chromium' = $Template.Clone() 'Google Chrome' = $Template.Clone() 'Microsoft Edge' = $Template.Clone() # Opera, why do you have to be so different? :'( 'Opera' = @{ 'AppData' = 'Roaming' 'LastVersion' = 'last_opera_version' 'ProfileBase' = 'Opera Software' 'ProfileNames' = 'Opera*' 'Settings' = 'opsettings' } 'Vivaldi' = $Template.Clone() } $BrowserTable.Brave.ProfileBase = 'BraveSoftware\Brave-Browser\User Data' $BrowserTable.Chromium.ProfileBase = 'Chromium\User Data' $BrowserTable.'Google Chrome'.ProfileBase = 'Google\Chrome\User Data' $BrowserTable.'Microsoft Edge'.ProfileBase = 'Microsoft\Edge\User Data' $BrowserTable.Vivaldi.ProfileBase = 'Vivaldi\User Data' # Set up or check the list of browsers to scan. if ( -not $Browsers ) { $Browsers = $BrowserTable.Keys } else { Foreach ( $BrowserName in $Browsers ) { if ( $BrowserName -notin $BrowserTable.Keys ) { throw "'$BrowserName' does not match any entries in the list of supported browsers." } } } # Set up the JSON parser for the Preferences files below. # This .NET method is necessary because ConvertFrom-Json can't handle duplicate entries with different cases. # https://github.com/pdq/PowerShell-Scanners/issues/23 Add-Type -AssemblyName System.Web.Extensions $JsonParser = New-Object -TypeName System.Web.Script.Serialization.JavaScriptSerializer if ( $OnlyCurrentUser ) { if ( (whoami) -eq 'NT AUTHORITY\SYSTEM' ) { Write-Warning 'The current user is SYSTEM. This usually means no user is logged on to this computer.' Exit } $UserPaths = Get-Item '~' } else { $UserPaths = Get-ChildItem -Directory -Path "$env:SystemDrive\Users" } Foreach ( $User in $UserPaths ) { Foreach ( $BrowserName in $Browsers ) { $Browser = $BrowserTable.$BrowserName # Get profiles. $ProfileBase = "$($User.FullName)\AppData\$($Browser.AppData)\$($Browser.ProfileBase)" if ( Test-Path $ProfileBase ) { Set-Location -Path $ProfileBase } else { # Browser is not installed, or the user has never opened it. Continue } $Profiles = Get-Item -Path $Browser.ProfileNames Foreach ( $Profile in $Profiles ) { $BrowserSettings = $null $SecurePreferencesFile = "$($Profile.FullName)\Secure Preferences" $SecurePreferencesJson = $null if ( Test-Path $SecurePreferencesFile ) { $SecurePreferencesText = Get-Content -Raw $SecurePreferencesFile -Encoding utf8 $SecurePreferencesJson = $JsonParser.DeserializeObject($SecurePreferencesText) # See if this file contains extension data. if ( $SecurePreferencesJson.extensions."$($Browser.Settings)" ) { $BrowserSettings = $SecurePreferencesJson.extensions."$($Browser.Settings)".GetEnumerator() } else { Write-Verbose "Unable to find the extensions.$($Browser.Settings) node in: $SecurePreferencesFile" } } else { Write-Verbose "Unable to find a 'Secure Preferences' file in: $($Profile.FullName)" } $PreferencesFile = "$($Profile.FullName)\Preferences" $PreferencesJson = $null if ( Test-Path $PreferencesFile ) { # The only thing we care about in Preferences is the last browser version. $PreferencesText = Get-Content -Raw $PreferencesFile -Encoding utf8 $PreferencesJson = $JsonParser.DeserializeObject($PreferencesText) # Check for extension data if it wasn't in SecurePreferences. if ( -not $BrowserSettings ) { if ( $PreferencesJson.extensions."$($Browser.Settings)" ) { Write-Verbose "Falling back to Preferences file for: $($Profile.FullName)" $BrowserSettings = $PreferencesJson.extensions."$($Browser.Settings)".GetEnumerator() } else { Write-Verbose "Unable to find the extensions.$($Browser.Settings) node in: $PreferencesFile" Write-Verbose "No extension data found for: $($Profile.FullName), moving to the next profile" Continue } } } else { Write-Verbose "Unable to find a 'Preferences' file in: $($Profile.FullName)" } if ( -not $BrowserSettings ) { Write-Verbose "No Preferences files found in: $($Profile.FullName), moving to the next profile" Continue } Foreach ( $Extension in $BrowserSettings ) { $ID = $Extension.Key $Extension = $Extension.Value $Name = $Extension.manifest.name # Ignore blank names. if ( -Not $Name ) { Write-Verbose "Blank name for ID '$ID' in: $SecurePreferencesFile" Continue } # install_time got replaced by first_install_time at some point. # https://github.com/pdqcom/PowerShell-Scanners/issues/119 if ( $Extension.first_install_time ) { $InstallDate = ConvertFrom-InstallTime $Extension.first_install_time } else { $InstallDate = ConvertFrom-InstallTime $Extension.install_time } # If disable_reasons is empty, it means extension is enabled. # https://chromium.googlesource.com/chromium/src/+/refs/heads/main/extensions/browser/disable_reason.h if ( $Extension.disable_reasons ) { $Status = $false } else { $Status = $true } $Output = [Ordered]@{ 'Browser' = $BrowserName 'Name' = $Name 'Enabled' = [Bool] $Status 'Description' = [String] $Extension.manifest.description 'Extension Version' = [String] $Extension.manifest.version 'Browser Version' = [String] $PreferencesJson.extensions."$($Browser.LastVersion)" 'Default Install' = [Bool] $Extension.was_installed_by_default 'OEM Install' = [Bool] $Extension.was_installed_by_oem 'ID' = $ID 'Install Date' = $InstallDate 'Last Update' = ConvertFrom-InstallTime $Extension.last_update_time 'User' = [String] $User.Name 'Profile' = [String] $Profile.Name } if ( $EnablePermissions ) { # Convert Permissions array into a multi-line string. # This multi-line string is kind of ugly in Inventory, so it's disabled by default. $Output.Permissions = [String]($Extension.manifest.permissions -Join "`n") } [PSCustomObject]$Output } } } }

VS Code

This script inventories VS Code extensions from user profiles and returns the user, publisher, extension name, and version.

Get-ChildItem "C:\Users\*\.vscode\extensions\*" -Directory | ForEach-Object { if ($_.Name -match '^(?<publisher>[^.]+)\.(?<name>.+)-(?<version>[\d.]+)$') { [PSCustomObject]@{ User = $_.FullName.Split('\')[2] Publisher = $matches['publisher'] Name = $matches['name'] Version = $matches['version'] } } }

8. Validate configurations (registry, config file, etc.)

Configuration drift is how “temporary exceptions” become permanent problems. This is an example of some things you can look for. Everyone's environment is different, so you can use this script as a starting point.

[CmdletBinding()] param () $AllowedLocalAdmins = @( 'BUILTIN\Administrators' 'NT AUTHORITY\SYSTEM' 'WEB\Domain Admins' ) function New-CheckResult { param ( [Parameter(Mandatory)] [string]$CheckName, [Parameter(Mandatory)] [bool]$Passed, [Parameter(Mandatory)] [string]$Expected, [Parameter(Mandatory)] [string]$Actual, [string]$Severity = 'Info', [string]$Details = 'N/A', [string]$ErrorMessage = 'N/A' ) [pscustomobject]@{ ComputerName = $env:COMPUTERNAME CheckName = $CheckName Passed = $Passed Severity = $Severity Expected = $Expected Actual = $Actual Details = $Details ErrorMessage = $ErrorMessage } } function Invoke-ConfigCheck { param ( [Parameter(Mandatory)] [string]$CheckName, [Parameter(Mandatory)] [string]$Expected, [Parameter(Mandatory)] [string]$Severity, [Parameter(Mandatory)] [scriptblock]$ScriptBlock ) try { $result = & $ScriptBlock $params = @{ CheckName = $CheckName Passed = [bool]$result.Passed Severity = $Severity Expected = $Expected Actual = [string]$result.Actual Details = [string]$result.Details } New-CheckResult @params } catch { $params = @{ CheckName = $CheckName Passed = $false Severity = $Severity Expected = $Expected Actual = 'Check failed' Details = 'N/A' ErrorMessage = $_.Exception.Message } New-CheckResult @params } } $Checks = @( @{ CheckName = 'No unexpected local administrators' Severity = 'High' Expected = 'Only approved accounts/groups in local Administrators' ScriptBlock = { $admins = @(Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop) $adminNames = @( $admins | Select-Object -ExpandProperty Name ) $unexpectedAdmins = @( $adminNames | Where-Object { $_ -notin $AllowedLocalAdmins } ) [pscustomobject]@{ Passed = $unexpectedAdmins.Count -eq 0 Actual = if ($unexpectedAdmins.Count -eq 0) { 'Compliant' } else { $unexpectedAdmins -join ', ' } Details = "AdminCount=$($adminNames.Count)" } } } @{ CheckName = 'BitLocker protected on system drive' Severity = 'High' Expected = 'ProtectionStatus=On' ScriptBlock = { $volume = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction Stop [pscustomobject]@{ Passed = $volume.ProtectionStatus -eq 'On' Actual = "ProtectionStatus=$($volume.ProtectionStatus)" Details = "VolumeStatus=$($volume.VolumeStatus); EncryptionMethod=$($volume.EncryptionMethod); EncryptionPct=$($volume.EncryptionPercentage)" } } } @{ CheckName = 'Windows Firewall enabled' Severity = 'Medium' Expected = 'Domain, Private, and Public profiles enabled' ScriptBlock = { $profiles = @(Get-NetFirewallProfile -ErrorAction Stop) $disabledProfiles = @($profiles | Where-Object { -not $_.Enabled }) [pscustomobject]@{ Passed = $disabledProfiles.Count -eq 0 Actual = if ($disabledProfiles.Count -eq 0) { 'All profiles enabled' } else { "$($disabledProfiles.Name -join ', ') disabled" } Details = ($profiles | ForEach-Object { "$($_.Name)=$($_.Enabled)" }) -join '; ' } } } ) foreach ($check in $Checks) { $params = @{ CheckName = $check.CheckName Expected = $check.Expected Severity = $check.Severity ScriptBlock = $check.ScriptBlock } Invoke-ConfigCheck @params }

9. Check Windows Defender

Windows Defender checks are useful for quick endpoint hygiene reviews. This script verifies that Defender cmdlets are available, checks the WinDefend service, and reports practical health indicators like real-time protection, signature age, and quick scan age.

[CmdletBinding()] param () # Get-MpComputerStatus is available when Microsoft Defender management cmdlets exist. # On older operating systems, or systems without Defender components, this may not be available. if (-not (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue)) { throw "Unable to find Get-MpComputerStatus. Available on Windows 10/Server 2016 or higher." } # WinDefend is the Microsoft Defender Antivirus service. # If this service is missing, Defender is likely not installed or not available on this system. $DefenderService = Get-Service -Name WinDefend -ErrorAction SilentlyContinue if (-not $DefenderService) { throw "The Windows Defender service was not found." } # Get-MpComputerStatus returns many properties. # We intentionally collect the full object, then output only the few fields that are useful at a glance. $Status = Get-MpComputerStatus -ErrorAction Stop $Issues = @() # Service should normally be Running if Defender is active. if ($DefenderService.Status -ne 'Running') { $Issues += 'ServiceNotRunning' } # AntivirusEnabled confirms Defender AV is enabled. if (-not $Status.AntivirusEnabled) { $Issues += 'AntivirusDisabled' } # RealTimeProtectionEnabled is one of the most important practical Defender checks. if (-not $Status.RealTimeProtectionEnabled) { $Issues += 'RealTimeProtectionDisabled' } # Signature age is a simple way to spot stale protection. # Adjust the threshold if your environment has a different patch/update cadence. if ($Status.AntivirusSignatureAge -gt 7) { $Issues += 'SignaturesOlderThan7Days' } # A quick scan being old does not always mean the device is unhealthy, # but it is useful as a lightweight hygiene signal. if ($Status.QuickScanAge -gt 14) { $Issues += 'QuickScanOlderThan14Days' } [pscustomobject]@{ ComputerName = $env:COMPUTERNAME # Overall rollup for filtering/reporting. Healthy = $Issues.Count -eq 0 Issues = if ($Issues) { $Issues -join ', ' } else { 'None' } # Core Defender state. ServiceStatus = $DefenderService.Status.ToString() AntivirusEnabled = [bool]$Status.AntivirusEnabled RealTimeProtectionEnabled = [bool]$Status.RealTimeProtectionEnabled # Freshness and scan hygiene. SignatureAgeDays = $Status.AntivirusSignatureAge QuickScanAgeDays = $Status.QuickScanAge }

10. Verify Secure Boot status

Secure Boot checks help confirm whether devices are ready for updated UEFI certificate requirements. This script checks for Windows UEFI CA 2023 and KEK 2K CA 2023 indicators, reads the servicing registry value, and includes the last boot time for context. If things go well, you should true true and updated

try { $params = @{ Name = 'db' ErrorAction = 'Stop' } $dbBytes = (Get-SecureBootUEFI @params).Bytes $DB = [System.Text.Encoding]::ASCII.GetString($dbBytes) -match 'Windows UEFI CA 2023' } catch { $DB = $false } try { $params = @{ Name = 'KEK' ErrorAction = 'Stop' } $kekBytes = (Get-SecureBootUEFI @params).Bytes $KEK = [System.Text.Encoding]::ASCII.GetString($kekBytes) -match 'KEK 2K CA 2023' } catch { $KEK = $false } try { $params = @{ Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing' Name = 'UEFICA2023Status' ErrorAction = 'Stop' } $UEFICA2023Status = Get-ItemPropertyValue @params } catch { $UEFICA2023Status = $null } try { $params = @{ ClassName = 'Win32_OperatingSystem' ErrorAction = 'Stop' } $LastBoot = (Get-CimInstance @params).LastBootUpTime } catch { $LastBoot = $null } [PSCustomObject]@{ DB = $DB KEK = $KEK UEFICA2023Status = $UEFICA2023Status LastBoot = $LastBoot }

How to use these PowerShell scripts safely

Test these scripts in a lab or test environment before using them broadly. Make sure they run cleanly, return the data you expect, and work across the Windows versions and permission levels you manage.

Before adding any script to a production workflow, read through the commands so you understand what they check, where the data comes from, and how the results can fit into your existing process. That flexibility is the real strength of PowerShell: You can adjust the filters, properties, and output to match your environment instead of forcing your environment to match the script.


PowerShell is still one of the fastest ways for sysadmins to answer practical Windows endpoint questions at scale. These scripts give you a starting point for troubleshooting, inventory, and security hygiene without making you click through eleventy-seven management screens like it’s a punishment.

Use the outputs as signals, not final verdicts. The real value comes from combining these checks with your environment’s baselines, approved configurations, and remediation workflows.

And to execute these scripts at scale, sign up for a free PDQ trial and take advantage of the PowerShell Scanner.

Related articles