EndpointComplianceScan.psm1
|
# EndpointComplianceScan 1.0.0, built by tools/Build-Module.ps1 from the scripts # beside this file. Each command runs its script; change the scripts, not this. function Invoke-EndpointComplianceScan { <# .SYNOPSIS Scans the local Windows endpoint for evidence against one or more compliance frameworks, chosen at run time: CMMC, NIST SP 800-171 Rev 3, NIST SP 800-53 Rev 5, the DISA STIGs for Windows 11, Windows Server 2019, 2022 and 2025, Windows Defender Firewall, Active Directory Domain and Forest, and Windows PAW, CIS Controls v8.1, NIST CSF 2.0, PCI DSS 4.0.1 or HIPAA. .DESCRIPTION Read-only. The scan changes no setting and writes nothing to the registry. Its only network traffic is writing reports to a share when -OutputPath names one; on a domain controller it reads Active Directory from itself (localhost) over LDAP. Its only side effects are temporary files (the secedit and auditpol exports), deleted as soon as they are read, the report files, and, when -FallbackPath and -LogPath are given, the reports waiting there and the run log. It works in two stages so that every pass/fail rule can be tested anywhere: 1. Collect: gather raw facts about the machine into a snapshot. 2. Evaluate: judge the snapshot against each framework's requirements. Given several frameworks, the machine is collected once and evaluated against each in turn. One snapshot holds the facts for every framework, so a saved snapshot can be evaluated against any of them later with -FromSnapshot. Each framework is a catalog in the frameworks folder (frameworks/README.md describes the format). A requirement gets its evidence from named checks in this script, from setting expectations in the catalog (a registry value, a security policy value, an audit subcategory, a user right, a service, a Windows feature or a directory test), or from both. Its result is the worst of those results: Fail evidence collected and it misses the threshold Error a collector or check threw; the message is in the report Accepted a Fail that a deviation in the local settings file accepts; the report keeps the evidence and the reason Skipped the check applies but could not run (usually: not elevated) Manual the endpoint cannot show this; the note says what can Pass evidence collected and it meets the threshold NotApplicable the thing checked is absent (for example, RDP is off, or local accounts on a domain controller) Nothing passes by default. A fact missing from the snapshot is an Error, never a Pass. An accepted deviation is listed by rule, with its reason, under Deviations in ComplianceScan.local.psd1 (see ComplianceScan.example.psd1); it accepts a Fail, never an Error. A STIG catalog names the Windows edition and build it was written for. Run on any other, the scan warns, and the report carries the warning. This is evidence for an assessment, not a verdict. Most requirements in most frameworks are procedural or need evidence from outside the endpoint, so the report never states that a machine is compliant and never computes a score. Catalogs for copyrighted standards (CIS, PCI DSS) hold only the requirements an endpoint can show evidence for; the report says so. Writes these files per run into -OutputPath, named by scan ID (computer and collection time, UTC) so a shared folder of many machines' reports sorts by computer, then time: compliance-scan-<scan id>-<framework>.csv one row per requirement compliance-scan-<scan id>-<framework>.json requirements, checks and thresholds compliance-scan-<scan id>-<framework>.records.json flat records for Power BI compliance-scan-<scan id>-<framework>.html a self-contained report compliance-scan-<scan id>-<framework>.cklb STIG Viewer 3 checklist (STIGs, with -StigPath) compliance-scan-<scan id>-<framework>.ckl STIG Viewer 2 checklist (STIGs, with -StigPath) New-ComplianceRollup combines a folder of these reports from many machines into one page per framework. Exit codes: 0 no Fail and no Error (accepted deviations included); 1 at least one Fail; 2 at least one Error, or the scan could not start. Error wins over Fail because a run with errors can't be relied on either way. With several frameworks, the worst of them. For unattended runs, Register-ComplianceScanTask schedules this script. -FallbackPath keeps the reports when -OutputPath can't be written and delivers them on a later run; -LogPath keeps a row per run. .PARAMETER Framework The frameworks to evaluate, by Id or catalog file name, for example CMMC, NIST-800-53r5 or disa-stig-ws2022-ms. Give several as a list or one comma-separated string. Required; run without it to list them. .PARAMETER Level The framework level to assess, such as L1 (CMMC), Low (NIST SP 800-53), CAT I (STIG) or IG1 (CIS). A level includes the levels before it. Defaults to the framework's highest level. For CMMC, 1 and 2 also work. With several frameworks, the level must belong to each. .PARAMETER OutputPath Folder for the report files. Defaults to a Reports folder in the current PowerShell folder, created if needed. .PARAMETER FallbackPath Folder for the reports when -OutputPath can't be written, such as a share that's away. A later run that can write to -OutputPath first moves the waiting reports there, oldest first. Without it, a folder that can't be written stops the scan. A fallback folder that is a junction or link, or that another account owns, stops the scan; a waiting file that is a link, or that another account owns, stays where it is and isn't delivered. .PARAMETER KeepDays Days a report may wait in -FallbackPath before it's removed. Defaults to 90. .PARAMETER LogPath A CSV file that gets one row per run: the time (UTC), computer, frameworks, exit code, where the reports went, how many waiting files were delivered, are still waiting or were removed, and any error. Rows older than a year are dropped. .PARAMETER ProfilePath Thresholds file to use on top of ComplianceScan.example.psd1 and the framework's own thresholds, which can also list accepted deviations. Without it, a ComplianceScan.local.psd1 beside the script is used if present, else %ProgramData%\EndpointComplianceScan\ComplianceScan.local.psd1. That one stops the scan if it or its folder is a junction or link, or is owned by an account other than SYSTEM, Administrators, TrustedInstaller or the one running the scan, since any user can create a folder in ProgramData. .PARAMETER SnapshotPath Also save the collected snapshot to this JSON file, for review elsewhere or as a test fixture. The snapshot holds the computer name and local account names. .PARAMETER FromSnapshot Evaluate a saved snapshot instead of scanning this machine. Works on any operating system. .PARAMETER StigPath The DISA STIG zip from cyber.mil, or the XCCDF file inside it, for the STIG being scanned. With several frameworks it applies to the STIG among them. With it, a STIG scan also writes STIG Viewer checklists (.cklb for STIG Viewer 3, .ckl for STIG Viewer 2) holding every rule's text and the scan's results. .PARAMETER IncludeSnapshot Embed the snapshot in the JSON report. .PARAMETER PassThru Also write the requirement results to the pipeline. .EXAMPLE Invoke-EndpointComplianceScan -Framework CMMC Full CMMC Level 2 scan. Run elevated, or the checks that need elevation are Skipped. .EXAMPLE Invoke-EndpointComplianceScan -Framework DISA-STIG-Win11 -Level 'CAT I' -SnapshotPath .\pc01.snapshot.json STIG CAT I rules only, and keep the raw snapshot. .EXAMPLE Invoke-EndpointComplianceScan -Framework DISA-STIG-WS2022-MS -StigPath .\U_MS_Windows_Server_2022_V2R11_STIG.zip -OutputPath \\fileserver\scans Server 2022 member server STIG, with a STIG Viewer checklist, into a shared folder. .EXAMPLE Invoke-EndpointComplianceScan -Framework NIST-800-53r5 -Level Low -FromSnapshot ./pc01.snapshot.json Evaluate a saved snapshot against another framework. .EXAMPLE Invoke-EndpointComplianceScan -Framework CMMC, DISA-STIG-Win11 -OutputPath \\fileserver\scans -FallbackPath C:\ProgramData\EndpointComplianceScan\Pending One collection, evaluated against CMMC and the Windows 11 STIG. If the share is away, the reports wait locally until a later run. .NOTES Version notes 0.1.0 2026-10-23 First draft: catalog of all 110 requirements, 30 endpoint checks, CSV and JSON reports. 0.2.0 2026-10-23 Multiple frameworks, chosen with -Framework: CMMC, NIST SP 800-171 Rev 3, NIST SP 800-53 Rev 5, DISA Windows 11 STIG, CIS Controls v8.1, NIST CSF 2.0, PCI DSS 4.0.1, HIPAA. Setting expectations in catalogs, an optional features collector, three new checks. Renamed from Invoke-CmmcEndpointScan.ps1. 0.3.0 2026-10-23 Windows Server: DISA STIGs for Server 2019, 2022 and 2025, member server and domain controller. User rights expectations and collector, Kerberos policy, Server features, platform warning when a catalog is for another Windows, local accounts NotApplicable on a domain controller. Snapshot schema 3. 0.4.0 2026-10-24 Reports: a self-contained HTML report, flat JSON records for Power BI, and a STIG Viewer checklist (.ckl) with -StigPath. Reports are named by scan ID. 0.4.1 2026-10-24 STIG Viewer checklist: characters XML can't hold, such as NUL from a registry value, no longer stop the .ckl being written. Registry strings drop trailing NULs. 0.5.0 2026-10-24 STIG Viewer 3 checklist (.cklb) beside the .ckl, which STIG Viewer 3 doesn't open. 0.6.0 2026-10-24 Catalog generator tools/Update-FrameworkCatalog.ps1, with pinned sources and failover. The Windows 11 STIG catalog follows DISA's V2R11. 0.7.0 2026-10-24 Server 2022 and 2025 STIG catalogs follow DISA's V2R11 and V1R4. DomainOnly expectations are NotApplicable off a domain. 0.8.0 2026-10-24 DISA Windows Defender Firewall STIG V2R2. Registry expectations can fall back from the Group Policy value to the local setting (FallbackPath). 0.8.1 2026-10-25 STIG fixes from a domain controller scan: SMB1 settings are NotApplicable when SMB1 is removed (NotApplicableIf), StandaloneOnly expectations, and DISA's full value lists for diagnostic data, Delivery Optimization and the administrator UAC prompt. 0.9.0 2026-10-25 DISA Active Directory Domain and Forest STIGs, with a directory collector on domain controllers (ADSI, or the ActiveDirectory module). Snapshot schema 4. 0.9.1 2026-10-25 Directory findings name well-known SIDs (Everyone, Anonymous Logon) instead of showing the SID. 0.10.0 2026-10-26 DISA Windows PAW STIG. Device Guard code integrity and local privileged group collectors, and endpoint tests for rules that need judgement. Snapshot schema 5. 0.10.1 2026-10-26 Optional features fall back to CIM (Win32_OptionalFeature) when DISM fails, as it can under PowerShell 7. 0.11.0 2026-10-27 Accepted deviations, listed with a reason in the local settings file, report as Accepted instead of Fail. Progress per collector and a time estimate at launch. 0.11.1 2026-10-27 Console output: the launch notice and the results header are short blocks set apart in colour, notes and findings wrap between words to the window, and the report files are listed one per line. 0.11.2 2026-10-27 Test scripts install Pester and PSScriptAnalyzer from the PowerShell Gallery, then a second site checked against a pinned SHA-256, then a local package. 0.12.0 2026-10-27 New-ComplianceRollup: one page per framework from a folder of many machines' reports, with each machine's latest counts and a requirement by machine grid. 0.12.1 2026-10-27 A relative -OutputPath (and the roll-up's -Path) is written under the current PowerShell folder, not the process's starting folder. 0.13.0 2026-10-28 HIPAA catalog complete (all 65 standards and specifications). Retired catalogs, such as the Server 2019 STIGs, say so in the scan output and HTML report. 0.14.0 2026-10-28 Roll-up trends: per-machine trend lines and changes since the previous scan, a weekly chart for all machines, and a trend CSV with one row per scan. 0.15.0 2026-10-29 Power BI template (powerbi\, .pbit on the Release) reading every .records.json in a folder. 0.15.1 2026-10-30 README: the template's parameters open empty; the table gives suggested values. 0.16.0 2026-10-30 Scheduled scans: several frameworks per run, -FallbackPath, -KeepDays and -LogPath, and Register-ComplianceScanTask. 0.16.1 2026-10-30 README: screenshots of the HTML report, the Power BI template and the roll-up, from the fictional WESTMARK test machines. 0.17.0 2026-11-01 Fixes before 1.0. Waiting reports that are links, or that another account owns, aren't delivered, and a fallback folder that is a link stops the scan. A scan run as SYSTEM skips per-user (HKCU) settings instead of failing them. Snapshot schema 6 (reads 5). BitLocker not installed (Windows Server without the feature) is a Fail, not a collector Error. 1.0.0 2026-11-02 First stable release, also published to the PowerShell Gallery as the EndpointComplianceScan module. Reports default to .\Reports in the current folder. A default local profile under %ProgramData%, refused when another account owns it. #> [CmdletBinding()] param( [string[]]$Framework, [string]$Level, [string]$OutputPath, [string]$FallbackPath, [ValidateRange(1, 3650)][int]$KeepDays = 90, [string]$LogPath, [string]$ProfilePath, [string]$SnapshotPath, [string]$FromSnapshot, [string]$StigPath, [switch]$IncludeSnapshot, [switch]$PassThru ) & (Join-Path $PSScriptRoot 'Invoke-EndpointComplianceScan.ps1') @PSBoundParameters } function New-ComplianceRollup { <# .SYNOPSIS Combines the scan reports in a folder into one roll-up page per framework. .DESCRIPTION Reads the compliance-scan-*.json reports that Invoke-EndpointComplianceScan writes, typically from many machines into one shared folder, and writes for each framework found: compliance-rollup-<framework>.html machines, trends, changes, and requirements by machine compliance-rollup-<framework>.csv the requirements by machine grid compliance-rollup-<framework>-trend.csv one row per scan in the trend window, with its counts Each computer counts once per framework, by its latest scan in the folder. Nothing is scanned or re-evaluated: the roll-up shows what the reports say. Like the scan, the roll-up is evidence for an assessment, not a verdict. It shows counts and states, never a score or a percentage. The page lists each machine with its counts, scan date, level and tool version, flagging a scan older than -MaxAge days, one run without elevation and one with a platform warning. Below that, a grid shows each requirement's state on each machine, with the requirements that fail on the most machines first. Machines scanned at another level or with another catalog version can have a different list of requirements; the page says so, and leaves a cell blank where a machine's scan doesn't include the requirement. Earlier scans in the folder, back to -TrendDays days, give each machine a small Fail and Error trend line and its change since the previous scan, a week-by-week chart for all machines, and a list of the requirements that newly Fail or Error, or no longer do, since each machine's previous scan. Counts compare only between scans at the same level with the same requirements; a trend line breaks where that changes, and the page says so. Exit code 0 when every framework's roll-up was written; 2 when the folder holds no scan reports, or the roll-up could not be written. .PARAMETER Path The folder of scan reports. .PARAMETER OutputPath Folder for the roll-up files. Defaults to -Path. .PARAMETER Framework Only roll up these framework Ids, such as CMMC or DISA-STIG-WS2025-MS. .PARAMETER MaxAge Days after which a machine's latest scan is flagged as stale, from 1 to 3650. Default 30. .PARAMETER AsOf The date to judge scan age against, in UTC. Defaults to now. .PARAMETER TrendDays Days of earlier scans to show trends from, counting back from -AsOf. Default 90. .PARAMETER Recurse Also read reports in subfolders of -Path. .EXAMPLE New-ComplianceRollup -Path \\fileserver\scans One roll-up per framework scanned, written into the same folder. .EXAMPLE New-ComplianceRollup -Path .\Reports -Framework DISA-STIG-WS2025-MS -MaxAge 7 -OutputPath .\Rollup .EXAMPLE New-ComplianceRollup -Path \\fileserver\scans -Recurse -TrendDays 180 Trends from the last six months of scans, including subfolders. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Runs New-ComplianceRollup.ps1, which only writes report files and has no -WhatIf.')] [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [string]$OutputPath, [string[]]$Framework = @(), [ValidateRange(1, 3650)][int]$MaxAge = 30, [Nullable[datetime]]$AsOf, [ValidateRange(1, 3650)][int]$TrendDays = 90, [switch]$Recurse ) & (Join-Path $PSScriptRoot 'New-ComplianceRollup.ps1') @PSBoundParameters } function Register-ComplianceScanTask { <# .SYNOPSIS Schedules Invoke-EndpointComplianceScan to scan this machine and write its reports to a shared folder. .DESCRIPTION Copies the scanner, its frameworks folder and the profile files beside it to an install folder (by default %ProgramFiles%\EndpointComplianceScan), so the task never runs code from a user folder or a share. Then it registers one scheduled task that runs the scan as SYSTEM, elevated, so every collector can read what it needs. The scan only reads; the task, the installed copy and the files under %ProgramData%\EndpointComplianceScan are all this leaves. The data folder under %ProgramData% is made readable and writable by SYSTEM and Administrators only, since ProgramData lets any user create files in a subfolder and a file planted in the fallback folder would reach the share as this computer's report. A data folder that holds a junction or link, or anything another account owns, is refused before anything is installed. Each run writes to -OutputPath. When that can't be written (the share is down, or the machine is off the network), the reports go to a fallback folder, %ProgramData%\EndpointComplianceScan\Pending, and the next run that reaches the share moves them there, oldest first. Waiting reports older than -KeepDays are removed. Each run adds a row to %ProgramData%\EndpointComplianceScan\scan-log.csv. The task runs as the computer account, so on a share it writes as DOMAIN\COMPUTER$. The README gives share permissions that let machines add their reports without reading anyone else's. Run elevated. Running it again replaces the task and refreshes the installed copy, removing a profile or STIG file an earlier run installed and this one doesn't. -Unregister removes the task; add -RemoveFiles to remove the installed copy too. -WhatIf shows what would change. Exit codes: 0 done (or, with -WhatIf, shown); 2 nothing was scheduled or removed, or it stopped partway, with the error written to the error stream. .PARAMETER Framework One or more frameworks to evaluate, as for the scanner. The machine is collected once per run and evaluated against each. .PARAMETER Level The level, as for the scanner. It applies to every framework given; without it each framework uses its highest level. .PARAMETER OutputPath The shared folder for the reports, such as \\fileserver\scans. .PARAMETER StigPath The DISA STIG zip or XCCDF file, as for the scanner. It is copied to the install folder, since the task can't read a user's folder. .PARAMETER ProfilePath A thresholds file to install as the scanner's ComplianceScan.local.psd1. Without it, a ComplianceScan.local.psd1 beside this script is installed if there is one. With neither, the scanner uses %ProgramData%\EndpointComplianceScan\ComplianceScan.local.psd1 when that exists, whatever -DataPath says. .PARAMETER Schedule Weekly (the default) or Daily. .PARAMETER At The start time, as HH:mm in local time. Defaults to 02:00. .PARAMETER DaysOfWeek The days a weekly task runs. Defaults to Monday. .PARAMETER RandomDelayMinutes Up to this many minutes are added to each start at random, so a fleet doesn't reach the share at once. Defaults to 120; 0 turns it off. .PARAMETER KeepDays Days a report may wait in the fallback folder before it's removed. Defaults to 90. .PARAMETER InstallPath Where to install the scanner. Defaults to %ProgramFiles%\EndpointComplianceScan. .PARAMETER DataPath Where the fallback folder and run log live. Defaults to %ProgramData%\EndpointComplianceScan. .PARAMETER TaskName The task's name. Defaults to 'Endpoint compliance scan'. .PARAMETER Unregister Remove the task instead of registering it. .PARAMETER RemoveFiles With -Unregister, also remove the installed copy. The fallback folder and run log stay, since they may hold reports not yet delivered. .EXAMPLE Register-ComplianceScanTask -Framework CMMC -OutputPath \\fileserver\scans Weekly CMMC scan, Mondays from 02:00 with up to two hours' random delay. .EXAMPLE Register-ComplianceScanTask -Framework CMMC, DISA-STIG-WS2022-MS -StigPath .\U_MS_Windows_Server_2022_V2R11_STIG.zip -OutputPath \\fileserver\scans -Schedule Daily -At 22:00 Daily scan against two frameworks, with STIG Viewer checklists for the STIG. .EXAMPLE Register-ComplianceScanTask -Unregister -RemoveFiles Remove the task and the installed copy. .NOTES Part of endpoint-compliance-scan. Windows PowerShell 5.1 or PowerShell 7 on Windows; the task itself runs Windows PowerShell 5.1, which every Windows has. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Passes -WhatIf and -Confirm to Register-ComplianceScanTask.ps1, which calls ShouldProcess.')] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Register')] param( [Parameter(ParameterSetName = 'Register', Mandatory)][string[]]$Framework, [Parameter(ParameterSetName = 'Register')][string]$Level, [Parameter(ParameterSetName = 'Register', Mandatory)][string]$OutputPath, [Parameter(ParameterSetName = 'Register')][string]$StigPath, [Parameter(ParameterSetName = 'Register')][string]$ProfilePath, [Parameter(ParameterSetName = 'Register')][ValidateSet('Weekly', 'Daily')][string]$Schedule = 'Weekly', [Parameter(ParameterSetName = 'Register')][ValidatePattern('^([01]?\d|2[0-3]):[0-5]\d$')][string]$At = '02:00', [Parameter(ParameterSetName = 'Register')][ValidateSet('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')][string[]]$DaysOfWeek = @('Monday'), [Parameter(ParameterSetName = 'Register')][ValidateRange(0, 1440)][int]$RandomDelayMinutes = 120, [Parameter(ParameterSetName = 'Register')][ValidateRange(1, 3650)][int]$KeepDays = 90, [string]$InstallPath, [Parameter(ParameterSetName = 'Register')][string]$DataPath, [string]$TaskName = 'Endpoint compliance scan', [Parameter(ParameterSetName = 'Unregister', Mandatory)][switch]$Unregister, [Parameter(ParameterSetName = 'Unregister')][switch]$RemoveFiles ) & (Join-Path $PSScriptRoot 'Register-ComplianceScanTask.ps1') @PSBoundParameters } Export-ModuleMember -Function Invoke-EndpointComplianceScan, New-ComplianceRollup, Register-ComplianceScanTask |