CYBER-0 ansible-testing update

This commit is contained in:
Ole
2026-08-31 13:35:40 +02:00
parent ca9312379a
commit 042ddc63d5
6 changed files with 60 additions and 162 deletions
+3
View File
@@ -83,6 +83,9 @@ RUN ansible-galaxy collection install \
community.crypto \
microsoft.sql
# ── Install gomplate (template renderer) ─────────────────
RUN apk add --no-cache gomplate
# ── Purge build-only dependencies ─────────────────────────
# apk del cascades to shared deps like util-linux (mount/umount).
# Re-add it with network access (not --no-network here).
+19 -21
View File
@@ -57,10 +57,8 @@ playbooks/
└── test_hitl.yml # Human-in-the-Loop with ansible.builtin.pause
reports/
├── render.go # Go renderer (text/template + external .gohtml)
├── render_report.py # Python renderer (terminal + markdown output)
├── report.gohtml # Go template for terminal box-drawing report
├── go.mod # Go module definition
├── report.gohtml # Go template rendered by gomplate (terminal box-drawing report)
└── sample-output.json # Example output for offline renderer tests
inventory.ini # Ansible inventory — all platform groups defined
run.sh # End-to-end wrapper: ansible → find JSON → render
@@ -85,7 +83,7 @@ run.sh # End-to-end wrapper: ansible → find JSON
▼
run.sh / render manually:
python3 reports/render_report.py reports/<hostname>-<date>.json
go run reports/render.go reports/<hostname>-<date>.json reports/report.gohtml
gomplate --context .=reports/<hostname>-<date>.json --file reports/report.gohtml
```
### Target Integrations
@@ -109,7 +107,7 @@ run.sh # End-to-end wrapper: ansible → find JSON
- Ansible ≥ 2.9 with the collections listed above (pre-installed in the Docker image)
- Python ≥ 3.6 (for the Python report renderer)
- Go ≥ 1.21 (optional, for the Go renderer)
- [gomplate](https://docs.gomplate.ca/installing/) (optional, single static binary, for the `.gohtml` template renderer)
- For Windows / VMware / Cisco targets: the Python libraries listed above
### Step 1: Configure Inventory
@@ -160,8 +158,8 @@ python3 reports/render_report.py reports/localhost-2026-08-14.json
# Markdown (for GitHub / GitLab wikis, PR comments):
python3 reports/render_report.py reports/localhost-2026-08-14.json --format md
# Go template renderer:
cd reports && go run render.go ../reports/localhost-2026-08-14.json report.gohtml
# gomplate template renderer:
gomplate --context .=reports/localhost-2026-08-14.json --file reports/report.gohtml
```
---
@@ -580,29 +578,31 @@ python3 reports/render_report.py reports/hostname-2026-08-14.json --format md >
Terminal output groups results by FR category with a failure-detail section.
Markdown output produces GFM tables plus per-failure sections with remediation.
### Go Renderer (`reports/render.go`)
### gomplate Renderer (`reports/report.gohtml`)
Requires Go >= 1.21. Uses `text/template` with an external `.gohtml` file:
Requires [gomplate](https://docs.gomplate.ca/installing/) (a single static
binary — no Go toolchain, no compilation). Renders a `.gohtml` file against
the JSON report loaded as the template's root context:
```bash
cd reports
go run render.go ../reports/hostname-2026-08-14.json report.gohtml
gomplate --context .=../reports/hostname-2026-08-14.json --file report.gohtml
```
The template file is standalone — customise it without recompiling. Registered
template functions:
The template file is standalone — customise it without recompiling anything.
Fields are accessed with plain dot notation (e.g. `.meta.target`), and the
template only relies on gomplate's built-in functions:
| Function | Purpose |
|---|---|
| `passIcon` | Maps `passed` value to ✅ PASS / ❌ FAIL / ❓ MANUAL |
| `severityIcon` | Maps severity to 🔴/🟠/🟡/🟢 |
| `title` | Capitalises first letter of each word |
| `divf` | Float division for compliance rate percentage |
| `add`, `sub` | Integer arithmetic |
| `passIcon` (in-template) | Maps `passed` value to ✅ PASS / ❌ FAIL / ❓ MANUAL |
| `severityIcon` (in-template) | Maps severity to 🔴/🟠/🟡/🟢 |
| `strings.Title` | Capitalises first letter of each word |
| `math.Div`, `math.Mul` | Compliance rate percentage |
Custom templates:
```bash
go run reports/render.go reports/*.json my-custom.gohtml
gomplate --context .=reports/<hostname>-<date>.json --file my-custom.gohtml
```
---
@@ -702,9 +702,7 @@ QEMU VM boots in ~6 seconds
| `playbooks/templates/test_service_check.yml` | Copy-and-fill template: `service_facts` |
| `playbooks/templates/test_hitl.yml` | Copy-and-fill template: `pause` + `delegate_to: localhost` |
| `reports/render_report.py` | Python renderer: terminal box-drawing and Markdown output |
| `reports/render.go` | Go renderer: `text/template` on external `.gohtml` |
| `reports/report.gohtml` | Go template producing the terminal box-drawing report |
| `reports/go.mod` | Go module (requires `golang.org/x/text`) |
| `reports/report.gohtml` | Go template producing the terminal box-drawing report, rendered by `gomplate` |
| `reports/sample-output.json` | Hand-crafted example report for offline renderer testing |
| `inventory.ini` | Ansible inventory with all platform groups and connection vars |
| `run.sh` | End-to-end wrapper: run ansible → find latest JSON → render |
-5
View File
@@ -1,5 +0,0 @@
module github.com/example/ansible-testing
go 1.21
require golang.org/x/text v0.14.0
-101
View File
@@ -1,101 +0,0 @@
// render.go — Go-based IEC 62443-3-3 SL2 compliance report renderer.
//
// Loads a JSON report produced by ansible-test and renders it through
// an external Go template file (report.gohtml by default).
//
// Usage:
// go run render.go <report.json> [template.gohtml]
//
// The template receives the full JSON document as an untyped map.
// Registered template functions:
// passIcon — maps passed value to ✅/❌/🔍/❓
// severityIcon — maps severity string to 🔴/🟠/🟡/🟢/⚪
// title — capitalizes first letter of each word
// divf — float64 division (a/b*100) for percentages
// add, sub — integer arithmetic
//
// Dependencies:
// golang.org/x/text v0.14.0 (for cases.Title)
package main
import (
"encoding/json"
"fmt"
"os"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: gomplate-report <report.json> [template.gohtml]\n")
os.Exit(1)
}
jsonPath := os.Args[1]
tmplPath := "reports/report.gohtml"
if len(os.Args) >= 3 {
tmplPath = os.Args[2]
}
data, err := os.ReadFile(jsonPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading JSON: %v\n", err)
os.Exit(1)
}
var report map[string]any
if err := json.Unmarshal(data, &report); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
os.Exit(1)
}
funcMap := template.FuncMap{
"passIcon": func(v any) string {
b, ok := v.(bool)
if !ok {
return "❓ MANUAL"
}
if b {
return "✅ PASS"
}
return "❌ FAIL"
},
"severityIcon": func(s string) string {
switch s {
case "critical":
return "🔴"
case "high":
return "🟠"
case "medium":
return "🟡"
case "low":
return "🟢"
}
return "⚪"
},
"title": cases.Title(language.English).String,
"divf": func(a, b int) float64 {
if b == 0 {
return 0
}
return float64(a) / float64(b) * 100.0
},
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
}
tmpl, err := template.New("report.gohtml").Funcs(funcMap).ParseFiles(tmplPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing template: %v\n", err)
os.Exit(1)
}
if err := tmpl.ExecuteTemplate(os.Stdout, "report.gohtml", report); err != nil {
fmt.Fprintf(os.Stderr, "Error rendering: %v\n", err)
os.Exit(1)
}
}
+32 -28
View File
@@ -1,59 +1,63 @@
{{- /*
report.gohtml — IEC 62443-3-3 SL2 Compliance Report Template
An external Go template rendered by reports/render.go. Receives the full
JSON document as an untyped map[string]any. Access fields via (index . "key").
Rendered directly by gomplate (https://gomplate.ca) — no custom Go binary
required. The JSON report is loaded as the root context, so fields are
accessed with plain dot notation (e.g. .meta.target).
Template functions (registered by render.go):
passIcon v → "✅ PASS" / "❌ FAIL" / "❓ MANUAL"
severityIcon s → 🔴/🟠/🟡/🟢/⚪
title s → "Hello World" (capitalizes words)
divf a b → a / b * 100.0 (percentage)
add a b / sub a b → integer arithmetic
Only gomplate's built-in functions (math.*, strings.*) are used, plus two
in-line sub-templates (passIcon/severityIcon) defined below.
To customize: copy this file, modify, run:
go run reports/render.go reports/*.json my-custom.gohtml
Usage:
gomplate --context .=reports/<hostname>-<date>.json --file reports/report.gohtml
To customize: copy this file, modify, and point --file at the copy.
*/ -}}
{{- define "passIcon" -}}
{{- if eq . true }}✅ PASS{{ else if eq . false }}❌ FAIL{{ else }}❓ MANUAL{{ end -}}
{{- end -}}
{{- define "severityIcon" -}}
{{- if eq . "critical" }}🔴{{ else if eq . "high" }}🟠{{ else if eq . "medium" }}🟡{{ else if eq . "low" }}🟢{{ else }}⚪{{ end -}}
{{- end -}}
╔══════════════════════════════════════════════════════════════════════════╗
║ IEC 62443-3-3 SECURITY LEVEL 2 — COMPLIANCE REPORT ║
╠══════════════════════════════════════════════════════════════════════════╣
║ Target: {{ printf "%-56s" (index .meta "target") }}║
║ Standard: {{ printf "%-56s" (index .meta "standard") }}║
║ Level: {{ printf "%-56s" (index .meta "security_level") }}║
║ Timestamp: {{ printf "%-56s" (index .meta "timestamp") }}║
║ Executed by: {{ printf "%-55s" (index .meta "executed_by") }}║
║ Target: {{ printf "%-56s" .meta.target }}║
║ Standard: {{ printf "%-56s" .meta.standard }}║
║ Level: {{ printf "%-56s" .meta.security_level }}║
║ Timestamp: {{ printf "%-56s" .meta.timestamp }}║
║ Executed by: {{ printf "%-55s" .meta.executed_by }}║
╠══════════════════════════════════════════════════════════════════════════╣
║ EXECUTIVE SUMMARY ║
╠══════════════════════════════════════════════════════════════════════════╣
║ ║
║ TOTAL PASSED FAILED REVIEW COMPLIANCE ║
║ ───── ────── ────── ────── ────────── ║
║ {{ printf "%-8d" (index .summary "total") }} {{ printf "%-9d" (index .summary "passed") }} {{ printf "%-9d" (index .summary "failed") }} {{ printf "%-9d" (index .summary "skipped") }} {{ if gt (index .summary "total") 0 }}{{ printf "%.1f%%" (divf (index .summary "passed") (index .summary "total")) }}{{ else }}N/A{{ end }}
║ {{ printf "%-8d" .summary.total }} {{ printf "%-9d" .summary.passed }} {{ printf "%-9d" .summary.failed }} {{ printf "%-9d" .summary.skipped }} {{ if gt .summary.total 0 }}{{ printf "%.1f%%" (mul (div .summary.passed .summary.total) 100) }}{{ else }}N/A{{ end }}
║ ║
╠══════════════════════════════════════════════════════════════════════════╣
║ RESULTS BY REQUIREMENT ║
╠══════════════════════════════════════════════════════════════════════════╣
║ ║
{{- $results := index . "results" }}
{{- range $i, $r := $results }}
║ {{ severityIcon (index $r "severity") }} [{{ index $r "test_id" }}] {{ passIcon (index $r "passed") }} {{ index $r "description" }}
║ Requirement: {{ index $r "requirement" }}
║ Expected: {{ index $r "expected" }}
║ Actual: {{ index $r "actual" }}
{{- if not (index $r "passed") }}
║ Fix: {{ index $r "remediation" }}
{{- range $i, $r := .results }}
║ {{ template "severityIcon" $r.severity }} [{{ $r.test_id }}] {{ template "passIcon" $r.passed }} {{ $r.description }}
║ Requirement: {{ $r.requirement }}
║ Expected: {{ $r.expected }}
║ Actual: {{ $r.actual }}
{{- if not $r.passed }}
║ Fix: {{ $r.remediation }}
{{- end }}
║ ║
{{- end }}
╠══════════════════════════════════════════════════════════════════════════╣
║ FAILURE DETAIL ║
╠══════════════════════════════════════════════════════════════════════════╣
{{- $failures := index . "failures" }}
{{- $failures := .failures }}
{{- if $failures }}
{{- range $i, $f := $failures }}
║ ❌ {{ index $f "test_id" }} — {{ index $f "description" }}
║ Severity: {{ index $f "severity" | title }}
║ Remediation: {{ index $f "remediation" }}
║ ❌ {{ $f.test_id }} — {{ $f.description }}
║ Severity: {{ $f.severity | strings.Title }}
║ Remediation: {{ $f.remediation }}
║ ║
{{- end }}
{{- else }}
+6 -7
View File
@@ -50,18 +50,17 @@ fi
echo ""
echo "[2/3] Latest report: $(basename "$LATEST_JSON")"
# ── Phase 3: Render with Go template ────────────────────────────
echo "[3/3] Rendering report with Go template..."
# ── Phase 3: Render with gomplate ───────────────────────────────
echo "[3/3] Rendering report with gomplate..."
echo ""
cd "$REPORT_DIR"
if ! go run render.go "$LATEST_JSON" report.gohtml 2>/dev/null; then
# Fallback: if Go isn't available, just cat the JSON
if ! gomplate --context ".=$LATEST_JSON" --file "$REPORT_DIR/report.gohtml" 2>/dev/null; then
# Fallback: if gomplate isn't available, just cat the JSON
echo "---"
echo "(Go not available; showing raw JSON summary)"
echo "(gomplate not available; showing raw JSON summary)"
python3 -c "
import json, sys
with open('$(basename "$LATEST_JSON")') as f:
with open('$LATEST_JSON') as f:
r = json.load(f)
s = r['summary']
print(f'Total: {s[\"total\"]} | Passed: {s[\"passed\"]} | Failed: {s[\"failed\"]} | Rate: {s[\"passed\"]/s[\"total\"]*100:.1f}%')