--- # suites/fr5_data_flow.yml # # FR5 — Restricted Data Flow (RDF) # IEC 62443-3-3 SL2 requirements: # SR 5.1 Network segmentation (firewall active, boundary filtering) # SR 5.2 Zone boundary protection # SR 5.3 General-purpose communication constraints (unnecessary services) # ── SR 5.1: Host-based firewall active ────────────────────────── - block: - name: "Gather: Check iptables/nftables rules exist" ansible.builtin.shell: | if command -v nft >/dev/null 2>&1; then nft list ruleset 2>/dev/null | grep -c 'accept\|drop\|reject' || echo "0" elif command -v iptables >/dev/null 2>&1; then iptables -L -n 2>/dev/null | grep -c 'ACCEPT\|DROP\|REJECT' || echo "0" else echo "NO FIREWALL" fi register: _fw_rules changed_when: false - name: "Evaluate: RDF-01" ansible.builtin.set_fact: test_results: "{{ test_results + [{ 'test_id': 'RDF-01', 'category': 'FR5 — Restricted Data Flow', 'requirement': 'SR 5.1 — Network Segmentation', 'description': 'A host-based firewall with active rules shall be present', 'passed': ( _fw_rules.stdout | trim != 'NO FIREWALL' and (_fw_rules.stdout | trim | int > 0) ), 'expected': 'Firewall with active filter rules', 'actual': ('Rules found: ' + (_fw_rules.stdout | trim)) if (_fw_rules.stdout | trim != 'NO FIREWALL' and (_fw_rules.stdout | trim | int > 0)) else (_fw_rules.stdout | trim), 'severity': 'critical', 'remediation': 'Install and configure iptables/nftables with default-deny inbound policy' }] }}" ignore_errors: yes - block: - name: "Gather: Check default inbound policy" ansible.builtin.shell: | if command -v nft >/dev/null 2>&1; then nft list chain inet filter INPUT 2>/dev/null | grep policy | awk '{print $NF}' || echo "UNKNOWN" elif command -v iptables >/dev/null 2>&1; then iptables -L INPUT -n 2>/dev/null | head -1 | awk '{print $4}' | tr -d ')' else echo "NO FIREWALL" fi register: _default_policy changed_when: false - name: "Evaluate: RDF-02" ansible.builtin.set_fact: test_results: "{{ test_results + [{ 'test_id': 'RDF-02', 'category': 'FR5 — Restricted Data Flow', 'requirement': 'SR 5.1 — Network Segmentation', 'description': 'Default inbound firewall policy shall be DROP', 'passed': (_default_policy.stdout | trim | lower == 'drop'), 'expected': 'Default inbound policy is DROP', 'actual': 'Default policy: ' + (_default_policy.stdout | trim), 'severity': 'high', 'remediation': 'Set default INPUT policy to DROP: iptables -P INPUT DROP' }] }}" ignore_errors: yes # ── SR 5.3: Unnecessary services ──────────────────────────────── - block: - name: "Gather: Check for unnecessary network services" ansible.builtin.shell: | # Services commonly flagged as unnecessary on ICS/OT systems for svc in telnet.socket rsh.socket rexec.socket rlogin.socket \ ftp.service vsftpd.service xinetd.service; do systemctl is-active "$svc" 2>/dev/null | grep -q 'active' && echo "$svc" done register: _bad_services changed_when: false - name: "Evaluate: RDF-03" ansible.builtin.set_fact: test_results: "{{ test_results + [{ 'test_id': 'RDF-03', 'category': 'FR5 — Restricted Data Flow', 'requirement': 'SR 5.3 — Communication Constraints', 'description': 'Insecure network services (telnet, rsh, ftp) shall be disabled', 'passed': (_bad_services.stdout | trim | length == 0), 'expected': 'No insecure legacy services active', 'actual': (_bad_services.stdout | trim | default('None', true)), 'severity': 'critical', 'remediation': 'Disable: systemctl disable --now ' }] }}" ignore_errors: yes - block: - name: "Gather: Check listening TCP ports" ansible.builtin.shell: | ss -tlnp 2>/dev/null | awk 'NR>1 {print $4}' | awk -F: '{print $NF}' | sort -n | uniq | tr '\n' ' ' register: _listening changed_when: false - name: "Evaluate: RDF-04" ansible.builtin.set_fact: test_results: "{{ test_results + [{ 'test_id': 'RDF-04', 'category': 'FR5 — Restricted Data Flow', 'requirement': 'SR 5.3 — Communication Constraints', 'description': 'Only necessary TCP ports shall be listening (document in exception list)', 'passed': 'review', 'expected': 'Documented and approved port list', 'actual': 'Listening ports: ' + (_listening.stdout | trim | default('Unable to determine', true)), 'severity': 'low', 'remediation': 'Review and disable unnecessary listening services' }] }}" ignore_errors: yes