108 lines
5.7 KiB
YAML
108 lines
5.7 KiB
YAML
---
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# 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
|