Compliance Automation

ESXiManScan

Jun 29, 2026~10 min readAnsible . PowerCLI . STIG
AnsiblePowerShellVMware PowerCLIESXi 7.0 / 8.0DISA STIGSTIG Viewer CKLB

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.

Why I built it

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.

01 Connect PowerCLI 02 Detect 7 / 8 / 9 03 Load rules per version 04 Evaluate live API 05 Shell checks SSH (Ansible) 06 Output .cklb + logs

How it works

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.

Phase 1 -- PowerCLI scan

  • Connects to each ESXi host and detects the major version (7.0 / 8.0 / 9.0), then dot-sources the matching rule set.
  • Caches commonly used objects once — advanced settings, esxcli, host services, vSwitches, port groups, VMkernel adapters — to avoid redundant API calls across rules.
  • Loads the blank STIG checklist for that version and populates asset metadata (hostname, IP, MAC, FQDN).
  • Runs each rule scriptblock, compares the result to its expected value, and marks the entry NotAFinding, Not_Applicable, or Open.
  • Saves the completed checklist to Completed_Checklists/ and writes a timestamped log to Logs/.

Phase 2 -- Shell checks

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

Supported versions

  • ESXi 7.0 — STIG V1R4.
  • ESXi 8.0 — STIG V2R3 (latest known build 24677879).
  • ESXi 9.0 — supported via its own rule set.

Version and build constants live at the top of ESXiManScan.ps1 and get bumped when new STIG releases or ESXi builds drop.

Prerequisites and layout

  • An Ansible control node with PowerShell (pwsh) installed.
  • The VMware PowerCLI module (VCF.PowerCLI) available to the runner.
  • SSH access to the target hosts as root.
  • Blank STIG Viewer checklists placed in 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

Running it

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
Credential handling

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.

Rule authoring

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 — networking

Returning "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.

Output

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>

Known limitations

  • V-258784 (ESXI-80-000229) is hardcoded Open — DoD certificate installation is out of scope for this tool.
  • V-258791 and V-258792 require SSH; the ESXi shell and SSH service must be enabled during the scan window.