Detect Device Compliance Problems Locally with PowerShell · CloudEngineerHub
Detection & Remediation9 min read4 views
Detecting Device Compliance Problems Locally with PowerShell
A detection-only PowerShell audit that mirrors the local signals Intune compliance policies evaluate, and explains per device exactly which check failed and why.
C
CloudEngineerHub Team
Published: September 6, 2026· Last Updated: September 28, 2026·9 min read
Compliance failures have a nasty habit of surfacing at the worst possible moment: the user gets locked out by a conditional access rule mid-project, or the security team pulls a report showing 4,000 non-compliant devices nobody was watching. The Intune admin center tells you that a device is non-compliant, but not which local signal broke, and not before it breaks. A small, read-only PowerShell audit that mirrors the signals your compliance policies evaluate gives you both an early warning and a per-device explanation, deployable anywhere Intune scripts run.
Visual Walkthrough
The engine behind every “Detect with PowerShell” runbook on this site.
Exit 1 flips the device into the remediation queue for this package.
remediate
3
Remediation executes
Fix script runs silently as SYSTEM with a 5-minute budget.
verify
4
Re-run detection
The same detection script must now exit 0 — proof, not hope.
telemetry
5
Results report
Per-device outcome lands in Reports → Remediations for trend analysis.
6
Repeat on schedule
The loop continues — drift gets caught the day it appears.
Continuous cycle
Proactive Remediations — The Detect → Fix → Verify Loop. 1. Detection script runs: Scheduled (daily/hourly): exit 0 healthy, exit 1 unhealthy — nothing more. — 2. Verdict: unhealthy: Exit 1 flips the device into the remediation queue for this package. — 3. Remediation executes: Fix script runs silently as SYSTEM with a 5-minute budget. — 4. Re-run detection: The same detection script must now exit 0 — proof, not hope. — 5. Results report: Per-device outcome lands in Reports → Remediations for trend analysis. — 6. Repeat on schedule: The loop continues — drift gets caught the day it appears.
C
CloudEngineerHub Team
Author
The CloudEngineerHub editorial team — Microsoft endpoint management and cloud engineers writing hands-on guides on Intune, Azure, Entra ID, Autopilot, Defender and PowerShell. Every article is reviewed against current Microsoft documentation.
This article provides a detection-only script that checks five things locally: BitLocker status on the OS volume, Defender real-time protection health, firewall profile enablement, the OS build against a configurable minimum, and Secure Boot state. It writes one structured log line per check, exits with a single code that rolls up the findings, and never modifies the device. You can point it at a pilot ring while you design your real compliance policies, or run it fleet-wide as a shadow audit that explains, per machine, why the portal says what it says.
One boundary up front: this script does not enforce anything. Intune compliance policies remain the enforcement mechanism that drives conditional access and device mark-up. What this gives you is the same class of local evidence, on your schedule, with explanations.
Symptoms
This audit is most valuable before the complaints arrive, but these are the situations that usually prompt someone to build it:
Conditional access starts blocking users on devices nobody flagged as out of compliance.
The admin center shows non-compliance with a setting reference, but the help desk cannot translate that into what the user should do.
Security asks "how many devices have real-time protection off?" and the only answer is a manual audit.
A migration from another management tool leaves you unsure which devices would pass a Windows compliance baseline.
Virtual machines, developer workstations, and home-office devices fail compliance for reasons that differ from the standard fleet, and you need a quick way to see why.
Possible Causes
Local compliance signals drift for predictable reasons:
Encryption was never completed or was suspended
BitLocker can be off because the device was never encrypted, because an admin suspended protection for a firmware update and never resumed it, or because the OS volume was re-created during a repair. All three look identical from the portal: encryption not enabled.
Defender health is real but invisible
Third-party antivirus in passive mode, disabled engines on devices that "work fine", and signature feeds stalled behind proxy problems all produce a Defender status that a compliance policy would fail, with no user-visible symptom until malware shows up.
Firewall profiles were turned off by hardening scripts
Legacy hardening packages, VPN clients, and copy-pasted "performance" scripts sometimes disable one firewall profile (Public is the usual victim). A compliance policy that requires all profiles enabled then fails the device.
Builds fall behind the floor
Devices that miss feature update waves, or that run long-frozen builds, drift below the minimum OS build your security baseline demands. The compliance failure is real; the per-device explanation is what's missing.
Secure Boot is off or unverifiable
Firmware settings flipped during repairs, older devices on legacy BIOS, and some virtualization platforms cannot answer a Secure Boot query at all. Each case deserves a different response, which is why the script distinguishes "disabled" from "unverifiable" in its log.
Detection Script
The script below is strictly read-only and runs in a few seconds. It is designed for 64-bit SYSTEM context through Intune, and each check is individually guarded so that one broken source (for example, a BitLocker cmdlet failing on an odd build) becomes a reported finding instead of a crashed run.
PowerShell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[ValidatePattern('^[A-Za-z]:$')]
[string] $OsDrive = 'C:',
[ValidatePattern('^\d+\.\d+\.\d+$')]
[string] $MinimumOsBuild = '10.0.19045',
[ValidateRange(1, 90)]
[int] $SignatureMaxAgeDays = 7
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
<#
.SYNOPSIS
Detection script: read-only local audit of the signals Intune compliance
policies commonly evaluate.
.DESCRIPTION
Checks five things and reports each result:
1. OS volume BitLocker protection status (Get-BitLockerVolume).
2. Defender service, antivirus engine, real-time protection, and
signature age (Get-MpComputerStatus).
3. Firewall profile enablement for Domain, Private, and Public
(Get-NetFirewallProfile).
4. OS build against the $MinimumOsBuild parameter.
5. Secure Boot state (Confirm-SecureBootUEFI, caught for non-UEFI or
virtualized platforms where the cmdlet cannot run).
This is a visibility mirror, not an enforcement mechanism: the script
NEVER changes the system. Exit 0 = all checks pass, 1 = one or more
compliance signals failed or could not be verified, 3 = unexpected error.
#>$logDir = 'C:\ProgramData\Company\Logs'$logFile = Join-Path $logDir'DeviceComplianceDetection.log'function Write-DetectionLog {
param([Parameter(Mandatory)][string] $Level, [Parameter(Mandatory)][string] $Message)
if (-not (Test-Path $logDir)) { New-Item -Path$logDir -ItemType Directory -Force | Out-Null }
"$([DateTime]::UtcNow.ToString('o')) [$Level] $Message" | Out-File -FilePath$logFile -Append -Encoding utf8
}
try {
Write-DetectionLog 'INFO''Device compliance signal audit started.'$issues = @()
# ---- Check 1: BitLocker on the OS volume ----try {
$osVolume = Get-BitLockerVolume -MountPoint$OsDrive -ErrorAction Stop
Write-DetectionLog 'INFO'"BitLocker $($OsDrive): ProtectionStatus=$($osVolume.ProtectionStatus) ConversionStatus=$($osVolume.ConversionStatus) VolumeType=$($osVolume.VolumeType)"if ($osVolume.ProtectionStatus -ne'On') {
Write-DetectionLog 'ERROR'"OS volume $($OsDrive) is not BitLocker protected (ProtectionStatus=$($osVolume.ProtectionStatus))."$issues += 'BitLocker protection off on OS volume'
}
}
catch {
Write-DetectionLog 'ERROR'"BitLocker state could not be read: $($_.Exception.Message)"$issues += 'BitLocker state unverifiable'
}
# ---- Check 2: Defender real-time protection health ----try {
$mp = Get-MpComputerStatus -ErrorAction Stop
Write-DetectionLog 'INFO'"Defender: AMService=$($mp.AMServiceEnabled) Antivirus=$($mp.AntivirusEnabled) RealTime=$($mp.RealTimeProtectionEnabled) SignatureAge(days)=$([math]::Round(((Get-Date) - $mp.AntivirusSignatureLastUpdated).TotalDays, 2))"if ($mp.AMServiceEnabled -ne$true) { Write-DetectionLog 'ERROR''Defender antimalware service is not enabled.'; $issues += 'Defender AM service disabled' }
if ($mp.AntivirusEnabled -ne$true) { Write-DetectionLog 'ERROR''Defender antivirus engine is not enabled (expected when a third-party AV is licensed and documented).' ; $issues += 'Defender antivirus engine disabled' }
if ($mp.RealTimeProtectionEnabled -ne$true) { Write-DetectionLog 'ERROR''Defender real-time protection is disabled.'; $issues += 'Defender real-time protection disabled' }
if ($mp.AntivirusSignatureLastUpdated -lt (Get-Date).AddDays(-$SignatureMaxAgeDays)) {
Write-DetectionLog 'ERROR'"Defender signatures older than $SignatureMaxAgeDays days."$issues += 'Defender signatures stale'
}
}
catch {
Write-DetectionLog 'ERROR'"Defender status could not be read: $($_.Exception.Message)"$issues += 'Defender status unverifiable'
}
# ---- Check 3: firewall profiles enabled ----try {
$profiles = Get-NetFirewallProfile -ErrorAction Stop
foreach ($profilein$profiles) {
Write-DetectionLog 'INFO'"Firewall profile $($profile.Name): Enabled=$($profile.Enabled)"if ($profile.Enabled -ne$true) {
Write-DetectionLog 'ERROR'"Firewall profile $($profile.Name) is disabled."$issues += "Firewall profile $($profile.Name) disabled"
}
}
}
catch {
Write-DetectionLog 'ERROR'"Firewall profile state could not be read: $($_.Exception.Message)"$issues += 'Firewall state unverifiable'
}
# ---- Check 4: OS build against the minimum ----try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
$currentBuild = [version] $os.Version
$minimumBuild = [version] $MinimumOsBuild
Write-DetectionLog 'INFO'"OS build $($currentBuild) (caption: $($os.Caption)) vs minimum $($minimumBuild)."if ($currentBuild -lt$minimumBuild) {
Write-DetectionLog 'ERROR'"OS build $($currentBuild) is below the configured minimum $($minimumBuild)."$issues += "OS build below $MinimumOsBuild"
}
}
catch {
Write-DetectionLog 'ERROR'"OS build could not be read: $($_.Exception.Message)"$issues += 'OS build unverifiable'
}
# ---- Check 5: Secure Boot ----$secureBoot = $nulltry {
$secureBoot = Confirm-SecureBootUEFI -ErrorAction Stop
}
catch {
Write-DetectionLog 'WARN'"Secure Boot could not be confirmed (non-UEFI firmware or unsupported platform): $($_.Exception.Message)"
}
if ($secureBoot -eq$true) {
Write-DetectionLog 'INFO''Secure Boot is enabled.'
}
elseif ($secureBoot -eq$false) {
Write-DetectionLog 'ERROR''Secure Boot is disabled in firmware.'$issues += 'Secure Boot disabled'
}
else {
Write-DetectionLog 'ERROR''Secure Boot state unverifiable on this platform; treat as a compliance signal gap.'$issues += 'Secure Boot unverifiable'
}
if ($issues.Count -eq0) {
Write-DetectionLog 'INFO''All compliance signals healthy.'
exit 0
}
Write-DetectionLog 'ERROR'"Compliance issues detected ($($issues.Count)): $($issues -join '; ')"
exit 1
}
catch {
Write-DetectionLog 'ERROR'"Unexpected failure: $($_.Exception.Message)"
exit 3
}
finally {
Write-DetectionLog 'INFO''Device compliance signal audit finished.'
}
Detection Logic
Five checks, evaluated in the order they typically matter to a Windows compliance policy:
BitLocker on the OS volume.Get-BitLockerVolume -MountPoint $OsDrive returns the volume's ProtectionStatus and ConversionStatus. Healthy is ProtectionStatus = On on the operating system volume. Anything else, including Off after a suspended update cycle, is a finding.
Defender health.Get-MpComputerStatus provides AMServiceEnabled, AntivirusEnabled, RealTimeProtectionEnabled, and AntivirusSignatureLastUpdated. The script fails the device if the antimalware service, the AV engine, or real-time protection is off, or if signatures are older than the $SignatureMaxAgeDays parameter (default 7). If your estate licenses third-party AV, tune this check and say so in prose with your security team.
Firewall profiles.Get-NetFirewallProfile returns the Domain, Private, and Public profiles. Each must report Enabled = $true. The script logs and counts each disabled profile separately, because "Public off" and "all off" are different incidents.
OS build floor.Win32_OperatingSystem.Version is cast to a [version] and compared against the $MinimumOsBuild parameter (default 10.0.19045, the Windows 10 22H2 build). Adjust the parameter to your actual floor; Windows 11 devices report builds in the same 10.0.xxxxx form, so the comparison holds across both.
Secure Boot.Confirm-SecureBootUEFI returns $true or $false. On non-UEFI firmware and some virtual platforms the cmdlet throws instead, which the script catches and records as "unverifiable". A hard $false is a finding; an unverifiable result is also counted, because a compliance policy will eventually evaluate it, but the log line tells you the difference.
Every check writes its own log line to C:\ProgramData\Company\Logs\DeviceComplianceDetection.log, so a single file per device explains every finding without a portal round trip.
Deploying Through Intune
This is a detection-only package; there is no remediation script.
In the Microsoft Intune admin center, open Devices → Scripts and remediations and choose + Add under the Windows 10/11 platform tab.
On Basics, name it Audit - local compliance signals, with a description noting it changes nothing.
On Detection script, upload detect.ps1. Set Run script in 64-bit PowerShell: Yes and Run this script using signed-on credentials: No; every check is machine-scoped and needs no user token.
On the Remediation script step, select No remediation script. The report is the deliverable.
Leave Enforce script signature check: No unless you sign internal scripts and distribute the signing certificates to devices.
On Schedule, choose Daily while you are establishing a baseline, then Weekly. Compliance drift is slow, but signature staleness and firewall changes happen within days.
On Assignments, start with a pilot ring that includes one of each hardware generation and one VM. Broaden to your full Windows fleet after the first week, using filters to exclude platforms you intentionally exempt.
Review and create, then review Device status after the first scheduled run.
Post-specific notes:
This audit complements, never replaces, real compliance policies; enforcement lives there.
Exit code 1 findings with "unverifiable" in the text usually mean non-UEFI firmware or a platform the Secure Boot cmdlet cannot query, not a misconfiguration.
If your estate runs third-party antivirus, adjust the Defender check before the first fleet run, or you will drown in expected findings.
Visual Walkthrough
Grace periods decoded: when "non-compliant" actually cuts access.
Decision TreeCompliance → Conditional Access — The Handshake5/5
Decision gate
Device evaluates compliance
Compliance is a signal; Conditional Access is the consequence.
Compliant
1
All settings pass
Encryption, AV health, OS version, jailbreak checks green.
2
Device marked compliant
State published to Entra for CA to consume.
3
CA grants access
Mail/files open — the compliant experience is frictionless.
Non-compliant
1
Grace period runs
Actions: mark non-compliant after N days — a designed buffer.
Compliance → Conditional Access — The Handshake. Gate: Device evaluates compliance — Compliant: All settings pass — Compliant: Device marked compliant — Compliant: CA grants access — Non-compliant: Grace period runs — Non-compliant: User notified — Non-compliant: CA blocks after grace — Non-compliant: Remediation restores
Exit Codes
Intune evaluates the detection script's exit code; any non-zero marks the device as an issue in the package's device status view. Since no remediation script is attached, the device state simply waits for your attention.
Code
Meaning
0
All five compliance signals pass
1
One or more signals failed or could not be verified; the log names each finding
3
Unexpected script error; inspect the log
Logging
The script writes to C:\ProgramData\Company\Logs\DeviceComplianceDetection.log. Each line carries a UTC timestamp, a level (INFO, WARN, ERROR), and a message. INFO lines record the observed value of every check, which makes the file a point-in-time compliance snapshot per device even when nothing fails. The directory is created on first run, and the file grows by roughly ten lines per run; monthly rotation keeps it tidy if you keep the package on a daily schedule. If your tenant also has real compliance policies assigned, cross-reference the device's compliance status in the admin center with this log to translate policy failure codes into local, explainable values.
Troubleshooting the Script
Every device reports "BitLocker state unverifiable". Confirm the script runs in 64-bit PowerShell; the BitLocker module lives in the 64-bit Windows PowerShell module path, and the check needs the Storage/BitLocker cmdlets available in SYSTEM context.
"Defender status unverifiable" on a working device. The Defender platform can refuse queries when its service is in a broken state, which is itself a finding. Also confirm you did not run the script in a context where the Defender PowerShell provider is stripped.
Secure Boot findings on physical devices that are clearly UEFI. Some firmware exposes Secure Boot state only when the setup password is absent or when a specific boot mode is active. Validate on one device with msinfo32 (Secure Boot State line) before assuming a script problem.
No log file appears. The script never ran; verify assignment, device last check-in, and that the device is a Windows platform.
Findings disagree with the compliance portal. Compliance policies and this script evaluate the same underlying platform state but not always the same source; note the exact setting reference in the portal and compare against the matching log line before escalating.
How to Validate the Fix
Because this is detection-only, validation is about proving the audit reads reality correctly:
Run the script manually on a known-healthy device in an elevated 64-bit PowerShell console. Expect exit code 0 and five INFO lines with plausible values.
In a test VM, disable a firewall profile or turn off real-time protection, re-run the script, and confirm the corresponding ERROR line and exit code 1. Revert the change afterwards.
Assign the package to a pilot group and compare the device status view against the devices' manual results; they should match.
On one non-UEFI or virtualized test machine, confirm the Secure Boot line reads as unverifiable rather than crashing the run.
Review the log file on two or three devices and confirm the values match what you see in Windows Security, msinfo32, and manage-bde -status.
Production Considerations
Execution context. The script runs as SYSTEM in 64-bit Windows PowerShell. It reads machine-scoped state only and touches no user profile data.
Idempotency. It is detection-only by design: no registry writes, no service changes, no firmware queries that alter state. Re-running is always safe.
Side effects. None; the only I/O beyond reads is the log file itself.
Security. No credentials or secrets are present, and the script cannot disable any security feature. Have your PowerShell automation SME and security team review it, agree the Defender and build-floor parameters for your estate, and validate in a test ring before broad production rollout.
Parameter hygiene.$MinimumOsBuild and $SignatureMaxAgeDays encode your standards; change them through a documented change process, not ad hoc.
Why not just use an Intune compliance policy instead of a script?
Do both, for different jobs. Compliance policies are the enforcement mechanism that drives conditional access; this script is a read-only mirror that explains, per device and per check, why a device would fail, and it can run before you have finalized which policies to assign. It cannot mark a device non-compliant or block access.
What does "Secure Boot unverifiable" actually mean?
It means Confirm-SecureBootUEFI could not run or could not return a definite answer, which happens on legacy BIOS firmware and on some virtualization platforms. The script treats it as a finding so the gap stays visible, but the log distinguishes it from a hard "Secure Boot disabled" so you can respond appropriately.
Can I extend the script with more checks, like TPM or password policy?
Yes, the pattern is deliberately repeatable: one guarded try/catch block per check, one INFO log line per observation, and findings appended to a single list that decides the exit code. Keep each check read-only and log the observed value, and the audit stays trustworthy as it grows.
Comments
0No comments yet — be the first to share your thoughts.