From 0335cad80aaae54ee4e287242a2e11f0dd5a1d16 Mon Sep 17 00:00:00 2001 From: oval Date: Tue, 7 Jul 2026 16:57:37 +0000 Subject: [PATCH] Add playbooks/suites/fr2_use_control.yml --- playbooks/suites/fr2_use_control.yml | 165 +++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 playbooks/suites/fr2_use_control.yml diff --git a/playbooks/suites/fr2_use_control.yml b/playbooks/suites/fr2_use_control.yml new file mode 100644 index 0000000..f7f13a8 --- /dev/null +++ b/playbooks/suites/fr2_use_control.yml @@ -0,0 +1,165 @@ +--- +# suites/fr2_use_control.yml +# +# FR2 — Use Control (UC) +# IEC 62443-3-3 SL2 requirements: +# SR 2.1 Authorization enforcement (sudo/privilege separation) +# SR 2.4 Audit log integrity +# SR 2.5 Session lock (terminal idle timeout) +# SR 2.8 Auditable events (auditd active) +# SR 2.9 Audit storage capacity + +# ── SR 2.1: Sudo privilege separation ─────────────────────────── + +- block: + - name: "Gather: Check for unrestricted sudo access (NOPASSWD ALL)" + ansible.builtin.shell: | + grep -r 'NOPASSWD.*ALL' /etc/sudoers /etc/sudoers.d/ 2>/dev/null | + grep -v '^\s*#' | grep -v '^$' || true + register: _nopasswd + changed_when: false + + - name: "Evaluate: UC-01" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-01', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.1 — Authorization Enforcement', + 'description': 'No user shall have unrestricted NOPASSWD sudo access to all commands', + 'passed': (_nopasswd.stdout | trim | length == 0), + 'expected': 'No NOPASSWD ALL entries in sudoers', + 'actual': (_nopasswd.stdout | trim | default('None found', true)), + 'severity': 'high', + 'remediation': 'Restrict sudo rules to specific commands and require authentication' + }] }}" + ignore_errors: yes + +- block: + - name: "Gather: Check sudoers file permissions" + ansible.builtin.stat: + path: /etc/sudoers + register: _sudoers_stat + + - name: "Evaluate: UC-02" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-02', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.1 — Authorization Enforcement', + 'description': '/etc/sudoers shall be owned by root:root with mode 0440', + 'passed': ( + _sudoers_stat.stat.exists and + _sudoers_stat.stat.pw_name == 'root' and + _sudoers_stat.stat.gr_name == 'root' and + _sudoers_stat.stat.mode == '0440' + ), + 'expected': 'root:root 0440', + 'actual': (_sudoers_stat.stat.pw_name + ':' + _sudoers_stat.stat.gr_name + ' ' + _sudoers_stat.stat.mode) if _sudoers_stat.stat.exists else 'FILE NOT FOUND', + 'severity': 'critical', + 'remediation': 'chown root:root /etc/sudoers && chmod 0440 /etc/sudoers' + }] }}" + ignore_errors: yes + +# ── SR 2.5: Session lock / terminal timeout ───────────────────── + +- block: + - name: "Gather: Check TMOUT setting in /etc/profile or /etc/bash.bashrc" + ansible.builtin.shell: | + for f in /etc/profile /etc/bash.bashrc /etc/profile.d/*.sh; do + [ -f "$f" ] && grep -h 'TMOUT=' "$f" 2>/dev/null + done | tail -1 | grep -oP 'TMOUT=\K[0-9]+' || echo "NOT SET" + register: _tmout + changed_when: false + + - name: "Evaluate: UC-03" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-03', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.5 — Session Lock', + 'description': 'Interactive shell sessions shall timeout after ≤ 900 seconds of inactivity', + 'passed': ( + _tmout.stdout | trim | regex_search('^[0-9]+$') and + (_tmout.stdout | trim | int > 0) and + (_tmout.stdout | trim | int <= 900) + ), + 'expected': 'TMOUT set between 1-900 seconds', + 'actual': 'TMOUT=' + (_tmout.stdout | trim), + 'severity': 'medium', + 'remediation': 'Add "readonly TMOUT=900" to /etc/profile' + }] }}" + ignore_errors: yes + +# ── SR 2.4: Audit log integrity (immutability) ────────────────── + +- block: + - name: "Gather: Check if auditd immutable mode is configured" + ansible.builtin.shell: | + grep -c '^\s*-e\s*2' /etc/audit/rules.d/*.rules /etc/audit/audit.rules 2>/dev/null || echo "0" + register: _audit_immutable + changed_when: false + + - name: "Evaluate: UC-04" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-04', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.4 — Audit Log Integrity', + 'description': 'Audit configuration shall be immutable (-e 2)', + 'passed': (_audit_immutable.stdout | trim | int > 0), + 'expected': 'audit.rules contains -e 2', + 'actual': ('Immutable rules found: ' + _audit_immutable.stdout) if (_audit_immutable.stdout | trim | int > 0) else 'Immutable flag NOT set', + 'severity': 'high', + 'remediation': 'Add "-e 2" to /etc/audit/audit.rules (requires reboot)' + }] }}" + ignore_errors: yes + +# ── SR 2.8: Auditable events (auditd running) ─────────────────── + +- block: + - name: "Gather: Check auditd service status" + ansible.builtin.shell: | + systemctl is-active auditd 2>/dev/null || echo "inactive" + register: _auditd_active + changed_when: false + + - name: "Evaluate: UC-05" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-05', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.8 — Auditable Events', + 'description': 'The audit daemon (auditd) shall be running and enabled', + 'passed': (_auditd_active.stdout | trim == 'active'), + 'expected': 'auditd service is active', + 'actual': 'auditd is ' + (_auditd_active.stdout | trim), + 'severity': 'high', + 'remediation': 'systemctl enable --now auditd' + }] }}" + ignore_errors: yes + +- block: + - name: "Gather: Check if critical system calls are audited" + ansible.builtin.shell: | + rules=0 + for call in execve execveat mount umount2 creat open openat truncate ftruncate; do + grep -rq "$call" /etc/audit/rules.d/ 2>/dev/null && rules=$((rules+1)) + done + echo "$rules" + register: _audit_sc + changed_when: false + + - name: "Evaluate: UC-06" + ansible.builtin.set_fact: + test_results: "{{ test_results + [{ + 'test_id': 'UC-06', + 'category': 'FR2 — Use Control', + 'requirement': 'SR 2.8 — Auditable Events', + 'description': 'Critical system events (exec, mount, file modifications) shall be audited', + 'passed': (_audit_sc.stdout | trim | int >= 4), + 'expected': '≥ 4 critical syscall types audited', + 'actual': (_audit_sc.stdout | trim) + ' of 9 critical syscall types audited', + 'severity': 'medium', + 'remediation': 'Add audit rules for execve, mount, creat/open/openat, truncate/ftruncate' + }] }}" + ignore_errors: yes