fix(wiring): activate cvss.rs + waf.rs (were shelf-ware); TypeSafe into CVSS + agent selection

Honesty audit found four modules written but not on the runtime path. Two mattered
and are now wired; two are noted.

cvss.rs — was NOT called; finding.cvss came from the old attack_graph ladder.
Now attack_graph::cvss_graded() bridges the class shape + demonstrated rung into
crate::cvss::grade (the FIRST-verbatim v3.1 equation), and enrich() sets
finding.cvss from the demonstrated vector, recording the potential ceiling in
the impact text. The class ladder remains only as a fallback for findings with
no evidence to grade.

waf.rs — the deterministic classifier was NOT run on any real exchange (only
WAF_OPS prompt text reached the agent). Now poc.rs classifies each re-run: a PoC
answered by a WAF/CDN is Unverifiable, not "gone" — closing a false-demotion
where an edge block looked like a fix.

TypeSafe (System One) extended per the build-with docs:
- CVSS via System One: when impact_demonstrated < 0.5, the finding's CVSS is
  re-graded with impact receipts stripped — the calibrated judgment, not just
  the rung, decides the demonstrated number.
- Agent selection: typesafe_prune_agents() asks one batched request (the
  fan-out pattern), a Noul per chosen agent, and drops only those it calibrates
  as clearly irrelevant (p < 0.25), never prunes to empty. Additive over the
  LLM selection; skipped without a key.

Still shelf-ware, flagged honestly (not wired): inbox.rs (mail.tm/SMS happens
via agent prompt instructions, the Rust client is unused) and pomdp.rs
(redundant — belief.rs is the one on the path).

374 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 5 committed 2026-09-19 18:06:50 -03:00
1 parent c3de51d508
commit 48c38d4798
3 files changed
+180 -3

No files matched your search

@@ -261,6 +261,67 @@ fn temporal(f: &Finding) -> (&'static str, &'static str) {
}
/// Derive a CVSS v3.1 base score + vector for a finding.
/// Evidence-graded CVSS via the FIRST-verbatim calculator in `crate::cvss`.
///
/// The class proposes the vector's shape (which of C/I/A it *can* affect, the
/// scope, the exploitability axes); the demonstrated rung decides which impact
/// metrics actually have a receipt. `crate::cvss::grade` then keeps two scores:
/// the demonstrated one (what the evidence proved, the reported number) and the
/// potential one (what the class could reach). This is what wires the real
/// CVSS 3.1 equation — the older `cvss_for` is kept only as a fallback for a
/// finding with no structured evidence to grade.
pub fn cvss_graded(f: &Finding) -> Option<crate::cvss::Graded> {
use crate::cvss::{Ac, Av, Imp, Pr, Scope, Ui, Vector};
let n: u32 = f.cwe.chars().skip_while(|c| !c.is_ascii_digit()).take_while(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap_or(0);
if n == 0 {
return None;
}
let imp = |s: &str| match s { "H" => Imp::High, "L" => Imp::Low, _ => Imp::None };
// Class → the impact shape it CAN have (the potential ceiling) + scope.
let (c, i, a, scope) = match n {
77 | 78 | 94 | 95 | 502 | 917 | 1336 => ("H", "H", "H", Scope::Changed),
89 | 943 | 564 => ("H", "H", "L", Scope::Unchanged),
22 | 23 | 35 | 98 | 73 => ("H", "N", "N", Scope::Unchanged),
918 => ("H", "L", "N", Scope::Changed),
639 | 862 | 863 | 284 | 285 | 306 | 566 | 425 => ("H", "H", "N", Scope::Unchanged),
287 | 288 | 289 | 290 | 347 | 345 | 384 => ("H", "H", "N", Scope::Unchanged),
79 | 80 | 83 | 87 => ("L", "L", "N", Scope::Changed),
352 => ("N", "H", "N", Scope::Unchanged),
611 | 776 | 827 => ("H", "N", "L", Scope::Changed),
319 | 522 | 798 | 312 | 256 | 257 | 321 => ("H", "N", "N", Scope::Unchanged),
200 | 209 | 538 | 540 | 548 | 532 | 530 => ("L", "N", "N", Scope::Unchanged),
307 | 799 | 770 | 400 => ("N", "N", "L", Scope::Unchanged),
601 => ("L", "L", "N", Scope::Changed),
1021 => ("N", "L", "N", Scope::Unchanged),
113 | 93 | 644 => ("L", "L", "N", Scope::Unchanged),
525 | 524 => ("L", "N", "N", Scope::Unchanged),
_ => ("L", "N", "N", Scope::Unchanged),
};
let authenticated = f.auth_context.eq_ignore_ascii_case("authenticated") || !f.account.is_empty();
let proposed = Vector {
// Web engagement defaults; the class overrides where it matters.
av: Av::Network,
ac: match n { 362 | 208 | 385 => Ac::High, _ => Ac::Low }, // race/timing = high AC
pr: if authenticated { Pr::Low } else { Pr::None },
ui: match n { 79 | 80 | 83 | 87 | 352 | 601 | 1021 => Ui::Required, _ => Ui::None },
scope,
c: imp(c),
i: imp(i),
a: imp(a),
};
// The demonstrated rung decides which impact metrics carry a receipt.
let rung = demonstrated_rung(f);
let has_c = matches!(rung, Rung::ReadData | Rung::ReadSensitive | Rung::Wrote | Rung::Executed | Rung::CrossedSystem);
let has_i = matches!(rung, Rung::Wrote | Rung::Executed | Rung::CrossedSystem);
let has_a = matches!(rung, Rung::Executed | Rung::CrossedSystem);
Some(crate::cvss::grade(proposed, move |m| match m {
"C" => has_c,
"I" => has_i,
"A" => has_a,
_ => true,
}))
}
pub fn cvss_for(f: &Finding) -> (f64, String) {
let n: u32 = f.cwe.chars().skip_while(|c| !c.is_ascii_digit()).take_while(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap_or(0);
let authenticated = f.auth_context.eq_ignore_ascii_case("authenticated") || !f.account.is_empty();
@@ -389,8 +450,22 @@ pub fn enrich(findings: &mut [Finding]) {
// A severity word without the industry-standard number makes the reader
// re-derive it by hand or take it on faith.
if f.cvss.is_empty() {
let (score, vector) = cvss_for(f);
if score > 0.0 { f.cvss = format!("{score:.1} ({vector})"); }
// Evidence-graded first (FIRST-verbatim, demonstrated vs potential);
// fall back to the class ladder only when there is no evidence to grade.
match cvss_graded(f) {
Some(g) if g.demonstrated_score > 0.0 => {
f.cvss = format!("{:.1} ({})", g.demonstrated_score, g.demonstrated.vector_string());
// Record the potential ceiling in the impact text when it is
// meaningfully higher, so the reader sees both numbers.
if g.potential_score - g.demonstrated_score > 0.5 && !f.impact.contains("potential CVSS") {
f.impact = format!("{} (potential CVSS {:.1} if fully exploited)", f.impact, g.potential_score).trim().to_string();
}
}
_ => {
let (score, vector) = cvss_for(f);
if score > 0.0 { f.cvss = format!("{score:.1} ({vector})"); }
}
}
}
}
}
@@ -733,4 +808,17 @@ mod ladder_tests {
}
assert_eq!(temporal_factor("H", "C"), 1.0);
}
#[test]
fn graded_cvss_wires_the_first_calculator_and_splits_demonstrated_from_potential() {
let reached = sqli(Some(Evidence { attack: Some(ex(200, "ok")), ..Default::default() }));
let g = cvss_graded(&reached).expect("graded");
assert!(g.demonstrated_score <= g.potential_score);
let proven = sqli(Some(Evidence { attack: Some(ex(200, "password=hunter2 bearer eyJ...")), ..Default::default() }));
let g2 = cvss_graded(&proven).expect("graded");
assert!(g2.demonstrated_score >= g.demonstrated_score, "reading data cannot lower the score");
assert!(g2.demonstrated.vector_string().contains("CVSS:3.1/"));
}
}