Files
NeuroSploit/neurosploit-rs/crates/harness/src/report.rs
T
CyberSecurityUP 435463979b v3.5.0: Claude-Code-style interactive harness (REPL) + instruction-steered testing
- New persistent interactive session (app/src/repl.rs), launched when run with no args:
  banner, model selection, API-key config (/key) or subscription (/sub), then a live
  session to set /target, /repo, /auth, and free-text /focus instructions (or just type
  them) that STEER which agents run and how.
- Slash-commands: /model /providers /key /sub /target /repo /auth /focus /mcp /votes
  /agents /show /run /quit  (+ bare text = focus).
- RunConfig gains `instructions` and `auth`:
  * instructions bias both LLM agent-selection and the heuristic (focus keywords →
    injection/access-control/etc. agents get a strong boost)
  * operator directives (focus + auth) injected into recon and exploit prompts so agents
    test as an authenticated user and prioritise the requested vuln classes
- bump 3.4.1 → 3.5.0 (CLI, harness, reports, credits)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:58:35 -03:00

149 lines
6.3 KiB
Rust

use crate::types::Finding;
use std::path::{Path, PathBuf};
/// The blank, structured Typst template (rendering logic). Data (`meta`,
/// `findings`) is prepended by `typst_report` to make a self-contained file.
const TYPST_TEMPLATE: &str = include_str!("../../../templates/report.typ");
fn sev_rank(s: &str) -> u8 {
match s {
"Critical" => 0,
"High" => 1,
"Medium" => 2,
"Low" => 3,
_ => 4,
}
}
fn sev_color(s: &str) -> &'static str {
match s {
"Critical" => "#c0392b",
"High" => "#e67e22",
"Medium" => "#f1c40f",
"Low" => "#3498db",
_ => "#7f8c8d",
}
}
fn esc(s: &str) -> String {
s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}
/// Render an HTML report for the validated findings.
pub fn html(target: &str, findings: &[Finding]) -> String {
let mut sorted = findings.to_vec();
sorted.sort_by_key(|f| sev_rank(&f.severity));
let mut counts: std::collections::BTreeMap<&str, usize> = Default::default();
for f in &sorted {
*counts.entry(f.severity.as_str()).or_default() += 1;
}
let chips: String = if counts.is_empty() {
"<span class=chip style=background:#27ae60>No validated findings</span>".into()
} else {
counts
.iter()
.map(|(s, n)| format!("<span class=chip style=background:{}>{}: {}</span>", sev_color(s), s, n))
.collect()
};
let rows: String = sorted
.iter()
.enumerate()
.map(|(i, f)| {
format!(
"<section class=finding><h3><span class=sev style=background:{}>{}</span> {}. {}</h3>\
<div class=m>{} · {} · CVSS {} · votes {} · conf {:.2}</div>\
<div class=m>Endpoint: {}</div>\
<h4>Payload</h4><pre>{}</pre><h4>Evidence</h4><pre>{}</pre>\
<h4>Impact</h4><p>{}</p><h4>Remediation</h4><p>{}</p></section>",
sev_color(&f.severity), esc(&f.severity), i + 1, esc(&f.title),
esc(&f.agent), esc(&f.cwe), esc(&f.cvss), esc(&f.votes), f.confidence,
esc(&f.endpoint), esc(&f.payload), esc(&f.evidence), esc(&f.impact), esc(&f.remediation),
)
})
.collect();
let body = if rows.is_empty() {
"<p><em>No validated findings were produced for this engagement.</em></p>".to_string()
} else {
rows
};
format!(
"<!DOCTYPE html><html><head><meta charset=utf-8><title>NeuroSploit Report — {t}</title><style>\
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}}\
.sev{{color:#fff;border-radius:6px;padding:2px 8px;font-size:12px;margin-right:8px}}.m{{color:#666;font-size:12px}}\
pre{{background:#0f1117;color:#dfe6f3;padding:11px;border-radius:8px;overflow:auto;font-size:12.5px}}\
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.5.0 Rust harness · multi-model validated</div>\
<div>{chips}</div><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> &amp; <b>Red Team Leaders</b></p></body></html>",
t = esc(target), chips = chips, n = sorted.len(), body = body,
)
}
// ===== Typst report =====
/// Is the `typst` binary available on PATH?
fn typst_available() -> bool {
std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).any(|d| d.join("typst").is_file()))
.unwrap_or(false)
}
fn sorted_findings(findings: &[Finding]) -> Vec<Finding> {
let mut v = findings.to_vec();
v.sort_by_key(|f| sev_rank(&f.severity));
v
}
/// Escape a string for embedding inside a Typst `"..."` literal (single line).
fn tq(s: &str) -> String {
let cleaned: String = s.replace('\\', "\\\\").replace('"', "\\\"").replace(['\n', '\r'], " ");
format!("\"{}\"", cleaned)
}
/// Generate a self-contained `report.typ` (data + bundled template) in `dir`
/// and compile it to `report.pdf` via the `typst` binary. Falls back to leaving
/// the `.typ` when `typst` is unavailable.
pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io::Result<PathBuf> {
std::fs::create_dir_all(dir)?;
let run_id = dir.file_name().and_then(|s| s.to_str()).unwrap_or("run").to_string();
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.5.0"), tq("multi-model")
));
data.push_str("#let findings = (\n");
for f in sorted_findings(findings) {
data.push_str(&format!(
" (severity: {}, title: {}, agent: {}, cwe: {}, cvss: {}, endpoint: {}, payload: {}, evidence: {}, impact: {}, remediation: {}, votes: {}, confidence: {}),\n",
tq(&f.severity), tq(&f.title), tq(&f.agent), tq(&f.cwe), tq(&f.cvss),
tq(&f.endpoint), tq(&f.payload), tq(&f.evidence), tq(&f.impact),
tq(&f.remediation), tq(&f.votes), f.confidence,
));
}
data.push_str(")\n\n");
let typ_path = dir.join("report.typ");
std::fs::write(&typ_path, format!("{data}{TYPST_TEMPLATE}"))?;
if typst_available() {
let pdf_path = dir.join("report.pdf");
match std::process::Command::new("typst")
.arg("compile").arg(&typ_path).arg(&pdf_path).output()
{
Ok(o) if o.status.success() && pdf_path.exists() => return Ok(pdf_path),
Ok(o) => eprintln!("typst compile failed: {}",
String::from_utf8_lossy(&o.stderr).lines().next().unwrap_or("").trim()),
Err(e) => eprintln!("typst not runnable: {e}"),
}
}
Ok(typ_path)
}