zuvo:pentest -- Hybrid Penetration Testing
White-box source-to-sink analysis combined with optional black-box runtime verification. zuvo:pentest is stack-aware: it selects sources, sinks, framework-safe patterns, and CodeSift recipes based on the detected project stack before dispatching any agents. Findings are built from a deterministic candidate schema, then merged with canonical-key deduplication and centralized scoring.
When to use: Before releases, after zuvo:security-audit flags issues that need exploit verification, testing CMS installations, verifying security fixes.
Out of scope: Static code quality (use zuvo:code-audit), broad security posture review (use zuvo:security-audit), API contract audit (use zuvo:api-audit).
Argument Parsing
| Argument | Effect |
|---|---|
[path] |
Limit analysis to a specific directory |
--url <url> |
Target URL for runtime probes (enables hybrid mode) |
--from-audit <dir> |
VERIFICATION mode -- verify PT/SEC findings from a prior audit directory |
--cms joomla|wordpress|drupal |
Force-enable CMS overlay |
--quick |
PT1 + PT3 + PT4 only (critical gates), no live probing |
--scope <path> |
Alias for [path] |
--dimensions PT1,PT2 |
Audit only specified dimensions |
--verify-live |
Enable Phase 3 exploit verification against a running app |
--rate <N> |
Requests per second cap (default: 5) |
--source |
Force white-box track only -- skip live probes even if --url present |
--runtime |
Force black-box track only -- skip source analysis. Requires --url. |
--strict-v2 |
Count the 14 v2 vuln classes toward score caps / critical-gate totals — the 11 original (incl. both GraphQL classes) plus header_injection, token_compare_bypass, weak_credential_storage added 2026-08-01. Default: warning-only grace — identical spec to security-audit's "v2 class grace" section, and the graced set is all 14, not the original 11: a class added to v2 is a v2 class, and gracing 11 of 14 would silently enforce the three newest on day one. Warning-only through the 1.4.x window, full weight in 1.5.0; v2 findings are still reported + backlogged, excluded only from gate totals; identical v2-grace: warning-only (N v2 findings reported + backlogged, excluded from gate; --strict-v2 to enforce) line. |
Mode Table
| Mode | Track | Agent Type | Max Concurrent |
|---|---|---|---|
[path] |
White-box | Source analyzers | 3 global |
--url |
Hybrid (white + black) | Source + scanner | 3 global |
--cms <type> |
CMS overlay | CMS-aware analyzers | 3 global |
--quick |
White-box (critical only) | Source only | 1 |
--verify-live |
Verification | Exploit verifier | 1 |
--from-audit |
Import / verification | Lead + verifier | 3 global |
--source |
White-box only | Source analyzers | 3 global |
--runtime |
Black-box only | Scanner agents | 3 global |
Mandatory File Loading
Read in two stages. Stage 1 before Phase 0. Stage 2 before Phase 7.
Stage 1 -- Before starting (STOP if any missing):
CORE FILES LOADED:
1. ../../rules/security.md -- READ/MISSING
2. ../../shared/includes/env-compat.md -- READ/MISSING
3. ../../shared/includes/codesift-setup.md -- READ/MISSING
4. ../../shared/includes/pentest-output-contract.md -- READ/MISSING
5. ../../shared/includes/pentest-stack-detection.md -- READ/MISSING
6. ../../shared/includes/pentest-stack-profiles.md -- READ/MISSING
7. ../../shared/includes/pentest-safe-pattern-registry.md -- READ/MISSING
8. ../../shared/includes/pentest-finding-registry.md -- READ/MISSING
9. ../../shared/includes/pentest-source-sink-registry.md -- READ/MISSING
10. ../../shared/includes/pentest-dedup-scoring.md -- READ/MISSING
11. ../../shared/includes/no-pause-protocol.md -- READ/MISSING (HARD: no mid-loop pauses across dimensions/findings)
Stage 2 -- Before Phase 7 (report writing):
12. ../../shared/includes/backlog-protocol.md -- READ/MISSING
13. ../../shared/includes/run-logger.md -- READ/MISSING
14. ../../shared/includes/retrospective.md -- READ/MISSING
15. ../../shared/includes/report-output-location.md -- READ/MISSING (resolves canonical $ZUVO_DIR — output MUST go to zuvo/audits/pentest/)
If any Stage 1 file is missing, STOP.
Environment Compatibility
Read ../../shared/includes/env-compat.md for agent dispatch, path resolution, and progress tracking.
MANDATORY TOOL CALLS — Pentest Validity Gate
This pentest is INVALID if any tool below is skipped when its trigger condition holds. "DEFERRED", "N/A", "scope too narrow" are NOT valid reasons.
| Tool | Trigger | Reason | Skip allowed? |
|---|---|---|---|
scan_secrets |
Always | PT5 secret-leakage primitives | NO |
search_patterns(injection sinks/eval/deserialize/command-injection) |
Always | PT2/PT3 sink discovery | NO |
trace_call_chain |
Every PT2/PT3 candidate sink | KEY — white-box source-to-sink tracing | NO when sinks exist |
find_references |
Every cited sink | Who reaches this dangerous sink | NO when sinks exist |
trace_route |
Backend framework detected | Entry points (sources) for source-to-sink chains | NO when backend exists |
audit_scan |
Always | Compound check covering several PT dimensions | NO |
taint_trace |
Django detected | PT2/PT3 Django taint propagation | NO when Django |
| Stack-specific (nest_audit/php_security_scan/python_audit) | Framework/language detected | Stack-specific pentest gates | NO when matches |
Forbidden escape hatches: same as security-audit (DEFERRED, N/A, codesift: unavailable, retrospective: skipped — all REJECTED).
Required POSTAMBLE: report on disk → retro appended → ~/.zuvo/append-runlog exit 0. Every HIGH/CRITICAL finding needs path/to/file.ext:LINE citation resolving in current tree (verify-audit gate).
Mandatory acknowledgment (REQUIRED — print verbatim before Phase 0)
Mandatory-tools-acknowledgment: I will run scan_secrets + search_patterns(injection sinks) + trace_call_chain (every cited sink) + find_references (every cited sink) + trace_route (when backend) + audit_scan + taint_trace (when Django) + stack-specific tools for this pentest. Every HIGH/CRITICAL finding will cite a `path/to/file.ext:LINE` resolving in the current tree.
CodeSift Integration
Use the deterministic preload helper FIRST. Run ~/.zuvo/compute-preload pentest "$PWD" before any ToolSearch. Copy the printed [CodeSift matching trace] verbatim and issue the printed ToolSearch(query="select:...") line. Math gate enforced.
Read ../../shared/includes/codesift-setup.md for the full initialization sequence.
Summary: Run the CodeSift setup from codesift-setup.md at skill start. Use the stack profile from pentest-stack-profiles.md to choose the right tools for the active stack. If CodeSift is unavailable, fall back to grep/find/manual tracing.
CodeSift Optimizations
| Task | CodeSift | Fallback |
|---|---|---|
| Scope inventory | get_file_tree or stack-specific route map tools |
find + rg --files |
| Sink discovery | search_text |
Grep |
| Trace input to sink | trace_call_chain + find_references |
Manual grep for call sites |
| Handler / view inventory | get_file_outline, search_symbols, trace_route |
Sequential Read + Grep |
| Guard / auth coverage | search_symbols + find_references |
Grep for decorators, middleware, auth helpers |
Degraded Mode (CodeSift unavailable)
| CodeSift tool | Fallback | Lost capability |
|---|---|---|
trace_call_chain |
Grep for import references |
No transitive call graph |
find_references |
Grep for callers |
Weaker constant propagation |
search_symbols |
Grep for function / class names |
Less precise matching |
get_file_outline |
Read full file |
More tokens consumed |
trace_route |
Manual route tracing | No route-to-handler map |
Safety Gates (NON-NEGOTIABLE)
Gate 1 -- Authorization
Prompt the user for explicit authorization before any probing. Block on "no".
PENETRATION TESTING requires authorization.
Target: {URL or CWD}
Do you have authorization to test this system? [y/n]
Gate 2 -- No Production
Refuse if URL lacks localhost/127.0.0.1/staging/dev. Override only with --i-know-what-im-doing.
Gate 3 -- Read-Only Default
GET requests proceed freely. POST/PUT/DELETE require per-endpoint approval. Exception: POST /auth/login.
Gate 4 -- Rate Cap
Max --rate req/s (default 5). Stop after 3 consecutive 5xx.
Gate 5 -- PII Scrub
Mask tokens, passwords, cookies, email addresses, and API keys in all output.
Gate 6 -- Database Safety
White-box: zero DB queries. Black-box: time-based detection only. Verify-live: read-only payloads.
Phase 0: Detect Mode, Stack, and Scope
0.1 Detect Mode
Source code in CWD? -> WHITE-BOX track (Phase 1 source analysis)
--url without source? -> BLACK-BOX track (Phase 1 scanner)
--from-audit? -> VERIFICATION mode (skip to Phase 3)
0.2 Detect Stack and Load Stack Rules
Follow pentest-stack-detection.md.
Required actions:
- Detect the primary backend stack.
- Detect frontend and site-framework overlays, including Next App Router, Next Pages Router, and React SPA where applicable.
- Detect headless CMS overlays.
- Detect CMS overlays, including WordPress / Joomla / Drupal.
- Detect framework overlays from
pentest-stack-detection.md. - Load the matching files from
../../rules/. - Load safe-pattern IDs from
pentest-safe-pattern-registry.md. - Load finding-type to CWE / probe mappings from
pentest-finding-registry.md. - Load source/sink discovery regexes from
pentest-source-sink-registry.md. - Select the active stack profile and any active overlays from
pentest-stack-profiles.md.
0.3 Build Authoritative Scope Inventory
Before dispatching any Phase 1 agent:
- Resolve the target scope from
[path]or--scope. - Build the complete file inventory for that scope.
- Record one authoritative
files_in_scopecount. - Pass that exact count to every agent.
Print:
SCOPE INVENTORY
Scope: src/example/
Files in scope: 92
Stack profile: python-django
0.4 CMS Detection
[ -f configuration.php ] && echo "CMS: Joomla"
[ -f wp-config.php ] && echo "CMS: WordPress"
[ -f sites/default/settings.php ] && echo "CMS: Drupal"
If CMS detected or --cms is set, enable the CMS overlay in Phase 5.
0.5 Tool Availability
which curl 2>/dev/null && echo "curl: OK" || echo "curl: MISSING"
which semgrep 2>/dev/null && echo "semgrep: available" || echo "semgrep: fallback to grep"
which testssl 2>/dev/null && echo "testssl: available" || echo "testssl: fallback to openssl"
which gitleaks 2>/dev/null && echo "gitleaks: available" || echo "gitleaks: skip secret scan"
which npm pip-audit osv-scanner 2>/dev/null | head -1 || true # SCA tool presence
0.5b Dependency Vulnerability Scan (SCA)
Follow ../../shared/includes/scanner-invocation.md (exit-code trap: a scanner exits non-zero WHEN it finds vulns — that is success, key DEGRADED on output not exit code).
Pentest run alone (no security-audit / dependency-audit in the same chain) must still cover known-vulnerable dependencies. Run SCA only when a resolved dependency tree exists — a lockfile/manifest preflight prevents a false "0 vulns" on an un-built tree:
# Preflight: SCA needs a RESOLVED, PINNED dependency tree. osv-scanner is the
# universal auditor (npm/pnpm/yarn/poetry/Pipfile/Cargo/Go/...) — prefer it; fall
# back to the ecosystem tool that actually consumes the lockfile present.
# CRITICAL: npm/pnpm/pip-audit/osv-scanner EXIT NON-ZERO WHEN VULNS ARE FOUND — a
# non-zero exit is a SUCCESSFUL scan, not a failure. So key the degraded decision on
# whether the tool produced usable JSON output, NOT on its exit code.
run_sca() { OUT="$("$@" 2>/dev/null)"; case "$OUT" in ''|null) return 1 ;; *) printf '%s\n' "$OUT"; return 0 ;; esac; }
# Audit EVERY lockfile present (polyglot/monorepo) — not just the first.
audit_lock() { # $1 = lockfile path
local LOCK="$1"
if command -v osv-scanner >/dev/null 2>&1; then
run_sca osv-scanner --format json --lockfile "$LOCK" || echo "DEGRADED: osv-scanner produced no output for $LOCK"
else case "$(basename "$LOCK")" in
package-lock.json) run_sca npm audit --json || echo "DEGRADED: npm audit needs node_modules (npm ci) for $LOCK" ;;
pnpm-lock.yaml) run_sca pnpm audit --json || echo "DEGRADED: pnpm audit needs install for $LOCK" ;;
yarn.lock) echo "DEGRADED: no yarn auditor without osv-scanner ($LOCK)" ;;
poetry.lock|Pipfile.lock) echo "DEGRADED: pip-audit audits the env not $LOCK — install osv-scanner or run in venv" ;;
requirements.txt) run_sca pip-audit -r "$LOCK" -f json && echo "NOTE: $LOCK may be unpinned" || echo "DEGRADED: pip-audit -r failed for $LOCK" ;;
*) echo "DEGRADED: $LOCK present but no auditor without osv-scanner — install osv-scanner (IC-5 reduced, NOT an IC-2 failure)" ;;
esac; fi
}
FOUND=$(find . -maxdepth 3 \( -name package-lock.json -o -name pnpm-lock.yaml -o -name yarn.lock \
-o -name poetry.lock -o -name Pipfile.lock -o -name requirements.txt -o -name Cargo.lock -o -name go.sum \) \
-not -path '*/node_modules/*' 2>/dev/null)
if [ -z "$FOUND" ]; then
echo "DEGRADED: SCA skipped (no lockfile); IC-5 class_coverage reduced, NOT an IC-2 surface-gate failure"
else
printf '%s\n' "$FOUND" | while read -r L; do [ -n "$L" ] && { echo "== SCA: $L =="; audit_lock "$L"; }; done
fi
SCA findings map to finding_type per dependency CVE. Absent SCA lowers IC-5 class_coverage and
emits the DEGRADED line — it never fails the IC-2 surface gate (a project with no lockfile is not
an un-enumerated attack surface). Never emit a "0 dependency vulns" result from an un-built tree.
Missing tools never block the audit -- they reduce coverage. Document this in the report. Check whether Playwright browser tooling is available in the current environment.
0.6 Build Phase 1 Context
Create a shared context block for all source-analysis agents:
PHASE 1 CONTEXT
Stack profile: {active profile}
Framework overlays: {overlay list}
Files in scope: {N}
Scope path: {path}
Active dimensions: {PT list}
Safe-pattern IDs: {relevant pattern_id list}
Source families: {relevant source regex families}
Sink families: {relevant sink regex families}
Materialize these logical artifacts for later phases:
phase1-contextphase1-findingsphase1-exclusionsprobe-plan
PT6 and PT7 must consume phase1-context, phase1-findings, and phase1-exclusions.
Phase 1: Source Analysis (WHITE-BOX) or Scanning (BLACK-BOX)
WHITE-BOX Track -- Source Analyzer Agents
Spawn via the task/agent dispatch model from env-compat.md. Max 3 agents in parallel globally.
Dispatch-Failure Fallback (breadth is non-negotiable)
If agent dispatch is throttled, errors out, or returns partial output (API rate-limit, token exhaustion, a workflow that dies with 0 usable output, etc.), you MUST NOT silently fall back to "inline, go deep on a few high-signal sinks and call it done." That converts a breadth tool into a spot-check and produces a falsely-complete report.
When dispatch fails, do exactly one of:
- Complete breadth inline. Re-run the remaining dimensions yourself sequentially (reduce parallelism, add backoff/retries), iterating until the union of
files_checkedreaches every file infiles_in_scopeacross all in-scope dimensions. Slower is fine; partial is not. - Declare it incomplete. If you genuinely cannot cover the full scope this run, the Coverage Gate (2.4) FAILs by construction: set
VERDICT = INCOMPLETE, label the run DEGRADED, and list every uncovered file. Do not emit a passing/partial grade.
A coverage shortfall caused by infrastructure failure is still a coverage shortfall. "The 7-agent workflow rate-limited so I checked the obvious sinks inline" is a Coverage Gate FAIL, never a PASS with a footnote.
Global Rules
Every source-analysis agent must:
- Use the active stack profile from
pentest-stack-profiles.md. - Seed discovery from
pentest-source-sink-registry.mdbefore using broader semantic search. - Match framework defenses by canonical
pattern_idfrompentest-safe-pattern-registry.md. - Resolve
cweandprobe_template_idfrompentest-finding-registry.mdwhen thetypeexists there. - Use the shared
files_in_scopeinventory from Phase 0.3. - Emit candidate records using
pentest-output-contract.md. - Classify each record as
confirmed,excluded, orneeds_review. - Never assign final IDs such as
PT3-001.
Batch 1 (parallel) -- independent sink tracing:
- PT1 (Injection: SQL, NoSQL, command, SSTI): Trace every attacker-controlled input to query/exec/eval sink using the active stack profile.
- PT2 (XSS and Output Encoding): Trace attacker-controlled input to HTML/template/output sinks using the active stack profile.
- PT5 (SSRF and External Interactions): Trace attacker-controlled URLs or hosts to outbound HTTP/file retrieval sinks. If the stack has no outbound client primitives, emit a coverage note as
status=excluded, not a finding.
Batch 2 (parallel) -- cross-endpoint reasoning:
- PT3 (Auth and Session): Session fixation, cookie flags, OAuth / OIDC state handling, password / 2FA flows, token binding, replay windows.
- PT4 (Authorization and Access Control): Guard coverage on every mutation endpoint. Guard + query filter defense in depth. IDOR / org / tenant scoping.
- Next.js
'use server'sub-check (MANDATORY when Next.js detected): enumerate EVERY module containing a'use server'directive (grep -rl "['\"]use server['\"]" --include=*.ts --include=*.tsx) and, for each exported server action, assert a first-line auth/authz guard (anauth()/getCurrentUser()/requireRole()/tenant-scope check before any data access or mutation). A'use server'export with no guard before its first DB/mutation call is a CRITICAL IDOR/broken-access finding — server actions are public POST endpoints by default, callable by anyone who can reach the route. List the un-guarded actions byfile:lineas evidence; "looks internal" is not a guard.
- Next.js
Batch 3 (parallel, after Batch 1 + Batch 2 merge):
- PT6 (Input Vectors): File uploads, CSRF, header injection, redirect edge cases, parser abuse. Must consume the merged Phase 1 context and exclusions.
- PT7 (Business Logic): TOCTOU, race conditions, state-machine bypass, price-from-client, replay windows. Must consume the merged Phase 1 context and exclusions.
Candidate Record Example
Agents emit candidate records, not final findings:
{
"schema_version": "4.0",
"dimension": "PT3",
"type": "oauth_state_missing",
"title": "OAuth implicit flow does not require state parameter",
"severity": "HIGH",
"status": "confirmed",
"reason_code": "proof_complete",
"source_agent": "PT3-auth-session",
"source_file": "src/app/oauth.py:101",
"sink_file": "src/app/oauth.py:528",
"function_symbol": "OAuthAuthorizeView.approve",
"endpoint": "GET /oauth/authorize/",
"trace": ["step1", "step2"],
"scope_ok": true,
"files_checked": 92,
"files_in_scope": 92,
"defense_present": false,
"defense_sufficient": false,
"confidence": 0.85,
"confidence_rationale": "Complete code trace with no effective defense and a clear exploit path.",
"verification_status": "trace_confirmed",
"safe_pattern_matched": [],
"canonical_key": "PT3|OAuthAuthorizeView.approve|oauth_state_missing|GET /oauth/authorize/",
"probe_template_id": "probe-oauth-state",
"suggested_probe": "GET /oauth/authorize/?response_type=token&state=",
"runtime_preconditions": ["pre-approved app exists"],
"safe_to_probe": true,
"cwe": "CWE-352"
}
BLACK-BOX Track -- Scanner Agents
Dispatch follows ../../shared/includes/execution-policy.md through env-compat. Reuse existing
authorization within that policy; session restrictions take precedence. Run each required gate
and report its actual independence or an unmet requirement.
If no source code is available, dispatch scanner agents instead. One per dimension, max 3 in parallel globally. Each follows a 4-phase cycle:
- Recon
- Experiment
- Test
- Verify
Scanner agents must still emit candidate records using pentest-output-contract.md.
Findings below confidence 0.7 are not promoted to final findings.
VERIFICATION Mode
Skip Phase 1 entirely. Parse PT/SEC findings from --from-audit and normalize them into candidate records. When the imported findings come from security-audit, apply Cross-Skill Reconciliation (IC-3) from pentest-dedup-scoring.md: merge a pentest and a security-audit record for the same {cwe, file, sink_symbol, source_or_entry} (all four present and equal — fail-closed) into one finding, resolving verification-status and severity on their two separate axes per IC-3, while keeping distinct-source records separate.
Rules:
- Preserve original IDs in audit trail only.
- Recompute
canonical_key. - Set
verification_status="needs_runtime_check"unless runtime evidence already exists. - Preserve or assign
reason_code. - If more than 50% of imported findings reference stale code (file missing or line content changed materially), warn the user and suggest re-running the underlying audit.
Phase 2: Merge, Scope Enforcement, Dedup, Coverage Sanity
Collect all Phase 1 candidate records into a single queue.
2.1 Scope Enforcement
Before any scoring or reporting:
- Check
scope_okon every candidate. - If
source_fileorsink_fileis outside the requested scope, force:status="excluded"- exclusion reason
out_of_scope
- Do not count out-of-scope records in final findings.
2.2 Deduplication
Follow pentest-dedup-scoring.md.
Do not deduplicate on file:line. Use canonical_key and merge alias evidence into one survivor.
2.3 Audit Trail and Context Artifacts
Preserve an audit_trail[] collection for:
- source agent names
- duplicate merges
- status changes
reason_code- matched safe patterns
- resolved
probe_template_id
Update these logical artifacts after merge:
phase1-findingsphase1-exclusionsprobe-plan
2.4 Coverage Gate (HARD — blocks the verdict, not a report footnote)
Follow pentest-dedup-scoring.md.
Take the union of every dimension's files_checked and compare it against the authoritative files_in_scope inventory from Phase 0.3. Compute coverage_pct = |union(files_checked)| / files_in_scope.
This is a verdict-blocking gate, not an advisory note. Going deep on a handful of high-signal sinks while leaving most of the attack surface un-enumerated is the failure this gate exists to catch:
- Every in-scope file NOT covered by any dimension must be explicitly listed with a documented reason code (
out_of_scope,no_sink_primitives,static_asset,generated, etc.). A reason of "low signal" or "looked unlikely" is not acceptable — every route handler / entry point / sink-bearing module must actually be traced or justified. coverage_gate = PASSonly whencoverage_pct ≥ 0.90AND every remaining uncovered file has a documented reason code.- Otherwise
coverage_gate = FAIL. A FAIL means the run did not cover its declared scope: setVERDICT = INCOMPLETE, capgradeatFAIL, and list the uncovered files in the report. You may not print a passing/partial grade over an un-enumerated attack surface.
Record coverage_pct, files_checked_union, uncovered_files[], and coverage_gate in findings.json.summary.
2.5 Queue Summary
Print a merge summary with separate counts:
MERGED QUEUE
Confirmed: 9
Needs Review: 2
Exclusions: 6
Duplicates collapsed: 3
Low-signal dimensions: PT1
Phase 3: Exploit Verification (CONDITIONAL -- requires --verify-live)
Skip if: --verify-live is not set. Findings remain trace_confirmed or needs_runtime_check.
Process findings sequentially (one at a time -- Gate 3 read-only).
For each confirmed finding:
- Feasibility check: endpoint reachable? auth required? environment safe?
safe_to_probe=true? - Resolve
probe_template_idfrompentest-finding-registry.md. 2b.probe_template_id == "static-only"→ DO NOT PROBE. STOP HERE for this finding. Leaveverification_status: trace_confirmed, recordprobe: skipped (static-only by registry), and move on. The registry marks a typestatic-onlyprecisely because a runtime proof would BE the attack:redos's "probe" is driving a catastrophic-backtracking regex against a live target — i.e. deliberately causing the denial of service Gate 4 (rate cap) and Gate 6 (read-only, reversible payloads) exist to prevent. There is no### static-onlytemplate and there must never be one; "craft a probe from the mapped template" does NOT apply to this branch. - Reuse
suggested_probeif present; otherwise craft a minimal read-only probe from the mapped template. - Prefer the matching verify-live template from
pentest-finding-registry.md, then refine it with overlay-specific ideas frompentest-stack-profiles.md. - Execute via
curlor Playwright. - Capture evidence.
Update verification_status:
runtime_verifiedblocked_by_defenseneeds_runtime_check
Update confidence:
runtime_verified-> 0.95blocked_by_defense-> 0.6 and move toexcluded- failed reproduction without proof of defense -> remain
needs_runtime_check
If safe_to_probe=false, do not improvise around the constraint. Record the unmet runtime_preconditions and keep the finding in needs_runtime_check.
Phase 4: Runtime Checks (PT8)
Skip if: zuvo:security-audit --live-url already ran and its live results still match the current code / environment.
Dispatch a lightweight agent to check:
- security headers
- CORS
- TLS version
- error disclosure
- cookie flags
Rate limit per --rate.
PT8 contributes narrative runtime coverage, not a new scoring dimension unless explicitly requested by the user.
Phase 5: CMS Overlay (CONDITIONAL)
Skip if: No CMS detected in Phase 0.4 and no --cms flag.
Apply the CMS overlay from pentest-stack-profiles.md:
- Version -> CVE lookup
- Extension / plugin inventory + known vulnerability check
- Admin panel exposure
- Config review (debug mode, error reporting, DB credentials)
- File permissions
- Custom extension code review (trace input to sink)
Phase 6: Confidence Filter and MUST-GATEs
Apply three filter layers to the merged queue:
Layer 1 -- Confidence Threshold
status=confirmed with confidence < 0.7 is demoted to needs_review.
Layer 2 -- Status Correction
Apply pentest-output-contract.md hard rules centrally:
defense_sufficient=true->excludedscope_ok=false->excluded- hedged language without proof ->
needs_review - preserve
reason_codeon every disposition change
Layer 3 -- MUST-GATEs
- ASSUME-EXPLOIT: Treat a trace as exploitable unless defense is proven sufficient.
- NO-HEDGING: "Might be vulnerable" -> prove it or move it out of final findings.
- PROOF: Every final finding needs a source trace or runtime evidence.
- FULL-COVERAGE: Agent must report
files_checkedagainst the full scope inventory. - STRICT-SEQUENCE: Agent must follow
sink -> trace -> defense -> verdict.
Print filter summary:
CONFIDENCE FILTER
Confirmed input: 13
Demoted to Needs Review: 2
Excluded by rules: 4
Final findings: 9
Phase 7: Report Generation
7.1 Scoring
Follow pentest-dedup-scoring.md.
Requirements:
- compute
raw_scorefirst - compute
final_scoreafter score caps - emit
cap_reason - emit
grade - emit
verdict
Per-dimension score remains 0-10:
- 0 = critical gate fail (undefended sink confirmed)
- 5 = defenses present but gaps found
- 8 = minor issues only
- 10 = all checks pass
Critical gates: PT1=0 OR PT3<3 OR PT4<3 -> grade FAIL.
Coverage gate (from 2.4): coverage_gate = FAIL -> VERDICT = INCOMPLETE, grade capped at FAIL. A pentest that did not enumerate its declared attack surface cannot earn a passing or partial grade, regardless of per-dimension scores. The grade reflects what was actually checked, not the subset that happened to be high-signal.
7.2 Output Directory
Resolve the canonical zuvo output root (see ../../shared/includes/report-output-location.md), then create the pentest bundle under audits/. Never write to a bare pentest-results/ at the scope path — that scatters output outside the visible project-root zuvo/ folder.
ZUVO_DIR="${ZUVO_OUTPUT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/zuvo}"
PENTEST_DIR="$ZUVO_DIR/audits/pentest"
mkdir -p "$PENTEST_DIR/evidence"
All report paths in 7.3–7.4 are written under $PENTEST_DIR/ — i.e. <project-root>/zuvo/audits/pentest/. The --from-audit <dir> flag consumes this same directory.
7.3 Write Reports
$PENTEST_DIR/pentest-report.md (zuvo/audits/pentest/pentest-report.md) -- Full report:
- Metadata (target, date, mode, scope, stack profile, tools used, gates passed)
- Executive summary (grade, verdict, raw score, final score, cap reason, finding counts)
- Critical findings
- All final findings grouped by dimension
- Needs Review
- Exclusions applied
- Audit trail summary
- Coverage sanity results
- Variant analysis results
- Remediation roadmap
- Manual checklist
$PENTEST_DIR/findings.json (zuvo/audits/pentest/findings.json) -- Machine-readable:
{
"schema_version": "4.0",
"metadata": {
"target": "/repo",
"scope": "src/app/",
"stack_profile": "python-django",
"files_in_scope": 92
},
"summary": {
"total_findings": 9,
"by_severity": { "critical": 0, "high": 4, "medium": 5 },
"by_dimension": { "PT3": 3, "PT4": 2, "PT7": 4 },
"raw_score": 73,
"final_score": 60,
"cap_reason": "HIGH findings present",
"grade": "C",
"verdict": "WARN",
"critical_gates": "PASS",
"coverage_pct": 0.97,
"files_checked_union": 89,
"uncovered_files": [{ "path": "public/vendor.js", "reason_code": "static_asset" }],
"coverage_gate": "PASS"
},
"findings": [{ "id": "PT3-001", "reason_code": "proof_complete", "probe_template_id": "probe-oauth-state", "merged_from": ["PT3-auth-session"] }],
"needs_review": [{ "dimension": "PT6", "reason_code": "runtime_required" }],
"exclusions": [{ "reason_code": "out_of_scope" }],
"audit_trail": [{ "canonical_key": "PT3|...", "source_agent": "PT3-auth-session" }]
}
summary.* must be generated from the final findings[] collection only. If the counts do not match, the run is invalid and must be corrected before output.
7.4 Self-Check
Validate the machine-readable output before printing the completion block:
./scripts/validate-pentest-output.sh "$PENTEST_DIR/findings.json"
Required checks:
summary.total_findings == len(findings[])summary.by_severitymatches finalfindings[]summary.by_dimensionmatches finalfindings[]- every record in
findings[],needs_review[], andexclusions[]hasreason_code
If validation fails, the run is invalid. Correct the output before printing PENTEST COMPLETE.
$PENTEST_DIR/executive-summary.md (zuvo/audits/pentest/executive-summary.md) -- 1-page stakeholder overview.
$PENTEST_DIR/evidence/PT3-001/ (zuvo/audits/pentest/evidence/…) -- Per-finding evidence:
trace.mdpoc.sh(if--verify-live)screenshot.png(if Playwright)
Finding Format
### PT{D}-{NNN}: {Title}
Dimension: PT{D} -- {name}
Severity: CRITICAL / HIGH / MEDIUM
Confidence: {0.0-1.0}
Verification Status: trace_confirmed / runtime_verified / needs_runtime_check
Probe Template ID: {probe_template_id}
CWE: CWE-{NNN}
Reason Code: {reason_code}
Source: {file}:{line}
Sink: {file}:{line}
Trace: source -> [intermediate steps] -> sink
Defense Present: {yes/no}
Defense Sufficient: {yes/no}
Canonical Key: {canonical_key}
Merged From: {source_agent list}
Evidence: {code trace or runtime capture, scrubbed, max 20 lines}
Impact: {what an attacker achieves}
Fix: {remediation with code}
7.5 Backlog Integration
For each CRITICAL / HIGH final finding, suggest a backlog entry:
zuvo:backlog add "PT3-001: OAuth implicit flow missing state" --severity HIGH --source pentest --dimension PT3
7.6 Next-Action Routing
| Condition | Suggested Action |
|---|---|
| CRITICAL/HIGH findings | zuvo:refactor to fix vulnerable code paths |
| Auth gaps found | zuvo:security-audit full for broader posture check |
| Out-of-scope issues surfaced | Suggest a focused follow-up audit on the owning module |
| Runtime preconditions not met | Emit a follow-up --verify-live probe plan instead of closing the item |
| CMS issues | Update CMS + extensions, re-run zuvo:pentest --cms |
| All clear | Auto-chain to zuvo:review (see below) |
PENTEST COMPLETE
Grade: [A/B/C/D/FAIL] | Verdict: [PASS/WARN/FAIL] Raw Score: [N] / 100 | Final Score: [N] / 100 | Cap: [reason/none] Mode: [white-box / hybrid / black-box / verification] Dimensions: [N scored] | Critical gates: [PASS/FAIL] Findings: [N critical] / [N high] / [N medium] / [N total]
Validity Gate (REQUIRED — print BEFORE Run line, AFTER retro append + append-runlog)
VALIDITY GATE
triggers_held: language=<X> framework=<X> backend=<yes|no> django=<yes|no>
required_tool_calls:
scan_secrets: [<N> hits | NOT_CALLED — VIOLATES_TRIGGER]
search_patterns(sinks): [<N> hits | NOT_CALLED — VIOLATES_TRIGGER]
trace_call_chain: [<N> sink chains traced | NOT_CALLED — VIOLATES_TRIGGER]
find_references: [<N> ref-checks | NOT_CALLED — VIOLATES_TRIGGER]
trace_route: [<N> entry points | not_required (no backend) | NOT_CALLED — VIOLATES_TRIGGER]
audit_scan: [<N> compound findings | NOT_CALLED — VIOLATES_TRIGGER]
taint_trace: [<N> taints | not_required (no Django) | NOT_CALLED — VIOLATES_TRIGGER]
stack_specific: [<result> | not_required | NOT_CALLED — VIOLATES_TRIGGER]
coverage:
files_in_scope: <N>
files_checked_union: <M>
coverage_pct: <X%>
uncovered_undocumented: [<count> — list files | 0]
coverage_gate: [PASS | FAIL — <pct below 0.90 / N files uncovered with no reason code>]
postamble:
retros_log_appended: [yes(bytes_added=N) | NOT_APPENDED — VIOLATES_REQUIRED_POSTAMBLE]
retros_md_appended: [yes(entry_count=N) | NOT_APPENDED — VIOLATES_REQUIRED_POSTAMBLE]
verify_audit_pass: [yes(<verified>/<total>) | NOT_RUN | REJECTED]
gate_status: [PASS | FAIL — <which gates missing>]
If gate_status = FAIL OR coverage_gate = FAIL → VERDICT = INCOMPLETE, append [VALIDITY GATE FAIL] / [COVERAGE GATE FAIL] to NOTES.
Append the Run line via the retro-gated wrapper (NOT direct >> runs.log):
printf '%b\n' "$RUN_LINE" | ~/.zuvo/append-runlog
Run: pentest - -dimensions
Retrospective (REQUIRED)
Follow the retrospective protocol from retrospective.md.
Gate check -> structured questions -> TSV emit -> markdown append.
If gate check skips: print RETRO: skipped (trivial session) and proceed.
After printing this block, append the Run: line value (without the Run: prefix) to the log file path resolved per run-logger.md.
Verdict rules come from pentest-dedup-scoring.md.
Post-Pentest: Automatic Review Chain (MANDATORY)
After logging the pentest run, automatically invoke zuvo:review on changed files. Do not ask the user.
Scope selection:
- If there are uncommitted / staged changes:
Skill(skill="zuvo:review") - If pentest applied and committed fixes:
Skill(skill="zuvo:review", args="HEAD~N") - If pentest was audit-only (no code changes made): skip the chain, print
No code changes -- review chain skipped.
# Check for uncommitted changes first
git diff --stat HEAD
# If output is empty, check for recent commits from this session
git log --oneline -5
# Then invoke with appropriate scope
Skill(skill="zuvo:review")
This ensures adversarial code review runs before push on all supported platforms.
Execution Notes
- Use stack-aware recipes from
pentest-stack-profiles.md, not hard-coded TS-only heuristics. - Use framework overlays from
pentest-stack-detection.mdandpentest-stack-profiles.mdwhen the repo matches them. - Use Sonnet-class models for source analyzers and scanners.
- Use a stronger model for exploit verification on CRITICAL findings when available.
- Max 3 parallel agents globally.
- White-box agents are read-only.
- Black-box agents require Gate 1 authorization.
- Estimated durations:
--quick~5 min, white-box ~15 min, hybrid ~25 min, full +--verify-live~40 min.