Add playbooks/suites/fr5_data_flow.yml
This commit is contained in:
@@ -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 <service>'
|
||||||
|
}] }}"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user