--- # ══════════════════════════════════════════════════════════════════════ # 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