v3.6.4: fix #33 — mode-aware grounding so white-box SAST findings aren't demoted

The grounding gate ran in empirical mode for every engagement, demoting
white-box (and skills/n8n audit) findings that had passed the n-model vote
because a file:line code citation isn't raw tool output. Grounding is now
mode-aware:
- Symbolic (white-box SAST / skills): a file:line reference into the reviewed
  source, or a quote of code present in it, is the receipt — no live target.
- Empirical (black-box / host / AI): evidence must resemble tool output (as before).
- Either (grey-box): a source citation OR a tool receipt grounds a finding.
The symbolic check runs against the reviewed source corpus (not the transcript)
and falls back to a structural file:line + quote check when the corpus is
unavailable. Adds unit tests incl. a regression test for #33.
This commit is contained in:
CyberSecurityUP
2026-07-19 17:48:19 -03:00
parent 53c07b9a9c
commit a61e75b601
15 changed files with 260 additions and 91 deletions
+2 -2
View File
@@ -871,7 +871,7 @@ dependencies = [
[[package]]
name = "neurosploit"
version = "3.6.3"
version = "3.6.4"
dependencies = [
"anyhow",
"clap",
@@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
version = "3.6.3"
version = "3.6.4"
dependencies = [
"anyhow",
"futures",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
version = "3.6.3"
version = "3.6.4"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
+4 -4
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.3 — interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
//! NeuroSploit v3.6.4 — 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.3 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.3 — a Rust multi-model harness that drives a pool of LLMs \
about = "NeuroSploit v3.6.4 — multi-model autonomous pentest harness",
long_about = "NeuroSploit v3.6.4 — 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\
@@ -721,7 +721,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.3 · by Joas A Santos & Red Team Leaders");
println!(" ┌─ NeuroSploit v3.6.4 · by Joas A Santos & Red Team Leaders");
println!(" │ run id : {run_id}");
println!(" │ target : {}", cfg.target);
println!(" │ models : {}", cfg.models.join(", "));
+2 -2
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.3 — interactive session (Claude-Code / Codex / Cursor-CLI style).
//! NeuroSploit v3.6.4 — 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
@@ -357,7 +357,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" ███╗ ██╗███████╗██╗ ██╗██████╗ ██████╗");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.3");
println!(" ████╗ ██║██╔════╝██║ ██║██╔══██╗██╔═══██╗ NeuroSploit v3.6.4");
println!(" ██╔██╗ ██║█████╗ ██║ ██║██████╔╝██║ ██║ interactive harness");
println!(" ██║╚██╗██║██╔══╝ ██║ ██║██╔══██╗██║ ██║ by Joas A Santos");
println!(" ██║ ╚████║███████╗╚██████╔╝██║ ██║╚██████╔╝ & Red Team Leaders");
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.3 — TUI "Mission Control" mode.
//! NeuroSploit v3.6.4 — 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:
+1 -1
View File
@@ -1,4 +1,4 @@
//! POMDP belief-state world model (v3.6.3).
//! POMDP belief-state world model (v3.6.4).
//!
//! The target is only partially observable, so we don't track booleans — we
//! track a **belief**: a property graph whose nodes (host / service / vuln /
+169 -39
View File
@@ -1,20 +1,36 @@
//! Verification / grounding engine (v3.6.3).
//! Verification / grounding engine (v3.6.4).
//!
//! Hard rule: **no claim enters the world model without a tool receipt** — raw
//! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination
//! anchor that complements the POMDP belief gate:
//! Hard rule: **no claim enters the world model without a receipt** — evidence,
//! not the LLM's bare assertion. This is the anti-hallucination anchor that
//! complements the POMDP belief gate. What counts as a receipt depends on the
//! engagement, so grounding runs in one of three modes:
//!
//! - **Black-box**: grounding is empirical — the finding's evidence must look
//! like raw tool output (an HTTP response, an OOB callback, an error oracle),
//! not prose.
//! - **White-box**: grounding is symbolic — a file:line reference into the
//! reviewed source (reachability/taint), checked against the collected context.
//! - **Empirical** (black-box / host / AI-endpoint): the finding's evidence must
//! look like raw tool output (an HTTP response, an OOB callback, an error
//! oracle, a shell receipt) — not prose.
//! - **Symbolic** (white-box SAST / skills audit): the receipt is a `file:line`
//! (or `file:section`) reference into the reviewed source, or a quote of code
//! that actually appears in it. There is NO live target to hit, so requiring an
//! HTTP-style receipt here is wrong — a code citation IS the receipt.
//! - **Either** (grey-box): both worlds are present (source review + a running
//! app), so a finding is grounded if it has a symbolic OR an empirical receipt.
//!
//! Ungrounded claims are flagged (`receipt_missing`) so the reward layer can
//! penalize them (the "claim without receipt" term).
use crate::types::Finding;
/// How a finding must be grounded, per engagement type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroundMode {
/// Black-box / host / AI endpoint: evidence must resemble raw tool output.
Empirical,
/// White-box SAST / skills audit: evidence must reference the reviewed source.
Symbolic,
/// Grey-box: accept either a source citation or an empirical receipt.
Either,
}
/// Verdict of grounding a single finding.
pub struct Grounded {
pub ok: bool,
@@ -35,47 +51,91 @@ fn looks_empirical(evidence: &str) -> bool {
}
/// White-box: evidence should reference a source location present in `context`.
/// `context` is the reviewed SOURCE (not the model transcript). When the source
/// context is unavailable, fall back to structural checks so a well-formed
/// `file:line` + code quote still grounds (a SAST finding must never be silently
/// dropped just because the caller couldn't supply the corpus).
fn looks_symbolic(f: &Finding, context: &str) -> bool {
// endpoint like file.ext:line, and the file appears in the reviewed source.
let loc = &f.endpoint;
if let Some((file, _)) = loc.rsplit_once(':') {
let base = file.rsplit('/').next().unwrap_or(file);
if !base.is_empty() && context.contains(base) {
return true;
let loc = f.endpoint.trim();
// A file:line / file:section reference is the canonical symbolic receipt.
let has_file_ref = loc.rsplit_once(':')
.map(|(file, tail)| {
let base = file.rsplit(['/', '\\']).next().unwrap_or(file);
// looks like a path/file (has an extension or a separator) and a
// line/section follows — i.e. not a "host:port" style endpoint.
!base.is_empty()
&& (base.contains('.') || file.contains('/'))
&& !tail.trim().is_empty()
})
.unwrap_or(false);
if !context.is_empty() {
// Strongest: the referenced file actually appears in the reviewed source.
if let Some((file, _)) = loc.rsplit_once(':') {
let base = file.rsplit(['/', '\\']).next().unwrap_or(file);
if !base.is_empty() && context.contains(base) {
return true;
}
}
}
// or the evidence quotes code that is actually in the context
!f.evidence.trim().is_empty()
&& f.evidence.split_whitespace().take(6).collect::<Vec<_>>().join(" ")
// Or the evidence quotes a distinctive code token present in the source.
let quote_matches = f.evidence
.split_whitespace()
.filter(|t| t.len() > 4 && context.contains(*t))
.count()
>= 2
}
/// Ground a finding. `context` is the reviewed source for white-box (empty for
/// black-box). Returns whether it has a valid receipt and of what kind.
pub fn ground(f: &Finding, context: &str, whitebox: bool) -> Grounded {
if whitebox && !context.is_empty() {
if looks_symbolic(f, context) {
return Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() };
.count();
if quote_matches >= 2 {
return true;
}
return Grounded { ok: false, kind: "missing", reason: "no source reference into reviewed code".into() };
// Source is present but neither the file nor a quote matched → still
// accept a well-formed file:line ref with quoted evidence, since the
// bounded corpus may simply not include the referenced file.
return has_file_ref && f.evidence.trim().len() >= 12;
}
if looks_empirical(&f.evidence) {
Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() }
} else {
Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() }
// No source corpus available: ground on a well-formed file:line reference
// backed by non-trivial quoted evidence.
has_file_ref && f.evidence.trim().len() >= 12
}
/// Ground a finding under `mode`. `context` is the reviewed SOURCE for symbolic/
/// either modes (empty for pure empirical). Returns whether it has a valid
/// receipt and of what kind.
pub fn ground(f: &Finding, context: &str, mode: GroundMode) -> Grounded {
let symbolic = || looks_symbolic(f, context);
let empirical = || looks_empirical(&f.evidence);
match mode {
GroundMode::Symbolic => {
if symbolic() {
Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() }
} else {
Grounded { ok: false, kind: "missing", reason: "no source reference (file:line) into reviewed code".into() }
}
}
GroundMode::Either => {
if symbolic() {
Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() }
} else if empirical() {
Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() }
} else {
Grounded { ok: false, kind: "missing", reason: "no source reference nor tool receipt".into() }
}
}
GroundMode::Empirical => {
if empirical() {
Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() }
} else {
Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() }
}
}
}
}
/// Apply the grounding gate to a finding set. Ungrounded findings are flagged
/// (receipt recorded in `votes`) and demoted to unvalidated so they never get
/// reported as confirmed. Returns (kept, demoted_count).
pub fn gate(mut findings: Vec<Finding>, context: &str, whitebox: bool) -> (Vec<Finding>, usize) {
/// Apply the grounding gate to a finding set under `mode`. Ungrounded findings
/// are flagged (receipt recorded in `votes`) and demoted to unvalidated so they
/// never get reported as confirmed. Returns (kept, demoted_count).
pub fn gate(mut findings: Vec<Finding>, context: &str, mode: GroundMode) -> (Vec<Finding>, usize) {
let mut demoted = 0;
for f in findings.iter_mut() {
let g = ground(f, context, whitebox);
let g = ground(f, context, mode);
if !g.ok {
f.validated = false;
f.votes = format!("{} · receipt_missing", f.votes);
@@ -85,3 +145,73 @@ pub fn gate(mut findings: Vec<Finding>, context: &str, whitebox: bool) -> (Vec<F
findings.retain(|f| f.validated);
(findings, demoted)
}
#[cfg(test)]
mod tests {
use super::*;
fn sast_finding() -> Finding {
// A typical SAST finding: file:line endpoint + a code quote as evidence,
// and NO HTTP/tool-output markers (there is no live target to hit).
Finding {
title: "SQL injection via string-formatted query".into(),
severity: "High".into(),
cwe: "CWE-89".into(),
endpoint: "src/db/users.py:42".into(),
evidence: "query = \"SELECT * FROM users WHERE id = \" + request.args.get('id')".into(),
validated: true,
confidence: 0.8,
..Default::default()
}
}
#[test]
fn sast_finding_grounds_symbolically_against_source() {
let src = "def get(id):\n query = \"SELECT * FROM users WHERE id = \" + request.args.get('id')\n";
assert!(ground(&sast_finding(), src, GroundMode::Symbolic).ok,
"a file:line SAST finding whose code appears in the source must ground");
}
#[test]
fn sast_finding_grounds_even_without_source_corpus() {
// Regression: the whitebox gate used to run in EMPIRICAL mode (bug #33),
// demoting every SAST finding because code quotes lack HTTP-style markers.
// A well-formed file:line + quoted evidence must ground on its own.
assert!(ground(&sast_finding(), "", GroundMode::Symbolic).ok,
"SAST finding must not be demoted for lacking a tool receipt");
}
#[test]
fn symbolic_rejects_bare_prose() {
let f = Finding { endpoint: "the login flow".into(),
evidence: "The application seems insecure.".into(), validated: true, ..Default::default() };
assert!(!ground(&f, "", GroundMode::Symbolic).ok,
"prose with no source reference must NOT ground symbolically");
}
#[test]
fn empirical_still_requires_tool_output() {
// Black-box unchanged: a code quote is not an empirical receipt.
assert!(!ground(&sast_finding(), "", GroundMode::Empirical).ok);
let http = Finding {
endpoint: "https://t/login".into(),
evidence: "HTTP/1.1 200 OK\nset-cookie: sid=1; \nserver: nginx\n<script>alert(1)</script>".into(),
validated: true, ..Default::default() };
assert!(ground(&http, "", GroundMode::Empirical).ok);
}
#[test]
fn either_accepts_symbolic_or_empirical() {
assert!(ground(&sast_finding(), "", GroundMode::Either).ok, "grey-box accepts a source citation");
}
#[test]
fn gate_keeps_grounded_and_demotes_prose() {
let good = sast_finding();
let bad = Finding { title: "vibes".into(), endpoint: "somewhere".into(),
evidence: "looks bad".into(), validated: true, ..Default::default() };
let (kept, demoted) = gate(vec![good, bad], "", GroundMode::Symbolic);
assert_eq!(kept.len(), 1);
assert_eq!(demoted, 1);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
//! NeuroSploit v3.6.3 harness — a robust multi-model runtime for the
//! NeuroSploit v3.6.4 harness — a robust multi-model runtime for the
//! markdown-driven autonomous pentest engine.
//!
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
+26 -14
View File
@@ -356,7 +356,7 @@ pub async fn run(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<Str
findings.extend(chained);
findings = dedup_findings(findings);
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await
}
/// White-box engagement: analyse a repository's source for vulnerabilities.
@@ -418,7 +418,7 @@ pub async fn run_whitebox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: S
let _ = tx.send(format!("{} candidate finding(s) (deduped) — validating", candidates.len())).await;
let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, tx).await
finish(cfg, lib, "{}".into(), transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Symbolic, context, tx).await
}
/// Greybox engagement: review the source code AND exploit the running app in one
@@ -561,7 +561,7 @@ pub async fn run_greybox(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Se
findings.extend(chained);
findings = dedup_findings(findings);
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Either, context, tx).await
}
const CHAIN_SYS: &str = "You are a post-exploitation & attack-chaining specialist. You are given ONE confirmed foothold plus any loot already gathered. DECIDE the most promising directions to expand from THIS foothold and pursue them with real tools: post-exploitation (loot credentials/tokens/keys/config/source), credential reuse, privilege escalation (horizontal AND vertical), lateral movement to adjacent services/hosts, data exfiltration, and reaching NEW attack surface the foothold exposes (e.g. SSRF→cloud metadata creds→IAM, SQLi→DB dump→credential reuse→admin, arbitrary file read→secrets→RCE, IDOR→account takeover, auth bypass→internal APIs). PROVE each escalated step with a real tool receipt. Report ONLY NEW findings beyond the input, plus any new loot you discovered (creds, tokens, hosts, internal endpoints) so later stages can reuse it. Authorized engagement; never destructive/DoS.";
@@ -912,16 +912,28 @@ async fn refute_pass(findings: Vec<Finding>, pool: &ModelPool, vote_n: usize, tx
}
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 {
// --- Grounding gate: no claim without a tool receipt (anti-hallucination) ---
// White/grey carry source context; black-box is verified empirically.
let whitebox = cfg.repo.is_some() && cfg.target.starts_with('/');
selected: Vec<Agent>, rl: &mut RlState, gmode: crate::grounding::GroundMode, source_ctx: String,
tx: Sender<String>) -> RunOutput {
use crate::grounding::GroundMode;
// --- Grounding gate: no claim without a receipt (anti-hallucination) ---
// The receipt is empirical (tool output) for black-box, symbolic (file:line
// into the reviewed source) for white-box SAST / skills audits, or either for
// grey-box. Symbolic grounding is checked against the SOURCE corpus, not the
// model transcript, so a code citation is honoured as its own receipt.
let ground_ctx = if source_ctx.is_empty() { transcript.as_str() } else { source_ctx.as_str() };
let before = findings.len();
let (kept, demoted) = crate::grounding::gate(findings, &transcript, whitebox);
let (kept, demoted) = crate::grounding::gate(findings, ground_ctx, gmode);
findings = kept;
if demoted > 0 {
let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) (no tool receipt)")).await;
let receipt = match gmode {
GroundMode::Symbolic => "no source reference",
GroundMode::Either => "no source reference nor tool receipt",
GroundMode::Empirical => "no tool receipt",
};
let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) ({receipt})")).await;
}
// White-box/skills are symbolic → deterministic belief; grey-box carries source too.
let whitebox = matches!(gmode, GroundMode::Symbolic | GroundMode::Either);
// --- v3.5.2 report-hygiene & exploitation-depth pass ---
// Calibrate inflated/unproven High-Critical to Medium, flag exposures that
@@ -1277,7 +1289,7 @@ pub async fn run_host(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sende
findings.extend(chained);
findings = dedup_findings(findings);
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await
}
/// AI-red-team doctrine prepended to every AI/LLM/agent test prompt.
@@ -1397,7 +1409,7 @@ pub async fn run_ai(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
if cfg.offline {
let _ = tx.send("offline: no AI exploitation performed".into()).await;
return finish(cfg, lib, recon, String::new(), vec![], agents, &mut rl, tx).await;
return finish(cfg, lib, recon, String::new(), vec![], agents, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await;
}
let cap = if cfg.max_agents > 0 { cfg.max_agents.min(agents.len()) } else { agents.len() };
let selected: Vec<Agent> = agents.into_iter().take(cap).collect();
@@ -1444,7 +1456,7 @@ pub async fn run_ai(cfg: RunConfig, lib: &Library, pool: &ModelPool, tx: Sender<
findings.extend(chained);
findings = dedup_findings(findings);
let findings = refute_pass(findings, pool, cfg.vote_n, &tx).await;
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, tx).await
finish(cfg, lib, recon, transcript, findings, selected, &mut rl, crate::grounding::GroundMode::Empirical, String::new(), tx).await
}
/// White-box Skills/plugin audit: read the skill .md file or a folder of them and
@@ -1463,7 +1475,7 @@ pub async fn run_skills_audit(cfg: RunConfig, lib: &Library, pool: &ModelPool, t
let mut rl = cfg.rl_path.as_ref().map(|p| RlState::load(Path::new(p))).unwrap_or_default();
if cfg.offline || context.is_empty() {
let _ = tx.send("offline or empty skills input — nothing audited".into()).await;
return finish(cfg, lib, "{}".into(), String::new(), vec![], agents, &mut rl, tx).await;
return finish(cfg, lib, "{}".into(), String::new(), vec![], agents, &mut rl, crate::grounding::GroundMode::Symbolic, String::new(), tx).await;
}
let directives = operator_directives(&cfg);
let raw: Vec<(String, String, Vec<Finding>)> = stream::iter(agents.iter().cloned())
@@ -1494,5 +1506,5 @@ pub async fn run_skills_audit(cfg: RunConfig, lib: &Library, pool: &ModelPool, t
let transcript = transcript_of(&raw);
let candidates = dedup_findings(raw.iter().flat_map(|(_, _, f)| f.clone()).collect());
let findings = validate(candidates, pool, CODE_VOTE_SYS, cfg.vote_n, &tx).await;
finish(cfg, lib, "{}".into(), transcript, findings, agents, &mut rl, tx).await
finish(cfg, lib, "{}".into(), transcript, findings, agents, &mut rl, crate::grounding::GroundMode::Symbolic, context, tx).await
}
+1 -1
View File
@@ -1,4 +1,4 @@
//! POMDP decision layer (v3.6.3): value-of-information planning + the
//! POMDP decision layer (v3.6.4): value-of-information planning + the
//! anti-hallucination gate.
//!
//! The choice "scan more vs exploit now" is **not** a heuristic here — it falls
+1 -1
View File
@@ -1,4 +1,4 @@
//! Deterministic HTTP request/response analysis (v3.6.3).
//! Deterministic HTTP request/response analysis (v3.6.4).
//!
//! Before the LLM recon runs, the harness performs a **real** probe of the
//! target and captures observed facts — status, headers, security headers,
+3 -3
View File
@@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
.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.6.3 Rust harness · multi-model validated</div>\
<div class=meta>Target: <b>{t}</b> · v3.6.4 Rust harness · multi-model validated</div>\
<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.6.3 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
<p class=meta>Authorized testing only. Findings confirmed by multi-model adversarial voting.<br>NeuroSploit v3.6.4 · by <b>Joas A Santos</b> &amp; <b>Red Team Leaders</b></p></body></html>",
t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block,
)
}
@@ -135,7 +135,7 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io::
let mut data = String::new();
data.push_str(&format!(
"#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n",
tq(target), tq(&run_id), tq("NeuroSploit v3.6.3"), tq("multi-model")
tq(target), tq(&run_id), tq("NeuroSploit v3.6.4"), tq("multi-model")
));
data.push_str("#let findings = (\n");
for f in sorted_findings(findings) {