mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
[pitboss/grind] deferred session-0001 (20260521T201327Z-3848)
This commit is contained in:
parent
3a35cd6c8f
commit
159a779f31
19 changed files with 305 additions and 69 deletions
|
|
@ -471,8 +471,8 @@ pub enum Commands {
|
|||
///
|
||||
/// Dynamic verification is on by default. This flag is a no-op when
|
||||
/// verification is already enabled via config. Use `--no-verify` to
|
||||
/// disable it for a single run. Requires the binary to be built with
|
||||
/// `--features dynamic`; without that feature this flag is silently ignored.
|
||||
/// disable it for a single run. Default builds include dynamic support;
|
||||
/// custom `--no-default-features` builds need `--features dynamic`.
|
||||
#[cfg_attr(not(feature = "dynamic"), arg(hide = true))]
|
||||
#[arg(long, help_heading = "Dynamic", conflicts_with = "no_verify")]
|
||||
verify: bool,
|
||||
|
|
|
|||
|
|
@ -352,13 +352,19 @@ pub fn handle_command(
|
|||
config.scanner.harden_profile = profile.to_owned();
|
||||
}
|
||||
}
|
||||
// Without the dynamic feature, --verify / --no-verify / --unsafe-sandbox /
|
||||
// --backend / --harden are silently accepted (no-op).
|
||||
// Without the dynamic feature, keep the user's verify toggle in
|
||||
// the resolved config so the scan command can either suppress the
|
||||
// warning (`--no-verify`) or explain why verification is static-only.
|
||||
#[cfg(not(feature = "dynamic"))]
|
||||
{
|
||||
let _ = verify;
|
||||
let _ = no_verify;
|
||||
let _ = verify_all_confidence;
|
||||
if no_verify {
|
||||
config.scanner.verify = false;
|
||||
} else if verify {
|
||||
config.scanner.verify = true;
|
||||
}
|
||||
if verify_all_confidence {
|
||||
config.scanner.verify_all_confidence = true;
|
||||
}
|
||||
let _ = unsafe_sandbox;
|
||||
let _ = backend;
|
||||
let _ = harden;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,116 @@ pub fn compute_stable_hash(diag: &Diag) -> u64 {
|
|||
u64::from_le_bytes(bytes[..8].try_into().unwrap())
|
||||
}
|
||||
|
||||
/// Aggregate status counts for dynamic verification verdicts attached to
|
||||
/// findings.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct DynamicVerificationSummary {
|
||||
pub total: usize,
|
||||
pub confirmed: usize,
|
||||
pub not_confirmed: usize,
|
||||
pub inconclusive: usize,
|
||||
pub unsupported: usize,
|
||||
}
|
||||
|
||||
impl DynamicVerificationSummary {
|
||||
pub fn from_diags(diags: &[Diag]) -> Self {
|
||||
let mut summary = Self::default();
|
||||
for diag in diags {
|
||||
let Some(verdict) = diag
|
||||
.evidence
|
||||
.as_ref()
|
||||
.and_then(|ev| ev.dynamic_verdict.as_ref())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
summary.total += 1;
|
||||
match verdict.status {
|
||||
crate::evidence::VerifyStatus::Confirmed => summary.confirmed += 1,
|
||||
crate::evidence::VerifyStatus::NotConfirmed => summary.not_confirmed += 1,
|
||||
crate::evidence::VerifyStatus::Inconclusive => summary.inconclusive += 1,
|
||||
crate::evidence::VerifyStatus::Unsupported => summary.unsupported += 1,
|
||||
}
|
||||
}
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn is_empty(self) -> bool {
|
||||
self.total == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable dynamic summary used by both CLI and server scan logs.
|
||||
pub fn format_dynamic_verification_summary(summary: &DynamicVerificationSummary) -> String {
|
||||
let noun = if summary.total == 1 {
|
||||
"verdict"
|
||||
} else {
|
||||
"verdicts"
|
||||
};
|
||||
format!(
|
||||
"{} {} ({} confirmed, {} not confirmed, {} inconclusive, {} unsupported)",
|
||||
summary.total,
|
||||
noun,
|
||||
summary.confirmed,
|
||||
summary.not_confirmed,
|
||||
summary.inconclusive,
|
||||
summary.unsupported
|
||||
)
|
||||
}
|
||||
|
||||
/// Apply dynamic verification to a completed scan.
|
||||
///
|
||||
/// Returns the configured verifier options so callers that perform later
|
||||
/// composite-chain re-verification can reuse preloaded summaries and callgraph
|
||||
/// context.
|
||||
#[cfg(feature = "dynamic")]
|
||||
pub(crate) fn verify_findings_for_scan(
|
||||
diags: &mut [Diag],
|
||||
project_name: &str,
|
||||
db_path: &Path,
|
||||
scan_path: &Path,
|
||||
config: &Config,
|
||||
verbose: bool,
|
||||
use_index_db: bool,
|
||||
) -> Option<crate::dynamic::verify::VerifyOptions> {
|
||||
if !config.scanner.verify {
|
||||
return None;
|
||||
}
|
||||
|
||||
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 operator
|
||||
// passes `--verbose`.
|
||||
opts.trace_verbose = verbose;
|
||||
|
||||
if use_index_db && db_path.exists() {
|
||||
opts.db_path = Some(db_path.to_path_buf());
|
||||
// Preload cross-file summaries once so the spec-derivation pipeline
|
||||
// can resolve the enclosing function and callgraph entry context
|
||||
// without re-hitting SQLite per finding. Best-effort: a load failure
|
||||
// logs and falls through to the substring heuristics.
|
||||
opts.summaries = load_verify_summaries(project_name, db_path, scan_path);
|
||||
if let Some(ref summaries) = opts.summaries {
|
||||
opts.callgraph = Some(load_verify_callgraph(summaries));
|
||||
}
|
||||
}
|
||||
|
||||
let telemetry_log = crate::dynamic::telemetry::log_path();
|
||||
for diag in diags {
|
||||
let mut result = crate::dynamic::verify::verify_finding(diag, &opts);
|
||||
if result.status == crate::dynamic::report::VerifyStatus::Confirmed
|
||||
&& let Some(ref log_path) = telemetry_log
|
||||
{
|
||||
result.wrong =
|
||||
crate::dynamic::telemetry::feedback_wrong_for_finding(log_path, &result.finding_id);
|
||||
}
|
||||
if let Some(ref mut ev) = diag.evidence {
|
||||
ev.dynamic_verdict = Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
Some(opts)
|
||||
}
|
||||
|
||||
/// Rollup data for grouped findings (e.g. 38 occurrences of `rs.quality.unwrap`).
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RollupData {
|
||||
|
|
@ -562,53 +672,23 @@ pub fn handle(
|
|||
// below can reuse the same preloaded summaries / callgraph without
|
||||
// a second SQLite round-trip.
|
||||
#[cfg(feature = "dynamic")]
|
||||
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
|
||||
// operator passes `--verbose`.
|
||||
opts.trace_verbose = verbose;
|
||||
// Enable the verdict cache (§12 Q5) when an index DB is in use.
|
||||
// When index_mode is Off, the DB is never created, so no cache.
|
||||
if index_mode != IndexMode::Off && db_path.exists() {
|
||||
opts.db_path = Some(db_path.clone());
|
||||
// Preload cross-file summaries once so the spec-derivation
|
||||
// pipeline can resolve the enclosing function's `FuncSummary`
|
||||
// (strategy 3) and its static `entry_kind` (strategy 4)
|
||||
// without re-hitting SQLite per finding. Best-effort: a load
|
||||
// failure logs and falls through to the substring heuristics.
|
||||
opts.summaries = load_verify_summaries(&project_name, &db_path, &scan_path);
|
||||
// Build the whole-program callgraph from the preloaded summaries
|
||||
// so strategy 4 can walk reverse edges to a route handler / CLI
|
||||
// entry when the sink lives in a leaf helper.
|
||||
if let Some(ref s) = opts.summaries {
|
||||
opts.callgraph = Some(load_verify_callgraph(s));
|
||||
}
|
||||
}
|
||||
// Phase 29 follow-up: resolve the telemetry events log path once
|
||||
// per scan so the per-finding `wrong:` stamp is a cheap fs read,
|
||||
// not a directories-crate lookup each iteration. `None` (no
|
||||
// log path resolvable on this host) leaves every `wrong` as
|
||||
// `None` — the eval-corpus tabulator treats that as "no signal."
|
||||
let telemetry_log = crate::dynamic::telemetry::log_path();
|
||||
for diag in &mut diags {
|
||||
let mut result = crate::dynamic::verify::verify_finding(diag, &opts);
|
||||
if result.status == crate::dynamic::report::VerifyStatus::Confirmed {
|
||||
if let Some(ref log_path) = telemetry_log {
|
||||
result.wrong = crate::dynamic::telemetry::feedback_wrong_for_finding(
|
||||
log_path,
|
||||
&result.finding_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(ref mut ev) = diag.evidence {
|
||||
ev.dynamic_verdict = Some(result);
|
||||
}
|
||||
}
|
||||
Some(opts)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verify_opts: Option<crate::dynamic::verify::VerifyOptions> = verify_findings_for_scan(
|
||||
&mut diags,
|
||||
&project_name,
|
||||
&db_path,
|
||||
&scan_path,
|
||||
config,
|
||||
verbose,
|
||||
index_mode != IndexMode::Off,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "dynamic"))]
|
||||
if config.scanner.verify && !suppress_status {
|
||||
eprintln!(
|
||||
"{}: dynamic verification is enabled, but this binary was built without dynamic support; running static-only. Rebuild with `cargo build --features dynamic` or set `[scanner] verify = false`.",
|
||||
style("warning").yellow().bold()
|
||||
);
|
||||
}
|
||||
|
||||
// ── Baseline write (§M6.5): persist current findings as stripped baseline
|
||||
if let Some(bw_path) = baseline_write {
|
||||
|
|
@ -3473,6 +3553,58 @@ fn apply_suppressions(diags: &mut [Diag]) {
|
|||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// dynamic verification summary tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod dynamic_summary_tests {
|
||||
use super::*;
|
||||
use crate::evidence::{Evidence, VerifyResult, VerifyStatus};
|
||||
|
||||
fn diag_with_status(status: VerifyStatus) -> Diag {
|
||||
Diag {
|
||||
evidence: Some(Evidence {
|
||||
dynamic_verdict: Some(VerifyResult {
|
||||
finding_id: "abc123".into(),
|
||||
status,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
differential: None,
|
||||
replay_stable: None,
|
||||
wrong: None,
|
||||
hardening_outcome: None,
|
||||
}),
|
||||
..Evidence::default()
|
||||
}),
|
||||
..Diag::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_summary_counts_verdict_statuses() {
|
||||
let diags = vec![
|
||||
diag_with_status(VerifyStatus::Confirmed),
|
||||
diag_with_status(VerifyStatus::NotConfirmed),
|
||||
diag_with_status(VerifyStatus::Inconclusive),
|
||||
diag_with_status(VerifyStatus::Unsupported),
|
||||
Diag::default(),
|
||||
];
|
||||
|
||||
let summary = DynamicVerificationSummary::from_diags(&diags);
|
||||
|
||||
assert_eq!(summary.total, 4);
|
||||
assert_eq!(summary.confirmed, 1);
|
||||
assert_eq!(summary.not_confirmed, 1);
|
||||
assert_eq!(summary.inconclusive, 1);
|
||||
assert_eq!(summary.unsupported, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// deduplicate_taint_flows tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@
|
|||
//! All submodules are read-only consumers of the static engine's output.
|
||||
//! Nothing in this tree mutates SSA, taint, or label state.
|
||||
//!
|
||||
//! Off by default. Enable with `--features dynamic`. Heavy deps (container
|
||||
//! runtime client, fuzzer harness) live behind the same gate.
|
||||
//! Included in default builds. Custom `--no-default-features` builds can enable
|
||||
//! it with `--features dynamic`.
|
||||
//!
|
||||
//! # Spec derivation strategies
|
||||
//!
|
||||
|
|
|
|||
12
src/fmt.rs
12
src/fmt.rs
|
|
@ -52,6 +52,18 @@ pub fn render_console(
|
|||
}
|
||||
}
|
||||
|
||||
let dynamic_summary = crate::commands::scan::DynamicVerificationSummary::from_diags(diags);
|
||||
if !dynamic_summary.is_empty() {
|
||||
out.push_str(&format!(
|
||||
"{} {}\n\n",
|
||||
style("Dynamic verification:").cyan().bold(),
|
||||
style(crate::commands::scan::format_dynamic_verification_summary(
|
||||
&dynamic_summary
|
||||
))
|
||||
.dim()
|
||||
));
|
||||
}
|
||||
|
||||
let suppressed_count = diags.iter().filter(|d| d.suppressed).count();
|
||||
let active_count = diags.len() - suppressed_count;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
//! pipeline before reaching this layer.
|
||||
|
||||
use crate::chain::finding::ChainFinding;
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::commands::scan::{Diag, DynamicVerificationSummary};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -42,6 +42,7 @@ pub fn build_findings_json(
|
|||
let mut out = json!({
|
||||
"findings": findings,
|
||||
"chains": chains_array,
|
||||
"dynamic_verification": DynamicVerificationSummary::from_diags(diags),
|
||||
});
|
||||
if let Some(diff) = verdict_diff {
|
||||
out["verdict_diff"] = diff.clone();
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ impl JobManager {
|
|||
Some(&log_collector),
|
||||
)?;
|
||||
let pool = Indexer::init(&db_path)?;
|
||||
scan::scan_with_index_parallel_observer(
|
||||
let mut diags = scan::scan_with_index_parallel_observer(
|
||||
&project_name,
|
||||
pool,
|
||||
&config,
|
||||
|
|
@ -250,7 +250,23 @@ impl JobManager {
|
|||
Some(&log_collector),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
)?;
|
||||
for diag in &mut diags {
|
||||
diag.stable_hash = scan::compute_stable_hash(diag);
|
||||
}
|
||||
#[cfg(feature = "dynamic")]
|
||||
{
|
||||
let _verify_opts = scan::verify_findings_for_scan(
|
||||
&mut diags,
|
||||
&project_name,
|
||||
&db_path,
|
||||
&scan_root,
|
||||
&config,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Ok(diags)
|
||||
});
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
|
||||
|
|
@ -274,6 +290,16 @@ impl JobManager {
|
|||
for d in &mut diags {
|
||||
d.stable_hash = scan::compute_stable_hash(d);
|
||||
}
|
||||
let dynamic_summary = scan::DynamicVerificationSummary::from_diags(&diags);
|
||||
if !dynamic_summary.is_empty() {
|
||||
log_collector.info(
|
||||
format!(
|
||||
"Dynamic verification: {}",
|
||||
scan::format_dynamic_verification_summary(&dynamic_summary)
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
log_collector.info(format!("Scan completed: {} findings", diags.len()), None);
|
||||
(JobStatus::Completed, Some(Arc::new(diags)), None)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -717,6 +717,8 @@ pub struct ScannerQuality {
|
|||
pub symex_verified_rate: f64,
|
||||
/// Count broken down by symbolic verdict label.
|
||||
pub symex_breakdown: HashMap<String, usize>,
|
||||
/// Dynamic verifier verdict counts from the latest scan.
|
||||
pub dynamic_verification: crate::commands::scan::DynamicVerificationSummary,
|
||||
}
|
||||
|
||||
/// One issue-category bucket (rule-family derived). Broader than OWASP, with
|
||||
|
|
|
|||
|
|
@ -837,6 +837,9 @@ fn compute_scanner_quality(
|
|||
call_resolution_rate,
|
||||
symex_verified_rate,
|
||||
symex_breakdown: breakdown,
|
||||
dynamic_verification: crate::commands::scan::DynamicVerificationSummary::from_diags(
|
||||
findings,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ struct StartScanRequest {
|
|||
/// `false` - force off even if config says on.
|
||||
/// absent - inherit config default.
|
||||
///
|
||||
/// Requires `--features dynamic`; `true` returns 400 when the
|
||||
/// feature is absent.
|
||||
/// Included in default builds; custom builds without `dynamic` return 400
|
||||
/// when verification is requested.
|
||||
verify: Option<bool>,
|
||||
/// Also verify `Confidence < Medium` findings. Default false.
|
||||
verify_all_confidence: Option<bool>,
|
||||
|
|
@ -126,6 +126,13 @@ async fn start_scan(
|
|||
config.scanner.verify_all_confidence = true;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "dynamic"))]
|
||||
if config.scanner.verify || config.scanner.verify_all_confidence {
|
||||
return Err(bad_request(
|
||||
"dynamic verification is enabled, but this binary was built without dynamic support; rebuild with `cargo build --features dynamic` or skip dynamic verification for this scan",
|
||||
));
|
||||
}
|
||||
|
||||
let event_tx = state.event_tx.clone();
|
||||
let db_pool = state.db_pool.clone();
|
||||
let database_dir = state.database_dir.clone();
|
||||
|
|
|
|||
|
|
@ -256,8 +256,9 @@ pub struct ScannerConfig {
|
|||
/// `Evidence::dynamic_verdict`. Use `--no-verify` (CLI) or set
|
||||
/// `verify = false` in `nyx.toml` to disable.
|
||||
///
|
||||
/// Requires the binary to be built with `--features dynamic`; without
|
||||
/// that feature the setting has no effect.
|
||||
/// Included in default builds. Custom `--no-default-features` builds need
|
||||
/// `--features dynamic`; without that feature the CLI warns and runs
|
||||
/// static-only.
|
||||
///
|
||||
/// Migration note: existing `nyx.toml` files that already set
|
||||
/// `verify = false` keep the opt-out behaviour; only the inherited
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue