2026-07-12 09:39:55 +01:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# smoke-test.sh — Violin Kali/Parrot Release Readiness Check
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
# Exit non-zero on any failure. Runs from anywhere in the repo tree.
|
|
|
|
|
# Usage:
|
|
|
|
|
# ./scripts/smoke-test.sh # full check
|
|
|
|
|
# ./scripts/smoke-test.sh --no-install # skip install/smoke/cleanup cycle
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
# ── Determine repo root ──────────────────────────────────────────────────────
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
|
|
|
|
|
|
python3 scripts/violin_guard.py check-release
|
|
|
|
|
|
|
|
|
|
SKILL_DIR="skills/pentest"
|
|
|
|
|
PLAYBOOK_DIR="$SKILL_DIR/playbooks"
|
|
|
|
|
REF_DIR="$SKILL_DIR/references"
|
|
|
|
|
TEMPLATES_DIR="$SKILL_DIR/templates"
|
|
|
|
|
|
|
|
|
|
SKIP_INSTALL=false
|
|
|
|
|
[[ "${1:-}" == "--no-install" ]] && SKIP_INSTALL=true
|
|
|
|
|
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
FAILURES=""
|
|
|
|
|
|
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
pass() { echo " [✓] $1"; ((PASS+=1)); }
|
|
|
|
|
fail() { echo " [✗] $1"; ((FAIL+=1)); FAILURES+=" - $1"$'\n'; }
|
|
|
|
|
header(){ echo ""; echo "━━━ $1 ━━━"; }
|
|
|
|
|
summary(){ echo ""; echo "────────────────────────────────────────"; echo " PASS: $PASS FAIL: $FAIL"; echo "────────────────────────────────────────"; }
|
|
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
seed_history_fixture(){
|
|
|
|
|
python3 - "$1" "$2" "$3" <<'PY'
|
|
|
|
|
import sys
|
|
|
|
|
from plugins.violin_guard import history
|
|
|
|
|
history.append_history(sys.argv[1], sys.argv[2], sys.argv[3], 0)
|
|
|
|
|
PY
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 09:39:55 +01:00
|
|
|
# =============================================================================
|
|
|
|
|
# 1. YAML Validity
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "1. YAML Validity"
|
|
|
|
|
|
|
|
|
|
for yaml_file in distribution.yaml config.yaml "$TEMPLATES_DIR"/scope-template.yaml; do
|
|
|
|
|
fname="$(basename "$yaml_file")"
|
|
|
|
|
if python3 -c "
|
|
|
|
|
import yaml, sys
|
|
|
|
|
try:
|
|
|
|
|
yaml.safe_load(open('$yaml_file', encoding='utf-8'))
|
|
|
|
|
print('OK')
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f'FAIL: {e}')
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
" 2>/dev/null; then
|
|
|
|
|
pass "YAML valid: $fname"
|
|
|
|
|
else
|
|
|
|
|
fail "YAML invalid: $fname"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 2. Reference Files Exist
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "2. Reference Files Exist"
|
|
|
|
|
|
|
|
|
|
if [ -d "$REF_DIR" ]; then
|
|
|
|
|
ref_count=0
|
|
|
|
|
while IFS= read -r -d '' ref; do
|
|
|
|
|
echo " [✓] $(basename "$ref")"
|
|
|
|
|
((ref_count+=1))
|
|
|
|
|
done < <(find "$REF_DIR" -maxdepth 1 -type f -name '*.md' -print0)
|
|
|
|
|
pass "Reference files present: $ref_count"
|
|
|
|
|
else
|
|
|
|
|
fail "Reference directory missing: $REF_DIR"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Check additional referenced paths in SKILL.md ──────────────────────────
|
|
|
|
|
# Also verify that referenced subpaths mentioned in SKILL.md actually exist
|
|
|
|
|
missing_ref=0
|
|
|
|
|
for path in \
|
|
|
|
|
"$SKILL_DIR/references/tool-discovery.md" \
|
|
|
|
|
"$SKILL_DIR/references/tool-catalog.md" \
|
|
|
|
|
"$SKILL_DIR/references/kali-parrot-paths.md" \
|
|
|
|
|
"$SKILL_DIR/references/cve-apis.md" \
|
|
|
|
|
"$SKILL_DIR/references/standards.md" \
|
|
|
|
|
"$SKILL_DIR/references/methodology.md" \
|
|
|
|
|
"$SKILL_DIR/references/retrospective.md" \
|
|
|
|
|
; do
|
|
|
|
|
if [ -f "$path" ]; then
|
|
|
|
|
pass "Referenced file exists: $(basename "$path")"
|
|
|
|
|
else
|
|
|
|
|
fail "Missing referenced file: $path"
|
|
|
|
|
((missing_ref+=1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 3. No Stale ./evidence Paths in Playbooks
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "3. Stale Path Check"
|
|
|
|
|
|
|
|
|
|
# Check for literal ./evidence, ./scope, ./report paths in playbooks only
|
|
|
|
|
# (templates may legitimately mention ./scope.yaml as a copy instruction)
|
|
|
|
|
if grep -rn '\./evidence\b\|\./scope\b\|\./report\b' "$PLAYBOOK_DIR" --include='*.md' 2>/dev/null; then
|
|
|
|
|
fail "Stale ./ paths found in playbooks (above)"
|
|
|
|
|
else
|
|
|
|
|
pass "No stale ./evidence/./scope/./report paths in playbooks"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Also check skills/pentest-references for stale paths
|
|
|
|
|
if grep -rn '\./evidence\b' "$SKILL_DIR/references" --include='*.md' 2>/dev/null; then
|
|
|
|
|
fail "Stale ./evidence paths in references/ (above)"
|
|
|
|
|
else
|
|
|
|
|
pass "No stale ./evidence paths in references/"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check templates for stale paths (except the intentional copy-instruction)
|
|
|
|
|
stale_in_templates=$(grep -rn '\./evidence\b' "$TEMPLATES_DIR" --include='*.yaml' --include='*.md' 2>/dev/null || true)
|
|
|
|
|
if [ -n "$stale_in_templates" ]; then
|
|
|
|
|
# scope-template.yaml intentionally says "./scope.yaml" — that's OK
|
|
|
|
|
echo " (template ./paths are intentional — scope-template.yaml copy instruction)"
|
|
|
|
|
pass "Template paths are intentional references, not stale"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-12 09:39:55 +01:00
|
|
|
# =============================================================================
|
|
|
|
|
# 3.5 Bootstrap Enforcement — guard hard-blocks unbooted target interaction
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "3.5 Bootstrap Enforcement"
|
|
|
|
|
|
|
|
|
|
SMOKE_ENG="engagements/_smoke-bootstrap-$$"
|
|
|
|
|
rm -rf "$SMOKE_ENG"
|
|
|
|
|
|
|
|
|
|
# (a) Unbooted: check-bootstrap must exit 1 with BOOTSTRAP REQUIRED
|
|
|
|
|
set +e
|
|
|
|
|
unbooted_output=$(python3 scripts/violin_guard.py check-bootstrap --eng-dir "$SMOKE_ENG" 2>&1)
|
|
|
|
|
unbooted_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$unbooted_exit" -eq 1 ] && echo "$unbooted_output" | grep -q "BOOTSTRAP REQUIRED"; then
|
|
|
|
|
pass "Unbooted check-bootstrap exits 1 with 'BOOTSTRAP REQUIRED'"
|
|
|
|
|
else
|
|
|
|
|
fail "Unbooted check-bootstrap did not behave as expected (exit=$unbooted_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (b) Unbooted: check-command with missing scope must exit 1
|
|
|
|
|
set +e
|
|
|
|
|
unbooted_cmd=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_ENG/no-scope.yaml" --phase recon --command "curl http://10.129.245.218" 2>&1)
|
|
|
|
|
unbooted_cmd_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$unbooted_cmd_exit" -eq 1 ] && echo "$unbooted_cmd" | grep -q "BOOTSTRAP REQUIRED"; then
|
|
|
|
|
pass "check-command with missing scope exits 1 with 'BOOTSTRAP REQUIRED'"
|
|
|
|
|
else
|
|
|
|
|
fail "check-command with missing scope did not block (exit=$unbooted_cmd_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (c) Bootstrapped: check-bootstrap must exit 0
|
|
|
|
|
mkdir -p "$SMOKE_ENG"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$SMOKE_ENG/state/ptt.md"
|
|
|
|
|
# Update a PTT row so stale-PTT detection doesn't fire
|
2026-08-01 21:56:15 +01:00
|
|
|
python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_ENG" --id PT-001 --status "[x]" --note "smoke bootstrap" --skill pentest --technique recon >/dev/null 2>&1
|
2026-07-12 09:39:55 +01:00
|
|
|
cp skills/pentest/templates/hypothesis-board.md "$SMOKE_ENG/hypotheses.md"
|
|
|
|
|
echo "# Command History — smoke" > "$SMOKE_ENG/state/history.md"
|
|
|
|
|
cat > "$SMOKE_ENG/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: Smoke
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-07'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
domains: [nimbus.htb]
|
|
|
|
|
ip_addresses: [10.129.245.218]
|
|
|
|
|
app_type: webapp
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing, social-engineering, persistence, stealth-evasion, malware-delivery, destructive-payloads]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-07T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
|
|
|
|
|
set +e
|
|
|
|
|
booted_output=$(python3 scripts/violin_guard.py check-bootstrap --eng-dir "$SMOKE_ENG" 2>&1)
|
|
|
|
|
booted_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$booted_exit" -eq 0 ] && echo "$booted_output" | grep -q "bootstrap complete"; then
|
|
|
|
|
pass "Bootstrapped check-bootstrap exits 0"
|
|
|
|
|
else
|
|
|
|
|
fail "Bootstrapped check-bootstrap did not exit 0 (exit=$booted_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (d) Bootstrapped in-scope: check-command must exit 0
|
|
|
|
|
set +e
|
|
|
|
|
inscope_output=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_ENG/scope/scope.yaml" --phase recon --command "curl http://10.129.245.218" 2>&1)
|
|
|
|
|
inscope_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$inscope_exit" -eq 0 ]; then
|
|
|
|
|
pass "Bootstrapped in-scope command allowed"
|
|
|
|
|
else
|
|
|
|
|
fail "Bootstrapped in-scope command blocked (exit=$inscope_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (e) Bootstrapped out-of-scope: check-command must exit 1
|
|
|
|
|
set +e
|
|
|
|
|
outofscope_output=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_ENG/scope/scope.yaml" --phase recon --command "curl http://other.example.com" 2>&1)
|
|
|
|
|
outofscope_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$outofscope_exit" -eq 1 ] && echo "$outofscope_output" | grep -q "outside approved scope"; then
|
|
|
|
|
pass "Bootstrapped out-of-scope command blocked"
|
|
|
|
|
else
|
|
|
|
|
fail "Bootstrapped out-of-scope command not blocked (exit=$outofscope_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Cleanup
|
|
|
|
|
rm -rf "$SMOKE_ENG"
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 3.6 PTT / History Guard Enforcement
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "3.6 PTT & History Guard Enforcement"
|
|
|
|
|
|
|
|
|
|
SMOKE_GUARD="engagements/_smoke-guard-$$"
|
|
|
|
|
mkdir -p "$SMOKE_GUARD"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$SMOKE_GUARD/state/ptt.md"
|
|
|
|
|
echo "# Command History — smoke" > "$SMOKE_GUARD/state/history.md"
|
|
|
|
|
cat > "$SMOKE_GUARD/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: Guard-Smoke
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-07'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
domains: [nimbus.htb]
|
|
|
|
|
ip_addresses: [10.129.245.218]
|
|
|
|
|
app_type: webapp
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-07T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
|
|
|
|
|
# (a) record-ptt updates a PTT row and exits 0
|
|
|
|
|
set +e
|
2026-08-01 21:56:15 +01:00
|
|
|
ptt_out=$(python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_GUARD" --id PT-001 --status "[x]" --note "smoke test passed" --skill pentest --technique recon 2>&1)
|
2026-07-12 09:39:55 +01:00
|
|
|
ptt_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$ptt_exit" -eq 0 ] && echo "$ptt_out" | grep -q "PT-001"; then
|
|
|
|
|
pass "record-ptt updates PT-001 row and exits 0"
|
|
|
|
|
else
|
|
|
|
|
fail "record-ptt failed (exit=$ptt_exit): $ptt_out"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (b) Verify the PTT row actually changed on disk
|
|
|
|
|
if grep -q 'PT-001.*\[x\]' "$SMOKE_GUARD/state/ptt.md" 2>/dev/null; then
|
|
|
|
|
pass "PT-001 row shows [x] on disk"
|
|
|
|
|
else
|
|
|
|
|
fail "PT-001 row did not update on disk"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (c) record-ptt with invalid status exits 1
|
|
|
|
|
set +e
|
2026-08-01 21:56:15 +01:00
|
|
|
bad_ptt=$(python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_GUARD" --id PT-001 --status INVALID --skill pentest --technique recon 2>&1 || true)
|
2026-07-12 09:39:55 +01:00
|
|
|
bad_ptt_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$bad_ptt_exit" -eq 1 ] || echo "$bad_ptt" | grep -qi "invalid"; then
|
|
|
|
|
pass "record-ptt rejects invalid status marker"
|
|
|
|
|
else
|
|
|
|
|
fail "record-ptt did not reject invalid status (exit=$bad_ptt_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (d) record-ptt with non-existent PT id exits 1
|
|
|
|
|
set +e
|
2026-08-01 21:56:15 +01:00
|
|
|
bad_id=$(python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_GUARD" --id PT-999 --status "[~]" --skill pentest --technique recon 2>&1)
|
2026-07-12 09:39:55 +01:00
|
|
|
bad_id_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$bad_id_exit" -eq 1 ] && echo "$bad_id" | grep -qi "not found"; then
|
|
|
|
|
pass "record-ptt rejects non-existent PT id"
|
|
|
|
|
else
|
|
|
|
|
fail "record-ptt did not reject bad PT id (exit=$bad_id_exit)"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
# (e) Removed administrative/model-facing commands are absent.
|
|
|
|
|
for removed in review-and-release finding sync-done record-history message-tick skill-status check-skill-loaded; do
|
|
|
|
|
set +e
|
|
|
|
|
removed_out=$(python3 scripts/violin_guard.py "$removed" --help 2>&1)
|
|
|
|
|
removed_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$removed_exit" -ne 0 ] && echo "$removed_out" | grep -qi "invalid choice"; then
|
|
|
|
|
pass "removed CLI command is absent: $removed"
|
|
|
|
|
else
|
|
|
|
|
fail "removed CLI command is still accepted: $removed"
|
|
|
|
|
fi
|
|
|
|
|
done
|
2026-07-12 09:39:55 +01:00
|
|
|
|
|
|
|
|
# (h) Stale-PTT detection: check-bootstrap warns when all rows are pristine
|
|
|
|
|
# Use a SEPARATE fresh engagement (test (a) already updated PT-001 in $SMOKE_GUARD)
|
|
|
|
|
STALE_ENG="engagements/_smoke-stale-$$"
|
|
|
|
|
mkdir -p "$STALE_ENG"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$STALE_ENG/state/ptt.md"
|
|
|
|
|
cp skills/pentest/templates/hypothesis-board.md "$STALE_ENG/hypotheses.md"
|
|
|
|
|
echo "# Command History — stale" > "$STALE_ENG/state/history.md"
|
|
|
|
|
echo "test: ok" > "$STALE_ENG/scope/scope.yaml"
|
|
|
|
|
set +e
|
|
|
|
|
stale_out=$(python3 scripts/violin_guard.py check-bootstrap --eng-dir "$STALE_ENG" 2>&1)
|
|
|
|
|
stale_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
rm -rf "$STALE_ENG"
|
|
|
|
|
if [ "$stale_exit" -eq 2 ] && echo "$stale_out" | grep -qi "never been updated"; then
|
|
|
|
|
pass "Stale PTT detection warns on pristine PTT (exit 2)"
|
|
|
|
|
else
|
|
|
|
|
fail "Stale-PTT detection unexpected (exit=$stale_exit): $stale_out"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Cleanup
|
|
|
|
|
rm -rf "$SMOKE_GUARD"
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 3.7 Freshness & Mandatory Skill-Load Gates
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "3.7 Freshness & Mandatory Skill-Load Gates"
|
|
|
|
|
|
|
|
|
|
SMOKE_FRESH="engagements/_smoke-fresh-$$"
|
|
|
|
|
mkdir -p "$SMOKE_FRESH"/{scope,state,evidence/vuln-research}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$SMOKE_FRESH/state/ptt.md"
|
|
|
|
|
cp skills/pentest/templates/hypothesis-board.md "$SMOKE_FRESH/hypotheses.md"
|
|
|
|
|
# Replace placeholder hypothesis with a valid one so the hypothesis guard passes
|
|
|
|
|
cat > "$SMOKE_FRESH/hypotheses.md" <<'MD'
|
|
|
|
|
# Hypothesis Board
|
|
|
|
|
|
|
|
|
|
## Active Theories
|
|
|
|
|
|
|
|
|
|
### H-001: web RCE
|
|
|
|
|
- **Status:** researching
|
|
|
|
|
- **Phase:** RECON
|
|
|
|
|
- **Target:** 10.129.45.113
|
|
|
|
|
- **Vuln class:** RCE
|
|
|
|
|
- **Rationale:** testing
|
|
|
|
|
- **Evidence:** evidence/recon/active/
|
|
|
|
|
- **Next step:** confirm
|
|
|
|
|
- **Linked findings:** none
|
|
|
|
|
- **Updated:** $(date '+%Y-%m-%d %H:%M')
|
|
|
|
|
MD
|
|
|
|
|
echo "# Command History — fresh" > "$SMOKE_FRESH/state/history.md"
|
2026-07-18 08:02:50 +01:00
|
|
|
seed_history_fixture "$SMOKE_FRESH" "nmap 10.129.45.113" RECON
|
2026-07-12 09:39:55 +01:00
|
|
|
# PTT "Last updated" set to now
|
|
|
|
|
sed -i "s|<YYYY-MM-DD HH:MM>|$(date '+%Y-%m-%d %H:%M')|" "$SMOKE_FRESH/state/ptt.md"
|
|
|
|
|
# Mark one RECON row done so desync detection has a baseline
|
2026-08-01 21:56:15 +01:00
|
|
|
python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_FRESH" --id PT-016 --status "[x]" --note "nmap done" --skill pentest --technique recon >/dev/null 2>&1
|
2026-07-12 09:39:55 +01:00
|
|
|
# Findings file present (non-empty) once vuln-research underway
|
|
|
|
|
echo "## Findings" > "$SMOKE_FRESH/evidence/vuln-research/findings.md"
|
|
|
|
|
# Skill-load marker for session 'fresh'
|
|
|
|
|
touch "$SMOKE_FRESH/state/.skill-loaded-fresh"
|
|
|
|
|
cat > "$SMOKE_FRESH/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: Fresh-Smoke
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-08'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
domains: [fresh.htb]
|
|
|
|
|
ip_addresses: [10.129.45.113]
|
|
|
|
|
app_type: webapp
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-08T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
|
|
|
|
|
# (a) Target-touching command WITH skill marker + --session-id passes (exit 0)
|
|
|
|
|
set +e
|
|
|
|
|
fresh_ok=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_FRESH/scope/scope.yaml" --eng-dir "$SMOKE_FRESH" --session-id fresh --phase recon --command "nmap 10.129.45.113" 2>&1)
|
|
|
|
|
fresh_ok_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$fresh_ok_exit" -ne 1 ]; then
|
|
|
|
|
pass "Fresh engagement: target command passes skill gate with marker + --session-id (REVIEW warnings allowed)"
|
|
|
|
|
else
|
|
|
|
|
fail "Fresh engagement: target command unexpectedly blocked (exit=$fresh_ok_exit): $fresh_ok"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (b) Target-touching command WITHOUT --session-id/--skill-loaded-file is BLOCKED (Gap #1 fix)
|
|
|
|
|
set +e
|
|
|
|
|
fresh_noskill=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_FRESH/scope/scope.yaml" --eng-dir "$SMOKE_FRESH" --phase recon --command "nmap 10.129.45.113" 2>&1)
|
|
|
|
|
fresh_noskill_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
if [ "$fresh_noskill_exit" -eq 1 ] && echo "$fresh_noskill" | grep -q "skill load gate"; then
|
|
|
|
|
pass "Gap #1 fix: target command blocked when skill-load marker missing/omitted"
|
|
|
|
|
else
|
|
|
|
|
fail "Gap #1 fix: missing skill-load gate did not block (exit=$fresh_noskill_exit): $fresh_noskill"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (c) Stale PTT (no 'Last updated') raises REVIEW (exit 2, Gap #2)
|
|
|
|
|
SMOKE_STALEPTT="engagements/_smoke-staleptt-$$"
|
|
|
|
|
mkdir -p "$SMOKE_STALEPTT"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$SMOKE_STALEPTT/state/ptt.md"
|
|
|
|
|
# remove the Last updated line entirely
|
|
|
|
|
sed -i '/Last updated/d' "$SMOKE_STALEPTT/state/ptt.md"
|
|
|
|
|
cp skills/pentest/templates/hypothesis-board.md "$SMOKE_STALEPTT/hypotheses.md"
|
|
|
|
|
# Overwrite with an active (researching) hypothesis so the hypothesis guard passes;
|
|
|
|
|
# the stale signal we test is the PTT missing 'Last updated', not the hypothesis guard.
|
|
|
|
|
cat > "$SMOKE_STALEPTT/hypotheses.md" <<'MD'
|
|
|
|
|
# Hypothesis Board
|
|
|
|
|
|
|
|
|
|
## Active Theories
|
|
|
|
|
|
|
|
|
|
### H-001: stale ptt test
|
|
|
|
|
- **Status:** researching
|
|
|
|
|
- **Phase:** EXPLOITATION
|
|
|
|
|
- **Target:** 10.129.45.113
|
|
|
|
|
- **Vuln class:** RCE
|
|
|
|
|
- **Rationale:** test
|
|
|
|
|
- **Evidence:** x
|
|
|
|
|
- **Next step:** confirm
|
|
|
|
|
- **Linked findings:** none
|
|
|
|
|
- **Updated:** 2026-07-08 00:00
|
|
|
|
|
MD
|
|
|
|
|
echo "# Command History" > "$SMOKE_STALEPTT/state/history.md"
|
2026-07-18 08:02:50 +01:00
|
|
|
seed_history_fixture "$SMOKE_STALEPTT" "curl 10.129.45.113" EXPLOITATION
|
2026-08-01 21:56:15 +01:00
|
|
|
python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_STALEPTT" --id PT-040 --status "[~]" --note "exploiting" --skill pentest --technique exploit-validation --hypothesis-id H-001 >/dev/null 2>&1
|
2026-07-12 09:39:55 +01:00
|
|
|
touch "$SMOKE_STALEPTT/state/.skill-loaded-stale"
|
|
|
|
|
cat > "$SMOKE_STALEPTT/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: StalePTT
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-08'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
ip_addresses: [10.129.45.113]
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-08T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
set +e
|
|
|
|
|
stale_ptt_out=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_STALEPTT/scope/scope.yaml" --eng-dir "$SMOKE_STALEPTT" --session-id stale --phase exploitation --command "curl 10.129.45.113" 2>&1)
|
|
|
|
|
stale_ptt_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
rm -rf "$SMOKE_STALEPTT"
|
|
|
|
|
if [ "$stale_ptt_exit" -eq 2 ] && echo "$stale_ptt_out" | grep -q "Last updated"; then
|
|
|
|
|
pass "Gap #2 fix: stale/missing PTT 'Last updated' raises REVIEW"
|
|
|
|
|
else
|
|
|
|
|
fail "Gap #2 fix: stale PTT not flagged (exit=$stale_ptt_exit): $stale_ptt_out"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# (d) Stale hypotheses (Candidate linking FIND-) raises REVIEW (exit 2, Gap #3)
|
|
|
|
|
SMOKE_STALEHYP="engagements/_smoke-stalehyp-$$"
|
|
|
|
|
mkdir -p "$SMOKE_STALEHYP"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$SMOKE_STALEHYP/state/ptt.md"
|
|
|
|
|
sed -i "s|<YYYY-MM-DD HH:MM>|$(date '+%Y-%m-%d %H:%M')|" "$SMOKE_STALEHYP/state/ptt.md"
|
|
|
|
|
# Inject a Candidate hypothesis that already links a finding (contradiction)
|
|
|
|
|
cat > "$SMOKE_STALEHYP/hypotheses.md" <<'MD'
|
|
|
|
|
# Hypothesis Board
|
|
|
|
|
|
|
|
|
|
## Active Theories
|
|
|
|
|
|
|
|
|
|
### H-001: stale candidate
|
|
|
|
|
- **Status:** Candidate
|
|
|
|
|
- **Phase:** EXPLOITATION
|
|
|
|
|
- **Target:** 10.129.45.113
|
|
|
|
|
- **Vuln class:** RCE
|
|
|
|
|
- **Rationale:** test
|
|
|
|
|
- **Evidence:** x
|
|
|
|
|
- **Next step:** promote
|
|
|
|
|
- **Linked findings:** FIND-001
|
|
|
|
|
- **Updated:** 2026-07-08 00:00
|
|
|
|
|
MD
|
|
|
|
|
echo "# Command History" > "$SMOKE_STALEHYP/state/history.md"
|
2026-07-18 08:02:50 +01:00
|
|
|
seed_history_fixture "$SMOKE_STALEHYP" "curl 10.129.45.113" EXPLOITATION
|
2026-08-01 21:56:15 +01:00
|
|
|
python3 scripts/violin_guard.py record-ptt --eng-dir "$SMOKE_STALEHYP" --id PT-040 --status "[~]" --note "exploiting" --skill pentest --technique exploit-validation --hypothesis-id H-001 >/dev/null 2>&1
|
2026-07-12 09:39:55 +01:00
|
|
|
touch "$SMOKE_STALEHYP/state/.skill-loaded-sh"
|
|
|
|
|
cat > "$SMOKE_STALEHYP/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: StaleHyp
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-08'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
ip_addresses: [10.129.45.113]
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-08T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
set +e
|
|
|
|
|
stale_hyp_out=$(python3 scripts/violin_guard.py check-command --scope "$SMOKE_STALEHYP/scope/scope.yaml" --eng-dir "$SMOKE_STALEHYP" --session-id sh --phase exploitation --command "curl 10.129.45.113" 2>&1)
|
|
|
|
|
stale_hyp_exit=$?
|
|
|
|
|
set -e
|
|
|
|
|
rm -rf "$SMOKE_STALEHYP"
|
|
|
|
|
if [ "$stale_hyp_exit" -eq 2 ] && echo "$stale_hyp_out" | grep -q "Candidate but already links"; then
|
|
|
|
|
pass "Gap #3 fix: Candidate hypothesis linking a finding raises REVIEW"
|
|
|
|
|
else
|
|
|
|
|
fail "Gap #3 fix: stale hypothesis not flagged (exit=$stale_hyp_exit): $stale_hyp_out"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Cleanup
|
|
|
|
|
rm -rf "$SMOKE_FRESH"
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
2026-07-18 08:02:50 +01:00
|
|
|
# 3.8 Canonical Batch Review Lifecycle
|
2026-07-12 09:39:55 +01:00
|
|
|
# =============================================================================
|
2026-07-18 08:02:50 +01:00
|
|
|
header "3.8 Canonical Batch Review Lifecycle (violin_review_batch)"
|
2026-07-12 09:39:55 +01:00
|
|
|
|
|
|
|
|
GATES_DIR="engagements/_smoke-gates-$$"
|
|
|
|
|
mkdir -p "$GATES_DIR"/{scope,state,evidence}
|
|
|
|
|
cp skills/pentest/templates/ptt.md "$GATES_DIR/state/ptt.md"
|
|
|
|
|
sed -i "s|<YYYY-MM-DD HH:MM>|$(date '+%Y-%m-%d %H:%M')|" "$GATES_DIR/state/ptt.md"
|
|
|
|
|
cp skills/pentest/templates/hypothesis-board.md "$GATES_DIR/hypotheses.md"
|
|
|
|
|
# Seed an active hypothesis so the hypothesis guard passes on fresh engagement.
|
|
|
|
|
cat > "$GATES_DIR/hypotheses.md" <<'MD'
|
|
|
|
|
# Hypothesis Board
|
|
|
|
|
|
|
|
|
|
## Active Theories
|
|
|
|
|
|
|
|
|
|
### H-001: initial recon
|
|
|
|
|
- **Status:** verified
|
|
|
|
|
- **Phase:** RECON
|
|
|
|
|
- **Target:** 10.129.45.113
|
|
|
|
|
- **Vuln class:** recon
|
|
|
|
|
- **Rationale:** establishing baseline
|
|
|
|
|
- **Evidence:** evidence/recon/active/
|
|
|
|
|
- **Next step:** confirm
|
|
|
|
|
- **Linked findings:** none
|
|
|
|
|
- **Updated:** $(date '+%Y-%m-%d %H:%M')
|
|
|
|
|
MD
|
|
|
|
|
echo "# Command History" > "$GATES_DIR/state/history.md"
|
|
|
|
|
touch "$GATES_DIR/state/.skill-loaded-gate"
|
|
|
|
|
cat > "$GATES_DIR/scope/scope.yaml" <<'YAML'
|
|
|
|
|
engagement:
|
|
|
|
|
client: GateSmoke
|
|
|
|
|
tester: smoke
|
|
|
|
|
date: '2026-07-08'
|
|
|
|
|
duration: '1h'
|
|
|
|
|
targets:
|
|
|
|
|
ip_addresses: [10.129.45.113]
|
|
|
|
|
mode: active-recon
|
|
|
|
|
depth: black-box
|
|
|
|
|
rules_of_engagement:
|
|
|
|
|
max_requests_per_second: 5
|
|
|
|
|
forbidden_actions: [credential-stuffing]
|
|
|
|
|
authorisation:
|
|
|
|
|
confirmed: true
|
|
|
|
|
confirmed_by: smoke
|
|
|
|
|
confirmed_at: '2026-07-08T00:00:00Z'
|
|
|
|
|
YAML
|
|
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
# Seed the active PTT row whose identity review-batch must preserve.
|
2026-08-01 21:56:15 +01:00
|
|
|
python3 scripts/violin_guard.py record-ptt --eng-dir "$GATES_DIR" --id PT-010 --status "[~]" --note "active recon" --skill pentest --technique recon >/dev/null 2>&1
|
2026-07-18 08:02:50 +01:00
|
|
|
# Drive the canonical service handler directly.
|
2026-07-12 09:39:55 +01:00
|
|
|
python3 - "$GATES_DIR" <<'PY'
|
2026-07-18 08:02:50 +01:00
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from pathlib import Path
|
2026-07-12 09:39:55 +01:00
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
from plugins.violin_guard import history, service, state
|
2026-07-12 09:39:55 +01:00
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
engagement = Path(sys.argv[1])
|
|
|
|
|
command = "nmap -sV 10.129.45.113"
|
|
|
|
|
receipt = engagement / "evidence" / "executions" / "review.json"
|
|
|
|
|
receipt.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
state.atomic_json(receipt, {
|
|
|
|
|
"command": command,
|
|
|
|
|
"phase": "RECON",
|
|
|
|
|
"completed_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
|
|
|
"exit_code": 0,
|
|
|
|
|
"evidence_paths": {"manifest": receipt.relative_to(engagement).as_posix()},
|
|
|
|
|
})
|
|
|
|
|
history.append_history(
|
|
|
|
|
engagement, command, "RECON", 0, receipt.relative_to(engagement).as_posix()
|
|
|
|
|
)
|
|
|
|
|
state.mark_pending_sync(engagement, command, "RECON", "PT-010")
|
2026-07-12 09:39:55 +01:00
|
|
|
|
2026-07-18 08:02:50 +01:00
|
|
|
reviewed = json.loads(service.handle_review_batch({
|
|
|
|
|
"eng_dir": str(engagement),
|
|
|
|
|
"id": "PT-010",
|
|
|
|
|
"status": "[~]",
|
|
|
|
|
"note": "Reviewed the completed recon receipt",
|
|
|
|
|
}))
|
|
|
|
|
assert reviewed["status"] == "ok", reviewed
|
|
|
|
|
assert reviewed["released"] is True
|
|
|
|
|
assert not state.has_pending_sync(engagement)
|
|
|
|
|
assert "[reviewed-batch:" in (engagement / "state" / "ptt.md").read_text()
|
|
|
|
|
for removed in ("handle_sync_done", "handle_review_and_release", "handle_finding"):
|
|
|
|
|
assert not hasattr(service, removed), removed
|
|
|
|
|
print(" ok: violin_review_batch reviewed PTT and released the batch")
|
2026-07-12 09:39:55 +01:00
|
|
|
print("GATES_OK")
|
|
|
|
|
PY
|
|
|
|
|
gates_exit=$?
|
|
|
|
|
if [ "$gates_exit" -eq 0 ]; then
|
2026-07-18 08:02:50 +01:00
|
|
|
pass "3.8 Canonical review lifecycle: receipt->PTT review->batch release"
|
2026-07-12 09:39:55 +01:00
|
|
|
else
|
2026-07-18 08:02:50 +01:00
|
|
|
fail "3.8 Canonical review lifecycle failed (see python output above)"
|
2026-07-12 09:39:55 +01:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
rm -rf "$GATES_DIR"
|
|
|
|
|
|
2026-07-12 09:39:55 +01:00
|
|
|
# =============================================================================
|
|
|
|
|
# 4. Playbook Section Coverage
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "4. Playbook Section Coverage"
|
|
|
|
|
|
|
|
|
|
playbook_count=0
|
|
|
|
|
section_issues=0
|
|
|
|
|
while IFS= read -r -d '' pb; do
|
|
|
|
|
name="$(basename "$pb" .md)"
|
|
|
|
|
has_blocked=$(grep -q '^## .*Blocked\|^## Blocked' "$pb" 2>/dev/null && echo "yes" || echo "no")
|
|
|
|
|
has_evidence=$(grep -q '^## Evidence' "$pb" 2>/dev/null && echo "yes" || echo "no")
|
|
|
|
|
has_stop=$(grep -q '^## Stop' "$pb" 2>/dev/null && echo "yes" || echo "no")
|
|
|
|
|
case "$name" in
|
|
|
|
|
scoping|recon|vuln-research|exploitation|reporting|tools|post-exploitation)
|
|
|
|
|
echo " [phase b=$has_blocked e=$has_evidence s=$has_stop] $name"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo " [vuln b=$has_blocked e=$has_evidence s=$has_stop] $name"
|
|
|
|
|
if [ "$has_blocked" = "no" ] || [ "$has_evidence" = "no" ] || [ "$has_stop" = "no" ]; then
|
|
|
|
|
((section_issues+=1))
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
((playbook_count+=1))
|
|
|
|
|
done < <(find "$PLAYBOOK_DIR" -maxdepth 1 -type f -name '*.md' -print0)
|
|
|
|
|
|
|
|
|
|
pass "Playbooks counted: $playbook_count"
|
|
|
|
|
if [ "$section_issues" -gt 0 ]; then
|
|
|
|
|
fail "$section_issues per-vulnerability playbook(s) missing required sections (## Blocked, ## Evidence, ## Stop)"
|
|
|
|
|
else
|
|
|
|
|
pass "All per-vulnerability playbooks have ## Blocked, ## Evidence, ## Stop sections"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 5. Verify skill_view References in SKILL.md Files
|
|
|
|
|
# =============================================================================
|
|
|
|
|
header "5. skill_view Reference Check"
|
|
|
|
|
|
|
|
|
|
# Find all skill_view() calls across all SKILL.md files in the repo
|
|
|
|
|
sv_refs=$(grep -rn 'skill_view(' "$REPO_ROOT/skills" --include='SKILL.md' 2>/dev/null || true)
|
|
|
|
|
if [ -z "$sv_refs" ]; then
|
|
|
|
|
pass "No skill_view() references found in SKILL.md files"
|
|
|
|
|
else
|
|
|
|
|
echo " Found skill_view() calls in SKILL.md files:"
|
|
|
|
|
sv_issues=0
|
|
|
|
|
while IFS= read -r line; do
|
|
|
|
|
filepath="$(echo "$line" | cut -d: -f1)"
|
|
|
|
|
linenum="$(echo "$line" | cut -d: -f2)"
|
|
|
|
|
content="$(echo "$line" | cut -d: -f3-)"
|
|
|
|
|
echo " $filepath:$linenum"
|
|
|
|
|
|
|
|
|
|
# Extract skill name from skill_view(name='X', ...) or skill_view("X", ...)
|
|
|
|
|
sv_name=$(echo "$content" | sed -n "s/.*skill_view(name=['\"]\([^'\"]*\)['\"].*/\1/p")
|
|
|
|
|
if [ -z "$sv_name" ]; then
|
|
|
|
|
# Try skill_view("X") pattern
|
|
|
|
|
sv_name=$(echo "$content" | sed -n 's/.*skill_view(["'\'']\([^"'\'']*\)["'\''].*/\1/p')
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$sv_name" ]; then
|
|
|
|
|
echo " → Could not parse skill name from: $content"
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check if the referenced skill exists locally under skills/
|
|
|
|
|
local_skill_path="$REPO_ROOT/skills/$sv_name/SKILL.md"
|
|
|
|
|
if [ -f "$local_skill_path" ]; then
|
|
|
|
|
pass "Referenced skill '$sv_name' exists locally: $local_skill_path"
|
|
|
|
|
else
|
|
|
|
|
# Check if it's a Hermes official/installed skill
|
|
|
|
|
hermes_skill_path="$HOME/.hermes/skills/$sv_name/SKILL.md"
|
|
|
|
|
if [ -f "$hermes_skill_path" ]; then
|
|
|
|
|
pass "Referenced skill '$sv_name' exists as Hermes skill"
|
|
|
|
|
else
|
|
|
|
|
# Check for category-qualified names like official/research/domain-intel
|
|
|
|
|
# These need to be checked differently — find any SKILL.md with that name
|
|
|
|
|
found_any=$(find "$HOME/.hermes/skills" -name 'SKILL.md' -path "*/$sv_name/SKILL.md" 2>/dev/null | head -1)
|
|
|
|
|
if [ -n "$found_any" ]; then
|
|
|
|
|
pass "Referenced skill '$sv_name' found: $found_any"
|
|
|
|
|
else
|
|
|
|
|
fail "Referenced skill '$sv_name' not found locally or in Hermes skills"
|
|
|
|
|
((sv_issues+=1))
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
done <<< "$sv_refs"
|
|
|
|
|
if [ "$sv_issues" -eq 0 ]; then
|
|
|
|
|
pass "All skill_view() references resolve to existing skills"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 6. Install + Smoke Chat + Cleanup
|
|
|
|
|
# =============================================================================
|
|
|
|
|
if [ "$SKIP_INSTALL" = true ]; then
|
|
|
|
|
header "6. Install / Smoke / Cleanup (SKIPPED --no-install)"
|
|
|
|
|
pass "Skipped per --no-install flag"
|
|
|
|
|
else
|
|
|
|
|
header "6. Install / Smoke Chat / Cleanup"
|
|
|
|
|
|
|
|
|
|
SMOKE_PROFILE="violin-smoke-$(date +%s)"
|
|
|
|
|
|
|
|
|
|
# ── Install ──
|
|
|
|
|
echo " Installing profile as '$SMOKE_PROFILE'..."
|
2026-07-12 09:39:55 +01:00
|
|
|
# Under Windows git-bash, REPO_ROOT is a /c/... path that Python's pathlib
|
|
|
|
|
# mangles into \c\... — convert to a native Windows path so `hermes` (Python)
|
|
|
|
|
# resolves distribution.yaml at the repo root. No-op on native Linux/Kali.
|
|
|
|
|
if [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "cygwin"* ]]; then
|
|
|
|
|
INSTALL_SRC="$(cygpath -w "$REPO_ROOT" 2>/dev/null || echo "$REPO_ROOT")"
|
|
|
|
|
else
|
|
|
|
|
INSTALL_SRC="$REPO_ROOT"
|
|
|
|
|
fi
|
|
|
|
|
if hermes profile install "$INSTALL_SRC" --name "$SMOKE_PROFILE" -y 2>&1; then
|
2026-07-12 09:39:55 +01:00
|
|
|
pass "Profile installed: $SMOKE_PROFILE"
|
|
|
|
|
else
|
|
|
|
|
fail "Profile install failed"
|
|
|
|
|
# Still try to clean up on failure
|
|
|
|
|
hermes profile delete "$SMOKE_PROFILE" -y 2>/dev/null || true
|
|
|
|
|
summary
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Show profile info ──
|
|
|
|
|
echo " Profile info:"
|
|
|
|
|
if hermes profile show "$SMOKE_PROFILE" 2>&1; then
|
|
|
|
|
pass "profile show succeeded"
|
|
|
|
|
else
|
|
|
|
|
fail "profile show failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Tools summary ──
|
|
|
|
|
if hermes -p "$SMOKE_PROFILE" tools --summary 2>&1; then
|
|
|
|
|
pass "tools --summary succeeded"
|
|
|
|
|
else
|
|
|
|
|
fail "tools --summary failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Smoke chat ──
|
|
|
|
|
echo " Running smoke chat..."
|
|
|
|
|
if hermes -p "$SMOKE_PROFILE" chat -q "Smoke test: reply with 'Violin profile loaded and responding'" -Q 2>&1; then
|
|
|
|
|
pass "Smoke chat succeeded"
|
|
|
|
|
else
|
|
|
|
|
fail "Smoke chat failed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Config check ──
|
|
|
|
|
if hermes -p "$SMOKE_PROFILE" config check 2>&1; then
|
|
|
|
|
pass "config check succeeded"
|
|
|
|
|
else
|
|
|
|
|
fail "config check reported issues"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ── Cleanup ──
|
|
|
|
|
echo " Cleaning up profile..."
|
|
|
|
|
if hermes profile delete "$SMOKE_PROFILE" -y 2>&1; then
|
|
|
|
|
pass "Profile deleted: $SMOKE_PROFILE"
|
|
|
|
|
else
|
|
|
|
|
fail "Profile cleanup failed"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Results
|
|
|
|
|
# =============================================================================
|
|
|
|
|
summary
|
|
|
|
|
|
|
|
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
|
|
|
echo "FAILURES:" >&2
|
|
|
|
|
echo "$FAILURES" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
else
|
|
|
|
|
echo " All checks passed. Release ready."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|