262 lines
14 KiB
YAML
262 lines
14 KiB
YAML
---
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# IEC 62443-3-3 SL2 — Hyper-V Cluster Node Compliance
|
|
#
|
|
# Target type : Windows Server Hyper-V hosts (standalone or cluster nodes)
|
|
# Connection : WinRM — same as windows_server.yml
|
|
# Collections : ansible.windows
|
|
# Python pkg : pywinrm
|
|
#
|
|
# Inventory group : hyperv_hosts (see assets.yml)
|
|
#
|
|
# Run:
|
|
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/hyperv_cluster.yml
|
|
#
|
|
# This playbook runs two layers of checks:
|
|
# Host layer — Windows Server hardening (same as windows_server.yml)
|
|
# Hyper-V layer — VM configuration, vSwitch isolation, secure boot
|
|
#
|
|
# PowerShell modules used:
|
|
# Hyper-V — built-in on all Hyper-V hosts
|
|
# FailoverClusters — for cluster-aware checks (if applicable)
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
- name: "IEC 62443-3-3 SL2 — Hyper-V Cluster Node Compliance"
|
|
hosts: hyperv_hosts
|
|
gather_facts: yes
|
|
vars:
|
|
report_dir: "../../reports"
|
|
|
|
pre_tasks:
|
|
- name: "Ensure report directory exists"
|
|
ansible.builtin.file:
|
|
path: "{{ report_dir }}"
|
|
state: directory
|
|
mode: "0755"
|
|
delegate_to: localhost
|
|
run_once: true
|
|
|
|
tasks:
|
|
|
|
# ── FR3 · SR 3.4: VMs using Generation 2 (UEFI + Secure Boot) ────────────
|
|
# Gen 2 VMs support UEFI, Secure Boot, and vTPM. Gen 1 cannot.
|
|
|
|
- block:
|
|
- name: "Gather: VM list with generation and Secure Boot state"
|
|
ansible.windows.win_shell: |
|
|
@(Get-VM | Where-Object { $_.State -ne 'Off' -or $true } |
|
|
ForEach-Object {
|
|
$sb = $false
|
|
try { $sb = (Get-VMFirmware -VM $_ -ErrorAction Stop).SecureBootEnabled } catch {}
|
|
[PSCustomObject]@{
|
|
Name = $_.Name
|
|
Generation = $_.Generation
|
|
State = $_.State.ToString()
|
|
SecureBoot = $sb
|
|
}
|
|
}) | ConvertTo-Json -AsArray -Compress
|
|
register: _vm_list
|
|
|
|
- name: "Evaluate: HV-SI-01 — All VMs use Generation 2 with Secure Boot"
|
|
ansible.builtin.set_fact:
|
|
_vms: "{{ _vm_list.stdout | from_json }}"
|
|
|
|
- name: "Evaluate: HV-SI-01 — record result"
|
|
ansible.builtin.set_fact:
|
|
test_results: "{{ test_results | default([]) + [{
|
|
'test_id': 'HV-SI-01',
|
|
'category': 'FR3 — System Integrity',
|
|
'requirement': 'SR 3.4 — Software and Information Integrity',
|
|
'description': 'All VMs shall use Generation 2 (UEFI) with Secure Boot enabled',
|
|
'passed': (
|
|
_vms | length > 0 and
|
|
(_vms | rejectattr('Generation', 'equalto', 2) | list | length == 0) and
|
|
(_vms | selectattr('SecureBoot', 'equalto', false) | list | length == 0)
|
|
),
|
|
'expected': 'All VMs: Generation=2, SecureBoot=true',
|
|
'actual': 'Gen1: ' + (_vms | rejectattr('Generation', 'equalto', 2) | map(attribute='Name') | join(', ') | default('none', true))
|
|
+ ' | SecureBoot off: ' + (_vms | selectattr('SecureBoot', 'equalto', false) | map(attribute='Name') | join(', ') | default('none', true)),
|
|
'severity': 'high',
|
|
'remediation': 'Convert Gen1 VMs to Gen2 at next maintenance window; Set-VMFirmware <VMName> -EnableSecureBoot On'
|
|
}] }}"
|
|
ignore_errors: yes
|
|
|
|
# ── FR5 · SR 5.2: Virtual switch types — no external switch for ICS VMs ──
|
|
|
|
- block:
|
|
- name: "Gather: Virtual switch list with type and bound adapters"
|
|
ansible.windows.win_shell: |
|
|
@(Get-VMSwitch | Select-Object Name, SwitchType, AllowManagementOS,
|
|
@{N='NetAdapterNames';E={ ($_ | Get-VMSwitchTeam -ErrorAction SilentlyContinue).NetAdapterNames -join ',' }}) |
|
|
ConvertTo-Json -AsArray -Compress
|
|
register: _vswitches
|
|
|
|
- name: "Evaluate: HV-RDF-01 — No ICS VM on switch shared with management OS"
|
|
ansible.builtin.set_fact:
|
|
_sw_json: "{{ _vswitches.stdout | from_json }}"
|
|
|
|
- name: "Evaluate: HV-RDF-01 — External switches with AllowManagementOS=true"
|
|
ansible.builtin.set_fact:
|
|
test_results: "{{ test_results + [{
|
|
'test_id': 'HV-RDF-01',
|
|
'category': 'FR5 — Restricted Data Flow',
|
|
'requirement': 'SR 5.2 — Zone Boundary Protection',
|
|
'description': 'External vSwitches shared with the management OS shall not carry ICS VM traffic',
|
|
'passed': 'review',
|
|
'expected': 'ICS VMs connected to Private or Internal switches only',
|
|
'actual': 'External switches with AllowManagementOS=true: '
|
|
+ (_sw_json | selectattr('SwitchType', 'equalto', 'External')
|
|
| selectattr('AllowManagementOS')
|
|
| map(attribute='Name') | join(', ') | default('none', true)),
|
|
'severity': 'critical',
|
|
'remediation': 'Assign ICS VMs to a dedicated Internal vSwitch; disable AllowManagementOS on ICS switches'
|
|
}] }}"
|
|
ignore_errors: yes
|
|
|
|
# ── FR2 · SR 2.8: Hyper-V audit logging — VM connect activity ────────────
|
|
|
|
- block:
|
|
- name: "Gather: Hyper-V audit log entries (last 50 events)"
|
|
ansible.windows.win_shell: |
|
|
$events = Get-WinEvent -LogName 'Microsoft-Windows-Hyper-V-VMMS-Admin' `
|
|
-MaxEvents 50 -ErrorAction SilentlyContinue
|
|
$count = if ($events) { $events.Count } else { 0 }
|
|
[PSCustomObject]@{
|
|
LogExists = [bool](Get-WinEvent -ListLog 'Microsoft-Windows-Hyper-V-VMMS-Admin' -ErrorAction SilentlyContinue)
|
|
EventCount = $count
|
|
} | ConvertTo-Json -Compress
|
|
register: _hv_audit
|
|
|
|
- name: "Evaluate: HV-UC-01 — Hyper-V admin event log active"
|
|
ansible.builtin.set_fact:
|
|
_hv_audit_json: "{{ _hv_audit.stdout | from_json }}"
|
|
|
|
- name: "Evaluate: HV-UC-01 — record result"
|
|
ansible.builtin.set_fact:
|
|
test_results: "{{ test_results + [{
|
|
'test_id': 'HV-UC-01',
|
|
'category': 'FR2 — Use Control',
|
|
'requirement': 'SR 2.8 — Auditable Events',
|
|
'description': 'Hyper-V VMMS Admin event log shall be present and collecting events',
|
|
'passed': (_hv_audit_json.LogExists | bool),
|
|
'expected': 'Microsoft-Windows-Hyper-V-VMMS-Admin log exists',
|
|
'actual': 'LogExists=' + (_hv_audit_json.LogExists | string) + ', RecentEvents=' + (_hv_audit_json.EventCount | string),
|
|
'severity': 'medium',
|
|
'remediation': 'Enable the Hyper-V VMMS Admin event log via Event Viewer or wevtutil'
|
|
}] }}"
|
|
ignore_errors: yes
|
|
|
|
# ── FR3 · SR 3.2: VM integration services version ─────────────────────────
|
|
# Outdated integration services can expose VMs to vulnerabilities.
|
|
|
|
- block:
|
|
- name: "Gather: VM integration services version summary"
|
|
ansible.windows.win_shell: |
|
|
@(Get-VM | Where-Object { $_.State -eq 'Running' } |
|
|
ForEach-Object {
|
|
$vmics = Get-VMIntegrationService -VM $_ |
|
|
Where-Object { -not $_.Enabled }
|
|
[PSCustomObject]@{
|
|
VMName = $_.Name
|
|
DisabledServices = ($vmics | Select-Object -ExpandProperty Name) -join ', '
|
|
DisabledCount = $vmics.Count
|
|
}
|
|
}) | ConvertTo-Json -AsArray -Compress
|
|
register: _ics_versions
|
|
|
|
- name: "Evaluate: HV-SI-02 — All critical integration services enabled"
|
|
ansible.builtin.set_fact:
|
|
_ics_json: "{{ _ics_versions.stdout | from_json }}"
|
|
|
|
- name: "Evaluate: HV-SI-02 — record result"
|
|
ansible.builtin.set_fact:
|
|
test_results: "{{ test_results + [{
|
|
'test_id': 'HV-SI-02',
|
|
'category': 'FR3 — System Integrity',
|
|
'requirement': 'SR 3.2 — Malicious Code Protection',
|
|
'description': 'All running VMs shall have Hyper-V Integration Services fully enabled',
|
|
'passed': (
|
|
_ics_json | selectattr('DisabledCount', 'greaterthan', 0) | list | length == 0
|
|
),
|
|
'expected': 'No disabled integration services on any running VM',
|
|
'actual': (
|
|
'VMs with disabled services: '
|
|
+ (_ics_json | selectattr('DisabledCount', 'greaterthan', 0) | map(attribute='VMName') | join(', ') | default('none', true))
|
|
),
|
|
'severity': 'medium',
|
|
'remediation': 'Enable-VMIntegrationService -VMName <name> -Name "Guest Service Interface"'
|
|
}] }}"
|
|
ignore_errors: yes
|
|
|
|
# ── HITL · FR5 · SR 5.2: Physical host network cable review ──────────────
|
|
# Confirm management NIC and ICS NIC are physically separate cables/switches.
|
|
|
|
- block:
|
|
- name: "Gather: [HITL] Physical network adapter list"
|
|
ansible.windows.win_shell: |
|
|
@(Get-NetAdapter | Where-Object { $_.Status -ne 'Not Present' } |
|
|
Select-Object Name, InterfaceDescription, Status, LinkSpeed, MacAddress) |
|
|
ConvertTo-Json -AsArray -Compress
|
|
register: _net_adapters
|
|
|
|
- name: "Display: [HITL] HV-RDF-HITL-01 — Physical NIC segregation"
|
|
ansible.builtin.debug:
|
|
msg: |
|
|
══════════════════════════════════════════════════════════════
|
|
MANUAL REVIEW REQUIRED · HV-RDF-HITL-01 · {{ inventory_hostname }}
|
|
══════════════════════════════════════════════════════════════
|
|
Requirement : SR 5.2 — Zone Boundary Protection
|
|
Check : Physical NICs for ICS vSwitch are on a separate
|
|
physical switch from the management/IT network
|
|
|
|
Detected network adapters
|
|
─────────────────────────
|
|
{% for nic in _net_adapters.stdout | from_json %}
|
|
{{ nic.Name }} | {{ nic.InterfaceDescription }} | {{ nic.Status }} | {{ nic.LinkSpeed }}
|
|
{% endfor %}
|
|
|
|
Verify physically:
|
|
- Which adapters are bound to the ICS vSwitch?
|
|
- Do those cables go to a different physical switch than management NICs?
|
|
══════════════════════════════════════════════════════════════
|
|
|
|
- name: "Prompt: HV-RDF-HITL-01 — verdict for {{ inventory_hostname }}"
|
|
ansible.builtin.pause:
|
|
prompt: |
|
|
Are ICS vSwitch uplink NICs physically separated (different switch) from management NICs?
|
|
Enter verdict [pass / fail / skip]:
|
|
register: _hitl_nic_verdict
|
|
delegate_to: localhost
|
|
|
|
- name: "Prompt: HV-RDF-HITL-01 — notes on failure"
|
|
ansible.builtin.pause:
|
|
prompt: "Describe the cabling gap (e.g. 'ICS and management share same ToR switch'):"
|
|
register: _hitl_nic_notes
|
|
delegate_to: localhost
|
|
when: _hitl_nic_verdict.user_input | lower | trim in ['fail', 'f']
|
|
|
|
- name: "Evaluate: HV-RDF-HITL-01"
|
|
ansible.builtin.set_fact:
|
|
test_results: "{{ test_results + [{
|
|
'test_id': 'HV-RDF-HITL-01',
|
|
'category': 'FR5 — Restricted Data Flow',
|
|
'requirement': 'SR 5.2 — Zone Boundary Protection',
|
|
'description': 'ICS vSwitch uplink NICs shall be physically separate from management network NICs',
|
|
'passed': (
|
|
'skipped' if (_hitl_nic_verdict.user_input | lower | trim in ['skip', 's', ''])
|
|
else (_hitl_nic_verdict.user_input | lower | trim in ['pass', 'p'])
|
|
),
|
|
'expected': 'Separate physical cables and switches for ICS and management traffic',
|
|
'actual': 'Adapters found: ' + (_net_adapters.stdout | from_json | map(attribute='Name') | join(', ')),
|
|
'severity': 'critical',
|
|
'remediation': 'Install dedicated NICs for ICS vSwitch and connect to isolated physical switch',
|
|
'reviewer': ansible_user_id,
|
|
'notes': (_hitl_nic_notes.user_input | trim) if _hitl_nic_notes is defined else ''
|
|
}] }}"
|
|
ignore_errors: yes
|
|
|
|
# ── Generate report ────────────────────────────────────────────────────────
|
|
|
|
- name: "Generate compliance report"
|
|
ansible.builtin.include_tasks: ../library/report.yml
|