From 943a21ef1213fe1551664730501c989aacbad07c Mon Sep 17 00:00:00 2001 From: oval Date: Tue, 7 Jul 2026 16:57:37 +0000 Subject: [PATCH] Add playbooks/suites/fr5_data_flow.yml --- playbooks/suites/fr5_data_flow.yml | 119 +++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 playbooks/suites/fr5_data_flow.yml diff --git a/playbooks/suites/fr5_data_flow.yml b/playbooks/suites/fr5_data_flow.yml new file mode 100644 index 0000000..b54c246 --- /dev/null +++ b/playbooks/suites/fr5_data_flow.yml @@ -0,0 +1,119 @@ +--- +# 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