Files
ansible-testing/methodologies/ansible/playbooks/templates/test_hitl.yml
T

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