feat(3.6.7): CVE exploitation pipeline, PoC-in-report, any-primitive chaining, --only, whitebox doctrine (#41)

Version 3.6.6 -> 3.6.7. +5 agents (430 -> 435).

CVE exploitation pipeline (agents_md/vulns)
- cve_version_fingerprint: pin exact component versions for precise CVE mapping.
- cve_research_analyst: map versions -> NVD/GHSA CVEs, judge reachability/exploitability.
- cve_poc_finder: locate/vet/adapt a public PoC, run non-destructively.
- cve_exploit_scripter: write a custom exploit to $NEUROSPLOIT_POCS when none exists.

Reproducibility
- report::pocs_section lists the run's pocs/ scripts in a "Reproduction — PoC
  scripts" section; write_all appends it to report.md. Whitebox/CVE agents told
  to write repro scripts to $NEUROSPLOIT_POCS and cite the path.

Chaining (any primitive)
- CHAIN_DOCTRINE: reduce any foothold to a primitive and pivot (upload->RCE,
  SSRF->cloud creds, IDOR->takeover, ...), reuse looted creds, reason about
  business logic. New chain_cve_to_rce_to_pivot recipe. Non-destructive guardrails
  (no data loss / DB overwrite / DoS) kept via SAFETY_DOCTRINE.

Re-test one vuln
- --only <agent> on run/whitebox/greybox sets cfg.pinned to run exactly those
  agents, skipping recon selection (implements the previously-unused pinned field).

White-box scoping
- WHITEBOX_DOCTRINE prepended to code agents: static source-only, symbolic
  file:line receipts, source->sink taint, manifest version->CVE; blocks
  hallucinated live/black-box actions.

Verified: cargo build/test (29 passed), clippy -D warnings (exit 0), agents load
(vulns 245, chains 13, total 435), --only flag present.


Claude-Session: https://claude.ai/code/session_01QDses7zTSa9YF7pPRjphvh

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joas A Santos
2026-08-04 10:44:18 -03:00
committed by GitHub
co-authored by Claude Fable 5
parent 76b56898d1
commit cb19e2194d
14 changed files with 423 additions and 40 deletions
+2 -2
View File
@@ -871,7 +871,7 @@ dependencies = [
[[package]]
name = "neurosploit"
version = "3.6.6"
version = "3.6.7"
dependencies = [
"anyhow",
"clap",
@@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
version = "3.6.6"
version = "3.6.7"
dependencies = [
"anyhow",
"futures",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.6.6"
version = "3.6.7"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
+36 -7
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.6 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
//! NeuroSploit v3.6.7 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
mod repl;
mod tui;
@@ -11,8 +11,8 @@ use std::path::{Path, PathBuf};
#[command(
name = "neurosploit",
version,
about = "NeuroSploit v3.6.6 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.6 — a Rust multi-model harness that drives a pool of LLMs \
about = "NeuroSploit v3.6.7 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.7 — a Rust multi-model harness that drives a pool of LLMs \
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
@@ -77,6 +77,11 @@ enum Cmd {
/// Open a Jira card per finding (needs the jira integration enabled).
#[arg(long)]
jira: bool,
/// Re-test ONLY these agent(s), skipping recon-based selection — repeatable
/// or comma/semicolon-separated (e.g. `--only sqli --only cve_hunter`).
/// Run `neurosploit agents` for the names.
#[arg(long = "only")]
only: Vec<String>,
/// Verbose: log each agent as it launches, recon, and votes.
#[arg(short, long)]
verbose: bool,
@@ -105,6 +110,9 @@ enum Cmd {
/// Open a Jira card per finding (needs the jira integration enabled).
#[arg(long)]
jira: bool,
/// Re-test ONLY these code agent(s) — repeatable or comma/semicolon-separated.
#[arg(long = "only")]
only: Vec<String>,
#[arg(short, long)]
verbose: bool,
},
@@ -139,6 +147,9 @@ enum Cmd {
subscription: bool,
#[arg(long)]
mcp: bool,
/// Re-test ONLY these agent(s) — repeatable or comma/semicolon-separated.
#[arg(long = "only")]
only: Vec<String>,
#[arg(short, long)]
verbose: bool,
},
@@ -379,7 +390,7 @@ async fn main() -> anyhow::Result<()> {
}
}
}
Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, objective, out_of_scope, jira, verbose } => {
Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, objective, out_of_scope, jira, only, verbose } => {
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
let mut cfg = RunConfig::new(&url);
cfg.max_agents = max_agents;
@@ -392,6 +403,7 @@ async fn main() -> anyhow::Result<()> {
cfg.instructions = focus;
cfg.objective = objective;
cfg.out_of_scope = out_of_scope;
cfg.pinned = parse_only(&only);
if !models.is_empty() {
cfg.models = models;
}
@@ -401,7 +413,7 @@ async fn main() -> anyhow::Result<()> {
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
post_integrations(&ig, &url, &out, jira, false, None).await;
}
Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, recon, offline, subscription, jira, verbose } => {
Cmd::Whitebox { path, models, max_agents, vote_n, chain_depth, recon, offline, subscription, jira, only, verbose } => {
let path = resolve_source(&base, &path)?; // local path OR github URL/owner/repo
let mut cfg = RunConfig::new(&path);
cfg.max_agents = max_agents;
@@ -411,6 +423,7 @@ async fn main() -> anyhow::Result<()> {
cfg.offline = offline;
cfg.subscription = subscription;
cfg.verbose = verbose;
cfg.pinned = parse_only(&only);
if !models.is_empty() {
cfg.models = models;
}
@@ -419,7 +432,7 @@ async fn main() -> anyhow::Result<()> {
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
post_integrations(&ig, &path, &out, jira, false, None).await;
}
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, verbose } => {
Cmd::Greybox { repo, url, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, only, verbose } => {
let repo = resolve_source(&base, &repo)?; // local path OR github URL/owner/repo
let url = if url.starts_with("http") { url } else { format!("https://{url}") };
let mut cfg = RunConfig::new(&url);
@@ -432,6 +445,7 @@ async fn main() -> anyhow::Result<()> {
cfg.subscription = subscription;
cfg.verbose = verbose;
cfg.instructions = focus;
cfg.pinned = parse_only(&only);
if !models.is_empty() {
cfg.models = models;
}
@@ -751,7 +765,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
println!(" │ ua : {ua}");
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
println!(" ┌─ NeuroSploit v3.6.6 · by Joas A Santos & Red Team Leaders");
println!(" ┌─ NeuroSploit v3.6.7 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
@@ -896,6 +910,21 @@ pub(crate) fn print_findings(out: &RunOutput) {
}
}
/// Parse repeated `--only` values into a clean agent allowlist. Accepts repeats
/// and comma/semicolon-separated lists (`--only sqli,xss` == `--only sqli --only xss`).
fn parse_only(vals: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for v in vals {
for part in v.split([',', ';']) {
let name = part.trim();
if !name.is_empty() && !out.iter().any(|x| x == name) {
out.push(name.to_string());
}
}
}
out
}
fn sanitize(s: &str) -> String {
let s = s.replace("https://", "").replace("http://", "");
let mut o: String = s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect();
+2 -2
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.6 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//! NeuroSploit v3.6.7 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
@@ -370,7 +370,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.6");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.7");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.6 — TUI "Mission Control" mode.
//! NeuroSploit v3.6.7 — TUI "Mission Control" mode.
//!
//! Concurrent panels that update live while the engagement runs in the
//! background, with a composer input that stays active during execution:
+81 -14
View File
@@ -313,6 +313,37 @@ const DECISION_DOCTRINE: &str = "DECIDE WHERE TO ATTACK (analyse, then act):\n\
- Build PoCs when needed: for issues that need an artifact to prove (clickjacking an HTML page that frames the target; CSRF an auto-submitting HTML form; a multi-step or timing exploit a script), WRITE the PoC to the run's PoC dir, run/validate it, and cite the file in the evidence.\n\
- Test control BYPASSES: when something returns 401/403/redirect or is 'blocked', try to bypass it (verb tampering, path/case/encoding normalization, X-Original-URL / X-Rewrite-URL / X-Forwarded-* headers, missing-vs-invalid token, direct object/API access) and confirm the bypass with the two requests.\n\n";
/// CHAIN doctrine: turn ANY foothold into the next step. A primitive→next-step
/// playbook (not an exhaustive script) so the agent always has a concrete pivot
/// to reason about, plus a push to chain toward BUSINESS impact — all under the
/// non-destructive SAFETY_DOCTRINE (prove RCE/access with a benign marker, never
/// harm data or state).
const CHAIN_DOCTRINE: &str = "CHAIN THE FOOTHOLD (pivot to deeper, provable impact — any primitive can chain):\n\
- Think in primitives, not labels: reduce the foothold to what it GIVES you (code exec, file read, file write, request forgery, a trusted identity, a leaked secret, arbitrary object access) and pick the next step from that.\n\
- Pivot playbook (attempt the fitting ones, prove each with a benign receipt):\n\
· File upload / write RCE: upload a webshell/handler to an executable path or poison a config/`.htaccess`/cron/serialized file; prove with a benign marker (`id`, unique echo, OOB DNS), not damage.\n\
· SSRF cloud/host takeover: hit `169.254.169.254` (IMDSv1/v2), GCP/Azure metadata, internal admin/actuator, `file://`/`gopher://`; loot temp creds/tokens and REUSE them.\n\
· SQLi RCE/LPE: stacked queries, `INTO OUTFILE`/`COPY TO`, UDF, `xp_cmdshell`, read secrets/creds; then reuse creds to log in and escalate.\n\
· LFI/path traversal RCE: log/session/wrapper poisoning, `/proc/self/environ`, read source & secrets; combine with an upload for exec.\n\
· XXE SSRF/file read creds; deserialization/SSTI RCE via a gadget/template sink; prove exec with a marker.\n\
· IDOR/BOLA/mass-assignment account/tenant takeover or role escalation (`role=admin`); open-redirect/XSS/CORS token/session theft ATO.\n\
· Exposed `.git`/backup/`.env`/secrets reconstruct source & keys auth to internal APIs, cloud, DB; default/leaked creds domain/service compromise.\n\
- Reuse loot relentlessly: every credential/JWT/cookie/API key/host you obtain is input to the next step carry it forward across modules and try it everywhere it might be accepted.\n\
- Understand the BUSINESS & LOGIC: reason about what the app is FOR (payments, orders, tenancy, KYC, entitlements) and chain toward business impact payment/price/coupon abuse, cross-tenant data access, entitlement/limit bypass, workflow/state-machine skips (skip approval/verification steps), race conditions on balance/stock. These compound: each finding updates your model of the app for the next probe.\n\
- Stop at proof: demonstrate the impact with the SMALLEST safe step and report the CHAIN end-to-end; never destroy, overwrite, encrypt, mass-exfiltrate, or DoS to 'prove' it.\n\n";
/// WHITEBOX doctrine: this is a STATIC source review — keep the agent in the code,
/// not on the wire. Prevents whitebox runs from hallucinating black-box network
/// actions (curl/nuclei/live requests) they cannot perform here, and pushes for
/// a symbolic `file:line` receipt plus a runnable repro PoC where it adds value.
const WHITEBOX_DOCTRINE: &str = "MODE: WHITE-BOX STATIC SOURCE REVIEW. You are reading source code, NOT a live target.\n\
- Source-only: reason strictly about the provided code. Do NOT curl, run nuclei, browse, or claim any live/HTTP/network result there is no running app here. Any \"I sent a request / got a response\" claim is a hallucination and will be rejected.\n\
- Symbolic receipt: EVERY finding's evidence is a `file:line` citation plus the exact vulnerable code quoted verbatim. The code citation IS the proof. No `file:line` + code quote do not report it.\n\
- Trace, don't guess: follow tainted input from its SOURCE (request param, env, deserialization, file) to a dangerous SINK (SQL/exec/eval/template/path/SSRF/deserialize). Report only when source reaches sink without effective sanitization; note the path (`entry sink`).\n\
- Version CVE (static): read dependency manifests (package.json, requirements.txt, go.mod, pom.xml, Gemfile.lock, Cargo.lock) and pin exact versions; map to known CVEs and cite the manifest line. Flag reachable, exploitable ones over merely-outdated ones.\n\
- Repro PoC (optional but valued): when a finding warrants it, WRITE a proof/repro script to $NEUROSPLOIT_POCS e.g. the exact malicious input + the request/CLI call that would trigger the sink, or a unit-style harness exercising the vulnerable function with a header comment (file:line it proves, how to run). Cite the PoC path in the evidence. Mark clearly that it demonstrates the code path (static-derived), not a live hit.\n\
- Calibrate: High/Critical only when the sink is reachable and exploitable from untrusted input; guarded/unreachable code is Low or a lead.\n\n";
/// Methodology directions for a modern JS SPA backed by a REST/GraphQL API
/// (Angular/React/Vue front + Node/Express-style API — the shape of OWASP Juice
/// Shop and many real apps). These are DIRECTIONS on HOW to hunt each vuln class,
@@ -456,20 +487,37 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
// Use the model to pick the agents whose preconditions match the recon —
// the harness reasons about *which* specialists to run, not all of them.
// Exception: when the operator pinned an explicit set (--only), run EXACTLY
// those and skip recon-based selection — used to re-test a single vuln.
let focus = cfg.instructions.clone().unwrap_or_default();
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
let selected: Vec<Agent> = if !chosen.is_empty() {
let selected: Vec<Agent> = if !cfg.pinned.is_empty() {
let sel: Vec<Agent> =
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
ranked.iter().filter(|a| cfg.pinned.iter().any(|p| p == &a.name)).cloned().collect();
if sel.is_empty() {
heuristic_select(&ranked, &recon, &focus, cap)
let _ = tx.send(format!("--only matched no agent ({}) — falling back to recon selection",
cfg.pinned.join(", "))).await;
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).take(cap).cloned().collect()
} else {
sel.into_iter().take(cap).collect()
let _ = tx.send(format!("--only: running exactly {} pinned agent(s): {}", sel.len(),
sel.iter().map(|a| a.name.clone()).collect::<Vec<_>>().join(", "))).await;
sel
}
} else {
// LLM selection failed/empty → recon+focus keyword heuristic, not a blind flat list.
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
heuristic_select(&ranked, &recon, &focus, cap)
let chosen = select_agents(pool, &recon, &focus, &ranked, &tx).await;
if !chosen.is_empty() {
let sel: Vec<Agent> =
ranked.iter().filter(|a| chosen.iter().any(|c| c == &a.name)).cloned().collect();
if sel.is_empty() {
heuristic_select(&ranked, &recon, &focus, cap)
} else {
sel.into_iter().take(cap).collect()
}
} else {
// LLM selection failed/empty → recon+focus keyword heuristic, not a blind flat list.
let _ = tx.send("selection empty — using recon-keyword heuristic".into()).await;
heuristic_select(&ranked, &recon, &focus, cap)
}
};
// Dedup: never run the same agent twice in one engagement.
let mut selected: Vec<Agent> = {
@@ -479,7 +527,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
// No creds given → always run the registration/form agent FIRST so the run
// reaches the authenticated surface (and the operator sees it happen). It
// self-registers one test account under the anti-flood guardrail.
if cfg.auth.as_deref().unwrap_or("").trim().is_empty() {
if cfg.pinned.is_empty() && cfg.auth.as_deref().unwrap_or("").trim().is_empty() {
if let Some(reg) = lib.vulns.iter().find(|a| a.name == "account_registration_and_forms") {
if !selected.iter().any(|a| a.name == reg.name) {
let _ = tx.send("no creds set — running account_registration_and_forms first to reach the authenticated surface".into()).await;
@@ -597,7 +645,22 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
}
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
let mut ranked: Vec<Agent> = if lib.code.is_empty() { lib.vulns.clone() } else { lib.code.clone() };
let pool_agents: Vec<Agent> = if lib.code.is_empty() { lib.vulns.clone() } else { lib.code.clone() };
let mut ranked: Vec<Agent> = if cfg.pinned.is_empty() {
pool_agents
} else {
// Operator pinned an explicit agent set (--only): re-test exactly those.
let sel: Vec<Agent> = pool_agents.iter()
.filter(|a| cfg.pinned.iter().any(|p| p == &a.name)).cloned().collect();
if sel.is_empty() {
let _ = tx.send(format!("--only matched no code agent ({}) — reviewing with the full set",
cfg.pinned.join(", "))).await;
pool_agents
} else {
let _ = tx.send(format!("--only: reviewing with exactly {} pinned agent(s)", sel.len())).await;
sel
}
};
ranked.sort_by(|a, b| rl.weight(&b.name).partial_cmp(&rl.weight(&a.name)).unwrap_or(std::cmp::Ordering::Equal));
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(ranked.len()) } else { ranked.len() };
let selected: Vec<Agent> = ranked.into_iter().take(cap).collect();
@@ -616,11 +679,15 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
let user = format!(
"{}\n\nSOURCE CODE TO REVIEW:\n```\n{}\n```\n\nReply ONLY with a JSON array of findings (may be empty []). \
Each item: {{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}} \
where `endpoint` is the file:line and `evidence` quotes the vulnerable code.",
where `endpoint` is the file:line and `evidence` quotes the vulnerable code. \
When a finding warrants a runnable proof, write a repro script to $NEUROSPLOIT_POCS and put its path in `payload`.",
ag.user.replace("{target}", "the provided repository").replace("{recon_json}", "{}"),
ctx
);
match pool.complete_routed(Task::Exploit, &ag.name, &ag.system, &user).await {
// Prepend the white-box doctrine so code agents stay in static
// source-review mode and never hallucinate live/black-box actions.
let sys = format!("{}{}", WHITEBOX_DOCTRINE, ag.system);
match pool.complete_routed(Task::Exploit, &ag.name, &sys, &user).await {
Ok((m, text)) => {
let f = extract_findings(&text, &ag.name);
let _ = txc.send(format!("analyze {} via {}{} candidate(s)", ag.name, m.label(), f.len())).await;
@@ -910,13 +977,13 @@ async fn chain_from_seed(pool: &ModelPool, target: &str, directives: &str, recon
};
let short: String = seed.title.chars().take(28).collect();
let user = format!(
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{depth}{decision}{safety}{doctrine}\
"AUTHORIZED engagement on {target}.\n\n{directives}{react}{depth}{decision}{chain}{safety}{doctrine}\
FOOTHOLD TO EXPAND (round {round}/{max}):\n- [{}] {} @ {} ({})\n payload: {}\n evidence: {}\n\n\
LOOT GATHERED (reuse it):\n{loot_block}\n\n{recipe_block}RECON:\n{recon_ctx}\n\n\
From THIS foothold, DECIDE the best directions and PROVE new impact post-exploitation (loot creds/keys/config/source), credential reuse, privilege escalation (horizontal & vertical), lateral movement to adjacent services/hosts, data exfiltration, and NEW attack surface it exposes. Every claim needs a real tool receipt.\n\n\
Reply ONLY JSON: {{\"findings\":[{{id,title,severity,cwe,endpoint,payload,evidence,impact,remediation,confidence}}],\"loot\":[\"cred:user:pass@host\",\"token:...\",\"host:10.0.0.5\",\"endpoint:/internal/api\"]}} (empty arrays are fine).",
seed.severity, seed.title, seed.endpoint, seed.cwe, seed.payload, seed.evidence,
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, safety = SAFETY_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
react = REACT_DOCTRINE, depth = DEPTH_DOCTRINE, decision = DECISION_DOCTRINE, chain = CHAIN_DOCTRINE, safety = SAFETY_DOCTRINE, doctrine = tool_doctrine(pool.mcp_config.is_some()),
);
let label = format!("chain:{short}");
match pool.complete_routed(Task::Exploit, &label, CHAIN_SYS, &user).await {
+41 -1
View File
@@ -413,11 +413,51 @@ pub fn json_report(target: &str, findings: &[Finding], run_id: &str, meta: &Enga
/// Write the full report bundle: Markdown, JSON, HTML, and the Typst/PDF.
/// Returns the primary artifact path (PDF if typst present, else the .typ).
/// A "## Reproduction — PoC scripts" section listing the runnable proof-of-concept
/// scripts agents wrote to `<run>/pocs/`. Each is a self-contained artifact the
/// operator can re-run to replicate a finding, so the report ships with a live
/// reproduction kit — not just prose. Empty string when no PoCs were produced.
pub fn pocs_section(dir: &Path) -> String {
let pocs = dir.join("pocs");
let mut entries: Vec<(String, String)> = Vec::new();
if let Ok(rd) = std::fs::read_dir(&pocs) {
for e in rd.flatten() {
let p = e.path();
if !p.is_file() { continue; }
let name = match p.file_name().and_then(|s| s.to_str()) { Some(n) => n.to_string(), None => continue };
// First non-empty comment line doubles as a one-line description.
let desc = std::fs::read_to_string(&p).ok()
.and_then(|t| t.lines()
.map(|l| l.trim())
.find(|l| l.starts_with('#') || l.starts_with("//") || l.starts_with("/*"))
.map(|l| l.trim_start_matches(['#', '/', '*', ' ']).trim().to_string()))
.unwrap_or_default();
entries.push((name, desc));
}
}
if entries.is_empty() { return String::new(); }
entries.sort();
let mut s = String::from("## Reproduction — PoC scripts\n\n");
s.push_str("Runnable proofs written to `pocs/` during the engagement. Re-run any of \
them to replicate the corresponding finding.\n\n");
for (name, desc) in entries {
if desc.is_empty() {
s.push_str(&format!("- `pocs/{name}`\n"));
} else {
s.push_str(&format!("- `pocs/{name}` — {desc}\n"));
}
}
s.push('\n');
s
}
pub fn write_all(target: &str, findings: &[Finding], dir: &Path) -> std::io::Result<PathBuf> {
std::fs::create_dir_all(dir)?;
let run_id = dir.file_name().and_then(|s| s.to_str()).unwrap_or("run").to_string();
let meta = read_meta(dir);
std::fs::write(dir.join("report.md"), markdown(target, findings, &meta))?;
let mut md = markdown(target, findings, &meta);
md.push_str(&pocs_section(dir));
std::fs::write(dir.join("report.md"), md)?;
std::fs::write(dir.join("report.json"), json_report(target, findings, &run_id, &meta))?;
std::fs::write(dir.join("report.html"), html(target, findings, &meta))?;
typst_report(target, findings, dir)