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

205 lines
9.8 KiB
YAML

---
# ══════════════════════════════════════════════════════════════════════════════
# IEC 62443-3-3 SL2 — Windows Server Compliance
#
# Target type : Windows Server 2016 / 2019 / 2022
# Connection : WinRM (HTTP :5985 or HTTPS :5986)
# Collections : ansible.windows (ships with Ansible)
# Python pkg : pywinrm (installed in ansible-node image)
# Privilege : No become required — WinRM user needs local admin rights
#
# Inventory group : windows_servers (see assets.yml)
#
# Run:
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/windows_server.yml
#
# WinRM quick-enable on target (run as Administrator):
# winrm quickconfig -q
# winrm set winrm/config/service/auth '@{Basic="true"}'
# winrm set winrm/config/service '@{AllowUnencrypted="true"}'
# # For production: use HTTPS and Kerberos transport instead
#
# Vault usage (recommended for passwords):
# ansible-vault encrypt_string 'MyPassword' --name ansible_password
# ══════════════════════════════════════════════════════════════════════════════
- name: "IEC 62443-3-3 SL2 — Windows Server Compliance"
hosts: windows_servers
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.5: Minimum password length ─────────────────────────────────
# Uses win_security_policy — no PowerShell shell-out needed.
- block:
- name: "Gather: Minimum password length (security policy)"
ansible.windows.win_security_policy:
section: System Access
key: MinimumPasswordLength
register: _min_pw_len
- name: "Evaluate: WIN-IAC-01 — Password minimum length ≥ 14"
ansible.builtin.set_fact:
test_results: "{{ test_results | default([]) + [{
'test_id': 'WIN-IAC-01',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.5 — Authenticator Strength',
'description': 'Windows local password policy minimum length shall be ≥ 14 characters',
'passed': (_min_pw_len.value | int >= 14),
'expected': 'MinimumPasswordLength ≥ 14',
'actual': 'MinimumPasswordLength = ' + (_min_pw_len.value | string),
'severity': 'high',
'remediation': 'Computer Configuration → Windows Settings → Security Settings → Account Policies → Password Policy → Minimum password length: 14'
}] }}"
ignore_errors: yes
# ── FR1 · SR 1.11: Account lockout threshold ──────────────────────────────
- block:
- name: "Gather: Account lockout threshold"
ansible.windows.win_security_policy:
section: System Access
key: LockoutBadCount
register: _lockout_count
- name: "Evaluate: WIN-IAC-02 — Account lockout ≤ 5 attempts"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-IAC-02',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.11 — Unsuccessful Login Attempts',
'description': 'Account lockout shall trigger after ≤ 5 failed login attempts',
'passed': (
(_lockout_count.value | int > 0) and
(_lockout_count.value | int <= 5)
),
'expected': 'LockoutBadCount between 1 and 5',
'actual': 'LockoutBadCount = ' + (_lockout_count.value | string),
'severity': 'high',
'remediation': 'Set Account lockout threshold to 5 in Local Security Policy'
}] }}"
ignore_errors: yes
# ── FR2 · SR 2.8: Audit policy — logon events ─────────────────────────────
# Uses win_audit_policy_system — no auditpol.exe shell-out needed.
- block:
- name: "Gather: Audit policy — Logon subcategory"
ansible.windows.win_audit_policy_system:
subcategory: Logon
register: _audit_logon
- name: "Evaluate: WIN-UC-01 — Logon events audited"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-UC-01',
'category': 'FR2 — Use Control',
'requirement': 'SR 2.8 — Auditable Events',
'description': 'Logon/logoff events shall be audited (success and failure)',
'passed': (_audit_logon.auditing_mode == 'success and failure'),
'expected': 'Logon: success and failure',
'actual': 'Logon: ' + _audit_logon.auditing_mode,
'severity': 'high',
'remediation': 'auditpol /set /subcategory:"Logon" /success:enable /failure:enable'
}] }}"
ignore_errors: yes
# ── FR2 · SR 2.8: Audit policy — privilege use ────────────────────────────
- block:
- name: "Gather: Audit policy — Sensitive Privilege Use"
ansible.windows.win_audit_policy_system:
subcategory: Sensitive Privilege Use
register: _audit_privuse
- name: "Evaluate: WIN-UC-02 — Privilege use audited"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-UC-02',
'category': 'FR2 — Use Control',
'requirement': 'SR 2.8 — Auditable Events',
'description': 'Sensitive privilege use (SeDebugPrivilege, SeTcbPrivilege, etc.) shall be audited',
'passed': (_audit_privuse.auditing_mode in ['success and failure', 'failure']),
'expected': 'Sensitive Privilege Use: failure (at minimum)',
'actual': 'Sensitive Privilege Use: ' + _audit_privuse.auditing_mode,
'severity': 'medium',
'remediation': 'auditpol /set /subcategory:"Sensitive Privilege Use" /failure:enable'
}] }}"
ignore_errors: yes
# ── FR5 · SR 5.1: Windows Firewall enabled on all profiles ────────────────
# PowerShell ConvertTo-Json + Ansible from_json filter avoids regex parsing.
- block:
- name: "Gather: Windows Firewall profile states"
ansible.windows.win_shell: |
@(Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction) |
ConvertTo-Json -Compress
register: _fw_profiles
- name: "Evaluate: WIN-RDF-01 — Firewall enabled on all profiles"
ansible.builtin.set_fact:
_fw_json: "{{ _fw_profiles.stdout | from_json }}"
- name: "Evaluate: WIN-RDF-01 — record result"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-RDF-01',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.1 — Network Segmentation',
'description': 'Windows Firewall shall be enabled on Domain, Private, and Public profiles',
'passed': (_fw_json | selectattr('Enabled', 'equalto', true) | list | length == 3),
'expected': 'All 3 firewall profiles Enabled = True',
'actual': _fw_json | map(attribute='Name') | zip(_fw_json | map(attribute='Enabled')) | list | string,
'severity': 'critical',
'remediation': 'Set-NetFirewallProfile -All -Enabled True'
}] }}"
ignore_errors: yes
# ── FR5 · SR 5.3: Telnet / FTP / RDP services ─────────────────────────────
# Uses win_service_info — typed return dict, no regex needed.
- block:
- name: "Gather: Telnet service state"
ansible.windows.win_service_info:
name: TlntSvr
register: _telnet_svc
- name: "Evaluate: WIN-RDF-02 — Telnet service disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'WIN-RDF-02',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.3 — Communication Constraints',
'description': 'The Telnet Server service (TlntSvr) shall be absent or disabled',
'passed': (
_telnet_svc.services | length == 0 or
_telnet_svc.services[0].start_mode == 'disabled'
),
'expected': 'TlntSvr: absent or start_mode=disabled',
'actual': (
'TlntSvr: state=' + _telnet_svc.services[0].state
+ ', start_mode=' + _telnet_svc.services[0].start_mode
) if _telnet_svc.services | length > 0 else 'TlntSvr: not installed',
'severity': 'critical',
'remediation': 'Stop-Service TlntSvr; Set-Service TlntSvr -StartupType Disabled'
}] }}"
ignore_errors: yes
# ── Generate report ────────────────────────────────────────────────────────
- name: "Generate compliance report"
ansible.builtin.include_tasks: ../library/report.yml