CYBER-0 initial concept ready
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
---
|
||||
# suites/fr1_auth.yml
|
||||
#
|
||||
# FR1 — Identification and Authentication Control (IAC)
|
||||
# IEC 62443-3-3 SL2 requirements:
|
||||
# SR 1.1 Unique user identification
|
||||
# SR 1.3 Account management (no default/unused accounts)
|
||||
# SR 1.4 Identifier strength (no empty passwords)
|
||||
# SR 1.5 Authenticator strength (password policy)
|
||||
# SR 1.7 Password lifetime/aging
|
||||
# SR 1.11 Unsuccessful login attempts (account lockout)
|
||||
#
|
||||
# Pattern: Every test is wrapped in a block + ignore_errors.
|
||||
# "Gather" tasks collect facts; "Evaluate" tasks judge pass/fail
|
||||
# and append to test_results[].
|
||||
|
||||
# ── SR 1.1: Unique User Identification ──────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Scan for duplicate UIDs"
|
||||
ansible.builtin.shell: |
|
||||
awk -F: '{print $3}' /etc/passwd | sort -n | uniq -d
|
||||
register: _dup_uid
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-01"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results | default([]) + [{
|
||||
'test_id': 'IAC-01',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.1 — Unique User Identification',
|
||||
'description': 'Every user account shall have a unique UID',
|
||||
'passed': (_dup_uid.stdout | trim | length == 0),
|
||||
'expected': 'No duplicate UIDs in /etc/passwd',
|
||||
'actual': (_dup_uid.stdout | trim | default('None found', true)),
|
||||
'severity': 'high',
|
||||
'remediation': 'Change duplicate UIDs with: usermod -u <new-uid> <user>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.3: No default/unused accounts ──────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check for well-known default accounts"
|
||||
ansible.builtin.shell: |
|
||||
for acct in games news gopher ftp nobody; do
|
||||
if grep -q "^${acct}:" /etc/passwd; then
|
||||
echo "$acct"
|
||||
fi
|
||||
done
|
||||
register: _default_accts
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-02"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-02',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.3 — Account Management',
|
||||
'description': 'Default/unnecessary system accounts shall be removed or disabled',
|
||||
'passed': (_default_accts.stdout | trim | length == 0),
|
||||
'expected': 'No default accounts present',
|
||||
'actual': (_default_accts.stdout | trim | default('None found', true)),
|
||||
'severity': 'medium',
|
||||
'remediation': 'Delete default accounts: userdel <account>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check accounts with valid shells that have never logged in"
|
||||
ansible.builtin.shell: |
|
||||
# Accounts with real shells that show no login records
|
||||
join -t: -1 1 -2 1 \
|
||||
<(grep -E ':(/bin/(ba)?sh|/usr/bin/(ba)?sh)$' /etc/passwd | sort) \
|
||||
<(lastlog | tail -n +2 | awk '{print $1, $NF}' | sort) 2>/dev/null |
|
||||
awk '$NF == "**Never" {print $1 " (shell: " $7 ")"}' || true
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: _unused
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-03"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-03',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.3 — Account Management',
|
||||
'description': 'Human user accounts that have never logged in shall be reviewed',
|
||||
'passed': (_unused.stdout | trim | length == 0),
|
||||
'expected': 'No unused human accounts with valid shells',
|
||||
'actual': (_unused.stdout | trim | default('None found', true)),
|
||||
'severity': 'low',
|
||||
'remediation': 'Lock stale accounts: usermod -L <user>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.4: No empty passwords ──────────────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check /etc/shadow for empty password fields"
|
||||
ansible.builtin.shell: |
|
||||
awk -F: '($2 == "" || $2 == "!" || $2 ~ /^\$[156]\$/) {next}
|
||||
$2 == "!!" || $2 == "*" {next}
|
||||
{print $1 " (field: " $2 ")"}' /etc/shadow
|
||||
register: _empty_pw
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-04"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-04',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.4 — Identifier Strength',
|
||||
'description': 'No account shall have an empty or trivially-weak password hash',
|
||||
'passed': (_empty_pw.stdout | trim | length == 0),
|
||||
'expected': 'All accounts have proper password hashes',
|
||||
'actual': (_empty_pw.stdout | trim | default('All accounts OK', true)),
|
||||
'severity': 'critical',
|
||||
'remediation': 'Set a password or lock the account: passwd -l <user>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.5: Password complexity (via pwquality / PAM) ───────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check pwquality minlen"
|
||||
ansible.builtin.shell: |
|
||||
grep -E '^\s*minlen\s*=' /etc/security/pwquality.conf 2>/dev/null | tail -1 || echo "NOT SET"
|
||||
register: _minlen
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-05"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-05',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.5 — Authenticator Strength',
|
||||
'description': 'Password minimum length shall be ≥ 14 characters',
|
||||
'passed': (
|
||||
(_minlen.stdout | regex_search('minlen\s*=\s*(\d+)', '\1') | default(['0'], true) | first | int) >= 14
|
||||
),
|
||||
'expected': 'minlen >= 14 in /etc/security/pwquality.conf',
|
||||
'actual': _minlen.stdout | trim,
|
||||
'severity': 'high',
|
||||
'remediation': 'Set minlen=14 in /etc/security/pwquality.conf'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check pwquality dcredit/ocredit/ucredit/lcredit"
|
||||
ansible.builtin.shell: |
|
||||
for p in dcredit ucredit lcredit ocredit minclass; do
|
||||
val=$(grep -E "^\s*${p}\s*=" /etc/security/pwquality.conf 2>/dev/null | tail -1 | awk -F= '{print $2}' | tr -d ' ')
|
||||
echo "${p}=${val:-NOT SET}"
|
||||
done
|
||||
register: _pwquality
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-06"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-06',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.5 — Authenticator Strength',
|
||||
'description': 'Password shall require at least 1 of each character class',
|
||||
'passed': (
|
||||
(_pwquality.stdout | regex_search('dcredit\s*=\s*-?1') and
|
||||
_pwquality.stdout | regex_search('ucredit\s*=\s*-?1') and
|
||||
_pwquality.stdout | regex_search('lcredit\s*=\s*-?1') and
|
||||
_pwquality.stdout | regex_search('ocredit\s*=\s*-?1'))
|
||||
or
|
||||
(_pwquality.stdout | regex_search('minclass\s*=\s*[3-4]'))
|
||||
),
|
||||
'expected': 'At least 3 character classes required',
|
||||
'actual': _pwquality.stdout | trim,
|
||||
'severity': 'medium',
|
||||
'remediation': 'Set dcredit=-1, ucredit=-1, lcredit=-1, ocredit=-1 in pwquality.conf'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.7: Password aging (max days) ───────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check PASS_MAX_DAYS in login.defs"
|
||||
ansible.builtin.shell: |
|
||||
grep '^\s*PASS_MAX_DAYS' /etc/login.defs | awk '{print $2}'
|
||||
register: _max_days
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-07"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-07',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.7 — Password Lifetime',
|
||||
'description': 'Password maximum age shall be ≤ 90 days',
|
||||
'passed': (
|
||||
_max_days.stdout | trim | regex_search('^[0-9]+$') and
|
||||
(_max_days.stdout | trim | int > 0) and
|
||||
(_max_days.stdout | trim | int <= 90)
|
||||
),
|
||||
'expected': 'PASS_MAX_DAYS ≤ 90',
|
||||
'actual': 'PASS_MAX_DAYS=' + (_max_days.stdout | trim | default('NOT SET', true)),
|
||||
'severity': 'medium',
|
||||
'remediation': 'Set PASS_MAX_DAYS 90 in /etc/login.defs'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check PASS_MIN_DAYS in login.defs"
|
||||
ansible.builtin.shell: |
|
||||
grep '^\s*PASS_MIN_DAYS' /etc/login.defs | awk '{print $2}'
|
||||
register: _min_days
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-08"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-08',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.7 — Password Lifetime',
|
||||
'description': 'Password minimum change interval shall be ≥ 1 day',
|
||||
'passed': (_min_days.stdout | trim | int >= 1),
|
||||
'expected': 'PASS_MIN_DAYS ≥ 1',
|
||||
'actual': 'PASS_MIN_DAYS=' + (_min_days.stdout | trim | default('NOT SET', true)),
|
||||
'severity': 'low',
|
||||
'remediation': 'Set PASS_MIN_DAYS 1 in /etc/login.defs'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.11: Account lockout ────────────────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check pam_tally2 or pam_faillock configuration"
|
||||
ansible.builtin.shell: |
|
||||
if grep -q 'pam_faillock\.so' /etc/pam.d/common-auth 2>/dev/null; then
|
||||
grep 'deny=' /etc/pam.d/common-auth | grep -oP 'deny=\K[0-9]+' || echo "0"
|
||||
elif grep -q 'pam_tally2\.so' /etc/pam.d/common-auth 2>/dev/null; then
|
||||
grep 'deny=' /etc/pam.d/common-auth | grep -oP 'deny=\K[0-9]+' || echo "0"
|
||||
else
|
||||
echo "LOCKOUT NOT CONFIGURED"
|
||||
fi
|
||||
register: _lockout
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-09"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-09',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.11 — Unsuccessful Login Attempts',
|
||||
'description': 'Account lockout shall trigger after ≤ 5 failed attempts',
|
||||
'passed': (
|
||||
_lockout.stdout | trim | regex_search('^[0-9]+$') and
|
||||
(_lockout.stdout | trim | int > 0) and
|
||||
(_lockout.stdout | trim | int <= 5)
|
||||
),
|
||||
'expected': 'Account lockout configured with deny ≤ 5',
|
||||
'actual': _lockout.stdout | trim,
|
||||
'severity': 'high',
|
||||
'remediation': 'Configure pam_faillock in /etc/pam.d/common-auth: deny=5'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── SR 1.5 (cont): Password history ─────────────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Check password history in PAM"
|
||||
ansible.builtin.shell: |
|
||||
grep -E 'pam_pwhistory\.so|remember=' /etc/pam.d/common-password 2>/dev/null |
|
||||
grep -oP 'remember=\K[0-9]+' || echo "NOT SET"
|
||||
register: _pw_history
|
||||
changed_when: false
|
||||
|
||||
- name: "Evaluate: IAC-10"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'IAC-10',
|
||||
'category': 'FR1 - Identification and Authentication Control',
|
||||
'requirement': 'SR 1.6 — Password History',
|
||||
'description': 'Password history shall prevent reuse of last 5+ passwords',
|
||||
'passed': (
|
||||
_pw_history.stdout | trim | regex_search('^[0-9]+$') and
|
||||
(_pw_history.stdout | trim | int >= 5)
|
||||
),
|
||||
'expected': 'pam_pwhistory remember ≥ 5',
|
||||
'actual': 'remember=' + (_pw_history.stdout | trim),
|
||||
'severity': 'medium',
|
||||
'remediation': 'Add "remember=5" to pam_pwhistory.so in /etc/pam.d/common-password'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
@@ -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
|
||||
@@ -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