cyber-0 updated with more examples

This commit is contained in:
Ole
2026-08-17 10:42:32 +02:00
parent 0658f743b4
commit 5d114b551a
16 changed files with 2520 additions and 24 deletions
+70
View File
@@ -0,0 +1,70 @@
---
# ══════════════════════════════════════════════════════════════════════
# TEMPLATE: Automated Shell-Based Test
# ══════════════════════════════════════════════════════════════════════
#
# The standard gather → evaluate pattern used throughout this framework.
# Copy this block into the appropriate FR suite file (suites/frN_*.yml)
# and fill in all UPPERCASE placeholders.
#
# How to use:
# 1. Copy the block below into suites/frN_category.yml
# 2. Replace every UPPERCASE placeholder
# 3. Write your gather shell command to produce meaningful stdout
# 4. Write the 'passed' Jinja2 expression that evaluates the result
# 5. Set severity: critical | high | medium | low
#
# Pass/fail expression patterns:
#
# # Empty output means no findings (good):
# 'passed': (_result.stdout | trim | length == 0),
#
# # Numeric threshold (value must exist and be within range):
# 'passed': (
# _result.stdout | trim | regex_search('^[0-9]+$') and
# (_result.stdout | trim | int > 0) and
# (_result.stdout | trim | int <= 90)
# ),
#
# # Extract a number from labelled output (e.g. "minlen = 14"):
# 'passed': (
# (_result.stdout | regex_search('label\s*=\s*(\d+)', '\1')
# | default(['0'], true) | first | int) >= 14
# ),
#
# # String match:
# 'passed': (_result.stdout | trim == 'expected_value'),
#
# # Specific value is absent:
# 'passed': ('dangerous_string' not in _result.stdout),
#
# ══════════════════════════════════════════════════════════════════════
# ── SUITE_ID: SHORT_DESCRIPTION ─────────────────────────────────────
- block:
- name: "Gather: DESCRIBE_WHAT_IS_COLLECTED"
ansible.builtin.shell: |
# Replace with your data collection command.
# Guidelines:
# - Use grep/awk/cut to narrow output to only the relevant data.
# - Produce empty stdout when no finding exists (makes 'passed' easy).
# - Exit 0 always; let Ansible evaluate the output, not the exit code.
echo "replace_me"
register: _result
changed_when: false
- name: "Evaluate: TEST_ID"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'TEST_ID',
'category': 'FR_NUMBER — CATEGORY_NAME',
'requirement': 'SR X.Y — REQUIREMENT_NAME',
'description': 'One-line description of what is being checked',
'passed': (_result.stdout | trim | length == 0),
'expected': 'What a passing system looks like',
'actual': (_result.stdout | trim | default('OK', true)),
'severity': 'high',
'remediation': 'Exact command or configuration change to fix this finding'
}] }}"
ignore_errors: yes
+73
View File
@@ -0,0 +1,73 @@
---
# ══════════════════════════════════════════════════════════════════════
# TEMPLATE: File Permission / Ownership Check
# ══════════════════════════════════════════════════════════════════════
#
# Uses ansible.builtin.stat — no shell command needed.
# Prefer this over shelling out to stat(1) for file attribute checks.
# The stat module returns a structured dict with typed values, which
# makes the 'passed' expression straightforward and readable.
#
# Useful stat attributes:
# stat.exists — bool: file is present
# stat.mode — string: octal permissions, e.g. '0640'
# stat.pw_name — string: owning user name, e.g. 'root'
# stat.gr_name — string: owning group name, e.g. 'shadow'
# stat.size — int: file size in bytes
# stat.isreg — bool: is a regular file
# stat.isdir — bool: is a directory
# stat.islnk — bool: is a symlink
#
# Common 'passed' expression patterns:
#
# # File exists with exact owner/group/mode:
# 'passed': (
# _stat.stat.exists and
# _stat.stat.pw_name == 'root' and
# _stat.stat.gr_name == 'root' and
# _stat.stat.mode == '0640'
# ),
#
# # File must NOT exist:
# 'passed': not _stat.stat.exists,
#
# # File must be a regular file (not a symlink) with tight permissions:
# 'passed': (
# _stat.stat.exists and
# _stat.stat.isreg and
# not _stat.stat.islnk and
# _stat.stat.mode in ['0400', '0440', '0600']
# ),
#
# ══════════════════════════════════════════════════════════════════════
# ── SUITE_ID: SHORT_DESCRIPTION ─────────────────────────────────────
- block:
- name: "Gather: Stat /path/to/file"
ansible.builtin.stat:
path: /path/to/file
register: _stat
- name: "Evaluate: TEST_ID"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'TEST_ID',
'category': 'FR_NUMBER — CATEGORY_NAME',
'requirement': 'SR X.Y — REQUIREMENT_NAME',
'description': '/path/to/file shall be owned by root:root with mode 0640',
'passed': (
_stat.stat.exists and
_stat.stat.pw_name == 'root' and
_stat.stat.gr_name == 'root' and
_stat.stat.mode == '0640'
),
'expected': 'root:root 0640',
'actual': (
(_stat.stat.pw_name + ':' + _stat.stat.gr_name + ' ' + _stat.stat.mode)
if _stat.stat.exists else 'FILE NOT FOUND'
),
'severity': 'high',
'remediation': 'chown root:root /path/to/file && chmod 0640 /path/to/file'
}] }}"
ignore_errors: yes
+107
View File
@@ -0,0 +1,107 @@
---
# ══════════════════════════════════════════════════════════════════════
# TEMPLATE: Human-in-the-Loop (HITL) Test
# ══════════════════════════════════════════════════════════════════════
#
# Use when a control cannot be evaluated automatically and requires a
# human reviewer to observe evidence and record a verdict.
#
# Examples of controls that need HITL:
# - Physical access controls / badge logs
# - Operator training records
# - Network diagram review
# - Custom application security configuration
# - Vendor-specific proprietary interfaces
#
# How it works (Ansible-native):
# 1. Gather tasks run against the remote host as normal.
# 2. ansible.builtin.debug displays the evidence on the console.
# 3. ansible.builtin.pause with 'delegate_to: localhost' prompts the
# reviewer on the control node, regardless of the remote target.
# For multi-host runs, the prompt fires once per host so each
# target gets an independent human verdict.
# 4. The reviewer's verdict (pass/fail/skip) and any notes are
# captured in the test_results[] record alongside the raw evidence.
#
# 'passed' field values used here:
# true — reviewer entered 'pass' or 'p'
# false — reviewer entered 'fail' or 'f'
# 'skipped' — reviewer pressed Enter or entered 'skip'/'s'
#
# All three values are handled by the report renderers.
#
# ══════════════════════════════════════════════════════════════════════
# ── SUITE_ID: SHORT_DESCRIPTION ─────────────────────────────────────
- block:
# ── Gather evidence from the remote host ────────────────────────
- name: "Gather: [HITL] DESCRIBE_WHAT_IS_COLLECTED"
ansible.builtin.shell: |
# Collect the evidence the reviewer needs to make a decision.
# Keep output focused: show only what is relevant to the check.
echo "Replace with your evidence-gathering command"
register: _hitl_evidence
changed_when: false
# ── Present the evidence to the reviewer (appears in Ansible log) ─
- name: "Display: [HITL] TEST_ID — evidence for review"
ansible.builtin.debug:
msg: |
══════════════════════════════════════════════════════════════
MANUAL REVIEW REQUIRED · TEST_ID · {{ inventory_hostname }}
══════════════════════════════════════════════════════════════
Requirement : SR X.Y — REQUIREMENT_NAME
Check : DESCRIPTION
Evidence
────────
{{ _hitl_evidence.stdout | default('(no output collected)') | indent(1) }}
══════════════════════════════════════════════════════════════
# ── Reviewer enters verdict on the control node ─────────────────
# delegate_to: localhost ensures the prompt appears locally even
# when this playbook targets remote hosts.
- name: "Prompt: TEST_ID — verdict for {{ inventory_hostname }}"
ansible.builtin.pause:
prompt: |
Review the evidence above for {{ inventory_hostname }}.
Does it satisfy SR X.Y — REQUIREMENT_NAME?
Enter verdict [pass / fail / skip]:
register: _hitl_verdict
delegate_to: localhost
# ── Capture reviewer notes on failure ───────────────────────────
# This task only runs when the verdict is fail/f, so _hitl_notes
# may be undefined for pass/skip results. The evaluate task below
# uses 'is defined' to handle this safely.
- name: "Prompt: TEST_ID — notes for {{ inventory_hostname }} (fail only)"
ansible.builtin.pause:
prompt: "Describe the gap or finding (required for audit trail):"
register: _hitl_notes
delegate_to: localhost
when: _hitl_verdict.user_input | lower | trim in ['fail', 'f']
# ── Evaluate: record verdict + evidence in test_results[] ───────
- name: "Evaluate: TEST_ID"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'TEST_ID',
'category': 'FR_NUMBER — CATEGORY_NAME',
'requirement': 'SR X.Y — REQUIREMENT_NAME',
'description': 'DESCRIPTION',
'passed': (
'skipped'
if (_hitl_verdict.user_input | lower | trim in ['skip', 's', ''])
else (_hitl_verdict.user_input | lower | trim in ['pass', 'p'])
),
'expected': 'Reviewer confirmed control is in place',
'actual': _hitl_evidence.stdout | trim | default('(no evidence collected)', true),
'severity': 'SEVERITY',
'remediation': 'REMEDIATION',
'reviewer': ansible_user_id,
'notes': (_hitl_notes.user_input | trim)
if _hitl_notes is defined
else ''
}] }}"
ignore_errors: yes
+107
View File
@@ -0,0 +1,107 @@
---
# ══════════════════════════════════════════════════════════════════════
# TEMPLATE: Service State Check
# ══════════════════════════════════════════════════════════════════════
#
# Uses ansible.builtin.service_facts — no shell command needed.
# service_facts gathers all service states into ansible_facts.services
# as a dict keyed by service name. Prefer this over shelling out to
# systemctl for any service-related check.
#
# IMPORTANT — run service_facts ONCE per suite, not once per test.
# Put this gather task at the TOP of your suite file:
#
# - name: "Gather: Load all service states"
# ansible.builtin.service_facts:
#
# Then each test block below can query ansible_facts.services without
# running additional commands.
#
# Service dict structure (ansible_facts.services['sshd.service']):
# name: 'sshd.service'
# state: 'running' | 'stopped' | 'failed' | 'inactive'
# status: 'enabled' | 'disabled' | 'masked' | 'static' | 'unknown'
#
# Common 'passed' expression patterns:
#
# # Service must be running and enabled:
# 'passed': (
# ansible_facts.services['sshd.service'] is defined and
# ansible_facts.services['sshd.service'].state == 'running' and
# ansible_facts.services['sshd.service'].status == 'enabled'
# ),
#
# # Service must NOT be running (insecure service check):
# 'passed': (
# ansible_facts.services['telnet.socket'] is not defined or
# ansible_facts.services['telnet.socket'].state != 'running'
# ),
#
# # Any of several insecure services must all be absent/inactive:
# 'passed': (
# ['telnet.socket', 'rsh.socket', 'ftp.service']
# | map('extract', ansible_facts.services)
# | select('defined')
# | selectattr('state', 'equalto', 'running')
# | list | length == 0
# ),
#
# ══════════════════════════════════════════════════════════════════════
# ── Put this ONCE at the top of the suite file ───────────────────────
#
# - name: "Gather: Load all service states (suite-wide)"
# ansible.builtin.service_facts:
#
# ── Per-test blocks below ────────────────────────────────────────────
# ── SUITE_ID: Service must be running ───────────────────────────────
- block:
- name: "Evaluate: TEST_ID — SERVICE_NAME is running and enabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'TEST_ID',
'category': 'FR_NUMBER — CATEGORY_NAME',
'requirement': 'SR X.Y — REQUIREMENT_NAME',
'description': 'SERVICE_NAME shall be running and enabled at boot',
'passed': (
ansible_facts.services['SERVICE_NAME.service'] is defined and
ansible_facts.services['SERVICE_NAME.service'].state == 'running' and
ansible_facts.services['SERVICE_NAME.service'].status == 'enabled'
),
'expected': 'SERVICE_NAME: state=running, status=enabled',
'actual': (
'state=' + ansible_facts.services['SERVICE_NAME.service'].state
+ ', status=' + ansible_facts.services['SERVICE_NAME.service'].status
) if ansible_facts.services['SERVICE_NAME.service'] is defined
else 'SERVICE_NAME.service: not found in service facts',
'severity': 'high',
'remediation': 'systemctl enable --now SERVICE_NAME'
}] }}"
ignore_errors: yes
# ── SUITE_ID: Insecure service must NOT be running ──────────────────
- block:
- name: "Evaluate: TEST_ID — INSECURE_SERVICE_NAME is not running"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'TEST_ID',
'category': 'FR_NUMBER — CATEGORY_NAME',
'requirement': 'SR X.Y — REQUIREMENT_NAME',
'description': 'INSECURE_SERVICE_NAME shall be disabled and not running',
'passed': (
ansible_facts.services['INSECURE_SERVICE_NAME.service'] is not defined or
ansible_facts.services['INSECURE_SERVICE_NAME.service'].state != 'running'
),
'expected': 'INSECURE_SERVICE_NAME: absent or not running',
'actual': (
'state=' + ansible_facts.services['INSECURE_SERVICE_NAME.service'].state
) if ansible_facts.services['INSECURE_SERVICE_NAME.service'] is defined
else 'not installed',
'severity': 'critical',
'remediation': 'systemctl disable --now INSECURE_SERVICE_NAME'
}] }}"
ignore_errors: yes