| name | pentest |
| description | Hybrid white-box + black-box penetration testing across 7 dimensions (PT1-PT7). Stack-aware source-to-sink tracing, exploit verification, CMS overlay, and deterministic finding aggregation. Uses explicit candidate schemas, canonical-key deduplication, score caps, and MUST-GATE enforcement. Flags: zuvo:pentest [path] | --url <url> | --from-audit <dir> | --cms <type> | --quick | --scope <path> | --dimensions PT1,PT2 | --verify-live | --rate <N> | --source | --runtime |
| codesift_tools | {"always":["analyze_project","index_status","index_folder","index_file","plan_turn","scan_secrets","search_patterns","trace_call_chain","find_references","audit_scan","search_text","search_symbols","get_symbol","get_symbols","get_file_tree","get_file_outline","trace_route"],"by_stack":{"typescript":["get_type_info","resolve_constant_value"],"javascript":[],"python":["python_audit","analyze_async_correctness","resolve_constant_value"],"php":["php_project_audit","php_security_scan","resolve_php_namespace"],"kotlin":["analyze_sealed_hierarchy","find_extension_functions","trace_flow_chain","trace_suspend_chain","trace_compose_tree","analyze_compose_recomposition","trace_hilt_graph","trace_room_schema","analyze_kmp_declarations","extract_kotlin_serialization_contract"],"nestjs":["nest_audit"],"nextjs":["framework_audit","nextjs_route_map"],"astro":["astro_audit","astro_actions_audit","astro_hydration_audit","astro_middleware","astro_sessions"],"hono":["analyze_hono_app","audit_hono_security"],"express":[],"fastify":[],"react":["react_quickstart","analyze_hooks","analyze_renders"],"django":["analyze_django_settings","effective_django_view_security","taint_trace"],"fastapi":["trace_fastapi_depends","get_pydantic_models"],"flask":["find_framework_wiring"],"jest":[],"yii":["resolve_php_service","trace_php_event","find_php_views"],"prisma":["analyze_prisma_schema"],"drizzle":[],"sql":["sql_audit"],"postgres":["migration_lint"]}} |
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 11 new v2 vuln classes toward score caps / critical-gate totals. Default: warning-only grace — identical spec to security-audit's "v2 class grace" section (the 11 classes incl. both GraphQL classes; warning-only through the 1.4.x window, full weight in 1.5.0; v2 findings 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):
11. ../../shared/includes/backlog-protocol.md -- READ/MISSING
12. ../../shared/includes/run-logger.md -- READ/MISSING
13. ../../shared/includes/retrospective.md -- READ/MISSING
14. ../../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_scope count.
- 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
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:
run_sca() { OUT="$("$@" 2>/dev/null)"; case "$OUT" in ''|null) return 1 ;; *) printf '%s\n' "$OUT"; return 0 ;; esac; }
audit_lock() {
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-context
phase1-findings
phase1-exclusions
probe-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_checked reaches every file in files_in_scope across 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.md before using broader semantic search.
- Match framework defenses by canonical
pattern_id from pentest-safe-pattern-registry.md.
- Resolve
cwe and probe_template_id from pentest-finding-registry.md when the type exists there.
- Use the shared
files_in_scope inventory from Phase 0.3.
- Emit candidate records using
pentest-output-contract.md.
- Classify each record as
confirmed, excluded, or needs_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 (an auth()/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 by file:line as evidence; "looks internal" is not a guard.
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
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_ok on every candidate.
- If
source_file or sink_file is 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-findings
phase1-exclusions
probe-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 = PASS only when coverage_pct ≥ 0.90 AND every remaining uncovered file has a documented reason code.
- Otherwise
coverage_gate = FAIL. A FAIL means the run did not cover its declared scope: set VERDICT = INCOMPLETE, cap grade at FAIL, 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_id from pentest-finding-registry.md.
- Reuse
suggested_probe if 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 from pentest-stack-profiles.md.
- Execute via
curl or Playwright.
- Capture evidence.
Update verification_status:
runtime_verified
blocked_by_defense
needs_runtime_check
Update confidence:
runtime_verified -> 0.95
blocked_by_defense -> 0.6 and move to excluded
- 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 -> excluded
scope_ok=false -> excluded
- hedged language without proof ->
needs_review
- preserve
reason_code on 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_checked against 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_score first
- compute
final_score after 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_severity matches final findings[]
summary.by_dimension matches final findings[]
- every record in
findings[], needs_review[], and exclusions[] has reason_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.md
poc.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.4 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.5 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.md and pentest-stack-profiles.md when 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.