mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-06-30 07:15:30 +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) {
|
||||
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() {
|
||||
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.
|
||||
|
||||
pub mod agents;
|
||||
pub mod attack_graph;
|
||||
pub mod creds;
|
||||
pub mod models;
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
|
||||
@@ -596,9 +596,11 @@ async fn validate(candidates: Vec<Finding>, pool: &ModelPool, sys: &str, vote_n:
|
||||
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 {
|
||||
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.
|
||||
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")),
|
||||
validated: false,
|
||||
votes: String::new(),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -69,8 +69,26 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
||||
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!(
|
||||
"<!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}}\
|
||||
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}}\
|
||||
@@ -80,9 +98,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
|
||||
.b{{color:#8b5cf6;font-weight:800}}</style></head><body>\
|
||||
<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>{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>",
|
||||
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".
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@@ -47,6 +66,12 @@ impl Default for Finding {
|
||||
confidence: 0.0,
|
||||
validated: false,
|
||||
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