mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-30 21:19:49 +02:00
feat(assurance): target gate (default-deny), evidence-graded CVSS, audit anchoring, P1–P5 bundle
The five immediate priorities from the assurance review — the harness-core ones, not the commercial/research items (Ed25519, enterprise mode, ablation, multi-target benchmark are deferred, noted as such). P1 — target authorization gate. scope.rs::validate_target checks protocol, host, port and URL prefix before ANY recon. A capability token that does not cover the CLI target now refuses the run with DENY_TARGET_OUTSIDE_GRANT, audits it, and exits non-zero — closing the auto-trust-the-target bypass. RunOutput carries a `denied` code the CLI turns into a non-zero exit. P6 — cvss.rs: the FIRST v3.1 base equation verbatim (roundup, scope coefficients), validated against first.org reference vectors (9.8, 6.1, 10.0, 7.8, 7.5, 5.3, 3.1). grade() drops any impact metric that raises severity without a receipt to a *demonstrated* vector, keeping the *potential* one for context — SQLi with no extraction scores 0 demonstrated / 9.8 potential, never a manufactured critical. P4 — audit.rs anchoring: signed checkpoints of the chain head, local and (with NEUROSPLOIT_ANCHOR_DIR) external append-only. verify_anchored() catches truncation (chain shorter than an anchor) and silent rebuilds (head hash no longer matches), and forged anchors via signature. `neurosploit audit --anchor`. P5 — assurance.rs bundle: one assurance.json per run — every artifact with its SHA-256, which of P1–P5 it evidenced (present/partial/absent, never flattered), a bundle hash and a signature. `neurosploit assurance <run> [--verify]`. Also +8 deterministic validators earlier this session (19→27). Deferred and documented: Ed25519 tokens (#3), enterprise mode (#25), benchmark/ablation (#20/#21), model pinning + reproducibility (#22/#23), README claims taxonomy (#24), per-agent seccomp (#14). 347 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8894649ccb
commit
b4903575c0
8 files changed
+1150
-5
No files matched your search
@@ -182,6 +182,16 @@ enum Cmd {
|
||||
/// Run id (`ns-…`) or a path to the run directory.
|
||||
run: String,
|
||||
},
|
||||
/// Verify a finished run's audit trail — the hash chain and, with --anchor,
|
||||
/// the signed anchors that catch truncation and silent rebuilds.
|
||||
Audit {
|
||||
/// Run id (`ns-…`) or path to the run directory.
|
||||
run: String,
|
||||
/// Also verify anchors (P4): truncation, rebuild and, if a key is set,
|
||||
/// anchor signatures.
|
||||
#[arg(long = "anchor")]
|
||||
anchor: bool,
|
||||
},
|
||||
/// Compliance mapping: re-frame a finished run's findings against PCI-DSS,
|
||||
/// HIPAA or SOC 2 controls.
|
||||
Compliance {
|
||||
@@ -206,6 +216,15 @@ enum Cmd {
|
||||
#[arg(long = "apply")]
|
||||
apply: bool,
|
||||
},
|
||||
/// Assemble/print/verify a run's assurance bundle (P1–P5 in one manifest).
|
||||
Assurance {
|
||||
/// Run id (`ns-…`) or path to the run directory.
|
||||
run: String,
|
||||
/// Verify the bundle against the run dir (hashes + signature) instead
|
||||
/// of assembling a fresh one.
|
||||
#[arg(long = "verify")]
|
||||
verify: bool,
|
||||
},
|
||||
/// Manage the Kali sandbox container (up · exec · down).
|
||||
Sandbox {
|
||||
#[command(subcommand)]
|
||||
@@ -574,6 +593,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
Err(e) => anyhow::bail!("rebuild failed: {e}"),
|
||||
}
|
||||
}
|
||||
Cmd::Audit { run, anchor } => handle_audit(&base, &run, anchor)?,
|
||||
Cmd::Assurance { run, verify } => handle_assurance(&base, &run, verify)?,
|
||||
Cmd::Compliance { run, framework, include_leads } => handle_compliance(&base, &run, &framework, include_leads)?,
|
||||
Cmd::Poc { run, repeats, apply } => handle_poc(&base, &run, repeats, apply).await?,
|
||||
Cmd::Sandbox { cmd } => handle_sandbox(cmd).await?,
|
||||
@@ -619,6 +640,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
apply_creds(&mut cfg, creds.as_deref()).await;
|
||||
let out = run_engagement(&base, cfg, mcp, false).await?;
|
||||
print_findings(&out);
|
||||
if let Some(code) = &out.denied { anyhow::bail!("{code}"); }
|
||||
let ig = harness::integrations::Integrations::load(&repl::proj_dir());
|
||||
post_integrations(&ig, &url, &out, jira, false, None).await;
|
||||
}
|
||||
@@ -1100,6 +1122,11 @@ async fn run_mode(base: &Path, cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow:
|
||||
}
|
||||
|
||||
pub(crate) fn print_findings(out: &RunOutput) {
|
||||
if let Some(code) = &out.denied {
|
||||
eprintln!("\n\x1b[1;31m⛔ RUN REFUSED\x1b[0m — {code}");
|
||||
eprintln!(" The target was not authorized. Nothing was tested. See audit.jsonl.");
|
||||
return;
|
||||
}
|
||||
println!("\n=== {} validated finding(s) ===", out.findings.len());
|
||||
if !out.findings.is_empty() {
|
||||
let mut by = std::collections::BTreeMap::new();
|
||||
@@ -1347,6 +1374,54 @@ fn load_findings(dir: &std::path::Path) -> anyhow::Result<Vec<harness::types::Fi
|
||||
Ok(serde_json::from_str(&text)?)
|
||||
}
|
||||
|
||||
fn handle_assurance(base: &std::path::Path, run: &str, verify: bool) -> anyhow::Result<()> {
|
||||
let dir = resolve_run(base, run)?;
|
||||
let key = std::env::var("NEUROSPLOIT_PROVENANCE_KEY").ok().filter(|k| !k.trim().is_empty()).map(|k| k.into_bytes());
|
||||
if verify {
|
||||
let text = std::fs::read_to_string(dir.join("assurance.json"))
|
||||
.map_err(|e| anyhow::anyhow!("no assurance.json in {}: {e}", dir.display()))?;
|
||||
let bundle: harness::assurance::Bundle = serde_json::from_str(&text)?;
|
||||
match bundle.verify(&dir, key.as_deref()) {
|
||||
Ok(()) => println!(" \x1b[1;32m✓ assurance bundle verified\x1b[0m — {} artifact(s){}", bundle.artifacts.iter().filter(|a| a.present).count(), if key.is_some() { ", signature valid" } else { " (signature NOT checked)" }),
|
||||
Err(e) => { println!(" \x1b[1;31m✗ {e}\x1b[0m"); anyhow::bail!("assurance verification failed"); }
|
||||
}
|
||||
} else {
|
||||
let bundle = harness::assurance::Bundle::build(&dir);
|
||||
let bundle = match &key { Some(k) => bundle.sign(k), None => bundle };
|
||||
let out = dir.join("assurance.json");
|
||||
std::fs::write(&out, serde_json::to_string_pretty(&bundle)?)?;
|
||||
print!("\n{}", bundle.summary());
|
||||
println!(" \x1b[2mbundle hash {} · saved → {}\x1b[0m", &bundle.bundle_hash[..16.min(bundle.bundle_hash.len())], out.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_audit(base: &std::path::Path, run: &str, anchor: bool) -> anyhow::Result<()> {
|
||||
let dir = resolve_run(base, run)?;
|
||||
let log = harness::audit::AuditLog::open(dir.join("audit.jsonl"));
|
||||
if anchor {
|
||||
// A provenance key verifies anchor signatures too; without it we still
|
||||
// catch truncation and rebuilds by hash, and say the sigs are unchecked.
|
||||
let key = std::env::var("NEUROSPLOIT_PROVENANCE_KEY").ok().filter(|k| !k.trim().is_empty()).map(|k| k.into_bytes());
|
||||
match log.verify_anchored(key.as_deref()) {
|
||||
Ok(rep) => {
|
||||
println!(" \x1b[1;32m✓ audit chain intact\x1b[0m — {} record(s)", rep.records);
|
||||
println!(" \x1b[1;32m✓ {} anchor(s) consistent\x1b[0m{}", rep.anchors, if rep.signed { " (signatures verified)" } else { " (signatures NOT checked — set NEUROSPLOIT_PROVENANCE_KEY)" });
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" \x1b[1;31m✗ {e}\x1b[0m");
|
||||
anyhow::bail!("audit verification failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match log.verify() {
|
||||
Ok(n) => println!(" \x1b[1;32m✓ audit chain intact\x1b[0m — {n} record(s). (Add --anchor to check truncation/rebuild.)"),
|
||||
Err(e) => { println!(" \x1b[1;31m✗ {e}\x1b[0m"); anyhow::bail!("audit verification failed"); }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_compliance(base: &std::path::Path, run: &str, frameworks: &[String], include_leads: bool) -> anyhow::Result<()> {
|
||||
use harness::compliance::{map_findings, Framework};
|
||||
let dir = resolve_run(base, run)?;
|
||||
|
||||
Reference in new issue
Block a user