cyber-0 updated with more examples
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user