feat(harness): /only REPL command; redesign the HTML report (drop attack-chain, match Typst)

/only <agent,...> (app/src/repl.rs): the REPL had no way to pin an exact
agent set the way the CLI's --only flag does - Session gained a `pinned`
field, wired into RunConfig in both start_background() (the live
background-run path) and the blocking run() fallback. Needed so the web
console's exploitation jobs can drive a real interactive REPL session
(for live input while a run streams) without losing lead-pinning, which
only existed as a CLI flag until now. Also usable directly from a
terminal REPL session.

HTML report (crates/harness/src/report.rs, html()): rebuilt to match the
Typst PDF template's design (templates/report.typ) instead of its own
inconsistent styling - violet brand accent, an asset table, a 5-box
executive-summary grid (all severities, zero-count included, matching
Typst's grid exactly), a Vulnerability Summary table, and severity-
left-bordered finding cards with a compact field grid (Criticality /
Status / OWASP-CWE / Confidence / Location / Agent / Auth context) before
Description-Impact / Proof of Concept / Evidence / Remediation - same
field order and labels as the Typst template. Dropped the Mermaid
attack-path/kill-chain section entirely (the web console's live
Generative Attack Path Chaining graph covers that now, interactively).
Also tidied two pre-existing formatting quirks while in there: OWASP/CWE
left a dangling " · " when CWE was empty, and the confidence cell said
"<votes-string> votes" even when the votes field already contained a
compound descriptor like "1/1 · receipt_missing".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
This commit is contained in:
CyberSecurityUP
2026-08-23 16:04:34 -03:00
co-authored by Claude Sonnet 5
parent ecc2a9ae03
commit 07bed42467
2 changed files with 108 additions and 53 deletions
+20 -1
View File
@@ -143,7 +143,7 @@ struct LiveCheckpoint {
const COMMANDS: &[&str] = &[
"/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
"/repo", "/auth", "/creds", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline",
"/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
"/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/only", "/theme", "/clear", "/run", "/stop", "/continue", "/runs", "/results", "/report",
"/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/expand", "/integrations", "/quit",
];
@@ -264,6 +264,10 @@ struct Session {
color: bool,
/// Engagement scope from onboarding: web | infra | cloud | ai | skills.
scope: &'static str,
/// Explicit agent allowlist (`/only <agent>[,agent2,...]`) — when non-empty,
/// /run tests EXACTLY these agents and skips recon-based selection, same as
/// the CLI's `--only` flag. Empty = normal recon-driven auto-selection.
pinned: Vec<String>,
}
impl Default for Session {
@@ -292,6 +296,7 @@ impl Default for Session {
attachments: Vec::new(),
color: true,
scope: "web",
pinned: Vec::new(),
}
}
}
@@ -664,6 +669,17 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
s.max_agents = arg.parse().unwrap_or(s.max_agents); println!(" max agents: {}", s.max_agents);
}
}
"/only" => {
if arg.is_empty() {
if s.pinned.is_empty() { println!(" pinned agents: (none) — recon-driven auto-selection · set with /only <agent>[,agent2,...] · /agents list for names"); }
else { println!(" pinned agents ({}): {} — /run tests exactly these · /only clear to unpin", s.pinned.len(), s.pinned.join(", ")); }
} else if arg == "clear" {
s.pinned.clear(); println!(" pinned agents cleared — back to recon-driven auto-selection");
} else {
s.pinned = arg.split([',', ';']).map(str::trim).filter(|x| !x.is_empty()).map(String::from).collect();
println!(" pinned agents ({}): {} — /run tests exactly these, skipping recon-based selection", s.pinned.len(), s.pinned.join(", "));
}
}
"/clear" => { print!("\x1b[2J\x1b[H"); }
"/run" | "/go" => {
if active.as_ref().map(|a| !a.done.load(Ordering::Relaxed)).unwrap_or(false) {
@@ -1108,6 +1124,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
cfg.objective = s.objective.clone();
cfg.out_of_scope = s.out_of_scope.clone();
cfg.auth = s.auth.clone();
cfg.pinned = s.pinned.clone();
// Multiple /auth identities → prepend the access-control (IDOR/BOLA/BFLA) directive.
if let Some(rd) = roles_directive(&s.roles) {
let base = cfg.instructions.clone().unwrap_or_default();
@@ -1182,6 +1199,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
cfg.objective = s.objective.clone();
cfg.out_of_scope = s.out_of_scope.clone();
cfg.auth = s.auth.clone();
cfg.pinned = s.pinned.clone();
if matches!(mode_e, crate::Mode::Grey) { cfg.repo = s.repo.clone(); }
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
crate::subscription_preflight(&cfg).await; // warn early if the CLI isn't logged in
@@ -1728,6 +1746,7 @@ fn help() {
h("/sub on|off", "use local subscription login instead of an API key");
println!("\n \x1b[2mRUN & MONITOR\x1b[0m");
h("/only <agent,..>", "pin exact agent(s) for /run, skipping recon-based selection (clear to unpin)");
h("/run", "launch (runs in the BACKGROUND — keep typing)");
h("/status [n]", "live progress + findings while running (or a past run #)");
h("/logs [n]", "recent activity feed of the running test (recon/tools/findings)");