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.claude/skills/pentest/SKILL.md
GitHubpentest — 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:
- Workers run → logs uploaded to app immediately per worker as each completes
- Gate 2: human reviews all execution logs in app → approves
- Findings analyzed and uploaded to app
- Gate 3: human reviews, edits, and adds manual findings in app → approves
- Report generated from current app findings (includes human additions) → uploaded
- 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-companionclone? (SetPENTEST_COMPANION_PATHin 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:8001if running inside Docker (/.dockerenv exists), otherwisehttp://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, orWHITE_BOX - APP_CREDENTIALS_HINT:
credentialsfield (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_HINTisn/aor 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:
- Write raw output to disk:
{ENGAGEMENT_DIR}/raw/PORT_SCAN_raw.txt - 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}}]}'
- On POST success: add
"PORT_SCAN"to LOGS_UPLOADED. On failure: note error, disk file is fallback. - Store WORKER_RESULTS["PORT_SCAN"] ← structured fields only (findings_preview, tests_performed, executed_commands, tool_availability).
For PASSIVE_RECON:
- Write raw output to disk:
{ENGAGEMENT_DIR}/raw/PASSIVE_RECON_raw.txt - 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}}]}'
- On POST success: add
"PASSIVE_RECON"to LOGS_UPLOADED. On failure: note error. - 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:
- Write
{ENGAGEMENT_DIR}/raw/VULN_SCAN_raw.txt - 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}}]}'
- Add
"VULN_SCAN"to LOGS_UPLOADED on success. - Store WORKER_RESULTS["VULN_SCAN"] ← structured fields only.
For LOGIC_PROBE:
- Write
{ENGAGEMENT_DIR}/raw/LOGIC_PROBE_raw.txt - 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}}]}'
- Add
"LOGIC_PROBE"to LOGS_UPLOADED on success. - 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:
- Write
{ENGAGEMENT_DIR}/raw/EXPLOIT_raw.txt - 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}}]}'
- Add
"EXPLOIT"to LOGS_UPLOADED on success. - Store WORKER_RESULTS["EXPLOIT"].
Ask: "Post-exploitation — authorise privilege escalation and lateral movement? (yes / no)"
If yes: dispatch POST_EXPLOIT worker, then immediately:
- Write
{ENGAGEMENT_DIR}/raw/POST_EXPLOIT_raw.txt - POST log to companion app (phase: "POST_EXPLOIT")
- Add
"POST_EXPLOIT"to LOGS_UPLOADED on success. - 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.