mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-30 13:09:36 +02:00
feat(container,coverage): Strix-1.6.2-inspired capabilities
- Container image scanning: new `container` mode + 4 skills (vuln, secret, misconfig, SBOM) driving trivy/grype/syft headless, read-only. Scans an OCI ref / tar / Dockerfile for vulnerable packages (CVE/fixed-in/KEV), exposed secrets in any layer, Dockerfile+runtime misconfig, and writes an SBOM in both SPDX and CycloneDX to the run's sbom/. Also exposed as an MCP tool (neurosploit_container). - Coverage report: every run writes coverage.md — which agents ran (tested surface), findings per agent, and the high-value classes NOT covered — so the reader sees the engagement's reach. Added to the assurance bundle. - Login-verification evidence: doctrine now requires capturing the login request/response + a Playwright screenshot and recording success/failure before authenticated testing. - HTTP traffic export: `neurosploit traffic <run>` turns the intercepted flows.jsonl into a traffic.http archive for external tools. Not ported: Asset Discovery (enterprise-only, skipped per request). 383 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1adc882f6d
commit
e49595b8bf
12 files changed
+318
-7
No files matched your search
@@ -197,6 +197,12 @@ enum Cmd {
|
||||
/// Run id (`ns-…`) or a path to the run directory.
|
||||
run: String,
|
||||
},
|
||||
/// Export a run's archived HTTP traffic (from flows.jsonl) as a .http file
|
||||
/// for external inspection tools.
|
||||
Traffic {
|
||||
/// Run id or path.
|
||||
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 {
|
||||
@@ -394,6 +400,27 @@ enum Cmd {
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
/// Container: scan an OCI image (repo:tag / tar / Dockerfile) for vulnerable
|
||||
/// packages, exposed secrets and misconfigurations, and emit an SBOM
|
||||
/// (SPDX + CycloneDX). Uses trivy / grype / syft headless.
|
||||
Container {
|
||||
/// Image reference, local tar, or Dockerfile path.
|
||||
image: String,
|
||||
#[arg(long = "model")]
|
||||
models: Vec<String>,
|
||||
#[arg(long, default_value_t = 0)]
|
||||
max_agents: usize,
|
||||
#[arg(long, default_value_t = 1)]
|
||||
vote_n: usize,
|
||||
#[arg(long)]
|
||||
offline: bool,
|
||||
#[arg(long)]
|
||||
subscription: bool,
|
||||
#[arg(long)]
|
||||
focus: Option<String>,
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
},
|
||||
Host {
|
||||
/// Target host or IP.
|
||||
target: String,
|
||||
@@ -780,6 +807,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
Cmd::Audit { run, anchor } => handle_audit(&base, &run, anchor)?,
|
||||
Cmd::Traffic { run } => handle_traffic(&base, &run)?,
|
||||
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?,
|
||||
@@ -900,6 +928,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
let out = run_mode(&base, cfg, false, Mode::Mobile).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Container { image, models, max_agents, vote_n, offline, subscription, focus, verbose } => {
|
||||
let mut cfg = RunConfig::new(&image);
|
||||
cfg.max_agents = max_agents;
|
||||
cfg.vote_n = vote_n;
|
||||
cfg.offline = offline;
|
||||
cfg.subscription = subscription;
|
||||
cfg.verbose = verbose;
|
||||
cfg.instructions = focus;
|
||||
if !models.is_empty() { cfg.models = models; }
|
||||
let out = run_mode(&base, cfg, false, Mode::Container).await?;
|
||||
print_findings(&out);
|
||||
}
|
||||
Cmd::Host { target, models, creds, focus, max_agents, vote_n, chain_depth, recon, offline, subscription, verbose } => {
|
||||
let mut cfg = RunConfig::new(&target);
|
||||
cfg.max_agents = max_agents;
|
||||
@@ -1104,7 +1144,7 @@ pub(crate) async fn apply_creds(cfg: &mut RunConfig, path: Option<&str>) {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Mode { Black, White, Grey, Host, Ai, Skills, Mobile }
|
||||
pub(crate) enum Mode { Black, White, Grey, Host, Ai, Skills, Mobile, Container }
|
||||
|
||||
pub(crate) async fn run_greybox_engagement(base: &Path, cfg: RunConfig, mcp: bool) -> anyhow::Result<RunOutput> {
|
||||
run_mode(base, cfg, mcp, Mode::Grey).await
|
||||
@@ -1204,7 +1244,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
|
||||
println!(" │ repo : {}", cfg.repo.clone().unwrap_or_default());
|
||||
}
|
||||
println!(" └─ mode : {}{}{}",
|
||||
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Ai => "ai/llm", Mode::Skills => "skills/n8n audit", Mode::Mobile => "mobile/binary", Mode::Black => "black-box" },
|
||||
match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Ai => "ai/llm", Mode::Skills => "skills/n8n audit", Mode::Mobile => "mobile/binary", Mode::Container => "container-scan", Mode::Black => "black-box" },
|
||||
if cfg.subscription { " · subscription" } else { " · api" },
|
||||
if mcp { " · mcp" } else { "" });
|
||||
|
||||
@@ -1244,6 +1284,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode:
|
||||
Mode::Ai => harness::pipeline::run_ai(cfg, &lib, &pool, tx).await,
|
||||
Mode::Skills => harness::pipeline::run_skills_audit(cfg, &lib, &pool, tx).await,
|
||||
Mode::Mobile => harness::run_mobile(cfg, &lib, &pool, tx).await,
|
||||
Mode::Container => harness::run_container(cfg, &lib, &pool, tx).await,
|
||||
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
|
||||
}
|
||||
});
|
||||
@@ -1595,6 +1636,27 @@ fn handle_assurance(base: &std::path::Path, run: &str, verify: bool) -> anyhow::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_traffic(base: &std::path::Path, run: &str) -> anyhow::Result<()> {
|
||||
let dir = resolve_run(base, run)?;
|
||||
let flows = std::fs::read_to_string(dir.join("flows.jsonl"))
|
||||
.map_err(|e| anyhow::anyhow!("no flows.jsonl in {} (was the run started with --intercept own?): {e}", dir.display()))?;
|
||||
let mut out = String::from("# NeuroSploit HTTP traffic archive\n# One exchange per block; bodies are not captured for tunnelled HTTPS.\n\n");
|
||||
let mut n = 0usize;
|
||||
for line in flows.lines().filter(|l| !l.trim().is_empty()) {
|
||||
let v: serde_json::Value = match serde_json::from_str(line) { Ok(x) => x, Err(_) => continue };
|
||||
let method = v.get("method").and_then(|x| x.as_str()).unwrap_or("GET");
|
||||
let url = v.get("url").and_then(|x| x.as_str()).unwrap_or("");
|
||||
let status = v.get("status").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
let ct = v.get("content_type").and_then(|x| x.as_str()).unwrap_or("");
|
||||
out.push_str(&format!("### {method} {url}\n=> HTTP {status} {ct}\n\n"));
|
||||
n += 1;
|
||||
}
|
||||
let dest = dir.join("traffic.http");
|
||||
std::fs::write(&dest, out)?;
|
||||
println!(" exported {n} exchange(s) -> {}", dest.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"));
|
||||
|
||||
@@ -100,7 +100,8 @@ fn tool_list() -> Value {
|
||||
{ "name": "neurosploit_report", "description": "Read a finished run's Markdown report.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_rebuild", "description": "Rebuild a run's report artifacts from its findings (no model calls).", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_internal", "description": "Internal-network / Active Directory attack-graph analysis: paths to crown jewels and the choke point to fix first.", "inputSchema": { "type": "object", "properties": { "graph": { "type": "string", "description": "Path to a graph JSON" }, "scaffold": { "type": "string", "description": "Domain to scaffold, e.g. corp.local" }, "from": { "type": "string", "description": "Foothold node id" } } } },
|
||||
{ "name": "neurosploit_compliance", "description": "Map a finished run's findings onto PCI-DSS, HIPAA or SOC 2 controls.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" }, "framework": { "type": "string", "enum": ["pci-dss","hipaa","soc2"] } }, "required": ["run"] } }
|
||||
{ "name": "neurosploit_compliance", "description": "Map a finished run's findings onto PCI-DSS, HIPAA or SOC 2 controls.", "inputSchema": { "type": "object", "properties": { "run": { "type": "string" }, "framework": { "type": "string", "enum": ["pci-dss","hipaa","soc2"] } }, "required": ["run"] } },
|
||||
{ "name": "neurosploit_container", "description": "Scan an OCI container image (repo:tag / tar / Dockerfile) for vulnerable packages, secrets, misconfig and emit an SBOM.", "inputSchema": { "type": "object", "properties": { "image": { "type": "string" }, "model": { "type": "string" }, "subscription": { "type": "boolean" } }, "required": ["image"] } }
|
||||
])
|
||||
}
|
||||
|
||||
@@ -143,6 +144,13 @@ fn handle_call(id: Option<Value>, req: &Value, exe: &std::path::Path) -> Value {
|
||||
if let Some(sc) = s("scaffold") { argv.push("--scaffold".into()); argv.push(sc); }
|
||||
if let Some(fr) = s("from") { argv.push("--from".into()); argv.push(fr); }
|
||||
}
|
||||
"neurosploit_container" => {
|
||||
let Some(image) = s("image") else { return tool_err(id, "image is required") };
|
||||
argv.push("container".into()); argv.push(image);
|
||||
if let Some(m) = s("model") { argv.push("--model".into()); argv.push(m); }
|
||||
if b("subscription") { argv.push("--subscription".into()); }
|
||||
argv.push("-v".into());
|
||||
}
|
||||
"neurosploit_compliance" => {
|
||||
let Some(run) = s("run") else { return tool_err(id, "run is required") };
|
||||
argv.push("compliance".into()); argv.push(run);
|
||||
|
||||
@@ -148,7 +148,7 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(512);
|
||||
let models = cfg.models.join(", ");
|
||||
let mode_s = match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Ai => "ai/llm", Mode::Skills => "skills/n8n", Mode::Mobile => "mobile/binary", Mode::Black => "black-box" };
|
||||
let mode_s = match mode { Mode::White => "white-box", Mode::Grey => "greybox", Mode::Host => "host/infra", Mode::Ai => "ai/llm", Mode::Skills => "skills/n8n", Mode::Mobile => "mobile/binary", Mode::Container => "container-scan", Mode::Black => "black-box" };
|
||||
let target_s = cfg.target.clone();
|
||||
|
||||
// ---- terminal setup FIRST: on a non-TTY this errors before we spawn any
|
||||
@@ -166,6 +166,7 @@ pub async fn run(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> anyh
|
||||
Mode::Ai => harness::pipeline::run_ai(cfg, &lib, &pool, tx).await,
|
||||
Mode::Skills => harness::pipeline::run_skills_audit(cfg, &lib, &pool, tx).await,
|
||||
Mode::Mobile => harness::run_mobile(cfg, &lib, &pool, tx).await,
|
||||
Mode::Container => harness::run_container(cfg, &lib, &pool, tx).await,
|
||||
Mode::Black => harness::run(cfg, &lib, &pool, tx).await,
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user