Compliance Automation
ESXiManScan is an Ansible role and PowerCLI tool I wrote to automate DISA STIG compliance scanning of VMware ESXi hosts. It connects to each host, runs version-appropriate rule checks, and produces a populated STIG Viewer checklist (a .cklb file) that is ready for submission or review — turning a tedious manual control-by-control review into a repeatable, hands-off scan.
STIG checklists for ESXi run to hundreds of controls, and doing them by hand for every host is slow and error-prone. This one came straight out of the day job — the reviews are repetitive, but most of the evidence is sitting right there in the API, so they are a perfect target for automation. I wanted a tool that reads the same evidence an assessor would, fills in the official STIG Viewer checklist, and leaves only the genuinely manual items for a person to confirm.
The result reads each rule, queries the host live, and writes back NotAFinding, Not_Applicable, or Open into a real .cklb — so the human time goes to the few controls that actually need judgment.
ESXiManScan works in two phases. Most rules are evaluated through PowerCLI, and the handful that require raw filesystem state are handled over SSH by Ansible and merged back into the same checklist.
Completed_Checklists/ and writes a timestamped log to Logs/.Two controls inspect raw filesystem state that PowerCLI cannot reach, so the Ansible play SSHes into each host, runs the shell commands, then merges the results into the existing checklist with a PowerShell XML update step (matched by hostname):
# V-258791 /etc/vmware/settings must be 0 bytes
stat -c "%s" /etc/vmware/settings
# V-258792 no vmx.log override in /etc/vmware/config (expect no output)
grep "^vmx\.log" /etc/vmware/config Version and build constants live at the top of ESXiManScan.ps1 and get bumped when new STIG releases or ESXi builds drop.
pwsh) installed.VCF.PowerCLI) available to the runner.root.files/Blank_Checklists/.The role lays out like this:
roles/
esximanscan/
tasks/
main.yml
files/
ESXiManScan.ps1
Rules/
ESXi70_Rules.ps1
ESXi80_Rules.ps1
ESXi90_Rules.ps1
Blank_Checklists/
ESXi80_V2R3_BlankChecklist.cklb
Completed_Checklists/ (generated at runtime)
Logs/ (generated at runtime) Blank checklists follow a fixed naming convention so the script can locate the right one for each version:
ESXi80_V2R3_BlankChecklist.cklb
ESXi70_V1R4_BlankChecklist.cklb Include the role in a play targeting your ESXi hosts. The scan runs once and iterates over all connected hosts:
- hosts: esxi
gather_facts: false
roles:
- esximanscan Store each host root password in an Ansible Vault variable (ansible_password) and run with the vault prompt:
ansible-playbook -i inventory.ini esxi_stig.yml --ask-vault-pass The root password is passed to PowerShell through the PASSWORD environment variable and converted to a SecureString at runtime -- it is never written to disk. The scan task sets no_log: true so credentials never show up in Ansible output.
Rules live in Rules/ESXi<ver>_Rules.ps1 as PSCustomObject entries appended to a $rules array. Each carries the STIG identifiers, a scriptblock that returns a single value, and the value that counts as compliant:
$rules += [PSCustomObject]@{
VULN_ID = 'V-258757'
STIG_ID = 'ESXI-80-000196'
Command = { $script:advSettings['Security.AccountLockFailures'].Value }
PassResult = '3'
FailComments = 'Account lockout threshold must be 3 or fewer failed attempts.'
} Each scriptblock has access to the objects cached once at scan start, which keeps rule logic short and fast:
$script:vmhost — the VMHost object$script:esxcli — EsxCli v2 interface$script:advSettings — advanced settings$script:hostServices — host services$script:vmkAdapters — VMkernel adapters$script:vSwitches and $script:portGroups — networkingReturning "NotApplicable" from a scriptblock marks the control Not Applicable; using { return $false } with a documented FailComments flags a control as always-open for manual review.
Every run produces a completed checklist in Completed_Checklists/ and a per-host log in Logs/ with pass / fail / N/A per rule. Each checklist entry records exactly how the determination was made, in a consistent format:
ESXiManScan-1.0
Command: <scriptblock or shell command>
Expected Result: <PassResult value>
Actual Result: <observed value>