--- # 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