Files
ansible-testing/methodologies/ansible/playbooks/examples/windows_client.yml
T

247 lines
13 KiB
YAML

---
# ══════════════════════════════════════════════════════════════════════════════
# IEC 62443-3-3 SL2 — Windows Client / Workstation Compliance
#
# Target type : Windows 10 / 11 workstations, operator HMI stations
# Connection : WinRM — same as windows_server.yml
# Collections : ansible.windows
# Python pkg : pywinrm
#
# Inventory group : windows_clients (see assets.yml)
#
# Run:
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/windows_client.yml
#
# Notes:
# - Operator HMI workstations often run as local accounts (not domain-joined).
# The domain_check HITL test flags this for reviewer attention.
# - BitLocker status requires the Hyper-V / TPM chip; VMs may legitimately
# fail WIN-CLI-02 if they are not TPM-enabled.
# ══════════════════════════════════════════════════════════════════════════════
- name: "IEC 62443-3-3 SL2 — Windows Client Compliance"
hosts: windows_clients
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:
# ── FR1 · SR 1.3: Domain membership ───────────────────────────────────────
# Domain-joined workstations inherit password and lockout policy via GPO.
# Standalone / local-account machines need local policy verified separately.
- block:
- name: "Gather: Domain membership status"
ansible.windows.win_shell: |
$cs = Get-WmiObject -Class Win32_ComputerSystem
[PSCustomObject]@{
PartOfDomain = $cs.PartOfDomain
Domain = if ($cs.PartOfDomain) { $cs.Domain } else { 'WORKGROUP' }
} | ConvertTo-Json -Compress
register: _domain_info
- name: "Evaluate: WIN-CLI-01 — Workstation is domain-joined"
ansible.builtin.set_fact:
test_results: "{{ test_results | default([]) + [{
'test_id': 'WIN-CLI-01',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.3 — Account Management',
'description': 'Workstations shall be domain-joined for centralised identity management',
'passed': ((_domain_info.stdout | from_json).PartOfDomain | bool),
'expected': 'PartOfDomain = true',
'actual': 'Domain = ' + (_domain_info.stdout | from_json).Domain,
'severity': 'medium',
'remediation': 'Join workstation to Active Directory domain'
}] }}"
ignore_errors: yes
# ── FR3 · SR 3.4: BitLocker full-disk encryption ──────────────────────────
- block:
- name: "Gather: BitLocker protection status on OS drive"
ansible.windows.win_shell: |
$vol = Get-BitLockerVolume -MountPoint $env:SystemDrive -ErrorAction SilentlyContinue
if ($vol) {
[PSCustomObject]@{
ProtectionStatus = $vol.ProtectionStatus.ToString()
EncryptionMethod = $vol.EncryptionMethod.ToString()
VolumeStatus = $vol.VolumeStatus.ToString()
} | ConvertTo-Json -Compress
} else {
'{"ProtectionStatus":"NotFound","EncryptionMethod":"None","VolumeStatus":"None"}'
}
register: _bitlocker
- name: "Evaluate: WIN-CLI-02 — BitLocker enabled on OS drive"
ansible.builtin.set_fact:
_bl: "{{ _bitlocker.stdout | from_json }}"
- name: "Evaluate: WIN-CLI-02 — record result"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-CLI-02',
'category': 'FR3 — System Integrity',
'requirement': 'SR 3.4 — Software and Information Integrity',
'description': 'OS drive shall be protected with BitLocker full-disk encryption',
'passed': (_bl.ProtectionStatus == 'On'),
'expected': 'ProtectionStatus = On',
'actual': 'ProtectionStatus = ' + _bl.ProtectionStatus + ', Method = ' + _bl.EncryptionMethod,
'severity': 'high',
'remediation': 'Enable-BitLocker -MountPoint C: -RecoveryPasswordProtector -EncryptionMethod XtsAes256'
}] }}"
ignore_errors: yes
# ── FR2 · SR 2.5: Screen lock timeout (registry-based check) ─────────────
# GPO sets HKLM\Software\Policies\Microsoft\Windows\Personalization\ScreenSaveTimeOut
# or the legacy HKCU path. We check the machine-level policy value.
- block:
- name: "Gather: Screen saver timeout registry value (machine policy)"
ansible.windows.win_reg_stat:
path: HKLM:\Software\Policies\Microsoft\Windows\Control Panel\Desktop
name: ScreenSaveTimeOut
register: _screensaver_timeout
- name: "Gather: Screen saver active registry value"
ansible.windows.win_reg_stat:
path: HKLM:\Software\Policies\Microsoft\Windows\Control Panel\Desktop
name: ScreenSaveActive
register: _screensaver_active
- name: "Evaluate: WIN-CLI-03 — Screen lock timeout ≤ 900 seconds"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-CLI-03',
'category': 'FR2 — Use Control',
'requirement': 'SR 2.5 — Session Lock',
'description': 'Workstation shall lock after ≤ 900 seconds (15 min) of inactivity',
'passed': (
_screensaver_active.exists and
(_screensaver_active.value | string == '1') and
_screensaver_timeout.exists and
(_screensaver_timeout.value | int > 0) and
(_screensaver_timeout.value | int <= 900)
),
'expected': 'ScreenSaveActive=1, ScreenSaveTimeOut ≤ 900',
'actual': (
'ScreenSaveActive=' + (_screensaver_active.value | default('NOT SET') | string)
+ ', ScreenSaveTimeOut=' + (_screensaver_timeout.value | default('NOT SET') | string)
),
'severity': 'medium',
'remediation': 'Apply GPO: Computer Configuration → Admin Templates → Control Panel → Personalization → Screen saver timeout = 900'
}] }}"
ignore_errors: yes
# ── FR5 · SR 5.1: Windows Firewall on Public profile ─────────────────────
# Clients in the ICS zone should enforce the Public profile firewall.
- block:
- name: "Gather: Public firewall profile state and default inbound action"
ansible.windows.win_shell: |
Get-NetFirewallProfile -Name Public |
Select-Object Name, Enabled, DefaultInboundAction |
ConvertTo-Json -Compress
register: _pub_fw
- name: "Evaluate: WIN-CLI-04 — Public firewall profile blocks inbound"
ansible.builtin.set_fact:
_pub_fw_json: "{{ _pub_fw.stdout | from_json }}"
- name: "Evaluate: WIN-CLI-04 — record result"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-CLI-04',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.1 — Network Segmentation',
'description': 'Public firewall profile shall be enabled with default inbound Block',
'passed': (
_pub_fw_json.Enabled | bool and
_pub_fw_json.DefaultInboundAction == 'Block'
),
'expected': 'Public profile: Enabled=True, DefaultInboundAction=Block',
'actual': 'Public: Enabled=' + (_pub_fw_json.Enabled | string) + ', DefaultInboundAction=' + _pub_fw_json.DefaultInboundAction,
'severity': 'high',
'remediation': 'Set-NetFirewallProfile -Name Public -Enabled True -DefaultInboundAction Block'
}] }}"
ignore_errors: yes
# ── HITL · FR1 · SR 1.3: Physical access and USB port controls ────────────
# Cannot be verified remotely; requires physical inspection or policy review.
- block:
- name: "Gather: [HITL] USB storage device policy registry"
ansible.windows.win_reg_stat:
path: HKLM:\SYSTEM\CurrentControlSet\Services\UsbStor
name: Start
register: _usb_stor
- name: "Display: [HITL] WIN-CLI-HITL-01 — USB storage and physical access"
ansible.builtin.debug:
msg: |
══════════════════════════════════════════════════════════════
MANUAL REVIEW REQUIRED · WIN-CLI-HITL-01 · {{ inventory_hostname }}
══════════════════════════════════════════════════════════════
Requirement : SR 1.3 — Account Management
Check : Physical USB ports and removable media controls
Registry evidence (UsbStor Start value):
HKLM\SYSTEM\CurrentControlSet\Services\UsbStor\Start
Value: {{ _usb_stor.value | default('KEY NOT FOUND') }}
(4 = disabled, 3 = manual/enabled, key absent = check GPO)
Also confirm:
- Unused USB ports physically blocked or disabled in BIOS
- No unauthorised removable media found on the workstation
══════════════════════════════════════════════════════════════
- name: "Prompt: WIN-CLI-HITL-01 — verdict for {{ inventory_hostname }}"
ansible.builtin.pause:
prompt: |
USB storage is {{ 'DISABLED (Start=4)' if _usb_stor.value | default(3) | int == 4 else 'ENABLED or UNKNOWN' }} by registry policy.
After physical confirmation, does this workstation satisfy SR 1.3 USB/removable media controls?
Enter verdict [pass / fail / skip]:
register: _hitl_usb_verdict
delegate_to: localhost
- name: "Prompt: WIN-CLI-HITL-01 — notes on failure"
ansible.builtin.pause:
prompt: "Describe finding (e.g. 'USB port active, no media policy applied'):"
register: _hitl_usb_notes
delegate_to: localhost
when: _hitl_usb_verdict.user_input | lower | trim in ['fail', 'f']
- name: "Evaluate: WIN-CLI-HITL-01"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-CLI-HITL-01',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.3 — Account Management',
'description': 'USB storage and removable media shall be disabled or physically controlled',
'passed': (
'skipped' if (_hitl_usb_verdict.user_input | lower | trim in ['skip', 's', ''])
else (_hitl_usb_verdict.user_input | lower | trim in ['pass', 'p'])
),
'expected': 'USB storage disabled (UsbStor Start=4) AND physical ports secured',
'actual': 'UsbStor Start = ' + (_usb_stor.value | default('NOT SET') | string),
'severity': 'high',
'remediation': 'Set HKLM\SYSTEM\CurrentControlSet\Services\UsbStor\Start = 4 via GPO, and physically block or tape ports',
'reviewer': ansible_user_id,
'notes': (_hitl_usb_notes.user_input | trim) if _hitl_usb_notes is defined else ''
}] }}"
ignore_errors: yes
# ── Generate report ────────────────────────────────────────────────────────
- name: "Generate compliance report"
ansible.builtin.include_tasks: ../library/report.yml