mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-08-14 13:40:23 +02:00
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:
co-authored by
Claude Fable 5
parent
76b56898d1
commit
cb19e2194d
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user