From ca9312379adb7f237f9fbf356ed128090bfacfc0 Mon Sep 17 00:00:00 2001 From: Ole Date: Mon, 17 Aug 2026 15:12:16 +0200 Subject: [PATCH] CYBER-0 readme updateed --- README.md | 1304 +++++++++++++++++++++++++++-------------------------- 1 file changed, 673 insertions(+), 631 deletions(-) diff --git a/README.md b/README.md index 82124b9..e328576 100644 --- a/README.md +++ b/README.md @@ -5,23 +5,28 @@ Ansible playbooks reimagined as a **test framework** for industrial control syst aborts on failure, produces structured JSON, and renders into human-readable reports via Go templates or Python. +Supports automated testing across **Linux, Windows Server, Windows Clients, +MS SQL Server, VMware vSphere, Hyper-V, Cisco IOS switches, and Cisco ASA +firewalls** — from a single containerised Ansible control node. + --- ## Table of Contents 1. [Architecture](#architecture) -2. [Ansible Control Node — QEMU-Bootable](#ansible-control-node--qemu-bootable-alpine-linux) -3. [Quick Start](#quick-start) -4. [How It Works](#how-it-works) -5. [The Test Pattern (The "Secret Sauce")](#the-test-pattern-the-secret-sauce) -6. [JSON Output Schema](#json-output-schema) -7. [IEC 62443-3-3 SL2 Coverage](#iec-62443-3-3-sl2-coverage) -8. [Adding a New Test](#adding-a-new-test) +2. [Quick Start](#quick-start) +3. [Platform-Specific Examples](#platform-specific-examples) +4. [The Test Pattern](#the-test-pattern) +5. [Human-in-the-Loop Tests](#human-in-the-loop-tests) +6. [Adding a New Test](#adding-a-new-test) +7. [JSON Output Schema](#json-output-schema) +8. [IEC 62443-3-3 SL2 Coverage](#iec-62443-3-3-sl2-coverage) 9. [Rendering Reports](#rendering-reports) -10. [File Reference](#file-reference) -11. [Design Decisions & Tradeoffs](#design-decisions--tradeoffs) -12. [Comparison to Alternatives](#comparison-to-alternatives) -13. [Roadmap](#roadmap) +10. [Ansible Control Node — Container & QEMU VM](#ansible-control-node--container--qemu-vm) +11. [File Reference](#file-reference) +12. [Design Decisions & Tradeoffs](#design-decisions--tradeoffs) +13. [Comparison to Alternatives](#comparison-to-alternatives) +14. [Roadmap](#roadmap) --- @@ -29,82 +34,621 @@ reports via Go templates or Python. ``` playbooks/ -├── site.yml # Entry point: orchestrates all FR suites -├── suites/ -│ ├── fr1_auth.yml # FR1: Identification & Authentication Control -│ ├── fr2_use_control.yml # FR2: Use Control (audit, sudo, sessions) -│ └── fr5_data_flow.yml # FR5: Restricted Data Flow (firewall, services) +├── site.yml # Entry point for Linux targets (all FR suites) +├── suites/ # Included task-list suites for Linux +│ ├── fr1_auth.yml # FR1: Identification & Authentication (10 tests) +│ ├── fr2_use_control.yml # FR2: Use Control — audit, sudo, sessions (6 tests) +│ └── fr5_data_flow.yml # FR5: Restricted Data Flow — firewall, services (4 tests) ├── library/ │ └── report.yml # Aggregates test_results[] → JSON → disk +├── examples/ # ◄ Standalone playbooks — one per platform type +│ ├── linux_vm.yml # Linux: SSH hardening, sysctl, sudo logging +│ ├── windows_server.yml # Windows Server: WinRM, security policy, audit +│ ├── windows_client.yml # Windows Client: BitLocker, screen lock, USB +│ ├── mssql_server.yml # MS SQL Server: auth mode, sa, xp_cmdshell, audit +│ ├── vmware_vsphere.yml # VMware ESXi: lockdown, NTP, SSH/Shell services +│ ├── hyperv_cluster.yml # Hyper-V: Gen2, vSwitch isolation, integration svc +│ ├── cisco_switch.yml # Cisco IOS: SSH v2, no SNMPv1, banner, NTP +│ └── cisco_firewall.yml # Cisco ASA: no Telnet, IKEv2, syslog, AAA, ACL +└── templates/ # ◄ Copy-and-fill templates for writing new tests + ├── test_automated.yml # Shell-based gather → evaluate pattern + ├── test_file_check.yml # File permission check via ansible.builtin.stat + ├── test_service_check.yml # Service state check via service_facts + └── test_hitl.yml # Human-in-the-Loop with ansible.builtin.pause + reports/ -├── render.go # Go renderer (text/template on external .gohtml) -├── render_report.py # Python renderer (terminal + markdown modes) -├── report.gohtml # Go template file for terminal-box-drawing report +├── render.go # Go renderer (text/template + external .gohtml) +├── render_report.py # Python renderer (terminal + markdown output) +├── report.gohtml # Go template for terminal box-drawing report ├── go.mod # Go module definition -└── sample-output.json # Example output for offline rendering tests -inventory.ini # Ansible inventory (target host list) -run.sh # End-to-end wrapper: ansible → find json → render -README.md # This file +└── sample-output.json # Example output for offline renderer tests +inventory.ini # Ansible inventory — all platform groups defined +run.sh # End-to-end wrapper: ansible → find JSON → render ``` ### Data Flow ``` - ansible-playbook site.yml + ansible-playbook .yml │ - ├── FR1 suite ──┐ - ├── FR2 suite ──┤ Each task appends to test_results[] - ├── FR5 suite ──┘ via set_fact, wrapped in ignore_errors + ├── Gather tasks ──┐ + │ ├─ collect system state (shell, stat, ios_command, etc.) + ├── Evaluate tasks─┘ + │ judge pass/fail, append to test_results[] │ ▼ library/report.yml - - Assembles __report dict with summary, by_category, failures - - Prints summary to console + - Assembles __report dict with summary, by_category, by_severity, failures + - Prints summary box to console - Writes JSON to reports/-.json │ ▼ - run.sh → python3 reports/render_report.py reports/*.json - → go run reports/render.go reports/*.json reports/report.gohtml + run.sh / render manually: + python3 reports/render_report.py reports/-.json + go run reports/render.go reports/-.json reports/report.gohtml +``` + +### Target Integrations + +| Platform | Ansible Collection | Connection | Python library | +|---|---|---|---| +| **Linux** | built-in | SSH | — | +| **Windows Server / Client** | `ansible.windows` | WinRM + NTLM/Kerberos | `pywinrm` | +| **MS SQL Server** | `ansible.windows` | WinRM → PowerShell `Invoke-Sqlcmd` | `pywinrm` | +| **VMware vSphere ESXi** | `community.vmware` | vSphere SOAP API (delegate_to: localhost) | `pyvmomi` | +| **Hyper-V** | `ansible.windows` | WinRM → PowerShell Hyper-V cmdlets | `pywinrm` | +| **Cisco IOS / IOS-XE** | `cisco.ios` | SSH via `network_cli` | `paramiko`, `netmiko` | +| **Cisco ASA** | `cisco.asa` | SSH via `network_cli` | `paramiko` | +| **Cisco NX-OS** | `cisco.nxos` | SSH / NX-API via `network_cli` | `ncclient` | + +--- + +## Quick Start + +### Prerequisites + +- Ansible ≥ 2.9 with the collections listed above (pre-installed in the Docker image) +- Python ≥ 3.6 (for the Python report renderer) +- Go ≥ 1.21 (optional, for the Go renderer) +- For Windows / VMware / Cisco targets: the Python libraries listed above + +### Step 1: Configure Inventory + +Edit `inventory.ini`. Each platform group has the required connection variables +already set — just uncomment the hosts: + +```ini +[linux_vms] +linux-vm-01.example.com ansible_user=auditor + +[windows_servers] +win-srv-01.example.com + +[cisco_switches] +sw-core-01.example.com +``` + +Store passwords in Ansible Vault: +```bash +ansible-vault encrypt_string 'MyPassword' --name ansible_password +``` + +### Step 2: Run a Playbook + +```bash +# Linux targets — all FR suites via the main entry point: +ansible-playbook -i inventory.ini playbooks/site.yml --limit linux_vms -K + +# Or use the wrapper script (finds & renders the report automatically): +./run.sh --limit linux_vms -K + +# Platform-specific examples: +ansible-playbook -i inventory.ini playbooks/examples/windows_server.yml +ansible-playbook -i inventory.ini playbooks/examples/cisco_switch.yml +ansible-playbook -i inventory.ini playbooks/examples/vmware_vsphere.yml + +# Local self-test (localhost is pre-configured in inventory.ini): +ansible-playbook -i inventory.ini playbooks/site.yml --limit localhost -K +``` + +### Step 3: Render the Report + +```bash +# Terminal box-drawing format (default): +python3 reports/render_report.py reports/localhost-2026-08-14.json + +# Markdown (for GitHub / GitLab wikis, PR comments): +python3 reports/render_report.py reports/localhost-2026-08-14.json --format md + +# Go template renderer: +cd reports && go run render.go ../reports/localhost-2026-08-14.json report.gohtml ``` --- -## Ansible Control Node — Split Architecture +## Platform-Specific Examples -Two independent artifacts. The QEMU VM provides Docker; the Ansible container -runs the tests. Same container image runs on Docker Swarm or Kubernetes without -the VM layer. +The `playbooks/examples/` directory contains a complete, runnable playbook for +each supported platform. Each file: + +- Has a header comment with the required `inventory.ini` group vars and any + prerequisites (WinRM setup, SQLPS module, vCenter permissions, etc.) +- Follows the identical `block` → gather → evaluate → `ignore_errors` pattern +- Uses the most Ansible-native module available for each check (e.g. + `win_security_policy` instead of `win_shell` for Windows password policy) +- Ends with `include_tasks: ../library/report.yml` to produce a JSON report +- Includes at least one Human-in-the-Loop test where automated checks cannot + cover the full control + +### Linux VMs — `playbooks/examples/linux_vm.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/linux_vm.yml -K +``` + +Checks beyond the core FR suites: SSH root login and password auth settings, +sudo session logging (`Defaults log_input,log_output`), kernel IP forwarding +and ICMP redirect sysctl values, core dump disabled. + +### Windows Server — `playbooks/examples/windows_server.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/windows_server.yml +``` + +Uses `ansible.windows.win_security_policy` for password and lockout policy +(no PowerShell shell-out needed), `win_audit_policy_system` for audit subcategories, +`win_service_info` for Telnet/FTP service state, and `win_shell` with +`ConvertTo-Json` for Firewall profile states parsed via Ansible's `from_json` filter. + +### Windows Client — `playbooks/examples/windows_client.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/windows_client.yml +``` + +Checks: domain membership, BitLocker status on the OS drive, screen lock timeout +via registry (`win_reg_stat`), Public firewall profile. Includes a HITL check +for USB storage port controls with registry evidence displayed. + +### MS SQL Server — `playbooks/examples/mssql_server.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/mssql_server.yml +# Add per-host: mssql_instance=NAMED_INSTANCE (default: MSSQLSERVER) +``` + +Connects via WinRM to the Windows host, then runs `Invoke-Sqlcmd` via `win_shell` +to query SQL Server internals. Checks: Windows-only authentication mode, SA account +disabled, `xp_cmdshell` disabled, login audit level. HITL check for sysadmin +role membership against an authorised list. + +**Prerequisite on target:** `Install-Module SqlServer -Force -AllowClobber` + +### VMware vSphere — `playbooks/examples/vmware_vsphere.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/vmware_vsphere.yml +``` + +All tasks use `delegate_to: localhost` — no SSH to ESXi hosts. The inventory +host is the ESXi FQDN; `vcenter_hostname/username/password` are group vars +pointing at vCenter. Uses `community.vmware` info modules: +`vmware_host_lockdown_info`, `vmware_host_ntp_info`, `vmware_host_service_info`, +`vmware_host_config_info`. HITL check for vSwitch isolation using +`vmware_vswitch_info`. + +**Required vCenter read-only permissions:** +Host → Configuration → Security Profile, Advanced Settings; Global → Settings. + +### Hyper-V Cluster — `playbooks/examples/hyperv_cluster.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/hyperv_cluster.yml +``` + +Connects via WinRM and runs PowerShell Hyper-V cmdlets via `win_shell`. Checks: +all VMs use Generation 2 with Secure Boot enabled, External vSwitch +`AllowManagementOS` exposure (flagged as review), Hyper-V VMMS Admin event log +active, integration services enabled on all running VMs. HITL check for +physical NIC segregation between ICS and management networks. + +### Cisco Switch (IOS / IOS-XE) — `playbooks/examples/cisco_switch.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/cisco_switch.yml +``` + +Uses `cisco.ios.ios_command` to run `show` commands and parses output with +`regex_search` / `regex_findall`. Uses `ios_facts` (gathered automatically via +`gather_facts: yes`) for interface data. Checks: SSH v2 / no Telnet on VTY, +no SNMPv1/v2c community strings, login banner, NTP synchronised. HITL check +for port VLAN assignment and physical port labelling. + +### Cisco Firewall (ASA) — `playbooks/examples/cisco_firewall.yml` + +```bash +ansible-playbook -i inventory.ini playbooks/examples/cisco_firewall.yml +``` + +Uses `cisco.asa.asa_command` with `ansible_become=yes` (enable mode). Checks: +no Telnet management access, SSH access configured, IKEv1 disabled (IKEv2 only), +remote syslog server configured, AAA authentication for SSH/enable, inbound +ACLs applied on zone interfaces. HITL check for ACL rule review +(no broad `permit ip any any`). + +--- + +## The Test Pattern + +Every test follows a rigid **Gather → Evaluate → Record** structure inside an +Ansible `block` with `ignore_errors: yes`. This ensures the run never aborts +regardless of what is found on the target. + +```yaml +# ── SR 1.5: Password minimum length ──────────────────────────────────────── + +- block: + - name: "Gather: Check pwquality minlen" + ansible.builtin.shell: | + grep -E '^\s*minlen\s*=' /etc/security/pwquality.conf 2>/dev/null | tail -1 || echo "NOT SET" + register: _minlen + changed_when: false # gather tasks must never say "changed" + + - name: "Evaluate: IAC-05" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'IAC-05', + 'category': 'FR1 — Identification and Authentication Control', + 'requirement': 'SR 1.5 — Authenticator Strength', + 'description': 'Password minimum length shall be ≥ 14 characters', + 'passed': ( + (_minlen.stdout | regex_search('minlen\\s*=\\s*(\\d+)', '\\1') + | default(['0'], true) | first | int) >= 14 + ), + 'expected': 'minlen >= 14 in /etc/security/pwquality.conf', + 'actual': _minlen.stdout | trim, + 'severity': 'high', + 'remediation': 'Set minlen=14 in /etc/security/pwquality.conf' + }] }}" + ignore_errors: yes # NEVER abort the run +``` + +### Why `block` + `ignore_errors` Instead of `failed_when` + +| Approach | Behaviour | +|---|---| +| `failed_when: false` on shell task | Shell always "succeeds"; non-zero exit still shows red in Ansible output | +| `block` + `ignore_errors: yes` | Failures are captured and marked orange; execution continues; `register`ed variables remain available in the evaluate task | + +### Tips for Robust Checks + +| Tip | Why | +|---|---| +| `changed_when: false` on all gather tasks | Tests must never report as "changed" | +| `\| trim` on shell output | Shell often returns trailing newlines | +| `\| default('NOT SET', true)` | Prevents undefined-variable errors when files or keys are absent | +| Guard numeric comparisons | `'' \| int` = 0 in Jinja2 — a missing value can silently pass a `≤ 90` check; always verify the value exists and is non-zero first | +| `\| regex_search(pattern, '\\1')` | Use capture group syntax to extract a number cleanly, avoiding chained `regex_replace` calls that crash on `None` | +| `ConvertTo-Json` on Windows | Return structured data from `win_shell` and parse with Ansible's `from_json` filter instead of regex | +| Platform branching | Use `when: ansible_os_family == 'Debian'` variants for distro-specific commands | + +--- + +## Human-in-the-Loop Tests + +Some IEC 62443 SL2 controls cannot be verified automatically — physical access +controls, policy document review, proprietary vendor interfaces, or checks where +the output must be interpreted by a qualified reviewer. + +**HITL tests use `ansible.builtin.pause` with `delegate_to: localhost`**, which +prompts the reviewer on the Ansible control node even when running against remote +targets. For multi-host runs the prompt fires once per host, so each target gets +an independent verdict. + +```yaml +- block: + - name: "Gather: [HITL] Collect evidence" + ansible.builtin.shell: your-gather-command + register: _evidence + changed_when: false + + - name: "Display: [HITL] TEST_ID — evidence" + ansible.builtin.debug: + msg: | + ══════════════════════════════════════════════════════════════ + MANUAL REVIEW REQUIRED · TEST_ID · {{ inventory_hostname }} + ══════════════════════════════════════════════════════════════ + {{ _evidence.stdout | indent(1) }} + ══════════════════════════════════════════════════════════════ + + - name: "Prompt: TEST_ID — verdict" + ansible.builtin.pause: + prompt: "Enter verdict [pass / fail / skip]:" + register: _verdict + delegate_to: localhost # ← always prompts on the control node + + - name: "Prompt: TEST_ID — notes on failure" + ansible.builtin.pause: + prompt: "Describe the finding:" + register: _notes + delegate_to: localhost + when: _verdict.user_input | lower | trim in ['fail', 'f'] + + - name: "Evaluate: TEST_ID" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'TEST_ID', + 'passed': ( + 'skipped' if (_verdict.user_input | lower | trim in ['skip', 's', '']) + else (_verdict.user_input | lower | trim in ['pass', 'p']) + ), + 'reviewer': ansible_user_id, + 'notes': (_notes.user_input | trim) if _notes is defined else '' + }] }}" + ignore_errors: yes +``` + +The `reviewer` and `notes` fields are written into the JSON report alongside +the automated evidence, creating an auditable record of who reviewed what and +when. See `playbooks/templates/test_hitl.yml` for the full copy-and-fill template. + +**Note:** Playbooks containing HITL tests require an interactive terminal and +cannot be run unattended in a CI pipeline. Keep HITL tests in separate playbooks +or use Ansible tags to separate them from automated checks. + +--- + +## Adding a New Test + +### Step 1: Choose a Template + +The `playbooks/templates/` directory contains four ready-to-use templates. +Copy the one that matches your check type: + +| Template | Use when | +|---|---| +| `playbooks/templates/test_automated.yml` | Running a shell command and evaluating its output | +| `playbooks/templates/test_file_check.yml` | Checking file ownership, permissions, or existence (`ansible.builtin.stat`) | +| `playbooks/templates/test_service_check.yml` | Checking service running/enabled state (`service_facts`) | +| `playbooks/templates/test_hitl.yml` | Control requires human reviewer input | + +Every template has detailed comments explaining the `passed` expression patterns +relevant to that check type. + +### Step 2: Fill in the Placeholders + +Replace all UPPERCASE placeholders: `TEST_ID`, `FR_NUMBER`, `CATEGORY_NAME`, +`REQUIREMENT`, `DESCRIPTION`, `SEVERITY`, `REMEDIATION`, and the gather command. + +### Step 3: Add to a Suite or Example + +For Linux targets, append the block to the appropriate `suites/frN_*.yml` file +and ensure that suite is included in `site.yml`: + +```yaml +- name: "Suite: FRN — Category Name" + block: + - ansible.builtin.include_tasks: suites/frN_category.yml + ignore_errors: yes +``` + +For other platforms, append the block to the relevant `examples/` playbook. + +### Test ID Naming Convention + +- **Prefix**: Platform abbreviation + category (e.g., `IAC`, `UC`, `RDF` for + Linux; `WIN-IAC`, `SQL-UC`, `FW-RDF`, `SW-RDF`, `VMW-IAC` for platform examples) +- **Number**: Sequential within prefix, zero-padded (`01`, `02`, ...) +- **HITL suffix**: Append `-HITL` for human-in-the-loop tests (e.g., `WIN-CLI-HITL-01`) + +--- + +## JSON Output Schema + +Each test produces one entry in `test_results[]`. The complete report schema: + +```jsonc +{ + "meta": { + "standard": "IEC 62443-3-3", + "security_level": "SL2", + "target": "ics-gateway-01", // inventory_hostname + "timestamp": "2026-08-14T09:00:00Z", // ISO 8601 + "executed_by": "auditor" // ansible_user_id + }, + "summary": { + "total": 20, + "passed": 14, + "failed": 4, + "review": 1, // passed == "review" (manual review items) + "skipped": 1 // passed == "skipped" (HITL skipped or not applicable) + }, + "by_category": [ // [["FR1 — ...", [{...}]], ...] + ["FR1 — Identification and Authentication Control", [{...}, {...}]], + ["FR2 — Use Control", [{...}]] + ], + "by_severity": { + "critical": [{...}], + "high": [{...}], + "medium": [{...}], + "low": [{...}] + }, + "failures": [{...}], // Only tests where passed == false + "results": [ + { + "test_id": "IAC-05", + "category": "FR1 — ...", + "requirement": "SR 1.5 — Authenticator Strength", + "description": "Password minimum length shall be >= 14 characters", + "passed": false, // bool | "review" | "skipped" + "expected": "minlen >= 14", + "actual": "minlen = 8", + "severity": "high", // critical | high | medium | low + "remediation": "Set minlen=14 in /etc/security/pwquality.conf" + // HITL tests also carry: "reviewer", "notes" + } + ] +} +``` + +### `passed` Field Semantics + +| Value | Meaning | Report icon | +|---|---|---| +| `true` | Automated check passed | ✅ | +| `false` | Automated check failed | ❌ | +| `"review"` | Listed for human assessment (port inventory, ACL review) | 🔍 | +| `"skipped"` | Reviewer entered `skip`; not applicable to this target | ⏭️ | + +### `severity` Field Semantics + +| Value | Meaning | +|---|---| +| `critical` | Allows immediate compromise (empty passwords, world-writable sudoers) | +| `high` | Defeats a core SL2 control (no firewall, no auditd, weak password policy) | +| `medium` | Weakens a control (password aging not set, no session timeout) | +| `low` | Best-practice gap (extra listening ports, stale accounts) | + +--- + +## IEC 62443-3-3 SL2 Coverage + +### Core Linux Suites (`playbooks/suites/`) + +| Test ID | FR | SR | Description | Severity | +|---|---|---|---|---| +| IAC-01 | FR1 | SR 1.1 | No duplicate UIDs in /etc/passwd | high | +| IAC-02 | FR1 | SR 1.3 | No default/unnecessary system accounts | medium | +| IAC-03 | FR1 | SR 1.3 | No human accounts that have never logged in | low | +| IAC-04 | FR1 | SR 1.4 | No empty or trivially-weak password hashes | critical | +| IAC-05 | FR1 | SR 1.5 | Password min length >= 14 (pwquality) | high | +| IAC-06 | FR1 | SR 1.5 | >= 3 character classes required (pwquality) | medium | +| IAC-07 | FR1 | SR 1.7 | PASS_MAX_DAYS <= 90 and is set | medium | +| IAC-08 | FR1 | SR 1.7 | PASS_MIN_DAYS >= 1 | low | +| IAC-09 | FR1 | SR 1.11 | Account lockout after <= 5 failures (pam_faillock) | high | +| IAC-10 | FR1 | SR 1.6 | Password history >= 5 (pam_pwhistory) | medium | +| UC-01 | FR2 | SR 2.1 | No unrestricted NOPASSWD sudo | high | +| UC-02 | FR2 | SR 2.1 | /etc/sudoers owned root:root, mode 0440 | critical | +| UC-03 | FR2 | SR 2.5 | Shell idle timeout <= 900s (TMOUT) | medium | +| UC-04 | FR2 | SR 2.4 | Audit rules immutable (-e 2) | high | +| UC-05 | FR2 | SR 2.8 | auditd service active and enabled | high | +| UC-06 | FR2 | SR 2.8 | >= 4 critical syscall types audited | medium | +| RDF-01 | FR5 | SR 5.1 | Host-based firewall with active rules | critical | +| RDF-02 | FR5 | SR 5.1 | Default INPUT policy is DROP | high | +| RDF-03 | FR5 | SR 5.3 | No insecure legacy services (telnet, rsh, ftp) | critical | +| RDF-04 | FR5 | SR 5.3 | Listening TCP ports — review | low | + +### Additional Checks in Platform Examples + +| Platform | Example file | FR areas covered | +|---|---|---| +| Linux VM | `examples/linux_vm.yml` | FR1 (SSH hardening), FR2 (sudo logging), FR3 (sysctl, core dumps) | +| Windows Server | `examples/windows_server.yml` | FR1 (password/lockout policy), FR2 (audit policy), FR5 (firewall, Telnet) | +| Windows Client | `examples/windows_client.yml` | FR1 (domain join), FR3 (BitLocker), FR2 (screen lock), FR5 (firewall) | +| MS SQL Server | `examples/mssql_server.yml` | FR1 (auth mode, SA account, role review), FR2 (xp_cmdshell, audit) | +| VMware vSphere | `examples/vmware_vsphere.yml` | FR1 (lockdown, account lockout), FR2 (NTP), FR5 (SSH/Shell, vSwitch) | +| Hyper-V | `examples/hyperv_cluster.yml` | FR3 (SecureBoot, integration svc), FR2 (event log), FR5 (vSwitch/NIC) | +| Cisco Switch | `examples/cisco_switch.yml` | FR1 (SNMP, banner), FR2 (NTP), FR5 (SSH/Telnet, port shutdown) | +| Cisco Firewall | `examples/cisco_firewall.yml` | FR1 (IKEv2, AAA), FR2 (syslog), FR5 (Telnet, ACLs) | + +### Not Yet Implemented as Dedicated Suites + +| FR | Key SL2 Controls | Suggested Checks | +|---|---|---| +| FR3 — System Integrity | File integrity monitoring | AIDE/IMA service, `/proc/sys/kernel/kexec_load_disabled` | +| FR4 — Data Confidentiality | Encryption at rest/transit | TLS cipher audit on listening ports, LUKS/dm-crypt, SSH cipher suite | +| FR6 — Timely Response | Log forwarding, alerting | `rsyslog` remote config, auditd dispatcher, journald persistence | +| FR7 — Resource Availability | DoS protection, backup | Disk quota, systemd resource limits, backup schedule | + +--- + +## Rendering Reports + +### Python Renderer (`reports/render_report.py`) + +Zero dependencies beyond Python 3 stdlib. Two output formats: + +```bash +# Terminal box-drawing (default): +python3 reports/render_report.py reports/hostname-2026-08-14.json + +# Markdown for GitHub / GitLab: +python3 reports/render_report.py reports/hostname-2026-08-14.json --format md > REPORT.md +``` + +Terminal output groups results by FR category with a failure-detail section. +Markdown output produces GFM tables plus per-failure sections with remediation. + +### Go Renderer (`reports/render.go`) + +Requires Go >= 1.21. Uses `text/template` with an external `.gohtml` file: + +```bash +cd reports +go run render.go ../reports/hostname-2026-08-14.json report.gohtml +``` + +The template file is standalone — customise it without recompiling. Registered +template functions: + +| Function | Purpose | +|---|---| +| `passIcon` | Maps `passed` value to ✅ PASS / ❌ FAIL / ❓ MANUAL | +| `severityIcon` | Maps severity to 🔴/🟠/🟡/🟢 | +| `title` | Capitalises first letter of each word | +| `divf` | Float division for compliance rate percentage | +| `add`, `sub` | Integer arithmetic | + +Custom templates: +```bash +go run reports/render.go reports/*.json my-custom.gohtml +``` + +--- + +## Ansible Control Node — Container & QEMU VM + +Two independent deployment artifacts. The container image runs anywhere Docker +is available; the QEMU VM provides a self-contained appliance for environments +without existing Docker infrastructure. ### The Two Artifacts | Artifact | Defined by | Built with | Result | |---|---|---|---| -| **Alpine Docker Host** | `Dockerfile.alpine-host` | `./scripts/build-qemu.sh` | `output/ansible-node.qcow2` (~470MB) | -| **Ansible Control Node** | `Dockerfile.ansible` | `./scripts/build-ansible.sh` | `ansible-node` Docker image (~793MB) | +| **Alpine Docker Host** | `Dockerfile.alpine-host` | `./scripts/build-qemu.sh` | `output/ansible-node.qcow2` (~470 MB) | +| **Ansible Control Node** | `Dockerfile.ansible` | `./scripts/build-ansible.sh` | `ansible-node` Docker image (~793 MB) | + +The container image includes: Ansible 2.17, 10 collections (`ansible.windows`, +`cisco.asa`, `cisco.ios`, `cisco.nxos`, `community.vmware`, `community.general`, +`community.crypto`, `ansible.netcommon`, `ansible.utils`, `microsoft.sql`), +and all required Python libraries (`pywinrm`, `pyvmomi`, `pymssql`, `paramiko`, +`netmiko`, `ncclient`). ### Build & Launch ```bash # Prerequisites: Docker, QEMU (qemu-full), passwordless sudo for mount -# 1. Build the VM disk -./scripts/build-qemu.sh # Alpine + Docker + SSH → qcow2 - -# 2. Build the Ansible image -./scripts/build-ansible.sh # Ansible + 10 collections → Docker image -# Or push to a registry: +# 1. Build the Ansible container image +./scripts/build-ansible.sh +# Push to a registry: REGISTRY=my-registry ./scripts/build-ansible.sh --push +# 2. Build the VM disk (optional — only needed for QEMU deployment) +./scripts/build-qemu.sh # Alpine + Docker + SSH → qcow2 + # 3. Boot the VM -./scripts/run-qemu.sh # QEMU pc-q35-10.0, KVM, 1GB, 2 vCPUs +./scripts/run-qemu.sh # QEMU pc-q35-10.0, KVM, 1 GB, 2 vCPUs ``` -### How the User Interacts +### Interacting with the VM ``` QEMU VM boots in ~6 seconds │ - ├── ttyS0 (serial console) ──► auto-menu driven + ├── ttyS0 (serial console) ──► TTY menu │ ╔══════════════════════════════════════╗ │ ║ 1) Run all tests ║ │ ║ 2) Run FR1 — Auth ║ @@ -117,605 +661,100 @@ QEMU VM boots in ~6 seconds │ ║ 0) Shutdown ║ │ ╚══════════════════════════════════════╝ │ - ├── :8080 ──► Web UI (browser-based dashboard) - │ • Run buttons for each playbook - │ • Output streaming - │ • Report downloads (JSON) + ├── :8080 ──► Web UI (browser dashboard) + │ • Run buttons per playbook + │ • Live output streaming + │ • Report downloads (JSON / Markdown / PDF) │ └── :22 ──► SSH (ansible / ansible) ``` -Every option in the menu and every button in the web UI runs: -```bash -docker run --rm -it \ - -v /ansible/playbooks:/ansible/playbooks:ro \ - -v /ansible/reports:/ansible/reports \ - -v /ansible/inventory:/ansible/inventory:ro \ - ansible-node site.yml -``` - -### Architecture Diagram - -``` -Dockerfile.alpine-host Dockerfile.ansible - │ │ - ▼ ▼ -┌─────────────────────┐ ┌─────────────────────┐ -│ Alpine 3.20 │ │ Alpine 3.20 │ -│ Docker 26.1 daemon │ │ Ansible 2.17 │ -│ OpenRC (no systemd) │ │ 10 collections │ -│ tty-menu.sh │ │ pywinrm, pyvmomi, │ -│ webui.py (:8080) │ │ pymssql, paramiko, │ -│ sshd (:22) │ │ netmiko, ncclient │ -│ ~470MB qcow2 │ │ ~793MB Docker image │ -└─────────────────────┘ └─────────────────────┘ - │ │ - │ docker run ansible-node │ - └───────────────────────────────┘ - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ -┌────────┐ ┌──────────┐ ┌─────────┐ -│Windows │ │ Cisco │ │ VMware │ -│ WinRM │ │ SSH/CLI │ │ SOAP │ -└────────┘ └──────────┘ └─────────┘ -``` - -### Target Integrations - -| Target | Ansible Collection | Protocol | Python | -|---|---|---|---| -| **Windows** | `ansible.windows` 2.3 | WinRM + Kerberos | `pywinrm` 0.5 | -| **Cisco ASA** | `cisco.asa` 4.0 | SSH/CLI | `paramiko` 4.0 | -| **Cisco IOS/Catalyst** | `cisco.ios` 5.3 | SSH/CLI | `netmiko` 4.7 | -| **Cisco NX-OS** | `cisco.nxos` 5.3 | SSH/NX-API | `ncclient` 0.7 | -| **VMware vSphere** | `community.vmware` 4.3 | SOAP API | `pyvmomi` 9.1 | -| **MSSQL** | `microsoft.ad` 1.5 | TDS | `pymssql` 2.3 | -| **Linux** | built-in SSH | SSH | — | - -### Deployment Targets for the Ansible Image +### Deployment Targets for the Ansible Container | Environment | How | |---|---| -| **QEMU VM on Windows** | `docker run ansible-node` inside the Alpine Docker Host | -| **Docker Swarm** | `docker stack deploy` the `ansible-node` image directly | -| **Kubernetes** | `kubectl run` a Job or Pod with the `ansible-node` image | - -### Key Design Decisions - -| Decision | Rationale | -|---|---| -| Split: host vs. container | VM provides Docker; Ansible runs in a container. Same image on Swarm/K8s | -| Dockerfile as build system | Dependency resolution, layer caching. Dockerfile → `docker export` → qcow2 | -| `root=/dev/vda` (not LABEL) | Alpine's `nlplug-findfs` can't resolve labels on virtio-block | -| `modules=virtio_blk,ext4` on cmdline | `CONFIG_VIRTIO_BLK=m` — module not auto-detected by initramfs | -| TTY menu via `agetty -l` | Auto-launches menu instead of login prompt on serial console | -| Web UI: Python stdlib only | Zero dependencies; `http.server` + inline HTML + async JS | -| Direct `-kernel` boot (no extlinux) | QEMU loads kernel/initrd directly; no bootloader in disk image | - ---- - -## Quick Start - -### Prerequisites - -- Ansible ≥ 2.9 (core modules only: `shell`, `stat`, `copy`, `set_fact`, `debug`) -- Python ≥ 3.6 (for the Python report renderer) -- Go ≥ 1.21 (optional, for the Go template renderer) -- Target host: Linux with systemd (Debian/Ubuntu or RHEL/Rocky) - -### Run Against Localhost - -```bash -# Edit inventory.ini to add your target, or test locally: -echo "localhost ansible_connection=local" > inventory.ini - -# Run with sudo (many checks need root): -ansible-playbook -i inventory.ini playbooks/site.yml --limit localhost -K - -# Or use the wrapper script: -KEEP_SUDO=1 ./run.sh --limit localhost -K -``` - -### Render a Report - -```bash -# Terminal format (default): -python3 reports/render_report.py reports/localhost-2026-07-07.json - -# Markdown format (for GitHub/GitLab wikis): -python3 reports/render_report.py reports/localhost-2026-07-07.json --format md > REPORT.md - -# Go template (if Go is installed): -(cd reports && go run render.go ../sample-output.json report.gohtml) -``` - ---- - -## How It Works - -### The Test Pattern ("The Secret Sauce") - -Every test follows a rigid 3-step **Gather → Evaluate → Record** structure -wrapped in an Ansible `block` with `ignore_errors: yes`: - -```yaml -# ── SR 1.5: Password minimum length ─────────────────────────── - -- block: # ← 1) Wrap everything - - name: "Gather: Check pwquality minlen" # ← 2) Gather: read system state - ansible.builtin.shell: | - grep -E '^\s*minlen\s*=' /etc/security/pwquality.conf 2>/dev/null | tail -1 || echo "NOT SET" - register: _minlen # store raw output - changed_when: false # never report as "changed" - - - name: "Evaluate: IAC-05" # ← 3) Evaluate: judge pass/fail - ansible.builtin.set_fact: - test_results: "{{ test_results + [{ # append to shared list - 'test_id': 'IAC-05', - 'category': 'FR1 - Identification and Authentication Control', - 'requirement': 'SR 1.5 — Authenticator Strength', - 'description': 'Password minimum length shall be ≥ 14 characters', - 'passed': ( - _minlen.stdout | regex_search('minlen\\s*=\\s*([0-9]+)') - | regex_replace('minlen\\s*=\\s*', '') | int >= 14 - ), - 'expected': 'minlen >= 14 in /etc/security/pwquality.conf', - 'actual': _minlen.stdout | trim, - 'severity': 'high', - 'remediation': 'Set minlen=14 in /etc/security/pwquality.conf' - }] }}" - ignore_errors: yes # ← 4) NEVER abort the run -``` - -**Why this works**: `ignore_errors: yes` prevents Ansible from stopping. The -`set_fact` always executes (it never fails). The `block` ensures the gather and -evaluate steps share a scope, so `register`ed variables are available. - -### Why `block` + `ignore_errors` Instead of `failed_when` - -Using `failed_when: false` on individual tasks is tempting, but: - -| Approach | Behavior | -|---|---| -| `failed_when: false` on shell task | Shell always "succeeds"; but if the shell returns non-zero, Ansible still marks it red in output | -| `block` + `ignore_errors: yes` | Failures are captured, marked orange, but execution continues to the next block. Registered vars are still set. This is the **cleanest** pattern. | - ---- - -## JSON Output Schema - -Each test produces one entry in `test_results[]`. The complete report looks like: - -```jsonc -{ - "meta": { - "standard": "IEC 62443-3-3", // Standard being validated - "security_level": "SL2", // Target security level - "target": "ics-gateway-01", // inventory_hostname - "timestamp": "2026-07-07T16:42:00+02:00", // ISO 8601 - "executed_by": "auditor" // ansible_user_id - }, - "summary": { - "total": 15, // Total test count - "passed": 10, // passed == true - "failed": 4, // passed == false - "skipped": 1 // passed == "review" or "skipped" - }, - "by_category": [ // Results grouped by FR category - ["FR1 - ...", [{...}, {...}]], - ["FR2 — Use Control", [{...}]] - ], - "by_severity": { // Drill-down by criticality - "critical": [{...}], - "high": [{...}], - "medium": [{...}], - "low": [{...}] - }, - "failures": [{...}], // Only tests where passed == false - "results": [ // All results, ordered by execution - { - "test_id": "IAC-05", // Unique identifier (category-number) - "category": "FR1 - ...", // Which IEC 62443 FR section - "requirement": "SR 1.5 — ...", // Specific SR (System Requirement) - "description": "Password...", // Human-readable check - "passed": false, // bool | "review" | "skipped" - "expected": "minlen >= 14...", // What compliance looks like - "actual": "minlen = 8", // What we found - "severity": "high", // critical | high | medium | low - "remediation": "Set minlen=14..." // How to fix - } - ] -} -``` - -### `passed` Field Semantics - -| Value | Meaning | Icon | -|---|---|---| -| `true` | Automated check passed | ✅ | -| `false` | Automated check failed | ❌ | -| `"review"` | Requires human assessment | 🔍 | -| `"skipped"` | Not applicable to this target | ⏭️ | - -### `severity` Field Semantics - -| Value | Meaning | Examples | -|---|---|---| -| `critical` | Allows immediate compromise | Empty passwords, world-writable sudoers | -| `high` | Defeats a core SL2 control | No firewall, no auditd, weak password policy | -| `medium` | Weakens a control | Password aging not set, some unused accounts | -| `low` | Best-practice gap | Extra listening ports, stale accounts | - ---- - -## IEC 62443-3-3 SL2 Coverage - -IEC 62443-3-3 defines 7 Foundational Requirements (FRs), each with System -Requirements (SRs) at escalating Security Levels (SL1–SL4). SL2 adds to SL1. - -### Mapping of Implemented Tests - -| Test ID | FR | SR | Description | Severity | -|---|---|---|---|---| -| IAC-01 | FR1 | SR 1.1 | No duplicate UIDs in /etc/passwd | high | -| IAC-02 | FR1 | SR 1.3 | No default/unnecessary system accounts | medium | -| IAC-03 | FR1 | SR 1.3 | No human accounts that have never logged in | low | -| IAC-04 | FR1 | SR 1.4 | No empty or trivially-weak password hashes | critical | -| IAC-05 | FR1 | SR 1.5 | Password min length ≥ 14 (pwquality) | high | -| IAC-06 | FR1 | SR 1.5 | ≥ 3 character classes required (pwquality) | medium | -| IAC-07 | FR1 | SR 1.7 | PASS_MAX_DAYS ≤ 90 | medium | -| IAC-08 | FR1 | SR 1.7 | PASS_MIN_DAYS ≥ 1 | low | -| IAC-09 | FR1 | SR 1.11 | Account lockout after ≤ 5 failures (pam_faillock) | high | -| IAC-10 | FR1 | SR 1.6 | Password history ≥ 5 (pam_pwhistory) | medium | -| UC-01 | FR2 | SR 2.1 | No unrestricted NOPASSWD sudo | high | -| UC-02 | FR2 | SR 2.1 | /etc/sudoers owned root:root, mode 0440 | critical | -| UC-03 | FR2 | SR 2.5 | Shell idle timeout ≤ 900s (TMOUT) | medium | -| UC-04 | FR2 | SR 2.4 | Audit rules immutable (-e 2) | high | -| UC-05 | FR2 | SR 2.8 | auditd service active | high | -| UC-06 | FR2 | SR 2.8 | ≥ 4 critical syscall types audited | medium | -| RDF-01 | FR5 | SR 5.1 | Host-based firewall with rules | critical | -| RDF-02 | FR5 | SR 5.1 | Default INPUT policy is DROP | high | -| RDF-03 | FR5 | SR 5.3 | No insecure legacy services (telnet, rsh, ftp) | critical | -| RDF-04 | FR5 | SR 5.3 | Listening TCP ports documented | low | - -### Not Yet Implemented (FR3, FR4, FR6, FR7) - -| FR | Key SL2 Controls | Suggested Checks | -|---|---|---| -| FR3 — System Integrity | Malware protection, file integrity, secure boot | AIDE/IMA, ClamAV service, /proc/sys/kernel/secure_boot | -| FR4 — Data Confidentiality | Encryption at rest/transit | TLS version on listening ports, LUKS/dm-crypt, SSH ciphers | -| FR6 — Timely Response | Audit log forwarding, alerting | rsyslog remote config, auditd dispatcher | -| FR7 — Resource Availability | DoS protection, backup | Disk quota, CPU limits, backup cron/schedule | - ---- - -## Adding a New Test - -### Step-by-Step - -1. **Choose an FR category file** (or create a new `suites/frN_*.yml`). - -2. **Copy the test template**: - - ```yaml - # ── SR X.Y: Short description ─────────────────────────────── - - - block: - - name: "Gather: What to check" - ansible.builtin.shell: | - your-check-command-here - register: _myvar - changed_when: false - - - name: "Evaluate: XXX-NN" - ansible.builtin.set_fact: - test_results: "{{ test_results + [{ - 'test_id': 'XXX-NN', - 'category': 'FRN — Category Name', - 'requirement': 'SR X.Y — Requirement Name', - 'description': 'One-line description of the check', - 'passed': ( _myvar.stdout | trim | length > 0 ), # boolean expression - 'expected': 'What compliance looks like', - 'actual': _myvar.stdout | trim | default('NOT FOUND', true), - 'severity': 'high', # critical|high|medium|low - 'remediation': 'Command or steps to fix' - }] }}" - ignore_errors: yes - ``` - -3. **Register the suite** in `playbooks/site.yml`: - - ```yaml - - name: "Suite: FRN — Category Name" - block: - - ansible.builtin.include_tasks: suites/frN_category.yml - ignore_errors: yes - ``` - -4. **Add to the coverage table** in this README. - -### Test ID Naming Convention - -- **Prefix**: 3-letter abbreviation of the FR (e.g., `IAC`, `UC`, `RDF`, - `SYS`, `CONF`, `RESP`, `AVAIL`) -- **Number**: Sequential within that FR, padded to 2 digits (`01`, `02`, ...) -- **Examples**: `IAC-01`, `UC-12`, `RDF-05` - -### Tips for Writing Robust Checks - -| Tip | Why | -|---|---| -| Always use `changed_when: false` on gather tasks | Tests must never report as "changed" | -| Use `| trim` on shell output | Shell often returns trailing newlines | -| Use `| default('NOT FOUND', true)` for missing files | Prevents undefined-variable errors | -| Parse numbers with `| int` before comparison | String `"8"` ≠ integer 8 in Jinja2 | -| Use `| regex_search(...)` for structured config | More robust than exact-match grep | -| Test an `or` of `pam_faillock` and `pam_tally2` | Different distros use different PAM modules | - ---- - -## Rendering Reports - -### Python Renderer (`reports/render_report.py`) - -No dependencies beyond Python 3 stdlib. Two output formats: - -```bash -# Terminal box-drawing (default): -python3 reports/render_report.py reports/localhost-2026-07-07.json - -# Markdown (for wiki, PR comment, etc.): -python3 reports/render_report.py reports/localhost-2026-07-07.json --format md -``` - -**Terminal output** uses Unicode box-drawing characters (╔═╗║╚╝), groups results by -FR category, and shows a failure-detail section at the bottom. - -**Markdown output** produces a GitHub-flavored table plus per-failure sections -with remediation instructions. - -### Go Renderer (`reports/render.go`) - -Requires Go ≥ 1.21. Uses `text/template` with an external template file: - -```bash -cd reports -go run render.go sample-output.json report.gohtml -``` - -The template (`report.gohtml`) is a standalone file — you can customize it without -recompiling. Template functions: - -| Function | Purpose | -|---|---| -| `passIcon` | Maps `passed` value to ✅/❌/🔍 | -| `severityIcon` | Maps severity string to 🔴/🟠/🟡/🟢 | -| `title` | Capitalizes first letter of each word | -| `divf` | Float division for compliance rate | - -### Extending: Custom Go Templates - -Copy `report.gohtml` to `my-report.gohtml`, modify it, and run: - -```bash -go run reports/render.go reports/*.json my-report.gohtml -``` - -The template receives the full JSON document as `.` (a `map[string]any`). Access -fields with `(index . "key")` since it's untyped. +| QEMU VM (Windows host) | `docker run ansible-node` inside the Alpine Docker Host | +| Docker Swarm | `docker stack deploy` with the `ansible-node` image | +| Kubernetes | `kubectl create job` with the `ansible-node` image | +| CI/CD pipeline | `docker run --rm -v ...` in GitHub Actions / GitLab CI | --- ## File Reference -### `playbooks/site.yml` - -Entry-point playbook. Runs against `hosts: all`. Pre-tasks create the local -`reports/` directory. Each suite is included as a named task wrapped in a `block` -with `ignore_errors: yes`. The final task includes `library/report.yml` to -aggregate results. - -Variables set here: -- `report_dir: "./reports"` — where JSON output lands - -### `playbooks/suites/fr1_auth.yml` - -**FR1 — Identification and Authentication Control**. 10 tests covering: -- Duplicate UIDs (SR 1.1) -- Default/unused accounts (SR 1.3) -- Empty password hashes (SR 1.4) -- Password complexity via libpwquality (SR 1.5) -- Password history via pam_pwhistory (SR 1.6) -- Password aging via login.defs (SR 1.7) -- Account lockout via pam_faillock (SR 1.11) - -Uses: `/etc/passwd`, `/etc/shadow`, `/etc/security/pwquality.conf`, -`/etc/login.defs`, `/etc/pam.d/common-auth`, `/etc/pam.d/common-password`, -`lastlog` - -### `playbooks/suites/fr2_use_control.yml` - -**FR2 — Use Control**. 6 tests covering: -- Unrestricted sudo access (SR 2.1) -- sudoers file permissions (SR 2.1) -- Shell idle timeout via TMOUT (SR 2.5) -- auditd immutable mode (SR 2.4) -- auditd service status (SR 2.8) -- Audited syscall coverage (SR 2.8) - -Uses: `/etc/sudoers`, `/etc/sudoers.d/`, `/etc/profile`, `/etc/bash.bashrc`, -`/etc/audit/audit.rules`, `systemctl` - -### `playbooks/suites/fr5_data_flow.yml` - -**FR5 — Restricted Data Flow**. 4 tests covering: -- Host-based firewall with active rules (SR 5.1) -- Default DROP inbound policy (SR 5.1) -- Insecure legacy services disabled (SR 5.3) -- Listening port inventory for review (SR 5.3) - -Uses: `iptables`/`nft`, `systemctl`, `ss` - -### `playbooks/library/report.yml` - -**Report aggregation**. Included last by `site.yml`. Builds a `__report` dict -from the `test_results[]` list, prints a summary box to the console, and writes -`reports/-.json` to the local machine. - -Key computed fields: -- `summary.total/passed/failed/skipped` — basic counts -- `by_category` — results grouped by FR category string -- `by_severity` — results grouped into critical/high/medium/low buckets -- `failures` — only tests where `passed == false` - -### `reports/render_report.py` - -Python-based report renderer. Two modes: -- `terminal` (default) — Unicode box-drawing, grouped by category, failure details -- `md` — GitHub-flavored Markdown tables - -Functions: -- `pass_icon(v)` → ✅/❌/🔍/⏭️ based on bool/string -- `severity_icon(s)` → 🔴/🟠/🟡/🟢 -- `render_terminal(report)` — writes to stdout -- `render_markdown(report)` — writes to stdout - -### `reports/render.go` - -Go-based report renderer. Uses `text/template` and `golang.org/x/text/cases`. -Loads an external `.gohtml` template file so templates can be customized without -rebuilding. Registers template functions for pass/severity icons and arithmetic. - -### `reports/report.gohtml` - -Go template producing a terminal box-drawing report. Iterates over `results`, -renders a summary header, a per-result listing with icons, and a failure-detail -section. Uses template functions registered by `render.go`. - -### `reports/sample-output.json` - -A hand-crafted example JSON report for testing the renderers without running -Ansible. Contains 15 results (10 passed, 4 failed, 1 review) across FR1 and FR2. -Useful for CI validation of the report rendering pipeline. - -### `Dockerfile.alpine-host` - -Alpine Linux 3.20 with Docker daemon, SSH, TTY menu, and web UI. Used as the -blueprint for the QEMU-bootable VM disk. Built via `scripts/build-qemu.sh`. -Installs: `alpine-base`, `linux-virt`, `docker`, `openssh-server`, `python3`. -Provides: serial console menu (`scripts/tty-menu.sh`) and web UI (`webui/app.py`). - -### `Dockerfile.ansible` - -Ansible control node container image. Installs `ansible` + `sshpass` + 10 -Ansible collections + Python packages for Windows (pywinrm), Cisco (paramiko, -netmiko, ncclient), VMware (pyvmomi), and MSSQL (pymssql). Built via -`scripts/build-ansible.sh`. Deployable on Docker Swarm, Kubernetes, or inside -the Alpine Docker Host VM. - -### `scripts/build-qemu.sh` - -Converts `Dockerfile.alpine-host` into a bootable QEMU disk. Steps: Docker build -→ export rootfs → extract kernel + initramfs → create ext4 disk → convert to -qcow2. Outputs: `output/ansible-node.qcow2`, `output/vmlinuz-virt`, -`output/initramfs-virt`. - -### `scripts/build-ansible.sh` - -Builds the `ansible-node` Docker image from `Dockerfile.ansible`. Reports the -image size and layers. With `--push` and `REGISTRY` set, pushes to a container -registry for deployment. - -### `scripts/run-qemu.sh` - -Launches the Alpine Docker Host VM on QEMU `pc-q35-10.0`. Uses direct kernel -boot (`-kernel` / `-initrd`), virtio devices, user-mode networking with port -forwards (SSH on `:2222`, web UI on `:8090`). Supports `--gui`, `--vnc`, and -`--debug` modes. - -### `scripts/tty-menu.sh` - -Serial console menu script. Launched automatically by `agetty -l` on `ttyS0`. -Options: run individual playbooks, view reports, shell into the Ansible -container, shell into the Docker host, shutdown VM. All test options run -`docker run ansible-node` with shared volumes. - -### `webui/app.py` - -Minimal web dashboard for the **Alpine Docker Host VM**. Python 3 stdlib only, -zero dependencies. Serves on port 8080. Features: dark-themed UI with run buttons -per playbook, async output streaming, JSON report downloads. Runs playbooks -via `docker run ansible-node`. - -### `webui/container-app.py` - -Web dashboard for the **Ansible container** itself. Serves on port 8080. Features: -dark-themed UI, run buttons, async output, **JSON / Markdown / PDF export**. -PDF generation via `fpdf2` (pure Python, zero system deps). Markdown via bundled -`render_report.py`. Started automatically when the container runs with no arguments. - -### `scripts/entrypoint.sh` - -Container entrypoint. If invoked with no arguments, starts the web UI -(`container-app.py`). If arguments are given, passes them directly to -`ansible-playbook`. Enables both `docker run -p 8080:8080 ansible-node` and -`docker run ansible-node site.yml`. - -### `inventory.ini` - -Standard Ansible inventory. Structure: -```ini -[all] -localhost ansible_connection=local - -[ics_assets] -# Add real targets here -``` - -### `run.sh` - -End-to-end wrapper that: -1. Runs `ansible-playbook` with the given arguments -2. Finds the latest JSON report in `reports/` -3. Renders it via `render_report.py` (or `render.go` if available) -4. Falls back to a Python one-liner summary if neither is available +| Path | Purpose | +|---|---| +| `playbooks/site.yml` | Entry-point for Linux targets; orchestrates FR1, FR2, FR5 suites | +| `playbooks/suites/fr1_auth.yml` | 10 FR1 authentication tests for Linux | +| `playbooks/suites/fr2_use_control.yml` | 6 FR2 use-control tests for Linux | +| `playbooks/suites/fr5_data_flow.yml` | 4 FR5 data-flow tests for Linux | +| `playbooks/library/report.yml` | Aggregates `test_results[]` → JSON file on localhost | +| `playbooks/examples/linux_vm.yml` | Standalone example: Linux SSH + kernel hardening | +| `playbooks/examples/windows_server.yml` | Standalone example: Windows Server via WinRM | +| `playbooks/examples/windows_client.yml` | Standalone example: Windows Client / HMI workstation | +| `playbooks/examples/mssql_server.yml` | Standalone example: SQL Server via WinRM + Invoke-Sqlcmd | +| `playbooks/examples/vmware_vsphere.yml` | Standalone example: ESXi via vSphere API | +| `playbooks/examples/hyperv_cluster.yml` | Standalone example: Hyper-V via WinRM | +| `playbooks/examples/cisco_switch.yml` | Standalone example: Cisco IOS via network_cli | +| `playbooks/examples/cisco_firewall.yml` | Standalone example: Cisco ASA via network_cli | +| `playbooks/templates/test_automated.yml` | Copy-and-fill template: shell gather → evaluate | +| `playbooks/templates/test_file_check.yml` | Copy-and-fill template: `ansible.builtin.stat` | +| `playbooks/templates/test_service_check.yml` | Copy-and-fill template: `service_facts` | +| `playbooks/templates/test_hitl.yml` | Copy-and-fill template: `pause` + `delegate_to: localhost` | +| `reports/render_report.py` | Python renderer: terminal box-drawing and Markdown output | +| `reports/render.go` | Go renderer: `text/template` on external `.gohtml` | +| `reports/report.gohtml` | Go template producing the terminal box-drawing report | +| `reports/go.mod` | Go module (requires `golang.org/x/text`) | +| `reports/sample-output.json` | Hand-crafted example report for offline renderer testing | +| `inventory.ini` | Ansible inventory with all platform groups and connection vars | +| `run.sh` | End-to-end wrapper: run ansible → find latest JSON → render | +| `Dockerfile.ansible` | Ansible control node image (all collections + Python libs) | +| `Dockerfile.alpine-host` | Alpine VM image (Docker daemon + TTY menu + web UI) | +| `scripts/build-ansible.sh` | Builds `ansible-node` Docker image | +| `scripts/build-qemu.sh` | Converts `Dockerfile.alpine-host` → bootable qcow2 | +| `scripts/run-qemu.sh` | Launches the Alpine VM in QEMU | +| `scripts/tty-menu.sh` | Serial console menu (launched by `agetty -l` on ttyS0) | +| `webui/container-app.py` | Web UI inside the Ansible container (JSON/Markdown/PDF export) | +| `webui/app.py` | Web UI for the Alpine Docker Host VM | +| `scripts/entrypoint.sh` | Container entrypoint: web UI if no args, else `ansible-playbook` | --- ## Design Decisions & Tradeoffs -### Why Ansible and Not a Dedicated Compliance Scanner? - | Decision | Rationale | |---|---| -| **Ansible** | Already deployed in most OT environments. No new agent, no new approval. | -| **Shell-based checks** | `shell` module is the most flexible. We trade idempotence for expressiveness. `changed_when: false` keeps it clean. | -| **Inline `set_fact`** vs custom module | Custom Ansible modules require Python on the control node. Inline facts work everywhere. Less code to maintain. | -| **`test_results[]` list** vs file-per-test | A single growing list is simpler than per-file concatenation. At 100+ tests the memory footprint is negligible (~50KB). | -| **`ignore_errors` at block level** vs task level | Block-level isolates failures cleanly. A failed gather still reaches evaluate. | -| **JSON as canonical output** | Machine-readable, schema-validatable, ingestible by SIEM/SOAR/Jira. | -| **Go templates for rendering** | `text/template` is standard, fast, and supports external template files for customization without recompilation. | +| **Ansible** over dedicated scanners | Already deployed in most OT environments. No new agent, no new approval process. | +| **Shell-based checks** for Linux | `shell` module is the most flexible. `changed_when: false` keeps runs clean. | +| **Native modules** for Windows/Cisco | `win_security_policy`, `win_service_info`, `ios_command`, `vmware_host_lockdown_info` — typed return values avoid brittle text parsing. | +| **`delegate_to: localhost` for VMware** | vSphere API is consumed from the control node; no SSH to ESXi. | +| **`pause` for HITL** | Ansible-native, no custom tooling. `delegate_to: localhost` ensures the prompt always reaches the operator regardless of the remote target. | +| **Inline `set_fact`** vs custom module | Custom modules require Python on the control node. Inline facts work everywhere and are easier to audit. | +| **`test_results[]` list** vs file-per-test | A single growing list is simpler than per-file concatenation. At 100+ tests the memory footprint is negligible. | +| **JSON as canonical output** | Machine-readable, schema-validatable, ingestible by SIEM/SOAR/Jira/ServiceNow. | +| **Go templates for rendering** | `text/template` supports external template files so reports can be restyled without modifying Go code. | ### Known Limitations -1. **Shell-heavy**: Tests depend on shell commands. Different distros may need - different check commands (e.g., `apt` vs `rpm` package verification). Mitigate - with `when: ansible_os_family == 'Debian'` variants. +1. **Shell-heavy for Linux**: Linux checks depend on shell commands. Different + distros may use different paths or tools. Mitigate with + `when: ansible_os_family == 'Debian'` variants. -2. **No built-in Windows support**: Currently Linux-only. Windows checks would - need `win_shell`/`win_regedit` modules and a separate suite. +2. **No diff / drift detection**: Each run is independent. To detect configuration + drift between runs, diff two JSON reports externally (`jd`, `diff`, + or a time-series database). -3. **No diff/drift detection**: Each run is independent. To detect drift between - runs, diff two JSON reports externally (`jd`, `diff`, or a time-series DB). +3. **No CI exit code**: `ansible-playbook` exits 0 unless a task fails without + `ignore_errors`. For pipeline gates, parse `summary.failed` from the JSON + report and exit non-zero if `> 0`. -4. **No immediate pass/fail exit code**: `ansible-playbook` always exits 0 unless - a task fails without `ignore_errors`. For CI integration, parse the JSON - summary and use `--format json` output. +4. **HITL tests block automation**: Any playbook containing HITL tests requires + an interactive terminal. Keep HITL tests in separate playbooks or use Ansible + tags to separate them from fully-automated runs. -5. **Scalability at 500+ targets**: Running against large inventories is fine - (Ansible's strength), but the JSON file-per-host can be unwieldy. Consider - post-processing into a single aggregated report. +5. **Scalability at 500+ targets**: Multi-host runs work well, but one JSON file + per host can be unwieldy. Consider post-processing into a single aggregated + report. --- @@ -724,20 +763,23 @@ End-to-end wrapper that: | Tool | Type | Pros | Cons | |---|---|---|---| | **Inspec** | Ruby DSL, Chef ecosystem | Rich compliance profiles, CIS/STIG built-in | Ruby runtime; less common in OT | -| **Goss** | YAML config, Go binary | Fast, simple, YAML-based | No native IEC mapping; limited to local checks | -| **OpenSCAP** | XML-based, SCAP standard | NIST/STIG aligned, XCCDF/OVAL | Heavy, complex, US-govt focused | -| **Lynis** | Shell script, single binary | Broad Linux coverage, professional reports | Non-extensible output format | -| **This project** | Ansible + JSON + Go/Python templates | Zero new agents, customizable, IEC 62443 mapped | Requires Ansible; shell-dependent checks | +| **Goss** | YAML config, Go binary | Fast, simple | No native IEC mapping; local checks only | +| **OpenSCAP** | XML/SCAP standard | NIST/STIG aligned, XCCDF/OVAL | Heavy, complex, US-govt focused, Linux-only | +| **Lynis** | Shell script | Broad Linux coverage | Non-extensible output; Linux-only | +| **This project** | Ansible + JSON + Go/Python | Zero new agents; multi-platform; IEC 62443 mapped; HITL support | Requires Ansible; shell-dependent Linux checks | --- ## Roadmap -- [ ] **FR3 — System Integrity**: File integrity (AIDE/IMA), malware scanner status, secure boot, /tmp noexec -- [ ] **FR4 — Data Confidentiality**: TLS version/cipher audit on listening ports, disk encryption, SSH hardening -- [ ] **FR6 — Timely Response**: rsyslog remote forwarding, auditd dispatcher, journald persistent storage -- [ ] **FR7 — Resource Availability**: Disk quotas, CPU/memory limits, backup schedule verification -- [ ] **Aggregated multi-host report**: Single HTML/PDF report across all inventory hosts -- [ ] **GitLab CI / GitHub Actions integration**: Pipeline stage that runs playbook + posts markdown report as PR comment -- [ ] **Custom Ansible module**: Replace shell-based checks with a native `iec62443_test` module for cleaner code -- [ ] **CIS Benchmark mapping**: Dual-map tests to both IEC 62443-3-3 and CIS distribution benchmarks +- [x] FR1, FR2, FR5 core suites for Linux +- [x] Multi-platform examples: Windows, MSSQL, VMware, Hyper-V, Cisco IOS, Cisco ASA +- [x] Human-in-the-Loop test pattern with `ansible.builtin.pause` +- [x] Four copy-and-fill test templates (shell, stat, service_facts, HITL) +- [ ] **FR3 suite**: File integrity (AIDE/IMA), malware scanner status, secure boot, `/tmp noexec` +- [ ] **FR4 suite**: TLS version/cipher audit, disk encryption (LUKS), SSH cipher hardening +- [ ] **FR6 suite**: rsyslog remote forwarding, auditd dispatcher, journald persistent storage +- [ ] **FR7 suite**: Disk quotas, CPU/memory limits, backup schedule verification +- [ ] **Aggregated multi-host report**: Single HTML/PDF across all inventory hosts +- [ ] **CI/CD integration**: GitHub Actions / GitLab CI pipeline with Markdown report posted as PR comment +- [ ] **CIS Benchmark dual-mapping**: Each test maps to both IEC 62443-3-3 SR and CIS Benchmark control