mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0018 (20260517T044708Z-e058)
This commit is contained in:
parent
5b4181e4dd
commit
f87ef7f118
2 changed files with 129 additions and 3 deletions
|
|
@ -555,8 +555,12 @@ pub fn handle(
|
|||
}
|
||||
|
||||
// ── Dynamic verification (feature-gated) ─────────────────────────────
|
||||
// The constructed `VerifyOptions` is held in an `Option` scoped past
|
||||
// the per-finding loop so the composite-chain re-verification pass
|
||||
// below can reuse the same preloaded summaries / callgraph without
|
||||
// a second SQLite round-trip.
|
||||
#[cfg(feature = "dynamic")]
|
||||
if config.scanner.verify {
|
||||
let verify_opts: Option<crate::dynamic::verify::VerifyOptions> = if config.scanner.verify {
|
||||
let mut opts = crate::dynamic::verify::VerifyOptions::from_config(config);
|
||||
// Phase 30 (Track C observability): surface the per-finding
|
||||
// [`crate::dynamic::trace::VerifyTrace`] on stderr when the
|
||||
|
|
@ -599,7 +603,10 @@ pub fn handle(
|
|||
ev.dynamic_verdict = Some(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(opts)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// ── Baseline write (§M6.5): persist current findings as stripped baseline
|
||||
if let Some(bw_path) = baseline_write {
|
||||
|
|
@ -645,12 +652,39 @@ pub fn handle(
|
|||
max_depth: config.chain.max_depth,
|
||||
min_score: config.chain.min_score,
|
||||
};
|
||||
let chains = crate::chain::find_chains_with_reach(
|
||||
// `mut` is unused when the `dynamic` feature is off: composite
|
||||
// chain re-verification is the only mutator and is cfg-gated below.
|
||||
#[allow(unused_mut)]
|
||||
let mut chains = crate::chain::find_chains_with_reach(
|
||||
&chain_edges,
|
||||
&surface_map,
|
||||
chain_search_cfg,
|
||||
chain_reach,
|
||||
);
|
||||
|
||||
// Track G.3: composite chain re-verification. Only the top-N chains
|
||||
// by score reach the live composite run (cost control via
|
||||
// `[chain] reverify_top_n` — default 5, `0` to skip). Gated on the
|
||||
// master dynamic-verification switch (`scanner.verify`) so users who
|
||||
// skip per-finding verification do not pay the per-chain build /
|
||||
// sandbox cost. Mutates `chains` in place: each top-N chain's
|
||||
// `dynamic_verdict` / `severity` / `reverify_reason` flow through to
|
||||
// every downstream consumer (`filter_constituents`,
|
||||
// `build_findings_json`, `build_sarif_with_chains`, console
|
||||
// renderer).
|
||||
#[cfg(feature = "dynamic")]
|
||||
if let Some(ref opts) = verify_opts {
|
||||
if config.chain.reverify_top_n > 0 && !chains.is_empty() {
|
||||
let _ = crate::chain::reverify::reverify_top_chains(
|
||||
&mut chains,
|
||||
&diags,
|
||||
&surface_map,
|
||||
opts,
|
||||
config.chain.reverify_top_n,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let diags_for_output = crate::output::filter_constituents(
|
||||
diags.clone(),
|
||||
&chains,
|
||||
|
|
|
|||
|
|
@ -155,3 +155,95 @@ fn every_chain_composer_scenario_emits_at_least_one_chain() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Locks the scan-pipeline wiring contract: when dynamic verification is
|
||||
/// enabled (default), the composite chain re-verifier runs after the
|
||||
/// chain-composition pass and stamps each top-N chain's
|
||||
/// `dynamic_verdict` so downstream consumers (`build_findings_json`,
|
||||
/// `build_sarif_with_chains`, console renderer) see a populated field.
|
||||
///
|
||||
/// The verdict's *status* depends on the host's Python toolchain: when
|
||||
/// `python3 -m venv` succeeds and the per-language chain-step harness
|
||||
/// runs, the verdict resolves to `Confirmed`; when the toolchain is
|
||||
/// missing it falls through to `Inconclusive(BackendInsufficient)`.
|
||||
/// This test asserts only the wiring contract — that the field is
|
||||
/// populated and the detail string reports coverage — so it stays green
|
||||
/// on any host with a working `nyx` binary.
|
||||
///
|
||||
/// Gated on `feature = "dynamic"` because the reverifier lives behind
|
||||
/// that flag.
|
||||
#[cfg(feature = "dynamic")]
|
||||
#[test]
|
||||
fn flask_eval_chain_reverify_populates_dynamic_verdict() {
|
||||
let root = fixture_root("python/flask_eval");
|
||||
let value = run_scan_json(&root);
|
||||
|
||||
let chains = value
|
||||
.get("chains")
|
||||
.and_then(Value::as_array)
|
||||
.expect("`chains` array missing from scan output");
|
||||
assert!(!chains.is_empty(), "expected at least one composed chain");
|
||||
|
||||
let top = &chains[0];
|
||||
let dv = top
|
||||
.get("dynamic_verdict")
|
||||
.expect("`dynamic_verdict` key missing from top chain");
|
||||
assert!(
|
||||
!dv.is_null(),
|
||||
"top chain `dynamic_verdict` was null; wiring did not fire. Chain:\n{}",
|
||||
serde_json::to_string_pretty(top).unwrap_or_default()
|
||||
);
|
||||
|
||||
let status = dv
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.expect("verdict missing `status`");
|
||||
assert!(
|
||||
matches!(status, "Confirmed" | "Inconclusive" | "Unsupported"),
|
||||
"unexpected verdict status: {status:?}"
|
||||
);
|
||||
|
||||
let detail = dv
|
||||
.get("detail")
|
||||
.and_then(Value::as_str)
|
||||
.expect("verdict missing `detail`");
|
||||
for segment in ["derived", "built", "ran"] {
|
||||
assert!(
|
||||
detail.contains(segment),
|
||||
"verdict detail missing `{segment}` coverage segment: {detail:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror of the above: with `--no-verify` the chain-reverify pass is
|
||||
/// skipped and `dynamic_verdict` stays `null`. Locks the cost-control
|
||||
/// contract: users who opt out of dynamic verification do not pay the
|
||||
/// per-chain build / sandbox cost.
|
||||
#[cfg(feature = "dynamic")]
|
||||
#[test]
|
||||
fn flask_eval_chain_dynamic_verdict_is_null_when_verify_disabled() {
|
||||
let root = fixture_root("python/flask_eval");
|
||||
let assert = Command::cargo_bin("nyx")
|
||||
.expect("nyx binary")
|
||||
.args(["scan", "--no-verify", "--format", "json"])
|
||||
.arg(&root)
|
||||
.assert()
|
||||
.success();
|
||||
let stdout = String::from_utf8(assert.get_output().stdout.clone())
|
||||
.expect("nyx scan stdout is valid UTF-8");
|
||||
let value: Value = serde_json::from_str(&stdout)
|
||||
.expect("nyx scan --format json produced invalid JSON");
|
||||
|
||||
let chains = value
|
||||
.get("chains")
|
||||
.and_then(Value::as_array)
|
||||
.expect("`chains` array missing");
|
||||
assert!(!chains.is_empty());
|
||||
|
||||
let top = &chains[0];
|
||||
let dv = top.get("dynamic_verdict");
|
||||
assert!(
|
||||
matches!(dv, None | Some(Value::Null)),
|
||||
"top chain `dynamic_verdict` should be absent or null under --no-verify; got {dv:?}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue