pentest

Orchestrate a penetration test against an approved companion-app engagement — reads scope post-Gate-1, dispatches execution workers, uploads logs immediately per worker, structures findings, then driv

SKILL.md

GitHub

pentest — Penetration Test Orchestrator

You orchestrate penetration tests end-to-end. The companion app manages scope and human gate approvals. Human testers may contribute manual findings during Gate 3 — the report is generated after Gate 3 to include everything they add.

Execution flow:

  1. Workers run → logs uploaded to app immediately per worker as each completes
  2. Gate 2: human reviews all execution logs in app → approves
  3. Findings analyzed and uploaded to app
  4. Gate 3: human reviews, edits, and adds manual findings in app → approves
  5. Report generated from current app findings (includes human additions) → uploaded
  6. Gate 4: human approves report

Gate approvals happen only in the companion app. Never call gate2/complete, gate3/complete, or gate4/complete — those are triggered by the human clicking in the app. Upload data, notify the human, then poll.

Never execute test commands yourself. All Bash execution is delegated to /pentest-execute worker agents.


Step 0 — Resolve Engagement

Read the engagement ID from args. If not provided, ask:

"Paste the engagement ID from the companion app (visible in the URL on the engagement detail page)."

Store as ENGAGEMENT_ID.

Resolve companion app location:

if [ -n "$PENTEST_COMPANION_PATH" ]; then
  COMPANION_BASE="$PENTEST_COMPANION_PATH"
elif [ -f /.dockerenv ]; then
  COMPANION_BASE="/app"
else
  COMPANION_BASE=""
fi

If COMPANION_BASE is still empty, ask:

"What is the local path to your pentest-platform-companion clone? (Set PENTEST_COMPANION_PATH in your environment to skip this question on future runs.)"

Store the answer as COMPANION_BASE.

ENGAGEMENT_DIR="$COMPANION_BASE/engagements/$ENGAGEMENT_ID"

Store COMPANION_BASE and ENGAGEMENT_DIR.

Auth:

  • APP_URL: http://127.0.0.1:8001 if running inside Docker (/.dockerenv exists), otherwise http://localhost:8001
  • APP_USER / APP_PASS: Lead user credentials (from env or ask)
curl -s -X POST {APP_URL}/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"{APP_USER}","password":"{APP_PASS}"}'

Store token as APP_TOKEN. Stop if login fails.

Checkpoint detection:

test -f "{ENGAGEMENT_DIR}/pentest_checkpoint.json" && echo "exists" || echo "not found"

If found, read it and display:

══════════════════════════════════════════════════
  CHECKPOINT FOUND — ENGAGEMENT {ENGAGEMENT_ID}
══════════════════════════════════════════════════
Client    : {checkpoint.scope.client_name}
Last step : {checkpoint.last_step_completed}
Saved at  : {checkpoint.checkpoint_at}
══════════════════════════════════════════════════
Resume from checkpoint? (yes / no — no starts a fresh run)

If yes → jump to Resume Path. If no → continue to Step 1.


Resume Path

Load from checkpoint:

  • CLIENT_NAME, IP_RANGES, TARGET_URLS, EXCLUSIONS, SOW_TEXT, APP_ASSESSMENT_TYPE, CLOUD_CONSTRAINTS, CLOUD_PROVIDERS_DETECTED ← from checkpoint.scope
  • WORKER_RESULTS ← checkpoint.worker_results
  • LOGS_UPLOADED ← checkpoint.logs_uploaded (list of phases already POSTed to app)

Re-load credentials from {ENGAGEMENT_DIR}/credentials.txt. If absent and WHITE_BOX/GREY_BOX, ask the user. If BLACK_BOX, set CREDENTIALS = [].

Tell user: "Resuming — last completed step: {checkpoint.last_step_completed}."

Jump to first incomplete step:

  • RECON_DISPATCHED → Step 7 (scan workers)
  • SCAN_DISPATCHED → Step 8 (exploit auth)
  • EXPLOIT_DISPATCHED → Step 9 (analyze findings)
  • FINDINGS_UPLOADED → Step 10 (Gate 2 poll)
  • GATE_2_APPROVED → Step 11 (Gate 3 poll)
  • GATE_3_APPROVED → Step 12 (generate report)
  • REPORT_UPLOADED → Step 13 (Gate 4 poll)

Step 1 — Read and Verify Approved Scope

curl -s {APP_URL}/api/engagements/{ENGAGEMENT_ID} \
  -H "Authorization: Bearer {APP_TOKEN}"

Stop immediately if status is not GATE_1_APPROVED. Report the current status and tell the user to complete Gate 1 in the companion app first.

Fetch scope:

curl -s {APP_URL}/api/engagements/{ENGAGEMENT_ID}/scope \
  -H "Authorization: Bearer {APP_TOKEN}"

Extract and store:

  • SOW_TEXT: sow
  • IP_RANGES: ipRanges (split on commas/whitespace into array)
  • TARGET_URLS: [] — derived from PORT_SCAN open ports in Step 6
  • EXCLUSIONS: exclusions
  • APP_ASSESSMENT_TYPE: assessmentType — BLACK_BOX, GREY_BOX, or WHITE_BOX
  • APP_CREDENTIALS_HINT: credentials field (audit reference only)
  • CLIENT_NAME: from engagement name

Step 2 — Enrich: Credentials

Check for credentials file:

{ENGAGEMENT_DIR}/credentials.txt

Format — one per line: username:password, api_key:value, service_account:token

If absent:

  • APP_CREDENTIALS_HINT is n/a or empty → CREDENTIALS = [], black-box mode
  • Otherwise → show hint, ask user to provide path or paste credentials

Store as CREDENTIALS. Mask real values as [MASKED] everywhere. Never write real values to checkpoints, reports, logs, or findings.


Step 3 — Enrich: Cloud Provider Detection

Rule 1 — Loopback: 127.0.0.0/8, ::1, localhost → On-Premises Rule 2 — RFC-1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 → On-Premises Rule 3 — Public IPs: check offline CIDR databases.

Update cloud ranges before any cloud engagement:

cd "$COMPANION_BASE"
.venv/bin/python data/cloud-ranges/update_cloud_ranges.py
# Windows: .\.venv\Scripts\python.exe data\cloud-ranges\update_cloud_ranges.py

Run detection script if files exist:

cd "$COMPANION_BASE/backend"
.venv/bin/python3 - << 'EOF'
import json, ipaddress

ip_ranges_input = {IP_RANGES_AS_PYTHON_LIST}

def load_aws(path):
    with open(path) as f: d = json.load(f)
    return [r['ip_prefix'] for r in d.get('prefixes', [])]

def load_gcp(path):
    with open(path) as f: d = json.load(f)
    return [r['ipv4Prefix'] for r in d.get('prefixes', []) if 'ipv4Prefix' in r]

def load_azure(path):
    with open(path) as f: d = json.load(f)
    cidrs = []
    for v in d.get('values', []):
        cidrs.extend(v.get('properties', {}).get('addressPrefixes', []))
    return cidrs

def ip_in_cidrs(net_str, cidrs):
    try:
        net = ipaddress.ip_network(net_str, strict=False)
        for c in cidrs:
            try:
                if net.overlaps(ipaddress.ip_network(c, strict=False)): return True
            except: pass
    except: pass
    return False

results = {}
aws_cidrs   = load_aws('data/cloud-ranges/aws-ip-ranges.json')
gcp_cidrs   = load_gcp('data/cloud-ranges/gcp-ip-ranges.json')
azure_cidrs = load_azure('data/cloud-ranges/azure-ip-ranges.json')

for r in ip_ranges_input:
    net = r.strip()
    if not net: continue
    base = net.split('/')[0]
    addr = ipaddress.ip_address(base) if '/' not in net else ipaddress.ip_network(net, strict=False).network_address
    if addr.is_loopback or addr.is_private:
        results.setdefault('On-Premises', []).append(net)
    elif ip_in_cidrs(net, aws_cidrs):
        results.setdefault('AWS', []).append(net)
    elif ip_in_cidrs(net, gcp_cidrs):
        results.setdefault('GCP', []).append(net)
    elif ip_in_cidrs(net, azure_cidrs):
        results.setdefault('Azure', []).append(net)
    else:
        results.setdefault('On-Premises', []).append(net)

print(json.dumps(results))
EOF

If CIDR files absent → default all to On-Premises and warn the user.

Build CLOUD_PROVIDERS_DETECTED and CLOUD_CONSTRAINTS:

Provider Restricted Subnets Banned Categories max_ports/s max_http_req/s
AWS 169.254.169.254 DenialOfService, DNS_Flooding, ControlPlaneBruteforce 100 10
Azure 168.63.129.16, 169.254.169.254 DenialOfService, HypervisorExploitation, SharedComputeScans 150 15
GCP 169.254.169.254, metadata.google.internal DenialOfService, SpamCampaigns, GoogleInfrastructureBruteforce 100 10
On-Premises (none) (none — SOW exclusions only) unlimited unlimited

Step 4 — Pre-Engagement Confirmation

Display and wait for explicit approval:

══════════════════════════════════════════════════
  PENTEST PLATFORM — PRE-ENGAGEMENT REVIEW
══════════════════════════════════════════════════
Engagement   : {CLIENT_NAME} ({ENGAGEMENT_ID})
Assessment   : {APP_ASSESSMENT_TYPE}

TARGETS
{list each IP range / URL with resolved provider}

CLOUD CONSTRAINTS
{provider}: restricted: {subnets}, banned: {categories}
Rate limits: {max_ports/s} ports/s · {max_http_req/s} http req/s

CREDENTIALS
{count} entries loaded — values masked
Black-box: {yes/no}

EXCLUSIONS
{EXCLUSIONS from SOW}

SELF-PENTEST NOTE
{if target includes localhost/127.0.0.1: warn that all localhost services are in scope
including the companion app (port 8001) and any MCP servers (port 8000). Recommend
adding port 8000 to EXCLUSIONS before proceeding.}

RULES OF ENGAGEMENT
{SOW_TEXT summary}
══════════════════════════════════════════════════
Confirm to begin execution? (yes / no)

If no → stop. Ask what needs correcting.


Step 5 — Start Execution

Advance engagement status (internal — do not surface this API call or status change to the user):

curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/start \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json"

409 (already in progress) → continue. Any other non-2xx → report and stop.

Display to user:

Execution started — beginning reconnaissance phase.

Create directories:

mkdir -p "{ENGAGEMENT_DIR}/raw"

Initialize WORKER_RESULTS and LOGS_UPLOADED (tracks which phases are already POSTed to app):

{ "WORKER_RESULTS": {}, "LOGS_UPLOADED": [] }

Step 6 — Dispatch Recon Workers (parallel)

Inform user: "Dispatching PASSIVE_RECON and PORT_SCAN workers in parallel against {IP_RANGES}."

Dispatch both simultaneously:

Agent({
  description: "PASSIVE_RECON against {IP_RANGES}",
  subagent_type: "pentest-execute",
  prompt: "Execute a PASSIVE_RECON task.\nEngagement ID: {ENGAGEMENT_ID}\nTargets: {IP_RANGES}\nConstraints: {CLOUD_CONSTRAINTS as JSON}\nAssessment type: {APP_ASSESSMENT_TYPE}\nCredentials: {count} entries — read from {ENGAGEMENT_DIR}/credentials.txt\nApp URL: {APP_URL}\n\nCollect: DNS records (A/MX/TXT/NS/CNAME), certificate data, HTTP response headers on all services, banner grabs, subdomain enumeration, technology fingerprinting.\n\nFor every check performed, add to tests_performed with result PASS (no issue), FAIL (issue found), or INFO (informational). Include checks that pass cleanly.\n\nReturn JSON matching the worker output schema."
})

Agent({
  description: "PORT_SCAN against {IP_RANGES}",
  subagent_type: "pentest-execute",
  prompt: "Execute a PORT_SCAN task.\nEngagement ID: {ENGAGEMENT_ID}\nTargets: {IP_RANGES}\nConstraints: {CLOUD_CONSTRAINTS as JSON}\nAssessment type: {APP_ASSESSMENT_TYPE}\nCredentials: {count} entries — read from {ENGAGEMENT_DIR}/credentials.txt\nApp URL: {APP_URL}\n\nFull port scan with service and version detection. For every port checked, add to tests_performed: port, state (open/closed/filtered), service/version if detected.\n\nReturn JSON matching the worker output schema."
})

When each worker result arrives, immediately perform these actions in order before anything else:

For PORT_SCAN:

  1. Write raw output to disk: {ENGAGEMENT_DIR}/raw/PORT_SCAN_raw.txt
  2. POST log to companion app:
curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/logs \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"logs":[{"phase":"PORT_SCAN","rawOutput":"{worker.raw_output — credentials masked}","isOutOfScope":false,"hasElevatedAccess":{worker.elevated_access_confirmed}}]}'
  1. On POST success: add "PORT_SCAN" to LOGS_UPLOADED. On failure: note error, disk file is fallback.
  2. Store WORKER_RESULTS["PORT_SCAN"] ← structured fields only (findings_preview, tests_performed, executed_commands, tool_availability).

For PASSIVE_RECON:

  1. Write raw output to disk: {ENGAGEMENT_DIR}/raw/PASSIVE_RECON_raw.txt
  2. POST log to companion app:
curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/logs \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"logs":[{"phase":"PASSIVE_RECON","rawOutput":"{worker.raw_output — credentials masked}","isOutOfScope":false,"hasElevatedAccess":{worker.elevated_access_confirmed}}]}'
  1. On POST success: add "PASSIVE_RECON" to LOGS_UPLOADED. On failure: note error.
  2. Store WORKER_RESULTS["PASSIVE_RECON"] ← structured fields only.

After both workers complete and logs are posted:

Derive TARGET_URLS from PORT_SCAN: port 80/8080 open → http://{host}:{port}, port 443/8443 → https://{host}:{port}.

Checkpoint:

{
  "last_step_completed": "RECON_DISPATCHED",
  "checkpoint_at": "{UTC ISO timestamp}",
  "scope": { "client_name": "...", "ip_ranges": [...], "target_urls": [...], "exclusions": "...", "sow_text": "...", "assessment_type": "...", "cloud_constraints": [...], "cloud_providers_detected": [...] },
  "worker_results": { "PORT_SCAN": { "findings_preview": [...], "tests_performed": [...], "executed_commands": [...] }, "PASSIVE_RECON": { ... } },
  "logs_uploaded": ["PORT_SCAN", "PASSIVE_RECON"]
}

Step 7 — Dispatch Scan Workers (parallel)

Inform user: "Dispatching VULN_SCAN and LOGIC_PROBE workers against {TARGET_URLS}."

Dispatch simultaneously:

Agent({
  description: "VULN_SCAN against {TARGET_URLS}",
  subagent_type: "pentest-execute",
  prompt: "Execute a VULN_SCAN task.\nEngagement ID: {ENGAGEMENT_ID}\nTargets: {TARGET_URLS}\nConstraints: {CLOUD_CONSTRAINTS as JSON}\nAssessment type: {APP_ASSESSMENT_TYPE}\nContext from PORT_SCAN: {PORT_SCAN findings_preview — open ports, service versions, technologies}\nCredentials: {count} entries — read from {ENGAGEMENT_DIR}/credentials.txt\nApp URL: {APP_URL}\n\nRun all phases:\n1. WAF detection (wafw00f)\n2. Technology fingerprinting (whatweb)\n3. Security header audit (curl/nikto) — check every standard header; record PASS for headers that ARE present\n4. TLS/SSL assessment (testssl.sh)\n5. Nuclei CVE/misconfiguration template scan\n6. Nikto web server audit\n7. Directory/endpoint enumeration (gobuster)\n8. Exposure probing (sensitive paths, debug endpoints, admin panels)\n\nFor EVERY check — including headers present and controls working — add to tests_performed. Do not only record failures.\n\nReturn JSON matching the worker output schema."
})

Agent({
  description: "LOGIC_PROBE against {TARGET_URLS}",
  subagent_type: "pentest-execute",
  prompt: "Execute a LOGIC_PROBE task.\nEngagement ID: {ENGAGEMENT_ID}\nTargets: {TARGET_URLS}\nConstraints: {CLOUD_CONSTRAINTS as JSON}\nAssessment type: {APP_ASSESSMENT_TYPE}\nContext: {PORT_SCAN and VULN_SCAN findings_preview — endpoints, tech stack, auth mechanism, service versions}\nCredentials: {count} entries — read from {ENGAGEMENT_DIR}/credentials.txt\nApp URL: {APP_URL}\n\nTest:\n1. Authentication enforcement on every discovered endpoint\n2. Role-based access control — test each endpoint with each credential role\n3. Business logic flaws (state machine manipulation, parameter tampering, mass assignment)\n4. Input validation (injection, XSS, type coercion, boundary values)\n5. API security (broken object-level auth, rate limiting, error disclosure)\n6. Session management\n\nFor EVERY check — including endpoints correctly returning 401/403 — add to tests_performed with PASS, FAIL, or INFO.\n\nReturn JSON matching the worker output schema."
})

When each worker result arrives, immediately:

For VULN_SCAN:

  1. Write {ENGAGEMENT_DIR}/raw/VULN_SCAN_raw.txt
  2. POST log to companion app:
curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/logs \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"logs":[{"phase":"VULN_SCAN","rawOutput":"{worker.raw_output — credentials masked}","isOutOfScope":false,"hasElevatedAccess":{worker.elevated_access_confirmed}}]}'
  1. Add "VULN_SCAN" to LOGS_UPLOADED on success.
  2. Store WORKER_RESULTS["VULN_SCAN"] ← structured fields only.

For LOGIC_PROBE:

  1. Write {ENGAGEMENT_DIR}/raw/LOGIC_PROBE_raw.txt
  2. POST log to companion app:
curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/logs \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"logs":[{"phase":"LOGIC_PROBE","rawOutput":"{worker.raw_output — credentials masked}","isOutOfScope":false,"hasElevatedAccess":{worker.elevated_access_confirmed}}]}'
  1. Add "LOGIC_PROBE" to LOGS_UPLOADED on success.
  2. Store WORKER_RESULTS["LOGIC_PROBE"] ← structured fields only.

Checkpoint: Update with last_step_completed: "SCAN_DISPATCHED", updated worker_results and logs_uploaded.


Step 8 — Exploit Authorization (CLI gate)

This is the only gate where the skill asks the human for authorization in the CLI. Display confirmed vulnerabilities from WORKER_RESULTS:

══════════════════════════════════════════════════
  EXPLOIT PHASE — OPERATOR AUTHORISATION REQUIRED
══════════════════════════════════════════════════
Confirmed vulnerabilities:
{list findings_preview from VULN_SCAN + LOGIC_PROBE — severity and title only}

Cloud restrictions: {banned categories or "none (On-Premises)"}

Authorise exploitation of confirmed vulnerabilities? (yes / no)
"no" skips exploitation and documents it as not attempted.
══════════════════════════════════════════════════

If no: Set WORKER_RESULTS["EXPLOIT"] = {"skipped": true, "reason": "Not authorised by operator."}. Jump to Step 9.

If yes: Dispatch sequentially:

Agent({
  description: "EXPLOIT against {IP_RANGES}",
  subagent_type: "pentest-execute",
  prompt: "Execute an EXPLOIT task.\nEngagement ID: {ENGAGEMENT_ID}\nTargets: {IP_RANGES}\nConstraints: {CLOUD_CONSTRAINTS as JSON}\nContext: {VULN_SCAN + LOGIC_PROBE findings_preview — every attempt must reference a confirmed finding}\nCredentials: {count} entries — read from {ENGAGEMENT_DIR}/credentials.txt\nApp URL: {APP_URL}\nReturn JSON matching the worker output schema."
})

If elevated_access_confirmed: true — display: "⚠ ELEVATED ACCESS CONFIRMED. Review before proceeding." Wait for acknowledgement.

Immediately:

  1. Write {ENGAGEMENT_DIR}/raw/EXPLOIT_raw.txt
  2. POST log to companion app:
curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/logs \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"logs":[{"phase":"EXPLOIT","rawOutput":"{worker.raw_output — credentials masked}","isOutOfScope":false,"hasElevatedAccess":{worker.elevated_access_confirmed}}]}'
  1. Add "EXPLOIT" to LOGS_UPLOADED on success.
  2. Store WORKER_RESULTS["EXPLOIT"].

Ask: "Post-exploitation — authorise privilege escalation and lateral movement? (yes / no)"

If yes: dispatch POST_EXPLOIT worker, then immediately:

  1. Write {ENGAGEMENT_DIR}/raw/POST_EXPLOIT_raw.txt
  2. POST log to companion app (phase: "POST_EXPLOIT")
  3. Add "POST_EXPLOIT" to LOGS_UPLOADED on success.
  4. Store WORKER_RESULTS["POST_EXPLOIT"].

Checkpoint: Update with last_step_completed: "EXPLOIT_DISPATCHED", updated worker_results and logs_uploaded.


Step 9 — Analyze and Structure Findings

You (Claude, the orchestrator) are the analysis engine. Read all raw output files from {ENGAGEMENT_DIR}/raw/ and all structured data from WORKER_RESULTS. Do not pass data to an external workflow.

9a — Aggregate Tests Performed

Build ALL_TESTS from every worker's tests_performed array, plus additional checks visible in raw output not already captured. Sources: raw files on disk + WORKER_RESULTS structured fields.

[
  { "check": "Port 443/tcp (HTTPS)", "tool": "nmap", "worker": "PORT_SCAN", "result": "PASS", "notes": "Closed — not exposed" },
  { "check": "X-Frame-Options header", "tool": "nikto", "worker": "VULN_SCAN", "result": "FAIL", "finding_ref": "FIND-NNN" },
  { "check": "Auth enforced on GET /api/engagements", "tool": "curl", "worker": "LOGIC_PROBE", "result": "PASS", "notes": "Returns 401 without token" }
]

result: PASS (no vulnerability), FAIL (link finding_ref), INFO (observation only).

A test list with zero PASS entries is incomplete. Every assessment has controls that work correctly.

9b — Structure Findings

Build ALL_FINDINGS from all worker findings_preview arrays and any additional vulnerabilities identified in raw output review.

{
  "title": "Concise vulnerability title",
  "severity": "CRITICAL | HIGH | MEDIUM | LOW | INFORMATIONAL",
  "cvss_vector": "CVSS:3.1/AV:.../AC:.../PR:.../UI:.../S:.../C:.../I:.../A:...",
  "cvss_score": 0.0,
  "technical_summary": "What the vulnerability is and why it exists",
  "proof_of_concept": "Exact reproduction steps — credentials masked as [MASKED]",
  "impact": "What an attacker achieves",
  "remediation_steps": "Specific, actionable fix",
  "attack_path": "Target(s) and service(s) affected"
}

Severity must be UPPERCASE. CVSS must be CVSSv3 (CVSS:3.1/...). Deduplicate across workers.

CVSSv3 scoring guidance:

Scenario Vector Score
Network, no auth, high C+I impact AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N 9.1
Network, no auth, high availability (DoS) AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H 7.5
Network, low-priv auth, high impact AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N 8.8
Local, low priv, high confidentiality AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N 5.5
Missing security header AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N 4.3

9c — Identify Attack Chains

Review ALL_FINDINGS together. Identify chains where finding B is enabled or amplified by finding A. Build ATTACK_CHAINS:

{ "id": "AP-001", "title": "...", "steps": [{"step":1,"finding_ref":"FIND-NNN","action":"..."}], "combined_impact": "...", "time_to_exploit": "...", "prerequisite": "..." }

9d — Upload Findings to Companion App

Upload ALL_FINDINGS now — before Gate 2, so the human can see them during gate review and add/edit before Gate 3.

curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/findings \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "findings": [
      {
        "title": "{finding.title}",
        "severity": "{finding.severity — UPPERCASE}",
        "cvssVector": "{finding.cvss_vector — CVSSv3: CVSS:3.1/...}",
        "cvssScore": {finding.cvss_score},
        "technicalSummary": "{finding.technical_summary}",
        "proofOfConcept": "{finding.proof_of_concept — credentials masked}",
        "impact": "{finding.impact}",
        "remediationSteps": "{finding.remediation_steps}",
        "attackPath": "{finding.attack_path}"
      }
    ]
  }'

On failure: write to {ENGAGEMENT_DIR}/findings.json locally. Report to user.

Upload any logs that failed earlier (check LOGS_UPLOADED for missing phases — retry failed POSTs now):

# For each phase in [PORT_SCAN, PASSIVE_RECON, VULN_SCAN, LOGIC_PROBE, EXPLOIT, POST_EXPLOIT]
# that is NOT in LOGS_UPLOADED: retry the log POST using raw file content from disk

Checkpoint: Update with last_step_completed: "FINDINGS_UPLOADED".


Step 10 — Gate 2: Execution Audit (Poll for App Approval)

Display:

══════════════════════════════════════════════════
  GATE 2 — EXECUTION AUDIT
══════════════════════════════════════════════════
Uploaded to companion app:
  ✓ Execution logs: {LOGS_UPLOADED list} ({n} phases)
  ✓ {n} findings uploaded for preview
  {any phases that failed upload: "⚠ PHASE_NAME — upload failed, local copy at raw/PHASE_NAME_raw.txt"}

Action required in the companion app:
  {APP_URL}/engagements/{ENGAGEMENT_ID} → Gate 2 tab

Review the execution logs. When satisfied, click
"Complete Gate 2 — Approve Execution Audit" in the app.

Polling for GATE_2_AUDITED every 60 seconds...
══════════════════════════════════════════════════

Poll every 60 seconds:

curl -s {APP_URL}/api/engagements/{ENGAGEMENT_ID} \
  -H "Authorization: Bearer {APP_TOKEN}" | jq -r .status

Print on each poll: "Waiting for Gate 2 approval in app... (status: {status})"

Continue until status = GATE_2_AUDITED.

Checkpoint: Update with last_step_completed: "GATE_2_APPROVED".


Step 11 — Gate 3: Findings Review (Poll for App Approval)

Display:

══════════════════════════════════════════════════
  GATE 3 — FINDINGS REVIEW
══════════════════════════════════════════════════
Gate 2 approved. {n} findings are in the companion app.

Action required in the companion app:
  {APP_URL}/engagements/{ENGAGEMENT_ID} → Gate 3 tab

Review and edit findings. Add any manual findings from
human tester activity. When complete, click
"Complete Gate 3 — Approve Findings Review" in the app.

The report will be generated from the findings in the app
at the time Gate 3 is approved — including anything added manually.

Polling for GATE_3_REVIEWED every 60 seconds...
══════════════════════════════════════════════════

Poll every 60 seconds until status = GATE_3_REVIEWED.

Checkpoint: Update with last_step_completed: "GATE_3_APPROVED".


Step 12 — Generate Report (after Gate 3)

Gate 3 is approved. The human has reviewed, edited, and finalized findings — including any manual additions. The report is generated now so it reflects the approved finding set.

12a — Fetch Approved Findings from App

Fetch the current findings from the companion app — these are the Gate-3-approved findings, including any human tester additions:

curl -s {APP_URL}/api/engagements/{ENGAGEMENT_ID}/findings \
  -H "Authorization: Bearer {APP_TOKEN}"

Store as APPROVED_FINDINGS. These take precedence over the locally-analyzed findings for the report.

12b — Compose Report

Build the full report markdown. Use APPROVED_FINDINGS (from app), ALL_TESTS (from worker analysis), ATTACK_CHAINS (from worker analysis), and WORKER_RESULTS metadata.

# Penetration Test Report

**Engagement ID:** {ENGAGEMENT_ID}
**Assessment Date:** {today's date}
**Assessment Type:** {APP_ASSESSMENT_TYPE} — Web Application

---

## 1. Executive Summary

**Client:** {CLIENT_NAME}
**Environment:** {providers} ({IP_RANGES joined})
**Findings:** Critical: {n} | High: {n} | Medium: {n} | Low: {n} | Informational: {n} | Total: {n}
**Test Coverage:** {total} checks — {pass_count} passed · {fail_count} failed · {info_count} informational

{Business risk narrative — 2–3 paragraphs in non-technical language explaining who is at risk,
what data or operations are exposed, and what the blast radius of the most critical findings is.}

### Most Critical Attack Path
{Plain language description of AP-001 — fastest path to full compromise.}

---

## 2. Scope & Methodology

**Authorised Targets:**
{list each target with port/service description}

**Out of Scope:**
{EXCLUSIONS from SOW}

**Assessment Type:** {APP_ASSESSMENT_TYPE}

**Tools Used:**
{deduplicated tool list from all worker tool_availability.available arrays}

**Side Effects Documented:**
{any service disruptions, state changes, or test artifacts created during assessment}

---

## 3. Findings

**IMPORTANT: Include EVERY finding from APPROVED_FINDINGS — CRITICAL, HIGH, MEDIUM, LOW, and INFORMATIONAL.
Do not omit, summarise, or group lower-severity findings. Each finding gets its own full section with
all fields populated. The report is the client deliverable — every finding must have enough detail
for the client to understand and remediate the issue independently.**

Sorted by cvssScore descending, numbered FIND-001 onward. Total count must match the finding tally
in the Executive Summary. If counts do not match, a finding has been dropped — go back and add it.

{Repeat the block below for EVERY finding in APPROVED_FINDINGS:}

### FIND-{NNN}: {title}

**Severity:** {SEVERITY}
**CVSS Score:** {score}
**CVSS Vector:** {CVSSv3 vector}
**Affected Target:** {attackPath}

**Description:**
{technicalSummary}

**Reproduction Steps:**
\```
{proofOfConcept — credentials replaced with [MASKED]}
\```

**Business Impact:**
{impact}

**Remediation:**
{remediationSteps}

---

## 4. Test Coverage

Every check performed. PASS = no issue. FAIL = vulnerability confirmed. INFO = observation.

| # | Check | Tool | Phase | Result | Finding |
|---|---|---|---|---|---|
{one row per ALL_TESTS entry}

**Summary:** {pass_count} passed · {fail_count} failed · {info_count} informational · {total} total

---

## 5. Attack Paths

{For each ATTACK_CHAIN — numbered steps, combined impact, time to exploit, prerequisite}

---

## 6. Remediation Priority Matrix

| Priority | Finding | Severity | CVSS | Effort | Notes |
|---|---|---|---|---|---|
{APPROVED_FINDINGS sorted by cvssScore descending; note where fixing one eliminates preconditions for others}

---

## 7. Compliance Exclusions

{Items excluded per SOW + cloud ToS restrictions + tool availability gaps}

**Side effects during testing:** {document every disruption or test artifact}

---

*Report generated by Pentest Platform — engagement {ENGAGEMENT_ID}*
*Generated after Gate 3 approval — findings reflect human-reviewed and augmented set*
*QA sign-off pending — awaiting Gate 4 approval*

12c — Save and Upload Report

Write locally:

{ENGAGEMENT_DIR}/report_draft.md

Pre-upload self-check — verify before uploading:

  • Count FIND-NNN sections in the draft. Must equal the total finding count in the Executive Summary.
  • Every finding has: Severity, CVSS Score, CVSS Vector, Affected Target, Description, Reproduction Steps, Business Impact, Remediation.
  • No finding is described as "see above", "similar to FIND-NNN", or summarised in one line.
  • The Test Coverage table contains every check from ALL_TESTS — not a sample.

If any check fails, complete the missing sections before uploading.

Post to companion app:

curl -s -X POST {APP_URL}/api/engagements/{ENGAGEMENT_ID}/pipeline/report \
  -H "Authorization: Bearer {APP_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"content": "{report markdown — JSON string escaped}"}'

Checkpoint: Update with last_step_completed: "REPORT_UPLOADED".


Step 13 — Gate 4: Report Approval (Poll for App Approval)

Display:

══════════════════════════════════════════════════
  GATE 4 — REPORT APPROVAL
══════════════════════════════════════════════════
Gate 3 approved. Report generated from {n} approved findings
({n} from automated analysis + any manual additions from Gate 3).

Action required in the companion app:
  {APP_URL}/engagements/{ENGAGEMENT_ID} → Gate 4 tab

Review the full report. Complete the QA checklist.
When satisfied, click "Approve Report" in the app.

Polling for GATE_4_APPROVED every 60 seconds...
══════════════════════════════════════════════════

Poll every 60 seconds until status = GATE_4_APPROVED.


Step 14 — Complete

Delete checkpoint:

rm -f "{ENGAGEMENT_DIR}/pentest_checkpoint.json"

Display:

══════════════════════════════════════════════════
  PENTEST PLATFORM — ENGAGEMENT COMPLETE
══════════════════════════════════════════════════
Engagement    : {CLIENT_NAME} ({ENGAGEMENT_ID})
Assessment    : {APP_ASSESSMENT_TYPE}
Findings      : {Critical} Crit / {High} High / {Medium} Med / {Low} Low / {Info} Info
Tests run     : {total} checks — {pass} passed · {fail} failed · {info} informational
Logs uploaded : {LOGS_UPLOADED list}
Report (local): engagements/{ENGAGEMENT_ID}/report_draft.md
Report (app)  : {APP_URL}/engagements/{ENGAGEMENT_ID} → Gate 4 tab
══════════════════════════════════════════════════

Rules

  • Never execute test commands. All Bash execution is in /pentest-execute workers.
  • Never start before GATE_1_APPROVED. Stop if status is anything else.
  • Logs are uploaded immediately per worker. Write raw to disk and POST to /pipeline/logs as the first action when each worker result arrives — before checkpointing or dispatching anything else.
  • Gate approvals happen only in the app. Never call gate2/complete, gate3/complete, or gate4/complete. Poll; do not push.
  • Report is generated after Gate 3. It uses findings fetched from the app at that point — including any manual additions by human testers.
  • Credentials are execution-only. Mask as [MASKED] everywhere. Never write real values to checkpoints, reports, logs, or findings.
  • CVSS must be CVSSv3 format. CVSS:3.1/AV:.../... — backend rejects CVSSv4. Severity must be UPPERCASE.
  • Test coverage must include passes. A list with zero PASS entries is incomplete.
  • Cloud compliance exclusions are documented. Any test skipped due to provider ToS appears in Compliance Exclusions and in the execution logs.
  • Self-pentest risk. When target includes localhost, warn at Step 4 that all localhost services are in scope. Recommend adding orchestration tool ports to exclusions before scanning.