feat(report): rebuild reports on demand, from the CLI and the web

A PDF was only ever produced while a run was finishing. If `typst` was missing
at that moment — or the template improved afterwards — the operator had no way
to get one without re-running the whole engagement against the target.

report::rebuild() regenerates every artifact (md · json · html · pdf) from the
findings already on disk, exposed as `neurosploit rebuild <run-id|dir>` and as
POST /api/runs/:id/report with a "Generate report" button in the run view. The
endpoint shells out to the harness rather than reimplementing report generation
in JavaScript, so there is one implementation instead of two that drift, and it
says plainly when the PDF was skipped for want of `typst` instead of handing
back a link to a file that was never produced.

Also fixes write_all() to pass the run's pocs/ listing into the HTML report, so
a rebuilt report links the scripts each finding cites — the run-time path
already did this and the rebuild path silently did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 5 committed 2026-09-13 19:27:26 -03:00
1 parent 481a4eb1b9
commit 61ae3bc74d
6 files changed
+118 -2

No files matched your search

+19
View File
@@ -115,6 +115,12 @@ enum Cmd {
#[arg(short, long)]
verbose: bool,
},
/// Rebuild a finished run's report artifacts (md · json · html · pdf) from
/// its findings, without re-running the engagement.
Rebuild {
/// Run id (`ns-…`) or a path to the run directory.
run: String,
},
/// Issue or inspect a signed capability token (the engagement's authorization).
Capability {
#[command(subcommand)]
@@ -432,6 +438,19 @@ async fn main() -> anyhow::Result<()> {
}
}
}
Cmd::Rebuild { run } => {
// Accept either a path or a bare run id, resolved against the same
// runs root the engagement wrote to.
let dir = std::path::PathBuf::from(&run);
let dir = if dir.is_dir() { dir } else { base.join("runs").join(&run) };
if !dir.is_dir() {
anyhow::bail!("no such run directory: {}", dir.display());
}
match harness::report::rebuild(&dir) {
Ok(p) => println!(" report rebuilt → {}", p.display()),
Err(e) => anyhow::bail!("rebuild failed: {e}"),
}
}
Cmd::Capability { cmd } => handle_capability(cmd)?,
Cmd::Run { url, models, max_agents, vote_n, chain_depth, recon, offline, subscription, mcp, creds, focus, objective, out_of_scope, in_scope, environment, policy, jira, only, verbose } => {
let url = if url.starts_with("http") { url } else { format!("https://{url}") };