# Pentest

> 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

- Skill: `greglas75/pentest` (Agent Skill)
- Install (CLI): `npx skillmds@latest add greglas75/pentest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/greglas75/pentest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: greglas75 (https://skillmd.com/u/greglas75)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/greglas75/pentest

---


# 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):**

```text
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):**

```text
  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".

```text
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

```text
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:

1. Detect the primary backend stack.
2. Detect frontend and site-framework overlays, including Next App Router, Next Pages Router, and React SPA where applicable.
3. Detect headless CMS overlays.
4. Detect CMS overlays, including WordPress / Joomla / Drupal.
5. Detect framework overlays from `pentest-stack-detection.md`.
6. Load the matching files from `../../rules/`.
7. Load safe-pattern IDs from `pentest-safe-pattern-registry.md`.
8. Load finding-type to CWE / probe mappings from `pentest-finding-registry.md`.
9. Load source/sink discovery regexes from `pentest-source-sink-registry.md`.
10. 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:

1. Resolve the target scope from `[path]` or `--scope`.
2. Build the complete file inventory for that scope.
3. Record one authoritative `files_in_scope` count.
4. Pass that exact count to every agent.

Print:

```text
SCOPE INVENTORY
  Scope: src/example/
  Files in scope: 92
  Stack profile: python-django
```

### 0.4 CMS Detection

```bash
[ -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

```bash
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:

```bash
# 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:

```text
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:

1. **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.
2. **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:

1. Use the active stack profile from `pentest-stack-profiles.md`.
2. Seed discovery from `pentest-source-sink-registry.md` before using broader semantic search.
3. Match framework defenses by canonical `pattern_id` from `pentest-safe-pattern-registry.md`.
4. Resolve `cwe` and `probe_template_id` from `pentest-finding-registry.md` when the `type` exists there.
5. Use the shared `files_in_scope` inventory from Phase 0.3.
6. Emit **candidate records** using `pentest-output-contract.md`.
7. Classify each record as `confirmed`, `excluded`, or `needs_review`.
8. 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:

```json
{
  "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:

1. Recon
2. Experiment
3. Test
4. 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:

1. Check `scope_ok` on every candidate.
2. If `source_file` or `sink_file` is outside the requested scope, force:
   - `status="excluded"`
   - exclusion reason `out_of_scope`
3. 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:

```text
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:

1. Feasibility check: endpoint reachable? auth required? environment safe? `safe_to_probe=true`?
2. Resolve `probe_template_id` from `pentest-finding-registry.md`.
2b. **`probe_template_id == "static-only"` → DO NOT PROBE. STOP HERE for this finding.** Leave
   `verification_status: trace_confirmed`, record `probe: skipped (static-only by registry)`, and
   move on. The registry marks a type `static-only` precisely 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-only` template and there must
   never be one; "craft a probe from the mapped template" does NOT apply to this branch.
3. Reuse `suggested_probe` if present; otherwise craft a minimal read-only probe from the mapped template.
4. Prefer the matching verify-live template from `pentest-finding-registry.md`, then refine it with overlay-specific ideas from `pentest-stack-profiles.md`.
5. Execute via `curl` or Playwright.
6. 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`:

1. Version -> CVE lookup
2. Extension / plugin inventory + known vulnerability check
3. Admin panel exposure
4. Config review (debug mode, error reporting, DB credentials)
5. File permissions
6. 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:

```text
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.

```bash
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:

1. Metadata (target, date, mode, scope, stack profile, tools used, gates passed)
2. Executive summary (grade, verdict, raw score, final score, cap reason, finding counts)
3. Critical findings
4. All final findings grouped by dimension
5. Needs Review
6. Exclusions applied
7. Audit trail summary
8. Coverage sanity results
9. Variant analysis results
10. Remediation roadmap
11. Manual checklist

**`$PENTEST_DIR/findings.json`** (`zuvo/audits/pentest/findings.json`) -- Machine-readable:

```json
{
  "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:

```bash
./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

```text
### 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:

```text
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`):

```bash
printf '%b\n' "$RUN_LINE" | ~/.zuvo/append-runlog
```

Run: <ISO-8601-Z>	pentest	<project>	<N-critical>	<N-total>	<VERDICT>	-	<N>-dimensions	<NOTES>	<BRANCH>	<SHA7>	<INCLUDES>	<TIER>

### 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.`

```text
# 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.

