mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-07-08 20:37:52 +02:00
v3.5.0: attack graph + kill chain (OWASP/CWE/MITRE) + GPT 5.5/5.4/5.3-codex/5.2 + report graph
- Finding enriched with owasp / mitre / kill-chain stage / exploitability / business_impact / chains_from (attack-path edges). - attack_graph module: derive OWASP Top 10 + MITRE ATT&CK technique + kill-chain stage from CWE (heuristic, no extra model call); render a Mermaid attack-path flowchart (findings grouped by stage, explicit + implicit edges) and an ASCII kill chain for the REPL. - enrich() runs in finish() for every engagement. - HTML report gains an "Attack Path & Kill Chain" section (Mermaid via CDN, dark) plus a stage/sev/OWASP/MITRE/exploitability table. - REPL print_findings shows the ASCII kill-chain + severity summary after a run. - models: add GPT-5.5, GPT-5.4, GPT-5.4-mini, GPT-5.3-codex, GPT-5.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -345,9 +345,17 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
|
|||||||
|
|
||||||
pub(crate) fn print_findings(out: &RunOutput) {
|
pub(crate) fn print_findings(out: &RunOutput) {
|
||||||
println!("\n=== {} validated finding(s) ===", out.findings.len());
|
println!("\n=== {} validated finding(s) ===", out.findings.len());
|
||||||
println!("{}", serde_json::to_string_pretty(&out.findings).unwrap_or_default());
|
if !out.findings.is_empty() {
|
||||||
|
let mut by = std::collections::BTreeMap::new();
|
||||||
|
for f in &out.findings { *by.entry(f.severity.as_str()).or_insert(0) += 1; }
|
||||||
|
let chips: Vec<String> = by.iter().map(|(k, v)| format!("{k}:{v}")).collect();
|
||||||
|
println!(" severity: {}", chips.join(" "));
|
||||||
|
println!("\n \x1b[1mAttack path / kill chain\x1b[0m");
|
||||||
|
print!("{}", harness::attack_graph::ascii_killchain(&out.findings));
|
||||||
|
}
|
||||||
if !out.artifacts.is_empty() {
|
if !out.artifacts.is_empty() {
|
||||||
println!("artifacts: {}", out.artifacts.join(", "));
|
println!("\n artifacts: {}", out.artifacts.join(", "));
|
||||||
|
println!(" (full attack graph rendered in report.html)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//! Attack graph & kill-chain mapping.
|
||||||
|
//!
|
||||||
|
//! Enriches findings with OWASP Top 10 / MITRE ATT&CK / kill-chain stage /
|
||||||
|
//! exploitability (derived from CWE + severity when the model didn't supply
|
||||||
|
//! them), then renders an attack-path graph (Mermaid) and a kill-chain table for
|
||||||
|
//! the report, plus a compact ASCII summary for the REPL.
|
||||||
|
|
||||||
|
use crate::types::Finding;
|
||||||
|
|
||||||
|
/// CWE → (OWASP Top 10 2021, MITRE ATT&CK technique, kill-chain stage).
|
||||||
|
fn map_cwe(cwe: &str) -> (&'static str, &'static str, &'static str) {
|
||||||
|
let n: u32 = cwe.trim_start_matches("CWE-").parse().unwrap_or(0);
|
||||||
|
match n {
|
||||||
|
89 | 943 => ("A03:2021-Injection", "T1190", "initial-access"),
|
||||||
|
77 | 78 | 94 | 95 | 917 | 1336 => ("A03:2021-Injection", "T1059", "execution"),
|
||||||
|
79 | 80 => ("A03:2021-Injection", "T1059.007", "execution"),
|
||||||
|
90 => ("A03:2021-Injection", "T1190", "initial-access"),
|
||||||
|
611 | 776 => ("A05:2021-Security-Misconfiguration", "T1190", "initial-access"),
|
||||||
|
918 => ("A10:2021-SSRF", "T1090", "lateral"),
|
||||||
|
22 | 23 | 98 | 73 => ("A01:2021-Broken-Access-Control", "T1083", "execution"),
|
||||||
|
639 | 862 | 863 | 284 | 285 => ("A01:2021-Broken-Access-Control", "T1078", "privesc"),
|
||||||
|
287 | 384 | 613 | 620 => ("A07:2021-Auth-Failures", "T1078", "initial-access"),
|
||||||
|
798 | 522 | 321 | 256 | 257 | 312 | 319 => ("A07:2021-Auth-Failures", "T1552", "credential-access"),
|
||||||
|
502 => ("A08:2021-Software-Data-Integrity", "T1059", "execution"),
|
||||||
|
327 | 328 | 916 | 326 | 330 => ("A02:2021-Cryptographic-Failures", "T1600", "credential-access"),
|
||||||
|
200 | 209 | 538 | 540 | 532 => ("A05:2021-Security-Misconfiguration", "T1592", "recon"),
|
||||||
|
601 => ("A01:2021-Broken-Access-Control", "T1566", "initial-access"),
|
||||||
|
352 => ("A01:2021-Broken-Access-Control", "T1189", "execution"),
|
||||||
|
434 => ("A04:2021-Insecure-Design", "T1505.003", "execution"),
|
||||||
|
1321 | 915 => ("A08:2021-Software-Data-Integrity", "T1059", "execution"),
|
||||||
|
400 | 770 | 1333 | 799 => ("A04:2021-Insecure-Design", "T1499", "impact"),
|
||||||
|
_ => ("A04:2021-Insecure-Design", "T1190", "initial-access"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exploitability(sev: &str, conf: f64) -> &'static str {
|
||||||
|
match (sev, conf) {
|
||||||
|
(_, c) if c >= 0.85 => "trivial",
|
||||||
|
("Critical" | "High", _) => "moderate",
|
||||||
|
_ => "hard",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill in any empty mapping fields on each finding (does not overwrite model-set values).
|
||||||
|
pub fn enrich(findings: &mut [Finding]) {
|
||||||
|
for f in findings.iter_mut() {
|
||||||
|
let (owasp, mitre, stage) = map_cwe(&f.cwe);
|
||||||
|
if f.owasp.is_empty() { f.owasp = owasp.into(); }
|
||||||
|
if f.mitre.is_empty() { f.mitre = mitre.into(); }
|
||||||
|
if f.stage.is_empty() { f.stage = stage.into(); }
|
||||||
|
if f.exploitability.is_empty() { f.exploitability = exploitability(&f.severity, f.confidence).into(); }
|
||||||
|
if f.business_impact.is_empty() { f.business_impact = f.impact.clone(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STAGE_ORDER: &[&str] = &[
|
||||||
|
"recon", "initial-access", "execution", "credential-access", "privesc", "lateral", "exfil", "impact",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn stage_rank(s: &str) -> usize {
|
||||||
|
STAGE_ORDER.iter().position(|x| *x == s).unwrap_or(STAGE_ORDER.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mermaid flowchart of the attack path: findings grouped by kill-chain stage,
|
||||||
|
/// with explicit chains_from edges plus implicit stage→stage progression.
|
||||||
|
pub fn mermaid(findings: &[Finding]) -> String {
|
||||||
|
if findings.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let mut out = String::from("flowchart LR\n");
|
||||||
|
// stage subgraphs
|
||||||
|
let mut by_stage: std::collections::BTreeMap<usize, Vec<&Finding>> = Default::default();
|
||||||
|
for f in findings {
|
||||||
|
by_stage.entry(stage_rank(&f.stage)).or_default().push(f);
|
||||||
|
}
|
||||||
|
let node_id = |f: &Finding| -> String {
|
||||||
|
format!("n{}", sanitize_id(&f.id))
|
||||||
|
};
|
||||||
|
for (rank, group) in &by_stage {
|
||||||
|
let stage = STAGE_ORDER.get(*rank).copied().unwrap_or("other");
|
||||||
|
out.push_str(&format!(" subgraph S{rank}[\"{}\"]\n", stage));
|
||||||
|
for f in group {
|
||||||
|
out.push_str(&format!(" {}[\"{}<br/>{} · {}\"]\n",
|
||||||
|
node_id(f), esc(&f.title), esc(&f.severity), esc(&f.owasp)));
|
||||||
|
}
|
||||||
|
out.push_str(" end\n");
|
||||||
|
}
|
||||||
|
// explicit chain edges
|
||||||
|
let ids: std::collections::HashMap<&str, &Finding> = findings.iter().map(|f| (f.id.as_str(), f)).collect();
|
||||||
|
let mut had_edge = false;
|
||||||
|
for f in findings {
|
||||||
|
for src in &f.chains_from {
|
||||||
|
if let Some(sf) = ids.get(src.as_str()) {
|
||||||
|
out.push_str(&format!(" {} --> {}\n", node_id(sf), node_id(f)));
|
||||||
|
had_edge = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// implicit progression between consecutive populated stages if no explicit edges
|
||||||
|
if !had_edge && by_stage.len() > 1 {
|
||||||
|
let ranks: Vec<usize> = by_stage.keys().copied().collect();
|
||||||
|
for w in ranks.windows(2) {
|
||||||
|
if let (Some(a), Some(b)) = (by_stage[&w[0]].first(), by_stage[&w[1]].first()) {
|
||||||
|
out.push_str(&format!(" {} -.-> {}\n", node_id(a), node_id(b)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compact ASCII kill-chain for the REPL: one line per stage with its findings.
|
||||||
|
pub fn ascii_killchain(findings: &[Finding]) -> String {
|
||||||
|
if findings.is_empty() {
|
||||||
|
return " (no findings to map)".into();
|
||||||
|
}
|
||||||
|
let mut by_stage: std::collections::BTreeMap<usize, Vec<&Finding>> = Default::default();
|
||||||
|
for f in findings {
|
||||||
|
by_stage.entry(stage_rank(&f.stage)).or_default().push(f);
|
||||||
|
}
|
||||||
|
let mut out = String::new();
|
||||||
|
for (rank, group) in &by_stage {
|
||||||
|
let stage = STAGE_ORDER.get(*rank).copied().unwrap_or("other");
|
||||||
|
out.push_str(&format!(" ▸ {:<16} ", stage));
|
||||||
|
let items: Vec<String> = group.iter()
|
||||||
|
.map(|f| format!("[{}] {} ({})", f.severity, f.title, f.mitre))
|
||||||
|
.collect();
|
||||||
|
out.push_str(&items.join("\n "));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_id(s: &str) -> String {
|
||||||
|
s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).take(24).collect()
|
||||||
|
}
|
||||||
|
fn esc(s: &str) -> String {
|
||||||
|
s.replace('"', "'").replace('\n', " ").chars().take(60).collect()
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
//! **N-model voting** before scoring and reporting.
|
//! **N-model voting** before scoring and reporting.
|
||||||
|
|
||||||
pub mod agents;
|
pub mod agents;
|
||||||
|
pub mod attack_graph;
|
||||||
pub mod creds;
|
pub mod creds;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn providers() -> Vec<Provider> {
|
|||||||
Provider { key: "anthropic", label: "Anthropic Claude", base_url: "https://api.anthropic.com/v1", env_key: "ANTHROPIC_API_KEY", kind: "cli",
|
Provider { key: "anthropic", label: "Anthropic Claude", base_url: "https://api.anthropic.com/v1", env_key: "ANTHROPIC_API_KEY", kind: "cli",
|
||||||
models: vec!["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"] },
|
models: vec!["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"] },
|
||||||
Provider { key: "openai", label: "OpenAI (ChatGPT)", base_url: "https://api.openai.com/v1", env_key: "OPENAI_API_KEY", kind: "cli",
|
Provider { key: "openai", label: "OpenAI (ChatGPT)", base_url: "https://api.openai.com/v1", env_key: "OPENAI_API_KEY", kind: "cli",
|
||||||
models: vec!["gpt-5.1", "gpt-5.1-codex", "o4"] },
|
models: vec!["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2", "gpt-5.1", "gpt-5.1-codex", "o4"] },
|
||||||
Provider { key: "xai", label: "xAI Grok", base_url: "https://api.x.ai/v1", env_key: "XAI_API_KEY", kind: "cli",
|
Provider { key: "xai", label: "xAI Grok", base_url: "https://api.x.ai/v1", env_key: "XAI_API_KEY", kind: "cli",
|
||||||
models: vec!["grok-4", "grok-4-fast"] },
|
models: vec!["grok-4", "grok-4-fast"] },
|
||||||
Provider { key: "gemini", label: "Google Gemini", base_url: "https://generativelanguage.googleapis.com/v1beta/openai", env_key: "GEMINI_API_KEY", kind: "cli",
|
Provider { key: "gemini", label: "Google Gemini", base_url: "https://generativelanguage.googleapis.com/v1beta/openai", env_key: "GEMINI_API_KEY", kind: "cli",
|
||||||
|
|||||||
@@ -596,9 +596,11 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
|
|||||||
validated.into_iter().filter(|f| f.validated).collect()
|
validated.into_iter().filter(|f| f.validated).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, findings: Vec<Finding>,
|
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec<Finding>,
|
||||||
selected: Vec<Agent>, rl: &mut RlState, tx: Sender<String>) -> RunOutput {
|
selected: Vec<Agent>, rl: &mut RlState, tx: Sender<String>) -> RunOutput {
|
||||||
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
|
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
|
||||||
|
// Map findings to OWASP / MITRE / kill-chain stage for the attack graph.
|
||||||
|
crate::attack_graph::enrich(&mut findings);
|
||||||
|
|
||||||
// RL update: reward agents that produced validated findings; gently decay idle.
|
// RL update: reward agents that produced validated findings; gently decay idle.
|
||||||
let hit: std::collections::HashMap<&str, f64> = findings.iter().fold(Default::default(), |mut m, f| {
|
let hit: std::collections::HashMap<&str, f64> = findings.iter().fold(Default::default(), |mut m, f| {
|
||||||
@@ -725,6 +727,7 @@ fn extract_findings(text: &str, agent: &str) -> Vec<Finding> {
|
|||||||
confidence: conf(o.get("confidence")),
|
confidence: conf(o.get("confidence")),
|
||||||
validated: false,
|
validated: false,
|
||||||
votes: String::new(),
|
votes: String::new(),
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -69,8 +69,26 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
|||||||
rows
|
rows
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Attack graph (Mermaid) + kill-chain table.
|
||||||
|
let graph = crate::attack_graph::mermaid(&sorted);
|
||||||
|
let graph_block = if graph.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
let rows: String = sorted.iter().map(|f| format!(
|
||||||
|
"<tr><td>{}</td><td><span class=sev style=background:{}>{}</span></td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>",
|
||||||
|
esc(&f.stage), sev_color(&f.severity), esc(&f.severity), esc(&f.title),
|
||||||
|
esc(&f.owasp), esc(&f.mitre), esc(&f.exploitability))).collect();
|
||||||
|
format!(
|
||||||
|
"<h2>Attack Path & Kill Chain</h2>\
|
||||||
|
<div class=mermaid>{graph}</div>\
|
||||||
|
<table class=kc><tr><th>Stage</th><th>Sev</th><th>Finding</th><th>OWASP</th><th>MITRE</th><th>Exploitability</th></tr>{rows}</table>\
|
||||||
|
<script type=module>import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';mermaid.initialize({{startOnLoad:true,theme:'dark'}});</script>"
|
||||||
|
)
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"<!DOCTYPE html><html><head><meta charset=utf-8><title>NeuroSploit Report — {t}</title><style>\
|
"<!DOCTYPE html><html><head><meta charset=utf-8><title>NeuroSploit Report — {t}</title><style>\
|
||||||
|
table.kc{{border-collapse:collapse;width:100%;margin:14px 0;font-size:13px}}table.kc th,table.kc td{{border:1px solid #e3e3e3;padding:6px 9px;text-align:left}}\
|
||||||
|
.mermaid{{background:#0f1117;border-radius:10px;padding:16px;margin:14px 0;overflow:auto}}\
|
||||||
body{{font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;max-width:860px;margin:40px auto;padding:0 24px}}\
|
body{{font:14px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;max-width:860px;margin:40px auto;padding:0 24px}}\
|
||||||
h1{{margin:0}}.meta{{color:#666;margin:4px 0 18px}}.chip{{color:#fff;border-radius:999px;padding:4px 12px;margin-right:8px;font-size:13px;font-weight:600}}\
|
h1{{margin:0}}.meta{{color:#666;margin:4px 0 18px}}.chip{{color:#fff;border-radius:999px;padding:4px 12px;margin-right:8px;font-size:13px;font-weight:600}}\
|
||||||
.finding{{border:1px solid #e3e3e3;border-radius:12px;padding:16px 20px;margin:16px 0}}.finding h3{{margin:0 0 8px;font-size:16px}}\
|
.finding{{border:1px solid #e3e3e3;border-radius:12px;padding:16px 20px;margin:16px 0}}.finding h3{{margin:0 0 8px;font-size:16px}}\
|
||||||
@@ -80,9 +98,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
|||||||
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
|
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
|
||||||
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
|
<h1><span class=b>NeuroSploit</span> Penetration Test Report</h1>\
|
||||||
<div class=meta>Target: <b>{t}</b> · v3.5.0 Rust harness · multi-model validated</div>\
|
<div class=meta>Target: <b>{t}</b> · v3.5.0 Rust harness · multi-model validated</div>\
|
||||||
<div>{chips}</div><h2>Findings ({n})</h2>{body}\
|
<div>{chips}</div>{graph_block}<h2>Findings ({n})</h2>{body}\
|
||||||
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.0 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.5.0 · by <b>Joas A Santos</b> & <b>Red Team Leaders</b></p></body></html>",
|
||||||
t = esc(target), chips = chips, n = sorted.len(), body = body,
|
t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,25 @@ pub struct Finding {
|
|||||||
/// Per-model vote summary, e.g. "3/4 confirmed".
|
/// Per-model vote summary, e.g. "3/4 confirmed".
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub votes: String,
|
pub votes: String,
|
||||||
|
// --- attack-graph / kill-chain mapping (best-effort, optional) ---
|
||||||
|
/// OWASP Top 10 category, e.g. "A03:2021-Injection".
|
||||||
|
#[serde(default)]
|
||||||
|
pub owasp: String,
|
||||||
|
/// MITRE ATT&CK technique id, e.g. "T1190".
|
||||||
|
#[serde(default)]
|
||||||
|
pub mitre: String,
|
||||||
|
/// Kill-chain stage: recon|initial-access|execution|privesc|lateral|exfil|impact.
|
||||||
|
#[serde(default)]
|
||||||
|
pub stage: String,
|
||||||
|
/// Exploitability: trivial|moderate|hard.
|
||||||
|
#[serde(default)]
|
||||||
|
pub exploitability: String,
|
||||||
|
/// Business impact, one line.
|
||||||
|
#[serde(default)]
|
||||||
|
pub business_impact: String,
|
||||||
|
/// IDs of findings this one chains from (attack-path edges).
|
||||||
|
#[serde(default)]
|
||||||
|
pub chains_from: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Finding {
|
impl Default for Finding {
|
||||||
@@ -47,6 +66,12 @@ impl Default for Finding {
|
|||||||
confidence: 0.0,
|
confidence: 0.0,
|
||||||
validated: false,
|
validated: false,
|
||||||
votes: String::new(),
|
votes: String::new(),
|
||||||
|
owasp: String::new(),
|
||||||
|
mitre: String::new(),
|
||||||
|
stage: String::new(),
|
||||||
|
exploitability: String::new(),
|
||||||
|
business_impact: String::new(),
|
||||||
|
chains_from: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user