cyber-0 updated with more examples
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
---
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# IEC 62443-3-3 SL2 — Cisco ASA Firewall Compliance
|
||||
#
|
||||
# Target type : Cisco ASA 9.x+ (physical or virtual)
|
||||
# Connection : SSH via ansible.netcommon.network_cli
|
||||
# Collections : cisco.asa, ansible.netcommon (installed in ansible-node image)
|
||||
# Python pkg : paramiko (installed in ansible-node image)
|
||||
#
|
||||
# Inventory group : [cisco_firewalls] (see inventory.ini)
|
||||
#
|
||||
# Run:
|
||||
# ansible-playbook -i inventory.ini playbooks/examples/cisco_firewall.yml
|
||||
#
|
||||
# ASA-specific notes:
|
||||
# - asa_command returns stdout as a list, same as ios_command.
|
||||
# - 'show running-config' on ASA is a single large string; use regex_search
|
||||
# and regex_findall to extract specific configuration lines.
|
||||
# - 'enable' privilege is required for most 'show' commands.
|
||||
# ansible_become=yes + ansible_become_method=enable handles this.
|
||||
# - Multi-context ASAs: add 'changeto context <name>' as a command prefix,
|
||||
# or target individual context admin contexts.
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
- name: "IEC 62443-3-3 SL2 — Cisco ASA Firewall Compliance"
|
||||
hosts: cisco_firewalls
|
||||
gather_facts: yes # runs cisco.asa.asa_facts → ansible_net_*
|
||||
vars:
|
||||
report_dir: "../../reports"
|
||||
|
||||
pre_tasks:
|
||||
- name: "Gather local facts for report timestamp and user"
|
||||
ansible.builtin.setup:
|
||||
gather_subset:
|
||||
- date_time
|
||||
- user_id
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: "Ensure report directory exists"
|
||||
ansible.builtin.file:
|
||||
path: "{{ report_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
tasks:
|
||||
|
||||
# ── FR5 · SR 5.3: No Telnet on VTY lines — SSH only ──────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: VTY line transport configuration"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show running-config | include telnet|ssh
|
||||
register: _mgmt_access
|
||||
|
||||
- name: "Evaluate: FW-RDF-01 — Telnet management access disabled"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results | default([]) + [{
|
||||
'test_id': 'FW-RDF-01',
|
||||
'category': 'FR5 — Restricted Data Flow',
|
||||
'requirement': 'SR 5.3 — Communication Constraints',
|
||||
'description': 'Telnet management access to the ASA shall be disabled',
|
||||
'passed': (
|
||||
_mgmt_access.stdout[0] | regex_search('telnet [0-9]') is none
|
||||
),
|
||||
'expected': 'No telnet <network> lines in running-config',
|
||||
'actual': _mgmt_access.stdout[0] | regex_findall('telnet[^\n]+') | join(' | ') | default('No telnet statements found', true),
|
||||
'severity': 'critical',
|
||||
'remediation': 'Remove all "telnet" management statements; use "ssh" only'
|
||||
}] }}"
|
||||
|
||||
- name: "Evaluate: FW-RDF-02 — SSH management access configured"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-RDF-02',
|
||||
'category': 'FR5 — Restricted Data Flow',
|
||||
'requirement': 'SR 5.3 — Communication Constraints',
|
||||
'description': 'SSH management access shall be restricted to specific management networks',
|
||||
'passed': (_mgmt_access.stdout[0] | regex_search('ssh [0-9]') is not none),
|
||||
'expected': 'At least one ssh <network> statement present',
|
||||
'actual': _mgmt_access.stdout[0] | regex_findall('ssh[^\n]+') | join(' | ') | default('No SSH access statements', true),
|
||||
'severity': 'high',
|
||||
'remediation': 'ssh <management-net> <mask> <interface>\nssh version 2'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── FR1 · SR 1.2: IKEv1 disabled — IKEv2 only for VPN ────────────────────
|
||||
# IKEv1 is vulnerable to several known attacks. IEC 62443 SL2 requires v2.
|
||||
|
||||
- block:
|
||||
- name: "Gather: IKE/ISAKMP policy configuration"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show running-config | include crypto isakmp|crypto ikev
|
||||
register: _ike_cfg
|
||||
|
||||
- name: "Evaluate: FW-IAC-01 — IKEv1 disabled"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-IAC-01',
|
||||
'category': 'FR1 — Identification and Authentication Control',
|
||||
'requirement': 'SR 1.2 — Software Process and Device Identification',
|
||||
'description': 'IKEv1 (crypto isakmp) shall be disabled; only IKEv2 is permitted',
|
||||
'passed': (
|
||||
_ike_cfg.stdout[0] | regex_search('crypto isakmp enable') is none and
|
||||
_ike_cfg.stdout[0] | regex_search('crypto isakmp policy') is none
|
||||
),
|
||||
'expected': 'No crypto isakmp enable or isakmp policy statements',
|
||||
'actual': _ike_cfg.stdout[0] | regex_findall('crypto isakmp[^\n]+') | join(' | ') | default('No IKEv1 config found', true),
|
||||
'severity': 'high',
|
||||
'remediation': 'no crypto isakmp enable\nno crypto isakmp policy <n>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── FR2 · SR 2.8: Syslog forwarding to remote server ─────────────────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Syslog configuration"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show running-config | include logging
|
||||
register: _syslog_cfg
|
||||
|
||||
- name: "Evaluate: FW-UC-01 — Syslog forwarding to remote host"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-UC-01',
|
||||
'category': 'FR2 — Use Control',
|
||||
'requirement': 'SR 2.8 — Auditable Events',
|
||||
'description': 'ASA syslog shall be forwarded to a remote syslog server (not stored locally only)',
|
||||
'passed': (
|
||||
_syslog_cfg.stdout[0] | regex_search('logging host') is not none and
|
||||
_syslog_cfg.stdout[0] | regex_search('logging enable') is not none
|
||||
),
|
||||
'expected': 'logging enable; logging host <interface> <syslog-server>',
|
||||
'actual': _syslog_cfg.stdout[0] | regex_findall('logging[^\n]+') | join(' | ') | default('No logging config', true),
|
||||
'severity': 'high',
|
||||
'remediation': 'logging enable\nlogging host <inside/mgmt> <syslog-server-ip>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── FR1 · SR 1.11: AAA authentication for management ─────────────────────
|
||||
# Require AAA (TACACS+/RADIUS) for management access; reject local fallback
|
||||
# without documented justification.
|
||||
|
||||
- block:
|
||||
- name: "Gather: AAA and local user configuration"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show running-config | include ^aaa|^username
|
||||
register: _aaa_cfg
|
||||
|
||||
- name: "Evaluate: FW-IAC-02 — AAA authentication configured for SSH/enable"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-IAC-02',
|
||||
'category': 'FR1 — Identification and Authentication Control',
|
||||
'requirement': 'SR 1.11 — Unsuccessful Login Attempts',
|
||||
'description': 'Management access (SSH and enable) shall use AAA authentication',
|
||||
'passed': (
|
||||
_aaa_cfg.stdout[0] | regex_search('aaa authentication ssh') is not none or
|
||||
_aaa_cfg.stdout[0] | regex_search('aaa authentication enable') is not none
|
||||
),
|
||||
'expected': 'aaa authentication ssh|enable console <server-group> LOCAL',
|
||||
'actual': _aaa_cfg.stdout[0] | regex_findall('aaa authentication[^\n]+') | join(' | ') | default('No AAA authentication config', true),
|
||||
'severity': 'high',
|
||||
'remediation': 'aaa authentication ssh console TACACS+ LOCAL\naaa authentication enable console TACACS+ LOCAL'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── FR5 · SR 5.2: Default deny — check access-group on interfaces ─────────
|
||||
|
||||
- block:
|
||||
- name: "Gather: Interface ACL bindings and ACL counts"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show running-config | include access-group
|
||||
- show access-list | include elements
|
||||
register: _acl_cfg
|
||||
|
||||
- name: "Evaluate: FW-RDF-03 — Access lists applied inbound on all interfaces"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-RDF-03',
|
||||
'category': 'FR5 — Restricted Data Flow',
|
||||
'requirement': 'SR 5.2 — Zone Boundary Protection',
|
||||
'description': 'Access control lists shall be applied inbound on all zone-facing interfaces',
|
||||
'passed': (_acl_cfg.stdout[0] | regex_findall('access-group.*in interface') | length > 0),
|
||||
'expected': 'At least one access-group <name> in interface <name>',
|
||||
'actual': _acl_cfg.stdout[0] | regex_findall('access-group[^\n]+') | join(' | ') | default('No access-group statements', true),
|
||||
'severity': 'critical',
|
||||
'remediation': 'access-group <ACL_NAME> in interface <outside|ics_zone>'
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── HITL · FR5 · SR 5.2: Firewall rule review ────────────────────────────
|
||||
# Collect the full ACL and ask the reviewer if rules are minimal / correct.
|
||||
|
||||
- block:
|
||||
- name: "Gather: [HITL] Full access-list detail for review"
|
||||
cisco.asa.asa_command:
|
||||
commands:
|
||||
- show access-list
|
||||
register: _full_acl
|
||||
|
||||
- name: "Display: [HITL] FW-RDF-HITL-01 — ACL rule review"
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
══════════════════════════════════════════════════════════════
|
||||
MANUAL REVIEW REQUIRED · FW-RDF-HITL-01 · {{ inventory_hostname }}
|
||||
══════════════════════════════════════════════════════════════
|
||||
Requirement : SR 5.2 — Zone Boundary Protection
|
||||
Check : Firewall rules implement least-privilege; no
|
||||
'permit any any' or broad permit rules exist
|
||||
|
||||
Access list summary
|
||||
───────────────────
|
||||
{{ _full_acl.stdout[0] | truncate(1200, false) | indent(1) }}
|
||||
|
||||
Verify:
|
||||
- No 'permit ip any any' or 'permit any any' rules present
|
||||
- Rules are specific (source/destination/service all named)
|
||||
- Each rule has a documented business justification
|
||||
- Implicit deny at the end of each list
|
||||
══════════════════════════════════════════════════════════════
|
||||
|
||||
- name: "Prompt: FW-RDF-HITL-01 — verdict for {{ inventory_hostname }}"
|
||||
ansible.builtin.pause:
|
||||
prompt: |
|
||||
Do the firewall ACLs implement least-privilege with no broad permit rules?
|
||||
Enter verdict [pass / fail / skip]:
|
||||
register: _hitl_acl_verdict
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Prompt: FW-RDF-HITL-01 — notes on failure"
|
||||
ansible.builtin.pause:
|
||||
prompt: "Describe the offending rule(s) (e.g. 'ACL OUTSIDE line 3: permit ip any any'):"
|
||||
register: _hitl_acl_notes
|
||||
delegate_to: localhost
|
||||
when: _hitl_acl_verdict.user_input | lower | trim in ['fail', 'f']
|
||||
|
||||
- name: "Evaluate: FW-RDF-HITL-01"
|
||||
ansible.builtin.set_fact:
|
||||
test_results: "{{ test_results + [{
|
||||
'test_id': 'FW-RDF-HITL-01',
|
||||
'category': 'FR5 — Restricted Data Flow',
|
||||
'requirement': 'SR 5.2 — Zone Boundary Protection',
|
||||
'description': 'Firewall rules shall implement least-privilege; no broad permit rules',
|
||||
'passed': (
|
||||
'skipped' if (_hitl_acl_verdict.user_input | lower | trim in ['skip', 's', ''])
|
||||
else (_hitl_acl_verdict.user_input | lower | trim in ['pass', 'p'])
|
||||
),
|
||||
'expected': 'All permit rules are specific (src/dst/svc); no permit any any',
|
||||
'actual': 'Full ACL captured — see report evidence',
|
||||
'severity': 'critical',
|
||||
'remediation': 'Replace broad permit rules with specific source/destination/service entries',
|
||||
'reviewer': ansible_user_id,
|
||||
'notes': (_hitl_acl_notes.user_input | trim) if _hitl_acl_notes is defined else ''
|
||||
}] }}"
|
||||
ignore_errors: yes
|
||||
|
||||
# ── Generate report ────────────────────────────────────────────────────────
|
||||
|
||||
- name: "Generate compliance report"
|
||||
ansible.builtin.include_tasks: ../library/report.yml
|
||||
Reference in New Issue
Block a user