From 678f0f5d48350a3a30fb4a44555b3f94523a5579 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sat, 16 May 2026 02:26:41 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0003 (20260516T052512Z-20f8) --- src/auth_analysis/auth_markers.rs | 278 +++++++++++++++++++++++++++ src/auth_analysis/mod.rs | 1 + src/baseline.rs | 2 + src/chain/feasibility.rs | 2 + src/chain/reverify.rs | 4 + src/dynamic/repro.rs | 44 +++++ src/dynamic/telemetry.rs | 83 ++++++++ src/dynamic/verify.rs | 48 +++++ src/evidence.rs | 18 ++ src/rank.rs | 10 + src/surface/datastore.rs | 105 +++++++++- src/surface/external.rs | 89 ++++++++- src/surface/lang/go_gin.rs | 11 +- src/surface/lang/java_quarkus.rs | 7 +- src/surface/lang/java_servlet.rs | 7 +- src/surface/lang/java_spring.rs | 8 +- src/surface/lang/js_express.rs | 15 +- src/surface/lang/js_koa.rs | 15 +- src/surface/lang/python_django.rs | 10 +- src/surface/lang/python_fastapi.rs | 12 +- src/surface/lang/python_flask.rs | 10 +- src/surface/lang/rust_actix.rs | 9 +- src/surface/lang/rust_axum.rs | 8 +- tests/chain_reverify.rs | 2 + tests/common/fixture_harness.rs | 6 + tests/console_snapshot.rs | 8 + tests/fix_validation_e2e.rs | 4 + tests/go_fixtures.rs | 2 + tests/java_fixtures.rs | 2 + tests/js_fixtures.rs | 2 + tests/json_snapshot.rs | 6 + tests/php_fixtures.rs | 2 + tests/repro_determinism.rs | 2 + tests/repro_hermetic.rs | 2 + tests/sarif_dynamic_verdict_tests.rs | 12 ++ 35 files changed, 737 insertions(+), 109 deletions(-) create mode 100644 src/auth_analysis/auth_markers.rs diff --git a/src/auth_analysis/auth_markers.rs b/src/auth_analysis/auth_markers.rs new file mode 100644 index 00000000..d38e09b7 --- /dev/null +++ b/src/auth_analysis/auth_markers.rs @@ -0,0 +1,278 @@ +//! Canonical per-framework authentication-marker registry. +//! +//! Both the Phase 22 surface probes (`src/surface/lang/*.rs`) and the +//! auth-analysis recogniser consult this module so a marker that is +//! known to one side cannot drift away from the other. Each constant +//! is a flat `&[&str]` of identifier shapes that signal a route is +//! gated behind authentication; surface probes match the leaf segment +//! of a decorator / middleware / extractor identifier +//! (case-insensitive), and the auth analyser folds these into its +//! per-language `login_guard_names` / `authorization_check_names` +//! tables via [`router_auth_markers_for_lang`]. +//! +//! The lists were lifted verbatim from the per-probe constants that +//! shipped with Phase 22; further additions land here and propagate to +//! every consumer at once. +//! +//! Lookups: prefer [`is_router_auth_marker`] for the framework-aware +//! dispatch, fall back to [`is_known_router_auth_marker`] when the +//! framework is not yet identified at the call site. + +use crate::symbol::Lang; + +/// Frameworks the surface probes recognise. Distinct from +/// [`crate::surface::Framework`] (which carries pretty-print metadata) +/// so this module stays free of surface-layer types and can be +/// imported by `auth_analysis::extract` without a circular dep. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AuthFramework { + Flask, + FastApi, + Django, + Spring, + JavaServlet, + Quarkus, + Express, + Koa, + Gin, + ActixWeb, + Axum, +} + +/// Flask (`@login_required`, `@requires_auth`, …). +pub const FLASK_DECORATORS: &[&str] = &[ + "login_required", + "auth_required", + "jwt_required", + "token_required", + "requires_auth", + "authenticated", + "require_login", +]; + +/// FastAPI (`Depends(get_current_user)`, `@login_required`, …). +pub const FASTAPI_DECORATORS: &[&str] = &[ + "login_required", + "auth_required", + "jwt_required", + "token_required", + "requires_auth", + "authenticated", + "require_auth", + "require_login", + "current_user", +]; + +/// Django (`@login_required`, `@permission_required`, …). +pub const DJANGO_DECORATORS: &[&str] = &[ + "login_required", + "permission_required", + "user_passes_test", + "staff_member_required", + "csrf_protect", + "require_authenticated", + "auth_required", +]; + +/// Spring (`@PreAuthorize`, `@Secured`, …). +pub const SPRING_ANNOTATIONS: &[&str] = &[ + "PreAuthorize", + "PostAuthorize", + "Secured", + "RolesAllowed", + "AuthenticationPrincipal", +]; + +/// Java Servlet / JAX-RS (`@RolesAllowed`, `@RequiresAuthentication`, …). +pub const SERVLET_ANNOTATIONS: &[&str] = &[ + "RolesAllowed", + "DenyAll", + "RequiresAuthentication", + "RequiresUser", +]; + +/// Quarkus (`@Authenticated`, `@RolesAllowed`, …). +pub const QUARKUS_ANNOTATIONS: &[&str] = &[ + "Authenticated", + "RolesAllowed", + "DenyAll", + "RequiresAuthentication", +]; + +/// Express middleware (`app.use(requireAuth)`, `passport.authenticate`, …). +pub const EXPRESS_MIDDLEWARES: &[&str] = &[ + "requireAuth", + "requireUser", + "isAuthenticated", + "ensureAuthenticated", + "ensureLoggedIn", + "authenticate", + "authMiddleware", + "verifyToken", + "verifyJwt", + "checkJwt", + "passport", + "jwt", +]; + +/// Koa middleware. +pub const KOA_MIDDLEWARES: &[&str] = &[ + "requireAuth", + "requireUser", + "isAuthenticated", + "ensureAuthenticated", + "authenticate", + "authMiddleware", + "verifyToken", + "verifyJwt", + "checkJwt", + "passport", + "jwt", + "koaJwt", +]; + +/// Gin middleware (`router.Use(AuthRequired())`, `jwt.JWT()`, …). +pub const GIN_MIDDLEWARES: &[&str] = &[ + "AuthRequired", + "JWT", + "JWTAuth", + "Auth", + "RequireAuth", + "RequireUser", + "VerifyToken", + "BasicAuth", +]; + +/// actix-web extractors (`Identity`, `BearerAuth`, …). +pub const ACTIX_EXTRACTORS: &[&str] = &[ + "Identity", + "BearerAuth", + "BasicAuth", + "JwtClaims", + "Authenticated", + "User", +]; + +/// axum extractors (`Extension`, `BearerAuth`, …). +pub const AXUM_EXTRACTORS: &[&str] = &[ + "Extension &'static [&'static str] { + match framework { + AuthFramework::Flask => FLASK_DECORATORS, + AuthFramework::FastApi => FASTAPI_DECORATORS, + AuthFramework::Django => DJANGO_DECORATORS, + AuthFramework::Spring => SPRING_ANNOTATIONS, + AuthFramework::JavaServlet => SERVLET_ANNOTATIONS, + AuthFramework::Quarkus => QUARKUS_ANNOTATIONS, + AuthFramework::Express => EXPRESS_MIDDLEWARES, + AuthFramework::Koa => KOA_MIDDLEWARES, + AuthFramework::Gin => GIN_MIDDLEWARES, + AuthFramework::ActixWeb => ACTIX_EXTRACTORS, + AuthFramework::Axum => AXUM_EXTRACTORS, + } +} + +/// Case-insensitive whole-string match against the per-framework list. +pub fn is_router_auth_marker(framework: AuthFramework, marker: &str) -> bool { + let m = marker.trim(); + markers_for(framework) + .iter() + .any(|cand| cand.eq_ignore_ascii_case(m)) +} + +/// Loose match against every framework's list. Used when the call +/// site has the language but not the specific framework — e.g. an +/// auth-analyser folding "is this a known router-level guard?" into a +/// per-language ruleset where the framework split is opaque. +pub fn is_known_router_auth_marker(marker: &str) -> bool { + let m = marker.trim(); + [ + FLASK_DECORATORS, + FASTAPI_DECORATORS, + DJANGO_DECORATORS, + SPRING_ANNOTATIONS, + SERVLET_ANNOTATIONS, + QUARKUS_ANNOTATIONS, + EXPRESS_MIDDLEWARES, + KOA_MIDDLEWARES, + GIN_MIDDLEWARES, + ACTIX_EXTRACTORS, + AXUM_EXTRACTORS, + ] + .iter() + .any(|list| list.iter().any(|cand| cand.eq_ignore_ascii_case(m))) +} + +/// Every router-auth marker the canonical registry knows for `lang`. +/// Used by `auth_analysis::config::default_for` to seed +/// `login_guard_names` so a marker added here propagates into the +/// per-language guard list without a second edit. +pub fn router_auth_markers_for_lang(lang: Lang) -> Vec<&'static str> { + let lists: &[&[&str]] = match lang { + Lang::Python => &[FLASK_DECORATORS, FASTAPI_DECORATORS, DJANGO_DECORATORS], + Lang::Java => &[SPRING_ANNOTATIONS, SERVLET_ANNOTATIONS, QUARKUS_ANNOTATIONS], + Lang::JavaScript | Lang::TypeScript => &[EXPRESS_MIDDLEWARES, KOA_MIDDLEWARES], + Lang::Go => &[GIN_MIDDLEWARES], + Lang::Rust => &[ACTIX_EXTRACTORS, AXUM_EXTRACTORS], + _ => &[], + }; + let mut out: Vec<&'static str> = lists.iter().flat_map(|l| l.iter().copied()).collect(); + out.sort_unstable(); + out.dedup(); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flask_login_required_resolves_case_insensitively() { + assert!(is_router_auth_marker(AuthFramework::Flask, "login_required")); + assert!(is_router_auth_marker(AuthFramework::Flask, "Login_Required")); + assert!(!is_router_auth_marker(AuthFramework::Flask, "something_else")); + } + + #[test] + fn spring_preauthorize_resolves() { + assert!(is_router_auth_marker(AuthFramework::Spring, "PreAuthorize")); + assert!(!is_router_auth_marker(AuthFramework::Spring, "GetMapping")); + } + + #[test] + fn known_marker_matches_across_frameworks() { + // `RolesAllowed` shows up in Spring, Servlet, and Quarkus — + // the framework-agnostic helper finds it regardless. + assert!(is_known_router_auth_marker("RolesAllowed")); + assert!(is_known_router_auth_marker("login_required")); + assert!(!is_known_router_auth_marker("not_an_auth_marker_xyz")); + } + + #[test] + fn python_router_markers_cover_every_framework() { + let markers = router_auth_markers_for_lang(Lang::Python); + for &decorator in FLASK_DECORATORS { + assert!(markers.contains(&decorator), "missing flask: {decorator}"); + } + for &decorator in FASTAPI_DECORATORS { + assert!(markers.contains(&decorator), "missing fastapi: {decorator}"); + } + for &decorator in DJANGO_DECORATORS { + assert!(markers.contains(&decorator), "missing django: {decorator}"); + } + } + + #[test] + fn router_markers_for_unknown_lang_is_empty() { + assert!(router_auth_markers_for_lang(Lang::Ruby).is_empty()); + assert!(router_auth_markers_for_lang(Lang::Php).is_empty()); + } +} diff --git a/src/auth_analysis/mod.rs b/src/auth_analysis/mod.rs index 62d46bf8..2298650d 100644 --- a/src/auth_analysis/mod.rs +++ b/src/auth_analysis/mod.rs @@ -56,6 +56,7 @@ //! - [`sql_semantics`]: ACL-join and `user_id`-predicate detection without a //! SQL parser +pub mod auth_markers; pub mod checks; pub mod config; pub mod extract; diff --git a/src/baseline.rs b/src/baseline.rs index ec544705..ac9a8ea1 100644 --- a/src/baseline.rs +++ b/src/baseline.rs @@ -446,6 +446,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }), ..Default::default() }); diff --git a/src/chain/feasibility.rs b/src/chain/feasibility.rs index 4f096915..fe021db6 100644 --- a/src/chain/feasibility.rs +++ b/src/chain/feasibility.rs @@ -108,6 +108,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } diff --git a/src/chain/reverify.rs b/src/chain/reverify.rs index 6ad1e8ef..ae0d7849 100644 --- a/src/chain/reverify.rs +++ b/src/chain/reverify.rs @@ -129,6 +129,8 @@ impl CompositeReverifier for DefaultCompositeReverifier { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } } @@ -252,6 +254,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } diff --git a/src/dynamic/repro.rs b/src/dynamic/repro.rs index 300da090..51799dc6 100644 --- a/src/dynamic/repro.rs +++ b/src/dynamic/repro.rs @@ -516,6 +516,26 @@ pub enum ReplayResult { }, } +/// Tri-state map of [`ReplayResult`] onto the eval-corpus +/// `VerifyResult::replay_stable` field shape. +/// +/// * `Some(true)` — replay matched the recorded outcome. +/// * `Some(false)` — replay diverged or aborted in a way that the M7 +/// Gate-5 inversion treats as instability. +/// * `None` — replay was not informative (toolchain mismatched, docker +/// unavailable, or the bundle had no `reproduce.sh`). The corpus +/// tabulator treats `None` as "no signal" and excludes the row from +/// the per-cell `stable_replays` numerator. +pub fn replay_stability(result: &ReplayResult) -> Option { + match result { + ReplayResult::Pass => Some(true), + ReplayResult::Mismatch | ReplayResult::UnexpectedError { .. } => Some(false), + ReplayResult::DockerUnavailable + | ReplayResult::ToolchainMismatch + | ReplayResult::ScriptInvocationFailed { .. } => None, + } +} + /// Phase 28 — Track H.3. Run `reproduce.sh` in `bundle_root` and map the /// shell exit code into a [`ReplayResult`]. /// @@ -648,6 +668,8 @@ mod tests { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, } } @@ -780,6 +802,28 @@ mod tests { } } + #[test] + fn replay_stability_maps_to_eval_corpus_tristate() { + // The eval-corpus tabulator wants Pass → stable, anything that + // looks like instability → unstable, and infra-blocked variants + // → no signal (None) so the per-cell stable_replays denominator + // is not inflated by a row that never had a chance to replay. + assert_eq!(replay_stability(&ReplayResult::Pass), Some(true)); + assert_eq!(replay_stability(&ReplayResult::Mismatch), Some(false)); + assert_eq!( + replay_stability(&ReplayResult::UnexpectedError { exit_code: 9 }), + Some(false) + ); + assert_eq!(replay_stability(&ReplayResult::DockerUnavailable), None); + assert_eq!(replay_stability(&ReplayResult::ToolchainMismatch), None); + assert_eq!( + replay_stability(&ReplayResult::ScriptInvocationFailed { + message: "missing".into() + }), + None, + ); + } + #[test] fn replay_bundle_reports_missing_script() { let dir = TempDir::new().unwrap(); diff --git a/src/dynamic/telemetry.rs b/src/dynamic/telemetry.rs index 1b3b9da9..5ea0da74 100644 --- a/src/dynamic/telemetry.rs +++ b/src/dynamic/telemetry.rs @@ -502,6 +502,51 @@ pub fn read_events(path: &Path) -> Result, TelemetryReadE Ok(out) } +/// Scan the `verify_feedback` records in an events log for the given +/// finding id and return the matching `VerifyResult::wrong` value. +/// +/// * `Some(true)` — most-recent feedback for this finding was +/// `wrong:`. +/// * `Some(false)` — most-recent feedback was `right`. +/// * `None` — no feedback recorded for this finding. +/// +/// Multiple records for the same finding collapse to the **last** one +/// in file order: callers run `nyx verify-feedback` more than once when +/// they correct an earlier judgment, and the latest reading is the +/// authoritative one. The events log is read via the raw JSONL path +/// (NOT [`read_events`]) because `verify_feedback` rows were written +/// before the `schema_version`-envelope migration and may legitimately +/// pre-date the schema bump; a missing `schema_version` here is not +/// fatal. +pub fn feedback_wrong_for_finding(path: &Path, finding_id: &str) -> Option { + let file = std::fs::File::open(path).ok()?; + let reader = BufReader::new(file); + let mut latest: Option = None; + for line in reader.lines().map_while(Result::ok) { + if line.trim().is_empty() { + continue; + } + let Ok(value) = serde_json::from_str::(&line) else { + continue; + }; + if value.get("event").and_then(|v| v.as_str()) != Some("verify_feedback") { + continue; + } + if value.get("finding_id").and_then(|v| v.as_str()) != Some(finding_id) { + continue; + } + let Some(feedback) = value.get("feedback").and_then(|v| v.as_str()) else { + continue; + }; + if feedback.starts_with("wrong:") || feedback == "wrong" { + latest = Some(true); + } else if feedback == "right" { + latest = Some(false); + } + } + latest +} + // ── Rank delta telemetry ────────────────────────────────────────────────────── /// One telemetry event per ranked finding that carries a dynamic verdict delta. @@ -598,6 +643,44 @@ mod tests { } } + #[test] + fn feedback_wrong_for_finding_returns_latest_record() { + use std::io::Write; + let dir = TempDir::new().unwrap(); + let log = dir.path().join("events.jsonl"); + let mut f = std::fs::File::create(&log).unwrap(); + // Three records for the same finding: initial wrong, later + // overridden by right. The latest wins. + writeln!( + f, + r#"{{"event":"verify_feedback","finding_id":"abc1","feedback":"wrong:sample"}}"# + ) + .unwrap(); + writeln!( + f, + r#"{{"event":"verify_feedback","finding_id":"abc2","feedback":"wrong:other"}}"# + ) + .unwrap(); + writeln!( + f, + r#"{{"event":"verify_feedback","finding_id":"abc1","feedback":"right"}}"# + ) + .unwrap(); + // Non-feedback rows are ignored. + writeln!(f, r#"{{"event":"verify","finding_id":"abc1"}}"#).unwrap(); + f.flush().unwrap(); + assert_eq!(feedback_wrong_for_finding(&log, "abc1"), Some(false)); + assert_eq!(feedback_wrong_for_finding(&log, "abc2"), Some(true)); + assert_eq!(feedback_wrong_for_finding(&log, "missing"), None); + } + + #[test] + fn feedback_wrong_for_finding_tolerates_missing_file() { + let dir = TempDir::new().unwrap(); + let log = dir.path().join("nonexistent.jsonl"); + assert_eq!(feedback_wrong_for_finding(&log, "abc1"), None); + } + #[test] fn emit_writes_valid_json() { let dir = TempDir::new().unwrap(); diff --git a/src/dynamic/verify.rs b/src/dynamic/verify.rs index 65c3a3bf..85732c75 100644 --- a/src/dynamic/verify.rs +++ b/src/dynamic/verify.rs @@ -286,6 +286,8 @@ fn entry_kind_unsupported_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } @@ -328,6 +330,8 @@ fn spec_derivation_failed_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } @@ -344,6 +348,8 @@ fn spec_derivation_failed_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } @@ -449,6 +455,8 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } @@ -531,6 +539,8 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } @@ -559,6 +569,8 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } } @@ -734,6 +746,8 @@ fn build_verdict( attempts: attempts.clone(), toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential.clone(), + replay_stable: None, + wrong: None, }, &run.harness_source, &run.entry_source, @@ -754,6 +768,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential, + replay_stable: None, + wrong: None, }; } @@ -767,6 +783,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential, + replay_stable: None, + wrong: None, } } else if run.unrelated_crash { // Phase 08 §C.4: the harness crashed but the death @@ -786,6 +804,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: None, + replay_stable: None, + wrong: None, } } else if run.no_benign_control { // Phase 07 §4.1: vuln oracle + sink-hit fired but the @@ -804,6 +824,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: None, + replay_stable: None, + wrong: None, } } else if let Some(d) = run.differential.as_ref() { // Differential ran but didn't produce `Confirmed`. Map @@ -825,6 +847,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential, + replay_stable: None, + wrong: None, } } crate::evidence::DifferentialVerdict::ReversedDifferential => { @@ -842,6 +866,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential, + replay_stable: None, + wrong: None, } } crate::evidence::DifferentialVerdict::Confirmed @@ -855,6 +881,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: run.differential, + replay_stable: None, + wrong: None, }, } } else if run.oracle_collision { @@ -871,6 +899,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: None, + replay_stable: None, + wrong: None, } } else { VerifyResult { @@ -883,6 +913,8 @@ fn build_verdict( attempts, toolchain_match: Some(toolchain_match.to_owned()), differential: None, + replay_stable: None, + wrong: None, } } } @@ -896,6 +928,8 @@ fn build_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, Err(RunError::Harness(e)) => { // Defence-in-depth residual for `EntryKindUnsupported` from the @@ -939,6 +973,8 @@ fn build_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } Err(RunError::BuildFailed { stderr, attempts: build_att }) => VerifyResult { @@ -951,6 +987,8 @@ fn build_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, Err(RunError::Sandbox(e)) => VerifyResult { finding_id: finding_id.to_owned(), @@ -962,6 +1000,8 @@ fn build_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, } } @@ -1041,6 +1081,8 @@ mod tests { attempts: vec![], toolchain_match: Some("exact".to_owned()), differential: None, + replay_stable: None, + wrong: None, }; // Insert. @@ -1090,6 +1132,8 @@ mod tests { attempts: vec![], toolchain_match: Some("exact".to_owned()), differential: None, + replay_stable: None, + wrong: None, }; insert_verdict_cache(&db_path, "spec_aaa", "hash_xyz", "", "python-3.11", &result); @@ -1125,6 +1169,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; insert_verdict_cache(db_path, "spec", "hash", "", "python-3", &result); assert!(!db_path.exists(), "insert must not create a new DB"); @@ -1179,6 +1225,8 @@ mod tests { attempts: vec![], toolchain_match: Some("exact".to_owned()), differential: None, + replay_stable: None, + wrong: None, }; // Insert directly with the old corpus_version bypassing the helper. diff --git a/src/evidence.rs b/src/evidence.rs index ae47374f..c62ddf7a 100644 --- a/src/evidence.rs +++ b/src/evidence.rs @@ -566,6 +566,24 @@ pub struct VerifyResult { /// `BuildFailed`, `NoBenignControl`, `NotConfirmed` with vuln-only). #[serde(default, skip_serializing_if = "Option::is_none")] pub differential: Option, + /// Eval-corpus repro stability flag. `Some(true)` when `reproduce.sh` + /// inside the verifier's bundle replayed green (`ReplayResult::Pass`), + /// `Some(false)` when it diverged or aborted, `None` when no replay + /// has been attempted (host infrastructure missing, backend not + /// supported, etc.). Drives the `stable_replays` column in + /// `tests/eval_corpus/tabulate.py` — the eval-corpus + /// `repro_stability` budget cannot fire until this field carries a + /// `Some(true)` for at least one Confirmed row. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub replay_stable: Option, + /// Eval-corpus manual-triage flag. `Some(true)` when the user + /// recorded a `wrong:` verdict via `nyx verify-feedback` or + /// when an automated ground-truth pass marked this finding as a + /// false confirmed. `Some(false)` when explicitly marked right; + /// `None` when no triage has happened. Drives the + /// `wrong_confirmed` column in `tests/eval_corpus/tabulate.py`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub wrong: Option, } // ───────────────────────────────────────────────────────────────────────────── diff --git a/src/rank.rs b/src/rank.rs index 66235f51..3e0c97e3 100644 --- a/src/rank.rs +++ b/src/rank.rs @@ -1157,6 +1157,8 @@ mod tests { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, } } @@ -1177,6 +1179,8 @@ mod tests { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, } } @@ -1191,6 +1195,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } @@ -1205,6 +1211,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } @@ -1219,6 +1227,8 @@ mod tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } diff --git a/src/surface/datastore.rs b/src/surface/datastore.rs index 7a72f050..574a829e 100644 --- a/src/surface/datastore.rs +++ b/src/surface/datastore.rs @@ -92,6 +92,20 @@ const DRIVER_RULES: &[DriverRule] = &[ DriverRule { leaf: "diesel::sql_query", kind: DataStoreKind::Sql, label: "Diesel" }, DriverRule { leaf: "PgConnection::establish", kind: DataStoreKind::Sql, label: "Diesel" }, + // Type-qualified — fires when the SSA type-fact engine resolves a + // receiver to `TypeKind::DatabaseConnection` regardless of the bare + // callee name (e.g. `conn = psycopg2.connect(); conn.cursor()` → + // typed_call_receivers maps the `.cursor` ordinal to "DatabaseConnection"). + DriverRule { leaf: "DatabaseConnection.cursor", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "DatabaseConnection.execute", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "DatabaseConnection.query", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "DatabaseConnection.exec", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "DatabaseConnection.prepare", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "DatabaseConnection.commit", kind: DataStoreKind::Sql, label: "Database connection" }, + DriverRule { leaf: "FileHandle.read", kind: DataStoreKind::Filesystem, label: "Filesystem" }, + DriverRule { leaf: "FileHandle.write", kind: DataStoreKind::Filesystem, label: "Filesystem" }, + DriverRule { leaf: "FileHandle.close", kind: DataStoreKind::Filesystem, label: "Filesystem" }, + // Filesystem (best-effort: language-agnostic open()-family) DriverRule { leaf: "open", kind: DataStoreKind::Filesystem, label: "Filesystem" }, ]; @@ -99,15 +113,28 @@ const DRIVER_RULES: &[DriverRule] = &[ /// Walk every function summary's callee list and emit one /// [`SurfaceNode::DataStore`] per matched driver call. De-duped on /// `(file, line, label)`. +/// +/// When the bare callee name does not hit a rule, the type-fact engine's +/// per-call `typed_call_receivers` map (read off the matching +/// [`crate::summary::SsaFuncSummary`]) is consulted: a callee whose +/// receiver was resolved to `TypeKind::DatabaseConnection` or +/// `TypeKind::FileHandle` is retried under the type-qualified name +/// `"DatabaseConnection."` / `"FileHandle."`, picking up +/// the bound-receiver call shapes (`conn.cursor()` after +/// `conn = psycopg2.connect()`) that the name-only matcher misses. pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec { let mut out: Vec = Vec::new(); let mut seen: std::collections::HashSet<(String, u32, String)> = std::collections::HashSet::new(); for (key, summary) in summaries.iter() { + let typed = summaries.get_ssa(key).map(|s| s.typed_call_receivers.as_slice()); for callee in &summary.callees { - let Some(rule) = match_rule(&callee.name) else { - continue; - }; + let rule = match_rule(&callee.name).or_else(|| { + typed + .and_then(|t| container_for_ordinal(t, callee.ordinal)) + .and_then(|c| match_rule(&qualify(c, &callee.name))) + }); + let Some(rule) = rule else { continue }; let location = call_site_location(summary, callee); let dedup = ( location.file.clone(), @@ -117,7 +144,6 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec { if !seen.insert(dedup) { continue; } - let _ = key; out.push(SurfaceNode::DataStore(DataStore { location, kind: rule.kind, @@ -128,6 +154,25 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec { out } +/// Last segment of a callee text after the final `.` or `::`. +fn leaf_segment(name: &str) -> &str { + let after_colon = name.rsplit("::").next().unwrap_or(name); + after_colon.rsplit('.').next().unwrap_or(after_colon) +} + +/// Build a type-qualified callee name (`"{container}.{method}"`) for +/// retry-matching when the bare callee text did not hit any rule. +fn qualify(container: &str, callee_name: &str) -> String { + format!("{}.{}", container, leaf_segment(callee_name)) +} + +/// Linear-scan helper since `typed_call_receivers` is a small +/// `Vec<(ordinal, container)>` per function. Typical lengths are 0 to a +/// few dozen; a HashMap-per-summary would be wasteful. +fn container_for_ordinal(typed: &[(u32, String)], ordinal: u32) -> Option<&str> { + typed.iter().find(|(o, _)| *o == ordinal).map(|(_, c)| c.as_str()) +} + fn match_rule(callee: &str) -> Option<&'static DriverRule> { let cl = callee.trim().to_ascii_lowercase(); // Normalize `::` → `.` so segment-split treats both as separators. @@ -290,4 +335,56 @@ mod tests { let nodes = detect_data_stores(&gs); assert_eq!(nodes.len(), 1); } + + #[test] + fn typed_receiver_database_connection_resolves_bound_cursor() { + // `conn = psycopg2.connect(); conn.cursor()` — the bare callee + // `conn.cursor` is not in DRIVER_RULES, but the SSA type-fact + // engine populates `typed_call_receivers` with + // `(ordinal, "DatabaseConnection")` for the `.cursor` ordinal. + // The detector retries under `DatabaseConnection.cursor` and + // emits a Sql datastore node. + use crate::summary::ssa_summary::SsaFuncSummary; + let mut gs = GlobalSummaries::new(); + let key = FuncKey::new_function(Lang::Python, "app.py", "load", None); + let summary = FuncSummary { + name: "load".into(), + file_path: "app.py".into(), + lang: "python".into(), + param_count: 0, + callees: vec![ + { + let mut c = CalleeSite::bare("conn.cursor"); + c.ordinal = 7; + c.span = Some((4, 8)); + c + }, + ], + ..Default::default() + }; + gs.insert(key.clone(), summary); + let mut ssa = SsaFuncSummary::default(); + ssa.typed_call_receivers + .push((7, "DatabaseConnection".into())); + gs.insert_ssa(key, ssa); + let nodes = detect_data_stores(&gs); + assert_eq!(nodes.len(), 1, "expected typed retry to hit; got {nodes:?}"); + let SurfaceNode::DataStore(ds) = &nodes[0] else { + panic!() + }; + assert_eq!(ds.kind, DataStoreKind::Sql); + assert_eq!(ds.label, "Database connection"); + assert_eq!(ds.location.line, 4); + } + + #[test] + fn typed_receiver_without_ssa_summary_falls_through() { + // No SsaFuncSummary inserted → bare `client.cursor` does not match + // any rule and `typed_call_receivers` is unreachable. Detector + // emits zero nodes (no panic on missing SSA side). + let mut gs = GlobalSummaries::new(); + let (k, s) = summary_with_callees("load", "app.py", &["client.cursor"]); + gs.insert(k, s); + assert!(detect_data_stores(&gs).is_empty()); + } } diff --git a/src/surface/external.rs b/src/surface/external.rs index 1bba2fbc..11d7175f 100644 --- a/src/surface/external.rs +++ b/src/surface/external.rs @@ -76,17 +76,50 @@ const CLIENT_RULES: &[ClientRule] = &[ ClientRule { leaf: "socket.gethostbyname", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" }, ClientRule { leaf: "dns.lookup", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" }, ClientRule { leaf: "net.LookupIP", kind: ExternalServiceKind::HttpApi, label: "DNS resolver" }, + + // Type-qualified — fires when the SSA type-fact engine resolves a + // receiver to `TypeKind::HttpClient` regardless of the bare callee + // name (`session = requests.Session(); session.get(url)` → + // typed_call_receivers maps the `.get` ordinal to "HttpClient", so + // the bound-receiver call surfaces as an outbound HTTP node even + // though `requests.get` is the only direct-import rule above). + ClientRule { leaf: "HttpClient.get", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.post", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.put", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.delete", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.patch", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.request", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.head", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "HttpClient.options", kind: ExternalServiceKind::HttpApi, label: "HTTP client" }, + ClientRule { leaf: "RequestBuilder.send", kind: ExternalServiceKind::HttpApi, label: "HTTP request builder" }, + ClientRule { leaf: "URL.openConnection", kind: ExternalServiceKind::HttpApi, label: "URL connection" }, + ClientRule { leaf: "URL.openStream", kind: ExternalServiceKind::HttpApi, label: "URL connection" }, ]; +/// Walk every function summary's callee list and emit one +/// [`SurfaceNode::ExternalService`] per matched outbound-client call. +/// +/// When the bare callee name does not hit a rule, the type-fact engine's +/// per-call `typed_call_receivers` map (read off the matching +/// [`crate::summary::SsaFuncSummary`]) is consulted: a callee whose +/// receiver was resolved to `TypeKind::HttpClient` / +/// `TypeKind::RequestBuilder` / `TypeKind::Url` is retried under the +/// type-qualified name `"{container}."`, picking up the +/// bound-receiver call shapes (`client = requests.Session(); +/// client.get(url)`) that the name-only matcher misses. pub fn detect_external_services(summaries: &GlobalSummaries) -> Vec { let mut out: Vec = Vec::new(); let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new(); - for (_key, summary) in summaries.iter() { + for (key, summary) in summaries.iter() { + let typed = summaries.get_ssa(key).map(|s| s.typed_call_receivers.as_slice()); for callee in &summary.callees { - let Some(rule) = match_rule(&callee.name) else { - continue; - }; + let rule = match_rule(&callee.name).or_else(|| { + typed + .and_then(|t| container_for_ordinal(t, callee.ordinal)) + .and_then(|c| match_rule(&qualify(c, &callee.name))) + }); + let Some(rule) = rule else { continue }; let location = call_site_location(summary, Some(callee)); if !seen.insert((location.file.clone(), rule.label.to_string())) { continue; @@ -118,6 +151,19 @@ pub fn detect_external_services(summaries: &GlobalSummaries) -> Vec out } +fn leaf_segment(name: &str) -> &str { + let after_colon = name.rsplit("::").next().unwrap_or(name); + after_colon.rsplit('.').next().unwrap_or(after_colon) +} + +fn qualify(container: &str, callee_name: &str) -> String { + format!("{}.{}", container, leaf_segment(callee_name)) +} + +fn container_for_ordinal(typed: &[(u32, String)], ordinal: u32) -> Option<&str> { + typed.iter().find(|(o, _)| *o == ordinal).map(|(_, c)| c.as_str()) +} + fn match_rule(callee: &str) -> Option<&'static ClientRule> { let cl = callee.trim().to_ascii_lowercase(); let cl_segments = cl.replace("::", "."); @@ -195,6 +241,41 @@ mod tests { assert!(nodes.is_empty(), "bare rules FP-matched on {nodes:?}"); } + #[test] + fn typed_receiver_http_client_resolves_bound_session_get() { + // `client = requests.Session(); client.get(url)` — the bare + // callee `client.get` is not in CLIENT_RULES, but the SSA type + // engine resolves the receiver to `TypeKind::HttpClient`. The + // detector retries under `HttpClient.get` and emits an HTTP + // external-service node. + use crate::summary::ssa_summary::SsaFuncSummary; + let mut gs = GlobalSummaries::new(); + let key = FuncKey::new_function(Lang::Python, "client.py", "fetch", None); + let summary = FuncSummary { + name: "fetch".into(), + file_path: "client.py".into(), + lang: "python".into(), + param_count: 0, + callees: vec![{ + let mut c = CalleeSite::bare("client.get"); + c.ordinal = 3; + c.span = Some((9, 5)); + c + }], + ..Default::default() + }; + gs.insert(key.clone(), summary); + let mut ssa = SsaFuncSummary::default(); + ssa.typed_call_receivers.push((3, "HttpClient".into())); + gs.insert_ssa(key, ssa); + let nodes = detect_external_services(&gs); + assert_eq!(nodes.len(), 1, "expected typed retry to hit; got {nodes:?}"); + let SurfaceNode::ExternalService(es) = &nodes[0] else { + panic!() + }; + assert_eq!(es.label, "HTTP client"); + } + #[test] fn bare_got_rule_matches_segmented_callee() { let mut gs = GlobalSummaries::new(); diff --git a/src/surface/lang/go_gin.rs b/src/surface/lang/go_gin.rs index 566e3bdf..a2614964 100644 --- a/src/surface/lang/go_gin.rs +++ b/src/surface/lang/go_gin.rs @@ -18,16 +18,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_MIDDLEWARES: &[&str] = &[ - "AuthRequired", - "JWT", - "JWTAuth", - "Auth", - "RequireAuth", - "RequireUser", - "VerifyToken", - "BasicAuth", -]; +pub use crate::auth_analysis::auth_markers::GIN_MIDDLEWARES as AUTH_MIDDLEWARES; const VERBS: &[&str] = &[ "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "Any", diff --git a/src/surface/lang/java_quarkus.rs b/src/surface/lang/java_quarkus.rs index 445b4a74..4439eb63 100644 --- a/src/surface/lang/java_quarkus.rs +++ b/src/surface/lang/java_quarkus.rs @@ -21,12 +21,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_ANNOTATIONS: &[&str] = &[ - "Authenticated", - "RolesAllowed", - "DenyAll", - "RequiresAuthentication", -]; +pub use crate::auth_analysis::auth_markers::QUARKUS_ANNOTATIONS as AUTH_ANNOTATIONS; const QUARKUS_DI: &[&str] = &[ "ApplicationScoped", diff --git a/src/surface/lang/java_servlet.rs b/src/surface/lang/java_servlet.rs index d3dced74..1a48e42a 100644 --- a/src/surface/lang/java_servlet.rs +++ b/src/surface/lang/java_servlet.rs @@ -18,12 +18,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_ANNOTATIONS: &[&str] = &[ - "RolesAllowed", - "DenyAll", - "RequiresAuthentication", - "RequiresUser", -]; +pub use crate::auth_analysis::auth_markers::SERVLET_ANNOTATIONS as AUTH_ANNOTATIONS; const SERVLET_VERBS: &[(&str, HttpMethod)] = &[ ("doGet", HttpMethod::GET), diff --git a/src/surface/lang/java_spring.rs b/src/surface/lang/java_spring.rs index 5018ea72..9d85379a 100644 --- a/src/surface/lang/java_spring.rs +++ b/src/surface/lang/java_spring.rs @@ -16,13 +16,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_ANNOTATIONS: &[&str] = &[ - "PreAuthorize", - "PostAuthorize", - "Secured", - "RolesAllowed", - "AuthenticationPrincipal", -]; +pub use crate::auth_analysis::auth_markers::SPRING_ANNOTATIONS as AUTH_ANNOTATIONS; const MAPPING_ANNOTATIONS: &[(&str, Option)] = &[ ("RequestMapping", None), diff --git a/src/surface/lang/js_express.rs b/src/surface/lang/js_express.rs index 7a76d956..725891a5 100644 --- a/src/surface/lang/js_express.rs +++ b/src/surface/lang/js_express.rs @@ -17,20 +17,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_MIDDLEWARES: &[&str] = &[ - "requireAuth", - "requireUser", - "isAuthenticated", - "ensureAuthenticated", - "ensureLoggedIn", - "authenticate", - "authMiddleware", - "verifyToken", - "verifyJwt", - "checkJwt", - "passport", - "jwt", -]; +pub use crate::auth_analysis::auth_markers::EXPRESS_MIDDLEWARES as AUTH_MIDDLEWARES; const VERBS: &[&str] = &[ "get", "post", "put", "delete", "patch", "options", "head", "all", diff --git a/src/surface/lang/js_koa.rs b/src/surface/lang/js_koa.rs index faf25a31..e4a238d4 100644 --- a/src/surface/lang/js_koa.rs +++ b/src/surface/lang/js_koa.rs @@ -15,20 +15,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_MIDDLEWARES: &[&str] = &[ - "requireAuth", - "requireUser", - "isAuthenticated", - "ensureAuthenticated", - "authenticate", - "authMiddleware", - "verifyToken", - "verifyJwt", - "checkJwt", - "passport", - "jwt", - "koaJwt", -]; +pub use crate::auth_analysis::auth_markers::KOA_MIDDLEWARES as AUTH_MIDDLEWARES; const VERBS: &[&str] = &[ "get", "post", "put", "delete", "patch", "options", "head", "all", diff --git a/src/surface/lang/python_django.rs b/src/surface/lang/python_django.rs index e6d82b43..c81226b4 100644 --- a/src/surface/lang/python_django.rs +++ b/src/surface/lang/python_django.rs @@ -26,15 +26,7 @@ use std::collections::HashMap; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_DECORATORS: &[&str] = &[ - "login_required", - "permission_required", - "user_passes_test", - "staff_member_required", - "csrf_protect", - "require_authenticated", - "auth_required", -]; +pub use crate::auth_analysis::auth_markers::DJANGO_DECORATORS as AUTH_DECORATORS; const CBV_BASES: &[&str] = &[ "View", diff --git a/src/surface/lang/python_fastapi.rs b/src/surface/lang/python_fastapi.rs index f574658b..1b39765c 100644 --- a/src/surface/lang/python_fastapi.rs +++ b/src/surface/lang/python_fastapi.rs @@ -21,17 +21,7 @@ use tree_sitter::{Node, Tree}; /// Auth markers recognised in the decorator stack. FastAPI's primary /// auth idiom is `Depends(...)` parameter injection, handled separately. -pub const AUTH_DECORATORS: &[&str] = &[ - "login_required", - "auth_required", - "jwt_required", - "token_required", - "requires_auth", - "authenticated", - "require_auth", - "require_login", - "current_user", -]; +pub use crate::auth_analysis::auth_markers::FASTAPI_DECORATORS as AUTH_DECORATORS; /// Auth-callee names recognised inside a `Depends(...)` parameter. const AUTH_DEPENDS_CALLEES: &[&str] = &[ diff --git a/src/surface/lang/python_flask.rs b/src/surface/lang/python_flask.rs index d4defef7..acfb3b05 100644 --- a/src/surface/lang/python_flask.rs +++ b/src/surface/lang/python_flask.rs @@ -28,15 +28,7 @@ use tree_sitter::{Node, Tree}; /// last `attribute` / `identifier` segment — so `@login_required`, /// `@auth.login_required`, and `@flask_login.login_required` all /// match. Match is case-insensitive on the underscored form. -pub const AUTH_DECORATORS: &[&str] = &[ - "login_required", - "auth_required", - "jwt_required", - "token_required", - "requires_auth", - "authenticated", - "require_login", -]; +pub use crate::auth_analysis::auth_markers::FLASK_DECORATORS as AUTH_DECORATORS; /// Detect every Flask route in a parsed Python file. /// diff --git a/src/surface/lang/rust_actix.rs b/src/surface/lang/rust_actix.rs index 382b8bd2..13a6f802 100644 --- a/src/surface/lang/rust_actix.rs +++ b/src/surface/lang/rust_actix.rs @@ -16,14 +16,7 @@ use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode}; use std::path::Path; use tree_sitter::{Node, Tree}; -pub const AUTH_EXTRACTORS: &[&str] = &[ - "Identity", - "BearerAuth", - "BasicAuth", - "JwtClaims", - "Authenticated", - "User", -]; +pub use crate::auth_analysis::auth_markers::ACTIX_EXTRACTORS as AUTH_EXTRACTORS; const ROUTE_MACROS: &[(&str, Option)] = &[ ("get", Some(HttpMethod::GET)), diff --git a/src/surface/lang/rust_axum.rs b/src/surface/lang/rust_axum.rs index 715d72db..6113f390 100644 --- a/src/surface/lang/rust_axum.rs +++ b/src/surface/lang/rust_axum.rs @@ -25,13 +25,7 @@ const VERBS: &[(&str, HttpMethod)] = &[ ("options", HttpMethod::OPTIONS), ]; -pub const AUTH_EXTRACTORS: &[&str] = &[ - "Extension) -> VerifyRe attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs index 4e776714..a8e48e29 100644 --- a/tests/common/fixture_harness.rs +++ b/tests/common/fixture_harness.rs @@ -515,6 +515,8 @@ pub fn run_shape_fixture_lang( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, } } Err(RunError::NoPayloadsForCap) => VerifyResult { @@ -527,6 +529,8 @@ pub fn run_shape_fixture_lang( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, Err(e) => VerifyResult { finding_id: spec.finding_id.clone(), @@ -538,6 +542,8 @@ pub fn run_shape_fixture_lang( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, } } diff --git a/tests/console_snapshot.rs b/tests/console_snapshot.rs index 54a46b11..69dbdd55 100644 --- a/tests/console_snapshot.rs +++ b/tests/console_snapshot.rs @@ -72,6 +72,8 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }, VerifyStatus::NotConfirmed => VerifyResult { finding_id: "abc123".into(), @@ -89,6 +91,8 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }, VerifyStatus::Unsupported => VerifyResult { finding_id: "abc123".into(), @@ -100,6 +104,8 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, VerifyStatus::Inconclusive => VerifyResult { finding_id: "abc123".into(), @@ -111,6 +117,8 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }, }; diff --git a/tests/fix_validation_e2e.rs b/tests/fix_validation_e2e.rs index 0b38442b..6d20f186 100644 --- a/tests/fix_validation_e2e.rs +++ b/tests/fix_validation_e2e.rs @@ -53,6 +53,8 @@ fn set_verdict( attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }); } } @@ -166,6 +168,8 @@ fn new_confirmed_fails_no_new_confirmed_gate() { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }); } } diff --git a/tests/go_fixtures.rs b/tests/go_fixtures.rs index b2c0627e..8bd993fa 100644 --- a/tests/go_fixtures.rs +++ b/tests/go_fixtures.rs @@ -59,6 +59,8 @@ mod go_fixture_tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } diff --git a/tests/java_fixtures.rs b/tests/java_fixtures.rs index e1c60f52..97d1e84a 100644 --- a/tests/java_fixtures.rs +++ b/tests/java_fixtures.rs @@ -67,6 +67,8 @@ mod java_fixture_tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } diff --git a/tests/js_fixtures.rs b/tests/js_fixtures.rs index fac4591e..db9120a8 100644 --- a/tests/js_fixtures.rs +++ b/tests/js_fixtures.rs @@ -60,6 +60,8 @@ mod js_fixture_tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } diff --git a/tests/json_snapshot.rs b/tests/json_snapshot.rs index 79043011..e2e182d0 100644 --- a/tests/json_snapshot.rs +++ b/tests/json_snapshot.rs @@ -58,6 +58,8 @@ fn json_dynamic_verdict_confirmed_serialises_correctly() { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }), ..Default::default() }); @@ -96,6 +98,8 @@ fn json_dynamic_verdict_not_confirmed_serialises_correctly() { attempts: vec![], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }), ..Default::default() }); @@ -159,6 +163,8 @@ fn json_unsupported_verdict_has_reason() { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }), ..Default::default() }); diff --git a/tests/php_fixtures.rs b/tests/php_fixtures.rs index 4f62fa99..6058f26b 100644 --- a/tests/php_fixtures.rs +++ b/tests/php_fixtures.rs @@ -59,6 +59,8 @@ mod php_fixture_tests { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; } diff --git a/tests/repro_determinism.rs b/tests/repro_determinism.rs index 5590cf16..3a197ed8 100644 --- a/tests/repro_determinism.rs +++ b/tests/repro_determinism.rs @@ -68,6 +68,8 @@ mod repro_determinism_tests { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, } } diff --git a/tests/repro_hermetic.rs b/tests/repro_hermetic.rs index df9bc982..d1dbab35 100644 --- a/tests/repro_hermetic.rs +++ b/tests/repro_hermetic.rs @@ -87,6 +87,8 @@ mod repro_hermetic_tests { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, } } diff --git a/tests/sarif_dynamic_verdict_tests.rs b/tests/sarif_dynamic_verdict_tests.rs index d67914ba..ccc98293 100644 --- a/tests/sarif_dynamic_verdict_tests.rs +++ b/tests/sarif_dynamic_verdict_tests.rs @@ -74,6 +74,8 @@ fn sarif_confirmed_verdict_sets_partial_fingerprint() { }], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -107,6 +109,8 @@ fn sarif_not_confirmed_verdict_sets_partial_fingerprint() { attempts: vec![], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -134,6 +138,8 @@ fn sarif_unsupported_verdict_sets_partial_fingerprint() { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -166,6 +172,8 @@ fn sarif_inconclusive_verdict_sets_partial_fingerprint() { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -214,6 +222,8 @@ fn sarif_confirmed_verdict_nyx_dynamic_verdict_contains_triggered_payload() { attempts: vec![], toolchain_match: Some("exact".into()), differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict)); @@ -245,6 +255,8 @@ fn sarif_all_four_statuses_produce_partial_fingerprint() { attempts: vec![], toolchain_match: None, differential: None, + replay_stable: None, + wrong: None, }; let result = sarif_result(diag_with_verdict(verdict));