diff --git a/benches/dynamic_bench.rs b/benches/dynamic_bench.rs index 5c74a342..34fec934 100644 --- a/benches/dynamic_bench.rs +++ b/benches/dynamic_bench.rs @@ -129,7 +129,10 @@ fn bench_sandbox_run_payload(c: &mut Criterion) { let spec = make_sqli_spec(); let harness = harness::build(&spec).expect("harness build"); let payloads = payloads_for(Cap::SQL_QUERY); - let payload = payloads.iter().find(|p| !p.is_benign).expect("sqli payload"); + let payload = payloads + .iter() + .find(|p| !p.is_benign) + .expect("sqli payload"); let opts = SandboxOptions { timeout: std::time::Duration::from_secs(10), ..SandboxOptions::default() @@ -192,10 +195,19 @@ fn bench_docker_exec_warm(c: &mut Criterion) { let container = "nyx-bench-exec-warm"; let _ = std::process::Command::new("docker") .args([ - "run", "-d", "--rm", "--name", container, - "--cap-drop=ALL", "--security-opt", "no-new-privileges:true", - "--network", "none", - "python:3-slim", "sleep", "300", + "run", + "-d", + "--rm", + "--name", + container, + "--cap-drop=ALL", + "--security-opt", + "no-new-privileges:true", + "--network", + "none", + "python:3-slim", + "sleep", + "300", ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) @@ -239,7 +251,10 @@ fn bench_docker_payload_cost(c: &mut Criterion) { let spec = make_sqli_spec(); let built = harness::build(&spec).expect("harness build"); let payloads = payloads_for(Cap::SQL_QUERY); - let payload = payloads.iter().find(|p| !p.is_benign).expect("sqli payload"); + let payload = payloads + .iter() + .find(|p| !p.is_benign) + .expect("sqli payload"); let opts = SandboxOptions { timeout: std::time::Duration::from_secs(30), backend: SandboxBackend::Docker, diff --git a/build.rs b/build.rs index 3e1efb4b..54959ac3 100644 --- a/build.rs +++ b/build.rs @@ -154,10 +154,12 @@ fn emit_seccomp_policy() { .iter() .find(|(n, _)| *n == cap_name.as_str()) .map(|(_, b)| *b) - .unwrap_or_else(|| panic!( - "seccomp_policy.toml references unknown Cap '{cap_name}' — \ + .unwrap_or_else(|| { + panic!( + "seccomp_policy.toml references unknown Cap '{cap_name}' — \ add it to CAP_BIT_FOR_NAME in build.rs first" - )); + ) + }); out.push_str(&format!(" (0x{bit:08x}_u32, &[\n")); for name in allow { out.push_str(&format!(" \"{}\",\n", escape(name))); @@ -335,7 +337,9 @@ fn emit_image_digests() { out.push_str("// generated by build.rs from tools/image-builder/images.toml — do not edit\n\n"); // IMAGE_DIGESTS: only entries with a non-empty digest survive. - out.push_str("pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = phf::phf_map! {\n"); + out.push_str( + "pub static IMAGE_DIGESTS: phf::Map<&'static str, &'static str> = phf::phf_map! {\n", + ); for e in &entries { if e.digest.is_empty() { continue; @@ -351,7 +355,9 @@ fn emit_image_digests() { // IMAGE_BASES: every entry, digest stripped. Used by docker.rs when no // digest is pinned yet so a `docker pull ` is still possible. - out.push_str("pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = phf::phf_map! {\n"); + out.push_str( + "pub static IMAGE_BASES: phf::Map<&'static str, &'static str> = phf::phf_map! {\n", + ); for e in &entries { out.push_str(&format!( " \"{}\" => \"{}\",\n", @@ -404,8 +410,12 @@ fn parse_image_catalogue(src: &str) -> Vec { continue; } - let Some(slot) = current.as_mut() else { continue }; - let Some((key, value)) = line.split_once('=') else { continue }; + let Some(slot) = current.as_mut() else { + continue; + }; + let Some((key, value)) = line.split_once('=') else { + continue; + }; let key = key.trim(); let value = value.trim().trim_matches('"').trim_matches('\''); match key { diff --git a/src/auth_analysis/auth_markers.rs b/src/auth_analysis/auth_markers.rs index d38e09b7..2fb66312 100644 --- a/src/auth_analysis/auth_markers.rs +++ b/src/auth_analysis/auth_markers.rs @@ -236,9 +236,18 @@ mod tests { #[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")); + 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] diff --git a/src/baseline.rs b/src/baseline.rs index b74bee5a..1bf8ceef 100644 --- a/src/baseline.rs +++ b/src/baseline.rs @@ -147,23 +147,20 @@ pub fn diags_to_baseline_entries(diags: &[Diag]) -> Vec { /// `path`, and `rule_id` — no source code snippets or flow steps. pub fn write_baseline(path: &Path, diags: &[Diag]) -> crate::errors::NyxResult<()> { let entries = diags_to_baseline_entries(diags); - let json = serde_json::to_string_pretty(&entries).map_err(|e| { - crate::errors::NyxError::Msg(format!("baseline serialize error: {e}")) - })?; + let json = serde_json::to_string_pretty(&entries) + .map_err(|e| crate::errors::NyxError::Msg(format!("baseline serialize error: {e}")))?; if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() { - std::fs::create_dir_all(parent).map_err(|e| { - crate::errors::NyxError::Msg(format!( - "cannot create baseline dir {}: {e}", - parent.display() - )) - })?; - } + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent).map_err(|e| { + crate::errors::NyxError::Msg(format!( + "cannot create baseline dir {}: {e}", + parent.display() + )) + })?; + } std::fs::write(path, json).map_err(|e| { - crate::errors::NyxError::Msg(format!( - "cannot write baseline {}: {e}", - path.display() - )) + crate::errors::NyxError::Msg(format!("cannot write baseline {}: {e}", path.display())) }) } @@ -183,9 +180,7 @@ fn classify_transition( Transition::FlippedNotConfirmed } // NotConfirmed → Confirmed: regression - (Some(VerifyStatus::NotConfirmed), Some(VerifyStatus::Confirmed)) => { - Transition::Regressed - } + (Some(VerifyStatus::NotConfirmed), Some(VerifyStatus::Confirmed)) => Transition::Regressed, // None / Inconclusive / Unsupported → Confirmed (_, Some(VerifyStatus::Confirmed)) => Transition::FlippedConfirmed, // Everything else: treat as unchanged (e.g. Confirmed → Inconclusive @@ -380,9 +375,7 @@ pub fn format_diff_console(diff: &VerdictDiff) -> String { } Transition::FlippedConfirmed => { non_unchanged += 1; - lines.push(format!( - " + {hash_str}: new Confirmed at {loc}" - )); + lines.push(format!(" + {hash_str}: new Confirmed at {loc}")); } Transition::Unchanged => {} } @@ -402,7 +395,7 @@ pub fn format_diff_console(diff: &VerdictDiff) -> String { #[cfg(test)] mod tests { use super::*; - use crate::commands::scan::{compute_stable_hash, Diag}; + use crate::commands::scan::{Diag, compute_stable_hash}; use crate::evidence::{Evidence, VerifyResult, VerifyStatus}; use crate::patterns::{FindingCategory, Severity}; @@ -471,7 +464,10 @@ mod tests { )]; let diff = compute_verdict_diff(&[], ¤t); assert_eq!(diff.entries[0].transition, Transition::New); - assert_eq!(diff.entries[0].current_status, Some(VerifyStatus::Confirmed)); + assert_eq!( + diff.entries[0].current_status, + Some(VerifyStatus::Confirmed) + ); } #[test] @@ -620,7 +616,10 @@ mod tests { let tmp = tempfile::NamedTempFile::new().unwrap(); write_baseline(tmp.path(), &[d]).unwrap(); let content = std::fs::read_to_string(tmp.path()).unwrap(); - assert!(!content.contains("SECRET CODE"), "baseline must not contain source code"); + assert!( + !content.contains("SECRET CODE"), + "baseline must not contain source code" + ); } #[test] diff --git a/src/callgraph.rs b/src/callgraph.rs index 884b3ace..1b2ebaab 100644 --- a/src/callgraph.rs +++ b/src/callgraph.rs @@ -259,7 +259,6 @@ impl ClassMethodIndex { .unwrap_or_default(), } } - } // ── Type hierarchy index ──────────────────────────────────────────────── @@ -955,10 +954,9 @@ impl FileReachMap { fn normalize<'a>(&self, path: &'a str) -> std::borrow::Cow<'a, str> { match self.scan_root.as_deref() { - Some(root) => std::borrow::Cow::Owned(crate::symbol::normalize_namespace( - path, - Some(root), - )), + Some(root) => { + std::borrow::Cow::Owned(crate::symbol::normalize_namespace(path, Some(root))) + } None => std::borrow::Cow::Borrowed(path), } } @@ -2926,7 +2924,10 @@ mod tests { let transitive = callers_transitive(&cg, &sink_key); let caller_names: std::collections::HashSet = transitive.iter().map(|k| k.name.clone()).collect(); - assert!(caller_names.contains("process"), "process should reach sink"); + assert!( + caller_names.contains("process"), + "process should reach sink" + ); assert!(caller_names.contains("handle"), "handle should reach sink"); assert_eq!(transitive.len(), 2, "sink itself must be excluded"); diff --git a/src/chain/edges.rs b/src/chain/edges.rs index cd0c8d92..cf2da89b 100644 --- a/src/chain/edges.rs +++ b/src/chain/edges.rs @@ -182,30 +182,28 @@ pub fn pick_chain_cap(bits: u32) -> Option { while remaining != 0 { let bit = 1u32 << remaining.trailing_zeros(); if let Some(cap) = Cap::from_bits(bit) - && lookup_impact(cap, None).is_some() { - return Some(cap); - } + && lookup_impact(cap, None).is_some() + { + return Some(cap); + } remaining &= !bit; } lowest_cap(bits) } -fn locate_reach( - loc: &SourceLocation, - surface: &SurfaceMap, - reach: Option<&FileReachMap>, -) -> Reach { +fn locate_reach(loc: &SourceLocation, surface: &SurfaceMap, reach: Option<&FileReachMap>) -> Reach { // Pass 1: file-local match (legacy behaviour, always applies). for node in &surface.nodes { if let SurfaceNode::EntryPoint(ep) = node - && ep.handler_location.file == loc.file { - return Reach::Reachable { - location: ep.location.clone(), - method: ep.method, - route: ep.route.clone(), - auth_required: ep.auth_required, - }; - } + && ep.handler_location.file == loc.file + { + return Reach::Reachable { + location: ep.location.clone(), + method: ep.method, + route: ep.route.clone(), + auth_required: ep.auth_required, + }; + } } // Pass 2: transitive caller match via the call graph. Only fires // when `reach` is supplied — keeps the legacy file-local behaviour @@ -213,14 +211,15 @@ fn locate_reach( if let Some(reach) = reach { for node in &surface.nodes { if let SurfaceNode::EntryPoint(ep) = node - && reach.reaches(&ep.handler_location.file, &loc.file) { - return Reach::Reachable { - location: ep.location.clone(), - method: ep.method, - route: ep.route.clone(), - auth_required: ep.auth_required, - }; - } + && reach.reaches(&ep.handler_location.file, &loc.file) + { + return Reach::Reachable { + location: ep.location.clone(), + method: ep.method, + route: ep.route.clone(), + auth_required: ep.auth_required, + }; + } } } Reach::Unreachable diff --git a/src/chain/feasibility.rs b/src/chain/feasibility.rs index 63da9be1..8c1599cd 100644 --- a/src/chain/feasibility.rs +++ b/src/chain/feasibility.rs @@ -69,7 +69,10 @@ impl Feasibility { /// in the doc's table can fire. Phase 25's scoring pass uses this /// flavour. pub fn for_finding(diag: &Diag) -> Feasibility { - let verdict = diag.evidence.as_ref().and_then(|e| e.dynamic_verdict.as_ref()); + let verdict = diag + .evidence + .as_ref() + .and_then(|e| e.dynamic_verdict.as_ref()); Self::bucket_from_verdict(verdict, diag.confidence) } @@ -82,9 +85,7 @@ impl Feasibility { ) -> Feasibility { match verdict.map(|v| v.status) { Some(VerifyStatus::Confirmed) => Feasibility::Confirmed, - Some(VerifyStatus::Inconclusive) - if static_confidence == Some(Confidence::High) => - { + Some(VerifyStatus::Inconclusive) if static_confidence == Some(Confidence::High) => { Feasibility::InconclusiveHighConf } _ => Feasibility::Unverified, diff --git a/src/chain/finding.rs b/src/chain/finding.rs index 9ad49e87..e8b1ccc1 100644 --- a/src/chain/finding.rs +++ b/src/chain/finding.rs @@ -210,23 +210,14 @@ mod tests { #[test] fn stable_hash_changes_with_member_order() { - let a = ChainFinding::compute_stable_hash( - &[member(1), member(2)], - ImpactCategory::Rce, - ); - let b = ChainFinding::compute_stable_hash( - &[member(2), member(1)], - ImpactCategory::Rce, - ); + let a = ChainFinding::compute_stable_hash(&[member(1), member(2)], ImpactCategory::Rce); + let b = ChainFinding::compute_stable_hash(&[member(2), member(1)], ImpactCategory::Rce); assert_ne!(a, b); } #[test] fn stable_hash_changes_with_impact() { - let a = ChainFinding::compute_stable_hash( - &[member(1), member(2)], - ImpactCategory::Rce, - ); + let a = ChainFinding::compute_stable_hash(&[member(1), member(2)], ImpactCategory::Rce); let b = ChainFinding::compute_stable_hash( &[member(1), member(2)], ImpactCategory::BrowserToLocalRce, diff --git a/src/chain/impact.rs b/src/chain/impact.rs index bf6c1f10..351e9653 100644 --- a/src/chain/impact.rs +++ b/src/chain/impact.rs @@ -250,9 +250,10 @@ pub fn lookup_impact(source: Cap, adjacent: Option) -> Option build_errors += 1, @@ -400,7 +400,11 @@ fn run_chain_steps( let mut prev_output: Option> = None; let last_idx = built_steps.len().saturating_sub(1); for (idx, (workdir, spec)) in built_steps.iter().enumerate() { - let step_terminal = if idx == last_idx { Some(terminal) } else { None }; + let step_terminal = if idx == last_idx { + Some(terminal) + } else { + None + }; let step = lang::compose_chain_step(spec.lang, prev_output.as_deref(), step_terminal); let step_path = workdir.join(&step.filename); @@ -459,7 +463,13 @@ fn run_chain_steps( } } } - (steps_run, sandbox_errors, steps_timeout, nonzero_exits, final_sink_hit) + ( + steps_run, + sandbox_errors, + steps_timeout, + nonzero_exits, + final_sink_hit, + ) } /// Phase 26 — Track G.3: drive composite dynamic re-verification for @@ -472,7 +482,13 @@ pub fn reverify_chain( surface: &SurfaceMap, opts: &VerifyOptions, ) -> ChainReverifyResult { - reverify_chain_with(chain, member_diags, surface, opts, &DefaultCompositeReverifier) + reverify_chain_with( + chain, + member_diags, + surface, + opts, + &DefaultCompositeReverifier, + ) } /// Inject-the-reverifier flavour of [`reverify_chain`]. @@ -630,7 +646,10 @@ mod tests { assert!(!result.was_downgraded()); assert_eq!(result.severity_after, ChainSeverity::Critical); assert_eq!(chain.severity, ChainSeverity::Critical); - assert_eq!(chain.dynamic_verdict.as_ref().unwrap().status, VerifyStatus::Confirmed); + assert_eq!( + chain.dynamic_verdict.as_ref().unwrap().status, + VerifyStatus::Confirmed + ); assert!(chain.reverify_reason.is_none()); } @@ -690,7 +709,10 @@ mod tests { ); assert!(results.is_empty()); for c in &chains { - assert!(c.dynamic_verdict.is_none(), "no verdict attached when top_n=0"); + assert!( + c.dynamic_verdict.is_none(), + "no verdict attached when top_n=0" + ); } } diff --git a/src/chain/score.rs b/src/chain/score.rs index 5e64ed7e..bd310574 100644 --- a/src/chain/score.rs +++ b/src/chain/score.rs @@ -178,8 +178,13 @@ mod tests { #[test] fn category_weights_strictly_ordered() { - assert!(category_weight(ImpactCategory::BrowserToLocalRce) > category_weight(ImpactCategory::Rce)); - assert!(category_weight(ImpactCategory::Rce) > category_weight(ImpactCategory::SessionHijack)); + assert!( + category_weight(ImpactCategory::BrowserToLocalRce) + > category_weight(ImpactCategory::Rce) + ); + assert!( + category_weight(ImpactCategory::Rce) > category_weight(ImpactCategory::SessionHijack) + ); assert!( category_weight(ImpactCategory::SessionHijack) > category_weight(ImpactCategory::InternalNetworkAccess) diff --git a/src/chain/search.rs b/src/chain/search.rs index 7f764115..9ab7fb22 100644 --- a/src/chain/search.rs +++ b/src/chain/search.rs @@ -120,8 +120,16 @@ pub fn find_chains_with_reach( .filter(|e| edge_reaches_entry(e, entry, reach)) .collect(); candidates.sort_by(|a, b| { - (a.finding.stable_hash, &a.finding.rule_id, &a.finding.location) - .cmp(&(b.finding.stable_hash, &b.finding.rule_id, &b.finding.location)) + ( + a.finding.stable_hash, + &a.finding.rule_id, + &a.finding.location, + ) + .cmp(&( + b.finding.stable_hash, + &b.finding.rule_id, + &b.finding.location, + )) }); for sink in &sinks { // Scope candidates to the sink: same-file match (legacy), @@ -139,13 +147,9 @@ pub fn find_chains_with_reach( }) .copied() .collect(); - if let Some(chain) = compose_chain( - entry, - sink, - &scoped, - cfg.max_depth, - local_listener_present, - ) && chain.score >= cfg.min_score + if let Some(chain) = + compose_chain(entry, sink, &scoped, cfg.max_depth, local_listener_present) + && chain.score >= cfg.min_score { chains.push(chain); } @@ -201,15 +205,9 @@ fn is_loopback_label(s: &str) -> bool { || lower.contains("://localhost") } -fn edge_reaches_entry( - edge: &ChainEdge, - entry: &EntryPoint, - reach: Option<&FileReachMap>, -) -> bool { +fn edge_reaches_entry(edge: &ChainEdge, entry: &EntryPoint, reach: Option<&FileReachMap>) -> bool { let route_method_match = match &edge.reach { - Reach::Reachable { route, method, .. } => { - *route == entry.route && *method == entry.method - } + Reach::Reachable { route, method, .. } => *route == entry.route && *method == entry.method, Reach::Unreachable => return false, }; if !route_method_match { @@ -265,8 +263,7 @@ fn compose_chain( let bound = scoped.len().min(max_depth); let path: Vec<&ChainEdge> = scoped[..bound].to_vec(); let sink_cap = sole_cap(sink.cap_bits)?; - let (impact, member_impacts) = - resolve_impact(&path, sink_cap, entry, local_listener_present)?; + let (impact, member_impacts) = resolve_impact(&path, sink_cap, entry, local_listener_present)?; let mut chain = build_chain(entry, sink, &path, impact, &member_impacts); // SSRF + LocalListener refinement (Phase 24 deferred close): when // the implied impact is `InternalNetworkAccess` AND the SurfaceMap @@ -394,9 +391,7 @@ fn build_chain( /// member edge has `Feasibility::Confirmed` the composite verdict /// inherits that confirmation; otherwise `None` (Phase 26 will run a /// real composite re-verification pass). -fn composite_dynamic_verdict( - _path: &[ChainEdge], -) -> Option { +fn composite_dynamic_verdict(_path: &[ChainEdge]) -> Option { None } @@ -649,7 +644,9 @@ mod tests { ) }; let mut surface_no_listener = SurfaceMap::new(); - surface_no_listener.nodes.push(entry("app.py", "/fetch", false)); + surface_no_listener + .nodes + .push(entry("app.py", "/fetch", false)); surface_no_listener .nodes .push(sink("app.py", 20, "requests.get", Cap::SSRF)); @@ -662,7 +659,10 @@ mod tests { }, ); assert_eq!(baseline.len(), 1); - assert_eq!(baseline[0].implied_impact, ImpactCategory::InternalNetworkAccess); + assert_eq!( + baseline[0].implied_impact, + ImpactCategory::InternalNetworkAccess + ); let mut surface_with_listener = surface_no_listener.clone(); surface_with_listener @@ -681,7 +681,10 @@ mod tests { }, ); assert_eq!(boosted.len(), 1); - assert_eq!(boosted[0].implied_impact, ImpactCategory::InternalNetworkAccess); + assert_eq!( + boosted[0].implied_impact, + ImpactCategory::InternalNetworkAccess + ); let ratio = boosted[0].score / baseline[0].score; assert!( (ratio - LOCAL_LISTENER_BOOST).abs() < 1e-9, @@ -693,9 +696,7 @@ mod tests { fn score_threshold_drops_low_score_chains() { let mut surface = SurfaceMap::new(); surface.nodes.push(entry("app.py", "/r", false)); - surface - .nodes - .push(sink("app.py", 20, "open", Cap::FILE_IO)); + surface.nodes.push(sink("app.py", 20, "open", Cap::FILE_IO)); let e = edge_with( "app.py", 10, @@ -724,12 +725,9 @@ mod tests { surface.nodes.push(entry("routes.py", "/exec", false)); // Sink lives in a helper file the entry handler transitively // reaches, not the entry file itself. - surface.nodes.push(sink( - "helper.py", - 20, - "os.system", - Cap::CODE_EXEC, - )); + surface + .nodes + .push(sink("helper.py", 20, "os.system", Cap::CODE_EXEC)); let e = edge_with( "routes.py", 10, @@ -798,15 +796,9 @@ mod tests { surface.nodes.push(entry("a.js", "/run", false)); surface.nodes.push(entry("b.js", "/run", false)); surface.nodes.push(entry("c.py", "/run", false)); - surface - .nodes - .push(sink("a.js", 7, "eval", Cap::CODE_EXEC)); - surface - .nodes - .push(sink("b.js", 7, "eval", Cap::CODE_EXEC)); - surface - .nodes - .push(sink("c.py", 7, "eval", Cap::CODE_EXEC)); + surface.nodes.push(sink("a.js", 7, "eval", Cap::CODE_EXEC)); + surface.nodes.push(sink("b.js", 7, "eval", Cap::CODE_EXEC)); + surface.nodes.push(sink("c.py", 7, "eval", Cap::CODE_EXEC)); let edges = vec![ edge_with( "a.js", @@ -845,7 +837,11 @@ mod tests { let mut hashes: Vec = chains.iter().map(|c| c.stable_hash).collect(); hashes.sort(); hashes.dedup(); - assert_eq!(hashes.len(), 3, "surviving chains must have distinct hashes"); + assert_eq!( + hashes.len(), + 3, + "surviving chains must have distinct hashes" + ); } /// File-affinity gate on `edge_reaches_entry`: an entry only @@ -858,12 +854,8 @@ mod tests { let mut surface = SurfaceMap::new(); surface.nodes.push(entry("a.js", "/run", false)); surface.nodes.push(entry("b.js", "/run", false)); - surface - .nodes - .push(sink("a.js", 7, "eval", Cap::CODE_EXEC)); - surface - .nodes - .push(sink("b.js", 7, "eval", Cap::CODE_EXEC)); + surface.nodes.push(sink("a.js", 7, "eval", Cap::CODE_EXEC)); + surface.nodes.push(sink("b.js", 7, "eval", Cap::CODE_EXEC)); // Single finding lives in a.js only. Both entries match // route+method but only entry@a.js shares the file. let edges = vec![edge_with( diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 599a8dd6..3babd6ee 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -389,7 +389,12 @@ pub fn handle_command( )?; } #[cfg(feature = "dynamic")] - Commands::VerifyFeedback { finding_id, wrong, right, upload } => { + Commands::VerifyFeedback { + finding_id, + wrong, + right, + upload, + } => { handle_verify_feedback(&finding_id, wrong.as_deref(), right, upload)?; } #[cfg(not(feature = "dynamic"))] @@ -477,8 +482,8 @@ fn handle_verify_feedback( right: bool, upload: bool, ) -> crate::errors::NyxResult<()> { - use std::io::Write; use std::fs::OpenOptions; + use std::io::Write; let _ = upload; // Upload not yet implemented (reserved). diff --git a/src/commands/scan.rs b/src/commands/scan.rs index 6e508feb..bfdd07f4 100644 --- a/src/commands/scan.rs +++ b/src/commands/scan.rs @@ -370,7 +370,10 @@ fn load_verify_summaries( } }; let root_str = scan_root.to_string_lossy().into_owned(); - Some(Arc::new(crate::summary::merge_summaries(all, Some(&root_str)))) + Some(Arc::new(crate::summary::merge_summaries( + all, + Some(&root_str), + ))) } /// Build the whole-program [`crate::callgraph::CallGraph`] from a @@ -446,60 +449,59 @@ pub fn handle( let chain_reach_slot: std::sync::OnceLock = std::sync::OnceLock::new(); - let (mut diags, surface_map): (Vec, crate::surface::SurfaceMap) = if index_mode - == IndexMode::Off - { - scan_filesystem_with_observer( - &scan_path, - config, - show_progress, - None, - None, - None, - Some(&preview_tier_seen), - Some(&chain_reach_slot), - )? - } else { - if index_mode == IndexMode::Rebuild || !db_path.exists() { - tracing::debug!("Scanning filesystem index filesystem"); - crate::commands::index::build_index( - &project_name, + let (mut diags, surface_map): (Vec, crate::surface::SurfaceMap) = + if index_mode == IndexMode::Off { + scan_filesystem_with_observer( &scan_path, - &db_path, config, show_progress, - )?; - } + None, + None, + None, + Some(&preview_tier_seen), + Some(&chain_reach_slot), + )? + } else { + if index_mode == IndexMode::Rebuild || !db_path.exists() { + tracing::debug!("Scanning filesystem index filesystem"); + crate::commands::index::build_index( + &project_name, + &scan_path, + &db_path, + config, + show_progress, + )?; + } - let pool = Indexer::init(&db_path)?; - if config.database.vacuum_on_startup { - let idx = Indexer::from_pool(&project_name, &pool)?; - idx.vacuum()?; - } - // Indexed scan path: persist + return the SurfaceMap so the - // Phase 25 chain composer can walk it. `scan_with_index_parallel_observer` - // already builds and persists the map into the `surface_map` - // SQLite table; reload it through the same pool so the indexed - // chain emission matches the non-indexed branch. - let scan_pool = Arc::clone(&pool); - let diags = scan_with_index_parallel_observer( - &project_name, - scan_pool, - config, - show_progress, - &scan_path, - None, - None, - None, - Some(&preview_tier_seen), - Some(&chain_reach_slot), - )?; - let surface_map = { - let idx = Indexer::from_pool(&project_name, &pool)?; - idx.load_surface_map()?.unwrap_or_default() + let pool = Indexer::init(&db_path)?; + if config.database.vacuum_on_startup { + let idx = Indexer::from_pool(&project_name, &pool)?; + idx.vacuum()?; + } + // Indexed scan path: persist + return the SurfaceMap so the + // Phase 25 chain composer can walk it. `scan_with_index_parallel_observer` + // already builds and persists the map into the `surface_map` + // SQLite table; reload it through the same pool so the indexed + // chain emission matches the non-indexed branch. + let scan_pool = Arc::clone(&pool); + let diags = scan_with_index_parallel_observer( + &project_name, + scan_pool, + config, + show_progress, + &scan_path, + None, + None, + None, + Some(&preview_tier_seen), + Some(&chain_reach_slot), + )?; + let surface_map = { + let idx = Indexer::from_pool(&project_name, &pool)?; + idx.load_surface_map()?.unwrap_or_default() + }; + (diags, surface_map) }; - (diags, surface_map) - }; // Print the Preview-tier banner to stderr once, after file enumeration // completes and before the console output. Suppressed under --quiet and @@ -646,8 +648,7 @@ pub fn handle( // empty (legacy / AST-only paths that never built a call graph), // the chain layer falls back to file-local reach. let chain_reach = chain_reach_slot.get(); - let chain_edges = - crate::chain::findings_to_edges_with_reach(&diags, &surface_map, chain_reach); + let chain_edges = crate::chain::findings_to_edges_with_reach(&diags, &surface_map, chain_reach); let chain_search_cfg = crate::chain::ChainSearchConfig { max_depth: config.chain.max_depth, min_score: config.chain.min_score, @@ -697,21 +698,15 @@ pub fn handle( let diff_value = verdict_diff .as_ref() .map(|d| serde_json::to_value(d).unwrap_or(serde_json::Value::Null)); - let out = crate::output::build_findings_json( - &diags_for_output, - &chains, - diff_value.as_ref(), - ); + let out = + crate::output::build_findings_json(&diags_for_output, &chains, diff_value.as_ref()); let json = serde_json::to_string(&out) .map_err(|e| crate::errors::NyxError::Msg(e.to_string()))?; println!("{json}"); } OutputFormat::Sarif => { - let sarif = crate::output::build_sarif_with_chains( - &diags_for_output, - &chains, - &scan_path, - ); + let sarif = + crate::output::build_sarif_with_chains(&diags_for_output, &chains, &scan_path); let json = serde_json::to_string_pretty(&sarif) .map_err(|e| crate::errors::NyxError::Msg(e.to_string()))?; println!("{json}"); @@ -725,12 +720,7 @@ pub fn handle( tracing::debug!("Printing to console"); print!( "{}", - crate::fmt::render_console( - &diags_for_output, - &project_name, - Some(&stats), - &chains, - ) + crate::fmt::render_console(&diags_for_output, &project_name, Some(&stats), &chains,) ); if let Some(ref diff) = verdict_diff { println!("\nBaseline comparison:"); @@ -769,10 +759,7 @@ pub fn handle( if let (Some(diff), Some(gate_name)) = (&verdict_diff, gate) { if !crate::baseline::check_gate(diff, gate_name) { if !suppress_status { - eprintln!( - "Gate '{}' violated. Exit code 2.", - gate_name - ); + eprintln!("Gate '{}' violated. Exit code 2.", gate_name); } std::process::exit(2); } @@ -2235,9 +2222,8 @@ pub(crate) fn scan_filesystem_with_observer( } if let Some(out) = chain_reach_out { - let _ = out.set( - crate::callgraph::FileReachMap::build(&call_graph).with_scan_root(Some(root)), - ); + let _ = + out.set(crate::callgraph::FileReachMap::build(&call_graph).with_scan_root(Some(root))); } // ── Pass 2: re-run with cross-file global summaries ────────────────── @@ -2311,15 +2297,14 @@ pub(crate) fn scan_filesystem_with_observer( // `surface_map` SQLite table. The map is returned alongside the // diagnostics so consumers (e.g. `nyx surface`) can avoid scanning // twice. - let surface_map = crate::surface::build::build_surface_map( - &crate::surface::build::SurfaceBuildInputs { + let surface_map = + crate::surface::build::build_surface_map(&crate::surface::build::SurfaceBuildInputs { files: &all_paths, scan_root: Some(root), global_summaries: &gs, call_graph: &call_graph, config: cfg, - }, - ); + }); if let Some(p) = progress { p.record_pass2_ms(pass2_start.elapsed().as_millis() as u64); } @@ -3142,15 +3127,14 @@ pub fn scan_with_index_parallel_observer( // view. Errors here are logged but not propagated — the surface // map is an additive Phase F deliverable, not a scan gate. { - let surface_map = crate::surface::build::build_surface_map( - &crate::surface::build::SurfaceBuildInputs { + let surface_map = + crate::surface::build::build_surface_map(&crate::surface::build::SurfaceBuildInputs { files: &files, scan_root: Some(scan_root), global_summaries: &global_summaries, call_graph: &call_graph, config: cfg, - }, - ); + }); let mut idx = Indexer::from_pool(project, &pool)?; if let Err(e) = idx.replace_surface_map(&surface_map) { tracing::warn!("failed to persist surface_map: {e}"); diff --git a/src/commands/surface.rs b/src/commands/surface.rs index 42faa759..04720504 100644 --- a/src/commands/surface.rs +++ b/src/commands/surface.rs @@ -100,12 +100,13 @@ pub fn load_or_build( ) -> NyxResult { if let Ok((project, db_path)) = get_project_info(scan_root, database_dir) && db_path.exists() - && let Ok(pool) = Indexer::init(&db_path) - && let Ok(idx) = Indexer::from_pool(&project, &pool) - && let Ok(Some(map)) = idx.load_surface_map() - && !map.nodes.is_empty() { - return Ok(map); - } + && let Ok(pool) = Indexer::init(&db_path) + && let Ok(idx) = Indexer::from_pool(&project, &pool) + && let Ok(Some(map)) = idx.load_surface_map() + && !map.nodes.is_empty() + { + return Ok(map); + } build_from_filesystem(scan_root, config) } @@ -151,11 +152,7 @@ fn build_full_from_filesystem(scan_root: &Path, config: &Config) -> NyxResult GlobalSummaries { +fn build_summaries_inline(files: &[PathBuf], scan_root: &Path, config: &Config) -> GlobalSummaries { let root_str = scan_root.to_string_lossy().into_owned(); let mg = config.module_graph.as_deref(); files @@ -279,7 +276,8 @@ pub fn render_text(map: &SurfaceMap, scan_root: Option<&Path>) -> String { } for &i in indices { match &map.nodes[i] { - SurfaceNode::DataStore(_) | SurfaceNode::ExternalService(_) + SurfaceNode::DataStore(_) + | SurfaceNode::ExternalService(_) | SurfaceNode::DangerousLocal(_) => { if !entry_indices.is_empty() { continue; @@ -456,10 +454,18 @@ pub fn render_dot(map: &SurfaceMap) -> String { escape_dot(&ep.handler_name), ), "box", - if ep.auth_required { "#3aa57c" } else { "#3072c4" }, + if ep.auth_required { + "#3aa57c" + } else { + "#3072c4" + }, ), SurfaceNode::DataStore(ds) => ( - format!("DataStore ({})\\n{}", ds_kind_str(ds.kind), escape_dot(&ds.label)), + format!( + "DataStore ({})\\n{}", + ds_kind_str(ds.kind), + escape_dot(&ds.label) + ), "cylinder", "#b07a18", ), @@ -543,9 +549,7 @@ fn render_svg(map: &SurfaceMap) -> NyxResult> { mod tests { use super::*; use crate::entry_points::HttpMethod; - use crate::surface::{ - EntryPoint, Framework, SourceLocation, SurfaceEdge, SurfaceNode, - }; + use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceEdge, SurfaceNode}; fn flask_fixture_map() -> SurfaceMap { let mut map = SurfaceMap::new(); @@ -598,12 +602,13 @@ mod tests { #[test] fn text_render_groups_reaches_under_entry() { let mut m = flask_fixture_map(); - m.nodes - .push(SurfaceNode::DangerousLocal(crate::surface::DangerousLocal { + m.nodes.push(SurfaceNode::DangerousLocal( + crate::surface::DangerousLocal { location: SourceLocation::new("app.py", 12, 1), function_name: "eval".into(), cap_bits: crate::labels::Cap::CODE_EXEC.bits(), - })); + }, + )); // Build edge after canonicalize so indices are stable. m.canonicalize(); let ep_idx = m @@ -657,10 +662,7 @@ mod tests { let canon = project_dir.canonicalize().unwrap(); let files = collect_files(&canon, &cfg).unwrap(); let summaries = build_summaries_inline(&files, &canon, &cfg); - let names: Vec = summaries - .iter() - .map(|(k, _)| k.qualified_name()) - .collect(); + let names: Vec = summaries.iter().map(|(k, _)| k.qualified_name()).collect(); assert!( names.iter().any(|n| n.ends_with("run")), "summaries should contain `run`, got {names:?}" diff --git a/src/database.rs b/src/database.rs index 90db6642..21e55611 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1913,10 +1913,7 @@ pub mod index { /// per project. The map is canonicalised before serialisation so /// `replace_surface_map` + `load_surface_map` round-trip is /// byte-identical for structurally identical maps. - pub fn replace_surface_map( - &mut self, - map: &crate::surface::SurfaceMap, - ) -> NyxResult<()> { + pub fn replace_surface_map(&mut self, map: &crate::surface::SurfaceMap) -> NyxResult<()> { let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let mut canon = map.clone(); let bytes = canon diff --git a/src/dynamic/build_sandbox.rs b/src/dynamic/build_sandbox.rs index 0c156e34..93c9f669 100644 --- a/src/dynamic/build_sandbox.rs +++ b/src/dynamic/build_sandbox.rs @@ -42,7 +42,11 @@ pub fn prepare_rust(spec: &HarnessSpec, workdir: &Path) -> Result Result Result<(), String> { @@ -86,10 +93,14 @@ fn try_build_rust_binary(workdir: &Path, binary_dest: &Path) -> Result<(), Strin .env("PATH", std::env::var("PATH").unwrap_or_default()) .env("HOME", std::env::var("HOME").unwrap_or_default()) // Inherit CARGO_HOME so the local registry cache is reused. - .env("CARGO_HOME", std::env::var("CARGO_HOME").unwrap_or_else(|_| { - dirs_next_cargo_home() - })) - .env("RUSTUP_HOME", std::env::var("RUSTUP_HOME").unwrap_or_default()) + .env( + "CARGO_HOME", + std::env::var("CARGO_HOME").unwrap_or_else(|_| dirs_next_cargo_home()), + ) + .env( + "RUSTUP_HOME", + std::env::var("RUSTUP_HOME").unwrap_or_default(), + ) .output() .map_err(|e| format!("cargo build: {e}"))?; @@ -101,8 +112,7 @@ fn try_build_rust_binary(workdir: &Path, binary_dest: &Path) -> Result<(), Strin // Copy binary to cache location. let compiled = workdir.join("target").join("release").join("nyx_harness"); if compiled.exists() { - std::fs::copy(&compiled, binary_dest) - .map_err(|e| format!("copy binary: {e}"))?; + std::fs::copy(&compiled, binary_dest).map_err(|e| format!("copy binary: {e}"))?; } Ok(()) @@ -137,7 +147,10 @@ fn compute_rust_lockfile_hash(workdir: &Path) -> String { h.update(&content); } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } /// Result of a successful build. @@ -168,10 +181,7 @@ impl From for BuildError { /// /// If a compatible cache entry exists, returns it immediately. Otherwise /// builds in isolation and caches the result. -pub fn prepare_python( - spec: &HarnessSpec, - workdir: &Path, -) -> Result { +pub fn prepare_python(spec: &HarnessSpec, workdir: &Path) -> Result { let lockfile_hash = compute_lockfile_hash(workdir); let cache_path = build_cache_path(&lockfile_hash, "python", &spec.toolchain_id)?; @@ -217,11 +227,7 @@ pub fn prepare_python( }) } -fn try_build_venv( - venv_path: &Path, - workdir: &Path, - spec: &HarnessSpec, -) -> Result<(), String> { +fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Result<(), String> { // Find python binary. let python = python_binary(spec); @@ -262,10 +268,7 @@ fn try_build_venv( fn python_binary(spec: &HarnessSpec) -> String { // Try the pinned version first; fall back to python3. - let ver = spec - .toolchain_id - .strip_prefix("python-") - .unwrap_or("3"); + let ver = spec.toolchain_id.strip_prefix("python-").unwrap_or("3"); let candidate = format!("python{ver}"); if which_exists(&candidate) { return candidate; @@ -290,7 +293,10 @@ fn compute_lockfile_hash(workdir: &Path) -> String { } } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } fn build_cache_path( @@ -308,9 +314,7 @@ fn build_cache_path( "cannot determine cache dir", )) })?; - dirs.cache_dir() - .join("dynamic") - .join("build-cache") + dirs.cache_dir().join("dynamic").join("build-cache") }; let name = format!("{lockfile_hash}-{language}-{toolchain_id}"); @@ -366,7 +370,9 @@ pub fn prepare_node(spec: &HarnessSpec, workdir: &Path) -> Result 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } match try_npm_install(workdir) { Ok(()) => { @@ -389,7 +395,10 @@ pub fn prepare_node(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { @@ -430,14 +439,22 @@ fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { fn compute_node_lockfile_hash(workdir: &Path) -> String { let mut h = Hasher::new(); - for fname in &["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"] { + for fname in &[ + "package.json", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + ] { if let Ok(content) = std::fs::read(workdir.join(fname)) { h.update(fname.as_bytes()); h.update(&content); } } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── Go build sandbox ────────────────────────────────────────────────────────── @@ -470,7 +487,9 @@ pub fn prepare_go(spec: &HarnessSpec, workdir: &Path) -> Result 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } let _ = std::fs::remove_dir_all(&cache_path); std::fs::create_dir_all(&cache_path)?; @@ -490,23 +509,41 @@ pub fn prepare_go(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { let go_bin = std::env::var("NYX_GO_BIN").unwrap_or_else(|_| "go".to_owned()); let output = Command::new(&go_bin) - .args(["build", "-o", binary_dest.to_str().unwrap_or("nyx_harness"), "."]) + .args([ + "build", + "-o", + binary_dest.to_str().unwrap_or("nyx_harness"), + ".", + ]) .current_dir(workdir) .env_clear() .env("PATH", std::env::var("PATH").unwrap_or_default()) .env("HOME", std::env::var("HOME").unwrap_or_default()) - .env("GOPATH", std::env::var("GOPATH").unwrap_or_else(|_| { - std::env::var("HOME").map(|h| format!("{h}/go")).unwrap_or_else(|_| "/tmp/go".to_owned()) - })) - .env("GOMODCACHE", std::env::var("GOMODCACHE").unwrap_or_else(|_| { - std::env::var("HOME").map(|h| format!("{h}/go/pkg/mod")).unwrap_or_else(|_| "/tmp/gomod".to_owned()) - })) + .env( + "GOPATH", + std::env::var("GOPATH").unwrap_or_else(|_| { + std::env::var("HOME") + .map(|h| format!("{h}/go")) + .unwrap_or_else(|_| "/tmp/go".to_owned()) + }), + ) + .env( + "GOMODCACHE", + std::env::var("GOMODCACHE").unwrap_or_else(|_| { + std::env::var("HOME") + .map(|h| format!("{h}/go/pkg/mod")) + .unwrap_or_else(|_| "/tmp/gomod".to_owned()) + }), + ) .output() .map_err(|e| format!("go build: {e}"))?; @@ -529,7 +566,10 @@ fn compute_go_source_hash(workdir: &Path) -> String { h.update(&content); } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── Java build sandbox ──────────────────────────────────────────────────────── @@ -592,7 +632,9 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } match try_compile_java(workdir, &cache_path, target_release) { Ok(()) => { @@ -622,7 +664,10 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result Option { } } -fn try_compile_java(workdir: &Path, cache_path: &Path, target_release: Option) -> Result<(), String> { +fn try_compile_java( + workdir: &Path, + cache_path: &Path, + target_release: Option, +) -> Result<(), String> { let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned()); // If the harness emitter shipped a `pom.xml`, stage Maven-resolved @@ -792,9 +841,10 @@ fn collect_class_files(root: &Path) -> Vec { if path.is_dir() { stack.push(path); } else if path.extension().map(|e| e == "class").unwrap_or(false) - && let Ok(rel) = path.strip_prefix(root) { - out.push(rel.to_path_buf()); - } + && let Ok(rel) = path.strip_prefix(root) + { + out.push(rel.to_path_buf()); + } } } out.sort(); @@ -826,7 +876,10 @@ fn compute_java_source_hash(workdir: &Path, target_release: Option) -> Stri h.update(b":release=host"); } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── PHP build sandbox ───────────────────────────────────────────────────────── @@ -869,7 +922,9 @@ pub fn prepare_php(spec: &HarnessSpec, workdir: &Path) -> Result 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } match try_composer_install(workdir) { Ok(()) => { @@ -892,7 +947,10 @@ pub fn prepare_php(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { @@ -922,7 +980,10 @@ fn compute_php_lockfile_hash(workdir: &Path) -> String { } } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── C build sandbox ─────────────────────────────────────────────────────────── @@ -959,7 +1020,9 @@ pub fn prepare_c( for attempt in 0..MAX_ATTEMPTS { if attempt > 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } let _ = std::fs::remove_dir_all(&cache_path); std::fs::create_dir_all(&cache_path)?; @@ -979,7 +1042,10 @@ pub fn prepare_c( } } - Err(BuildError::BuildFailed { stderr: last_err, attempts: MAX_ATTEMPTS }) + Err(BuildError::BuildFailed { + stderr: last_err, + attempts: MAX_ATTEMPTS, + }) } fn try_build_c_binary(workdir: &Path, binary_dest: &Path, static_link: bool) -> Result<(), String> { @@ -1032,7 +1098,12 @@ pub(crate) fn static_link_env_override() -> bool { ) } -fn run_cc(cc_bin: &str, workdir: &Path, binary_dest: &Path, leading_flags: &[&str]) -> Result<(), String> { +fn run_cc( + cc_bin: &str, + workdir: &Path, + binary_dest: &Path, + leading_flags: &[&str], +) -> Result<(), String> { let binary_str = binary_dest.to_str().unwrap_or("nyx_harness"); let mut args: Vec<&str> = leading_flags.to_vec(); args.extend(["-o", binary_str, "main.c"]); @@ -1067,7 +1138,10 @@ fn compute_c_source_hash(workdir: &Path, static_link: bool) -> String { h.update(b"static"); } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── C++ build sandbox ───────────────────────────────────────────────────────── @@ -1093,7 +1167,9 @@ pub fn prepare_cpp(spec: &HarnessSpec, workdir: &Path) -> Result 0 { - std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1])); + std::thread::sleep(std::time::Duration::from_secs( + BACKOFF[attempt as usize - 1], + )); } let _ = std::fs::remove_dir_all(&cache_path); std::fs::create_dir_all(&cache_path)?; @@ -1113,7 +1189,10 @@ pub fn prepare_cpp(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { @@ -1122,7 +1201,14 @@ fn try_build_cpp_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String "c++".to_owned() }); let output = Command::new(&cxx_bin) - .args(["-O0", "-g", "-std=c++17", "-o", binary_dest.to_str().unwrap_or("nyx_harness"), "main.cpp"]) + .args([ + "-O0", + "-g", + "-std=c++17", + "-o", + binary_dest.to_str().unwrap_or("nyx_harness"), + "main.cpp", + ]) .current_dir(workdir) .env_clear() .env("PATH", std::env::var("PATH").unwrap_or_default()) @@ -1145,7 +1231,10 @@ fn compute_cpp_source_hash(workdir: &Path) -> String { } } let out = h.finalize(); - format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()) + ) } // ── Uniform per-language build dispatch (Phase 26 — composite chains) ──────── @@ -1251,10 +1340,14 @@ fn start_isolated_build_container( network_none: bool, ) -> bool { let mut args: Vec<&str> = vec![ - "run", "-d", "--rm", - "--name", name, + "run", + "-d", + "--rm", + "--name", + name, "--cap-drop=ALL", - "--security-opt", "no-new-privileges:true", + "--security-opt", + "no-new-privileges:true", ]; if network_none { args.extend_from_slice(&["--network", "none"]); @@ -1319,16 +1412,22 @@ pub fn prepare_rust_in_docker(workdir: &Path) -> Result<(), String> { return Err("failed to start rust:slim build container; image may not be available".into()); } - let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() }; + let _guard = BuildContainerGuard { + docker: docker.clone(), + name: container.clone(), + }; copy_workdir_to_build_container(&docker, workdir, &container, "/build"); // CARGO_NET_OFFLINE prevents any registry contact; std lib is pre-built in the image. let _ = std::process::Command::new(&docker) .args([ "exec", - "-e", "CARGO_NET_OFFLINE=true", + "-e", + "CARGO_NET_OFFLINE=true", &container, - "sh", "-c", "cd /build && cargo build --release 2>&1", + "sh", + "-c", + "cd /build && cargo build --release 2>&1", ]) .output(); @@ -1347,10 +1446,15 @@ pub fn prepare_node_in_docker(workdir: &Path) -> Result<(), String> { let container = build_container_id("nodebuild", workdir); if !start_isolated_build_container(&docker, &container, "node:20-slim", true) { - return Err("failed to start node:20-slim build container; image may not be available".into()); + return Err( + "failed to start node:20-slim build container; image may not be available".into(), + ); } - let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() }; + let _guard = BuildContainerGuard { + docker: docker.clone(), + name: container.clone(), + }; copy_workdir_to_build_container(&docker, workdir, &container, "/build"); // npm install may fail if the registry is unreachable (--network none), but the @@ -1359,7 +1463,8 @@ pub fn prepare_node_in_docker(workdir: &Path) -> Result<(), String> { .args([ "exec", &container, - "sh", "-c", + "sh", + "-c", "cd /build && npm install --no-save --no-audit --no-fund 2>&1", ]) .output(); @@ -1379,20 +1484,29 @@ pub fn prepare_go_in_docker(workdir: &Path) -> Result<(), String> { let container = build_container_id("gobuild", workdir); if !start_isolated_build_container(&docker, &container, "golang:1.21-slim", true) { - return Err("failed to start golang:1.21-slim build container; image may not be available".into()); + return Err( + "failed to start golang:1.21-slim build container; image may not be available".into(), + ); } - let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() }; + let _guard = BuildContainerGuard { + docker: docker.clone(), + name: container.clone(), + }; copy_workdir_to_build_container(&docker, workdir, &container, "/build"); // GOPROXY=off prevents module downloads; std library is pre-compiled in the image. let _ = std::process::Command::new(&docker) .args([ "exec", - "-e", "GOPROXY=off", - "-e", "GONOSUMDB=*", + "-e", + "GOPROXY=off", + "-e", + "GONOSUMDB=*", &container, - "sh", "-c", "cd /build && go build ./... 2>&1", + "sh", + "-c", + "cd /build && go build ./... 2>&1", ]) .output(); @@ -1413,26 +1527,26 @@ pub fn prepare_java_in_docker(workdir: &Path) -> Result<(), String> { // Bridge network: Maven must download exec-maven-plugin from Maven Central. // Filesystem isolation still holds: /tmp inside the container is private. - if !start_isolated_build_container( - &docker, - &container, - "maven:3.9-eclipse-temurin-21", - false, - ) { + if !start_isolated_build_container(&docker, &container, "maven:3.9-eclipse-temurin-21", false) { return Err( "failed to start maven:3.9-eclipse-temurin-21 build container; image may not be available" .into(), ); } - let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() }; + let _guard = BuildContainerGuard { + docker: docker.clone(), + name: container.clone(), + }; copy_workdir_to_build_container(&docker, workdir, &container, "/build"); let _ = std::process::Command::new(&docker) .args([ "exec", &container, - "sh", "-c", "cd /build && mvn --no-transfer-progress validate 2>&1", + "sh", + "-c", + "cd /build && mvn --no-transfer-progress validate 2>&1", ]) .output(); @@ -1451,10 +1565,15 @@ pub fn prepare_php_in_docker(workdir: &Path) -> Result<(), String> { let container = build_container_id("phpbuild", workdir); if !start_isolated_build_container(&docker, &container, "composer:2", true) { - return Err("failed to start composer:2 build container; image may not be available".into()); + return Err( + "failed to start composer:2 build container; image may not be available".into(), + ); } - let _guard = BuildContainerGuard { docker: docker.clone(), name: container.clone() }; + let _guard = BuildContainerGuard { + docker: docker.clone(), + name: container.clone(), + }; copy_workdir_to_build_container(&docker, workdir, &container, "/build"); // Empty require{} means no packages to fetch; post-install-cmd still fires. @@ -1462,7 +1581,8 @@ pub fn prepare_php_in_docker(workdir: &Path) -> Result<(), String> { .args([ "exec", &container, - "sh", "-c", + "sh", + "-c", "cd /build && composer install --no-dev --no-interaction --prefer-dist 2>&1", ]) .output(); @@ -1519,11 +1639,7 @@ mod tests { #[test] fn java_source_hash_differs_across_target_release() { let dir = tempfile::TempDir::new().unwrap(); - std::fs::write( - dir.path().join("Vuln.java"), - "public class Vuln {}\n", - ) - .unwrap(); + std::fs::write(dir.path().join("Vuln.java"), "public class Vuln {}\n").unwrap(); let h_none = compute_java_source_hash(dir.path(), None); let h17 = compute_java_source_hash(dir.path(), Some(17)); let h21 = compute_java_source_hash(dir.path(), Some(21)); @@ -1568,7 +1684,10 @@ mod tests { copy_dir_all(src.path(), dst.path()).unwrap(); assert_eq!(std::fs::read(dst.path().join("a.txt")).unwrap(), b"hello"); - assert_eq!(std::fs::read(dst.path().join("sub").join("b.txt")).unwrap(), b"world"); + assert_eq!( + std::fs::read(dst.path().join("sub").join("b.txt")).unwrap(), + b"world" + ); } #[test] @@ -1760,7 +1879,11 @@ mod tests { let result = dispatch_prepare(&spec, dir.path(), ProcessHardeningProfile::Standard) .expect("TypeScript dispatch must succeed on a workdir with no package.json"); - assert_eq!(result.lang, Lang::TypeScript, "lang field must echo the spec's"); + assert_eq!( + result.lang, + Lang::TypeScript, + "lang field must echo the spec's" + ); assert!( !result.cache_hit, "first dispatch on a fresh cache must be a cache miss; got {result:?}", diff --git a/src/dynamic/corpus.rs b/src/dynamic/corpus.rs index 6b7620b8..476b6163 100644 --- a/src/dynamic/corpus.rs +++ b/src/dynamic/corpus.rs @@ -67,9 +67,9 @@ mod xss; mod xxe; pub use registry::{ - audit_marker_collisions, benign_payload_for, benign_payload_for_lang, materialise_bytes, - payloads_for, payloads_for_lang, resolve_benign_control, resolve_benign_control_lang, - CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL, + CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL, audit_marker_collisions, benign_payload_for, + benign_payload_for_lang, materialise_bytes, payloads_for, payloads_for_lang, + resolve_benign_control, resolve_benign_control_lang, }; /// Re-exported canonical [`Oracle`] type. diff --git a/src/dynamic/corpus/audit.rs b/src/dynamic/corpus/audit.rs index 39401394..ce413d6b 100644 --- a/src/dynamic/corpus/audit.rs +++ b/src/dynamic/corpus/audit.rs @@ -19,8 +19,8 @@ //! The runtime `corpus_registry::audit` test mirrors both checks so //! failure surfaces in `cargo test` output, not just `cargo build`. -use super::registry::{CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL}; use super::CuratedPayload; +use super::registry::{CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL}; use crate::labels::Cap; /// Byte-level equality for `&'static str` usable in const eval. @@ -121,9 +121,7 @@ pub fn audit_benign_controls_runtime() -> Result<(), String> { } match p.benign_control { Some(r) => { - let found = slice - .iter() - .any(|q| q.is_benign && q.label == r.label); + let found = slice.iter().any(|q| q.is_benign && q.label == r.label); if !found { return Err(format!( "({:?}, {:?}) vuln payload {:?} references missing \ @@ -180,17 +178,18 @@ pub fn audit_benign_label_uniqueness_runtime() -> Result<(), String> { continue; } if let Some(prev_lang) = bucket.insert(p.label, lang) - && prev_lang != lang { - return Err(format!( - "benign label {:?} for cap {:#x} is registered in both \ + && prev_lang != lang + { + return Err(format!( + "benign label {:?} for cap {:#x} is registered in both \ {:?} and {:?} — lang-agnostic resolve_benign_control \ could match the wrong language", - p.label, - cap.bits(), - prev_lang, - lang, - )); - } + p.label, + cap.bits(), + prev_lang, + lang, + )); + } } } Ok(()) @@ -206,7 +205,6 @@ mod corpus_registry { fn audit() { audit_benign_controls_runtime().expect("benign_control audit failed"); audit_cap_coverage_runtime().expect("cap coverage audit failed"); - audit_benign_label_uniqueness_runtime() - .expect("benign label uniqueness audit failed"); + audit_benign_label_uniqueness_runtime().expect("benign label uniqueness audit failed"); } } diff --git a/src/dynamic/corpus/cmdi/c.rs b/src/dynamic/corpus/cmdi/c.rs index aadeccd5..0abf7f37 100644 --- a/src/dynamic/corpus/cmdi/c.rs +++ b/src/dynamic/corpus/cmdi/c.rs @@ -19,7 +19,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-c" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-c", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/cpp.rs b/src/dynamic/corpus/cmdi/cpp.rs index 462be343..0dca6aeb 100644 --- a/src/dynamic/corpus/cmdi/cpp.rs +++ b/src/dynamic/corpus/cmdi/cpp.rs @@ -22,7 +22,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-cpp" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-cpp", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/go.rs b/src/dynamic/corpus/cmdi/go.rs index d2ea660a..cfb0fad0 100644 --- a/src/dynamic/corpus/cmdi/go.rs +++ b/src/dynamic/corpus/cmdi/go.rs @@ -19,7 +19,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-go" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-go", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/java.rs b/src/dynamic/corpus/cmdi/java.rs index e6991e62..62d44630 100644 --- a/src/dynamic/corpus/cmdi/java.rs +++ b/src/dynamic/corpus/cmdi/java.rs @@ -17,7 +17,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-java" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-java", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/javascript.rs b/src/dynamic/corpus/cmdi/javascript.rs index c7d20b0a..6539f46f 100644 --- a/src/dynamic/corpus/cmdi/javascript.rs +++ b/src/dynamic/corpus/cmdi/javascript.rs @@ -17,7 +17,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-javascript" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-javascript", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/php.rs b/src/dynamic/corpus/cmdi/php.rs index 071150f6..8b2a560e 100644 --- a/src/dynamic/corpus/cmdi/php.rs +++ b/src/dynamic/corpus/cmdi/php.rs @@ -17,7 +17,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-php" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-php", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/python.rs b/src/dynamic/corpus/cmdi/python.rs index bdb99ffe..29bb2145 100644 --- a/src/dynamic/corpus/cmdi/python.rs +++ b/src/dynamic/corpus/cmdi/python.rs @@ -22,7 +22,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-python" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-python", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/ruby.rs b/src/dynamic/corpus/cmdi/ruby.rs index bf1440c5..71eaa155 100644 --- a/src/dynamic/corpus/cmdi/ruby.rs +++ b/src/dynamic/corpus/cmdi/ruby.rs @@ -18,7 +18,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-ruby" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-ruby", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/cmdi/rust.rs b/src/dynamic/corpus/cmdi/rust.rs index f8bbb52c..b37129db 100644 --- a/src/dynamic/corpus/cmdi/rust.rs +++ b/src/dynamic/corpus/cmdi/rust.rs @@ -22,7 +22,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign", + }), no_benign_control_rationale: None, }, // Benign control: plain text that should never produce the cmdi marker. diff --git a/src/dynamic/corpus/cmdi/typescript.rs b/src/dynamic/corpus/cmdi/typescript.rs index 3245614d..7591b4e6 100644 --- a/src/dynamic/corpus/cmdi/typescript.rs +++ b/src/dynamic/corpus/cmdi/typescript.rs @@ -17,7 +17,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "cmdi-benign-typescript" }), + benign_control: Some(PayloadRef { + label: "cmdi-benign-typescript", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/crypto/go.rs b/src/dynamic/corpus/crypto/go.rs index 0b498440..99045d7d 100644 --- a/src/dynamic/corpus/crypto/go.rs +++ b/src/dynamic/corpus/crypto/go.rs @@ -11,7 +11,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_WEAK", label: "crypto-go-weak-random", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -19,7 +21,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/crypto/go/vuln.go"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + probe_predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], benign_control: Some(PayloadRef { label: "crypto-go-benign", }), @@ -29,7 +33,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_STRONG", label: "crypto-go-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/crypto/java.rs b/src/dynamic/corpus/crypto/java.rs index 3276d5c8..952b705d 100644 --- a/src/dynamic/corpus/crypto/java.rs +++ b/src/dynamic/corpus/crypto/java.rs @@ -22,7 +22,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_WEAK", label: "crypto-java-weak-random", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -30,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/crypto/java/vuln.java"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + probe_predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], benign_control: Some(PayloadRef { label: "crypto-java-benign", }), @@ -40,7 +44,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_STRONG", label: "crypto-java-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/crypto/php.rs b/src/dynamic/corpus/crypto/php.rs index fc6818fb..148622fb 100644 --- a/src/dynamic/corpus/crypto/php.rs +++ b/src/dynamic/corpus/crypto/php.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_WEAK", label: "crypto-php-weak-random", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/crypto/php/vuln.php"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + probe_predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], benign_control: Some(PayloadRef { label: "crypto-php-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_STRONG", label: "crypto-php-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/crypto/python.rs b/src/dynamic/corpus/crypto/python.rs index 8b0915ed..8f1d89d0 100644 --- a/src/dynamic/corpus/crypto/python.rs +++ b/src/dynamic/corpus/crypto/python.rs @@ -20,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_WEAK", label: "crypto-python-weak-random", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -28,7 +30,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/crypto/python/vuln.py"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + probe_predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], benign_control: Some(PayloadRef { label: "crypto-python-benign", }), @@ -38,7 +42,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_STRONG", label: "crypto-python-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/crypto/rust.rs b/src/dynamic/corpus/crypto/rust.rs index 3895fcd7..24acfea7 100644 --- a/src/dynamic/corpus/crypto/rust.rs +++ b/src/dynamic/corpus/crypto/rust.rs @@ -11,7 +11,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_WEAK", label: "crypto-rust-weak-random", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -19,7 +21,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/crypto/rust/vuln.rs"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + probe_predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], benign_control: Some(PayloadRef { label: "crypto-rust-benign", }), @@ -29,7 +33,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"NYX_CRYPTO_STRONG", label: "crypto-rust-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::WeakKeyEntropy { max_bits: WEAK_BITS }], + predicates: &[ProbePredicate::WeakKeyEntropy { + max_bits: WEAK_BITS, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/go.rs b/src/dynamic/corpus/data_exfil/go.rs index d3afeb47..5fec1fff 100644 --- a/src/dynamic/corpus/data_exfil/go.rs +++ b/src/dynamic/corpus/data_exfil/go.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-go-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/go/vuln.go"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-go-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-go-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/java.rs b/src/dynamic/corpus/data_exfil/java.rs index 8b6af8db..43f5da96 100644 --- a/src/dynamic/corpus/data_exfil/java.rs +++ b/src/dynamic/corpus/data_exfil/java.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-java-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/java/vuln.java"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-java-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-java-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/js.rs b/src/dynamic/corpus/data_exfil/js.rs index 47c47a79..80c104ab 100644 --- a/src/dynamic/corpus/data_exfil/js.rs +++ b/src/dynamic/corpus/data_exfil/js.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-js-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/js/vuln.js"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-js-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-js-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/php.rs b/src/dynamic/corpus/data_exfil/php.rs index a1895826..32c89475 100644 --- a/src/dynamic/corpus/data_exfil/php.rs +++ b/src/dynamic/corpus/data_exfil/php.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-php-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/php/vuln.php"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-php-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-php-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/python.rs b/src/dynamic/corpus/data_exfil/python.rs index 827e15e1..16405f30 100644 --- a/src/dynamic/corpus/data_exfil/python.rs +++ b/src/dynamic/corpus/data_exfil/python.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-python-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/python/vuln.py"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-python-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-python-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/ruby.rs b/src/dynamic/corpus/data_exfil/ruby.rs index 9526cb49..e1e3c1b2 100644 --- a/src/dynamic/corpus/data_exfil/ruby.rs +++ b/src/dynamic/corpus/data_exfil/ruby.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-ruby-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/ruby/vuln.rb"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-ruby-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-ruby-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/data_exfil/rust.rs b/src/dynamic/corpus/data_exfil/rust.rs index 6bdb2e77..eb09a508 100644 --- a/src/dynamic/corpus/data_exfil/rust.rs +++ b/src/dynamic/corpus/data_exfil/rust.rs @@ -10,7 +10,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"attacker.test", label: "data-exfil-rust-external", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -18,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/data_exfil/rust/vuln.rs"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "data-exfil-rust-benign", }), @@ -28,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"127.0.0.1", label: "data-exfil-rust-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::OutboundHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::OutboundHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/deserialize/java.rs b/src/dynamic/corpus/deserialize/java.rs index 8ee9931b..53dc5622 100644 --- a/src/dynamic/corpus/deserialize/java.rs +++ b/src/dynamic/corpus/deserialize/java.rs @@ -29,9 +29,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/java/Vuln.java", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/java/Vuln.java"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked { require_invoked: true, @@ -55,9 +53,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/java/Benign.java", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/java/Benign.java"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/deserialize/php.rs b/src/dynamic/corpus/deserialize/php.rs index 14d1c706..866fe2f9 100644 --- a/src/dynamic/corpus/deserialize/php.rs +++ b/src/dynamic/corpus/deserialize/php.rs @@ -26,9 +26,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/php/vuln.php", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/php/vuln.php"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked { require_invoked: true, @@ -53,9 +51,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/php/benign.php", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/php/benign.php"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/deserialize/python.rs b/src/dynamic/corpus/deserialize/python.rs index 2c4f3d57..79be82bb 100644 --- a/src/dynamic/corpus/deserialize/python.rs +++ b/src/dynamic/corpus/deserialize/python.rs @@ -25,9 +25,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/python/vuln.py", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/python/vuln.py"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked { require_invoked: true, @@ -49,9 +47,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/python/benign.py", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/python/benign.py"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/deserialize/ruby.rs b/src/dynamic/corpus/deserialize/ruby.rs index 9889a510..55743c7b 100644 --- a/src/dynamic/corpus/deserialize/ruby.rs +++ b/src/dynamic/corpus/deserialize/ruby.rs @@ -26,9 +26,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/ruby/vuln.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/ruby/vuln.rb"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked { require_invoked: true, @@ -50,9 +48,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 7, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/deserialize/ruby/benign.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/deserialize/ruby/benign.rb"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/fmt_string/c.rs b/src/dynamic/corpus/fmt_string/c.rs index bba50e38..47ae303b 100644 --- a/src/dynamic/corpus/fmt_string/c.rs +++ b/src/dynamic/corpus/fmt_string/c.rs @@ -32,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "fmt-string-benign" }), + benign_control: Some(PayloadRef { + label: "fmt-string-benign", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/open_redirect/go.rs b/src/dynamic/corpus/open_redirect/go.rs index 28b1edcf..62019259 100644 --- a/src/dynamic/corpus/open_redirect/go.rs +++ b/src/dynamic/corpus/open_redirect/go.rs @@ -21,7 +21,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-go-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -29,7 +31,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/vuln.go"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-go-benign", }), @@ -39,7 +43,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-go-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/java.rs b/src/dynamic/corpus/open_redirect/java.rs index c9c468be..33753ecb 100644 --- a/src/dynamic/corpus/open_redirect/java.rs +++ b/src/dynamic/corpus/open_redirect/java.rs @@ -26,7 +26,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-java-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -34,7 +36,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Vuln.java"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-java-benign", }), @@ -44,7 +48,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-java-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/js.rs b/src/dynamic/corpus/open_redirect/js.rs index 8d13ac43..984d9254 100644 --- a/src/dynamic/corpus/open_redirect/js.rs +++ b/src/dynamic/corpus/open_redirect/js.rs @@ -20,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-js-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -28,7 +30,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/vuln.js"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-js-benign", }), @@ -38,7 +42,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-js-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/php.rs b/src/dynamic/corpus/open_redirect/php.rs index 504d65aa..bdba4239 100644 --- a/src/dynamic/corpus/open_redirect/php.rs +++ b/src/dynamic/corpus/open_redirect/php.rs @@ -22,7 +22,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-php-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -30,7 +32,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/vuln.php"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-php-benign", }), @@ -40,7 +44,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-php-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/python.rs b/src/dynamic/corpus/open_redirect/python.rs index ecd8ae4c..ee61581b 100644 --- a/src/dynamic/corpus/open_redirect/python.rs +++ b/src/dynamic/corpus/open_redirect/python.rs @@ -21,7 +21,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-python-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -29,7 +31,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/vuln.py"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-python-benign", }), @@ -39,7 +43,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-python-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/ruby.rs b/src/dynamic/corpus/open_redirect/ruby.rs index 5a504bcb..6b19acd5 100644 --- a/src/dynamic/corpus/open_redirect/ruby.rs +++ b/src/dynamic/corpus/open_redirect/ruby.rs @@ -20,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-ruby-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -28,7 +30,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/vuln.rb"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-ruby-benign", }), @@ -38,7 +42,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-ruby-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/open_redirect/rust.rs b/src/dynamic/corpus/open_redirect/rust.rs index 4f649596..d8a47599 100644 --- a/src/dynamic/corpus/open_redirect/rust.rs +++ b/src/dynamic/corpus/open_redirect/rust.rs @@ -20,7 +20,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"https://attacker.test/", label: "open-redirect-rust-absolute", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: false, provenance: PayloadProvenance::Curated, @@ -28,7 +30,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ deprecated_at_corpus_version: None, fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/vuln.rs"], oob_nonce_slot: false, - probe_predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + probe_predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], benign_control: Some(PayloadRef { label: "open-redirect-rust-benign", }), @@ -38,7 +42,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ bytes: b"/dashboard", label: "open-redirect-rust-benign", oracle: Oracle::SinkProbe { - predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: ALLOWLIST }], + predicates: &[ProbePredicate::RedirectHostNotIn { + allowlist: ALLOWLIST, + }], }, is_benign: true, provenance: PayloadProvenance::Curated, diff --git a/src/dynamic/corpus/path_trav/rust.rs b/src/dynamic/corpus/path_trav/rust.rs index 81feb067..ce08d50a 100644 --- a/src/dynamic/corpus/path_trav/rust.rs +++ b/src/dynamic/corpus/path_trav/rust.rs @@ -21,7 +21,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ ], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "path-traversal-benign" }), + benign_control: Some(PayloadRef { + label: "path-traversal-benign", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/registry.rs b/src/dynamic/corpus/registry.rs index 1b10da25..9f00f2b1 100644 --- a/src/dynamic/corpus/registry.rs +++ b/src/dynamic/corpus/registry.rs @@ -23,12 +23,12 @@ use std::collections::HashMap; use std::sync::OnceLock; +use super::{CapCorpus, CuratedPayload, Oracle}; use super::{ cmdi, crypto, data_exfil, deserialize, fmt_string, header_injection, json_parse, ldap, open_redirect, path_trav, prototype_pollution, sqli, ssrf, ssti, unauthorized_id, xpath, xss, xxe, }; -use super::{CapCorpus, CuratedPayload, Oracle}; use crate::dynamic::oracle::ProbePredicate; use crate::labels::Cap; use crate::symbol::Lang; @@ -93,7 +93,11 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[ (Cap::HTML_ESCAPE, Lang::Rust, xss::rust::PAYLOADS), (Cap::FMT_STRING, Lang::C, fmt_string::c::PAYLOADS), (Cap::DESERIALIZE, Lang::Java, deserialize::java::PAYLOADS), - (Cap::DESERIALIZE, Lang::Python, deserialize::python::PAYLOADS), + ( + Cap::DESERIALIZE, + Lang::Python, + deserialize::python::PAYLOADS, + ), (Cap::DESERIALIZE, Lang::Php, deserialize::php::PAYLOADS), (Cap::DESERIALIZE, Lang::Ruby, deserialize::ruby::PAYLOADS), (Cap::SSTI, Lang::Python, ssti::python_jinja2::PAYLOADS), @@ -113,20 +117,68 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[ (Cap::XPATH_INJECTION, Lang::Python, xpath::python::PAYLOADS), (Cap::XPATH_INJECTION, Lang::Php, xpath::php::PAYLOADS), (Cap::XPATH_INJECTION, Lang::JavaScript, xpath::js::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Java, header_injection::java::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Python, header_injection::python::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Php, header_injection::php::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Ruby, header_injection::ruby::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::JavaScript, header_injection::js::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Go, header_injection::go::PAYLOADS), - (Cap::HEADER_INJECTION, Lang::Rust, header_injection::rust::PAYLOADS), - (Cap::OPEN_REDIRECT, Lang::Java, open_redirect::java::PAYLOADS), - (Cap::OPEN_REDIRECT, Lang::Python, open_redirect::python::PAYLOADS), + ( + Cap::HEADER_INJECTION, + Lang::Java, + header_injection::java::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::Python, + header_injection::python::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::Php, + header_injection::php::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::Ruby, + header_injection::ruby::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::JavaScript, + header_injection::js::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::Go, + header_injection::go::PAYLOADS, + ), + ( + Cap::HEADER_INJECTION, + Lang::Rust, + header_injection::rust::PAYLOADS, + ), + ( + Cap::OPEN_REDIRECT, + Lang::Java, + open_redirect::java::PAYLOADS, + ), + ( + Cap::OPEN_REDIRECT, + Lang::Python, + open_redirect::python::PAYLOADS, + ), (Cap::OPEN_REDIRECT, Lang::Php, open_redirect::php::PAYLOADS), - (Cap::OPEN_REDIRECT, Lang::Ruby, open_redirect::ruby::PAYLOADS), - (Cap::OPEN_REDIRECT, Lang::JavaScript, open_redirect::js::PAYLOADS), + ( + Cap::OPEN_REDIRECT, + Lang::Ruby, + open_redirect::ruby::PAYLOADS, + ), + ( + Cap::OPEN_REDIRECT, + Lang::JavaScript, + open_redirect::js::PAYLOADS, + ), (Cap::OPEN_REDIRECT, Lang::Go, open_redirect::go::PAYLOADS), - (Cap::OPEN_REDIRECT, Lang::Rust, open_redirect::rust::PAYLOADS), + ( + Cap::OPEN_REDIRECT, + Lang::Rust, + open_redirect::rust::PAYLOADS, + ), ( Cap::PROTOTYPE_POLLUTION, Lang::JavaScript, @@ -142,16 +194,48 @@ const ENTRIES: &[(Cap, Lang, &[CuratedPayload])] = &[ (Cap::CRYPTO, Lang::Php, crypto::php::PAYLOADS), (Cap::CRYPTO, Lang::Go, crypto::go::PAYLOADS), (Cap::CRYPTO, Lang::Rust, crypto::rust::PAYLOADS), - (Cap::JSON_PARSE, Lang::JavaScript, json_parse::javascript::PAYLOADS), + ( + Cap::JSON_PARSE, + Lang::JavaScript, + json_parse::javascript::PAYLOADS, + ), (Cap::JSON_PARSE, Lang::Python, json_parse::python::PAYLOADS), (Cap::JSON_PARSE, Lang::Ruby, json_parse::ruby::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Python, unauthorized_id::python::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Ruby, unauthorized_id::ruby::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Java, unauthorized_id::java::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Php, unauthorized_id::php::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::JavaScript, unauthorized_id::js::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Go, unauthorized_id::go::PAYLOADS), - (Cap::UNAUTHORIZED_ID, Lang::Rust, unauthorized_id::rust::PAYLOADS), + ( + Cap::UNAUTHORIZED_ID, + Lang::Python, + unauthorized_id::python::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::Ruby, + unauthorized_id::ruby::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::Java, + unauthorized_id::java::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::Php, + unauthorized_id::php::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::JavaScript, + unauthorized_id::js::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::Go, + unauthorized_id::go::PAYLOADS, + ), + ( + Cap::UNAUTHORIZED_ID, + Lang::Rust, + unauthorized_id::rust::PAYLOADS, + ), (Cap::DATA_EXFIL, Lang::Python, data_exfil::python::PAYLOADS), (Cap::DATA_EXFIL, Lang::Ruby, data_exfil::ruby::PAYLOADS), (Cap::DATA_EXFIL, Lang::Java, data_exfil::java::PAYLOADS), @@ -355,7 +439,7 @@ pub fn audit_marker_collisions() -> Vec<(&'static str, &'static str, &'static st #[cfg(test)] mod tests { use super::*; - use crate::dynamic::corpus::{benign_payload_for, CORPUS_VERSION}; + use crate::dynamic::corpus::{CORPUS_VERSION, benign_payload_for}; #[test] fn supported_caps_have_payloads() { @@ -404,8 +488,14 @@ mod tests { #[test] fn phase_11_caps_pair_benign_controls_per_lang() { let cases: &[(Cap, &[Lang])] = &[ - (Cap::CRYPTO, &[Lang::Java, Lang::Python, Lang::Php, Lang::Go, Lang::Rust]), - (Cap::JSON_PARSE, &[Lang::JavaScript, Lang::Python, Lang::Ruby]), + ( + Cap::CRYPTO, + &[Lang::Java, Lang::Python, Lang::Php, Lang::Go, Lang::Rust], + ), + ( + Cap::JSON_PARSE, + &[Lang::JavaScript, Lang::Python, Lang::Ruby], + ), ( Cap::UNAUTHORIZED_ID, &[ @@ -434,10 +524,7 @@ mod tests { for (cap, langs) in cases { for lang in *langs { let slice = payloads_for_lang(*cap, *lang); - assert!( - !slice.is_empty(), - "({cap:?}, {lang:?}) must have payloads", - ); + assert!(!slice.is_empty(), "({cap:?}, {lang:?}) must have payloads",); let vuln = slice .iter() .find(|p| !p.is_benign) @@ -596,7 +683,10 @@ mod tests { #[test] fn ssrf_has_oob_nonce_slot() { let has_oob = payloads_for(Cap::SSRF).iter().any(|p| p.oob_nonce_slot); - assert!(has_oob, "SSRF corpus must include an OOB-nonce-slot payload"); + assert!( + has_oob, + "SSRF corpus must include an OOB-nonce-slot payload" + ); } #[test] @@ -617,8 +707,7 @@ mod tests { .find(|p| p.oob_nonce_slot) .expect("must have OOB payload"); let url = "http://127.0.0.1:54321/mynonce"; - let bytes = - materialise_bytes(p, Some(url)).expect("OOB payload materialises with URL"); + let bytes = materialise_bytes(p, Some(url)).expect("OOB payload materialises with URL"); assert_eq!(&*bytes, url.as_bytes()); } @@ -637,7 +726,11 @@ mod tests { (Cap::SQL_QUERY, "sqli-tautology", "sqli-benign"), (Cap::SQL_QUERY, "sqli-union-nyx", "sqli-benign"), (Cap::CODE_EXEC, "cmdi-echo-marker", "cmdi-benign"), - (Cap::FILE_IO, "path-traversal-passwd", "path-traversal-benign"), + ( + Cap::FILE_IO, + "path-traversal-passwd", + "path-traversal-benign", + ), (Cap::SSRF, "ssrf-file-scheme", "ssrf-benign"), (Cap::HTML_ESCAPE, "xss-script-marker", "xss-benign-text"), ]; @@ -723,7 +816,10 @@ mod tests { let mut entries_by_cap: HashMap> = HashMap::new(); for &(cap, lang, slice) in CORPUS.entries { - entries_by_cap.entry(cap.bits()).or_default().push((lang, slice)); + entries_by_cap + .entry(cap.bits()) + .or_default() + .push((lang, slice)); } for (cap_bits, langs) in &entries_by_cap { if langs.len() != 1 { @@ -899,9 +995,8 @@ mod tests { .iter() .find(|p| !p.is_benign) .expect("each lang must have an LDAP vuln payload"); - let resolved = - super::resolve_benign_control_lang(vuln, Cap::LDAP_INJECTION, lang) - .expect("lang-aware benign control must resolve"); + let resolved = super::resolve_benign_control_lang(vuln, Cap::LDAP_INJECTION, lang) + .expect("lang-aware benign control must resolve"); assert!(resolved.is_benign); } } @@ -941,9 +1036,8 @@ mod tests { .iter() .find(|p| !p.is_benign) .expect("each lang must have an XPath vuln payload"); - let resolved = - super::resolve_benign_control_lang(vuln, Cap::XPATH_INJECTION, lang) - .expect("lang-aware benign control must resolve"); + let resolved = super::resolve_benign_control_lang(vuln, Cap::XPATH_INJECTION, lang) + .expect("lang-aware benign control must resolve"); assert!(resolved.is_benign); } } @@ -992,9 +1086,8 @@ mod tests { .iter() .find(|p| !p.is_benign) .expect("each lang must have a HEADER_INJECTION vuln payload"); - let resolved = - super::resolve_benign_control_lang(vuln, Cap::HEADER_INJECTION, lang) - .expect("lang-aware benign control must resolve"); + let resolved = super::resolve_benign_control_lang(vuln, Cap::HEADER_INJECTION, lang) + .expect("lang-aware benign control must resolve"); assert!(resolved.is_benign); } } @@ -1036,9 +1129,8 @@ mod tests { .iter() .find(|p| !p.is_benign) .expect("each lang must have a PROTOTYPE_POLLUTION vuln payload"); - let resolved = - super::resolve_benign_control_lang(vuln, Cap::PROTOTYPE_POLLUTION, lang) - .expect("lang-aware benign control must resolve"); + let resolved = super::resolve_benign_control_lang(vuln, Cap::PROTOTYPE_POLLUTION, lang) + .expect("lang-aware benign control must resolve"); assert!(resolved.is_benign); } } diff --git a/src/dynamic/corpus/sqli/rust.rs b/src/dynamic/corpus/sqli/rust.rs index b8c09ff4..9b25ae4e 100644 --- a/src/dynamic/corpus/sqli/rust.rs +++ b/src/dynamic/corpus/sqli/rust.rs @@ -18,7 +18,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ fixture_paths: &["tests/benchmark/corpus/rust/sqli/sqli_rusqlite_format.rs"], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "sqli-benign" }), + benign_control: Some(PayloadRef { + label: "sqli-benign", + }), no_benign_control_rationale: None, }, CuratedPayload { @@ -32,7 +34,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ fixture_paths: &["tests/benchmark/corpus/rust/sqli/sqli_rusqlite_format.rs"], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "sqli-benign" }), + benign_control: Some(PayloadRef { + label: "sqli-benign", + }), no_benign_control_rationale: None, }, // Benign control: ordinary value that should never produce the SQL marker. diff --git a/src/dynamic/corpus/ssrf/rust.rs b/src/dynamic/corpus/ssrf/rust.rs index a5acd0ff..5dc800c1 100644 --- a/src/dynamic/corpus/ssrf/rust.rs +++ b/src/dynamic/corpus/ssrf/rust.rs @@ -26,7 +26,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ fixture_paths: &["tests/benchmark/corpus/rust/ssrf/ssrf_reqwest.rs"], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "ssrf-benign" }), + benign_control: Some(PayloadRef { + label: "ssrf-benign", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/ssti/java_thymeleaf.rs b/src/dynamic/corpus/ssti/java_thymeleaf.rs index 29c3a799..80c215ad 100644 --- a/src/dynamic/corpus/ssti/java_thymeleaf.rs +++ b/src/dynamic/corpus/ssti/java_thymeleaf.rs @@ -19,9 +19,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/java_thymeleaf/vuln.java", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/java_thymeleaf/vuln.java"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }], benign_control: Some(PayloadRef { @@ -39,9 +37,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/java_thymeleaf/benign.java", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/java_thymeleaf/benign.java"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/ssti/js_handlebars.rs b/src/dynamic/corpus/ssti/js_handlebars.rs index bfb35c01..db1b0e3b 100644 --- a/src/dynamic/corpus/ssti/js_handlebars.rs +++ b/src/dynamic/corpus/ssti/js_handlebars.rs @@ -25,9 +25,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/js_handlebars/vuln.js", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/js_handlebars/vuln.js"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }], benign_control: Some(PayloadRef { @@ -45,9 +43,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/js_handlebars/benign.js", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/js_handlebars/benign.js"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/ssti/php_twig.rs b/src/dynamic/corpus/ssti/php_twig.rs index 8f5666d8..289f9bea 100644 --- a/src/dynamic/corpus/ssti/php_twig.rs +++ b/src/dynamic/corpus/ssti/php_twig.rs @@ -19,9 +19,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/php_twig/vuln.php", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/php_twig/vuln.php"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }], benign_control: Some(PayloadRef { @@ -39,9 +37,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/php_twig/benign.php", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/php_twig/benign.php"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/ssti/python_jinja2.rs b/src/dynamic/corpus/ssti/python_jinja2.rs index 439d1491..9c50cb79 100644 --- a/src/dynamic/corpus/ssti/python_jinja2.rs +++ b/src/dynamic/corpus/ssti/python_jinja2.rs @@ -26,9 +26,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/python_jinja2/vuln.py", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/python_jinja2/vuln.py"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }], benign_control: Some(PayloadRef { @@ -46,9 +44,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/python_jinja2/benign.py", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/python_jinja2/benign.py"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/ssti/ruby_erb.rs b/src/dynamic/corpus/ssti/ruby_erb.rs index 1e8a4576..e8049ec2 100644 --- a/src/dynamic/corpus/ssti/ruby_erb.rs +++ b/src/dynamic/corpus/ssti/ruby_erb.rs @@ -19,9 +19,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/ruby_erb/vuln.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/ruby_erb/vuln.rb"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }], benign_control: Some(PayloadRef { @@ -39,9 +37,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 8, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/ssti/ruby_erb/benign.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/ssti/ruby_erb/benign.rb"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/xss/rust.rs b/src/dynamic/corpus/xss/rust.rs index e39917a8..7ff2396c 100644 --- a/src/dynamic/corpus/xss/rust.rs +++ b/src/dynamic/corpus/xss/rust.rs @@ -18,7 +18,9 @@ pub const PAYLOADS: &[CuratedPayload] = &[ fixture_paths: &["tests/benchmark/corpus/rust/xss/axum_html/main.rs"], oob_nonce_slot: false, probe_predicates: &[], - benign_control: Some(PayloadRef { label: "xss-benign-text" }), + benign_control: Some(PayloadRef { + label: "xss-benign-text", + }), no_benign_control_rationale: None, }, CuratedPayload { diff --git a/src/dynamic/corpus/xxe/go.rs b/src/dynamic/corpus/xxe/go.rs index 44c4deb8..60a77f79 100644 --- a/src/dynamic/corpus/xxe/go.rs +++ b/src/dynamic/corpus/xxe/go.rs @@ -29,9 +29,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 15, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/go/vuln.go", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/go/vuln.go"], oob_nonce_slot: true, probe_predicates: &[], benign_control: None, @@ -57,9 +55,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/go/vuln.go", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/go/vuln.go"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::XxeEntityExpanded { require_expanded: true, @@ -82,9 +78,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/go/benign.go", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/go/benign.go"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/xxe/java.rs b/src/dynamic/corpus/xxe/java.rs index 885b8aaf..70436e5f 100644 --- a/src/dynamic/corpus/xxe/java.rs +++ b/src/dynamic/corpus/xxe/java.rs @@ -31,9 +31,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 15, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/java/Vuln.java", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/java/Vuln.java"], oob_nonce_slot: true, probe_predicates: &[], benign_control: None, @@ -59,9 +57,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/java/Vuln.java", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/java/Vuln.java"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::XxeEntityExpanded { require_expanded: true, @@ -84,9 +80,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/java/Benign.java", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/java/Benign.java"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/xxe/php.rs b/src/dynamic/corpus/xxe/php.rs index 6d62fa4a..d0df682a 100644 --- a/src/dynamic/corpus/xxe/php.rs +++ b/src/dynamic/corpus/xxe/php.rs @@ -29,9 +29,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 15, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/php/vuln.php", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/php/vuln.php"], oob_nonce_slot: true, probe_predicates: &[], benign_control: None, @@ -57,9 +55,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/php/vuln.php", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/php/vuln.php"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::XxeEntityExpanded { require_expanded: true, @@ -82,9 +78,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/php/benign.php", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/php/benign.php"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/xxe/python.rs b/src/dynamic/corpus/xxe/python.rs index 7eb1163b..da04b00a 100644 --- a/src/dynamic/corpus/xxe/python.rs +++ b/src/dynamic/corpus/xxe/python.rs @@ -39,9 +39,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 15, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/python/vuln.py", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/python/vuln.py"], oob_nonce_slot: true, probe_predicates: &[], benign_control: None, @@ -68,9 +66,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/python/vuln.py", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/python/vuln.py"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::XxeEntityExpanded { require_expanded: true, @@ -93,9 +89,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/python/benign.py", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/python/benign.py"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/corpus/xxe/ruby.rs b/src/dynamic/corpus/xxe/ruby.rs index 6dd09497..6cc3ee87 100644 --- a/src/dynamic/corpus/xxe/ruby.rs +++ b/src/dynamic/corpus/xxe/ruby.rs @@ -28,9 +28,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 15, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/ruby/vuln.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/ruby/vuln.rb"], oob_nonce_slot: true, probe_predicates: &[], benign_control: None, @@ -56,9 +54,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/ruby/vuln.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/ruby/vuln.rb"], oob_nonce_slot: false, probe_predicates: &[ProbePredicate::XxeEntityExpanded { require_expanded: true, @@ -81,9 +77,7 @@ pub const PAYLOADS: &[CuratedPayload] = &[ provenance: PayloadProvenance::Curated, since_corpus_version: 9, deprecated_at_corpus_version: None, - fixture_paths: &[ - "tests/dynamic_fixtures/xxe/ruby/benign.rb", - ], + fixture_paths: &["tests/dynamic_fixtures/xxe/ruby/benign.rb"], oob_nonce_slot: false, probe_predicates: &[], benign_control: None, diff --git a/src/dynamic/differential.rs b/src/dynamic/differential.rs index 3861bd73..04b4cd96 100644 --- a/src/dynamic/differential.rs +++ b/src/dynamic/differential.rs @@ -113,7 +113,10 @@ mod tests { #[test] fn rule_a_both_fire_is_collision() { - assert_eq!(evaluate(true, true), DifferentialVerdict::OracleCollisionSuspected); + assert_eq!( + evaluate(true, true), + DifferentialVerdict::OracleCollisionSuspected + ); } #[test] @@ -128,7 +131,10 @@ mod tests { #[test] fn rule_d_only_benign_fires_is_reversed() { - assert_eq!(evaluate(false, true), DifferentialVerdict::ReversedDifferential); + assert_eq!( + evaluate(false, true), + DifferentialVerdict::ReversedDifferential + ); } #[test] diff --git a/src/dynamic/environment.rs b/src/dynamic/environment.rs index 9761d707..98c42903 100644 --- a/src/dynamic/environment.rs +++ b/src/dynamic/environment.rs @@ -33,12 +33,12 @@ //! source file. The 10 MiB ceiling protects against runaway full-tree //! copy regressions called out in the Phase 09 acceptance. -use crate::callgraph::{callers_of, CallGraph}; +use crate::callgraph::{CallGraph, callers_of}; use crate::dynamic::spec::HarnessSpec; use crate::dynamic::toolchain::{self, ToolchainResolution}; use crate::summary::GlobalSummaries; use crate::symbol::{FuncKey, Lang}; -use crate::utils::project::{detect_frameworks, DetectedFramework}; +use crate::utils::project::{DetectedFramework, detect_frameworks}; use std::collections::HashSet; use std::io; use std::path::{Path, PathBuf}; @@ -139,7 +139,12 @@ pub fn extract_env_var_references(entry_file: &Path, lang: Lang) -> Vec ], Lang::JavaScript | Lang::TypeScript => &["process.env.", "process.env["], Lang::Java => &["System.getenv(", "getenv("], - Lang::Rust => &["std::env::var(", "env::var(", "env::var_os(", "std::env::var_os("], + Lang::Rust => &[ + "std::env::var(", + "env::var(", + "env::var_os(", + "std::env::var_os(", + ], Lang::Go => &["os.Getenv(", "os.LookupEnv("], Lang::Php => &["getenv(", "$_ENV[", "$_SERVER["], Lang::Ruby => &["ENV[", "ENV.fetch(", "ENV.fetch "], @@ -161,9 +166,12 @@ pub fn extract_env_var_references(entry_file: &Path, lang: Lang) -> Vec _ => extract_quoted_arg(tail), }; if let Some(name) = name - && !name.is_empty() && is_env_var_name(&name) && seen.insert(name.clone()) { - out.push(name); - } + && !name.is_empty() + && is_env_var_name(&name) + && seen.insert(name.clone()) + { + out.push(name); + } } } out @@ -199,7 +207,9 @@ fn extract_quoted_arg(s: &str) -> Option { if i >= bytes.len() { return None; } - std::str::from_utf8(&bytes[start..i]).ok().map(|s| s.to_owned()) + std::str::from_utf8(&bytes[start..i]) + .ok() + .map(|s| s.to_owned()) } /// Extract a bare identifier (e.g. `FOO` in `process.env.FOO`). Stops at @@ -241,11 +251,7 @@ fn is_env_var_name(s: &str) -> bool { /// /// Returned in deterministic source-order so two runs against the same /// inputs produce byte-identical env layouts. -pub fn build_secret_bag( - entry_file: &Path, - lang: Lang, - spec_hash: &str, -) -> Vec<(String, String)> { +pub fn build_secret_bag(entry_file: &Path, lang: Lang, spec_hash: &str) -> Vec<(String, String)> { let mut out: Vec<(String, String)> = Vec::new(); for name in extract_env_var_references(entry_file, lang) { let val = derive_secret(spec_hash, &name); @@ -288,9 +294,33 @@ const CONFIG_FILE_CANDIDATES: &[&str] = &[ /// user's pinned dependency set. Order is significant only insofar as /// the first match wins for [`CapturedDeps::lockfile_origin`]. const MANIFEST_FILES_BY_LANG: &[(Lang, &[&str])] = &[ - (Lang::Python, &["requirements.txt", "pyproject.toml", "Pipfile", "Pipfile.lock"]), - (Lang::JavaScript, &["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"]), - (Lang::TypeScript, &["package.json", "package-lock.json", "yarn.lock", "tsconfig.json"]), + ( + Lang::Python, + &[ + "requirements.txt", + "pyproject.toml", + "Pipfile", + "Pipfile.lock", + ], + ), + ( + Lang::JavaScript, + &[ + "package.json", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + ], + ), + ( + Lang::TypeScript, + &[ + "package.json", + "package-lock.json", + "yarn.lock", + "tsconfig.json", + ], + ), (Lang::Rust, &["Cargo.toml", "Cargo.lock"]), (Lang::Go, &["go.mod", "go.sum"]), (Lang::Java, &["pom.xml", "build.gradle", "build.gradle.kts"]), @@ -470,7 +500,8 @@ pub fn capture_project_dependencies_with_context( let manifests = collect_manifest_files(spec.lang, project_root); let lockfile = manifests.first().cloned(); - let source_closure = compute_source_closure(&entry_file, project_root, spec, summaries, callgraph); + let source_closure = + compute_source_closure(&entry_file, project_root, spec, summaries, callgraph); CapturedDeps { project_root: project_root.to_path_buf(), @@ -575,13 +606,8 @@ pub fn stage_workdir_full( Some(r) => r, None => continue, }; - running_bytes = copy_into_workdir( - manifest, - workdir, - &rel, - running_bytes, - &mut staged_sources, - )?; + running_bytes = + copy_into_workdir(manifest, workdir, &rel, running_bytes, &mut staged_sources)?; if lockfile_in_workdir.is_none() { lockfile_in_workdir = Some(workdir.join(&rel)); } @@ -596,8 +622,7 @@ pub fn stage_workdir_full( Some(r) => r, None => PathBuf::from(cfg.file_name().unwrap_or_default()), }; - running_bytes = - copy_into_workdir(cfg, workdir, &rel, running_bytes, &mut staged_sources)?; + running_bytes = copy_into_workdir(cfg, workdir, &rel, running_bytes, &mut staged_sources)?; } // Phase 11 — Track D.4: populate the per-spec secret bag for every @@ -642,14 +667,12 @@ fn copy_into_workdir( }; let size = metadata.len(); if running_bytes.saturating_add(size) > MAX_WORKDIR_BYTES { - return Err(io::Error::other( - format!( - "staged workdir would exceed {} bytes (next file `{}` = {} bytes)", - MAX_WORKDIR_BYTES, - rel.display(), - size - ), - )); + return Err(io::Error::other(format!( + "staged workdir would exceed {} bytes (next file `{}` = {} bytes)", + MAX_WORKDIR_BYTES, + rel.display(), + size + ))); } let dest = workdir.join(rel); if let Some(parent) = dest.parent() { @@ -669,8 +692,14 @@ fn resolve_under_root(project_root: &Path, entry_file: &str) -> PathBuf { } fn rel_under_root(path: &Path, root: &Path) -> Option { - let abs_path = path.canonicalize().ok().unwrap_or_else(|| path.to_path_buf()); - let abs_root = root.canonicalize().ok().unwrap_or_else(|| root.to_path_buf()); + let abs_path = path + .canonicalize() + .ok() + .unwrap_or_else(|| path.to_path_buf()); + let abs_root = root + .canonicalize() + .ok() + .unwrap_or_else(|| root.to_path_buf()); abs_path .strip_prefix(&abs_root) .ok() @@ -729,9 +758,11 @@ fn collect_config_files(entry_file: &Path, project_root: &Path) -> Vec let mut v = Vec::new(); v.push(project_root.to_path_buf()); if let Some(parent) = entry_file.parent() - && parent != project_root && parent.starts_with(project_root) { - v.push(parent.to_path_buf()); - } + && parent != project_root + && parent.starts_with(project_root) + { + v.push(parent.to_path_buf()); + } v }; for dir in &dirs { @@ -1253,7 +1284,11 @@ import './local-thing'; "from flask import Flask, request\nimport os\nimport requests\n", ) .unwrap(); - fs::write(root.join("requirements.txt"), "Flask==2.3.0\nrequests>=2.28\n").unwrap(); + fs::write( + root.join("requirements.txt"), + "Flask==2.3.0\nrequests>=2.28\n", + ) + .unwrap(); let spec = fake_spec("app.py", Lang::Python); let captured = capture_project_dependencies(root, &spec); assert!(captured.direct_deps.contains(&"flask".to_owned())); diff --git a/src/dynamic/framework/adapters/go_chi.rs b/src/dynamic/framework/adapters/go_chi.rs index 85cc43bb..c9203743 100644 --- a/src/dynamic/framework/adapters/go_chi.rs +++ b/src/dynamic/framework/adapters/go_chi.rs @@ -119,8 +119,10 @@ mod tests { fn skips_when_chi_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; let tree = parse(src); - assert!(GoChiAdapter - .detect(&summary("Show"), tree.root_node(), src) - .is_none()); + assert!( + GoChiAdapter + .detect(&summary("Show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/go_echo.rs b/src/dynamic/framework/adapters/go_echo.rs index 55db4023..717c1737 100644 --- a/src/dynamic/framework/adapters/go_echo.rs +++ b/src/dynamic/framework/adapters/go_echo.rs @@ -120,8 +120,10 @@ mod tests { fn skips_when_echo_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; let tree = parse(src); - assert!(GoEchoAdapter - .detect(&summary("Show"), tree.root_node(), src) - .is_none()); + assert!( + GoEchoAdapter + .detect(&summary("Show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/go_fiber.rs b/src/dynamic/framework/adapters/go_fiber.rs index 2a114d29..6c9dcbfd 100644 --- a/src/dynamic/framework/adapters/go_fiber.rs +++ b/src/dynamic/framework/adapters/go_fiber.rs @@ -126,8 +126,10 @@ mod tests { fn skips_when_fiber_not_imported() { let src: &[u8] = b"package main\nfunc Show() {}\n"; let tree = parse(src); - assert!(GoFiberAdapter - .detect(&summary("Show"), tree.root_node(), src) - .is_none()); + assert!( + GoFiberAdapter + .detect(&summary("Show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/go_gin.rs b/src/dynamic/framework/adapters/go_gin.rs index 7114c2b1..daad0b96 100644 --- a/src/dynamic/framework/adapters/go_gin.rs +++ b/src/dynamic/framework/adapters/go_gin.rs @@ -124,9 +124,11 @@ mod tests { fn skips_when_gin_not_imported() { let src: &[u8] = b"package main\nfunc Show(id string) {}\n"; let tree = parse(src); - assert!(GoGinAdapter - .detect(&summary("Show"), tree.root_node(), src) - .is_none()); + assert!( + GoGinAdapter + .detect(&summary("Show"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -134,9 +136,11 @@ mod tests { let src: &[u8] = b"package main\nimport \"github.com/gin-gonic/gin\"\nfunc init() { r := gin.Default(); r.GET(\"/users\", Show) }\nfunc Helper(x string) {}\n"; let tree = parse(src); - assert!(GoGinAdapter - .detect(&summary("Helper"), tree.root_node(), src) - .is_none()); + assert!( + GoGinAdapter + .detect(&summary("Helper"), tree.root_node(), src) + .is_none() + ); } #[test] diff --git a/src/dynamic/framework/adapters/go_routes.rs b/src/dynamic/framework/adapters/go_routes.rs index afc85e93..d43725c2 100644 --- a/src/dynamic/framework/adapters/go_routes.rs +++ b/src/dynamic/framework/adapters/go_routes.rs @@ -83,22 +83,13 @@ fn contains_any(haystack: &[u8], needles: &[&[u8]]) -> bool { /// Find a top-level `function_declaration` or a `method_declaration` /// whose name equals `target`. Returns the matching node. -pub fn find_go_function<'a>( - root: Node<'a>, - bytes: &'a [u8], - target: &str, -) -> Option> { +pub fn find_go_function<'a>(root: Node<'a>, bytes: &'a [u8], target: &str) -> Option> { let mut hit: Option> = None; walk_go(root, bytes, target, &mut hit); hit } -fn walk_go<'a>( - node: Node<'a>, - bytes: &'a [u8], - target: &str, - out: &mut Option>, -) { +fn walk_go<'a>(node: Node<'a>, bytes: &'a [u8], target: &str, out: &mut Option>) { if out.is_some() { return; } @@ -136,9 +127,10 @@ pub fn go_formal_names(func: Node<'_>, bytes: &[u8]) -> Vec { let mut pc = p.walk(); for c in p.named_children(&mut pc) { if c.kind() == "identifier" - && let Ok(text) = c.utf8_text(bytes) { - out.push(text.to_owned()); - } + && let Ok(text) = c.utf8_text(bytes) + { + out.push(text.to_owned()); + } } } out @@ -428,8 +420,7 @@ mod tests { let src: &[u8] = b"package main\nfunc init() { r := gin.New(); r.GET(\"/u/:id\", Show) }\nfunc Show(c interface{}) {}\n"; let tree = parse(src); - let (method, path) = - find_route_for_callee(tree.root_node(), src, "Show").expect("hit"); + let (method, path) = find_route_for_callee(tree.root_node(), src, "Show").expect("hit"); assert_eq!(method, HttpMethod::GET); assert_eq!(path, "/u/:id"); } @@ -439,8 +430,7 @@ mod tests { let src: &[u8] = b"package main\nfunc init() { r := chi.NewRouter(); r.Get(\"/x\", controllers.Show) }\n"; let tree = parse(src); - let (method, path) = - find_route_for_callee(tree.root_node(), src, "Show").expect("hit"); + let (method, path) = find_route_for_callee(tree.root_node(), src, "Show").expect("hit"); assert_eq!(method, HttpMethod::GET); assert_eq!(path, "/x"); } diff --git a/src/dynamic/framework/adapters/header_go.rs b/src/dynamic/framework/adapters/header_go.rs index 1a0d530b..92e41641 100644 --- a/src/dynamic/framework/adapters/header_go.rs +++ b/src/dynamic/framework/adapters/header_go.rs @@ -133,9 +133,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("Set")], ..Default::default() }; - assert!(HeaderGoAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderGoAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -146,9 +148,11 @@ mod tests { name: "Add".into(), ..Default::default() }; - assert!(HeaderGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -174,9 +178,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -195,9 +201,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderGoAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderGoAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -213,8 +221,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/header_java.rs b/src/dynamic/framework/adapters/header_java.rs index 124b6b04..6021e685 100644 --- a/src/dynamic/framework/adapters/header_java.rs +++ b/src/dynamic/framework/adapters/header_java.rs @@ -17,7 +17,15 @@ const ADAPTER_NAME: &str = "header-java"; fn callee_is_header_setter(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "setHeader" | "addHeader" | "setDateHeader" | "addDateHeader" | "setIntHeader" | "addIntHeader") + matches!( + last, + "setHeader" + | "addHeader" + | "setDateHeader" + | "addDateHeader" + | "setIntHeader" + | "addIntHeader" + ) } fn source_imports_servlet(file_bytes: &[u8]) -> bool { @@ -110,9 +118,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("setHeader")], ..Default::default() }; - assert!(HeaderJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -123,9 +133,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -143,8 +155,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/header_js.rs b/src/dynamic/framework/adapters/header_js.rs index 52587f73..962c16a6 100644 --- a/src/dynamic/framework/adapters/header_js.rs +++ b/src/dynamic/framework/adapters/header_js.rs @@ -18,7 +18,10 @@ const ADAPTER_NAME: &str = "header-js"; fn callee_is_header_setter(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "setHeader" | "header" | "set" | "writeHead" | "append") + matches!( + last, + "setHeader" | "header" | "set" | "writeHead" | "append" + ) } fn source_uses_node_http(file_bytes: &[u8]) -> bool { @@ -115,9 +118,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("setHeader")], ..Default::default() }; - assert!(HeaderJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -128,9 +133,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -146,8 +153,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/header_php.rs b/src/dynamic/framework/adapters/header_php.rs index 454997ac..8b2a4230 100644 --- a/src/dynamic/framework/adapters/header_php.rs +++ b/src/dynamic/framework/adapters/header_php.rs @@ -106,9 +106,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("header")], ..Default::default() }; - assert!(HeaderPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -119,15 +121,16 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_value_url_encoded() { - let src: &[u8] = - b" bool { matches!( last, "__setitem__" | "set_header" | "setdefault" | "add_header" | "append" - ) || matches!(name, "Response.headers.__setitem__" | "make_response" | "Response.headers.add") + ) || matches!( + name, + "Response.headers.__setitem__" | "make_response" | "Response.headers.add" + ) } fn source_imports_python_web(file_bytes: &[u8]) -> bool { @@ -116,9 +119,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("__setitem__")], ..Default::default() }; - assert!(HeaderPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -129,9 +134,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -149,8 +156,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/header_ruby.rs b/src/dynamic/framework/adapters/header_ruby.rs index 879c193f..f6df08c4 100644 --- a/src/dynamic/framework/adapters/header_ruby.rs +++ b/src/dynamic/framework/adapters/header_ruby.rs @@ -132,9 +132,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("set_header")], ..Default::default() }; - assert!(HeaderRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -145,9 +147,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -168,9 +172,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -188,9 +194,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -207,8 +215,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/header_rust.rs b/src/dynamic/framework/adapters/header_rust.rs index dae818d4..09023ff7 100644 --- a/src/dynamic/framework/adapters/header_rust.rs +++ b/src/dynamic/framework/adapters/header_rust.rs @@ -132,9 +132,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("insert")], ..Default::default() }; - assert!(HeaderRustAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderRustAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -145,9 +147,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(HeaderRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -173,9 +177,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -193,9 +199,11 @@ mod tests { }], ..Default::default() }; - assert!(HeaderRustAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + HeaderRustAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -215,8 +223,10 @@ mod tests { ], ..Default::default() }; - assert!(HeaderRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + HeaderRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_deserialize.rs b/src/dynamic/framework/adapters/java_deserialize.rs index 95fd4983..29992f94 100644 --- a/src/dynamic/framework/adapters/java_deserialize.rs +++ b/src/dynamic/framework/adapters/java_deserialize.rs @@ -90,8 +90,10 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(JavaDeserializeAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JavaDeserializeAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_micronaut.rs b/src/dynamic/framework/adapters/java_micronaut.rs index 5ea787c7..d097f490 100644 --- a/src/dynamic/framework/adapters/java_micronaut.rs +++ b/src/dynamic/framework/adapters/java_micronaut.rs @@ -45,10 +45,7 @@ fn class_path_prefix(class: Node<'_>, bytes: &[u8]) -> Option { hit } -fn method_verb_and_path( - method: Node<'_>, - bytes: &[u8], -) -> Option<(HttpMethod, String)> { +fn method_verb_and_path(method: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> { let mut hit: Option<(HttpMethod, String)> = None; iter_annotations(method, bytes, |ann, name| { if hit.is_some() { @@ -155,17 +152,21 @@ mod tests { fn skips_non_micronaut_file() { let src: &[u8] = b"@Controller\npublic class C {\n @GetMapping(\"/x\")\n public String x() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaMicronautAdapter - .detect(&summary("x"), tree.root_node(), src) - .is_none()); + assert!( + JavaMicronautAdapter + .detect(&summary("x"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_method_without_micronaut_verb() { let src: &[u8] = b"import io.micronaut.http.annotation.Controller;\n@Controller(\"/api\")\npublic class V {\n public String helper() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaMicronautAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + JavaMicronautAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_quarkus.rs b/src/dynamic/framework/adapters/java_quarkus.rs index 1321ed3d..75a2805c 100644 --- a/src/dynamic/framework/adapters/java_quarkus.rs +++ b/src/dynamic/framework/adapters/java_quarkus.rs @@ -39,17 +39,15 @@ fn class_path_prefix(class: Node<'_>, bytes: &[u8]) -> String { let mut prefix = String::new(); iter_annotations(class, bytes, |ann, name| { if name == "Path" - && let Some(p) = annotation_string_arg(ann, bytes) { - prefix = p; - } + && let Some(p) = annotation_string_arg(ann, bytes) + { + prefix = p; + } }); prefix } -fn method_verb_and_path( - method: Node<'_>, - bytes: &[u8], -) -> Option<(HttpMethod, String)> { +fn method_verb_and_path(method: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> { let mut verb: Option = None; let mut path = String::new(); iter_annotations(method, bytes, |ann, name| { @@ -57,9 +55,10 @@ fn method_verb_and_path( verb = Some(v); } if name == "Path" - && let Some(p) = annotation_string_arg(ann, bytes) { - path = p; - } + && let Some(p) = annotation_string_arg(ann, bytes) + { + path = p; + } }); Some((verb?, path)) } @@ -157,17 +156,21 @@ mod tests { fn skips_non_quarkus_file() { let src: &[u8] = b"@RestController\npublic class C {\n @GetMapping(\"/x\")\n public String x() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaQuarkusAdapter - .detect(&summary("x"), tree.root_node(), src) - .is_none()); + assert!( + JavaQuarkusAdapter + .detect(&summary("x"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_method_without_verb_annotation() { let src: &[u8] = b"import jakarta.ws.rs.Path;\n@Path(\"/api\")\npublic class V {\n public String helper() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaQuarkusAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + JavaQuarkusAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_routes.rs b/src/dynamic/framework/adapters/java_routes.rs index 08963efc..eed1da73 100644 --- a/src/dynamic/framework/adapters/java_routes.rs +++ b/src/dynamic/framework/adapters/java_routes.rs @@ -77,11 +77,7 @@ pub fn source_imports_micronaut(bytes: &[u8]) -> bool { pub fn source_imports_servlet(bytes: &[u8]) -> bool { let has_canonical = contains_any( bytes, - &[ - b"javax.servlet", - b"jakarta.servlet", - b"extends HttpServlet", - ], + &[b"javax.servlet", b"jakarta.servlet", b"extends HttpServlet"], ); if has_canonical { return true; @@ -113,12 +109,7 @@ pub fn find_class_with_method<'a>( hit } -fn walk<'a>( - node: Node<'a>, - bytes: &[u8], - target: &str, - out: &mut Option<(Node<'a>, Node<'a>)>, -) { +fn walk<'a>(node: Node<'a>, bytes: &[u8], target: &str, out: &mut Option<(Node<'a>, Node<'a>)>) { if out.is_some() { return; } @@ -126,21 +117,22 @@ fn walk<'a>( && let Some(body) = node .child_by_field_name("body") .or_else(|| named_child_of_kind(node, "class_body")) - { - let mut cur = body.walk(); - for member in body.children(&mut cur) { - if member.kind() != "method_declaration" { - continue; - } - if let Some(name) = member - .child_by_field_name("name") - .and_then(|n| n.utf8_text(bytes).ok()) - && name == target { - *out = Some((node, member)); - return; - } + { + let mut cur = body.walk(); + for member in body.children(&mut cur) { + if member.kind() != "method_declaration" { + continue; + } + if let Some(name) = member + .child_by_field_name("name") + .and_then(|n| n.utf8_text(bytes).ok()) + && name == target + { + *out = Some((node, member)); + return; } } + } let mut cur = node.walk(); for child in node.children(&mut cur) { walk(child, bytes, target, out); @@ -173,7 +165,10 @@ pub fn annotation_string_arg(ann: Node<'_>, bytes: &[u8]) -> Option { // Try `value = "…"` / `path = "…"` first so the keyword form is // not accidentally captured by the bare-string scan. for key in ["value", "path"] { - if let Some(start) = raw.find(&format!("{key} = ")).or_else(|| raw.find(&format!("{key}="))) { + if let Some(start) = raw + .find(&format!("{key} = ")) + .or_else(|| raw.find(&format!("{key}="))) + { let after = &raw[start..]; if let Some(open) = after.find('"') { let rest = &after[open + 1..]; @@ -300,16 +295,17 @@ pub fn extract_path_placeholders(path: &str) -> Vec { let mut i = 0; while i < bytes.len() { if bytes[i] == b'{' - && let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') { - let inner = &path[i + 1..i + 1 + end]; - let inner_name = inner.split(':').next().unwrap_or(inner).trim(); - let name = inner_name.strip_prefix('*').unwrap_or(inner_name); - if !name.is_empty() && !out.iter().any(|n| n == name) { - out.push(name.to_owned()); - } - i += end + 2; - continue; + && let Some(end) = bytes[i + 1..].iter().position(|&b| b == b'}') + { + let inner = &path[i + 1..i + 1 + end]; + let inner_name = inner.split(':').next().unwrap_or(inner).trim(); + let name = inner_name.strip_prefix('*').unwrap_or(inner_name); + if !name.is_empty() && !out.iter().any(|n| n == name) { + out.push(name.to_owned()); } + i += end + 2; + continue; + } i += 1; } out @@ -469,8 +465,7 @@ mod tests { #[test] fn class_extends_detects_servlet() { - let src: &[u8] = - b"public class V extends HttpServlet { public void doGet() {} }\n"; + let src: &[u8] = b"public class V extends HttpServlet { public void doGet() {} }\n"; let tree = parse(src); let (class, _) = find_class_with_method(tree.root_node(), src, "doGet").unwrap(); assert!(class_extends(class, src, "HttpServlet")); diff --git a/src/dynamic/framework/adapters/java_servlet.rs b/src/dynamic/framework/adapters/java_servlet.rs index 1fb92df6..0c2dfdc5 100644 --- a/src/dynamic/framework/adapters/java_servlet.rs +++ b/src/dynamic/framework/adapters/java_servlet.rs @@ -126,10 +126,12 @@ mod tests { let route = binding.route.unwrap(); assert_eq!(route.method, HttpMethod::GET); assert_eq!(route.path, "/admin"); - assert!(binding - .request_params - .iter() - .all(|p| matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .all(|p| matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -157,19 +159,24 @@ mod tests { #[test] fn skips_when_method_name_is_not_a_servlet_verb() { - let src: &[u8] = b"public class V extends HttpServlet { public void run(HttpServletRequest req) {} }\n"; + let src: &[u8] = + b"public class V extends HttpServlet { public void run(HttpServletRequest req) {} }\n"; let tree = parse(src); - assert!(JavaServletAdapter - .detect(&summary("run"), tree.root_node(), src) - .is_none()); + assert!( + JavaServletAdapter + .detect(&summary("run"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_no_servlet_signature_markers() { let src: &[u8] = b"public class V {\n public void doGet(String x) {}\n}\n"; let tree = parse(src); - assert!(JavaServletAdapter - .detect(&summary("doGet"), tree.root_node(), src) - .is_none()); + assert!( + JavaServletAdapter + .detect(&summary("doGet"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_spring.rs b/src/dynamic/framework/adapters/java_spring.rs index bf71c05c..d66f7809 100644 --- a/src/dynamic/framework/adapters/java_spring.rs +++ b/src/dynamic/framework/adapters/java_spring.rs @@ -49,17 +49,15 @@ fn class_route_prefix(class: Node<'_>, bytes: &[u8]) -> String { let mut prefix = String::new(); iter_annotations(class, bytes, |ann, name| { if name == "RequestMapping" - && let Some(p) = annotation_string_arg(ann, bytes) { - prefix = p; - } + && let Some(p) = annotation_string_arg(ann, bytes) + { + prefix = p; + } }); prefix } -fn method_route( - method: Node<'_>, - bytes: &[u8], -) -> Option<(HttpMethod, String)> { +fn method_route(method: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> { let mut hit: Option<(HttpMethod, String)> = None; iter_annotations(method, bytes, |ann, name| { if hit.is_some() { @@ -100,7 +98,10 @@ impl FrameworkAdapter for JavaSpringAdapter { // Quarkus / JAX-RS files often re-use `@Path` but the brief // routes those through `java-quarkus`; skip when the file // looks like Quarkus and is not also a Spring controller. - if source_imports_quarkus(file_bytes) && !file_bytes.windows(15).any(|w| w == b"@RestController") && !file_bytes.windows(11).any(|w| w == b"@Controller") { + if source_imports_quarkus(file_bytes) + && !file_bytes.windows(15).any(|w| w == b"@RestController") + && !file_bytes.windows(11).any(|w| w == b"@Controller") + { return None; } let (class, method) = find_class_with_method(ast, file_bytes, &summary.name)?; @@ -210,26 +211,32 @@ mod tests { let src: &[u8] = b"@RequestMapping(\"/api\")\npublic class C {\n @GetMapping(\"/x\")\n public String x() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaSpringAdapter - .detect(&summary("x"), tree.root_node(), src) - .is_none()); + assert!( + JavaSpringAdapter + .detect(&summary("x"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_quarkus_file() { let src: &[u8] = b"import io.quarkus.runtime.Quarkus;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\n@Path(\"/run\")\npublic class Q {\n @GET\n public String run() { return \"\"; }\n}\n"; let tree = parse(src); - assert!(JavaSpringAdapter - .detect(&summary("run"), tree.root_node(), src) - .is_none()); + assert!( + JavaSpringAdapter + .detect(&summary("run"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_plain_function() { let src: &[u8] = b"public class C { public int add(int a, int b) { return a + b; } }\n"; let tree = parse(src); - assert!(JavaSpringAdapter - .detect(&summary("add"), tree.root_node(), src) - .is_none()); + assert!( + JavaSpringAdapter + .detect(&summary("add"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/java_thymeleaf.rs b/src/dynamic/framework/adapters/java_thymeleaf.rs index 8494a673..51133187 100644 --- a/src/dynamic/framework/adapters/java_thymeleaf.rs +++ b/src/dynamic/framework/adapters/java_thymeleaf.rs @@ -123,9 +123,11 @@ mod tests { let src: &[u8] = b"import org.thymeleaf.TemplateEngine;\npublic class V { public static String run(String body) { TemplateEngine e = new TemplateEngine(); return e.process(body, null); } }\n"; let tree = parse_java(src); let summary = summary_for("run", &["body"], &[0]); - assert!(JavaThymeleafAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + JavaThymeleafAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -137,9 +139,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(JavaThymeleafAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JavaThymeleafAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -149,9 +153,11 @@ mod tests { let src: &[u8] = b"// org.thymeleaf.TemplateEngine is great\npublic class V { public static String run(String body) { TemplateEngine e = new TemplateEngine(); return e.process(\"static\", null); } }\n"; let tree = parse_java(src); let summary = summary_for("run", &["body"], &[0]); - assert!(JavaThymeleafAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JavaThymeleafAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -159,8 +165,10 @@ mod tests { let src: &[u8] = b"import org.thymeleaf.TemplateEngine;\npublic class V { public static String run(String body) { TemplateEngine e = new TemplateEngine(); return e.process(body, null); } }\n"; let tree = parse_java(src); let summary = summary_for("run", &["body"], &[]); - assert!(JavaThymeleafAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JavaThymeleafAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_express.rs b/src/dynamic/framework/adapters/js_express.rs index 9d7c04e4..a3643d54 100644 --- a/src/dynamic/framework/adapters/js_express.rs +++ b/src/dynamic/framework/adapters/js_express.rs @@ -107,10 +107,18 @@ mod tests { let route = binding.route.as_ref().unwrap(); assert_eq!(route.method, HttpMethod::GET); assert_eq!(route.path, "/users/:id"); - assert!(binding.request_params.iter().any(|p| p.name == "req" - && matches!(p.source, ParamSource::Implicit))); - assert!(binding.request_params.iter().any(|p| p.name == "res" - && matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "req" && matches!(p.source, ParamSource::Implicit)) + ); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "res" && matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -147,9 +155,11 @@ mod tests { function handler(ctx) { ctx.body = 'ok'; }\n\ app.get('/x', handler);\n"; let tree = parse_js(src); - assert!(JsExpressAdapter - .detect(&summary("handler"), tree.root_node(), src) - .is_none()); + assert!( + JsExpressAdapter + .detect(&summary("handler"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -159,8 +169,10 @@ mod tests { function other(req, res) { res.send('x'); }\n\ app.get('/x', other);\n"; let tree = parse_js(src); - assert!(JsExpressAdapter - .detect(&summary("missing"), tree.root_node(), src) - .is_none()); + assert!( + JsExpressAdapter + .detect(&summary("missing"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_fastify.rs b/src/dynamic/framework/adapters/js_fastify.rs index 5f04d2bc..3889fec7 100644 --- a/src/dynamic/framework/adapters/js_fastify.rs +++ b/src/dynamic/framework/adapters/js_fastify.rs @@ -148,8 +148,10 @@ mod tests { function h(req, res) {}\n\ app.get('/x', h);\n"; let tree = parse_js(src); - assert!(JsFastifyAdapter - .detect(&summary("h"), tree.root_node(), src) - .is_none()); + assert!( + JsFastifyAdapter + .detect(&summary("h"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_handlebars.rs b/src/dynamic/framework/adapters/js_handlebars.rs index 84faa6f0..750419e1 100644 --- a/src/dynamic/framework/adapters/js_handlebars.rs +++ b/src/dynamic/framework/adapters/js_handlebars.rs @@ -139,9 +139,11 @@ mod tests { let src: &[u8] = b"const Handlebars = require('handlebars');\nfunction render(body) {\n return Handlebars.compile(body)({});\n}\n"; let tree = parse_js(src); let summary = summary_for("render", &["body"], &[0]); - assert!(JsHandlebarsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + JsHandlebarsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -152,9 +154,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(JsHandlebarsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JsHandlebarsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -162,9 +166,11 @@ mod tests { let src: &[u8] = b"// uses Handlebars\nfunction render(body) {\n return Handlebars.compile(\"static\")({});\n}\n"; let tree = parse_js(src); let summary = summary_for("render", &["body"], &[0]); - assert!(JsHandlebarsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JsHandlebarsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -172,8 +178,10 @@ mod tests { let src: &[u8] = b"const Handlebars = require('handlebars');\nfunction render(body) {\n return Handlebars.compile(body)({});\n}\n"; let tree = parse_js(src); let summary = summary_for("render", &["body"], &[]); - assert!(JsHandlebarsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + JsHandlebarsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_koa.rs b/src/dynamic/framework/adapters/js_koa.rs index 3a6d2a0e..5be0d332 100644 --- a/src/dynamic/framework/adapters/js_koa.rs +++ b/src/dynamic/framework/adapters/js_koa.rs @@ -39,22 +39,13 @@ fn receiver_looks_like_koa(name: &str) -> bool { /// that reference `target`. Returns the matched call node so callers /// can stamp a middleware-shape binding when the verb-based dispatch /// fails to fire. -fn find_use_middleware<'a>( - root: Node<'a>, - bytes: &[u8], - target: &str, -) -> Option> { +fn find_use_middleware<'a>(root: Node<'a>, bytes: &[u8], target: &str) -> Option> { let mut hit: Option> = None; walk_for_use(root, bytes, target, &mut hit); hit } -fn walk_for_use<'a>( - node: Node<'a>, - bytes: &[u8], - target: &str, - out: &mut Option>, -) { +fn walk_for_use<'a>(node: Node<'a>, bytes: &[u8], target: &str, out: &mut Option>) { if out.is_some() { return; } @@ -108,8 +99,7 @@ impl FrameworkAdapter for JsKoaAdapter { .unwrap_or_default(); bind_path_params(&formals, path) }; - if let Some((method, path)) = - find_route_registration(ast, file_bytes, &summary.name, &recv) + if let Some((method, path)) = find_route_registration(ast, file_bytes, &summary.name, &recv) { let request_params = formals_for(&path); return Some(FrameworkBinding { @@ -180,8 +170,12 @@ mod tests { let route = binding.route.as_ref().unwrap(); assert_eq!(route.method, HttpMethod::GET); assert_eq!(route.path, "/users/:id"); - assert!(binding.request_params.iter().any(|p| p.name == "ctx" - && matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "ctx" && matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -205,8 +199,10 @@ mod tests { function h(req, res) {}\n\ router.get('/x', h);\n"; let tree = parse_js(src); - assert!(JsKoaAdapter - .detect(&summary("h"), tree.root_node(), src) - .is_none()); + assert!( + JsKoaAdapter + .detect(&summary("h"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_nest.rs b/src/dynamic/framework/adapters/js_nest.rs index bc5ced6f..8ac608d6 100644 --- a/src/dynamic/framework/adapters/js_nest.rs +++ b/src/dynamic/framework/adapters/js_nest.rs @@ -84,8 +84,7 @@ fn detect_nest( if !source_imports_nest(file_bytes) { return None; } - let (class_node, method_node) = - find_class_method(ast, file_bytes, &summary.name)?; + let (class_node, method_node) = find_class_method(ast, file_bytes, &summary.name)?; let prefix = class_controller_prefix(class_node, file_bytes)?; let (method, sub_path) = method_verb_and_path(method_node, file_bytes)?; let full_path = join_paths(&prefix, &sub_path); @@ -213,10 +212,7 @@ fn class_controller_prefix(class_node: Node<'_>, bytes: &[u8]) -> Option /// with one of the Nest verb decorators (`@Get`, `@Post`, ...). The /// `sub_path` is `""` when the decorator carries no argument /// (`@Get()` mounts at the controller prefix root). -fn method_verb_and_path( - method_node: Node<'_>, - bytes: &[u8], -) -> Option<(HttpMethod, String)> { +fn method_verb_and_path(method_node: Node<'_>, bytes: &[u8]) -> Option<(HttpMethod, String)> { const VERBS: &[&str] = &[ "Get", "Head", "Post", "Put", "Patch", "Delete", "Options", "All", ]; @@ -461,8 +457,7 @@ mod tests { fn parse_ts(src: &[u8]) -> tree_sitter::Tree { let mut parser = tree_sitter::Parser::new(); - let lang = - tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT); + let lang = tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT); parser.set_language(&lang).unwrap(); parser.parse(src, None).unwrap() } @@ -562,8 +557,10 @@ mod tests { compute(x: number) { return x + 1; }\n\ }\n"; let tree = parse_ts(src); - assert!(TsNestAdapter - .detect(&summary("compute", "typescript"), tree.root_node(), src) - .is_none()); + assert!( + TsNestAdapter + .detect(&summary("compute", "typescript"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/js_routes.rs b/src/dynamic/framework/adapters/js_routes.rs index 15d829d6..bc1e003f 100644 --- a/src/dynamic/framework/adapters/js_routes.rs +++ b/src/dynamic/framework/adapters/js_routes.rs @@ -140,22 +140,13 @@ pub fn strip_quotes(raw: &str) -> &str { /// arrow function whose binding name equals `target`. Returns the /// `formal_parameters` (or `formal_parameter` for shorthand arrows) /// node so callers can enumerate parameter names. -pub fn find_function_params<'a>( - root: Node<'a>, - bytes: &[u8], - target: &str, -) -> Option> { +pub fn find_function_params<'a>(root: Node<'a>, bytes: &[u8], target: &str) -> Option> { let mut hit: Option> = None; walk_for_params(root, bytes, target, &mut hit); hit } -fn walk_for_params<'a>( - node: Node<'a>, - bytes: &[u8], - target: &str, - out: &mut Option>, -) { +fn walk_for_params<'a>(node: Node<'a>, bytes: &[u8], target: &str, out: &mut Option>) { if out.is_some() { return; } @@ -311,15 +302,7 @@ pub fn bind_path_params(formals: &[String], path: &str) -> Vec { fn is_implicit_formal(name: &str) -> bool { matches!( name, - "req" - | "request" - | "res" - | "response" - | "reply" - | "ctx" - | "context" - | "next" - | "done" + "req" | "request" | "res" | "response" | "reply" | "ctx" | "context" | "next" | "done" ) } @@ -349,9 +332,7 @@ pub fn extract_path_placeholders(path: &str) -> Vec { b':' => { let start = i + 1; let mut j = start; - while j < bytes.len() - && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') - { + while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') { j += 1; } if j > start { @@ -456,10 +437,11 @@ fn walk_for_registration<'a>( && receiver_accepts(last_segment(object_text)) && let Some(args) = node.child_by_field_name("arguments") && call_args_reference_target(args, bytes, target) - && let Some(path) = first_string_arg(args, bytes) { - *out = Some((method, path)); - return; - } + && let Some(path) = first_string_arg(args, bytes) + { + *out = Some((method, path)); + return; + } // Fastify options-object: `fastify.route({ method, url, handler })`. if prop_text == "route" && receiver_accepts(last_segment(object_text)) @@ -507,11 +489,7 @@ pub fn first_string_arg(args: Node<'_>, bytes: &[u8]) -> Option { /// Parse a Fastify options-object call `fastify.route({ method, url, /// handler })` returning the bound `(method, url)` when the /// `handler:` property references `target`. -fn parse_options_route( - args: Node<'_>, - bytes: &[u8], - target: &str, -) -> Option<(HttpMethod, String)> { +fn parse_options_route(args: Node<'_>, bytes: &[u8], target: &str) -> Option<(HttpMethod, String)> { let mut cur = args.walk(); for c in args.named_children(&mut cur) { if c.kind() != "object" { @@ -525,7 +503,9 @@ fn parse_options_route( if pair.kind() != "pair" { continue; } - let Some(key) = pair.child_by_field_name("key").and_then(|n| n.utf8_text(bytes).ok()) + let Some(key) = pair + .child_by_field_name("key") + .and_then(|n| n.utf8_text(bytes).ok()) else { continue; }; diff --git a/src/dynamic/framework/adapters/kafka_java.rs b/src/dynamic/framework/adapters/kafka_java.rs index 849e396b..7a206d87 100644 --- a/src/dynamic/framework/adapters/kafka_java.rs +++ b/src/dynamic/framework/adapters/kafka_java.rs @@ -35,7 +35,12 @@ fn source_imports_kafka(file_bytes: &[u8]) -> bool { fn extract_topic(file_bytes: &[u8]) -> String { let text = std::str::from_utf8(file_bytes).unwrap_or(""); - for needle in ["topics = \"", "topics=\"", "topics = {\"", "subscribe(Arrays.asList(\""] { + for needle in [ + "topics = \"", + "topics=\"", + "topics = {\"", + "subscribe(Arrays.asList(\"", + ] { if let Some(idx) = text.find(needle) { let after = &text[idx + needle.len()..]; if let Some(end) = after.find('"') { diff --git a/src/dynamic/framework/adapters/kafka_python.rs b/src/dynamic/framework/adapters/kafka_python.rs index c1c98b15..8a91db70 100644 --- a/src/dynamic/framework/adapters/kafka_python.rs +++ b/src/dynamic/framework/adapters/kafka_python.rs @@ -129,8 +129,10 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(KafkaPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + KafkaPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ldap_php.rs b/src/dynamic/framework/adapters/ldap_php.rs index b732ccbc..50915bce 100644 --- a/src/dynamic/framework/adapters/ldap_php.rs +++ b/src/dynamic/framework/adapters/ldap_php.rs @@ -173,9 +173,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("ldap_search")], ..Default::default() }; - assert!(LdapPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + LdapPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -186,9 +188,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(LdapPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -203,8 +207,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("ldap_search")], ..Default::default() }; - assert!(LdapPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ldap_python.rs b/src/dynamic/framework/adapters/ldap_python.rs index 2d194989..9ed9fb2f 100644 --- a/src/dynamic/framework/adapters/ldap_python.rs +++ b/src/dynamic/framework/adapters/ldap_python.rs @@ -168,9 +168,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("search_s")], ..Default::default() }; - assert!(LdapPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + LdapPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -181,9 +183,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(LdapPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -198,8 +202,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("search_s")], ..Default::default() }; - assert!(LdapPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ldap_spring.rs b/src/dynamic/framework/adapters/ldap_spring.rs index 5d48ac8b..504b3b00 100644 --- a/src/dynamic/framework/adapters/ldap_spring.rs +++ b/src/dynamic/framework/adapters/ldap_spring.rs @@ -205,9 +205,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(LdapSpringAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapSpringAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -225,8 +227,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("search")], ..Default::default() }; - assert!(LdapSpringAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + LdapSpringAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/middleware_django.rs b/src/dynamic/framework/adapters/middleware_django.rs index c84f6fbd..12b727d2 100644 --- a/src/dynamic/framework/adapters/middleware_django.rs +++ b/src/dynamic/framework/adapters/middleware_django.rs @@ -17,11 +17,7 @@ fn callee_is_django_middleware(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); matches!( last, - "process_request" - | "process_response" - | "process_view" - | "process_exception" - | "__call__" + "process_request" | "process_response" | "process_view" | "process_exception" | "__call__" ) } diff --git a/src/dynamic/framework/adapters/middleware_express.rs b/src/dynamic/framework/adapters/middleware_express.rs index 4787e005..d48cf1c6 100644 --- a/src/dynamic/framework/adapters/middleware_express.rs +++ b/src/dynamic/framework/adapters/middleware_express.rs @@ -15,10 +15,7 @@ const ADAPTER_NAME: &str = "middleware-express"; fn callee_is_express(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!( - last, - "use" | "next" | "json" | "urlencoded" | "static" - ) + matches!(last, "use" | "next" | "json" | "urlencoded" | "static") } fn source_imports_express(file_bytes: &[u8]) -> bool { @@ -27,11 +24,7 @@ fn source_imports_express(file_bytes: &[u8]) -> bool { // import. Many non-middleware Express fixtures import the framework // but never declare middleware; gating on the registration shape // keeps the adapter focused on the function the brief targets. - const NEEDLES: &[&[u8]] = &[ - b"app.use(", - b"router.use(", - b"express.Router()", - ]; + const NEEDLES: &[&[u8]] = &[b"app.use(", b"router.use(", b"express.Router()"]; NEEDLES .iter() .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) diff --git a/src/dynamic/framework/adapters/migration_rails.rs b/src/dynamic/framework/adapters/migration_rails.rs index 80f0dc29..06820183 100644 --- a/src/dynamic/framework/adapters/migration_rails.rs +++ b/src/dynamic/framework/adapters/migration_rails.rs @@ -17,8 +17,7 @@ fn callee_is_rails_migration(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); matches!( last, - "up" - | "down" + "up" | "down" | "change" | "create_table" | "add_column" diff --git a/src/dynamic/framework/adapters/migration_sequelize.rs b/src/dynamic/framework/adapters/migration_sequelize.rs index 8665f07e..94e44e44 100644 --- a/src/dynamic/framework/adapters/migration_sequelize.rs +++ b/src/dynamic/framework/adapters/migration_sequelize.rs @@ -17,13 +17,7 @@ fn callee_is_sequelize_migration(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); matches!( last, - "up" - | "down" - | "createTable" - | "addColumn" - | "dropTable" - | "removeColumn" - | "addIndex" + "up" | "down" | "createTable" | "addColumn" | "dropTable" | "removeColumn" | "addIndex" ) } diff --git a/src/dynamic/framework/adapters/mod.rs b/src/dynamic/framework/adapters/mod.rs index de81d408..be75f4bb 100644 --- a/src/dynamic/framework/adapters/mod.rs +++ b/src/dynamic/framework/adapters/mod.rs @@ -11,6 +11,16 @@ //! the route / framework adapters; the per-cap sink adapters live //! here so the per-language verticals can ship independently. +pub mod go_chi; +pub mod go_echo; +pub mod go_fiber; +pub mod go_gin; +pub mod go_routes; +pub mod graphql_apollo; +pub mod graphql_gqlgen; +pub mod graphql_graphene; +pub mod graphql_juniper; +pub mod graphql_relay; pub mod header_go; pub mod header_java; pub mod header_js; @@ -18,11 +28,6 @@ pub mod header_php; pub mod header_python; pub mod header_ruby; pub mod header_rust; -pub mod go_chi; -pub mod go_echo; -pub mod go_fiber; -pub mod go_gin; -pub mod go_routes; pub mod java_deserialize; pub mod java_micronaut; pub mod java_quarkus; @@ -36,11 +41,6 @@ pub mod js_handlebars; pub mod js_koa; pub mod js_nest; pub mod js_routes; -pub mod graphql_apollo; -pub mod graphql_gqlgen; -pub mod graphql_graphene; -pub mod graphql_juniper; -pub mod graphql_relay; pub mod kafka_java; pub mod kafka_python; pub mod ldap_php; @@ -117,6 +117,15 @@ pub mod xxe_php; pub mod xxe_python; pub mod xxe_ruby; +pub use go_chi::GoChiAdapter; +pub use go_echo::GoEchoAdapter; +pub use go_fiber::GoFiberAdapter; +pub use go_gin::GoGinAdapter; +pub use graphql_apollo::GraphqlApolloAdapter; +pub use graphql_gqlgen::GraphqlGqlgenAdapter; +pub use graphql_graphene::GraphqlGrapheneAdapter; +pub use graphql_juniper::GraphqlJuniperAdapter; +pub use graphql_relay::GraphqlRelayAdapter; pub use header_go::HeaderGoAdapter; pub use header_java::HeaderJavaAdapter; pub use header_js::HeaderJsAdapter; @@ -124,10 +133,6 @@ pub use header_php::HeaderPhpAdapter; pub use header_python::HeaderPythonAdapter; pub use header_ruby::HeaderRubyAdapter; pub use header_rust::HeaderRustAdapter; -pub use go_chi::GoChiAdapter; -pub use go_echo::GoEchoAdapter; -pub use go_fiber::GoFiberAdapter; -pub use go_gin::GoGinAdapter; pub use java_deserialize::JavaDeserializeAdapter; pub use java_micronaut::JavaMicronautAdapter; pub use java_quarkus::JavaQuarkusAdapter; @@ -139,11 +144,6 @@ pub use js_fastify::JsFastifyAdapter; pub use js_handlebars::JsHandlebarsAdapter; pub use js_koa::JsKoaAdapter; pub use js_nest::{JsNestAdapter, TsNestAdapter}; -pub use graphql_apollo::GraphqlApolloAdapter; -pub use graphql_gqlgen::GraphqlGqlgenAdapter; -pub use graphql_graphene::GraphqlGrapheneAdapter; -pub use graphql_juniper::GraphqlJuniperAdapter; -pub use graphql_relay::GraphqlRelayAdapter; pub use kafka_java::KafkaJavaAdapter; pub use kafka_python::KafkaPythonAdapter; pub use ldap_php::LdapPhpAdapter; @@ -221,10 +221,7 @@ fn any_callee_matches( summary: &crate::summary::FuncSummary, predicate: impl Fn(&str) -> bool, ) -> bool { - summary - .callees - .iter() - .any(|c| predicate(c.name.as_str())) + summary.callees.iter().any(|c| predicate(c.name.as_str())) } /// True when any callee in `summary.callees` matches `name_pred` AND @@ -270,10 +267,7 @@ fn any_callee_matches_with_receiver( /// Per-language sigil stripping covers PHP (`$x`), Ruby (`@x`), and /// Java/Python/JS (no sigil). Leading whitespace is also trimmed so /// adapters can pass the raw `utf8_text` of the argument node. -pub(super) fn arg_is_tainted_param( - summary: &crate::summary::FuncSummary, - arg_text: &str, -) -> bool { +pub(super) fn arg_is_tainted_param(summary: &crate::summary::FuncSummary, arg_text: &str) -> bool { fn strip(s: &str) -> &str { s.trim() .trim_start_matches('$') @@ -281,15 +275,10 @@ pub(super) fn arg_is_tainted_param( .trim_start_matches('&') } let needle = strip(arg_text); - let Some(idx) = summary - .param_names - .iter() - .position(|p| strip(p) == needle) - else { + let Some(idx) = summary.param_names.iter().position(|p| strip(p) == needle) else { return false; }; - summary.tainted_sink_params.contains(&idx) - || summary.propagating_params.contains(&idx) + summary.tainted_sink_params.contains(&idx) || summary.propagating_params.contains(&idx) } /// True when any descendant identifier in `node`'s subtree resolves to diff --git a/src/dynamic/framework/adapters/nats_go.rs b/src/dynamic/framework/adapters/nats_go.rs index 77b0bae7..c494a62c 100644 --- a/src/dynamic/framework/adapters/nats_go.rs +++ b/src/dynamic/framework/adapters/nats_go.rs @@ -18,11 +18,7 @@ fn callee_is_nats(name: &str) -> bool { } fn source_imports_nats(file_bytes: &[u8]) -> bool { - const NEEDLES: &[&[u8]] = &[ - b"github.com/nats-io/nats.go", - b"nats.Connect", - b"nats.Msg", - ]; + const NEEDLES: &[&[u8]] = &[b"github.com/nats-io/nats.go", b"nats.Connect", b"nats.Msg"]; NEEDLES .iter() .any(|n| file_bytes.windows(n.len()).any(|w| w == *n)) diff --git a/src/dynamic/framework/adapters/php_codeigniter.rs b/src/dynamic/framework/adapters/php_codeigniter.rs index 1515e94d..fe7111ad 100644 --- a/src/dynamic/framework/adapters/php_codeigniter.rs +++ b/src/dynamic/framework/adapters/php_codeigniter.rs @@ -11,9 +11,9 @@ //! inner name (after the `:`) for each so a `$id` formal whose name //! matches the placeholder binds as [`super::super::ParamSource::PathSegment`]. -use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape}; #[cfg(test)] use crate::dynamic::framework::HttpMethod; +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape}; use crate::evidence::EntryKind; use crate::summary::FuncSummary; use crate::symbol::Lang; @@ -49,8 +49,7 @@ impl FrameworkAdapter for PhpCodeIgniterAdapter { let (func_node, class) = find_php_function(ast, file_bytes, &summary.name)?; let controller = class.and_then(|c| php_class_name(c, file_bytes)); - let (method, path) = - find_codeigniter_route(ast, file_bytes, &summary.name, controller)?; + let (method, path) = find_codeigniter_route(ast, file_bytes, &summary.name, controller)?; let formals = php_formal_names(func_node, file_bytes); let request_params = bind_php_path_params(&formals, &path); @@ -120,17 +119,21 @@ mod tests { fn skips_when_codeigniter_not_imported() { let src: &[u8] = b"get('users/(:num)', 'UserController::show');\n"; let tree = parse(src); - assert!(PhpCodeIgniterAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + PhpCodeIgniterAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_callable_does_not_reference_method() { let src: &[u8] = b"get('users/(:num)', 'UserController::show');\nclass UserController extends BaseController {\n public function helper($x) { return $x; }\n}\n"; let tree = parse(src); - assert!(PhpCodeIgniterAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + PhpCodeIgniterAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/php_laravel.rs b/src/dynamic/framework/adapters/php_laravel.rs index a1b70534..857e1f2d 100644 --- a/src/dynamic/framework/adapters/php_laravel.rs +++ b/src/dynamic/framework/adapters/php_laravel.rs @@ -12,9 +12,9 @@ //! a `class UserController { public function show($id) {…} }` //! declaration in the same file. -use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape}; #[cfg(test)] use crate::dynamic::framework::HttpMethod; +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, RouteShape}; use crate::evidence::EntryKind; use crate::summary::FuncSummary; use crate::symbol::Lang; @@ -50,8 +50,7 @@ impl FrameworkAdapter for PhpLaravelAdapter { let (func_node, class) = find_php_function(ast, file_bytes, &summary.name)?; let controller = class.and_then(|c| php_class_name(c, file_bytes)); - let (method, path) = - find_laravel_static_route(ast, file_bytes, &summary.name, controller)?; + let (method, path) = find_laravel_static_route(ast, file_bytes, &summary.name, controller)?; let formals = php_formal_names(func_node, file_bytes); let request_params = bind_php_path_params(&formals, &path); @@ -143,17 +142,21 @@ mod tests { fn skips_when_laravel_not_imported() { let src: &[u8] = b"( && let Some(name) = node .child_by_field_name("name") .and_then(|n| n.utf8_text(bytes).ok()) - && name == target { - let klass = if node.kind() == "method_declaration" { - here_class - } else { - None - }; - *out = Some((node, klass)); - return; - } + && name == target + { + let klass = if node.kind() == "method_declaration" { + here_class + } else { + None + }; + *out = Some((node, klass)); + return; + } let mut cur = node.walk(); for child in node.children(&mut cur) { walk(child, bytes, target, here_class, out); @@ -511,10 +512,7 @@ fn laravel_callable_matches( } } -fn parse_array_callable<'a>( - array: Node<'a>, - bytes: &'a [u8], -) -> Option<(Option, String)> { +fn parse_array_callable<'a>(array: Node<'a>, bytes: &'a [u8]) -> Option<(Option, String)> { let mut cur = array.walk(); let elements: Vec> = array .named_children(&mut cur) @@ -544,10 +542,7 @@ fn split_laravel_callable(literal: &str) -> (Option, String) { fn leaf(qualified: &str) -> &str { let last_backslash = qualified.rsplit('\\').next().unwrap_or(qualified); - last_backslash - .rsplit("::") - .next() - .unwrap_or(last_backslash) + last_backslash.rsplit("::").next().unwrap_or(last_backslash) } fn verb_method(verb: &str) -> Option { @@ -711,18 +706,12 @@ mod tests { extract_php_path_placeholders("/u/{id}/p/{slug?}"), vec!["id", "slug"] ); - assert_eq!( - extract_php_path_placeholders("/u/{id:[0-9]+}"), - vec!["id"] - ); + assert_eq!(extract_php_path_placeholders("/u/{id:[0-9]+}"), vec!["id"]); } #[test] fn extracts_codeigniter_placeholders() { - assert_eq!( - extract_php_path_placeholders("users/(:num)"), - vec!["num"] - ); + assert_eq!(extract_php_path_placeholders("users/(:num)"), vec!["num"]); assert_eq!( extract_php_path_placeholders("p/(:any)/c/(:segment)"), vec!["any", "segment"] @@ -778,20 +767,16 @@ mod tests { fn finds_laravel_static_route_with_string_callable() { let src: &[u8] = b"get('users/(:num)', 'UserController::show');\n"; let tree = parse(src); - let hit = find_codeigniter_route( - tree.root_node(), - src, - "show", - Some("UserController"), - ) - .unwrap(); + let hit = + find_codeigniter_route(tree.root_node(), src, "show", Some("UserController")).unwrap(); assert_eq!(hit.0, HttpMethod::GET); assert_eq!(hit.1, "users/(:num)"); } diff --git a/src/dynamic/framework/adapters/php_symfony.rs b/src/dynamic/framework/adapters/php_symfony.rs index 51fa51ea..a76320e8 100644 --- a/src/dynamic/framework/adapters/php_symfony.rs +++ b/src/dynamic/framework/adapters/php_symfony.rs @@ -165,17 +165,21 @@ mod tests { fn skips_when_symfony_not_imported() { let src: &[u8] = b"createTemplate($body);\n return $tpl->render([]);\n}\n"; let tree = parse_php(src); let summary = summary_for("render", &["body", "twig"], &[0]); - assert!(PhpTwigAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PhpTwigAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -158,9 +160,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(PhpTwigAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PhpTwigAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -170,9 +174,11 @@ mod tests { let src: &[u8] = b"createTemplate('static');\n return $tpl->render([]);\n}\n"; let tree = parse_php(src); let summary = summary_for("render", &["body", "twig"], &[0]); - assert!(PhpTwigAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PhpTwigAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -180,8 +186,10 @@ mod tests { let src: &[u8] = b"createTemplate($body);\n return $tpl->render([]);\n}\n"; let tree = parse_php(src); let summary = summary_for("render", &["body", "twig"], &[]); - assert!(PhpTwigAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PhpTwigAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/php_unserialize.rs b/src/dynamic/framework/adapters/php_unserialize.rs index d5209e6c..9c9d2eb9 100644 --- a/src/dynamic/framework/adapters/php_unserialize.rs +++ b/src/dynamic/framework/adapters/php_unserialize.rs @@ -68,9 +68,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(PhpUnserializeAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PhpUnserializeAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -81,8 +83,10 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(PhpUnserializeAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PhpUnserializeAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/pp_json_deep_assign.rs b/src/dynamic/framework/adapters/pp_json_deep_assign.rs index 612f0a30..19e24856 100644 --- a/src/dynamic/framework/adapters/pp_json_deep_assign.rs +++ b/src/dynamic/framework/adapters/pp_json_deep_assign.rs @@ -141,9 +141,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("JSON.parse")], ..Default::default() }; - assert!(PpJsonDeepAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PpJsonDeepAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -155,9 +157,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("JSON.parse")], ..Default::default() }; - assert!(PpJsonDeepAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpJsonDeepAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -176,8 +180,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("JSON.parse")], ..Default::default() }; - assert!(PpJsonDeepAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpJsonDeepAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/pp_lodash_merge.rs b/src/dynamic/framework/adapters/pp_lodash_merge.rs index 095f4c4e..510f29c4 100644 --- a/src/dynamic/framework/adapters/pp_lodash_merge.rs +++ b/src/dynamic/framework/adapters/pp_lodash_merge.rs @@ -13,7 +13,10 @@ use crate::symbol::Lang; fn callee_is_lodash_merge(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "merge" | "mergeWith" | "defaultsDeep" | "set" | "setWith") + matches!( + last, + "merge" | "mergeWith" | "defaultsDeep" | "set" | "setWith" + ) } /// True when `receiver` looks like a lodash module handle (`_`, `lodash`, @@ -152,9 +155,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("merge")], ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -165,9 +170,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -193,9 +200,11 @@ mod tests { }], ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -213,9 +222,11 @@ mod tests { }], ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -233,9 +244,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("merge")], ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -249,8 +262,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("merge")], ..Default::default() }; - assert!(PpLodashMergeJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpLodashMergeJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/pp_object_assign.rs b/src/dynamic/framework/adapters/pp_object_assign.rs index d2dc7398..fd37d5c8 100644 --- a/src/dynamic/framework/adapters/pp_object_assign.rs +++ b/src/dynamic/framework/adapters/pp_object_assign.rs @@ -117,9 +117,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("Object.assign")], ..Default::default() }; - assert!(PpObjectAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PpObjectAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -130,24 +132,27 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(PpObjectAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpObjectAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] fn skips_object_create_null_mitigation() { - let src: &[u8] = - b"function run(payload) { return Object.create(null); }\n"; + let src: &[u8] = b"function run(payload) { return Object.create(null); }\n"; let tree = parse_js(src); let summary = FuncSummary { name: "run".into(), callees: vec![crate::summary::CalleeSite::bare("Object.create")], ..Default::default() }; - assert!(PpObjectAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpObjectAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -164,8 +169,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("Object.assign")], ..Default::default() }; - assert!(PpObjectAssignJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PpObjectAssignJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/pubsub_python.rs b/src/dynamic/framework/adapters/pubsub_python.rs index 5456f5c2..d113f96a 100644 --- a/src/dynamic/framework/adapters/pubsub_python.rs +++ b/src/dynamic/framework/adapters/pubsub_python.rs @@ -11,10 +11,7 @@ const ADAPTER_NAME: &str = "pubsub-python"; fn callee_is_pubsub(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!( - last, - "subscribe" | "pull" | "callback" | "process_message" - ) + matches!(last, "subscribe" | "pull" | "callback" | "process_message") } fn source_imports_pubsub(file_bytes: &[u8]) -> bool { diff --git a/src/dynamic/framework/adapters/python_django.rs b/src/dynamic/framework/adapters/python_django.rs index 7334be3a..07f2d321 100644 --- a/src/dynamic/framework/adapters/python_django.rs +++ b/src/dynamic/framework/adapters/python_django.rs @@ -91,17 +91,19 @@ fn walk_url_registrations( { let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee); if matches!(last, "path" | "re_path" | "url") - && let Some(args) = node.child_by_field_name("arguments") { - let positional = positional_args(args); - if positional.len() >= 2 { - let view_arg = positional[1]; - if view_arg_references(view_arg, bytes, target, class_target) - && let Some(template) = first_string_arg(args, bytes) { - *out = Some(template); - return; - } + && let Some(args) = node.child_by_field_name("arguments") + { + let positional = positional_args(args); + if positional.len() >= 2 { + let view_arg = positional[1]; + if view_arg_references(view_arg, bytes, target, class_target) + && let Some(template) = first_string_arg(args, bytes) + { + *out = Some(template); + return; } } + } } let mut cur = node.walk(); for child in node.children(&mut cur) { @@ -137,12 +139,15 @@ fn view_arg_references( .and_then(|s| s.rfind('(').map(|i| &s[..i])) .and_then(|s| s.strip_suffix(".as_view")) && let Some(ct) = class_target - && class.rsplit_once('.').map(|(_, s)| s).unwrap_or(class) == ct - { - return true; - } + && class.rsplit_once('.').map(|(_, s)| s).unwrap_or(class) == ct + { + return true; + } let stripped = trimmed.trim_end_matches("()"); - let last = stripped.rsplit_once('.').map(|(_, s)| s).unwrap_or(stripped); + let last = stripped + .rsplit_once('.') + .map(|(_, s)| s) + .unwrap_or(stripped); last == target || stripped == target } @@ -191,12 +196,8 @@ impl FrameworkAdapter for PythonDjangoAdapter { // - urls.py registration referencing the function // - urls.py `ClassName.as_view()` registration referencing the enclosing class // - class-based view method name (path falls back to `/`) - let url_template = url_template_for( - ast, - file_bytes, - &summary.name, - cbv_class_name.as_deref(), - ); + let url_template = + url_template_for(ast, file_bytes, &summary.name, cbv_class_name.as_deref()); let (method, path) = if let Some(m) = cbv_method { (m, url_template.unwrap_or_else(|| "/".to_owned())) @@ -288,18 +289,23 @@ mod tests { fn skips_when_django_not_imported() { let src: &[u8] = b"def list_users(request):\n return None\n"; let tree = parse(src); - assert!(PythonDjangoAdapter - .detect(&summary("list_users"), tree.root_node(), src) - .is_none()); + assert!( + PythonDjangoAdapter + .detect(&summary("list_users"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_plain_helper_function() { - let src: &[u8] = b"from django.http import HttpResponse\ndef helper(x):\n return HttpResponse(x)\n"; + let src: &[u8] = + b"from django.http import HttpResponse\ndef helper(x):\n return HttpResponse(x)\n"; let tree = parse(src); - assert!(PythonDjangoAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + PythonDjangoAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -314,8 +320,10 @@ mod tests { // pipeline surfaces `SpecDerivationFailed`. let src: &[u8] = b"from django.http import HttpResponse\ndef authenticated(request, perm):\n return HttpResponse(perm)\n"; let tree = parse(src); - assert!(PythonDjangoAdapter - .detect(&summary("authenticated"), tree.root_node(), src) - .is_none()); + assert!( + PythonDjangoAdapter + .detect(&summary("authenticated"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/python_fastapi.rs b/src/dynamic/framework/adapters/python_fastapi.rs index ebcdf89d..d64bc50a 100644 --- a/src/dynamic/framework/adapters/python_fastapi.rs +++ b/src/dynamic/framework/adapters/python_fastapi.rs @@ -62,8 +62,14 @@ fn decorator_route_shape(decorator: Node<'_>, bytes: &[u8]) -> Option<(HttpMetho if target.kind() != "attribute" { return None; } - let object = target.child_by_field_name("object")?.utf8_text(bytes).ok()?; - let attr = target.child_by_field_name("attribute")?.utf8_text(bytes).ok()?; + let object = target + .child_by_field_name("object")? + .utf8_text(bytes) + .ok()?; + let attr = target + .child_by_field_name("attribute")? + .utf8_text(bytes) + .ok()?; if !receiver_looks_like_fastapi(object) { return None; } @@ -389,8 +395,10 @@ mod tests { fn skips_when_fastapi_not_imported() { let src: &[u8] = b"from flask import Flask\napp = Flask(__name__)\n@app.get(\"/x\")\ndef x():\n return 1\n"; let tree = parse(src); - assert!(PythonFastApiAdapter - .detect(&summary("x"), tree.root_node(), src) - .is_none()); + assert!( + PythonFastApiAdapter + .detect(&summary("x"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/python_flask.rs b/src/dynamic/framework/adapters/python_flask.rs index 1f12cb80..55dcdb3b 100644 --- a/src/dynamic/framework/adapters/python_flask.rs +++ b/src/dynamic/framework/adapters/python_flask.rs @@ -202,10 +202,12 @@ mod tests { .expect("binding"); let route = binding.route.unwrap(); assert_eq!(route.path, "/users/"); - assert!(binding - .request_params - .iter() - .any(|p| p.name == "id" && matches!(p.source, ParamSource::PathSegment(_)))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "id" && matches!(p.source, ParamSource::PathSegment(_))) + ); } #[test] @@ -234,17 +236,22 @@ mod tests { fn skips_when_flask_not_imported() { let src: &[u8] = b"def add(a, b):\n return a + b\n"; let tree = parse(src); - assert!(PythonFlaskAdapter - .detect(&summary("add"), tree.root_node(), src) - .is_none()); + assert!( + PythonFlaskAdapter + .detect(&summary("add"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_function_has_no_decorator() { - let src: &[u8] = b"from flask import Flask\napp = Flask(__name__)\ndef helper(x):\n return x\n"; + let src: &[u8] = + b"from flask import Flask\napp = Flask(__name__)\ndef helper(x):\n return x\n"; let tree = parse(src); - assert!(PythonFlaskAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + PythonFlaskAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/python_jinja2.rs b/src/dynamic/framework/adapters/python_jinja2.rs index 895bdd4a..a6ab77fe 100644 --- a/src/dynamic/framework/adapters/python_jinja2.rs +++ b/src/dynamic/framework/adapters/python_jinja2.rs @@ -92,9 +92,7 @@ impl FrameworkAdapter for PythonJinja2Adapter { ast: tree_sitter::Node<'_>, file_bytes: &[u8], ) -> Option { - let cheap_filter = file_bytes - .windows(b"jinja2".len()) - .any(|w| w == b"jinja2") + let cheap_filter = file_bytes.windows(b"jinja2".len()).any(|w| w == b"jinja2") || file_bytes .windows(b"from_string".len()) .any(|w| w == b"from_string") @@ -149,9 +147,11 @@ mod tests { b"from jinja2 import Template\ndef render(body):\n return Template(body).render()\n"; let tree = parse_python(src); let summary = summary_for("render", &["body"], &[0]); - assert!(PythonJinja2Adapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PythonJinja2Adapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -161,9 +161,11 @@ mod tests { let tree = parse_python(src); let mut summary = summary_for("view", &["body"], &[0]); summary.callees = vec![crate::summary::CalleeSite::bare("render_template_string")]; - assert!(PythonJinja2Adapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PythonJinja2Adapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -174,9 +176,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(PythonJinja2Adapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PythonJinja2Adapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -186,9 +190,11 @@ mod tests { let src: &[u8] = b"\"\"\"renders via jinja2.Template\"\"\"\ndef render(body):\n return Template(\"hello\").render()\n"; let tree = parse_python(src); let summary = summary_for("render", &["body"], &[0]); - assert!(PythonJinja2Adapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PythonJinja2Adapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -199,8 +205,10 @@ mod tests { b"from jinja2 import Template\ndef render(body):\n return Template(body).render()\n"; let tree = parse_python(src); let summary = summary_for("render", &["body"], &[]); - assert!(PythonJinja2Adapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PythonJinja2Adapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/python_pickle.rs b/src/dynamic/framework/adapters/python_pickle.rs index 9520aeed..36f4e5f5 100644 --- a/src/dynamic/framework/adapters/python_pickle.rs +++ b/src/dynamic/framework/adapters/python_pickle.rs @@ -34,9 +34,7 @@ impl FrameworkAdapter for PythonPickleAdapter { file_bytes: &[u8], ) -> Option { let matches_call = super::any_callee_matches(summary, callee_is_python_deserialize); - let matches_source = file_bytes - .windows(b"pickle".len()) - .any(|w| w == b"pickle") + let matches_source = file_bytes.windows(b"pickle".len()).any(|w| w == b"pickle") || file_bytes .windows(b"yaml.unsafe_load".len()) .any(|w| w == b"yaml.unsafe_load") @@ -77,9 +75,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(PythonPickleAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + PythonPickleAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -90,8 +90,10 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(PythonPickleAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + PythonPickleAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/python_routes.rs b/src/dynamic/framework/adapters/python_routes.rs index c0b77325..847e9f73 100644 --- a/src/dynamic/framework/adapters/python_routes.rs +++ b/src/dynamic/framework/adapters/python_routes.rs @@ -33,7 +33,12 @@ pub fn source_imports_flask(bytes: &[u8]) -> bool { pub fn source_imports_fastapi(bytes: &[u8]) -> bool { contains_any( bytes, - &[b"from fastapi", b"import fastapi", b"FastAPI(", b"APIRouter("], + &[ + b"from fastapi", + b"import fastapi", + b"FastAPI(", + b"APIRouter(", + ], ) } @@ -95,10 +100,11 @@ fn walk<'a>(node: Node<'a>, bytes: &[u8], target: &str) -> Option<(Node<'a>, Opt && let Some(name) = node .child_by_field_name("name") .and_then(|n| n.utf8_text(bytes).ok()) - && name == target { - let decorated = node.parent().filter(|p| p.kind() == "decorated_definition"); - return Some((node, decorated)); - } + && name == target + { + let decorated = node.parent().filter(|p| p.kind() == "decorated_definition"); + return Some((node, decorated)); + } let mut cur = node.walk(); for child in node.children(&mut cur) { if let Some(found) = walk(child, bytes, target) { diff --git a/src/dynamic/framework/adapters/python_starlette.rs b/src/dynamic/framework/adapters/python_starlette.rs index 8737e396..4542b7fa 100644 --- a/src/dynamic/framework/adapters/python_starlette.rs +++ b/src/dynamic/framework/adapters/python_starlette.rs @@ -8,9 +8,7 @@ //! to the handler does not matter. Methods are picked up from the //! `methods=[...]` kwarg when present and default to `GET`. -use crate::dynamic::framework::{ - FrameworkAdapter, FrameworkBinding, HttpMethod, RouteShape, -}; +use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding, HttpMethod, RouteShape}; use crate::evidence::EntryKind; use crate::summary::FuncSummary; use crate::symbol::Lang; @@ -50,12 +48,13 @@ fn walk_routes(node: Node<'_>, bytes: &[u8], target: &str, out: &mut Option<(Htt let last = callee.rsplit_once('.').map(|(_, s)| s).unwrap_or(callee); if matches!(last, "Route" | "WebSocketRoute") && let Some(args) = node.child_by_field_name("arguments") - && let Some(path) = first_string_arg(args, bytes) - && endpoint_references(args, bytes, target) { - let method = methods_kwarg(args, bytes).unwrap_or(HttpMethod::GET); - *out = Some((method, path)); - return; - } + && let Some(path) = first_string_arg(args, bytes) + && endpoint_references(args, bytes, target) + { + let method = methods_kwarg(args, bytes).unwrap_or(HttpMethod::GET); + *out = Some((method, path)); + return; + } } let mut cur = node.walk(); for child in node.children(&mut cur) { @@ -76,9 +75,10 @@ fn endpoint_references(args: Node<'_>, bytes: &[u8], target: &str) -> bool { }; if name_text == "endpoint" && let Some(value) = arg.child_by_field_name("value") - && identifier_matches(value, bytes, target) { - return true; - } + && identifier_matches(value, bytes, target) + { + return true; + } } else { seen_positional += 1; // Second positional argument is the endpoint when no @@ -204,8 +204,10 @@ mod tests { fn skips_when_starlette_not_imported() { let src: &[u8] = b"def homepage(request):\n return None\n"; let tree = parse(src); - assert!(PythonStarletteAdapter - .detect(&summary("homepage"), tree.root_node(), src) - .is_none()); + assert!( + PythonStarletteAdapter + .detect(&summary("homepage"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/redirect_go.rs b/src/dynamic/framework/adapters/redirect_go.rs index ff92e5be..267a29c2 100644 --- a/src/dynamic/framework/adapters/redirect_go.rs +++ b/src/dynamic/framework/adapters/redirect_go.rs @@ -111,7 +111,8 @@ mod tests { #[test] fn fires_on_gin_redirect() { - let src: &[u8] = b"package vuln\n\nimport (\n\t\"net/http\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ + let src: &[u8] = + b"package vuln\n\nimport (\n\t\"net/http\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ func Run(c *gin.Context, v string) {\n\tc.Redirect(http.StatusFound, v)\n}\n"; let tree = parse_go(src); let summary = FuncSummary { @@ -119,9 +120,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("Redirect")], ..Default::default() }; - assert!(RedirectGoAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectGoAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -132,9 +135,11 @@ mod tests { name: "Add".into(), ..Default::default() }; - assert!(RedirectGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -153,9 +158,11 @@ mod tests { ], ..Default::default() }; - assert!(RedirectGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -168,8 +175,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("Redirect")], ..Default::default() }; - assert!(RedirectGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/redirect_java.rs b/src/dynamic/framework/adapters/redirect_java.rs index 83cd704f..3b714889 100644 --- a/src/dynamic/framework/adapters/redirect_java.rs +++ b/src/dynamic/framework/adapters/redirect_java.rs @@ -108,9 +108,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("sendRedirect")], ..Default::default() }; - assert!(RedirectJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -121,9 +123,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(RedirectJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -144,8 +148,10 @@ mod tests { ], ..Default::default() }; - assert!(RedirectJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/redirect_js.rs b/src/dynamic/framework/adapters/redirect_js.rs index df462828..16c154fb 100644 --- a/src/dynamic/framework/adapters/redirect_js.rs +++ b/src/dynamic/framework/adapters/redirect_js.rs @@ -112,9 +112,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("redirect")], ..Default::default() }; - assert!(RedirectJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -125,9 +127,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(RedirectJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -143,8 +147,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("redirect")], ..Default::default() }; - assert!(RedirectJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/redirect_php.rs b/src/dynamic/framework/adapters/redirect_php.rs index ffb88aa8..af643ce6 100644 --- a/src/dynamic/framework/adapters/redirect_php.rs +++ b/src/dynamic/framework/adapters/redirect_php.rs @@ -73,13 +73,12 @@ impl FrameworkAdapter for RedirectPhpAdapter { return None; } let has_location_token = file_contains_location_header_token(file_bytes); - let matches_call = super::any_callee_matches(summary, |name| { - match callee_last_segment(name) { + let matches_call = + super::any_callee_matches(summary, |name| match callee_last_segment(name) { "redirect" | "withRedirect" | "RedirectResponse" => true, "header" => has_location_token, _ => false, - } - }); + }); let matches_source = source_imports_php_web(file_bytes); if matches_call && matches_source { Some(FrameworkBinding { @@ -109,17 +108,18 @@ mod tests { #[test] fn fires_on_header_location() { - let src: &[u8] = - b" bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "redirect" | "redirect_to" | "redirect!" ) + matches!(last, "redirect" | "redirect_to" | "redirect!") } fn source_imports_ruby_web(file_bytes: &[u8]) -> bool { @@ -110,9 +110,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("redirect")], ..Default::default() }; - assert!(RedirectRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -123,9 +125,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(RedirectRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -144,8 +148,10 @@ mod tests { ], ..Default::default() }; - assert!(RedirectRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/redirect_rust.rs b/src/dynamic/framework/adapters/redirect_rust.rs index 97e14f9e..e790ef24 100644 --- a/src/dynamic/framework/adapters/redirect_rust.rs +++ b/src/dynamic/framework/adapters/redirect_rust.rs @@ -128,9 +128,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("to")], ..Default::default() }; - assert!(RedirectRustAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectRustAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -141,9 +143,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(RedirectRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -166,9 +170,11 @@ mod tests { }], ..Default::default() }; - assert!(RedirectRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -189,9 +195,11 @@ mod tests { }], ..Default::default() }; - assert!(RedirectRustAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RedirectRustAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -211,8 +219,10 @@ mod tests { ], ..Default::default() }; - assert!(RedirectRustAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RedirectRustAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ruby_erb.rs b/src/dynamic/framework/adapters/ruby_erb.rs index 95ad27c1..6d7c43a6 100644 --- a/src/dynamic/framework/adapters/ruby_erb.rs +++ b/src/dynamic/framework/adapters/ruby_erb.rs @@ -26,7 +26,10 @@ fn callee_last_segment(name: &str) -> &str { } fn is_erb_entry(name: &str) -> bool { - matches!(callee_last_segment(name), "result" | "result_with_hash" | "new") + matches!( + callee_last_segment(name), + "result" | "result_with_hash" | "new" + ) } fn ast_confirms_tainted_call(root: Node<'_>, bytes: &[u8], summary: &FuncSummary) -> bool { @@ -61,7 +64,10 @@ fn walk(node: Node<'_>, bytes: &[u8], summary: &FuncSummary, found: &mut bool) { fn first_positional_arg<'a>(args: Node<'a>) -> Option> { let mut cur = args.walk(); for arg in args.named_children(&mut cur) { - if matches!(arg.kind(), "pair" | "hash_splat_argument" | "block_argument") { + if matches!( + arg.kind(), + "pair" | "hash_splat_argument" | "block_argument" + ) { continue; } return Some(arg); @@ -93,9 +99,7 @@ impl FrameworkAdapter for RubyErbAdapter { || file_bytes .windows(b"require \"erb\"".len()) .any(|w| w == b"require \"erb\"") - || file_bytes - .windows(b"Erubi".len()) - .any(|w| w == b"Erubi"); + || file_bytes.windows(b"Erubi".len()).any(|w| w == b"Erubi"); if !cheap_filter { return None; } @@ -139,9 +143,11 @@ mod tests { let src: &[u8] = b"require 'erb'\ndef render(body)\n ERB.new(body).result\nend\n"; let tree = parse_ruby(src); let summary = summary_for("render", &["body"], &[0]); - assert!(RubyErbAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RubyErbAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -152,9 +158,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(RubyErbAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RubyErbAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -163,9 +171,11 @@ mod tests { b"# require 'erb' is mentioned\ndef render(body)\n ERB.new(\"static\").result\nend\n"; let tree = parse_ruby(src); let summary = summary_for("render", &["body"], &[0]); - assert!(RubyErbAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RubyErbAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -173,8 +183,10 @@ mod tests { let src: &[u8] = b"require 'erb'\ndef render(body)\n ERB.new(body).result\nend\n"; let tree = parse_ruby(src); let summary = summary_for("render", &["body"], &[]); - assert!(RubyErbAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RubyErbAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ruby_hanami.rs b/src/dynamic/framework/adapters/ruby_hanami.rs index 3e1de949..d65b3ff2 100644 --- a/src/dynamic/framework/adapters/ruby_hanami.rs +++ b/src/dynamic/framework/adapters/ruby_hanami.rs @@ -172,7 +172,11 @@ mod tests { let binding = RubyHanamiAdapter .detect(&summary("call"), tree.root_node(), src) .expect("binding"); - let id = binding.request_params.iter().find(|p| p.name == "id").unwrap(); + let id = binding + .request_params + .iter() + .find(|p| p.name == "id") + .unwrap(); assert!(matches!(id.source, ParamSource::PathSegment(_))); } @@ -184,7 +188,11 @@ mod tests { let binding = RubyHanamiAdapter .detect(&summary("call"), tree.root_node(), src) .expect("binding"); - let req = binding.request_params.iter().find(|p| p.name == "req").unwrap(); + let req = binding + .request_params + .iter() + .find(|p| p.name == "req") + .unwrap(); assert!(matches!(req.source, ParamSource::Implicit)); } @@ -194,9 +202,11 @@ mod tests { b"require 'hanami/action'\nclass Plain\n def call(req)\n 'ok'\n end\nend\n"; let tree = parse(src); // No `Hanami::Action` superclass / include — must skip. - assert!(RubyHanamiAdapter - .detect(&summary("call"), tree.root_node(), src) - .is_none()); + assert!( + RubyHanamiAdapter + .detect(&summary("call"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -207,8 +217,10 @@ mod tests { // `Hanami::Action` substring, so this fixture in fact does // trip the marker — the test exists to document that bare // `Hanami::Action` superclass alone is sufficient. - assert!(RubyHanamiAdapter - .detect(&summary("call"), tree.root_node(), src) - .is_some()); + assert!( + RubyHanamiAdapter + .detect(&summary("call"), tree.root_node(), src) + .is_some() + ); } } diff --git a/src/dynamic/framework/adapters/ruby_marshal.rs b/src/dynamic/framework/adapters/ruby_marshal.rs index 466e223a..91eadb23 100644 --- a/src/dynamic/framework/adapters/ruby_marshal.rs +++ b/src/dynamic/framework/adapters/ruby_marshal.rs @@ -79,9 +79,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(RubyMarshalAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + RubyMarshalAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -92,8 +94,10 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(RubyMarshalAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + RubyMarshalAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/ruby_rails.rs b/src/dynamic/framework/adapters/ruby_rails.rs index 5d7fa484..df38a694 100644 --- a/src/dynamic/framework/adapters/ruby_rails.rs +++ b/src/dynamic/framework/adapters/ruby_rails.rs @@ -81,19 +81,31 @@ fn visit_routes<'a>( format!("{path_prefix}/{ident}"), format!("{ctrl_prefix}{ident}/"), ), - NestingKind::ScopePath => (format!("{path_prefix}/{ident}"), ctrl_prefix.to_owned()), + NestingKind::ScopePath => { + (format!("{path_prefix}/{ident}"), ctrl_prefix.to_owned()) + } }; recurse_into_block(node, bytes, controller, action, &path_pfx, &ctrl_pfx, out); return; } - if let Some(found) = try_route_mapping(node, bytes, controller, action, path_prefix, ctrl_prefix) { + if let Some(found) = + try_route_mapping(node, bytes, controller, action, path_prefix, ctrl_prefix) + { *out = Some(found); return; } } let mut cur = node.walk(); for child in node.children(&mut cur) { - visit_routes(child, bytes, controller, action, path_prefix, ctrl_prefix, out); + visit_routes( + child, + bytes, + controller, + action, + path_prefix, + ctrl_prefix, + out, + ); } } @@ -153,7 +165,15 @@ fn recurse_into_block<'a>( let mut cur = call.walk(); for child in call.named_children(&mut cur) { if child.kind() == "do_block" || child.kind() == "block" { - visit_routes(child, bytes, controller, action, path_prefix, ctrl_prefix, out); + visit_routes( + child, + bytes, + controller, + action, + path_prefix, + ctrl_prefix, + out, + ); } } } @@ -208,9 +228,7 @@ fn controller_matches(routes_ctrl: &str, controller_class: &str) -> bool { } fn rails_controller_path(class_name: &str) -> String { - let stripped = class_name - .strip_suffix("Controller") - .unwrap_or(class_name); + let stripped = class_name.strip_suffix("Controller").unwrap_or(class_name); // Rails routes use the singular-segment lower form joined by `/` // for module-namespaced controllers (`Api::Users` → `api/users`). let segments: Vec = stripped @@ -356,8 +374,15 @@ mod tests { .expect("binding"); let route = binding.route.unwrap(); assert_eq!(route.path, "/u/:id"); - let id = binding.request_params.iter().find(|p| p.name == "id").unwrap(); - assert!(matches!(id.source, crate::dynamic::framework::ParamSource::PathSegment(_))); + let id = binding + .request_params + .iter() + .find(|p| p.name == "id") + .unwrap(); + assert!(matches!( + id.source, + crate::dynamic::framework::ParamSource::PathSegment(_) + )); } #[test] @@ -409,9 +434,11 @@ mod tests { fn skips_when_class_is_not_a_controller() { let src: &[u8] = b"class Foo\n def bar\n 'ok'\n end\nend\n"; let tree = parse(src); - assert!(RubyRailsAdapter - .detect(&summary("bar"), tree.root_node(), src) - .is_none()); + assert!( + RubyRailsAdapter + .detect(&summary("bar"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -419,29 +446,29 @@ mod tests { let src: &[u8] = b"class UsersController < ApplicationController\n def index\n 'ok'\n end\nend\n"; let tree = parse(src); - assert!(RubyRailsAdapter - .detect(&summary("missing"), tree.root_node(), src) - .is_none()); + assert!( + RubyRailsAdapter + .detect(&summary("missing"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_files_without_rails_marker() { - let src: &[u8] = - b"class UsersController < Object\n def index\n 'ok'\n end\nend\n"; + let src: &[u8] = b"class UsersController < Object\n def index\n 'ok'\n end\nend\n"; let tree = parse(src); - assert!(RubyRailsAdapter - .detect(&summary("index"), tree.root_node(), src) - .is_none()); + assert!( + RubyRailsAdapter + .detect(&summary("index"), tree.root_node(), src) + .is_none() + ); } #[test] fn rails_controller_path_drops_suffix_and_snake_cases() { assert_eq!(rails_controller_path("UsersController"), "users"); assert_eq!(rails_controller_path("UserPostsController"), "user_posts"); - assert_eq!( - rails_controller_path("Api::UsersController"), - "api/users" - ); + assert_eq!(rails_controller_path("Api::UsersController"), "api/users"); assert_eq!(rails_controller_path("Foo"), "foo"); } } diff --git a/src/dynamic/framework/adapters/ruby_routes.rs b/src/dynamic/framework/adapters/ruby_routes.rs index e3a3c8d6..bd4b4736 100644 --- a/src/dynamic/framework/adapters/ruby_routes.rs +++ b/src/dynamic/framework/adapters/ruby_routes.rs @@ -96,10 +96,11 @@ fn walk_class<'a>( return; } if node.kind() == "class" - && let Some(method) = find_method_in_class(node, bytes, target) { - *out = Some((node, method)); - return; - } + && let Some(method) = find_method_in_class(node, bytes, target) + { + *out = Some((node, method)); + return; + } let mut cur = node.walk(); for child in node.children(&mut cur) { walk_class(child, bytes, target, out); @@ -109,7 +110,11 @@ fn walk_class<'a>( /// Find a `method` node named `target` directly inside a `class` /// body. Returns `None` when the class has no body or no method of /// that name. -pub fn find_method_in_class<'a>(class: Node<'a>, bytes: &'a [u8], target: &str) -> Option> { +pub fn find_method_in_class<'a>( + class: Node<'a>, + bytes: &'a [u8], + target: &str, +) -> Option> { let body = named_child_of_kind(class, "body_statement")?; let mut cur = body.walk(); for member in body.named_children(&mut cur) { @@ -117,9 +122,10 @@ pub fn find_method_in_class<'a>(class: Node<'a>, bytes: &'a [u8], target: &str) continue; } if let Some(name) = method_identifier(member, bytes) - && name == target { - return Some(member); - } + && name == target + { + return Some(member); + } } None } @@ -349,7 +355,10 @@ pub fn bind_path_params(formals: &[String], path: &str) -> Vec { } fn is_implicit_formal(name: &str) -> bool { - matches!(name, "env" | "request" | "req" | "params" | "response" | "res") + matches!( + name, + "env" | "request" | "req" | "params" | "response" | "res" + ) } /// Read the first positional symbol argument (`:foo`) from an @@ -489,8 +498,7 @@ mod tests { #[test] fn class_includes_detects_hanami_v2() { - let src: &[u8] = - b"class A\n include Hanami::Action\n def call(req)\n end\nend\n"; + let src: &[u8] = b"class A\n include Hanami::Action\n def call(req)\n end\nend\n"; let tree = parse(src); let mut cur = tree.root_node().walk(); let class = tree diff --git a/src/dynamic/framework/adapters/ruby_sinatra.rs b/src/dynamic/framework/adapters/ruby_sinatra.rs index 54a7c0d2..a44f1172 100644 --- a/src/dynamic/framework/adapters/ruby_sinatra.rs +++ b/src/dynamic/framework/adapters/ruby_sinatra.rs @@ -41,10 +41,11 @@ fn collect_routes(root: Node<'_>, bytes: &[u8]) -> Vec { fn visit(node: Node<'_>, bytes: &[u8], out: &mut Vec) { if node.kind() == "call" - && let Some(route) = try_route(node, bytes) { - out.push(route); - return; - } + && let Some(route) = try_route(node, bytes) + { + out.push(route); + return; + } // Sinatra routes live at top level or directly under a `class App < // Sinatra::Base` body — never inside a helper method's body. Skip // descent through `method` / `singleton_method` so a stray `get '/x' @@ -101,9 +102,10 @@ fn block_parameter_names(block: Node<'_>, bytes: &[u8]) -> Vec { let mut bc = child.walk(); for p in child.named_children(&mut bc) { if p.kind() == "identifier" - && let Ok(t) = p.utf8_text(bytes) { - out.push(t.to_owned()); - } + && let Ok(t) = p.utf8_text(bytes) + { + out.push(t.to_owned()); + } } } out @@ -196,8 +198,7 @@ mod tests { #[test] fn fires_on_marker_comment() { - let src: &[u8] = - b"# nyx-shape: sinatra\nget '/run' do |payload|\n payload\nend\n"; + let src: &[u8] = b"# nyx-shape: sinatra\nget '/run' do |payload|\n payload\nend\n"; let tree = parse(src); let binding = RubySinatraAdapter .detect(&summary("run"), tree.root_node(), src) @@ -207,13 +208,16 @@ mod tests { #[test] fn binds_path_placeholder() { - let src: &[u8] = - b"require 'sinatra'\nget '/u/:id' do |id|\n id\nend\n"; + let src: &[u8] = b"require 'sinatra'\nget '/u/:id' do |id|\n id\nend\n"; let tree = parse(src); let binding = RubySinatraAdapter .detect(&summary("id"), tree.root_node(), src) .expect("binding"); - let id = binding.request_params.iter().find(|p| p.name == "id").unwrap(); + let id = binding + .request_params + .iter() + .find(|p| p.name == "id") + .unwrap(); assert!(matches!(id.source, ParamSource::PathSegment(_))); } @@ -223,9 +227,11 @@ mod tests { let tree = parse(src); // No do/end block — the Sinatra adapter must not claim a // Rails-style `routes.draw` mapping. - assert!(RubySinatraAdapter - .detect(&summary("run"), tree.root_node(), src) - .is_none()); + assert!( + RubySinatraAdapter + .detect(&summary("run"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -243,9 +249,11 @@ mod tests { fn skips_when_sinatra_not_imported() { let src: &[u8] = b"get '/run' do |p|\n p\nend\n"; let tree = parse(src); - assert!(RubySinatraAdapter - .detect(&summary("run"), tree.root_node(), src) - .is_none()); + assert!( + RubySinatraAdapter + .detect(&summary("run"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -279,9 +287,11 @@ mod tests { let src: &[u8] = b"require 'sinatra'\ndef helper\n get '/run' do |payload|\n payload\n end\nend\n"; let tree = parse(src); - assert!(RubySinatraAdapter - .detect(&summary("run"), tree.root_node(), src) - .is_none()); + assert!( + RubySinatraAdapter + .detect(&summary("run"), tree.root_node(), src) + .is_none() + ); } #[test] diff --git a/src/dynamic/framework/adapters/rust_actix.rs b/src/dynamic/framework/adapters/rust_actix.rs index e2b47442..f7cf5e0e 100644 --- a/src/dynamic/framework/adapters/rust_actix.rs +++ b/src/dynamic/framework/adapters/rust_actix.rs @@ -114,18 +114,22 @@ mod tests { fn skips_when_actix_not_imported() { let src: &[u8] = b"#[get(\"/u\")]\nfn show() {}\n"; let tree = parse(src); - assert!(RustActixAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustActixAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_attribute_missing() { let src: &[u8] = b"use actix_web::App;\nfn helper(x: String) {}\n"; let tree = parse(src); - assert!(RustActixAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + RustActixAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } #[test] @@ -170,8 +174,10 @@ mod tests { async fn show() -> String { String::new() }\n\ async fn other() -> String { String::new() }\n"; let tree = parse(src); - assert!(RustActixAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustActixAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/rust_axum.rs b/src/dynamic/framework/adapters/rust_axum.rs index 23f95a02..a09efc48 100644 --- a/src/dynamic/framework/adapters/rust_axum.rs +++ b/src/dynamic/framework/adapters/rust_axum.rs @@ -116,17 +116,21 @@ mod tests { fn skips_when_axum_not_imported() { let src: &[u8] = b"fn show() {}\n"; let tree = parse(src); - assert!(RustAxumAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustAxumAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_route_does_not_reference_function() { let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/u\", get(show)) }\nfn helper() {}\n"; let tree = parse(src); - assert!(RustAxumAdapter - .detect(&summary("helper"), tree.root_node(), src) - .is_none()); + assert!( + RustAxumAdapter + .detect(&summary("helper"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/rust_rocket.rs b/src/dynamic/framework/adapters/rust_rocket.rs index b33be781..a2ecd43d 100644 --- a/src/dynamic/framework/adapters/rust_rocket.rs +++ b/src/dynamic/framework/adapters/rust_rocket.rs @@ -86,7 +86,8 @@ mod tests { #[test] fn fires_on_get_with_angle_placeholder() { - let src: &[u8] = b"use rocket::get;\n#[get(\"/u/\")]\nfn show(id: String) -> String { id }\n"; + let src: &[u8] = + b"use rocket::get;\n#[get(\"/u/\")]\nfn show(id: String) -> String { id }\n"; let tree = parse(src); let binding = RustRocketAdapter .detect(&summary("show"), tree.root_node(), src) @@ -118,8 +119,10 @@ mod tests { fn skips_when_rocket_not_imported() { let src: &[u8] = b"#[get(\"/u\")]\nfn show() {}\n"; let tree = parse(src); - assert!(RustRocketAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustRocketAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/rust_routes.rs b/src/dynamic/framework/adapters/rust_routes.rs index dcd1c26f..d53a933c 100644 --- a/src/dynamic/framework/adapters/rust_routes.rs +++ b/src/dynamic/framework/adapters/rust_routes.rs @@ -83,22 +83,13 @@ fn contains_any(haystack: &[u8], needles: &[&[u8]]) -> bool { /// Find a top-level `function_item` whose `name` field equals /// `target`. Walks the AST recursively so functions nested inside /// `impl` blocks are also matched. -pub fn find_rust_function<'a>( - root: Node<'a>, - bytes: &'a [u8], - target: &str, -) -> Option> { +pub fn find_rust_function<'a>(root: Node<'a>, bytes: &'a [u8], target: &str) -> Option> { let mut hit: Option> = None; walk_rs(root, bytes, target, &mut hit); hit } -fn walk_rs<'a>( - node: Node<'a>, - bytes: &'a [u8], - target: &str, - out: &mut Option>, -) { +fn walk_rs<'a>(node: Node<'a>, bytes: &'a [u8], target: &str, out: &mut Option>) { if out.is_some() { return; } @@ -143,9 +134,10 @@ fn push_pattern_name(pat: Node<'_>, bytes: &[u8], out: &mut Vec) { match pat.kind() { "identifier" => { if let Ok(text) = pat.utf8_text(bytes) - && text != "_" { - out.push(text.to_owned()); - } + && text != "_" + { + out.push(text.to_owned()); + } } "mut_pattern" | "ref_pattern" => { let mut cur = pat.walk(); @@ -310,10 +302,7 @@ pub fn rust_string_literal(node: Node<'_>, bytes: &[u8]) -> Option { } let raw = node.utf8_text(bytes).ok()?; let trimmed = raw.trim(); - if trimmed.len() >= 2 - && trimmed.starts_with('"') - && trimmed.ends_with('"') - { + if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { Some(trimmed[1..trimmed.len() - 1].to_owned()) } else { None @@ -324,10 +313,7 @@ pub fn rust_string_literal(node: Node<'_>, bytes: &[u8]) -> Option { /// for a `#[get("/path")]` / `#[post(...)]` / `#[route(...)]` macro. /// Returns `(method, path)` on first match. Used by both actix-web /// (`#[get("/path")]`) and rocket (same syntax). -pub fn find_method_attribute<'a>( - func: Node<'a>, - bytes: &'a [u8], -) -> Option<(HttpMethod, String)> { +pub fn find_method_attribute<'a>(func: Node<'a>, bytes: &'a [u8]) -> Option<(HttpMethod, String)> { let parent = func.parent()?; let mut cur = parent.walk(); let children: Vec> = parent.children(&mut cur).collect(); @@ -359,9 +345,10 @@ pub fn find_method_attribute<'a>( let mut cur = func.walk(); for c in func.children(&mut cur) { if c.kind() == "attribute_item" - && let Some(hit) = read_route_attribute(c, bytes) { - return Some(hit); - } + && let Some(hit) = read_route_attribute(c, bytes) + { + return Some(hit); + } } None } @@ -494,20 +481,14 @@ fn try_axum_route_call<'a>( /// Parse the `get(handler)` / `axum::routing::get(handler)` wrapper /// emitted by axum. Returns `(method, handler_node)` on success. -fn parse_axum_verb_wrapper<'a>( - node: Node<'a>, - bytes: &'a [u8], -) -> Option<(HttpMethod, Node<'a>)> { +fn parse_axum_verb_wrapper<'a>(node: Node<'a>, bytes: &'a [u8]) -> Option<(HttpMethod, Node<'a>)> { if node.kind() != "call_expression" { return None; } let func = node.child_by_field_name("function")?; let leaf = match func.kind() { "identifier" => func.utf8_text(bytes).ok()?, - "scoped_identifier" => func - .child_by_field_name("name")? - .utf8_text(bytes) - .ok()?, + "scoped_identifier" => func.child_by_field_name("name")?.utf8_text(bytes).ok()?, _ => return None, }; let method = verb_from_ident(leaf)?; @@ -613,10 +594,7 @@ fn try_actix_route_call<'a>( /// Parse `web::get().to(handler)` / `web::post().to(handler)` / /// `web::method(Method::PATCH).to(handler)` shapes. Returns /// `(method, handler_node)` on the first matching `.to(...)` call. -fn parse_actix_web_verb_to<'a>( - node: Node<'a>, - bytes: &'a [u8], -) -> Option<(HttpMethod, Node<'a>)> { +fn parse_actix_web_verb_to<'a>(node: Node<'a>, bytes: &'a [u8]) -> Option<(HttpMethod, Node<'a>)> { if node.kind() != "call_expression" { return None; } @@ -721,21 +699,21 @@ fn walk_warp<'a>( while let Some(p) = parent { if p.kind() == "call_expression" && let Some(func) = p.child_by_field_name("function") - && func.kind() == "field_expression" - && let Some(field) = func.child_by_field_name("field") - && let Ok(field_text) = field.utf8_text(bytes) - && matches!(field_text, "map" | "and_then" | "untuple_one") - { - let args = p.child_by_field_name("arguments"); - if let Some(args) = args { - let mut cur = args.walk(); - for c in args.named_children(&mut cur) { - if axum_callable_matches(c, bytes, target) { - hit_target = true; - } + && func.kind() == "field_expression" + && let Some(field) = func.child_by_field_name("field") + && let Ok(field_text) = field.utf8_text(bytes) + && matches!(field_text, "map" | "and_then" | "untuple_one") + { + let args = p.child_by_field_name("arguments"); + if let Some(args) = args { + let mut cur = args.walk(); + for c in args.named_children(&mut cur) { + if axum_callable_matches(c, bytes, target) { + hit_target = true; } } } + } // Detect verb-filter calls (`warp::get()`, `warp::post()`). let mut cur = p.walk(); for child in p.children(&mut cur) { @@ -843,8 +821,7 @@ mod tests { fn finds_axum_route_get() { let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/u/{id}\", get(show)) }\nfn show() {}\n"; let tree = parse(src); - let (method, path) = - find_axum_route(tree.root_node(), src, "show").expect("hit"); + let (method, path) = find_axum_route(tree.root_node(), src, "show").expect("hit"); assert_eq!(method, HttpMethod::GET); assert_eq!(path, "/u/{id}"); } @@ -853,8 +830,7 @@ mod tests { fn finds_axum_route_with_scoped_verb() { let src: &[u8] = b"use axum::Router;\nfn build() -> Router { Router::new().route(\"/x\", axum::routing::post(save)) }\nfn save() {}\n"; let tree = parse(src); - let (method, path) = - find_axum_route(tree.root_node(), src, "save").expect("hit"); + let (method, path) = find_axum_route(tree.root_node(), src, "save").expect("hit"); assert_eq!(method, HttpMethod::POST); assert_eq!(path, "/x"); } @@ -871,8 +847,7 @@ mod tests { #[test] fn finds_rocket_post_attribute() { - let src: &[u8] = - b"#[post(\"/save\", data = \"\")]\nfn save(body: String) {}\n"; + let src: &[u8] = b"#[post(\"/save\", data = \"\")]\nfn save(body: String) {}\n"; let tree = parse(src); let func = find_rust_function(tree.root_node(), src, "save").unwrap(); let (method, path) = find_method_attribute(func, src).expect("hit"); @@ -890,7 +865,11 @@ mod tests { #[test] fn binds_implicit_request_as_implicit() { - let formals = vec!["req".to_string(), "request".to_string(), "state".to_string()]; + let formals = vec![ + "req".to_string(), + "request".to_string(), + "state".to_string(), + ]; let bindings = bind_rust_path_params(&formals, "/x"); for b in &bindings { assert!(matches!(b.source, ParamSource::Implicit)); @@ -908,8 +887,7 @@ mod tests { fn finds_warp_path_macro_with_map_target() { let src: &[u8] = b"use warp::Filter;\nfn build() { let r = warp::path!(\"users\" / u32).map(show); }\nfn show(id: u32) -> String { String::new() }\n"; let tree = parse(src); - let (_method, path) = - find_warp_route(tree.root_node(), src, "show").expect("hit"); + let (_method, path) = find_warp_route(tree.root_node(), src, "show").expect("hit"); assert!(path.contains("users")); } @@ -923,8 +901,7 @@ mod tests { #[test] fn warp_multi_typed_anonymous_placeholders_bind_positionally() { let formals = vec!["user_id".to_string(), "post_slug".to_string()]; - let bindings = - bind_rust_path_params(&formals, "/users/{u32}/posts/{String}"); + let bindings = bind_rust_path_params(&formals, "/users/{u32}/posts/{String}"); assert!(matches!(bindings[0].source, ParamSource::PathSegment(_))); assert!(matches!(bindings[1].source, ParamSource::PathSegment(_))); } diff --git a/src/dynamic/framework/adapters/rust_warp.rs b/src/dynamic/framework/adapters/rust_warp.rs index 637066bb..bc3d60bc 100644 --- a/src/dynamic/framework/adapters/rust_warp.rs +++ b/src/dynamic/framework/adapters/rust_warp.rs @@ -112,17 +112,21 @@ mod tests { fn skips_when_warp_not_imported() { let src: &[u8] = b"fn show() {}\n"; let tree = parse(src); - assert!(RustWarpAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustWarpAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } #[test] fn skips_when_no_path_macro() { let src: &[u8] = b"use warp::Filter;\nfn show() {}\n"; let tree = parse(src); - assert!(RustWarpAdapter - .detect(&summary("show"), tree.root_node(), src) - .is_none()); + assert!( + RustWarpAdapter + .detect(&summary("show"), tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/scheduled_cron.rs b/src/dynamic/framework/adapters/scheduled_cron.rs index dc09eb96..2174be2c 100644 --- a/src/dynamic/framework/adapters/scheduled_cron.rs +++ b/src/dynamic/framework/adapters/scheduled_cron.rs @@ -139,8 +139,10 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(ScheduledCronAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + ScheduledCronAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/scheduled_sidekiq.rs b/src/dynamic/framework/adapters/scheduled_sidekiq.rs index 86eaf1d1..7d6189e3 100644 --- a/src/dynamic/framework/adapters/scheduled_sidekiq.rs +++ b/src/dynamic/framework/adapters/scheduled_sidekiq.rs @@ -15,10 +15,7 @@ const ADAPTER_NAME: &str = "scheduled-sidekiq"; fn callee_is_sidekiq(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!( - last, - "perform_async" | "perform_in" | "perform" | "set" - ) + matches!(last, "perform_async" | "perform_in" | "perform" | "set") } fn source_imports_sidekiq(file_bytes: &[u8]) -> bool { diff --git a/src/dynamic/framework/adapters/websocket_actioncable.rs b/src/dynamic/framework/adapters/websocket_actioncable.rs index 15588b51..6c377b4b 100644 --- a/src/dynamic/framework/adapters/websocket_actioncable.rs +++ b/src/dynamic/framework/adapters/websocket_actioncable.rs @@ -37,7 +37,12 @@ fn source_imports_actioncable(file_bytes: &[u8]) -> bool { fn extract_path(file_bytes: &[u8]) -> String { let text = std::str::from_utf8(file_bytes).unwrap_or(""); - for needle in ["stream_from '", "stream_from \"", "stream_for '", "stream_for \""] { + for needle in [ + "stream_from '", + "stream_from \"", + "stream_for '", + "stream_for \"", + ] { if let Some(idx) = text.find(needle) { let after = &text[idx + needle.len()..]; let close = if needle.ends_with('"') { '"' } else { '\'' }; diff --git a/src/dynamic/framework/adapters/xpath_java.rs b/src/dynamic/framework/adapters/xpath_java.rs index eb23eefa..99dd0097 100644 --- a/src/dynamic/framework/adapters/xpath_java.rs +++ b/src/dynamic/framework/adapters/xpath_java.rs @@ -27,7 +27,10 @@ const ADAPTER_NAME: &str = "xpath-java"; fn callee_is_xpath_eval(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "evaluate" | "compile" | "selectNodes" | "selectSingleNode") + matches!( + last, + "evaluate" | "compile" | "selectNodes" | "selectSingleNode" + ) } fn source_imports_xpath(file_bytes: &[u8]) -> bool { @@ -158,9 +161,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XpathJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -176,8 +181,10 @@ mod tests { }\n}\n"; let tree = parse_java(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xpath_js.rs b/src/dynamic/framework/adapters/xpath_js.rs index 0b868363..eddb78fb 100644 --- a/src/dynamic/framework/adapters/xpath_js.rs +++ b/src/dynamic/framework/adapters/xpath_js.rs @@ -142,9 +142,11 @@ mod tests { }\nmodule.exports = { run };\n"; let tree = parse_js(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathJsAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XpathJsAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -155,9 +157,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XpathJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -168,8 +172,10 @@ mod tests { }\nmodule.exports = { run };\n"; let tree = parse_js(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathJsAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathJsAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xpath_php.rs b/src/dynamic/framework/adapters/xpath_php.rs index fd22c3d4..2c1f1854 100644 --- a/src/dynamic/framework/adapters/xpath_php.rs +++ b/src/dynamic/framework/adapters/xpath_php.rs @@ -143,9 +143,11 @@ mod tests { }\n"; let tree = parse_php(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XpathPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -156,9 +158,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XpathPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -172,8 +176,10 @@ mod tests { }\n"; let tree = parse_php(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathPhpAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathPhpAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xpath_python.rs b/src/dynamic/framework/adapters/xpath_python.rs index 59cba13f..c2f7d7ac 100644 --- a/src/dynamic/framework/adapters/xpath_python.rs +++ b/src/dynamic/framework/adapters/xpath_python.rs @@ -25,7 +25,10 @@ const ADAPTER_NAME: &str = "xpath-python"; fn callee_is_xpath_eval(name: &str) -> bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); - matches!(last, "xpath" | "evaluate" | "find" | "findall" | "iterfind" | "XPath") + matches!( + last, + "xpath" | "evaluate" | "find" | "findall" | "iterfind" | "XPath" + ) } fn source_imports_lxml(file_bytes: &[u8]) -> bool { @@ -141,9 +144,11 @@ mod tests { return tree.xpath(\"//user[@name='\" + name + \"']\")\n"; let tree = parse_python(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XpathPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -154,9 +159,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XpathPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -168,8 +175,10 @@ mod tests { return q(tree, name=name)\n"; let tree = parse_python(src); let summary = summary_for("run", &["name"], &[0]); - assert!(XpathPythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XpathPythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xxe_go.rs b/src/dynamic/framework/adapters/xxe_go.rs index 54f23628..b4624e43 100644 --- a/src/dynamic/framework/adapters/xxe_go.rs +++ b/src/dynamic/framework/adapters/xxe_go.rs @@ -113,9 +113,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("NewDecoder")], ..Default::default() }; - assert!(XxeGoAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XxeGoAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -126,9 +128,11 @@ mod tests { name: "Add".into(), ..Default::default() }; - assert!(XxeGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -145,8 +149,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("NewDecoder")], ..Default::default() }; - assert!(XxeGoAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeGoAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xxe_java.rs b/src/dynamic/framework/adapters/xxe_java.rs index 11f3bc3f..87625ac1 100644 --- a/src/dynamic/framework/adapters/xxe_java.rs +++ b/src/dynamic/framework/adapters/xxe_java.rs @@ -161,9 +161,11 @@ mod tests { name: "run".into(), ..Default::default() }; - assert!(XxeJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -180,9 +182,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("parse")], ..Default::default() }; - assert!(XxeJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -200,8 +204,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("parse")], ..Default::default() }; - assert!(XxeJavaAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeJavaAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xxe_php.rs b/src/dynamic/framework/adapters/xxe_php.rs index 74346202..d827b941 100644 --- a/src/dynamic/framework/adapters/xxe_php.rs +++ b/src/dynamic/framework/adapters/xxe_php.rs @@ -19,7 +19,9 @@ pub struct XxePhpAdapter; const ADAPTER_NAME: &str = "xxe-php"; fn callee_is_xml_parser(name: &str) -> bool { - let last = name.rsplit_once("::").map(|(_, s)| s) + let last = name + .rsplit_once("::") + .map(|(_, s)| s) .or_else(|| name.rsplit_once('.').map(|(_, s)| s)) .or_else(|| name.rsplit_once("->").map(|(_, s)| s)) .unwrap_or(name); @@ -137,16 +139,19 @@ mod tests { #[test] fn fires_on_simplexml_load_string() { - let src: &[u8] = b" bool { let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name); matches!( last, - "XMLParser" - | "parse" - | "fromstring" - | "parseString" - | "XMLPullParser" - | "iterparse" + "XMLParser" | "parse" | "fromstring" | "parseString" | "XMLPullParser" | "iterparse" ) } @@ -126,9 +121,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("fromstring")], ..Default::default() }; - assert!(XxePythonAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XxePythonAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -139,9 +136,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XxePythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxePythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -156,9 +155,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("fromstring")], ..Default::default() }; - assert!(XxePythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxePythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -171,8 +172,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("fromstring")], ..Default::default() }; - assert!(XxePythonAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxePythonAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } } diff --git a/src/dynamic/framework/adapters/xxe_ruby.rs b/src/dynamic/framework/adapters/xxe_ruby.rs index 077740a1..3bd85070 100644 --- a/src/dynamic/framework/adapters/xxe_ruby.rs +++ b/src/dynamic/framework/adapters/xxe_ruby.rs @@ -17,7 +17,9 @@ pub struct XxeRubyAdapter; const ADAPTER_NAME: &str = "xxe-ruby"; fn callee_is_xml_parser(name: &str) -> bool { - let last = name.rsplit_once("::").map(|(_, s)| s) + let last = name + .rsplit_once("::") + .map(|(_, s)| s) .or_else(|| name.rsplit_once('.').map(|(_, s)| s)) .unwrap_or(name); matches!(last, "new" | "parse" | "XML" | "load") @@ -124,9 +126,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("new")], ..Default::default() }; - assert!(XxeRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XxeRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } #[test] @@ -137,9 +141,11 @@ mod tests { name: "add".into(), ..Default::default() }; - assert!(XxeRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -153,9 +159,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("new")], ..Default::default() }; - assert!(XxeRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -168,9 +176,11 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("XML")], ..Default::default() }; - assert!(XxeRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_none()); + assert!( + XxeRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_none() + ); } #[test] @@ -183,8 +193,10 @@ mod tests { callees: vec![crate::summary::CalleeSite::bare("XML")], ..Default::default() }; - assert!(XxeRubyAdapter - .detect(&summary, tree.root_node(), src) - .is_some()); + assert!( + XxeRubyAdapter + .detect(&summary, tree.root_node(), src) + .is_some() + ); } } diff --git a/src/dynamic/lang/c.rs b/src/dynamic/lang/c.rs index 8646082d..05639028 100644 --- a/src/dynamic/lang/c.rs +++ b/src/dynamic/lang/c.rs @@ -70,9 +70,12 @@ impl CShape { let kind = spec.entry_kind.tag(); let has_main_argv = (source.contains("int main(") || source.contains("int main (")) - && (source.contains("argc") || source.contains("char *argv") - || source.contains("char* argv") || source.contains("char **argv")); - let has_libfuzzer = source.contains("LLVMFuzzerTestOneInput") || entry == "LLVMFuzzerTestOneInput"; + && (source.contains("argc") + || source.contains("char *argv") + || source.contains("char* argv") + || source.contains("char **argv")); + let has_libfuzzer = + source.contains("LLVMFuzzerTestOneInput") || entry == "LLVMFuzzerTestOneInput"; if has_libfuzzer { return Self::LibfuzzerEntry; @@ -96,7 +99,10 @@ pub fn detect_shape(spec: &HarnessSpec) -> CShape { } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -735,9 +741,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!CEmitter.entry_kinds_supported().is_empty()); - assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::Function)); - assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand)); - assert!(CEmitter.entry_kinds_supported().contains(&EntryKindTag::LibraryApi)); + assert!( + CEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + CEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); + assert!( + CEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::LibraryApi) + ); } #[test] @@ -806,14 +824,20 @@ mod tests { !h.source.contains("char *new_argv[8]"), "fixed-size stack array must be gone — Argv(n>=6) used to overrun", ); - assert!(h.source.contains("char **new_argv = (char**)calloc(3, sizeof(char*))")); + assert!( + h.source + .contains("char **new_argv = (char**)calloc(3, sizeof(char*))") + ); assert!(h.source.contains("free(new_argv);")); let mut spec6 = make_spec(PayloadSlot::Argv(6)); spec6.entry_kind = EntryKind::CliSubcommand; spec6.entry_name = "nyx_entry_main".into(); let h6 = emit(&spec6).unwrap(); - assert!(h6.source.contains("char **new_argv = (char**)calloc(9, sizeof(char*))")); + assert!( + h6.source + .contains("char **new_argv = (char**)calloc(9, sizeof(char*))") + ); assert!(h6.source.contains("free(new_argv);")); } @@ -880,7 +904,10 @@ mod tests { // The install must come after `nyx_payload()` returns and before the // entry invocation — otherwise a crash inside payload decode would // be misattributed to the sink (would defeat Phase 08(b)). - let install_pos = h.source.find("__nyx_install_crash_guard(\"run\");").unwrap(); + let install_pos = h + .source + .find("__nyx_install_crash_guard(\"run\");") + .unwrap(); let payload_pos = h.source.find("char *payload = nyx_payload();").unwrap(); let invoke_pos = h.source.find("run(payload, strlen(payload));").unwrap(); assert!( @@ -927,7 +954,8 @@ mod tests { spec.entry_name = "main".into(); let h = emit(&spec).unwrap(); assert!( - h.source.contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), + h.source + .contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), "install_crash_guard must use the post-rename symbol when entry_name == 'main'", ); } @@ -938,14 +966,21 @@ mod tests { spec.entry_kind = EntryKind::LibraryApi; spec.entry_name = "LLVMFuzzerTestOneInput".into(); let h = emit(&spec).unwrap(); - assert!(h.source.contains("LLVMFuzzerTestOneInput((const uint8_t *)payload, strlen(payload))")); + assert!( + h.source + .contains("LLVMFuzzerTestOneInput((const uint8_t *)payload, strlen(payload))") + ); } #[test] fn emit_makefile_in_extra_files() { let spec = make_spec(PayloadSlot::Param(0)); let h = emit(&spec).unwrap(); - let mk = h.extra_files.iter().find(|(n, _)| n == "Makefile").expect("Makefile must be staged"); + let mk = h + .extra_files + .iter() + .find(|(n, _)| n == "Makefile") + .expect("Makefile must be staged"); assert!(mk.1.contains("nyx_harness: main.c entry.c")); } @@ -965,7 +1000,8 @@ mod tests { "probe_shim banner missing from chain step source", ); assert!( - step.source.contains("static void __nyx_install_crash_guard("), + step.source + .contains("static void __nyx_install_crash_guard("), "install_crash_guard missing from chain step source", ); let shim_pos = step diff --git a/src/dynamic/lang/cpp.rs b/src/dynamic/lang/cpp.rs index c96e0f33..39150bad 100644 --- a/src/dynamic/lang/cpp.rs +++ b/src/dynamic/lang/cpp.rs @@ -51,10 +51,12 @@ impl CppShape { let kind = spec.entry_kind.tag(); let has_main_argv = (source.contains("int main(") || source.contains("int main (")) - && (source.contains("argc") || source.contains("char *argv") - || source.contains("char* argv") || source.contains("char **argv")); - let has_libfuzzer = source.contains("LLVMFuzzerTestOneInput") - || entry == "LLVMFuzzerTestOneInput"; + && (source.contains("argc") + || source.contains("char *argv") + || source.contains("char* argv") + || source.contains("char **argv")); + let has_libfuzzer = + source.contains("LLVMFuzzerTestOneInput") || entry == "LLVMFuzzerTestOneInput"; if has_libfuzzer { return Self::LibfuzzerEntry; @@ -76,7 +78,10 @@ pub fn detect_shape(spec: &HarnessSpec) -> CppShape { } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -649,9 +654,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!CppEmitter.entry_kinds_supported().is_empty()); - assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::Function)); - assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand)); - assert!(CppEmitter.entry_kinds_supported().contains(&EntryKindTag::LibraryApi)); + assert!( + CppEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + CppEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); + assert!( + CppEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::LibraryApi) + ); } #[test] @@ -672,7 +689,8 @@ mod tests { #[test] fn shape_detect_libfuzzer() { - let src = "extern \"C\" int LLVMFuzzerTestOneInput(const uint8_t* d, size_t n) { return 0; }"; + let src = + "extern \"C\" int LLVMFuzzerTestOneInput(const uint8_t* d, size_t n) { return 0; }"; let mut spec = make_spec(PayloadSlot::Param(0)); spec.entry_kind = EntryKind::LibraryApi; spec.entry_name = "LLVMFuzzerTestOneInput".into(); @@ -713,7 +731,10 @@ mod tests { spec.entry_name = "nyx_entry_main".into(); let h = emit(&spec).unwrap(); assert!(h.source.contains("argv_storage.push_back(payload)")); - assert!(h.source.contains("nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())")); + assert!( + h.source + .contains("nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())") + ); } #[test] @@ -731,7 +752,9 @@ mod tests { ); assert!(h.source.contains("#undef main"), "undef guard missing"); assert!( - h.source.contains("__nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())"), + h.source.contains( + "__nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())" + ), "harness call site must target the renamed symbol", ); assert!(h.source.contains("int main(int argc, char *argv[])")); @@ -742,7 +765,10 @@ mod tests { let fh = emit(&fixture_spec).unwrap(); assert!(!fh.source.contains("#define main")); assert!(!fh.source.contains("#undef main")); - assert!(fh.source.contains("nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())")); + assert!( + fh.source + .contains("nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())") + ); } #[test] @@ -764,9 +790,18 @@ mod tests { h.source.contains("__nyx_install_crash_guard(\"run\");"), "install_crash_guard call site missing or wrong callee", ); - let install_pos = h.source.find("__nyx_install_crash_guard(\"run\");").unwrap(); - let payload_pos = h.source.find("std::string payload = nyx_payload();").unwrap(); - let invoke_pos = h.source.find("run(payload.c_str(), payload.size());").unwrap(); + let install_pos = h + .source + .find("__nyx_install_crash_guard(\"run\");") + .unwrap(); + let payload_pos = h + .source + .find("std::string payload = nyx_payload();") + .unwrap(); + let invoke_pos = h + .source + .find("run(payload.c_str(), payload.size());") + .unwrap(); assert!( payload_pos < install_pos && install_pos < invoke_pos, "install_crash_guard ordering wrong: payload_pos={payload_pos} install_pos={install_pos} invoke_pos={invoke_pos}", @@ -780,7 +815,8 @@ mod tests { spec.entry_name = "main".into(); let h = emit(&spec).unwrap(); assert!( - h.source.contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), + h.source + .contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), "install_crash_guard must use post-rename symbol when entry_name == 'main'", ); } @@ -814,7 +850,11 @@ mod tests { fn emit_cmake_in_extra_files() { let spec = make_spec(PayloadSlot::Param(0)); let h = emit(&spec).unwrap(); - let mk = h.extra_files.iter().find(|(n, _)| n == "CMakeLists.txt").expect("CMakeLists.txt must be staged"); + let mk = h + .extra_files + .iter() + .find(|(n, _)| n == "CMakeLists.txt") + .expect("CMakeLists.txt must be staged"); assert!(mk.1.contains("add_executable(nyx_harness main.cpp)")); } @@ -832,7 +872,8 @@ mod tests { "probe_shim banner missing from chain step source", ); assert!( - step.source.contains("inline void __nyx_install_crash_guard("), + step.source + .contains("inline void __nyx_install_crash_guard("), "install_crash_guard missing from chain step source", ); let shim_pos = step diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 3b465dfe..322e3be6 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -220,10 +220,10 @@ impl GoShape { let entry = spec.entry_name.as_str(); let kind = spec.entry_kind.tag(); - let has_http_handler = source.contains("http.ResponseWriter") - && source.contains("*http.Request"); - let has_gin_import = source.contains("github.com/gin-gonic/gin") - || source.contains("// nyx-shape: gin"); + let has_http_handler = + source.contains("http.ResponseWriter") && source.contains("*http.Request"); + let has_gin_import = + source.contains("github.com/gin-gonic/gin") || source.contains("// nyx-shape: gin"); let has_gin_ctx = source.contains("gin.Context") || source.contains("*gin.Context"); let has_echo = source.contains("github.com/labstack/echo") || source.contains("echo.New") @@ -286,7 +286,10 @@ pub fn detect_shape(spec: &HarnessSpec) -> GoShape { } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -595,7 +598,11 @@ pub fn emit(spec: &HarnessSpec) -> Result { // Phase 21 (Track M.3): GraphQLResolver short-circuit (gqlgen). if let crate::evidence::EntryKind::GraphQLResolver { type_name, field } = &spec.entry_kind { - return Ok(emit_graphql_resolver_harness(&spec.entry_name, type_name, field)); + return Ok(emit_graphql_resolver_harness( + &spec.entry_name, + type_name, + field, + )); } let entry_source = read_entry_source(&spec.entry_file); @@ -923,13 +930,7 @@ func nyxPayload() string {{ /// Imports required by the spliced probe shim. Always present, deduped /// against per-shape additions in [`imports_for_shape`]. -const SHIM_IMPORTS: &[&str] = &[ - "encoding/json", - "os/signal", - "strings", - "syscall", - "time", -]; +const SHIM_IMPORTS: &[&str] = &["encoding/json", "os/signal", "strings", "syscall", "time"]; fn imports_for_shape(shape: GoShape) -> String { let stdlib_base: &[&str] = &["encoding/base64", "os"]; @@ -939,10 +940,9 @@ fn imports_for_shape(shape: GoShape) -> String { GoShape::GinHandler => &["net/http", "net/http/httptest"], // Phase 17 framework variants drive a `httptest.NewServer` // bootstrap so they need the full net/http surface. - GoShape::GinRoute - | GoShape::EchoRoute - | GoShape::FiberRoute - | GoShape::ChiRoute => &["fmt", "net/http", "net/http/httptest"], + GoShape::GinRoute | GoShape::EchoRoute | GoShape::FiberRoute | GoShape::ChiRoute => { + &["fmt", "net/http", "net/http/httptest"] + } }; let local_pkgs: &[&str] = match shape { GoShape::GinHandler => &["nyx-harness/entry", "nyx-harness/entry/gin"], @@ -979,7 +979,10 @@ fn pre_call_setup(spec: &HarnessSpec) -> String { match &spec.payload_slot { PayloadSlot::EnvVar(name) => format!("\tos.Setenv({name:?}, payload)\n"), PayloadSlot::Argv(n) => { - let pads = (0..*n).map(|_| "\"\"".to_owned()).collect::>().join(", "); + let pads = (0..*n) + .map(|_| "\"\"".to_owned()) + .collect::>() + .join(", "); if pads.is_empty() { "\tos.Args = []string{\"nyx_harness\", payload}\n".to_string() } else { @@ -1037,34 +1040,18 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: GoShape, entry_fn: &str) -> Strin // because the synthetic entry.go ships a stdlib // `(w, r)` handler shim that mirrors the framework // handler's body. - GoShape::GinRoute => framework_route_invocation( - spec, - "NYX_GIN_TEST=1", - entry_fn, - use_body, - &query_param, - ), - GoShape::EchoRoute => framework_route_invocation( - spec, - "NYX_ECHO_TEST=1", - entry_fn, - use_body, - &query_param, - ), - GoShape::FiberRoute => framework_route_invocation( - spec, - "NYX_FIBER_TEST=1", - entry_fn, - use_body, - &query_param, - ), - GoShape::ChiRoute => framework_route_invocation( - spec, - "NYX_CHI_TEST=1", - entry_fn, - use_body, - &query_param, - ), + GoShape::GinRoute => { + framework_route_invocation(spec, "NYX_GIN_TEST=1", entry_fn, use_body, &query_param) + } + GoShape::EchoRoute => { + framework_route_invocation(spec, "NYX_ECHO_TEST=1", entry_fn, use_body, &query_param) + } + GoShape::FiberRoute => { + framework_route_invocation(spec, "NYX_FIBER_TEST=1", entry_fn, use_body, &query_param) + } + GoShape::ChiRoute => { + framework_route_invocation(spec, "NYX_CHI_TEST=1", entry_fn, use_body, &query_param) + } } } @@ -1187,10 +1174,7 @@ func main() {{ command: vec!["./nyx_harness".to_owned()], extra_files: vec![ ("go.mod".to_owned(), go_mod), - ( - "entry/nyx_auto_registry.go".to_owned(), - auto_registry, - ), + ("entry/nyx_auto_registry.go".to_owned(), auto_registry), ], entry_subpath: Some("entry/entry.go".to_owned()), } @@ -1591,9 +1575,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!GoEmitter.entry_kinds_supported().is_empty()); - assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::Function)); - assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::HttpRoute)); - assert!(GoEmitter.entry_kinds_supported().contains(&EntryKindTag::CliSubcommand)); + assert!( + GoEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + GoEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::HttpRoute) + ); + assert!( + GoEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); } #[test] @@ -1644,7 +1640,8 @@ mod tests { #[test] fn shape_detect_gin_route() { - let src = "package main\nimport \"github.com/gin-gonic/gin\"\nfunc Handle(c *gin.Context) {}"; + let src = + "package main\nimport \"github.com/gin-gonic/gin\"\nfunc Handle(c *gin.Context) {}"; let spec = make_spec_with(EntryKind::HttpRoute, "Handle", "entry.go"); assert_eq!(GoShape::detect(&spec, src), GoShape::GinRoute); } @@ -1769,7 +1766,8 @@ mod tests { "install_crash_guard definition missing from generated main.go", ); assert!( - h.source.contains("__nyx_install_crash_guard(\"HandleRequest\")"), + h.source + .contains("__nyx_install_crash_guard(\"HandleRequest\")"), "install_crash_guard call site missing or wrong callee in main()", ); let install_pos = h diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index 7f337ac8..2a46c81f 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -275,7 +275,6 @@ impl JavaShape { // the JDK accepts whitespace / newline / modifier variation that no // single template captures.) - // ── Probe shim (Phase 06 + Phase 08) ───────────────────────────────────────── /// Source of the `__nyx_probe` shim for the Java harness (Phase 06 — @@ -617,7 +616,11 @@ pub fn emit(spec: &HarnessSpec) -> Result { if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind { let entry_source = read_entry_source(&spec.entry_file); let entry_class = derive_entry_class(&entry_source); - return Ok(emit_scheduled_job_harness(spec, schedule.as_deref(), &entry_class)); + return Ok(emit_scheduled_job_harness( + spec, + schedule.as_deref(), + &entry_class, + )); } // Phase 21 (Track M.3): Middleware short-circuit (Spring HandlerInterceptor / Filter). @@ -1754,9 +1757,9 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> JavaShape::StaticMain => format!( " String[] mainArgs = new String[] {{ payload }};\n {entry_class}.main(mainArgs);" ), - JavaShape::ServletDoGet => format!( - " invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\");" - ), + JavaShape::ServletDoGet => { + format!(" invokeServlet({entry_class}.class, \"doGet\", payload, \"GET\");") + } JavaShape::ServletDoPost => format!( " invokeServlet({entry_class}.class, \"doPost\", payload, \"POST\");" ), @@ -1772,20 +1775,18 @@ fn invoke_for_shape(spec: &HarnessSpec, shape: JavaShape, entry_class: &str) -> " System.out.println(\"NYX_SPRING_TEST=1\");\n invokeReflective({entry_class}.class, \"{method}\", payload);" ) } else { - format!( - " invokeReflective({entry_class}.class, \"{method}\", payload);" - ) + format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") } } - JavaShape::QuarkusRoute => format!( - " invokeReflective({entry_class}.class, \"{method}\", payload);" - ), - JavaShape::MicronautRoute => format!( - " invokeReflective({entry_class}.class, \"{method}\", payload);" - ), - JavaShape::JunitTest => format!( - " invokeJunitTest({entry_class}.class, \"{method}\");" - ), + JavaShape::QuarkusRoute => { + format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") + } + JavaShape::MicronautRoute => { + format!(" invokeReflective({entry_class}.class, \"{method}\", payload);") + } + JavaShape::JunitTest => { + format!(" invokeJunitTest({entry_class}.class, \"{method}\");") + } } } @@ -1794,9 +1795,9 @@ fn shape_helpers(shape: JavaShape) -> &'static str { match shape { JavaShape::StaticMethod | JavaShape::StaticMain => "", JavaShape::ServletDoGet | JavaShape::ServletDoPost => SERVLET_HELPER, - JavaShape::SpringController - | JavaShape::QuarkusRoute - | JavaShape::MicronautRoute => REFLECTIVE_HELPER, + JavaShape::SpringController | JavaShape::QuarkusRoute | JavaShape::MicronautRoute => { + REFLECTIVE_HELPER + } JavaShape::JunitTest => JUNIT_HELPER, } } @@ -2522,15 +2523,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!JavaEmitter.entry_kinds_supported().is_empty()); - assert!(JavaEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::Function)); - assert!(JavaEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::HttpRoute)); - assert!(JavaEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::CliSubcommand)); + assert!( + JavaEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + JavaEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::HttpRoute) + ); + assert!( + JavaEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); } #[test] @@ -2602,7 +2609,8 @@ mod tests { #[test] fn shape_detect_junit_test() { - let src = "import org.junit.jupiter.api.Test;\npublic class V { @Test public void testRun() {} }"; + let src = + "import org.junit.jupiter.api.Test;\npublic class V { @Test public void testRun() {} }"; let spec = make_spec_with(EntryKind::Function, "testRun", "V.java"); assert_eq!(JavaShape::detect(&spec, src), JavaShape::JunitTest); } @@ -2689,7 +2697,11 @@ mod tests { let mut spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); spec.payload_slot = PayloadSlot::QueryParam("payload".into()); let harness = emit(&spec).unwrap(); - let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + let paths: Vec<&str> = harness + .extra_files + .iter() + .map(|(p, _)| p.as_str()) + .collect(); assert!( paths.contains(&"javax/servlet/http/HttpServletRequest.java"), "doGet bundle missing javax HttpServletRequest stub; got {paths:?}" @@ -2714,7 +2726,11 @@ mod tests { spec.payload_slot = PayloadSlot::HttpBody; let harness = emit(&spec).unwrap(); assert!(!harness.extra_files.is_empty(), "doPost bundle is empty"); - let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + let paths: Vec<&str> = harness + .extra_files + .iter() + .map(|(p, _)| p.as_str()) + .collect(); assert!(paths.contains(&"javax/servlet/http/HttpServlet.java")); assert!(paths.contains(&"jakarta/servlet/http/HttpServlet.java")); } @@ -2729,7 +2745,11 @@ mod tests { assert!( harness.extra_files.is_empty(), "non-servlet shape unexpectedly ships extra files: {:?}", - harness.extra_files.iter().map(|(p, _)| p).collect::>() + harness + .extra_files + .iter() + .map(|(p, _)| p) + .collect::>() ); } @@ -2756,7 +2776,11 @@ mod tests { ); let spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); let harness = emit(&spec).unwrap(); - let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + let paths: Vec<&str> = harness + .extra_files + .iter() + .map(|(p, _)| p.as_str()) + .collect(); // Servlet stubs are present (same as the non-OWASP servlet case). assert!(paths.contains(&"javax/servlet/http/HttpServletRequest.java")); // OWASP helpers + esapi + spring stubs are appended. @@ -2779,7 +2803,11 @@ mod tests { ); let spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); let harness = emit(&spec).unwrap(); - let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + let paths: Vec<&str> = harness + .extra_files + .iter() + .map(|(p, _)| p.as_str()) + .collect(); assert!( !paths.iter().any(|p| p.starts_with("org/owasp/")), "plain servlet entry unexpectedly bundles OWASP stubs: {paths:?}" @@ -2803,7 +2831,11 @@ mod tests { ); let spec = make_spec_with(EntryKind::Function, "run", &entry_file); let harness = emit(&spec).unwrap(); - let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + let paths: Vec<&str> = harness + .extra_files + .iter() + .map(|(p, _)| p.as_str()) + .collect(); assert!(paths.contains(&"org/owasp/benchmark/helpers/Utils.java")); // No servlet stubs for a non-servlet shape. assert!(!paths.iter().any(|p| p.starts_with("javax/servlet/"))); @@ -2965,7 +2997,10 @@ mod tests { "Java chain step must keep its NYX_PREV_OUTPUT forwarder" ); let shim_pos = step.source.find("__nyx_probe").unwrap(); - let driver_pos = step.source.find("System.getenv(\"NYX_PREV_OUTPUT\")").unwrap(); + let driver_pos = step + .source + .find("System.getenv(\"NYX_PREV_OUTPUT\")") + .unwrap(); assert!( shim_pos < driver_pos, "probe shim must come before the driver so the shim's helpers are in scope when a sink rewrite splices in" @@ -2983,10 +3018,7 @@ mod tests { // Drive the public `detect_shape(spec)` wrapper end-to-end: // write a representative source to a tempfile, then assert the // wrapper reads it and produces the expected JavaShape variant. - let dir = std::env::temp_dir().join(format!( - "nyx_detect_shape_{}", - std::process::id() - )); + let dir = std::env::temp_dir().join(format!("nyx_detect_shape_{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let cases: &[(&str, &str, &str, EntryKind, JavaShape)] = &[ ( diff --git a/src/dynamic/lang/java_owasp_stubs.rs b/src/dynamic/lang/java_owasp_stubs.rs index 2898609c..571bee9f 100644 --- a/src/dynamic/lang/java_owasp_stubs.rs +++ b/src/dynamic/lang/java_owasp_stubs.rs @@ -78,14 +78,8 @@ pub fn owasp_stub_files() -> Vec<(String, String)> { "org/owasp/benchmark/helpers/ThingInterface.java".to_owned(), thing_interface_stub(), ), - ( - "org/owasp/esapi/ESAPI.java".to_owned(), - esapi_stub(), - ), - ( - "org/owasp/esapi/Encoder.java".to_owned(), - encoder_stub(), - ), + ("org/owasp/esapi/ESAPI.java".to_owned(), esapi_stub()), + ("org/owasp/esapi/Encoder.java".to_owned(), encoder_stub()), ( "org/springframework/dao/DataAccessException.java".to_owned(), data_access_exception_stub(), @@ -344,10 +338,7 @@ mod tests { #[test] fn bundle_includes_owasp_helpers() { - let paths: Vec = owasp_stub_files() - .into_iter() - .map(|(p, _)| p) - .collect(); + let paths: Vec = owasp_stub_files().into_iter().map(|(p, _)| p).collect(); for required in &[ "org/owasp/benchmark/helpers/Utils.java", "org/owasp/benchmark/helpers/DatabaseHelper.java", @@ -457,6 +448,10 @@ mod tests { // count drift here usually means a stub was added without // updating the assertion or a stub got accidentally dropped. let files = owasp_stub_files(); - assert_eq!(files.len(), 13, "expected 9 owasp + 4 springframework stubs"); + assert_eq!( + files.len(), + 13, + "expected 9 owasp + 4 springframework stubs" + ); } } diff --git a/src/dynamic/lang/java_servlet_stubs.rs b/src/dynamic/lang/java_servlet_stubs.rs index 4880ab4e..da969d15 100644 --- a/src/dynamic/lang/java_servlet_stubs.rs +++ b/src/dynamic/lang/java_servlet_stubs.rs @@ -69,10 +69,7 @@ fn make_servlet_stubs(pkg: &str) -> Vec<(String, String)> { format!("{http_path}/HttpServletResponse.java"), http_servlet_response(&http), ), - ( - format!("{http_path}/HttpSession.java"), - http_session(&http), - ), + (format!("{http_path}/HttpSession.java"), http_session(&http)), (format!("{http_path}/Cookie.java"), cookie(&http)), ] } diff --git a/src/dynamic/lang/javascript.rs b/src/dynamic/lang/javascript.rs index cd1240b4..9e9e1f07 100644 --- a/src/dynamic/lang/javascript.rs +++ b/src/dynamic/lang/javascript.rs @@ -15,11 +15,13 @@ //! - [`PayloadSlot::Argv`] — coerced to positional `Param(0)` by build_call. use crate::dynamic::environment::{Environment, RuntimeArtifacts}; -use crate::dynamic::lang::{js_shared, ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter}; +use crate::dynamic::lang::{ + ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter, js_shared, +}; use crate::dynamic::spec::{EntryKindTag, HarnessSpec}; use crate::evidence::UnsupportedReason; -pub use js_shared::{detect_shape, materialize_node, probe_shim, JsShape}; +pub use js_shared::{JsShape, detect_shape, materialize_node, probe_shim}; /// Zero-sized [`LangEmitter`] handle for JavaScript. pub struct JavaScriptEmitter; @@ -115,7 +117,11 @@ mod tests { fn emit_env_var_slot() { let spec = make_spec(PayloadSlot::EnvVar("DB_HOST".into())); let harness = emit(&spec).unwrap(); - assert!(harness.source.contains("process.env[\"DB_HOST\"] = payload")); + assert!( + harness + .source + .contains("process.env[\"DB_HOST\"] = payload") + ); } #[test] @@ -155,5 +161,4 @@ mod tests { assert!(hint.contains("HttpRoute")); assert!(hint.contains("Phase 13")); } - } diff --git a/src/dynamic/lang/js_shared.rs b/src/dynamic/lang/js_shared.rs index c3573dc3..6dc3acdd 100644 --- a/src/dynamic/lang/js_shared.rs +++ b/src/dynamic/lang/js_shared.rs @@ -79,11 +79,21 @@ impl JsShape { // ── Framework / runtime markers ───────────────────────────── let has_express = source_has_marker( source, - &["require('express')", "require(\"express\")", "from 'express'", "from \"express\""], + &[ + "require('express')", + "require(\"express\")", + "from 'express'", + "from \"express\"", + ], ); let has_koa = source_has_marker( source, - &["require('koa')", "require(\"koa\")", "from 'koa'", "from \"koa\""], + &[ + "require('koa')", + "require(\"koa\")", + "from 'koa'", + "from \"koa\"", + ], ); let has_fastify = source_has_marker( source, @@ -109,7 +119,13 @@ impl JsShape { ); let has_next = source_has_marker( source, - &["from 'next'", "from \"next\"", "NextApiRequest", "NextApiResponse", "// nyx-shape: next"], + &[ + "from 'next'", + "from \"next\"", + "NextApiRequest", + "NextApiResponse", + "// nyx-shape: next", + ], ); let has_jsdom = source_has_marker( source, @@ -374,9 +390,10 @@ pub fn materialize_node(env: &Environment) -> RuntimeArtifacts { } for fw in &env.frameworks { if let Some(name) = node_framework_pkg_name(*fw) - && seen.insert(name.to_owned()) { - deps.push((name.to_owned(), "*")); - } + && seen.insert(name.to_owned()) + { + deps.push((name.to_owned(), "*")); + } } deps.sort_by(|a, b| a.0.cmp(&b.0)); @@ -406,10 +423,26 @@ pub fn materialize_node(env: &Environment) -> RuntimeArtifacts { fn is_node_builtin(name: &str) -> bool { matches!( name, - "fs" | "path" | "http" | "https" | "url" | "crypto" | "stream" - | "util" | "child_process" | "os" | "events" | "buffer" - | "querystring" | "zlib" | "assert" | "process" | "net" - | "tls" | "dns" | "readline" | "tty" + "fs" | "path" + | "http" + | "https" + | "url" + | "crypto" + | "stream" + | "util" + | "child_process" + | "os" + | "events" + | "buffer" + | "querystring" + | "zlib" + | "assert" + | "process" + | "net" + | "tls" + | "dns" + | "readline" + | "tty" ) } @@ -431,24 +464,54 @@ fn node_framework_pkg_name(fw: DetectedFramework) -> Option<&'static str> { fn extra_files_for_shape(shape: JsShape) -> Vec<(String, String)> { match shape { JsShape::Express => vec![ - ("package.json".to_owned(), package_json_for("express", "^4.19.2")), - ("package-lock.json".to_owned(), package_lock_skeleton("nyx-harness-express")), + ( + "package.json".to_owned(), + package_json_for("express", "^4.19.2"), + ), + ( + "package-lock.json".to_owned(), + package_lock_skeleton("nyx-harness-express"), + ), ], JsShape::Koa => vec![ - ("package.json".to_owned(), package_json_for("koa", "^2.15.3")), - ("package-lock.json".to_owned(), package_lock_skeleton("nyx-harness-koa")), + ( + "package.json".to_owned(), + package_json_for("koa", "^2.15.3"), + ), + ( + "package-lock.json".to_owned(), + package_lock_skeleton("nyx-harness-koa"), + ), ], JsShape::NextRoute => vec![ - ("package.json".to_owned(), package_json_for("next", "^14.2.5")), - ("package-lock.json".to_owned(), package_lock_skeleton("nyx-harness-next")), + ( + "package.json".to_owned(), + package_json_for("next", "^14.2.5"), + ), + ( + "package-lock.json".to_owned(), + package_lock_skeleton("nyx-harness-next"), + ), ], JsShape::BrowserEvent => vec![ - ("package.json".to_owned(), package_json_for("jsdom", "^24.1.1")), - ("package-lock.json".to_owned(), package_lock_skeleton("nyx-harness-jsdom")), + ( + "package.json".to_owned(), + package_json_for("jsdom", "^24.1.1"), + ), + ( + "package-lock.json".to_owned(), + package_lock_skeleton("nyx-harness-jsdom"), + ), ], JsShape::Fastify => vec![ - ("package.json".to_owned(), package_json_for("fastify", "^4.28.1")), - ("package-lock.json".to_owned(), package_lock_skeleton("nyx-harness-fastify")), + ( + "package.json".to_owned(), + package_json_for("fastify", "^4.28.1"), + ), + ( + "package-lock.json".to_owned(), + package_lock_skeleton("nyx-harness-fastify"), + ), ], JsShape::Nest => vec![ ( @@ -634,7 +697,11 @@ fn emit_class_method( is_typescript: bool, ) -> HarnessSource { let probe = probe_shim(); - let entry_subpath = if is_typescript { "entry.ts" } else { "entry.js" }; + let entry_subpath = if is_typescript { + "entry.ts" + } else { + "entry.js" + }; let entry_require_path = entry_require_path(entry_subpath); let mock_http = crate::dynamic::stubs::mock_source( crate::dynamic::stubs::MockKind::HttpClient, @@ -733,13 +800,13 @@ if (typeof _m !== 'function') {{ /// and publishes the payload onto `queue` so the handler fires /// synchronously. SQS is the only broker Node has a dedicated Phase /// 20 adapter for (`sqs-node`); the dispatch defaults to it. -fn emit_message_handler( - spec: &HarnessSpec, - queue: &str, - is_typescript: bool, -) -> HarnessSource { +fn emit_message_handler(spec: &HarnessSpec, queue: &str, is_typescript: bool) -> HarnessSource { let probe = probe_shim(); - let entry_subpath = if is_typescript { "entry.ts" } else { "entry.js" }; + let entry_subpath = if is_typescript { + "entry.ts" + } else { + "entry.js" + }; let entry_require_path = entry_require_path(entry_subpath); let handler = &spec.entry_name; let sqs_src = crate::dynamic::stubs::sqs_source(crate::symbol::Lang::JavaScript); @@ -808,7 +875,11 @@ _broker.subscribe({queue:?}, async (envelope) => {{ fn nyx_js_preamble(spec: &HarnessSpec, is_typescript: bool) -> (String, String) { let probe = probe_shim(); - let entry_subpath = if is_typescript { "entry.ts" } else { "entry.js" }; + let entry_subpath = if is_typescript { + "entry.ts" + } else { + "entry.js" + }; let require_path = entry_require_path(entry_subpath); let preamble = format!( r#"'use strict'; @@ -844,7 +915,11 @@ process.stdout.write('__NYX_SINK_HIT__\n'); (preamble, entry_subpath.to_owned()) } -fn emit_scheduled_job(spec: &HarnessSpec, schedule: Option<&str>, is_typescript: bool) -> HarnessSource { +fn emit_scheduled_job( + spec: &HarnessSpec, + schedule: Option<&str>, + is_typescript: bool, +) -> HarnessSource { let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript); let handler = &spec.entry_name; let schedule_repr = schedule.unwrap_or(""); @@ -2214,21 +2289,33 @@ mod tests { #[test] fn detect_express_via_require() { let src = "const express = require('express');\nfunction ping(req, res) {}"; - let spec = make_spec(EntryKind::Function, "ping", PayloadSlot::QueryParam("host".into())); + let spec = make_spec( + EntryKind::Function, + "ping", + PayloadSlot::QueryParam("host".into()), + ); assert_eq!(JsShape::detect(&spec, src), JsShape::Express); } #[test] fn detect_koa_via_require() { let src = "const Koa = require('koa');\nasync function ping(ctx) {}"; - let spec = make_spec(EntryKind::Function, "ping", PayloadSlot::QueryParam("host".into())); + let spec = make_spec( + EntryKind::Function, + "ping", + PayloadSlot::QueryParam("host".into()), + ); assert_eq!(JsShape::detect(&spec, src), JsShape::Koa); } #[test] fn detect_next_via_marker() { let src = "// nyx-shape: next\nmodule.exports = async function handler(req, res) {};"; - let spec = make_spec(EntryKind::HttpRoute, "handler", PayloadSlot::QueryParam("host".into())); + let spec = make_spec( + EntryKind::HttpRoute, + "handler", + PayloadSlot::QueryParam("host".into()), + ); assert_eq!(JsShape::detect(&spec, src), JsShape::NextRoute); } @@ -2248,7 +2335,8 @@ mod tests { #[test] fn detect_esm_default_export() { - let src = "// nyx-shape: esm-default\nexport default function runPing(host) { return host; }"; + let src = + "// nyx-shape: esm-default\nexport default function runPing(host) { return host; }"; let spec = make_spec(EntryKind::Function, "runPing", PayloadSlot::Param(0)); assert_eq!(JsShape::detect(&spec, src), JsShape::EsModuleDefault); } @@ -2262,7 +2350,11 @@ mod tests { #[test] fn emit_express_uses_mock_req_res() { - let spec = make_spec(EntryKind::HttpRoute, "ping", PayloadSlot::QueryParam("host".into())); + let spec = make_spec( + EntryKind::HttpRoute, + "ping", + PayloadSlot::QueryParam("host".into()), + ); let src = generate_for_shape(&spec, JsShape::Express, "entry.js"); assert!(src.contains("Express handler")); assert!(src.contains("_req.query[_payload_key] = payload")); @@ -2270,7 +2362,11 @@ mod tests { #[test] fn emit_koa_awaits_middleware() { - let spec = make_spec(EntryKind::HttpRoute, "ping", PayloadSlot::QueryParam("host".into())); + let spec = make_spec( + EntryKind::HttpRoute, + "ping", + PayloadSlot::QueryParam("host".into()), + ); let src = generate_for_shape(&spec, JsShape::Koa, "entry.js"); assert!(src.contains("await _mw(_ctx")); } @@ -2293,7 +2389,11 @@ mod tests { #[test] fn extra_files_for_express_has_package_json() { let extras = extra_files_for_shape(JsShape::Express); - assert!(extras.iter().any(|(p, c)| p == "package.json" && c.contains("express"))); + assert!( + extras + .iter() + .any(|(p, c)| p == "package.json" && c.contains("express")) + ); assert!(extras.iter().any(|(p, _)| p == "package-lock.json")); } diff --git a/src/dynamic/lang/mod.rs b/src/dynamic/lang/mod.rs index 3d285161..cb24498a 100644 --- a/src/dynamic/lang/mod.rs +++ b/src/dynamic/lang/mod.rs @@ -257,8 +257,7 @@ pub fn emit(spec: &HarnessSpec) -> Result { if !supported.is_empty() && !supported.contains(&spec.entry_kind.tag()) { return Err(UnsupportedReason::EntryKindUnsupported); } - dispatch(spec.lang, |e| e.emit(spec)) - .unwrap_or(Err(UnsupportedReason::LangUnsupported)) + dispatch(spec.lang, |e| e.emit(spec)).unwrap_or(Err(UnsupportedReason::LangUnsupported)) } /// Public free-fn dispatcher for the supported entry kinds of `lang`. @@ -276,9 +275,7 @@ pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKindTag] { /// callers do not need to special-case that path. pub fn entry_kind_hint(lang: Lang, attempted: EntryKindTag) -> String { dispatch(lang, |e| e.entry_kind_hint(attempted)).unwrap_or_else(|| { - format!( - "no harness emitter is registered for {lang:?}; attempted {attempted}" - ) + format!("no harness emitter is registered for {lang:?}; attempted {attempted}") }) } @@ -384,13 +381,13 @@ mod tests { T::WebSocket ); assert_eq!( - EntryKind::Middleware { name: "auth".into() }.tag(), + EntryKind::Middleware { + name: "auth".into() + } + .tag(), T::Middleware ); - assert_eq!( - EntryKind::Migration { version: None }.tag(), - T::Migration - ); + assert_eq!(EntryKind::Migration { version: None }.tag(), T::Migration); assert_eq!(EntryKind::Unknown.tag(), T::Unknown); } @@ -418,16 +415,14 @@ mod tests { // juniper (Rust), gqlgen (Go). TypeScript shares the JS // emitter so it inherits resolver dispatch. ( - Lang::Python - | Lang::JavaScript - | Lang::TypeScript - | Lang::Rust - | Lang::Go, + Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Rust | Lang::Go, T::GraphQLResolver, ) => true, // WebSocket: socketio + channels (Python), ws (JS), // actioncable (Ruby). - (Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Ruby, T::WebSocket) => true, + (Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Ruby, T::WebSocket) => { + true + } // Middleware: express (JS), django (Python), rails (Ruby), // spring (Java), laravel (PHP). ( @@ -442,11 +437,7 @@ mod tests { // Migration: rails (Ruby), django + flask (Python), // laravel (PHP), sequelize + prisma (JS). ( - Lang::Python - | Lang::JavaScript - | Lang::TypeScript - | Lang::Ruby - | Lang::Php, + Lang::Python | Lang::JavaScript | Lang::TypeScript | Lang::Ruby | Lang::Php, T::Migration, ) => true, _ => false, @@ -505,13 +496,7 @@ mod tests { Lang::TypeScript, Lang::Go, ]; - let unsupported_langs = [ - Lang::Php, - Lang::Ruby, - Lang::Rust, - Lang::C, - Lang::Cpp, - ]; + let unsupported_langs = [Lang::Php, Lang::Ruby, Lang::Rust, Lang::C, Lang::Cpp]; for lang in supported_langs { let supported = entry_kinds_supported(lang); assert!( diff --git a/src/dynamic/lang/php.rs b/src/dynamic/lang/php.rs index 12d448b5..ae166fd5 100644 --- a/src/dynamic/lang/php.rs +++ b/src/dynamic/lang/php.rs @@ -212,8 +212,8 @@ impl PhpShape { || source.contains("$router->post(") || source.contains("// nyx-shape: route"); let has_argv = source.contains("$argv") || source.contains("// nyx-shape: cli"); - let has_function_decl = source.contains("function ") - && !source.trim_start().starts_with(" PhpShape { } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -1124,7 +1127,11 @@ fn generate_source(spec: &HarnessSpec, shape: PhpShape) -> String { let call_expr = build_call_expr(spec, shape, entry_fn); let shim = probe_shim(); let toolchain_marker = build_toolchain_marker(shape); - let crash_callee = if entry_fn.is_empty() { "main" } else { entry_fn.as_str() }; + let crash_callee = if entry_fn.is_empty() { + "main" + } else { + entry_fn.as_str() + }; format!( r#" String { "null".to_owned() } } - PhpShape::RouteClosure - | PhpShape::LaravelRoute - | PhpShape::CodeIgniterRoute => { + PhpShape::RouteClosure | PhpShape::LaravelRoute | PhpShape::CodeIgniterRoute => { // Entry script publishes the route closure via // `$GLOBALS['__nyx_route']`. When the global is missing, // fall back to calling the named function directly. @@ -1608,15 +1613,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!PhpEmitter.entry_kinds_supported().is_empty()); - assert!(PhpEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::Function)); - assert!(PhpEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::HttpRoute)); - assert!(PhpEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::CliSubcommand)); + assert!( + PhpEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + PhpEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::HttpRoute) + ); + assert!( + PhpEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); } #[test] diff --git a/src/dynamic/lang/python.rs b/src/dynamic/lang/python.rs index 4964fcc3..76948faa 100644 --- a/src/dynamic/lang/python.rs +++ b/src/dynamic/lang/python.rs @@ -187,8 +187,7 @@ impl PythonShape { let kind = spec.entry_kind.tag(); // ── Framework-first detection ──────────────────────────────── - let has_flask = - source_has_marker(source, &["from flask", "import flask", "Flask("]); + let has_flask = source_has_marker(source, &["from flask", "import flask", "Flask("]); let has_fastapi = source_has_marker( source, &["from fastapi", "import fastapi", "FastAPI(", "APIRouter("], @@ -270,8 +269,7 @@ fn source_has_marker(source: &str, markers: &[&str]) -> bool { fn function_is_pytest(source: &str, name: &str) -> bool { let needle = format!("def {name}("); let async_needle = format!("async def {name}("); - (source.contains(&needle) || source.contains(&async_needle)) - && name.starts_with("test_") + (source.contains(&needle) || source.contains(&async_needle)) && name.starts_with("test_") } fn function_is_async(source: &str, name: &str) -> bool { @@ -613,8 +611,12 @@ fn python_framework_pkg_name(fw: DetectedFramework) -> Option<&'static str> { /// pre-Phase-12 behaviour. pub fn emit(spec: &HarnessSpec) -> Result { match &spec.payload_slot { - PayloadSlot::Param(_) | PayloadSlot::EnvVar(_) | PayloadSlot::Stdin - | PayloadSlot::QueryParam(_) | PayloadSlot::HttpBody | PayloadSlot::Argv(_) => {} + PayloadSlot::Param(_) + | PayloadSlot::EnvVar(_) + | PayloadSlot::Stdin + | PayloadSlot::QueryParam(_) + | PayloadSlot::HttpBody + | PayloadSlot::Argv(_) => {} } // Phase 03 (Track J.1): short-circuit to the deserialize harness @@ -1934,10 +1936,9 @@ fn read_entry_source(entry_file: &str) -> String { fn extra_files_for_shape(shape: PythonShape) -> Vec<(String, String)> { match shape { PythonShape::FlaskRoute => vec![("requirements.txt".to_owned(), "Flask\n".to_owned())], - PythonShape::FastApiRoute => vec![( - "requirements.txt".to_owned(), - "fastapi\nhttpx\n".to_owned(), - )], + PythonShape::FastApiRoute => { + vec![("requirements.txt".to_owned(), "fastapi\nhttpx\n".to_owned())] + } PythonShape::StarletteRoute => vec![( "requirements.txt".to_owned(), "starlette\nhttpx\n".to_owned(), @@ -2494,7 +2495,12 @@ fn build_call(spec: &HarnessSpec, func: &str) -> (String, String) { // Heuristic: identifiers starting with lowercase that look // like Python identifiers are kwargs; everything else is an // env var. - if name.chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) { + if name + .chars() + .next() + .map(|c| c.is_ascii_lowercase()) + .unwrap_or(false) + { let pre = String::new(); let call = format!("_entry_mod.{func}({name}=payload)"); (pre, call) @@ -2505,8 +2511,8 @@ fn build_call(spec: &HarnessSpec, func: &str) -> (String, String) { } } PayloadSlot::Stdin => { - let pre = "import io\nsys.stdin = io.TextIOWrapper(io.BytesIO(_payload_raw))\n" - .to_owned(); + let pre = + "import io\nsys.stdin = io.TextIOWrapper(io.BytesIO(_payload_raw))\n".to_owned(); let call = format!("_entry_mod.{func}()"); (pre, call) } @@ -2534,7 +2540,12 @@ fn build_call_args(spec: &HarnessSpec) -> (String, String) { (pre, args) } PayloadSlot::EnvVar(name) => { - if name.chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) { + if name + .chars() + .next() + .map(|c| c.is_ascii_lowercase()) + .unwrap_or(false) + { (String::new(), format!("{name}=payload")) } else { let pre = format!("os.environ[{name:?}] = payload\n"); @@ -2542,8 +2553,8 @@ fn build_call_args(spec: &HarnessSpec) -> (String, String) { } } PayloadSlot::Stdin => { - let pre = "import io\nsys.stdin = io.TextIOWrapper(io.BytesIO(_payload_raw))\n" - .to_owned(); + let pre = + "import io\nsys.stdin = io.TextIOWrapper(io.BytesIO(_payload_raw))\n".to_owned(); (pre, String::new()) } _ => (String::new(), "payload".to_owned()), @@ -2625,7 +2636,11 @@ mod tests { fn emit_env_var_slot_uppercase_sets_env() { let spec = make_spec(PayloadSlot::EnvVar("USER_INPUT".into())); let harness = emit(&spec).unwrap(); - assert!(harness.source.contains("os.environ[\"USER_INPUT\"] = payload")); + assert!( + harness + .source + .contains("os.environ[\"USER_INPUT\"] = payload") + ); assert!(harness.source.contains("login()")); } @@ -2687,7 +2702,8 @@ mod tests { #[test] fn shape_detect_fastapi() { - let src = "from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef index(): pass\n"; + let src = + "from fastapi import FastAPI\napp = FastAPI()\n@app.get('/')\ndef index(): pass\n"; let spec = make_spec_with(EntryKind::HttpRoute, "index"); assert_eq!(PythonShape::detect(&spec, src), PythonShape::FastApiRoute); } @@ -2809,15 +2825,21 @@ mod tests { #[test] fn extra_files_flask_pins_flask() { let extras = extra_files_for_shape(PythonShape::FlaskRoute); - assert!(extras.iter().any(|(p, c)| p == "requirements.txt" && c.contains("Flask"))); + assert!( + extras + .iter() + .any(|(p, c)| p == "requirements.txt" && c.contains("Flask")) + ); } #[test] fn extra_files_fastapi_pins_httpx() { let extras = extra_files_for_shape(PythonShape::FastApiRoute); - assert!(extras - .iter() - .any(|(p, c)| p == "requirements.txt" && c.contains("fastapi") && c.contains("httpx"))); + assert!( + extras.iter().any(|(p, c)| p == "requirements.txt" + && c.contains("fastapi") + && c.contains("httpx")) + ); } #[test] @@ -2832,9 +2854,9 @@ mod tests { #[test] fn extra_files_starlette_pins_httpx() { let extras = extra_files_for_shape(PythonShape::StarletteRoute); - assert!(extras.iter().any( - |(p, c)| p == "requirements.txt" && c.contains("starlette") && c.contains("httpx") - )); + assert!(extras.iter().any(|(p, c)| p == "requirements.txt" + && c.contains("starlette") + && c.contains("httpx"))); } fn make_spec_with(kind: EntryKind, name: &str) -> HarnessSpec { diff --git a/src/dynamic/lang/ruby.rs b/src/dynamic/lang/ruby.rs index 78da8456..3b844854 100644 --- a/src/dynamic/lang/ruby.rs +++ b/src/dynamic/lang/ruby.rs @@ -92,9 +92,7 @@ fn chain_step( terminal: Option<&ChainStepTerminal>, ) -> ChainStepHarness { let shim = probe_shim(); - let mut driver = String::from( - "prev = ENV[\"NYX_PREV_OUTPUT\"] || \"\"\n$stdout.write(prev)\n", - ); + let mut driver = String::from("prev = ENV[\"NYX_PREV_OUTPUT\"] || \"\"\n$stdout.write(prev)\n"); if let Some(t) = terminal { let callee = ruby_string_literal(&t.sink_callee); let sentinel = ruby_string_literal(ChainStepHarness::SINK_HIT_SENTINEL); @@ -211,7 +209,10 @@ pub fn detect_shape(spec: &HarnessSpec) -> RubyShape { } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -443,7 +444,10 @@ pub fn emit(spec: &HarnessSpec) -> Result { // Phase 21 (Track M.3): ScheduledJob short-circuit (Sidekiq workers). if let crate::evidence::EntryKind::ScheduledJob { schedule } = &spec.entry_kind { - return Ok(emit_scheduled_job_harness(&spec.entry_name, schedule.as_deref())); + return Ok(emit_scheduled_job_harness( + &spec.entry_name, + schedule.as_deref(), + )); } // Phase 21 (Track M.3): WebSocket short-circuit (ActionCable channels). @@ -1188,7 +1192,11 @@ fn generate_source(spec: &HarnessSpec, shape: RubyShape) -> String { let pre_call = build_pre_call(spec); let invocation = invoke_for_shape(spec, shape, entry_fn); let shim = probe_shim(); - let crash_callee = if entry_fn.is_empty() { "main" } else { entry_fn.as_str() }; + let crash_callee = if entry_fn.is_empty() { + "main" + } else { + entry_fn.as_str() + }; format!( r#"# Nyx dynamic harness — auto-generated, do not edit (Phase 15 — RubyShape::{shape:?}). @@ -1448,15 +1456,21 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!RubyEmitter.entry_kinds_supported().is_empty()); - assert!(RubyEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::Function)); - assert!(RubyEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::HttpRoute)); - assert!(RubyEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::CliSubcommand)); + assert!( + RubyEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); + assert!( + RubyEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::HttpRoute) + ); + assert!( + RubyEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::CliSubcommand) + ); } #[test] @@ -1576,8 +1590,14 @@ mod tests { #[test] fn parse_first_class_name_picks_up_class_decl() { - assert_eq!(parse_first_class_name("class Foo\nend\n"), Some("Foo".to_owned())); - assert_eq!(parse_first_class_name("class Bar < Base\nend\n"), Some("Bar".to_owned())); + assert_eq!( + parse_first_class_name("class Foo\nend\n"), + Some("Foo".to_owned()) + ); + assert_eq!( + parse_first_class_name("class Bar < Base\nend\n"), + Some("Bar".to_owned()) + ); assert_eq!(parse_first_class_name("def foo\nend\n"), None); } @@ -1590,7 +1610,8 @@ mod tests { "probe_shim banner missing from generated harness.rb — splicing regressed", ); assert!( - h.source.contains("def __nyx_install_crash_guard(sink_callee)"), + h.source + .contains("def __nyx_install_crash_guard(sink_callee)"), "install_crash_guard definition missing from generated harness.rb", ); assert!( diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index 60df449b..f9405bdd 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -793,7 +793,10 @@ fn main() {{ } fn read_entry_source(entry_file: &str) -> String { - let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)]; + let candidates = [ + PathBuf::from(entry_file), + PathBuf::from(".").join(entry_file), + ]; for path in &candidates { if let Ok(s) = std::fs::read_to_string(path) { return s; @@ -1079,29 +1082,28 @@ fn class_derives_default(entry_src: &str, class: &str) -> bool { let window_start = decl_pos.saturating_sub(256); let window = &entry_src[window_start..decl_pos]; if let Some(derive_pos) = window.rfind("#[derive(") - && let Some(end_rel) = window[derive_pos..].find(")]") { - let end = derive_pos + end_rel; - let derive_list = &window[derive_pos + "#[derive(".len()..end]; - let between = &window[end + ")]".len()..]; - // The derive attribute must directly precede the - // declaration — no other item / statement may sit - // between `#[derive(...)]` and the `struct` / - // `enum` token. Forbidden tokens (`;`, `{`, `}`, - // `=`, or another item keyword) signal the derive - // belongs to an earlier declaration. - let between_clean = strip_attrs_and_comments(between); - let forbidden = ['{', '}', ';', '=']; - let item_keyword = ["struct", "enum", "fn", "impl", "trait", "type", "mod"] - .iter() - .any(|kw| word_in_text(&between_clean, kw)); - let attaches_to_decl = !between_clean.chars().any(|c| forbidden.contains(&c)) - && !item_keyword; - if attaches_to_decl - && derive_list.split(',').any(|t| t.trim() == "Default") - { - return true; - } + && let Some(end_rel) = window[derive_pos..].find(")]") + { + let end = derive_pos + end_rel; + let derive_list = &window[derive_pos + "#[derive(".len()..end]; + let between = &window[end + ")]".len()..]; + // The derive attribute must directly precede the + // declaration — no other item / statement may sit + // between `#[derive(...)]` and the `struct` / + // `enum` token. Forbidden tokens (`;`, `{`, `}`, + // `=`, or another item keyword) signal the derive + // belongs to an earlier declaration. + let between_clean = strip_attrs_and_comments(between); + let forbidden = ['{', '}', ';', '=']; + let item_keyword = ["struct", "enum", "fn", "impl", "trait", "type", "mod"] + .iter() + .any(|kw| word_in_text(&between_clean, kw)); + let attaches_to_decl = + !between_clean.chars().any(|c| forbidden.contains(&c)) && !item_keyword; + if attaches_to_decl && derive_list.split(',').any(|t| t.trim() == "Default") { + return true; } + } } search_from = decl_pos + 1; } @@ -1143,8 +1145,7 @@ fn word_in_text(text: &str, kw: &str) -> bool { let mut i = 0usize; while i + kw_bytes.len() <= bytes.len() { if &bytes[i..i + kw_bytes.len()] == kw_bytes { - let before_ok = i == 0 - || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_'; + let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_'; let after_idx = i + kw_bytes.len(); let after_ok = after_idx >= bytes.len() || (!bytes[after_idx].is_ascii_alphanumeric() && bytes[after_idx] != b'_'); @@ -1319,15 +1320,10 @@ fn actix_invocation(spec: &HarnessSpec, func: &str) -> (String, String) { format!(" std::env::set_var({name:?}, &payload);\n"), format!("let _ = entry::{func}(\"\");"), ), - PayloadSlot::HttpBody => ( - String::new(), - format!("let _ = entry::{func}(&payload);"), - ), + PayloadSlot::HttpBody => (String::new(), format!("let _ = entry::{func}(&payload);")), PayloadSlot::QueryParam(name) => ( String::new(), - format!( - "let _ = entry::{func}(&format!(\"{name}={{}}\", payload));", - ), + format!("let _ = entry::{func}(&format!(\"{name}={{}}\", payload));",), ), _ => (String::new(), format!("let _ = entry::{func}(&payload);")), } @@ -1399,8 +1395,14 @@ mod tests { let cargo = harness.extra_files.iter().find(|(n, _)| n == "Cargo.toml"); assert!(cargo.is_some(), "Cargo.toml must be in extra_files"); let cargo_content = &cargo.unwrap().1; - assert!(cargo_content.contains("rusqlite"), "SQL_QUERY cap needs rusqlite dep"); - assert!(cargo_content.contains("bundled"), "rusqlite must use bundled feature"); + assert!( + cargo_content.contains("rusqlite"), + "SQL_QUERY cap needs rusqlite dep" + ); + assert!( + cargo_content.contains("bundled"), + "rusqlite must use bundled feature" + ); } #[test] @@ -1408,8 +1410,15 @@ mod tests { let mut spec = make_spec(PayloadSlot::Param(0)); spec.expected_cap = Cap::CODE_EXEC; let harness = emit(&spec).unwrap(); - let cargo = harness.extra_files.iter().find(|(n, _)| n == "Cargo.toml").unwrap(); - assert!(!cargo.1.contains("rusqlite"), "CODE_EXEC must not have rusqlite dep"); + let cargo = harness + .extra_files + .iter() + .find(|(n, _)| n == "Cargo.toml") + .unwrap(); + assert!( + !cargo.1.contains("rusqlite"), + "CODE_EXEC must not have rusqlite dep" + ); } #[test] @@ -1433,7 +1442,8 @@ mod tests { #[test] fn class_derives_default_matches_explicit_impl() { - let src = "struct UserService;\nimpl Default for UserService { fn default() -> Self { Self } }"; + let src = + "struct UserService;\nimpl Default for UserService { fn default() -> Self { Self } }"; assert!(class_derives_default(src, "UserService")); } @@ -1487,9 +1497,11 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty() { assert!(!RustEmitter.entry_kinds_supported().is_empty()); - assert!(RustEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::Function)); + assert!( + RustEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::Function) + ); } #[test] @@ -1516,7 +1528,8 @@ mod tests { // shape; the legacy [`RustShape::AxumHandler`] fires only on // weak detectors (`IntoResponse` / `Json(` without `use // axum::`). - let src = "use axum::extract::Query; pub fn handler(payload: &str) -> String { String::new() }"; + let src = + "use axum::extract::Query; pub fn handler(payload: &str) -> String { String::new() }"; let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs"); assert_eq!(RustShape::detect(&spec, src), RustShape::AxumRoute); } diff --git a/src/dynamic/lang/typescript.rs b/src/dynamic/lang/typescript.rs index 26535ca1..b551137d 100644 --- a/src/dynamic/lang/typescript.rs +++ b/src/dynamic/lang/typescript.rs @@ -15,7 +15,9 @@ //! runtime ignores. use crate::dynamic::environment::{Environment, RuntimeArtifacts}; -use crate::dynamic::lang::{js_shared, ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter}; +use crate::dynamic::lang::{ + ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter, js_shared, +}; use crate::dynamic::spec::{EntryKindTag, HarnessSpec}; use crate::evidence::UnsupportedReason; @@ -87,9 +89,11 @@ mod tests { #[test] fn entry_kinds_supported_is_non_empty_and_includes_http_route() { assert!(!TypeScriptEmitter.entry_kinds_supported().is_empty()); - assert!(TypeScriptEmitter - .entry_kinds_supported() - .contains(&EntryKindTag::HttpRoute)); + assert!( + TypeScriptEmitter + .entry_kinds_supported() + .contains(&EntryKindTag::HttpRoute) + ); } #[test] @@ -101,7 +105,9 @@ mod tests { #[test] fn typescript_emit_stages_entry_at_entry_js_for_node_resolution() { - let h = TypeScriptEmitter.emit(&make_spec(EntryKind::Function)).unwrap(); + let h = TypeScriptEmitter + .emit(&make_spec(EntryKind::Function)) + .unwrap(); // TS fixtures use ES-compatible syntax; the workdir layout matches // JavaScript so Node's CJS `require('./entry')` resolves without an // extension-loader hook. See js_shared::entry_subpath_for_shape. diff --git a/src/dynamic/mod.rs b/src/dynamic/mod.rs index e8149121..e779783e 100644 --- a/src/dynamic/mod.rs +++ b/src/dynamic/mod.rs @@ -78,8 +78,8 @@ pub mod oracle; pub mod policy; pub mod probe; pub mod rand; -pub mod repro; pub mod report; +pub mod repro; pub mod runner; pub mod sandbox; pub mod spec; @@ -91,4 +91,4 @@ pub mod verify; pub use report::{VerifyResult, VerifyStatus}; pub use spec::HarnessSpec; -pub use verify::{verify_finding, VerifyOptions}; +pub use verify::{VerifyOptions, verify_finding}; diff --git a/src/dynamic/mount_filter.rs b/src/dynamic/mount_filter.rs index 83d71bc6..8e55a9b6 100644 --- a/src/dynamic/mount_filter.rs +++ b/src/dynamic/mount_filter.rs @@ -53,7 +53,10 @@ fn scan_dir_recursive(project_root: &Path, dir: &Path, notes: &mut Vec bool { - matches!(name, ".git" | "node_modules" | "__pycache__" | ".tox" | "venv" | ".venv") + matches!( + name, + ".git" | "node_modules" | "__pycache__" | ".tox" | "venv" | ".venv" + ) } fn matches_dir_pattern(name: &str) -> Option<&'static str> { @@ -128,9 +131,17 @@ mod tests { #[test] fn detects_pem_file() { let dir = TempDir::new().unwrap(); - fs::write(dir.path().join("server.pem"), "-----BEGIN CERTIFICATE-----\n").unwrap(); + fs::write( + dir.path().join("server.pem"), + "-----BEGIN CERTIFICATE-----\n", + ) + .unwrap(); let notes = scan_sensitive_files(dir.path()); - assert!(notes.iter().any(|n| n.path.ends_with(".pem") || n.path.contains("server.pem"))); + assert!( + notes + .iter() + .any(|n| n.path.ends_with(".pem") || n.path.contains("server.pem")) + ); } #[test] @@ -146,6 +157,9 @@ mod tests { let dir = TempDir::new().unwrap(); fs::write(dir.path().join("main.py"), "print('hi')\n").unwrap(); let notes = scan_sensitive_files(dir.path()); - assert!(notes.is_empty(), "clean dir should produce no notes: {notes:?}"); + assert!( + notes.is_empty(), + "clean dir should produce no notes: {notes:?}" + ); } } diff --git a/src/dynamic/oob.rs b/src/dynamic/oob.rs index 49ad97f5..15eb3b92 100644 --- a/src/dynamic/oob.rs +++ b/src/dynamic/oob.rs @@ -63,7 +63,11 @@ impl OobListener { accept_loop(listener, hits_clone, shutdown_clone); }); - Ok(Self { port, hits, shutdown }) + Ok(Self { + port, + hits, + shutdown, + }) } /// Port the listener is bound to. @@ -86,10 +90,7 @@ impl OobListener { /// Returns `true` if `nonce` was received by the listener. pub fn was_nonce_hit(&self, nonce: &str) -> bool { - self.hits - .lock() - .map(|h| h.contains(nonce)) - .unwrap_or(false) + self.hits.lock().map(|h| h.contains(nonce)).unwrap_or(false) } /// Polls until `nonce` is recorded or `timeout` elapses. @@ -144,9 +145,10 @@ fn handle_connection(stream: TcpStream, hits: Arc>>) { let mut first_line = String::new(); if reader.read_line(&mut first_line).is_ok() && let Some(nonce) = parse_nonce_from_request_line(&first_line) - && let Ok(mut h) = hits.lock() { - h.insert(nonce); - } + && let Ok(mut h) = hits.lock() + { + h.insert(nonce); + } // Drain remaining headers so the client doesn't get ECONNRESET. loop { let mut line = String::new(); @@ -158,7 +160,8 @@ fn handle_connection(stream: TcpStream, hits: Arc>>) { } } let mut w = &stream; - let _ = w.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/plain\r\n\r\nok"); + let _ = + w.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/plain\r\n\r\nok"); } /// Extract the nonce from a `GET /{nonce} HTTP/1.1` request line. diff --git a/src/dynamic/oracle.rs b/src/dynamic/oracle.rs index e811b97e..1d0bca98 100644 --- a/src/dynamic/oracle.rs +++ b/src/dynamic/oracle.rs @@ -397,7 +397,9 @@ pub enum Oracle { /// declaration `const`-friendly (Phase 06 deferred the /// `Vec` shape the plan listed because the corpus is /// declared in static memory; a `Vec` would require runtime init). - SinkProbe { predicates: &'static [ProbePredicate] }, + SinkProbe { + predicates: &'static [ProbePredicate], + }, /// Phase 08 sink-site crash oracle. Fires iff at least one drained /// probe has [`ProbeKind::Crash { signal }`] with `signal ∈ signals`. /// A process-level abort that did not reach the sink handler leaves no @@ -584,9 +586,7 @@ pub fn oracle_fired_with_stubs( return false; } let idor_ok = cross.iter().all(|p| match p { - ProbePredicate::IdorBoundaryCrossed => { - probes_satisfy_idor_crossed(probes) - } + ProbePredicate::IdorBoundaryCrossed => probes_satisfy_idor_crossed(probes), _ => true, }); if !idor_ok { @@ -745,12 +745,15 @@ fn stdout_template_equals(stdout: &[u8], expected: u64) -> bool { } let parsed: serde_json::Result = serde_json::from_str(trimmed); let Ok(v) = parsed else { continue }; - let Some(render) = v.get("render") else { continue }; + let Some(render) = v.get("render") else { + continue; + }; let Some(s) = render.as_str() else { continue }; if let Ok(n) = s.trim().parse::() - && n == expected { - return true; - } + && n == expected + { + return true; + } } false } @@ -759,9 +762,9 @@ fn stdout_template_equals(stdout: &[u8], expected: u64) -> bool { /// [`ProbeKind::Deserialize`] record matching `require_invoked`. fn probes_satisfy_deserialize(probes: &[SinkProbe], require_invoked: bool) -> bool { probes.iter().any(|p| match &p.kind { - ProbeKind::Deserialize { gadget_chain_invoked } => { - *gadget_chain_invoked == require_invoked - } + ProbeKind::Deserialize { + gadget_chain_invoked, + } => *gadget_chain_invoked == require_invoked, _ => false, }) } @@ -795,8 +798,7 @@ fn probes_satisfy_count_gt(probes: &[SinkProbe], n: u32) -> bool { fn probes_satisfy_header_injected(probes: &[SinkProbe], header_name: &str) -> bool { probes.iter().any(|p| match &p.kind { ProbeKind::HeaderEmit { name, value } => { - (header_name == "*" || name.eq_ignore_ascii_case(header_name)) - && value.contains("\r\n") + (header_name == "*" || name.eq_ignore_ascii_case(header_name)) && value.contains("\r\n") } _ => false, }) @@ -813,9 +815,10 @@ fn probes_satisfy_header_injected(probes: &[SinkProbe], header_name: &str) -> bo /// `//host/...` references are parsed as off-origin. fn probes_satisfy_redirect_off_origin(probes: &[SinkProbe], allowlist: &[&str]) -> bool { probes.iter().any(|p| match &p.kind { - ProbeKind::Redirect { location, request_host } => { - redirect_is_off_origin(location, request_host, allowlist) - } + ProbeKind::Redirect { + location, + request_host, + } => redirect_is_off_origin(location, request_host, allowlist), _ => false, }) } @@ -861,7 +864,10 @@ fn probes_satisfy_weak_key(probes: &[SinkProbe], max_bits: u32) -> bool { /// [`ProbePredicate::IdorBoundaryCrossed`] (Phase 11 — Track J.9). fn probes_satisfy_idor_crossed(probes: &[SinkProbe]) -> bool { probes.iter().any(|p| match &p.kind { - ProbeKind::IdorAccess { caller_id, owner_id } => caller_id != owner_id, + ProbeKind::IdorAccess { + caller_id, + owner_id, + } => caller_id != owner_id, _ => false, }) } @@ -877,9 +883,7 @@ fn probes_satisfy_outbound_off_list(probes: &[SinkProbe], allowlist: &[&str]) -> if h.is_empty() { return false; } - !allowlist - .iter() - .any(|a| h == a.trim().to_ascii_lowercase()) + !allowlist.iter().any(|a| h == a.trim().to_ascii_lowercase()) } _ => false, }) @@ -899,9 +903,7 @@ pub(crate) fn redirect_is_off_origin( return false; }; let host_lower = host.to_ascii_lowercase(); - if !request_host.is_empty() - && host_lower == request_host.trim().to_ascii_lowercase() - { + if !request_host.is_empty() && host_lower == request_host.trim().to_ascii_lowercase() { return false; } !allowlist @@ -929,14 +931,15 @@ fn extract_redirect_host(location: &str) -> Option { return None; }; // Strip path / query / fragment from the host segment. - let end = rest - .find(['/', '?', '#']) - .unwrap_or(rest.len()); + let end = rest.find(['/', '?', '#']).unwrap_or(rest.len()); let authority = &rest[..end]; // Strip userinfo + port. Bracketed IPv6 authorities (`[::1]` or // `[::1]:8080`) must keep the brackets together — splitting on the // last `:` inside the literal would slice the address apart. - let after_userinfo = authority.rsplit_once('@').map(|(_, h)| h).unwrap_or(authority); + let after_userinfo = authority + .rsplit_once('@') + .map(|(_, h)| h) + .unwrap_or(authority); let host_only = if let Some(rest) = after_userinfo.strip_prefix('[') { match rest.find(']') { Some(end) => &after_userinfo[..end + 2], @@ -1077,7 +1080,10 @@ mod tests { let oracle = Oracle::SinkProbe { predicates: &[ ProbePredicate::CalleeEquals("os.system"), - ProbePredicate::ArgContains { index: 0, needle: "; echo" }, + ProbePredicate::ArgContains { + index: 0, + needle: "; echo", + }, ], }; let probes = vec![probe( @@ -1100,13 +1106,13 @@ mod tests { let oracle = Oracle::SinkProbe { predicates: &[ ProbePredicate::CalleeEquals("os.system"), - ProbePredicate::ArgContains { index: 0, needle: "NEVER_PRESENT" }, + ProbePredicate::ArgContains { + index: 0, + needle: "NEVER_PRESENT", + }, ], }; - let probes = vec![probe( - "os.system", - vec![ProbeArg::String("hello".into())], - )]; + let probes = vec![probe("os.system", vec![ProbeArg::String("hello".into())])]; assert!(!oracle_fired(&oracle, &outcome(), &probes)); } @@ -1158,7 +1164,10 @@ mod tests { #[test] fn arg_equals_predicate() { let oracle = Oracle::SinkProbe { - predicates: &[ProbePredicate::ArgEquals { index: 0, value: "exact" }], + predicates: &[ProbePredicate::ArgEquals { + index: 0, + value: "exact", + }], }; let hit = vec![probe("f", vec![ProbeArg::String("exact".into())])]; let miss = vec![probe("f", vec![ProbeArg::String("inexact".into())])]; @@ -1306,7 +1315,10 @@ mod tests { allowlist: &["example.com", "cdn.example.com"], }], }; - let probes = vec![redirect_probe("https://cdn.example.com/asset", "example.com")]; + let probes = vec![redirect_probe( + "https://cdn.example.com/asset", + "example.com", + )]; assert!(!oracle_fired(&oracle, &outcome(), &probes)); } @@ -1315,7 +1327,10 @@ mod tests { let oracle = Oracle::SinkProbe { predicates: &[ProbePredicate::RedirectHostNotIn { allowlist: &[] }], }; - let probes = vec![redirect_probe("https://example.com/dashboard", "example.com")]; + let probes = vec![redirect_probe( + "https://example.com/dashboard", + "example.com", + )]; assert!(!oracle_fired(&oracle, &outcome(), &probes)); } diff --git a/src/dynamic/policy.rs b/src/dynamic/policy.rs index 7d653b2e..cfa09c94 100644 --- a/src/dynamic/policy.rs +++ b/src/dynamic/policy.rs @@ -211,7 +211,9 @@ impl Scrubber { return true; } let lower = text.to_ascii_lowercase(); - PII_LITERAL_SUBSTRINGS.iter().any(|needle| lower.contains(*needle)) + PII_LITERAL_SUBSTRINGS + .iter() + .any(|needle| lower.contains(*needle)) } /// Scrub `text`, returning a new `String` whose value is either the @@ -572,7 +574,10 @@ mod tests { #[test] fn truncate_at_exact_boundary_unchanged() { let bytes = vec![0u8; PAYLOAD_CAPTURE_LIMIT_BYTES]; - assert_eq!(truncate_payload_bytes(&bytes).len(), PAYLOAD_CAPTURE_LIMIT_BYTES); + assert_eq!( + truncate_payload_bytes(&bytes).len(), + PAYLOAD_CAPTURE_LIMIT_BYTES + ); } #[test] diff --git a/src/dynamic/probe.rs b/src/dynamic/probe.rs index 1dc519bd..b493d456 100644 --- a/src/dynamic/probe.rs +++ b/src/dynamic/probe.rs @@ -307,7 +307,6 @@ pub enum ProbeKind { }, } - /// Bounded forensic snapshot captured alongside a [`SinkProbe`] /// (Phase 08 — Track C.5). /// @@ -515,9 +514,8 @@ impl ProbeChannel { .append(true) .create(true) .open(&self.path)?; - let line = serde_json::to_string(probe).map_err(|e| { - std::io::Error::new(std::io::ErrorKind::InvalidData, e) - })?; + let line = serde_json::to_string(probe) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; file.write_all(line.as_bytes())?; file.write_all(b"\n")?; Ok(()) @@ -611,13 +609,17 @@ mod tests { let dir = TempDir::new().unwrap(); let ch = ProbeChannel::for_workdir(dir.path()).unwrap(); let mut p = sample_probe("crash-test"); - p.kind = ProbeKind::Crash { signal: Signal::Sigsegv }; + p.kind = ProbeKind::Crash { + signal: Signal::Sigsegv, + }; ch.write(&p).unwrap(); let drained = ch.drain(); assert_eq!(drained.len(), 1); assert!(matches!( drained[0].kind, - ProbeKind::Crash { signal: Signal::Sigsegv } + ProbeKind::Crash { + signal: Signal::Sigsegv + } )); } @@ -660,7 +662,9 @@ mod tests { assert_eq!(w.payload_bytes.len(), policy::PAYLOAD_CAPTURE_LIMIT_BYTES); assert_eq!(w.env_snapshot.get("PATH").map(String::as_str), Some("/bin")); assert_eq!( - w.env_snapshot.get("AWS_SECRET_ACCESS_KEY").map(String::as_str), + w.env_snapshot + .get("AWS_SECRET_ACCESS_KEY") + .map(String::as_str), Some(policy::REDACTED_VALUE) ); assert_eq!(w.args_repr, vec!["ls; whoami".to_owned()]); diff --git a/src/dynamic/repro.rs b/src/dynamic/repro.rs index 0e4192e0..94b12ce8 100644 --- a/src/dynamic/repro.rs +++ b/src/dynamic/repro.rs @@ -136,7 +136,10 @@ pub fn write( fs::create_dir_all(&src_dir)?; // Also write Cargo.toml for Rust repro bundles. let cargo_content = crate::dynamic::lang::rust::generate_cargo_toml(spec.expected_cap); - fs::write(root.join("harness").join("Cargo.toml"), cargo_content.as_bytes())?; + fs::write( + root.join("harness").join("Cargo.toml"), + cargo_content.as_bytes(), + )?; src_dir.join("main.rs") } else { root.join("harness").join(format!("harness.{ext}")) @@ -145,7 +148,10 @@ pub fn write( // harness/Dockerfile.harness let dockerfile = dockerfile_for_spec(spec); - fs::write(root.join("harness").join("Dockerfile.harness"), dockerfile.as_bytes())?; + fs::write( + root.join("harness").join("Dockerfile.harness"), + dockerfile.as_bytes(), + )?; // payload/payload.bin + payload.meta.json fs::write(root.join("payload").join("payload.bin"), payload_bytes)?; @@ -154,7 +160,10 @@ pub fn write( "len": payload_bytes.len(), "encoding": "raw", }); - write_json(&root.join("payload").join("payload.meta.json"), &payload_meta)?; + write_json( + &root.join("payload").join("payload.meta.json"), + &payload_meta, + )?; // sandbox/options.json let sandbox_opts = serde_json::json!({ @@ -166,7 +175,10 @@ pub fn write( // sandbox/env.allowlist.json let env_list: Vec<&str> = opts.env_passthrough.iter().map(|s| s.as_str()).collect(); - write_json(&root.join("sandbox").join("env.allowlist.json"), &serde_json::json!(env_list))?; + write_json( + &root.join("sandbox").join("env.allowlist.json"), + &serde_json::json!(env_list), + )?; // expected/outcome.json — redacted let redacted_stdout = redact::redact(&outcome.stdout); @@ -235,7 +247,10 @@ pub fn write( // Per-project symlink (§12 Q1) let symlink = if let Some(proj_root) = project_root { - let link_dir = proj_root.join(".nyx").join("dynamic-cache").join("symlinks"); + let link_dir = proj_root + .join(".nyx") + .join("dynamic-cache") + .join("symlinks"); let _ = fs::create_dir_all(&link_dir); let link_path = link_dir.join(&spec.spec_hash); let _ = create_symlink(&root, &link_path); @@ -252,11 +267,12 @@ fn repro_root(spec_hash: &str) -> Result { let base = if let Ok(p) = std::env::var("NYX_REPRO_BASE") { PathBuf::from(p) } else { - let dirs = ProjectDirs::from("", "", "nyx") - .ok_or_else(|| ReproError::Io(std::io::Error::new( + let dirs = ProjectDirs::from("", "", "nyx").ok_or_else(|| { + ReproError::Io(std::io::Error::new( std::io::ErrorKind::NotFound, "cannot determine cache dir", - )))?; + )) + })?; dirs.cache_dir().join("dynamic").join("repro") }; @@ -328,7 +344,10 @@ fn resolve_dockerfile_from(spec: &HarnessSpec) -> String { format!("rust:{toolchain}-slim") } Lang::Python => { - format!("python:{}", spec.toolchain_id.strip_prefix("python-").unwrap_or("3")) + format!( + "python:{}", + spec.toolchain_id.strip_prefix("python-").unwrap_or("3") + ) } _ => "ubuntu:latest".to_owned(), } @@ -391,7 +410,10 @@ fn reproduce_script(spec: &HarnessSpec, payload_label: &str) -> String { // `reproduce.sh --docker` which sources the runtime from the pinned // image and bypasses the host toolchain entirely. let host_probe_cmd = match spec.lang { - Lang::Rust | Lang::Go | Lang::C | Lang::Cpp => "./harness/nyx_harness --help >/dev/null 2>&1 || test -x ./harness/nyx_harness".to_owned(), + Lang::Rust | Lang::Go | Lang::C | Lang::Cpp => { + "./harness/nyx_harness --help >/dev/null 2>&1 || test -x ./harness/nyx_harness" + .to_owned() + } Lang::Python => "command -v python3".to_owned(), Lang::JavaScript | Lang::TypeScript => "command -v node".to_owned(), Lang::Java => "command -v java".to_owned(), @@ -510,7 +532,10 @@ fn build_toolchain_lock(spec: &HarnessSpec, root: &Path) -> Result Result Option { /// /// Callers who want "did this bundle replay green?" semantics get a typed /// result instead of parsing shell output. -pub fn replay_bundle( - bundle_root: &Path, - extra_args: &[&str], -) -> ReplayResult { +pub fn replay_bundle(bundle_root: &Path, extra_args: &[&str]) -> ReplayResult { use std::process::Command; let script = bundle_root.join("reproduce.sh"); if !script.exists() { @@ -779,9 +804,17 @@ mod tests { let outcome = make_outcome(); let verdict = make_verdict(); let artifact = write( - &spec, &opts, &outcome, &verdict, - "# harness", "# entry", b"payload", "label", None, - ).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .unwrap(); let lock_path = artifact.root.join("toolchain.lock"); assert!(lock_path.exists(), "toolchain.lock missing"); let lock: serde_json::Value = @@ -848,9 +881,17 @@ mod tests { let dir = TempDir::new().unwrap(); unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) }; let artifact = write( - &make_spec(), &SandboxOptions::default(), &make_outcome(), &make_verdict(), - "# harness", "# entry", b"payload", "label", None, - ).unwrap(); + &make_spec(), + &SandboxOptions::default(), + &make_outcome(), + &make_verdict(), + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .unwrap(); let script = std::fs::read_to_string(artifact.root.join("reproduce.sh")).unwrap(); // Exit code 3 documented + emitted on host toolchain mismatch. assert!(script.contains("EXPECTED_TOOLCHAIN=\"python-3.11\"")); @@ -872,7 +913,8 @@ mod tests { std::fs::set_permissions( bundle.join("reproduce.sh"), std::fs::Permissions::from_mode(0o755), - ).unwrap(); + ) + .unwrap(); } assert_eq!(replay_bundle(&bundle, &[]), ReplayResult::Pass); } @@ -891,14 +933,16 @@ mod tests { std::fs::write( bundle.join("reproduce.sh"), format!("#!/bin/sh\nexit {code}\n"), - ).unwrap(); + ) + .unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions( bundle.join("reproduce.sh"), std::fs::Permissions::from_mode(0o755), - ).unwrap(); + ) + .unwrap(); } assert_eq!(replay_bundle(&bundle, &[]), *expected); } @@ -961,9 +1005,17 @@ mod tests { let outcome = make_outcome(); let verdict = make_verdict(); let artifact = write( - &spec, &opts, &outcome, &verdict, - "# harness", "# entry", b"payload", "label", None, - ).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .unwrap(); let resolved = bundle_root_for(&spec.spec_hash).unwrap(); assert_eq!(resolved, artifact.root); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; @@ -982,12 +1034,24 @@ mod tests { let verdict = make_verdict(); let artifact = write( - &spec, &opts, &outcome, &verdict, - "# harness", "# entry", b"payload", "label", None, - ).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .unwrap(); - let outcome_json = std::fs::read_to_string(artifact.root.join("expected/outcome.json")).unwrap(); - assert!(!outcome_json.contains("AKIAFAKETEST00000000"), "AWS key must be redacted in outcome.json"); + let outcome_json = + std::fs::read_to_string(artifact.root.join("expected/outcome.json")).unwrap(); + assert!( + !outcome_json.contains("AKIAFAKETEST00000000"), + "AWS key must be redacted in outcome.json" + ); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } diff --git a/src/dynamic/runner.rs b/src/dynamic/runner.rs index 023323cf..faa51a33 100644 --- a/src/dynamic/runner.rs +++ b/src/dynamic/runner.rs @@ -7,16 +7,16 @@ use crate::dynamic::build_sandbox; use crate::dynamic::corpus::{ - materialise_bytes, payloads_for, payloads_for_lang, resolve_benign_control, - resolve_benign_control_lang, Payload, + Payload, materialise_bytes, payloads_for, payloads_for_lang, resolve_benign_control, + resolve_benign_control_lang, }; use crate::dynamic::differential; use crate::dynamic::harness::{self, HarnessError}; -use crate::dynamic::oracle::{oracle_fired_with_stubs, probe_crash_signal, Oracle}; +use crate::dynamic::oracle::{Oracle, oracle_fired_with_stubs, probe_crash_signal}; use crate::dynamic::probe::{ProbeChannel, SinkProbe}; -use crate::dynamic::stubs::StubEvent; use crate::dynamic::sandbox::{self, SandboxBackend, SandboxError, SandboxOptions, SandboxOutcome}; use crate::dynamic::spec::HarnessSpec; +use crate::dynamic::stubs::StubEvent; use crate::dynamic::trace::{TraceStage, VerifyTrace}; use crate::evidence::{DifferentialOutcome, DifferentialVerdict}; use crate::symbol::Lang; @@ -105,10 +105,17 @@ pub enum RunError { /// at the verify boundary so unsupported-budget accounting /// distinguishes "no oracle exists" from "no payloads carved /// yet". - SoundOracleUnavailable { cap: crate::labels::Cap, lang: Lang, hint: String }, + SoundOracleUnavailable { + cap: crate::labels::Cap, + lang: Lang, + hint: String, + }, Harness(HarnessError), Sandbox(SandboxError), - BuildFailed { stderr: String, attempts: u32 }, + BuildFailed { + stderr: String, + attempts: u32, + }, } impl From for RunError { @@ -198,12 +205,13 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { if let Some(cmd0) = harness.command.first_mut() - && (cmd0 == "python3" || cmd0 == "python") { - let venv_python = build_result.venv_path.join("bin").join("python3"); - if venv_python.exists() { - *cmd0 = venv_python.to_string_lossy().into_owned(); - } + && (cmd0 == "python3" || cmd0 == "python") + { + let venv_python = build_result.venv_path.join("bin").join("python3"); + if venv_python.exists() { + *cmd0 = venv_python.to_string_lossy().into_owned(); } + } } Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { return Err(RunError::BuildFailed { stderr, attempts }); @@ -221,17 +229,18 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { - return Err(RunError::BuildFailed { - stderr, - attempts, - }); + return Err(RunError::BuildFailed { stderr, attempts }); } Err(_) => { // Io: fall back to whatever command was set (will likely fail at exec). @@ -240,7 +249,9 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { // npm install for dependency resolution (no deps in basic fixtures). - if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = build_sandbox::prepare_node(spec, &harness.workdir) { + if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = + build_sandbox::prepare_node(spec, &harness.workdir) + { return Err(RunError::BuildFailed { stderr, attempts }); } } @@ -284,7 +295,9 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { // composer install if composer.json is present. - if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = build_sandbox::prepare_php(spec, &harness.workdir) { + if let Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) = + build_sandbox::prepare_php(spec, &harness.workdir) + { return Err(RunError::BuildFailed { stderr, attempts }); } } @@ -352,9 +365,10 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result> = effective_opts.probe_channel.clone(); // Run only vuln (non-benign) payloads in the main loop. @@ -435,12 +449,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result Result = probe_channel .as_ref() .map(|ch| ch.drain()) @@ -605,7 +614,6 @@ fn uses_docker_backend(opts: &SandboxOptions) -> bool { } } - /// Generate a random 16-character hex nonce for OOB callback tracking. fn generate_nonce() -> String { use std::time::{SystemTime, UNIX_EPOCH}; @@ -629,7 +637,10 @@ mod tests { fn generate_nonce_is_16_hex_chars() { let n = generate_nonce(); assert_eq!(n.len(), 16); - assert!(n.chars().all(|c| c.is_ascii_hexdigit()), "nonce must be hex: {n}"); + assert!( + n.chars().all(|c| c.is_ascii_hexdigit()), + "nonce must be hex: {n}" + ); } #[test] diff --git a/src/dynamic/sandbox/docker.rs b/src/dynamic/sandbox/docker.rs index 6fbb51bf..1fc31994 100644 --- a/src/dynamic/sandbox/docker.rs +++ b/src/dynamic/sandbox/docker.rs @@ -90,7 +90,11 @@ pub fn ensure_image_pulled(image: &str) -> bool { // succeeds we can skip the network pull entirely. When it fails we fall // through to `docker pull` so registry-side rotations / first-time runs // still settle. - let ok = if docker_image_present(image) { true } else { docker_pull(image) }; + let ok = if docker_image_present(image) { + true + } else { + docker_pull(image) + }; cache.insert(image.to_owned(), ok); ok } @@ -249,7 +253,10 @@ mod tests { .expect("oob listener must bind on 127.0.0.1 in tests"), ); let args = network_args(&NetworkPolicy::OobOutbound { listener }); - assert!(args.iter().any(|a| a == "--add-host=host-gateway:host-gateway")); + assert!( + args.iter() + .any(|a| a == "--add-host=host-gateway:host-gateway") + ); } #[test] @@ -261,8 +268,8 @@ mod tests { fn image_reference_for_toolchain_known_returns_pinned_digest() { // The catalogue ships with hand-seeded sha256 digests for every // catalogue entry, so known IDs resolve to `@sha256:…` refs. - let r = image_reference_for_toolchain("python-3.11") - .expect("python-3.11 is in the catalogue"); + let r = + image_reference_for_toolchain("python-3.11").expect("python-3.11 is in the catalogue"); assert!(r.starts_with("python:3.11-slim@sha256:"), "got {r}"); } diff --git a/src/dynamic/sandbox/firecracker.rs b/src/dynamic/sandbox/firecracker.rs index 8b1b381b..07999dad 100644 --- a/src/dynamic/sandbox/firecracker.rs +++ b/src/dynamic/sandbox/firecracker.rs @@ -77,11 +77,15 @@ pub fn run( _opts: &SandboxOptions, ) -> Result { if !firecracker_available() { - return Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker)); + return Err(SandboxError::BackendUnavailable( + SandboxBackend::Firecracker, + )); } // Binary present but no VM logic yet. Surface BackendUnavailable // explicitly so callers do not mistakenly think the run succeeded. - Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker)) + Err(SandboxError::BackendUnavailable( + SandboxBackend::Firecracker, + )) } #[cfg(test)] @@ -122,7 +126,9 @@ mod tests { let result = run(&harness, b"", &opts); assert!(matches!( result, - Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker)) + Err(SandboxError::BackendUnavailable( + SandboxBackend::Firecracker + )) )); } } diff --git a/src/dynamic/sandbox/mod.rs b/src/dynamic/sandbox/mod.rs index 07426ff4..3637e7f7 100644 --- a/src/dynamic/sandbox/mod.rs +++ b/src/dynamic/sandbox/mod.rs @@ -24,7 +24,7 @@ use crate::dynamic::harness::BuiltHarness; use crate::dynamic::oob::OobListener; -use crate::dynamic::probe::{ProbeChannel, PROBE_PATH_ENV}; +use crate::dynamic::probe::{PROBE_PATH_ENV, ProbeChannel}; use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; @@ -276,15 +276,13 @@ pub struct SandboxOptions { /// default-deny seccomp filter scoped to [`SandboxOptions::seccomp_caps`]. /// Each primitive is best-effort; failures degrade to /// [`HardeningLevel::Partial`] without aborting the run. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ProcessHardeningProfile { #[default] Standard, Strict, } - /// Phase 20 follow-up (Track E.4 ablation harness): selectively skip or /// loosen individual Strict-profile primitives so the escape-fixture /// matrix can prove the acceptance literal "removing any one Phase 17 @@ -387,7 +385,10 @@ pub struct HostPort { impl HostPort { pub fn new(host: impl Into, port: u16) -> Self { - Self { host: host.into(), port } + Self { + host: host.into(), + port, + } } } @@ -415,13 +416,16 @@ impl HostPort { /// - [`NetworkPolicy::Open`] — unrestricted outbound. Docker: `bridge` /// with no egress filter. Reserved for diagnostic / dev-only runs; /// the verifier never sets this in production. -#[derive(Debug, Clone)] -#[derive(Default)] +#[derive(Debug, Clone, Default)] pub enum NetworkPolicy { #[default] None, - StubsOnly { allow: Vec }, - OobOutbound { listener: Arc }, + StubsOnly { + allow: Vec, + }, + OobOutbound { + listener: Arc, + }, Open, } @@ -460,7 +464,6 @@ impl NetworkPolicy { } } - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SandboxBackend { Auto, @@ -590,14 +593,14 @@ fn apply_oob_egress_filter(container_name: &str, oob_port: u16) { let rules: &[&[&str]] = &[ // Allow container → host OOB port (INPUT; docker0 bridge to host). - &["-I", "INPUT", "1", "-i", "docker0", - "-s", ip, "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT"], + &[ + "-I", "INPUT", "1", "-i", "docker0", "-s", ip, "-p", "tcp", "--dport", &port_str, "-j", + "ACCEPT", + ], // Drop all other container → host traffic (INPUT; position 2 fires after accept). - &["-I", "INPUT", "2", "-i", "docker0", - "-s", ip, "-j", "DROP"], + &["-I", "INPUT", "2", "-i", "docker0", "-s", ip, "-j", "DROP"], // Drop all container egress to external internet (FORWARD / DOCKER-USER). - &["-I", "DOCKER-USER", "1", - "-s", ip, "-j", "DROP"], + &["-I", "DOCKER-USER", "1", "-s", ip, "-j", "DROP"], ]; let mut applied = 0usize; @@ -617,7 +620,10 @@ fn apply_oob_egress_filter(container_name: &str, oob_port: u16) { if applied == rules.len() { oob_egress_registry().insert( container_name.to_owned(), - OobEgressState { container_ip, oob_port }, + OobEgressState { + container_ip, + oob_port, + }, ); } else { eprintln!( @@ -644,12 +650,12 @@ fn remove_oob_egress_filter(container_name: &str) { let ip = state.container_ip.as_str(); let rules: &[&[&str]] = &[ - &["-D", "INPUT", "-i", "docker0", - "-s", ip, "-p", "tcp", "--dport", &port_str, "-j", "ACCEPT"], - &["-D", "INPUT", "-i", "docker0", - "-s", ip, "-j", "DROP"], - &["-D", "DOCKER-USER", - "-s", ip, "-j", "DROP"], + &[ + "-D", "INPUT", "-i", "docker0", "-s", ip, "-p", "tcp", "--dport", &port_str, "-j", + "ACCEPT", + ], + &["-D", "INPUT", "-i", "docker0", "-s", ip, "-j", "DROP"], + &["-D", "DOCKER-USER", "-s", ip, "-j", "DROP"], ]; for rule in rules { @@ -680,7 +686,9 @@ fn container_registry() -> &'static dashmap::DashMap { /// on SIGKILL; the `sleep 300` in started containers bounds the leak window. #[cfg(unix)] extern "C" fn stop_all_containers() { - let Some(reg) = CONTAINER_REGISTRY.get() else { return }; + let Some(reg) = CONTAINER_REGISTRY.get() else { + return; + }; let bin = std::env::var("NYX_DOCKER_BIN").unwrap_or_else(|_| "docker".to_owned()); for entry in reg.iter() { // Remove OOB egress filter before stopping the container so stale @@ -779,10 +787,7 @@ pub fn run( // backend in that case so the harness picks up the host // venv / node_modules / vendor dir already prepared. let needs_host_deps = harness_needs_host_deps(harness); - if docker_available() - && harness_is_interpreted(&harness.command) - && !needs_host_deps - { + if docker_available() && harness_is_interpreted(&harness.command) && !needs_host_deps { run_docker(harness, payload_bytes, opts) } else if docker_available() && harness_is_native_binary(&harness.command) { run_native_binary_docker(harness, payload_bytes, opts) @@ -841,7 +846,9 @@ fn run_firecracker( } #[cfg(not(feature = "firecracker"))] { - Err(SandboxError::BackendUnavailable(SandboxBackend::Firecracker)) + Err(SandboxError::BackendUnavailable( + SandboxBackend::Firecracker, + )) } } @@ -880,12 +887,9 @@ fn rewrite_extra_env_for_container( && let Some(idx) = fs_stub_roots .iter() .position(|p| p.as_os_str() == std::ffi::OsStr::new(v)) - { - return ( - k.clone(), - format!("{}/{idx}", docker::STUB_MOUNT_ROOT), - ); - } + { + return (k.clone(), format!("{}/{idx}", docker::STUB_MOUNT_ROOT)); + } (k.clone(), v.clone()) }) .collect() @@ -930,7 +934,13 @@ fn run_docker( registry.insert(container_name.clone(), container_name.clone()); } - exec_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots) + exec_in_container( + &container_name, + harness, + payload_bytes, + opts, + &fs_stub_roots, + ) } /// Returns true when `docker info` succeeds using the current `NYX_DOCKER_BIN`. @@ -998,16 +1008,20 @@ fn start_container( "run".into(), "-d".into(), "--rm".into(), - "--name".into(), name.into(), + "--name".into(), + name.into(), "--cap-drop=ALL".into(), - "--security-opt".into(), "no-new-privileges:true".into(), - "--tmpfs".into(), "/tmp:size=128m,exec".into(), + "--security-opt".into(), + "no-new-privileges:true".into(), + "--tmpfs".into(), + "/tmp:size=128m,exec".into(), // Bind-mount the host workdir at the fixed `/work` path // read-write so harness code can reference `/work/...` without // threading the host tempdir through every layer. The mount // alone is sufficient to deliver harness files into the // container — no follow-up `docker cp` is needed. - "-v".into(), workdir_mount, + "-v".into(), + workdir_mount, ]; // Phase 10 / Phase 19 (Track D.3 + E.3): bind-mount each // filesystem-stub root at `STUB_MOUNT_ROOT/:rw` so the @@ -1141,8 +1155,10 @@ fn exec_in_container( // checks provide a second layer of defence on top of --cap-drop=ALL. // The container itself starts as root for setup (mkdir, docker cp), // but harness execution runs as nobody (uid/gid 65534). - "--user".into(), "65534:65534".into(), - "-e".into(), format!("NYX_PAYLOAD_B64={payload_b64}"), + "--user".into(), + "65534:65534".into(), + "-e".into(), + format!("NYX_PAYLOAD_B64={payload_b64}"), ]; // Mirror the process backend's `NYX_PAYLOAD` raw env var when the // payload bytes are valid UTF-8 (most curated payloads are ASCII). @@ -1157,10 +1173,11 @@ fn exec_in_container( // non-UTF-8 payloads (a `docker -e` argument must be valid UTF-8), // leaving consumers to decode `NYX_PAYLOAD_B64` themselves. if let Ok(s) = std::str::from_utf8(payload_bytes) - && !s.contains('\0') { - cmd_args.push("-e".into()); - cmd_args.push(format!("NYX_PAYLOAD={s}")); - } + && !s.contains('\0') + { + cmd_args.push("-e".into()); + cmd_args.push(format!("NYX_PAYLOAD={s}")); + } // Forward harness-specific env vars. for (k, v) in &harness.env { cmd_args.push("-e".into()); @@ -1276,7 +1293,11 @@ fn exec_in_container( /// fall through to the legacy tag mapping below so behaviour on a fresh /// catalogue stays unchanged. fn detect_image_for_harness(harness: &BuiltHarness) -> String { - let cmd0 = harness.command.first().map(|s| s.as_str()).unwrap_or("python3"); + let cmd0 = harness + .command + .first() + .map(|s| s.as_str()) + .unwrap_or("python3"); let base = std::path::Path::new(cmd0) .file_name() .and_then(|n| n.to_str()) @@ -1329,10 +1350,12 @@ fn run_native_binary_docker( let binary_path = match harness.command.first() { Some(p) => p.clone(), - None => return Err(SandboxError::Spawn(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "empty command for native binary", - ))), + None => { + return Err(SandboxError::Spawn(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "empty command for native binary", + ))); + } }; let container_name = workdir_to_container_name(&harness.workdir); @@ -1385,7 +1408,13 @@ fn run_native_binary_docker( registry.insert(container_name.clone(), container_name.clone()); } - exec_native_binary_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots) + exec_native_binary_in_container( + &container_name, + harness, + payload_bytes, + opts, + &fs_stub_roots, + ) } /// Execute a native binary already in the container at `/work/nyx_harness`. @@ -1403,8 +1432,10 @@ fn exec_native_binary_in_container( let mut cmd_args: Vec = vec![ "exec".into(), "-i".into(), - "--user".into(), "65534:65534".into(), - "-e".into(), format!("NYX_PAYLOAD_B64={payload_b64}"), + "--user".into(), + "65534:65534".into(), + "-e".into(), + format!("NYX_PAYLOAD_B64={payload_b64}"), ]; for (k, v) in &harness.env { cmd_args.push("-e".into()); @@ -1566,10 +1597,8 @@ fn run_process( None => (resolved_cmd_path.clone(), harness.command[1..].to_vec()), }; #[cfg(not(target_os = "macos"))] - let (effective_cmd_path, effective_cmd_args): (std::path::PathBuf, Vec) = ( - resolved_cmd_path.clone(), - harness.command[1..].to_vec(), - ); + let (effective_cmd_path, effective_cmd_args): (std::path::PathBuf, Vec) = + (resolved_cmd_path.clone(), harness.command[1..].to_vec()); let mut cmd = Command::new(&effective_cmd_path); cmd.args(&effective_cmd_args); @@ -1894,9 +1923,15 @@ mod tests { #[test] fn python_image_for_known_toolchains() { - assert_eq!(python_image_for_toolchain("python-3.11"), "python:3.11-slim"); + assert_eq!( + python_image_for_toolchain("python-3.11"), + "python:3.11-slim" + ); assert_eq!(python_image_for_toolchain("python-3"), "python:3-slim"); - assert_eq!(python_image_for_toolchain("python-3.12"), "python:3.12-slim"); + assert_eq!( + python_image_for_toolchain("python-3.12"), + "python:3.12-slim" + ); } #[test] @@ -1908,8 +1943,14 @@ mod tests { #[test] fn java_image_for_known_toolchains() { - assert_eq!(java_image_for_toolchain("java-21"), "eclipse-temurin:21-jre-jammy"); - assert_eq!(java_image_for_toolchain("java-17"), "eclipse-temurin:17-jre-jammy"); + assert_eq!( + java_image_for_toolchain("java-21"), + "eclipse-temurin:21-jre-jammy" + ); + assert_eq!( + java_image_for_toolchain("java-17"), + "eclipse-temurin:17-jre-jammy" + ); } #[test] @@ -1927,13 +1968,21 @@ mod tests { #[test] fn harness_is_interpreted_java() { - let cmd = vec!["java".to_owned(), "-cp".to_owned(), ".".to_owned(), "NyxHarness".to_owned()]; + let cmd = vec![ + "java".to_owned(), + "-cp".to_owned(), + ".".to_owned(), + "NyxHarness".to_owned(), + ]; assert!(harness_is_interpreted(&cmd)); } #[test] fn harness_is_interpreted_node() { - assert!(harness_is_interpreted(&["node".to_owned(), "harness.js".to_owned()])); + assert!(harness_is_interpreted(&[ + "node".to_owned(), + "harness.js".to_owned() + ])); } #[test] @@ -2076,7 +2125,10 @@ mod tests { fn fetch_docker_image_digest_short_returns_empty_on_bad_image() { // A non-existent image tag always returns empty (inspect fails). let digest = fetch_docker_image_digest_short("nyx-nonexistent-image:does-not-exist-99999"); - assert!(digest.is_empty(), "non-existent image must return empty digest"); + assert!( + digest.is_empty(), + "non-existent image must return empty digest" + ); } #[test] @@ -2174,7 +2226,10 @@ mod tests { fn rewrite_extra_env_passes_unrelated_pairs_through() { let extra = vec![ ("NYX_SQL_ENDPOINT".to_owned(), "/tmp/abc.db".to_owned()), - ("NYX_HTTP_ENDPOINT".to_owned(), "http://127.0.0.1:12345".to_owned()), + ( + "NYX_HTTP_ENDPOINT".to_owned(), + "http://127.0.0.1:12345".to_owned(), + ), ]; let out = rewrite_extra_env_for_container(&extra, &[]); assert_eq!(out, extra); @@ -2183,9 +2238,10 @@ mod tests { #[test] fn rewrite_extra_env_maps_fs_root_to_container_mount() { let host_root = PathBuf::from("/tmp/host-fs-root-abc"); - let extra = vec![ - ("NYX_FS_ROOT".to_owned(), host_root.to_string_lossy().into_owned()), - ]; + let extra = vec![( + "NYX_FS_ROOT".to_owned(), + host_root.to_string_lossy().into_owned(), + )]; let out = rewrite_extra_env_for_container(&extra, &[host_root]); assert_eq!(out.len(), 1); assert_eq!(out[0].0, "NYX_FS_ROOT"); @@ -2198,13 +2254,8 @@ mod tests { // active fs_stub_roots list is passed through unchanged. This // keeps the rewrite from accidentally clobbering an emitter- // supplied placeholder. - let extra = vec![ - ("NYX_FS_ROOT".to_owned(), "/some/host/path".to_owned()), - ]; - let out = rewrite_extra_env_for_container( - &extra, - &[PathBuf::from("/different/host/path")], - ); + let extra = vec![("NYX_FS_ROOT".to_owned(), "/some/host/path".to_owned())]; + let out = rewrite_extra_env_for_container(&extra, &[PathBuf::from("/different/host/path")]); assert_eq!(out, extra); } @@ -2212,9 +2263,10 @@ mod tests { fn rewrite_extra_env_indexes_multiple_fs_roots() { let root_a = PathBuf::from("/tmp/fs-a"); let root_b = PathBuf::from("/tmp/fs-b"); - let extra = vec![ - ("NYX_FS_ROOT".to_owned(), root_b.to_string_lossy().into_owned()), - ]; + let extra = vec![( + "NYX_FS_ROOT".to_owned(), + root_b.to_string_lossy().into_owned(), + )]; let out = rewrite_extra_env_for_container(&extra, &[root_a, root_b]); assert_eq!(out[0].1, format!("{}/1", docker::STUB_MOUNT_ROOT)); } @@ -2229,11 +2281,9 @@ mod tests { fn collect_fs_stub_roots_returns_paths_for_filesystem_stubs() { use crate::dynamic::stubs::StubKind; let dir = tempfile::TempDir::new().expect("tempdir"); - let harness = crate::dynamic::stubs::StubHarness::start( - &[StubKind::Filesystem], - dir.path(), - ) - .expect("start stub harness"); + let harness = + crate::dynamic::stubs::StubHarness::start(&[StubKind::Filesystem], dir.path()) + .expect("start stub harness"); let endpoint = harness.stubs()[0].endpoint(); let opts = SandboxOptions { stub_harness: Some(Arc::new(harness)), @@ -2248,11 +2298,9 @@ mod tests { fn collect_fs_stub_roots_skips_network_stubs() { use crate::dynamic::stubs::StubKind; let dir = tempfile::TempDir::new().expect("tempdir"); - let harness = crate::dynamic::stubs::StubHarness::start( - &[StubKind::Http, StubKind::Sql], - dir.path(), - ) - .expect("start stub harness"); + let harness = + crate::dynamic::stubs::StubHarness::start(&[StubKind::Http, StubKind::Sql], dir.path()) + .expect("start stub harness"); let opts = SandboxOptions { stub_harness: Some(Arc::new(harness)), ..SandboxOptions::default() diff --git a/src/dynamic/sandbox/process_linux.rs b/src/dynamic/sandbox/process_linux.rs index e386f55b..2f62f960 100644 --- a/src/dynamic/sandbox/process_linux.rs +++ b/src/dynamic/sandbox/process_linux.rs @@ -119,8 +119,14 @@ impl HardeningOutcome { self.chroot, self.seccomp, ]; - let applied = primitives.iter().filter(|s| matches!(s, PrimitiveStatus::Applied)).count(); - let failed = primitives.iter().filter(|s| matches!(s, PrimitiveStatus::Failed(_))).count(); + let applied = primitives + .iter() + .filter(|s| matches!(s, PrimitiveStatus::Applied)) + .count(); + let failed = primitives + .iter() + .filter(|s| matches!(s, PrimitiveStatus::Failed(_))) + .count(); match (applied, failed) { (_, 0) => HardeningLevel::Full, (0, _) => HardeningLevel::None, @@ -147,7 +153,10 @@ impl StatusPipe { if ret != 0 { return Err(std::io::Error::last_os_error()); } - Ok(Self { write_fd: fds[1], read_fd: fds[0] }) + Ok(Self { + write_fd: fds[1], + read_fd: fds[0], + }) } } @@ -289,7 +298,10 @@ fn last_errno() -> i32 { } fn apply_rlimit(resource: i32, bytes: u64) -> PrimitiveStatus { - let rl = Rlimit { cur: bytes, max: bytes }; + let rl = Rlimit { + cur: bytes, + max: bytes, + }; let ret = unsafe { setrlimit(resource, &rl) }; if ret == 0 { PrimitiveStatus::Applied @@ -498,7 +510,9 @@ impl OutcomeCollector { close_fd(self.write_fd); let read_fd = self.read_fd; let handle = std::thread::spawn(move || drain_outcome(read_fd)); - OutcomeJoiner { handle: Some(handle) } + OutcomeJoiner { + handle: Some(handle), + } } /// Call when `cmd.spawn()` failed. Closes both ends so neither fd @@ -607,10 +621,8 @@ fn build_plan(opts: &SandboxOptions, workdir: &Path) -> PreExecPlan { // prove that the corresponding seccomp slice carries its weight. let ablation = opts.ablation; let extras: Vec<&'static str> = ablation_extras(ablation); - let nrs = seccomp::allowed_syscall_numbers_with_extras( - opts.seccomp_caps, - extras.iter().copied(), - ); + let nrs = + seccomp::allowed_syscall_numbers_with_extras(opts.seccomp_caps, extras.iter().copied()); let program = seccomp::bpf::compile(&nrs, seccomp::syscalls::AUDIT_ARCH); let profile = match opts.process_hardening { @@ -718,7 +730,8 @@ fn nul_terminate(bytes: &[u8]) -> Vec { } fn canonicalize_workdir(workdir: &Path) -> Vec { - let canonical: PathBuf = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf()); + let canonical: PathBuf = + std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf()); let mut bytes = canonical.into_os_string().into_encoded_bytes(); if !bytes.ends_with(&[0]) { bytes.push(0); @@ -797,20 +810,30 @@ mod tests { let plan = build_plan(&opts, std::path::Path::new("/tmp")); // The arch check + ld nr + KILL + ALLOW alone are 5 instructions; // the BASE allowlist adds dozens more. - assert!(plan.seccomp_program.len() > 5, "BPF program too small: {}", plan.seccomp_program.len()); + assert!( + plan.seccomp_program.len() > 5, + "BPF program too small: {}", + plan.seccomp_program.len() + ); assert_eq!(plan.profile, ProcessHardeningProfileTag::Strict); } #[test] fn rlimit_as_bytes_floors_at_4_gib() { - let opts = SandboxOptions { memory_mib: 1, ..SandboxOptions::default() }; + let opts = SandboxOptions { + memory_mib: 1, + ..SandboxOptions::default() + }; let plan = build_plan(&opts, std::path::Path::new("/tmp")); assert_eq!(plan.rlimit_as_bytes, 4096_u64 * 1024 * 1024); } #[test] fn rlimit_as_bytes_scales_with_memory_mib() { - let opts = SandboxOptions { memory_mib: 1024, ..SandboxOptions::default() }; + let opts = SandboxOptions { + memory_mib: 1024, + ..SandboxOptions::default() + }; let plan = build_plan(&opts, std::path::Path::new("/tmp")); // 1024 MiB * 8 = 8192 MiB assert_eq!(plan.rlimit_as_bytes, 8192_u64 * 1024 * 1024); @@ -865,8 +888,14 @@ mod tests { // Every entry's source must be NUL-terminated for the `mount(2)` // call, and every dest must exist on disk. for m in &plan.bind_mounts { - assert!(m.source_nul.ends_with(&[0]), "source path must be NUL-terminated"); - assert!(m.dest_nul.ends_with(&[0]), "dest path must be NUL-terminated"); + assert!( + m.source_nul.ends_with(&[0]), + "source path must be NUL-terminated" + ); + assert!( + m.dest_nul.ends_with(&[0]), + "dest path must be NUL-terminated" + ); let dest_str = std::str::from_utf8(&m.dest_nul[..m.dest_nul.len() - 1]) .expect("dest path must be valid UTF-8"); assert!( @@ -920,8 +949,16 @@ mod tests { ..AblationMask::default() })); assert_eq!(flags & CLONE_NEWUSER, 0, "CLONE_NEWUSER must be dropped"); - assert_eq!(flags & CLONE_NEWPID, CLONE_NEWPID, "CLONE_NEWPID must persist"); - assert_eq!(flags & CLONE_NEWNS, CLONE_NEWNS, "CLONE_NEWNS must persist (bind-mount target)"); + assert_eq!( + flags & CLONE_NEWPID, + CLONE_NEWPID, + "CLONE_NEWPID must persist" + ); + assert_eq!( + flags & CLONE_NEWNS, + CLONE_NEWNS, + "CLONE_NEWNS must persist (bind-mount target)" + ); } #[test] @@ -931,7 +968,11 @@ mod tests { ..AblationMask::default() })); assert_eq!(flags & CLONE_NEWPID, 0, "CLONE_NEWPID must be dropped"); - assert_eq!(flags & CLONE_NEWUSER, CLONE_NEWUSER, "CLONE_NEWUSER must persist"); + assert_eq!( + flags & CLONE_NEWUSER, + CLONE_NEWUSER, + "CLONE_NEWUSER must persist" + ); } #[test] @@ -1054,8 +1095,8 @@ mod tests { ..SandboxOptions::default() }; let plan = build_plan(&opts, std::path::Path::new("/tmp")); - let socket_nr = seccomp::syscalls::syscall_number("socket") - .expect("socket in per-arch syscall map"); + let socket_nr = + seccomp::syscalls::syscall_number("socket").expect("socket in per-arch syscall map"); // BPF compile emits one JEQ per allowed syscall (+ a fixed arch // prelude + a default-deny tail), so encoding socket as a JEQ // instruction's k-field is the load-bearing signal. @@ -1080,8 +1121,8 @@ mod tests { ..SandboxOptions::default() }; let plan = build_plan(&opts, std::path::Path::new("/tmp")); - let setuid_nr = seccomp::syscalls::syscall_number("setuid") - .expect("setuid in per-arch syscall map"); + let setuid_nr = + seccomp::syscalls::syscall_number("setuid").expect("setuid in per-arch syscall map"); let program = plan.seccomp_program.as_slice(); let landed = program.iter().any(|insn| insn.k == setuid_nr); assert!( @@ -1104,8 +1145,8 @@ mod tests { ..SandboxOptions::default() }; let plan = build_plan(&opts, std::path::Path::new("/tmp")); - let socket_nr = seccomp::syscalls::syscall_number("socket") - .expect("socket in per-arch syscall map"); + let socket_nr = + seccomp::syscalls::syscall_number("socket").expect("socket in per-arch syscall map"); let landed = plan.seccomp_program.iter().any(|insn| insn.k == socket_nr); assert!( !landed, @@ -1148,5 +1189,4 @@ mod tests { outcome.no_new_privs, ); } - } diff --git a/src/dynamic/sandbox/process_macos.rs b/src/dynamic/sandbox/process_macos.rs index 704e0f3a..8be80d3b 100644 --- a/src/dynamic/sandbox/process_macos.rs +++ b/src/dynamic/sandbox/process_macos.rs @@ -124,7 +124,10 @@ const PROFILE_SOURCES: &[(&str, &str)] = &[ include_str!("../sandbox_profiles/path_traversal.sb"), ), ("ssrf", include_str!("../sandbox_profiles/ssrf.sb")), - ("deserialize", include_str!("../sandbox_profiles/deserialize.sb")), + ( + "deserialize", + include_str!("../sandbox_profiles/deserialize.sb"), + ), ("xxe", include_str!("../sandbox_profiles/xxe.sb")), ( "open_redirect", @@ -305,9 +308,7 @@ pub fn splice_deny_default(source: &str, seed: &str) -> String { rewritten.push('\n'); } rewritten.push('\n'); - rewritten.push_str( - ";; ── deny-default seed (spliced by NYX_SB_DENY_DEFAULT=1) ──────────\n", - ); + rewritten.push_str(";; ── deny-default seed (spliced by NYX_SB_DENY_DEFAULT=1) ──────────\n"); rewritten.push_str(seed.trim_end()); rewritten.push('\n'); rewritten @@ -378,7 +379,9 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> WrapResult { }, }; } - let profile = input.profile_override.unwrap_or_else(|| profile_for_caps(input.caps)); + let profile = input + .profile_override + .unwrap_or_else(|| profile_for_caps(input.caps)); // Profile keys must be `&'static str` (from `PROFILE_SOURCES`); reject // unknown overrides up-front so we don't accidentally wrap with a // profile we have no source for. @@ -411,7 +414,8 @@ pub fn wrap_plan(input: &WrapInput<'_>) -> WrapResult { } }; - let workdir_abs = std::fs::canonicalize(input.workdir).unwrap_or_else(|_| input.workdir.to_path_buf()); + let workdir_abs = + std::fs::canonicalize(input.workdir).unwrap_or_else(|_| input.workdir.to_path_buf()); let mut args: Vec = Vec::with_capacity(6 + input.cmd_args.len()); args.push("-f".to_owned()); @@ -573,7 +577,10 @@ mod tests { // resetting the env var below restores the default for subsequent // tests in the same process. unsafe { std::env::set_var(SANDBOX_EXEC_BIN_ENV, "/nonexistent/sandbox-exec") }; - assert_eq!(sandbox_exec_bin(), PathBuf::from("/nonexistent/sandbox-exec")); + assert_eq!( + sandbox_exec_bin(), + PathBuf::from("/nonexistent/sandbox-exec") + ); assert!(!sandbox_exec_available()); unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) }; } diff --git a/src/dynamic/sandbox/seccomp/bpf.rs b/src/dynamic/sandbox/seccomp/bpf.rs index 039b5f3d..f7ded070 100644 --- a/src/dynamic/sandbox/seccomp/bpf.rs +++ b/src/dynamic/sandbox/seccomp/bpf.rs @@ -71,7 +71,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec { // (1) jeq audit_arch ? next : KILL // KILL is at the very end; computed below after we know the size. let arch_check_idx = program.len(); - program.push(SockFilter { code: BPF_JMP | BPF_JEQ | BPF_K, jt: 0, jf: 0, k: audit_arch }); + program.push(SockFilter { + code: BPF_JMP | BPF_JEQ | BPF_K, + jt: 0, + jf: 0, + k: audit_arch, + }); // (2) ld [nr] program.push(SockFilter { @@ -90,7 +95,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec { // plus the KILL ret) to land on the ALLOW ret. Computed below. let first_check_idx = program.len(); for &nr in allowed_nrs { - program.push(SockFilter { code: BPF_JMP | BPF_JEQ | BPF_K, jt: 0, jf: 0, k: nr }); + program.push(SockFilter { + code: BPF_JMP | BPF_JEQ | BPF_K, + jt: 0, + jf: 0, + k: nr, + }); } // (KILL) ret KILL_PROCESS @@ -103,7 +113,12 @@ pub fn compile(allowed_nrs: &[u32], audit_arch: u32) -> Vec { }); // (ALLOW) ret ALLOW let allow_idx = program.len(); - program.push(SockFilter { code: BPF_RET | BPF_K, jt: 0, jf: 0, k: SECCOMP_RET_ALLOW }); + program.push(SockFilter { + code: BPF_RET | BPF_K, + jt: 0, + jf: 0, + k: SECCOMP_RET_ALLOW, + }); // Patch arch check: jt=0 (next on match), jf=N (KILL on mismatch). let arch_jf = (kill_idx - arch_check_idx - 1) as u8; diff --git a/src/dynamic/sandbox/seccomp/mod.rs b/src/dynamic/sandbox/seccomp/mod.rs index c4cbd248..d5687e05 100644 --- a/src/dynamic/sandbox/seccomp/mod.rs +++ b/src/dynamic/sandbox/seccomp/mod.rs @@ -34,7 +34,7 @@ pub mod syscalls; use std::collections::BTreeSet; use crate::dynamic::sandbox::seccomp::bpf::{SockFilter, SockFprog}; -use crate::dynamic::sandbox::seccomp::syscalls::{syscall_number, AUDIT_ARCH}; +use crate::dynamic::sandbox::seccomp::syscalls::{AUDIT_ARCH, syscall_number}; include!(concat!(env!("OUT_DIR"), "/seccomp_policy.rs")); @@ -174,15 +174,15 @@ mod tests { #[test] fn base_table_is_non_empty() { - assert!(!BASE.is_empty(), "seccomp BASE allowlist must include stdio + startup syscalls"); + assert!( + !BASE.is_empty(), + "seccomp BASE allowlist must include stdio + startup syscalls" + ); } #[test] fn cap_table_includes_known_caps() { - let known: Vec<&str> = CAP - .iter() - .map(|(_, _)| "_") - .collect(); + let known: Vec<&str> = CAP.iter().map(|(_, _)| "_").collect(); // We declared SQL_QUERY, FILE_IO, SSRF, CODE_EXEC, HTML_ESCAPE, // DESERIALIZE, HEADER_INJECTION, OPEN_REDIRECT in the toml; the // build script emits one entry per `[cap.X]` table. The exact diff --git a/src/dynamic/spec.rs b/src/dynamic/spec.rs index 7582ba8b..4140759a 100644 --- a/src/dynamic/spec.rs +++ b/src/dynamic/spec.rs @@ -298,7 +298,10 @@ impl HarnessSpec { } } - let evidence = diag.evidence.as_ref().ok_or(UnsupportedReason::NoFlowSteps)?; + let evidence = diag + .evidence + .as_ref() + .ok_or(UnsupportedReason::NoFlowSteps)?; // Phase 04 pre-step: when both callgraph *and* summaries are // present, walk reverse edges to a framework-bound ancestor. @@ -313,9 +316,10 @@ impl HarnessSpec { // strategies (FromFlowSteps / FromRuleNamespace / FromFuncSummaryAuto) // whenever the rule id happens to contain `.http.` / `.cli.`. if let (Some(s), Some(cg)) = (summaries, callgraph) - && let Some(spec) = derive_from_callgraph_walk_only(diag, evidence, s, cg) { - return Ok(spec); - } + && let Some(spec) = derive_from_callgraph_walk_only(diag, evidence, s, cg) + { + return Ok(spec); + } // Try each strategy in priority order; first non-None wins. if let Some(spec) = derive_from_flow_steps(diag, evidence, summaries) { @@ -327,8 +331,7 @@ impl HarnessSpec { if let Some(spec) = derive_from_func_summary_auto(diag, evidence, summaries) { return Ok(spec); } - if let Some(spec) = derive_from_callgraph_entry_full(diag, evidence, summaries, callgraph) - { + if let Some(spec) = derive_from_callgraph_entry_full(diag, evidence, summaries, callgraph) { return Ok(spec); } @@ -520,9 +523,10 @@ pub fn derive_from_rule_namespace_with( // language prefix when both are available. Disagreement is a stronger // signal of a mis-rooted finding than a missing extension. if let Some(path_lang) = lang_from_path(&diag.path) - && path_lang != lang { - return None; - } + && path_lang != lang + { + return None; + } let entry_function = resolve_enclosing_function(diag, evidence, summaries, lang) .unwrap_or_else(|| "".to_owned()); @@ -749,33 +753,34 @@ pub fn derive_from_callgraph_entry_full( // Step 0: callgraph-aware reverse-edge walk to the nearest entry-point // ancestor. Only fires when both summaries *and* callgraph are present. if let (Some(s), Some(cg)) = (summaries, callgraph) - && let Some(found) = find_entry_via_callgraph(diag, evidence, s, cg, lang) { - let entry_kind = found - .summary - .entry_kind - .as_ref() - .map(entry_kind_from_summary) - .unwrap_or_else(|| name_to_entry_kind(&found.summary.name)); - let entry_file = if !found.summary.file_path.is_empty() { - found.summary.file_path.clone() - } else { - diag.path.clone() - }; - let mut spec = finalize_spec( - diag, - entry_file, - found.summary.name.clone(), - lang, - expected_cap, - diag.path.clone(), - diag.line as u32, - SpecDerivationStrategy::FromCallgraphEntry, - Some(s), - ); - spec.entry_kind = entry_kind; - spec.spec_hash = compute_spec_hash(&spec); - return Some(spec); - } + && let Some(found) = find_entry_via_callgraph(diag, evidence, s, cg, lang) + { + let entry_kind = found + .summary + .entry_kind + .as_ref() + .map(entry_kind_from_summary) + .unwrap_or_else(|| name_to_entry_kind(&found.summary.name)); + let entry_file = if !found.summary.file_path.is_empty() { + found.summary.file_path.clone() + } else { + diag.path.clone() + }; + let mut spec = finalize_spec( + diag, + entry_file, + found.summary.name.clone(), + lang, + expected_cap, + diag.path.clone(), + diag.line as u32, + SpecDerivationStrategy::FromCallgraphEntry, + Some(s), + ); + spec.entry_kind = entry_kind; + spec.spec_hash = compute_spec_hash(&spec); + return Some(spec); + } // Step 1: try summary-based classification of the enclosing function. let summary_kind = enclosing_function_from_flow_steps(evidence) @@ -934,12 +939,13 @@ fn find_entry_via_callgraph<'a>( } let caller_key = &callgraph.graph[caller_node]; if let Some(caller_summary) = summaries.get(caller_key) - && is_entry_point(caller_summary, callgraph) { - return Some(EntryHit { - key: caller_key.clone(), - summary: caller_summary, - }); - } + && is_entry_point(caller_summary, callgraph) + { + return Some(EntryHit { + key: caller_key.clone(), + summary: caller_summary, + }); + } queue.push_back(caller_node); } } @@ -970,9 +976,10 @@ fn entry_kind_from_summary(_kind: &crate::entry_points::EntryKind) -> EntryKind fn lang_from_path(path: &str) -> Option { let p = Path::new(path); if let Some(ext) = p.extension().and_then(|e| e.to_str()) - && let Some(lang) = Lang::from_extension(ext) { - return Some(lang); - } + && let Some(lang) = Lang::from_extension(ext) + { + return Some(lang); + } // Fall back to a shebang / content sniff over the file head. let head = read_file_head(p, 200); if head.is_empty() { @@ -1305,12 +1312,13 @@ pub fn outermost_entry(steps: &[crate::evidence::FlowStep]) -> Option for step in steps { if matches!(step.kind, FlowStepKind::Source) && let Some(ref func) = step.function - && !func.is_empty() { - return Some(EntryRef { - file: step.file.clone(), - function: func.clone(), - }); - } + && !func.is_empty() + { + return Some(EntryRef { + file: step.file.clone(), + function: func.clone(), + }); + } } None } @@ -1401,7 +1409,10 @@ fn compute_spec_hash(spec: &HarnessSpec) -> String { let out = h.finalize(); let bytes = out.as_bytes(); - format!("{:016x}", u64::from_le_bytes(bytes[..8].try_into().unwrap())) + format!( + "{:016x}", + u64::from_le_bytes(bytes[..8].try_into().unwrap()) + ) } #[cfg(test)] @@ -1441,7 +1452,10 @@ mod tests { #[test] fn outermost_entry_picks_source_step() { - let steps = vec![source_step("src/main.rs", "handle_request"), sink_step("src/main.rs")]; + let steps = vec![ + source_step("src/main.rs", "handle_request"), + sink_step("src/main.rs"), + ]; let entry = outermost_entry(&steps).unwrap(); assert_eq!(entry.file, "src/main.rs"); assert_eq!(entry.function, "handle_request"); @@ -1580,7 +1594,10 @@ mod tests { let mut s2 = s1.clone(); s2.entry_file = "src/other.rs".into(); s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "entry_file mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "entry_file mutation must change spec_hash" + ); } #[test] @@ -1589,7 +1606,10 @@ mod tests { let mut s2 = s1.clone(); s2.entry_name = "other_handler".into(); s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "entry_name mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "entry_name mutation must change spec_hash" + ); } #[test] @@ -1598,17 +1618,26 @@ mod tests { let mut s2 = s1.clone(); s2.payload_slot = PayloadSlot::Param(1); s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "payload_slot mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "payload_slot mutation must change spec_hash" + ); let mut s3 = s1.clone(); s3.payload_slot = PayloadSlot::HttpBody; s3.spec_hash = compute_spec_hash(&s3); - assert_ne!(s1.spec_hash, s3.spec_hash, "payload_slot tag change must change spec_hash"); + assert_ne!( + s1.spec_hash, s3.spec_hash, + "payload_slot tag change must change spec_hash" + ); let mut s4 = s1.clone(); s4.payload_slot = PayloadSlot::EnvVar("NYX_INPUT".into()); s4.spec_hash = compute_spec_hash(&s4); - assert_ne!(s1.spec_hash, s4.spec_hash, "EnvVar payload_slot must change spec_hash"); + assert_ne!( + s1.spec_hash, s4.spec_hash, + "EnvVar payload_slot must change spec_hash" + ); } #[test] @@ -1618,7 +1647,10 @@ mod tests { let mut s2 = s1.clone(); s2.expected_cap = Cap::CODE_EXEC; s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "expected_cap mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "expected_cap mutation must change spec_hash" + ); } #[test] @@ -1627,7 +1659,10 @@ mod tests { let mut s2 = s1.clone(); s2.constraint_hints = vec!["prefix:admin/".into()]; s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "constraint_hints mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "constraint_hints mutation must change spec_hash" + ); } #[test] @@ -1636,7 +1671,10 @@ mod tests { let mut s2 = s1.clone(); s2.toolchain_id = "rust-nightly".into(); s2.spec_hash = compute_spec_hash(&s2); - assert_ne!(s1.spec_hash, s2.spec_hash, "toolchain_id mutation must change spec_hash"); + assert_ne!( + s1.spec_hash, s2.spec_hash, + "toolchain_id mutation must change spec_hash" + ); } // ── Phase 01: derivation strategies ────────────────────────────────────── @@ -1691,7 +1729,11 @@ mod tests { #[test] fn rule_namespace_strategy_fires_without_flow_steps() { use crate::labels::Cap; - let diag = diag_with_rule_id("py.cmdi.os_system", "app/handler.py", Cap::SHELL_ESCAPE.bits()); + let diag = diag_with_rule_id( + "py.cmdi.os_system", + "app/handler.py", + Cap::SHELL_ESCAPE.bits(), + ); let spec = HarnessSpec::from_finding(&diag).unwrap(); assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace); assert_eq!(spec.lang, Lang::Python); @@ -1713,11 +1755,7 @@ mod tests { fn rule_namespace_strategy_pins_rs_auth_mapping() { // Regression: `rs.auth.*` must map to `Lang::Rust` + `Cap::UNAUTHORIZED_ID`. // The plan calls out this exemplar but had no test coverage. - let diag = diag_with_rule_id( - "rs.auth.missing_ownership_check.taint", - "src/handler.rs", - 0, - ); + let diag = diag_with_rule_id("rs.auth.missing_ownership_check.taint", "src/handler.rs", 0); let spec = HarnessSpec::from_finding(&diag).unwrap(); assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace); assert_eq!(spec.lang, Lang::Rust); @@ -1729,7 +1767,11 @@ mod tests { fn rule_namespace_strategy_rejects_path_lang_mismatch() { use crate::labels::Cap; // `py.*` rule id, but a `.java` file — the cross-check refuses. - let diag = diag_with_rule_id("py.cmdi.os_system", "src/Main.java", Cap::SHELL_ESCAPE.bits()); + let diag = diag_with_rule_id( + "py.cmdi.os_system", + "src/Main.java", + Cap::SHELL_ESCAPE.bits(), + ); assert_eq!( HarnessSpec::from_finding(&diag).unwrap_err(), UnsupportedReason::SpecDerivationFailed @@ -1752,8 +1794,11 @@ mod tests { // Unregistered `taint-*` rule slugs (e.g. the legacy generic // `taint-unsanitised-flow`) are not in `CAP_RULE_REGISTRY`; the // shortcut must skip them so downstream strategies can try. - let diag = - diag_with_rule_id("taint-unsanitised-flow", "app/handler.py", Cap::SHELL_ESCAPE.bits()); + let diag = diag_with_rule_id( + "taint-unsanitised-flow", + "app/handler.py", + Cap::SHELL_ESCAPE.bits(), + ); // No flow_steps, no http/cli marker → ends in SpecDerivationFailed. assert_eq!( HarnessSpec::from_finding(&diag).unwrap_err(), @@ -1793,8 +1838,11 @@ mod tests { fn rule_namespace_strategy_taint_id_lang_follows_path_extension() { use crate::labels::Cap; // Same rule slug, different file extension → derives a Go spec. - let diag = - diag_with_rule_id("taint-data-exfiltration", "cmd/leak.go", Cap::DATA_EXFIL.bits()); + let diag = diag_with_rule_id( + "taint-data-exfiltration", + "cmd/leak.go", + Cap::DATA_EXFIL.bits(), + ); let spec = HarnessSpec::from_finding(&diag).unwrap(); assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace); assert_eq!(spec.lang, Lang::Go); @@ -1881,7 +1929,11 @@ mod tests { #[test] fn callgraph_entry_strategy_fires_on_cli_rule_id() { use crate::labels::Cap; - let diag = diag_with_rule_id("rs.cli.parse_subcommand", "src/main.rs", Cap::SHELL_ESCAPE.bits()); + let diag = diag_with_rule_id( + "rs.cli.parse_subcommand", + "src/main.rs", + Cap::SHELL_ESCAPE.bits(), + ); let spec = HarnessSpec::from_finding(&diag).unwrap(); assert_eq!(spec.derivation, SpecDerivationStrategy::FromCallgraphEntry); assert!(matches!(spec.entry_kind, EntryKind::CliSubcommand)); @@ -1928,7 +1980,14 @@ mod tests { } } - fn build_summary(name: &str, file: &str, lang: &str, sink_caps: u32, tainted_params: Vec, entry_kind: Option) -> FuncSummary { + fn build_summary( + name: &str, + file: &str, + lang: &str, + sink_caps: u32, + tainted_params: Vec, + entry_kind: Option, + ) -> FuncSummary { FuncSummary { name: name.into(), file_path: file.into(), @@ -1962,10 +2021,7 @@ mod tests { // enclosing function name. use crate::labels::Cap; let ev = Evidence { - flow_steps: vec![sink_only_step_with_function( - "app/handler.py", - "do_request", - )], + flow_steps: vec![sink_only_step_with_function("app/handler.py", "do_request")], sink_caps: Cap::SHELL_ESCAPE.bits(), ..Default::default() }; @@ -2004,10 +2060,7 @@ mod tests { gs.insert(key, summary); let ev = Evidence { - flow_steps: vec![sink_only_step_with_function( - "app/handler.py", - "do_request", - )], + flow_steps: vec![sink_only_step_with_function("app/handler.py", "do_request")], sink_caps: Cap::SHELL_ESCAPE.bits(), ..Default::default() }; @@ -2041,7 +2094,9 @@ mod tests { "python", Cap::SSRF.bits(), vec![], - Some(StaticEntryKind::FlaskRoute { method: HttpMethod::GET }), + Some(StaticEntryKind::FlaskRoute { + method: HttpMethod::GET, + }), ); let key = FuncKey::new_function(Lang::Python, "app/views.py", "index", Some(1)); gs.insert(key, summary); @@ -2302,7 +2357,10 @@ mod tests { }; stamp_framework_binding(&mut spec, binding); - assert_eq!(spec.entry_kind.tag(), crate::evidence::EntryKindTag::Function); + assert_eq!( + spec.entry_kind.tag(), + crate::evidence::EntryKindTag::Function + ); assert_eq!(spec.spec_hash, pre_hash); assert!(spec.framework.is_some()); } diff --git a/src/dynamic/stubs/filesystem.rs b/src/dynamic/stubs/filesystem.rs index 0211019a..59bcb20c 100644 --- a/src/dynamic/stubs/filesystem.rs +++ b/src/dynamic/stubs/filesystem.rs @@ -53,8 +53,7 @@ impl FilesystemStub { /// in restricted environments (e.g. CI sandboxes that share a /// read-only workdir). pub fn start(workdir: &Path) -> std::io::Result { - let tempdir = TempDir::new_in(workdir) - .or_else(|_| TempDir::new())?; + let tempdir = TempDir::new_in(workdir).or_else(|_| TempDir::new())?; let root = tempdir.path().to_owned(); Ok(Self { tempdir: Some(tempdir), @@ -88,7 +87,8 @@ impl FilesystemStub { // Canonicalise both sides where possible so symlinks / // relative path segments do not fool the prefix check. let resolved_root = std::fs::canonicalize(&self.root).unwrap_or_else(|_| self.root.clone()); - let resolved_cand = std::fs::canonicalize(candidate).unwrap_or_else(|_| candidate.to_owned()); + let resolved_cand = + std::fs::canonicalize(candidate).unwrap_or_else(|_| candidate.to_owned()); resolved_cand.starts_with(&resolved_root) } } @@ -145,10 +145,7 @@ mod tests { assert_eq!(events.len(), 1); assert_eq!(events[0].kind, StubKind::Filesystem); assert!(events[0].summary.contains("/etc/passwd")); - assert_eq!( - events[0].detail.get("op").map(String::as_str), - Some("read") - ); + assert_eq!(events[0].detail.get("op").map(String::as_str), Some("read")); } #[test] diff --git a/src/dynamic/stubs/http.rs b/src/dynamic/stubs/http.rs index eea1d556..7dfe5033 100644 --- a/src/dynamic/stubs/http.rs +++ b/src/dynamic/stubs/http.rs @@ -31,7 +31,7 @@ //! recording log lives under the workdir-rooted tempdir which is //! cleaned up by the verifier's tempdir handle. -use super::{monotonic_ns, StubEvent, StubKind, StubProvider}; +use super::{StubEvent, StubKind, StubProvider, monotonic_ns}; use std::collections::BTreeMap; use std::io::{BufRead, BufReader, Read, Write}; use std::net::{TcpListener, TcpStream}; @@ -182,7 +182,10 @@ impl StubProvider for HttpStub { } fn recording_endpoint(&self) -> Option<(&'static str, String)> { - Some((HTTP_STUB_LOG_ENV_VAR, self.log_path.to_string_lossy().into_owned())) + Some(( + HTTP_STUB_LOG_ENV_VAR, + self.log_path.to_string_lossy().into_owned(), + )) } fn drain_events(&self) -> Vec { @@ -227,9 +230,10 @@ fn accept_loop( let _ = stream.set_write_timeout(Some(Duration::from_secs(2))); if let Some(ev) = handle_connection(stream, MAX_REQUEST_BYTES) - && let Ok(mut g) = events.lock() { - g.push(ev); - } + && let Ok(mut g) = events.lock() + { + g.push(ev); + } } } @@ -257,21 +261,19 @@ fn handle_connection(mut stream: TcpStream, max_bytes: usize) -> Option() { - content_length = n.min(max_bytes); - } + if let Some(rest) = trimmed.to_ascii_lowercase().strip_prefix("content-length:") + && let Ok(n) = rest.trim().parse::() + { + content_length = n.min(max_bytes); + } headers.push(trimmed.to_owned()); } // Body, capped at content_length (already clamped to max_bytes). let mut body = vec![0u8; content_length]; - if content_length > 0 - && reader.read_exact(&mut body).is_err() { - body.clear(); - } + if content_length > 0 && reader.read_exact(&mut body).is_err() { + body.clear(); + } // Always reply 200 OK with no body. let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); @@ -419,8 +421,10 @@ mod tests { .append(true) .open(stub.log_path()) .unwrap(); - f.write_all(b"# method: POST\n# url: http://example.com/login\nPOST http://example.com/login\n") - .unwrap(); + f.write_all( + b"# method: POST\n# url: http://example.com/login\nPOST http://example.com/login\n", + ) + .unwrap(); drop(f); let events = stub.drain_events(); diff --git a/src/dynamic/stubs/ldap_server.rs b/src/dynamic/stubs/ldap_server.rs index 3c70103a..223bb09c 100644 --- a/src/dynamic/stubs/ldap_server.rs +++ b/src/dynamic/stubs/ldap_server.rs @@ -41,7 +41,7 @@ //! Signals the accept thread to shut down and connects to itself to //! wake the blocking `accept()`. -use super::{monotonic_ns, StubEvent, StubKind, StubProvider}; +use super::{StubEvent, StubKind, StubProvider, monotonic_ns}; use std::collections::BTreeMap; use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; @@ -105,10 +105,7 @@ impl LdapStub { detail: { let mut d = BTreeMap::new(); d.insert("filter".to_owned(), filter.to_owned()); - d.insert( - "entries_returned".to_owned(), - entries_returned.to_string(), - ); + d.insert("entries_returned".to_owned(), entries_returned.to_string()); d }, }; @@ -170,11 +167,7 @@ fn accept_loop( } } -fn handle_connection( - mut stream: TcpStream, - max_bytes: usize, - events: &Arc>>, -) { +fn handle_connection(mut stream: TcpStream, max_bytes: usize, events: &Arc>>) { let mut reader = match stream.try_clone() { Ok(s) => BufReader::new(s), Err(_) => return, @@ -240,7 +233,10 @@ fn match_filter(filter: &str) -> Vec<&'static str> { #[derive(Debug)] enum Filter<'a> { - Eq { attr: &'a str, pattern: &'a str }, + Eq { + attr: &'a str, + pattern: &'a str, + }, And(Vec>), Or(Vec>), /// Anything we did not recognise — treated as match-everything by diff --git a/src/dynamic/stubs/mod.rs b/src/dynamic/stubs/mod.rs index 74d5d71c..6267f603 100644 --- a/src/dynamic/stubs/mod.rs +++ b/src/dynamic/stubs/mod.rs @@ -64,15 +64,15 @@ pub mod redis; pub mod sql; pub mod xpath_document; -pub use broker_kafka::{kafka_source, KAFKA_PUBLISH_MARKER}; -pub use broker_nats::{nats_source, NATS_PUBLISH_MARKER}; -pub use broker_pubsub::{pubsub_source, PUBSUB_PUBLISH_MARKER}; -pub use broker_rabbit::{rabbit_source, RABBIT_PUBLISH_MARKER}; -pub use broker_sqs::{sqs_source, SQS_PUBLISH_MARKER}; +pub use broker_kafka::{KAFKA_PUBLISH_MARKER, kafka_source}; +pub use broker_nats::{NATS_PUBLISH_MARKER, nats_source}; +pub use broker_pubsub::{PUBSUB_PUBLISH_MARKER, pubsub_source}; +pub use broker_rabbit::{RABBIT_PUBLISH_MARKER, rabbit_source}; +pub use broker_sqs::{SQS_PUBLISH_MARKER, sqs_source}; pub use filesystem::FilesystemStub; pub use http::HttpStub; pub use ldap_server::LdapStub; -pub use mocks::{mock_source, MockKind}; +pub use mocks::{MockKind, mock_source}; pub use redis::RedisStub; pub use sql::SqlStub; @@ -330,8 +330,8 @@ impl StubHarness { /// so a per-stub event log keeps insertion order even when multiple /// stubs interleave writes. pub(crate) fn monotonic_ns() -> u64 { - use std::time::Instant; use std::sync::OnceLock; + use std::time::Instant; static ORIGIN: OnceLock = OnceLock::new(); let origin = *ORIGIN.get_or_init(Instant::now); origin.elapsed().as_nanos() as u64 @@ -407,11 +407,8 @@ mod tests { #[test] fn dedup_repeated_kinds_during_start() { let dir = TempDir::new().unwrap(); - let h = StubHarness::start( - &[StubKind::Sql, StubKind::Sql, StubKind::Sql], - dir.path(), - ) - .unwrap(); + let h = + StubHarness::start(&[StubKind::Sql, StubKind::Sql, StubKind::Sql], dir.path()).unwrap(); assert_eq!(h.len(), 1, "repeated kinds must be deduped"); } diff --git a/src/dynamic/stubs/redis.rs b/src/dynamic/stubs/redis.rs index d2c0dd8c..498c9c86 100644 --- a/src/dynamic/stubs/redis.rs +++ b/src/dynamic/stubs/redis.rs @@ -46,7 +46,11 @@ impl RedisStub { let shutdown_clone = Arc::clone(&shutdown); std::thread::spawn(move || accept_loop(listener, events_clone, shutdown_clone)); - Ok(Self { port, events, shutdown }) + Ok(Self { + port, + events, + shutdown, + }) } /// Port the listener is bound to. @@ -181,7 +185,10 @@ fn read_command(reader: &mut BufReader) -> Option> { } fn command_to_event(parts: &[String]) -> StubEvent { - let (cmd, args) = parts.split_first().map(|(c, a)| (c.as_str(), a)).unwrap_or(("", &[][..])); + let (cmd, args) = parts + .split_first() + .map(|(c, a)| (c.as_str(), a)) + .unwrap_or(("", &[][..])); let summary = if args.is_empty() { cmd.to_owned() } else { @@ -250,7 +257,8 @@ mod tests { let stub = RedisStub::start().unwrap(); let mut s = TcpStream::connect(format!("127.0.0.1:{}", stub.port())).unwrap(); // `GET sessions` - s.write_all(b"*2\r\n$3\r\nGET\r\n$8\r\nsessions\r\n").unwrap(); + s.write_all(b"*2\r\n$3\r\nGET\r\n$8\r\nsessions\r\n") + .unwrap(); s.flush().unwrap(); let mut reply = [0u8; 5]; let _ = s.read_exact(&mut reply); diff --git a/src/dynamic/stubs/sql.rs b/src/dynamic/stubs/sql.rs index 877df929..ff574cb7 100644 --- a/src/dynamic/stubs/sql.rs +++ b/src/dynamic/stubs/sql.rs @@ -30,7 +30,7 @@ //! On drop the DB file and the log file are deleted along with the //! enclosing tempdir handle. -use super::{monotonic_ns, StubEvent, StubKind, StubProvider}; +use super::{StubEvent, StubKind, StubProvider, monotonic_ns}; use std::fs::OpenOptions; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; @@ -60,8 +60,7 @@ impl SqlStub { /// files. When `workdir` is not writable, falls back to the /// process-wide temp directory. pub fn start(workdir: &Path) -> std::io::Result { - let tempdir = TempDir::new_in(workdir) - .or_else(|_| TempDir::new())?; + let tempdir = TempDir::new_in(workdir).or_else(|_| TempDir::new())?; let db_path = tempdir.path().join("nyx_sql_stub.db"); let log_path = tempdir.path().join("nyx_sql_stub.queries.log"); @@ -126,7 +125,10 @@ impl StubProvider for SqlStub { } fn recording_endpoint(&self) -> Option<(&'static str, String)> { - Some((SQL_STUB_LOG_ENV_VAR, self.log_path.to_string_lossy().into_owned())) + Some(( + SQL_STUB_LOG_ENV_VAR, + self.log_path.to_string_lossy().into_owned(), + )) } fn drain_events(&self) -> Vec { @@ -214,7 +216,8 @@ mod tests { fn record_query_lands_in_drain_events() { let dir = TempDir::new().unwrap(); let stub = SqlStub::start(dir.path()).unwrap(); - stub.record_query("SELECT * FROM users WHERE id = 1").unwrap(); + stub.record_query("SELECT * FROM users WHERE id = 1") + .unwrap(); let events = stub.drain_events(); assert_eq!(events.len(), 1); assert_eq!(events[0].kind, StubKind::Sql); @@ -230,7 +233,8 @@ mod tests { .append(true) .open(stub.log_path()) .unwrap(); - f.write_all(b"# driver: psycopg2\nSELECT * FROM accounts\n").unwrap(); + f.write_all(b"# driver: psycopg2\nSELECT * FROM accounts\n") + .unwrap(); drop(f); let events = stub.drain_events(); diff --git a/src/dynamic/stubs/xpath_document.rs b/src/dynamic/stubs/xpath_document.rs index 9669de00..04a0926d 100644 --- a/src/dynamic/stubs/xpath_document.rs +++ b/src/dynamic/stubs/xpath_document.rs @@ -47,7 +47,10 @@ pub const XPATH_CORPUS_NODE_COUNT: u32 = 3; /// `(filename, bytes)` pair the harness emitter folds into its /// [`crate::dynamic::lang::HarnessSource::extra_files`]. pub fn extra_file_pair() -> (String, String) { - (XPATH_CORPUS_FILENAME.to_owned(), XPATH_CORPUS_XML.to_owned()) + ( + XPATH_CORPUS_FILENAME.to_owned(), + XPATH_CORPUS_XML.to_owned(), + ) } #[cfg(test)] diff --git a/src/dynamic/telemetry.rs b/src/dynamic/telemetry.rs index b82e8f27..5199f1b1 100644 --- a/src/dynamic/telemetry.rs +++ b/src/dynamic/telemetry.rs @@ -768,7 +768,10 @@ mod tests { ); emit(&event); - assert!(!log.exists(), "log must not be created when NYX_NO_TELEMETRY=1"); + assert!( + !log.exists(), + "log must not be created when NYX_NO_TELEMETRY=1" + ); unsafe { std::env::remove_var("NYX_NO_TELEMETRY"); @@ -795,7 +798,9 @@ mod tests { .unwrap(); let err = read_events(&log).expect_err("schema 0 must be rejected"); match err { - TelemetryReadError::SchemaMismatch { expected, found, .. } => { + TelemetryReadError::SchemaMismatch { + expected, found, .. + } => { assert_eq!(expected, SCHEMA_VERSION); assert_eq!(found, 0); } diff --git a/src/dynamic/toolchain.rs b/src/dynamic/toolchain.rs index 40024506..0dc307aa 100644 --- a/src/dynamic/toolchain.rs +++ b/src/dynamic/toolchain.rs @@ -115,10 +115,12 @@ fn try_rust_toolchain_toml(root: &Path) -> Option { if line.starts_with('[') { in_toolchain = false; } - if in_toolchain && line.starts_with("channel") - && let Some(ver) = extract_version_from_toml_value(line) { - return Some(map_rust_version(&ver, RustPinOrigin::RustToolchainToml)); - } + if in_toolchain + && line.starts_with("channel") + && let Some(ver) = extract_version_from_toml_value(line) + { + return Some(map_rust_version(&ver, RustPinOrigin::RustToolchainToml)); + } } None } @@ -138,9 +140,10 @@ fn try_cargo_toml_rust_version(root: &Path) -> Option { for line in content.lines() { let line = line.trim(); if line.starts_with("rust-version") - && let Some(ver) = extract_version_from_toml_value(line) { - return Some(map_rust_version(&ver, RustPinOrigin::CargoToml)); - } + && let Some(ver) = extract_version_from_toml_value(line) + { + return Some(map_rust_version(&ver, RustPinOrigin::CargoToml)); + } } None } @@ -181,7 +184,7 @@ fn map_rust_version(version: &str, origin: RustPinOrigin) -> ToolchainResolution return ToolchainResolution { toolchain_id: "rust-nightly".to_owned(), pin_origin, - toolchain_drift: true, // nightly != stable reference image + toolchain_drift: true, // nightly != stable reference image version_string: version.to_owned(), }; } @@ -246,10 +249,14 @@ fn try_pyproject_toml(root: &Path) -> Option { // Look for `requires-python = ">=3.11"` or `python = "3.11"`. for line in content.lines() { let line = line.trim(); - if (line.starts_with("requires-python") || (line.starts_with("python") && line.contains('=') && !line.starts_with("python_requires"))) - && let Some(ver) = extract_version_from_toml_value(line) { - return Some(map_version(&ver, PinOrigin::PyprojectToml)); - } + if (line.starts_with("requires-python") + || (line.starts_with("python") + && line.contains('=') + && !line.starts_with("python_requires"))) + && let Some(ver) = extract_version_from_toml_value(line) + { + return Some(map_version(&ver, PinOrigin::PyprojectToml)); + } } None } @@ -266,10 +273,12 @@ fn try_pipfile(root: &Path) -> Option { if line.starts_with('[') { in_requires = false; } - if in_requires && line.starts_with("python_version") - && let Some(ver) = extract_version_from_toml_value(line) { - return Some(map_version(&ver, PinOrigin::Pipfile)); - } + if in_requires + && line.starts_with("python_version") + && let Some(ver) = extract_version_from_toml_value(line) + { + return Some(map_version(&ver, PinOrigin::Pipfile)); + } } None } @@ -331,9 +340,7 @@ fn map_version(version: &str, origin: PinOrigin) -> ToolchainResolution { ("3", Some("12")) => ("python-3.12".to_owned(), false), ("3", Some("13")) => ("python-3.13".to_owned(), false), // Older 3.x → nearest supported is 3.8 - ("3", Some(m)) if m.parse::().is_ok_and(|v| v < 8) => { - ("python-3.8".to_owned(), true) - } + ("3", Some(m)) if m.parse::().is_ok_and(|v| v < 8) => ("python-3.8".to_owned(), true), // Newer 3.x beyond catalog → use 3.13 as closest ("3", Some(_)) => ("python-3.13".to_owned(), true), ("3", None) => ("python-3".to_owned(), false), @@ -531,12 +538,8 @@ fn map_go_version(version: &str, origin: PinOrigin) -> ToolchainResolution { ("1", Some("21")) => ("go-1.21".to_owned(), false), ("1", Some("22")) => ("go-1.22".to_owned(), false), ("1", Some("23")) => ("go-1.23".to_owned(), false), - ("1", Some(m)) if m.parse::().is_ok_and(|v| v >= 24) => { - (format!("go-1.{m}"), true) - } - ("1", Some(m)) if m.parse::().is_ok_and(|v| v < 21) => { - (format!("go-1.{m}"), true) - } + ("1", Some(m)) if m.parse::().is_ok_and(|v| v >= 24) => (format!("go-1.{m}"), true), + ("1", Some(m)) if m.parse::().is_ok_and(|v| v < 21) => (format!("go-1.{m}"), true), _ => ("go-stable".to_owned(), false), }; @@ -570,14 +573,19 @@ fn try_pom_xml(root: &Path) -> Option { // Look for 21 or 21 for line in content.lines() { let trimmed = line.trim(); - for tag in &["", "", ""] { + for tag in &[ + "", + "", + "", + ] { if trimmed.starts_with(tag) - && let Some(inner) = trimmed.strip_prefix(tag) { - let version = inner.split('<').next().unwrap_or("").trim(); - if !version.is_empty() { - return Some(map_java_version(version, PinOrigin::PomXml)); - } + && let Some(inner) = trimmed.strip_prefix(tag) + { + let version = inner.split('<').next().unwrap_or("").trim(); + if !version.is_empty() { + return Some(map_java_version(version, PinOrigin::PomXml)); } + } } } None @@ -592,10 +600,12 @@ fn try_build_gradle(root: &Path) -> Option { let trimmed = line.trim(); // Groovy: sourceCompatibility = '21' or JavaVersion.VERSION_21 // Kotlin: sourceCompatibility = JavaVersion.VERSION_21 - if (trimmed.starts_with("sourceCompatibility") || trimmed.starts_with("languageVersion")) - && let Some(ver) = extract_java_version_from_gradle_line(trimmed) { - return Some(map_java_version(&ver, PinOrigin::BuildGradle)); - } + if (trimmed.starts_with("sourceCompatibility") + || trimmed.starts_with("languageVersion")) + && let Some(ver) = extract_java_version_from_gradle_line(trimmed) + { + return Some(map_java_version(&ver, PinOrigin::BuildGradle)); + } } } None @@ -606,7 +616,8 @@ fn extract_java_version_from_gradle_line(line: &str) -> Option { // and: languageVersion.set(JavaLanguageVersion.of(21)) let after_eq = line.split_once('=').map(|x| x.1).unwrap_or(line); // Try to find a number in the value. - let digits: String = after_eq.chars() + let digits: String = after_eq + .chars() .skip_while(|c| !c.is_ascii_digit()) .take_while(|c| c.is_ascii_digit()) .collect(); @@ -614,9 +625,7 @@ fn extract_java_version_from_gradle_line(line: &str) -> Option { // Try "VERSION_21" pattern. if let Some(pos) = after_eq.find("VERSION_") { let rest = &after_eq[pos + 8..]; - let digits: String = rest.chars() - .take_while(|c| c.is_ascii_digit()) - .collect(); + let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect(); if !digits.is_empty() { return Some(digits); } @@ -681,10 +690,12 @@ fn try_composer_json(root: &Path) -> Option { if json_line_has_key(trimmed, "require") { in_require = true; } - if in_require && trimmed.contains("\"php\"") - && let Some(ver) = extract_version_from_json_value(trimmed) { - return Some(map_php_version(&ver, PinOrigin::ComposerJson)); - } + if in_require + && trimmed.contains("\"php\"") + && let Some(ver) = extract_version_from_json_value(trimmed) + { + return Some(map_php_version(&ver, PinOrigin::ComposerJson)); + } // Stop at closing brace of require block. if in_require && (trimmed == "}," || trimmed == "}") { in_require = false; @@ -763,7 +774,11 @@ mod tests { #[test] fn pyproject_requires_python() { let dir = TempDir::new().unwrap(); - fs::write(dir.path().join("pyproject.toml"), "[project]\nrequires-python = \">=3.11\"\n").unwrap(); + fs::write( + dir.path().join("pyproject.toml"), + "[project]\nrequires-python = \">=3.11\"\n", + ) + .unwrap(); let r = resolve_python(dir.path()); assert_eq!(r.toolchain_id, "python-3.11"); assert_eq!(r.pin_origin, PinOrigin::PyprojectToml); @@ -772,7 +787,11 @@ mod tests { #[test] fn pipfile_python_version() { let dir = TempDir::new().unwrap(); - fs::write(dir.path().join("Pipfile"), "[requires]\npython_version = \"3.10\"\n").unwrap(); + fs::write( + dir.path().join("Pipfile"), + "[requires]\npython_version = \"3.10\"\n", + ) + .unwrap(); let r = resolve_python(dir.path()); assert_eq!(r.toolchain_id, "python-3.10"); assert_eq!(r.pin_origin, PinOrigin::Pipfile); @@ -793,7 +812,8 @@ mod tests { fs::write( dir.path().join("rust-toolchain.toml"), "[toolchain]\nchannel = \"stable\"\n", - ).unwrap(); + ) + .unwrap(); let r = resolve_rust(dir.path()); assert_eq!(r.toolchain_id, "rust-stable"); assert!(!r.toolchain_drift); @@ -816,7 +836,8 @@ mod tests { fs::write( dir.path().join("Cargo.toml"), "[package]\nname = \"foo\"\nrust-version = \"1.75\"\n", - ).unwrap(); + ) + .unwrap(); let r = resolve_rust(dir.path()); assert_eq!(r.pin_origin, PinOrigin::CargoToml); assert!(r.toolchain_id.starts_with("rust-1")); @@ -848,7 +869,8 @@ mod tests { fs::write( dir.path().join("package.json"), r#"{"engines": {"node": ">=18.0.0"}}"#, - ).unwrap(); + ) + .unwrap(); let r = resolve_node(dir.path()); assert_eq!(r.toolchain_id, "node-18"); } @@ -866,7 +888,11 @@ mod tests { #[test] fn go_mod_version() { let dir = TempDir::new().unwrap(); - fs::write(dir.path().join("go.mod"), "module example.com/app\n\ngo 1.22\n").unwrap(); + fs::write( + dir.path().join("go.mod"), + "module example.com/app\n\ngo 1.22\n", + ) + .unwrap(); let r = resolve_go(dir.path()); assert_eq!(r.toolchain_id, "go-1.22"); assert!(!r.toolchain_drift); @@ -902,7 +928,8 @@ mod tests { fs::write( dir.path().join("build.gradle"), "sourceCompatibility = '17'\ntargetCompatibility = '17'\n", - ).unwrap(); + ) + .unwrap(); let r = resolve_java(dir.path()); assert_eq!(r.toolchain_id, "java-17"); assert_eq!(r.pin_origin, PinOrigin::BuildGradle); @@ -924,7 +951,8 @@ mod tests { fs::write( dir.path().join("composer.json"), r#"{"require": {"php": ">=8.1"}}"#, - ).unwrap(); + ) + .unwrap(); let r = resolve_php(dir.path()); assert_eq!(r.toolchain_id, "php-8.1"); assert_eq!(r.pin_origin, PinOrigin::ComposerJson); @@ -982,7 +1010,10 @@ mod tests { #[test] fn json_line_has_key_rejects_key_in_value() { assert!(!json_line_has_key(r#" "type": "require","#, "require")); - assert!(!json_line_has_key(r#" "desc": "engines config","#, "engines")); + assert!(!json_line_has_key( + r#" "desc": "engines config","#, + "engines" + )); } #[test] diff --git a/src/dynamic/trace.rs b/src/dynamic/trace.rs index b4a45dc7..94d4fe6d 100644 --- a/src/dynamic/trace.rs +++ b/src/dynamic/trace.rs @@ -208,7 +208,10 @@ mod tests { #[test] fn jsonl_round_trips_through_serde() { let t = VerifyTrace::new(); - t.record(TraceStage::SandboxStarted, Some("payload=sqli-tautology".to_owned())); + t.record( + TraceStage::SandboxStarted, + Some("payload=sqli-tautology".to_owned()), + ); t.record(TraceStage::OracleObserved, Some("fired=true".to_owned())); let jsonl = t.to_jsonl(); let mut parsed = Vec::new(); diff --git a/src/dynamic/verify.rs b/src/dynamic/verify.rs index 44febb6c..0d4f5c68 100644 --- a/src/dynamic/verify.rs +++ b/src/dynamic/verify.rs @@ -5,18 +5,20 @@ use crate::callgraph::CallGraph; use crate::commands::scan::Diag; -use crate::dynamic::corpus::{payloads_for, CORPUS_VERSION}; +use crate::dynamic::corpus::{CORPUS_VERSION, payloads_for}; use crate::dynamic::oob::OobListener; use crate::dynamic::report::{AttemptSummary, VerifyResult, VerifyStatus}; -use crate::dynamic::runner::{run_spec, RunError}; -use crate::dynamic::sandbox::{toolchain_id_with_digest, SandboxOptions}; +use crate::dynamic::runner::{RunError, run_spec}; +use crate::dynamic::sandbox::{SandboxOptions, toolchain_id_with_digest}; use crate::dynamic::spec::{HarnessSpec, SPEC_FORMAT_VERSION}; use crate::dynamic::stubs::StubHarness; use crate::dynamic::telemetry::{self, SamplingPolicy, TelemetryEvent}; use crate::dynamic::toolchain; -use crate::evidence::{HardeningSummary, InconclusiveReason, SpecDerivationStrategy, UnsupportedReason}; #[cfg(target_os = "linux")] use crate::evidence::HardeningPrimitive; +use crate::evidence::{ + HardeningSummary, InconclusiveReason, SpecDerivationStrategy, UnsupportedReason, +}; use crate::summary::GlobalSummaries; use crate::utils::config::Config; use std::path::Path; @@ -208,10 +210,7 @@ impl VerifyOptions { /// [`verify_finding`]. fn lang_needs_host_libs(lang: crate::symbol::Lang) -> bool { use crate::symbol::Lang::*; - matches!( - lang, - Python | JavaScript | TypeScript | Java | Ruby | Php - ) + matches!(lang, Python | JavaScript | TypeScript | Java | Ruby | Php) } // ── Dynamic verdict cache helpers (§12 Q5) ─────────────────────────────────── @@ -391,8 +390,7 @@ fn spec_derivation_failed_verdict( policy: &SamplingPolicy, ) -> VerifyResult { if matches!(reason, UnsupportedReason::SpecDerivationFailed) && should_be_inconclusive(diag) { - let strategies: Vec = - HarnessSpec::derivation_strategies().to_vec(); + let strategies: Vec = HarnessSpec::derivation_strategies().to_vec(); let hint = derivation_failure_hint(diag); let inconclusive_reason = InconclusiveReason::SpecDerivationFailed { tried: strategies, @@ -542,9 +540,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { triggered_payload: None, reason: None, inconclusive_reason: Some(inconclusive_reason), - detail: Some(format!( - "dynamic execution refused by policy rule {rule}" - )), + detail: Some(format!("dynamic execution refused by policy rule {rule}")), attempts: vec![], toolchain_match: None, differential: None, @@ -626,9 +622,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { // structured `Inconclusive(BackendInsufficient)` so operators see // the backend gap instead of a quiet `Confirmed` against an // unhardened host. - if opts.refuse_filesystem_confirm - && spec.expected_cap.contains(crate::labels::Cap::FILE_IO) - { + if opts.refuse_filesystem_confirm && spec.expected_cap.contains(crate::labels::Cap::FILE_IO) { let backend = if cfg!(target_os = "macos") { "macos-process-without-sandbox-exec" } else { @@ -701,7 +695,11 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { Lang::Php => toolchain::resolve_php(Path::new(".")), _ => toolchain::resolve_python(Path::new(".")), }; - let toolchain_match = if toolchain_res.toolchain_drift { "drift" } else { "exact" }; + let toolchain_match = if toolchain_res.toolchain_drift { + "drift" + } else { + "exact" + }; // Enrich the resolved toolchain_id with the Docker image digest (§22.1). // The enriched ID is used as the toolchain_id component of the verdict cache // key so that image updates always invalidate stale cache entries. @@ -717,9 +715,10 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { &entry_hash, import_digest, &effective_toolchain_id, - ) { - return cached; - } + ) + { + return cached; + } // Phase 10 (Track D.3): spawn the boundary stubs the spec // demands *before* the sandbox runs. When `stubs_required` is @@ -787,14 +786,7 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { _ => 1, }; - let mut verdict = build_verdict( - &finding_id, - &spec, - result, - toolchain_match, - opts, - elapsed, - ); + let mut verdict = build_verdict(&finding_id, &spec, result, toolchain_match, opts, elapsed); // Phase 29 follow-up: stamp `replay_stable` from a `reproduce.sh` rerun // against the freshly written bundle. Opt-in (see @@ -807,7 +799,11 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { && let Some(bundle) = crate::dynamic::repro::bundle_root_for(&spec.spec_hash) && bundle.join("reproduce.sh").exists() { - let replay_args: &[&str] = if opts.replay_use_docker { &["--docker"] } else { &[] }; + let replay_args: &[&str] = if opts.replay_use_docker { + &["--docker"] + } else { + &[] + }; let replay = crate::dynamic::repro::replay_bundle(&bundle, replay_args); verdict.replay_stable = crate::dynamic::repro::replay_stability(&replay); } @@ -849,7 +845,6 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { verdict } - /// Project the platform-cfg'd [`crate::dynamic::sandbox::HardeningRecord`] /// into the portable [`HardeningSummary`] that lands on /// [`VerifyResult::hardening_outcome`]. Returns `None` when the run did @@ -961,10 +956,7 @@ fn build_verdict( let triggered_payload = run.attempts[i].payload_label.to_string(); let payloads = payloads_for(spec.expected_cap); let vuln_payloads: Vec<_> = payloads.iter().filter(|p| !p.is_benign).collect(); - let payload_bytes = vuln_payloads - .get(i) - .map(|p| p.bytes) - .unwrap_or(b""); + let payload_bytes = vuln_payloads.get(i).map(|p| p.bytes).unwrap_or(b""); let hardening_outcome = summarize_hardening(&run.attempts[i].outcome); // Emit repro artifact. @@ -1223,7 +1215,10 @@ fn build_verdict( // (cf. §10 decision 14 and the verify_result_json_shape contract). let (reason, detail) = match &e { crate::dynamic::harness::HarnessError::Unsupported(r) => (Some(r.clone()), None), - _ => (Some(UnsupportedReason::BackendUnavailable), Some(format!("{e}"))), + _ => ( + Some(UnsupportedReason::BackendUnavailable), + Some(format!("{e}")), + ), }; VerifyResult { finding_id: finding_id.to_owned(), @@ -1240,7 +1235,10 @@ fn build_verdict( hardening_outcome: None, } } - Err(RunError::BuildFailed { stderr, attempts: build_att }) => VerifyResult { + Err(RunError::BuildFailed { + stderr, + attempts: build_att, + }) => VerifyResult { finding_id: finding_id.to_owned(), status: VerifyStatus::Inconclusive, triggered_payload: None, @@ -1385,7 +1383,10 @@ mod tests { use crate::dynamic::sandbox::ProcessHardeningProfile; let opts = VerifyOptions::from_config(&Config::default()); assert!( - matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Standard), + matches!( + opts.sandbox.process_hardening, + ProcessHardeningProfile::Standard + ), "back-compat: missing harden_profile must keep the Standard baseline so \ existing call sites (process backend without `--harden=strict`) keep \ their pre-Phase-17 hardening matrix" @@ -1399,7 +1400,10 @@ mod tests { config.scanner.harden_profile = "strict".to_owned(); let opts = VerifyOptions::from_config(&config); assert!( - matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Strict), + matches!( + opts.sandbox.process_hardening, + ProcessHardeningProfile::Strict + ), "harden_profile=strict must engage the full Phase-17/18 lockdown so \ `--harden=strict` actually wraps the harness with sandbox-exec on macOS \ and layers chroot + seccomp on Linux" @@ -1451,7 +1455,10 @@ mod tests { config.scanner.harden_profile = "lockdown".to_owned(); let opts = VerifyOptions::from_config(&config); assert!( - matches!(opts.sandbox.process_hardening, ProcessHardeningProfile::Standard), + matches!( + opts.sandbox.process_hardening, + ProcessHardeningProfile::Standard + ), "unknown harden_profile values must degrade to Standard so a typo in \ nyx.toml does not silently leave the operator without the baseline \ hardening they were already paying for" @@ -1680,7 +1687,14 @@ mod tests { ); // Insert with current CORPUS_VERSION → must be a HIT. - insert_verdict_cache(&db_path, "spec_stale", "hash_stale", "", "python-3.11", &result); + insert_verdict_cache( + &db_path, + "spec_stale", + "hash_stale", + "", + "python-3.11", + &result, + ); let hit = lookup_verdict_cache(&db_path, "spec_stale", "hash_stale", "", "python-3.11"); assert!( hit.is_some(), diff --git a/src/evidence.rs b/src/evidence.rs index 74f411f6..a56278ba 100644 --- a/src/evidence.rs +++ b/src/evidence.rs @@ -311,10 +311,7 @@ pub enum EntryKind { /// class name and the method to drive so the lang emitter can build /// a `Cls().method()` invocation. Land in /// Phase 19. - ClassMethod { - class: String, - method: String, - }, + ClassMethod { class: String, method: String }, /// Message-queue subscriber / consumer. `queue` is the topic / /// stream / channel name; `message_schema`, when present, is a /// free-form JSON description of the expected message body that the @@ -335,23 +332,16 @@ pub enum EntryKind { }, /// GraphQL resolver — `type_name.field` pair the harness drives via /// an in-process GraphQL execution layer. Land in Phase 21. - GraphQLResolver { - type_name: String, - field: String, - }, + GraphQLResolver { type_name: String, field: String }, /// WebSocket handler — `path` is the canonical mount point; the /// harness opens a loopback ws connection and sends the payload as /// the first message frame. Land in Phase 21. - WebSocket { - path: String, - }, + WebSocket { path: String }, /// HTTP / framework middleware — `name` is the middleware identifier /// (class name, function name, registration key) the harness mounts /// on a synthetic pipeline before invoking it with a crafted /// request. Land in Phase 21. - Middleware { - name: String, - }, + Middleware { name: String }, /// Database migration / schema-change script — `version`, when /// present, is the migration revision identifier (Alembic / Flyway / /// Rails string) so the harness can pin the apply step. Land in @@ -408,8 +398,7 @@ impl<'de> Deserialize<'de> for EntryKind { { use serde::de::Error as _; - let value = serde_json::Value::deserialize(deserializer) - .map_err(D::Error::custom)?; + let value = serde_json::Value::deserialize(deserializer).map_err(D::Error::custom)?; // Bare-string form (legacy unit variants). if let Some(tag) = value.as_str() { @@ -440,10 +429,12 @@ impl<'de> Deserialize<'de> for EntryKind { class: String, method: String, } - serde_json::from_value::(body).ok().map(|f| Self::ClassMethod { - class: f.class, - method: f.method, - }) + serde_json::from_value::(body) + .ok() + .map(|f| Self::ClassMethod { + class: f.class, + method: f.method, + }) } "MessageHandler" => { #[derive(Deserialize)] @@ -452,10 +443,12 @@ impl<'de> Deserialize<'de> for EntryKind { #[serde(default)] message_schema: Option, } - serde_json::from_value::(body).ok().map(|f| Self::MessageHandler { - queue: f.queue, - message_schema: f.message_schema, - }) + serde_json::from_value::(body) + .ok() + .map(|f| Self::MessageHandler { + queue: f.queue, + message_schema: f.message_schema, + }) } "ScheduledJob" => { #[derive(Deserialize)] @@ -465,7 +458,9 @@ impl<'de> Deserialize<'de> for EntryKind { } serde_json::from_value::(body) .ok() - .map(|f| Self::ScheduledJob { schedule: f.schedule }) + .map(|f| Self::ScheduledJob { + schedule: f.schedule, + }) } "GraphQLResolver" => { #[derive(Deserialize)] @@ -473,10 +468,12 @@ impl<'de> Deserialize<'de> for EntryKind { type_name: String, field: String, } - serde_json::from_value::(body).ok().map(|f| Self::GraphQLResolver { - type_name: f.type_name, - field: f.field, - }) + serde_json::from_value::(body) + .ok() + .map(|f| Self::GraphQLResolver { + type_name: f.type_name, + field: f.field, + }) } "WebSocket" => { #[derive(Deserialize)] @@ -692,9 +689,7 @@ impl fmt::Display for InconclusiveReason { Self::ReversedDifferential => f.write_str( "reversed differential (benign payload fired, vulnerable payload did not)", ), - Self::UnrelatedCrash => { - f.write_str("harness crashed outside the instrumented sink") - } + Self::UnrelatedCrash => f.write_str("harness crashed outside the instrumented sink"), Self::BackendInsufficient { backend, oracle_kind, @@ -2248,8 +2243,12 @@ mod tests { type_name: "Query".into(), field: "user".into(), }, - EntryKind::WebSocket { path: "/ws/feed".into() }, - EntryKind::Middleware { name: "auth_filter".into() }, + EntryKind::WebSocket { + path: "/ws/feed".into(), + }, + EntryKind::Middleware { + name: "auth_filter".into(), + }, EntryKind::Migration { version: Some("0042_user_table".into()), }, diff --git a/src/fmt.rs b/src/fmt.rs index 4072e793..aeeba356 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -260,7 +260,9 @@ fn render_chains(chains: &[ChainFinding], _width: usize) -> String { let mut out = String::new(); out.push_str(&format!( "{}\n", - style(format!("Chains ({})", chains.len())).bold().underlined() + style(format!("Chains ({})", chains.len())) + .bold() + .underlined() )); for c in chains { let sev = chain_severity_tag(c.severity); @@ -301,7 +303,11 @@ fn render_chains(chains: &[ChainFinding], _width: usize) -> String { fn chain_severity_tag(s: crate::chain::finding::ChainSeverity) -> String { use crate::chain::finding::ChainSeverity; match s { - ChainSeverity::Critical => format!("{} {}", style("✖").red().bold(), style("[CRITICAL]").red().bold()), + ChainSeverity::Critical => format!( + "{} {}", + style("✖").red().bold(), + style("[CRITICAL]").red().bold() + ), ChainSeverity::High => format!("{} {}", style("✖").red(), style("[HIGH]").red()), ChainSeverity::Medium => format!("{} {}", style("⚠").yellow(), style("[MEDIUM]").yellow()), ChainSeverity::Low => format!("{} {}", style("●").dim(), style("[LOW]").dim()), @@ -609,14 +615,15 @@ fn format_inconclusive_reason(r: &crate::evidence::InconclusiveReason) -> String supported, .. } => { - format!( - "entry kind {attempted} unsupported for {lang:?} (supported: {supported:?})" - ) + format!("entry kind {attempted} unsupported for {lang:?} (supported: {supported:?})") } InconclusiveReason::NoBenignControl => "no benign control payload".to_string(), InconclusiveReason::ReversedDifferential => "reversed differential".to_string(), InconclusiveReason::UnrelatedCrash => "unrelated crash (not sink-site)".to_string(), - InconclusiveReason::BackendInsufficient { backend, oracle_kind } => { + InconclusiveReason::BackendInsufficient { + backend, + oracle_kind, + } => { format!("backend {backend} cannot enforce {oracle_kind} oracle") } InconclusiveReason::PolicyDeniedDynamic { rule, .. } => { diff --git a/src/output/sarif.rs b/src/output/sarif.rs index 58f8e6c5..8c9ce82f 100644 --- a/src/output/sarif.rs +++ b/src/output/sarif.rs @@ -117,11 +117,7 @@ pub fn build_sarif(diags: &[Diag], scan_root: &Path) -> Value { /// process the diagnostics. When the slice is empty the /// `properties.chains` array is still emitted (as `[]`) so consumers /// can rely on the key existing. -pub fn build_sarif_with_chains( - diags: &[Diag], - chains: &[ChainFinding], - scan_root: &Path, -) -> Value { +pub fn build_sarif_with_chains(diags: &[Diag], chains: &[ChainFinding], scan_root: &Path) -> Value { let mut rule_ids: Vec = Vec::new(); let mut rule_index_map: HashMap = HashMap::new(); @@ -270,7 +266,11 @@ pub fn build_sarif_with_chains( } } - if let Some(dv) = d.evidence.as_ref().and_then(|ev| ev.dynamic_verdict.as_ref()) { + if let Some(dv) = d + .evidence + .as_ref() + .and_then(|ev| ev.dynamic_verdict.as_ref()) + { result["partialFingerprints"] = json!({ "dynamic_verdict_status": serde_json::to_value(dv.status) .unwrap_or(Value::Null) @@ -316,9 +316,10 @@ pub fn build_sarif_with_chains( // reruns because both the finding's `stable_hash` and the // chain's `stable_hash` are byte-deterministic. if d.stable_hash != 0 - && let Some(chain_hash) = chain_member_of.get(&d.stable_hash) { - props.insert("chain_member_of".into(), json!(chain_hash)); - } + && let Some(chain_hash) = chain_member_of.get(&d.stable_hash) + { + props.insert("chain_member_of".into(), json!(chain_hash)); + } result["properties"] = Value::Object(props); @@ -448,13 +449,19 @@ mod tests { #[test] fn rule_description_taint_prefix_returns_fallback() { let desc = rule_description("taint-unsanitised-flow"); - assert!(desc.contains("Unsanitised"), "expected taint fallback, got: {desc}"); + assert!( + desc.contains("Unsanitised"), + "expected taint fallback, got: {desc}" + ); } #[test] fn rule_description_taint_with_suffix_normalises_to_base() { let desc = rule_description("taint-unsanitised-flow:foo.rs:42"); - assert!(desc.contains("Unsanitised"), "expected taint fallback, got: {desc}"); + assert!( + desc.contains("Unsanitised"), + "expected taint fallback, got: {desc}" + ); } #[test] diff --git a/src/output/severity.rs b/src/output/severity.rs index 854993c5..0c1aa614 100644 --- a/src/output/severity.rs +++ b/src/output/severity.rs @@ -98,14 +98,20 @@ mod tests { #[test] fn browser_local_rce_is_critical() { assert_eq!( - chain_severity(ImpactCategory::BrowserToLocalRce, &[edge(Feasibility::Confirmed)]), + chain_severity( + ImpactCategory::BrowserToLocalRce, + &[edge(Feasibility::Confirmed)] + ), ChainSeverity::Critical, ); } #[test] fn session_hijack_downgrades_on_all_unverified() { - let confirmed = chain_severity(ImpactCategory::SessionHijack, &[edge(Feasibility::Confirmed)]); + let confirmed = chain_severity( + ImpactCategory::SessionHijack, + &[edge(Feasibility::Confirmed)], + ); assert_eq!(confirmed, ChainSeverity::High); let unverified = chain_severity( ImpactCategory::SessionHijack, diff --git a/src/rank.rs b/src/rank.rs index 3dd8e095..4d0ef69f 100644 --- a/src/rank.rs +++ b/src/rank.rs @@ -222,11 +222,7 @@ pub fn rank_diags(diags: &mut [Diag]) { .and_then(|ev| ev.dynamic_verdict.as_ref()) .map(|dv| format!("{:?}", dv.status)) .unwrap_or_default(); - telemetry::emit_rank_delta(RankDeltaEvent::new( - d.finding_id.clone(), - status, - delta, - )); + telemetry::emit_rank_delta(RankDeltaEvent::new(d.finding_id.clone(), status, delta)); } } diags.sort_by(|a, b| { diff --git a/src/server/routes/surface.rs b/src/server/routes/surface.rs index fd35490f..155ca42e 100644 --- a/src/server/routes/surface.rs +++ b/src/server/routes/surface.rs @@ -26,14 +26,13 @@ async fn get_surface(State(state): State) -> ApiResult> { // Building the surface map can do filesystem IO + tree-sitter // parsing; keep it off the async runtime. - let join_result = tokio::task::spawn_blocking(move || { - load_or_build(&scan_root, &database_dir, &cfg) - }) - .await - .map_err(|e| ApiError::internal(format!("surface map task failed: {e}")))?; + let join_result = + tokio::task::spawn_blocking(move || load_or_build(&scan_root, &database_dir, &cfg)) + .await + .map_err(|e| ApiError::internal(format!("surface map task failed: {e}")))?; - let mut map = join_result - .map_err(|e| ApiError::internal(format!("failed to build surface map: {e}")))?; + let mut map = + join_result.map_err(|e| ApiError::internal(format!("failed to build surface map: {e}")))?; let bytes = map .to_json() .map_err(|e| ApiError::internal(format!("encode surface map: {e}")))?; diff --git a/src/surface/build.rs b/src/surface/build.rs index 89fb7605..dffa9676 100644 --- a/src/surface/build.rs +++ b/src/surface/build.rs @@ -29,9 +29,9 @@ use crate::summary::GlobalSummaries; use crate::surface::{ SurfaceMap, dangerous, datastore, external, lang::{ - go_gin, go_http, java_quarkus, java_servlet, java_spring, js_express, js_koa, - php_laravel, php_slim, python_django, python_fastapi, python_flask, - ruby_rails, ruby_sinatra, rust_actix, rust_axum, ts_next, + go_gin, go_http, java_quarkus, java_servlet, java_spring, js_express, js_koa, php_laravel, + php_slim, python_django, python_fastapi, python_flask, ruby_rails, ruby_sinatra, + rust_actix, rust_axum, ts_next, }, reachability, }; @@ -63,12 +63,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap { .as_mut() .and_then(|p| p.parse(&bytes, None)) .map(|tree| { - let mut all = python_flask::detect_flask_routes( - &tree, - &bytes, - path, - inputs.scan_root, - ); + let mut all = + python_flask::detect_flask_routes(&tree, &bytes, path, inputs.scan_root); all.extend(python_fastapi::detect_fastapi_routes( &tree, &bytes, @@ -165,12 +161,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap { .as_mut() .and_then(|p| p.parse(&bytes, None)) .map(|tree| { - let mut all = php_laravel::detect_laravel_routes( - &tree, - &bytes, - path, - inputs.scan_root, - ); + let mut all = + php_laravel::detect_laravel_routes(&tree, &bytes, path, inputs.scan_root); all.extend(php_slim::detect_slim_routes( &tree, &bytes, @@ -185,12 +177,8 @@ pub fn build_surface_map(inputs: &SurfaceBuildInputs<'_>) -> SurfaceMap { .as_mut() .and_then(|p| p.parse(&bytes, None)) .map(|tree| { - let mut all = ruby_sinatra::detect_sinatra_routes( - &tree, - &bytes, - path, - inputs.scan_root, - ); + let mut all = + ruby_sinatra::detect_sinatra_routes(&tree, &bytes, path, inputs.scan_root); all.extend(ruby_rails::detect_rails_routes( &tree, &bytes, @@ -435,13 +423,15 @@ def evaluator(): let files = vec![py]; let inputs = empty_inputs(&files, Some(dir.path()), &gs, &cg, &cfg); let map = build_surface_map(&inputs); - assert!(map - .nodes - .iter() - .any(|n| matches!(n, SurfaceNode::DangerousLocal(_)))); - assert!(map - .edges - .iter() - .any(|e| matches!(e.kind, crate::surface::EdgeKind::Reaches))); + assert!( + map.nodes + .iter() + .any(|n| matches!(n, SurfaceNode::DangerousLocal(_))) + ); + assert!( + map.edges + .iter() + .any(|e| matches!(e.kind, crate::surface::EdgeKind::Reaches)) + ); } } diff --git a/src/surface/datastore.rs b/src/surface/datastore.rs index 574a829e..b1368bc3 100644 --- a/src/surface/datastore.rs +++ b/src/surface/datastore.rs @@ -28,86 +28,314 @@ struct DriverRule { const DRIVER_RULES: &[DriverRule] = &[ // Python — relational - DriverRule { leaf: "psycopg2.connect", kind: DataStoreKind::Sql, label: "PostgreSQL (psycopg2)" }, - DriverRule { leaf: "psycopg.connect", kind: DataStoreKind::Sql, label: "PostgreSQL (psycopg3)" }, - DriverRule { leaf: "mysql.connector.connect", kind: DataStoreKind::Sql, label: "MySQL (mysql.connector)" }, - DriverRule { leaf: "MySQLdb.connect", kind: DataStoreKind::Sql, label: "MySQL (MySQLdb)" }, - DriverRule { leaf: "pymysql.connect", kind: DataStoreKind::Sql, label: "MySQL (PyMySQL)" }, - DriverRule { leaf: "sqlite3.connect", kind: DataStoreKind::Sql, label: "SQLite (sqlite3)" }, - DriverRule { leaf: "sqlalchemy.create_engine", kind: DataStoreKind::Sql, label: "SQLAlchemy" }, - DriverRule { leaf: "django.db.connection", kind: DataStoreKind::Sql, label: "Django ORM" }, + DriverRule { + leaf: "psycopg2.connect", + kind: DataStoreKind::Sql, + label: "PostgreSQL (psycopg2)", + }, + DriverRule { + leaf: "psycopg.connect", + kind: DataStoreKind::Sql, + label: "PostgreSQL (psycopg3)", + }, + DriverRule { + leaf: "mysql.connector.connect", + kind: DataStoreKind::Sql, + label: "MySQL (mysql.connector)", + }, + DriverRule { + leaf: "MySQLdb.connect", + kind: DataStoreKind::Sql, + label: "MySQL (MySQLdb)", + }, + DriverRule { + leaf: "pymysql.connect", + kind: DataStoreKind::Sql, + label: "MySQL (PyMySQL)", + }, + DriverRule { + leaf: "sqlite3.connect", + kind: DataStoreKind::Sql, + label: "SQLite (sqlite3)", + }, + DriverRule { + leaf: "sqlalchemy.create_engine", + kind: DataStoreKind::Sql, + label: "SQLAlchemy", + }, + DriverRule { + leaf: "django.db.connection", + kind: DataStoreKind::Sql, + label: "Django ORM", + }, // Python — kv / doc - DriverRule { leaf: "redis.Redis", kind: DataStoreKind::KeyValue, label: "Redis" }, - DriverRule { leaf: "redis.from_url", kind: DataStoreKind::KeyValue, label: "Redis" }, - DriverRule { leaf: "pymongo.MongoClient", kind: DataStoreKind::Document, label: "MongoDB" }, - DriverRule { leaf: "boto3.client", kind: DataStoreKind::BlobStore, label: "AWS (boto3)" }, - DriverRule { leaf: "boto3.resource", kind: DataStoreKind::BlobStore, label: "AWS (boto3)" }, - + DriverRule { + leaf: "redis.Redis", + kind: DataStoreKind::KeyValue, + label: "Redis", + }, + DriverRule { + leaf: "redis.from_url", + kind: DataStoreKind::KeyValue, + label: "Redis", + }, + DriverRule { + leaf: "pymongo.MongoClient", + kind: DataStoreKind::Document, + label: "MongoDB", + }, + DriverRule { + leaf: "boto3.client", + kind: DataStoreKind::BlobStore, + label: "AWS (boto3)", + }, + DriverRule { + leaf: "boto3.resource", + kind: DataStoreKind::BlobStore, + label: "AWS (boto3)", + }, // JavaScript / TypeScript — relational - DriverRule { leaf: "knex", kind: DataStoreKind::Sql, label: "Knex.js" }, - DriverRule { leaf: "createConnection", kind: DataStoreKind::Sql, label: "MySQL/Postgres (mysql/pg)" }, - DriverRule { leaf: "Sequelize", kind: DataStoreKind::Sql, label: "Sequelize" }, - DriverRule { leaf: "TypeORM.createConnection", kind: DataStoreKind::Sql, label: "TypeORM" }, - DriverRule { leaf: "PrismaClient", kind: DataStoreKind::Sql, label: "Prisma" }, - DriverRule { leaf: "pool.query", kind: DataStoreKind::Sql, label: "pg/mysql pool" }, - DriverRule { leaf: "client.query", kind: DataStoreKind::Sql, label: "pg client" }, - DriverRule { leaf: "db.query", kind: DataStoreKind::Sql, label: "Generic SQL driver" }, + DriverRule { + leaf: "knex", + kind: DataStoreKind::Sql, + label: "Knex.js", + }, + DriverRule { + leaf: "createConnection", + kind: DataStoreKind::Sql, + label: "MySQL/Postgres (mysql/pg)", + }, + DriverRule { + leaf: "Sequelize", + kind: DataStoreKind::Sql, + label: "Sequelize", + }, + DriverRule { + leaf: "TypeORM.createConnection", + kind: DataStoreKind::Sql, + label: "TypeORM", + }, + DriverRule { + leaf: "PrismaClient", + kind: DataStoreKind::Sql, + label: "Prisma", + }, + DriverRule { + leaf: "pool.query", + kind: DataStoreKind::Sql, + label: "pg/mysql pool", + }, + DriverRule { + leaf: "client.query", + kind: DataStoreKind::Sql, + label: "pg client", + }, + DriverRule { + leaf: "db.query", + kind: DataStoreKind::Sql, + label: "Generic SQL driver", + }, // JS — kv / doc - DriverRule { leaf: "redis.createClient", kind: DataStoreKind::KeyValue, label: "Redis (node-redis)" }, - DriverRule { leaf: "ioredis", kind: DataStoreKind::KeyValue, label: "ioredis" }, - DriverRule { leaf: "MongoClient.connect", kind: DataStoreKind::Document, label: "MongoDB (node)" }, - DriverRule { leaf: "AWS.S3", kind: DataStoreKind::BlobStore, label: "AWS S3" }, - + DriverRule { + leaf: "redis.createClient", + kind: DataStoreKind::KeyValue, + label: "Redis (node-redis)", + }, + DriverRule { + leaf: "ioredis", + kind: DataStoreKind::KeyValue, + label: "ioredis", + }, + DriverRule { + leaf: "MongoClient.connect", + kind: DataStoreKind::Document, + label: "MongoDB (node)", + }, + DriverRule { + leaf: "AWS.S3", + kind: DataStoreKind::BlobStore, + label: "AWS S3", + }, // Java — JDBC / Hibernate - DriverRule { leaf: "DriverManager.getConnection", kind: DataStoreKind::Sql, label: "JDBC" }, - DriverRule { leaf: "JdbcTemplate", kind: DataStoreKind::Sql, label: "Spring JdbcTemplate" }, - DriverRule { leaf: "EntityManager", kind: DataStoreKind::Sql, label: "JPA EntityManager" }, - DriverRule { leaf: "SessionFactory.openSession", kind: DataStoreKind::Sql, label: "Hibernate" }, - DriverRule { leaf: "Jedis", kind: DataStoreKind::KeyValue, label: "Jedis (Redis)" }, - DriverRule { leaf: "MongoClients.create", kind: DataStoreKind::Document, label: "MongoDB (java-driver)" }, - + DriverRule { + leaf: "DriverManager.getConnection", + kind: DataStoreKind::Sql, + label: "JDBC", + }, + DriverRule { + leaf: "JdbcTemplate", + kind: DataStoreKind::Sql, + label: "Spring JdbcTemplate", + }, + DriverRule { + leaf: "EntityManager", + kind: DataStoreKind::Sql, + label: "JPA EntityManager", + }, + DriverRule { + leaf: "SessionFactory.openSession", + kind: DataStoreKind::Sql, + label: "Hibernate", + }, + DriverRule { + leaf: "Jedis", + kind: DataStoreKind::KeyValue, + label: "Jedis (Redis)", + }, + DriverRule { + leaf: "MongoClients.create", + kind: DataStoreKind::Document, + label: "MongoDB (java-driver)", + }, // Go — sql + ORM - DriverRule { leaf: "sql.Open", kind: DataStoreKind::Sql, label: "database/sql" }, - DriverRule { leaf: "gorm.Open", kind: DataStoreKind::Sql, label: "GORM" }, - DriverRule { leaf: "sqlx.Connect", kind: DataStoreKind::Sql, label: "sqlx" }, - DriverRule { leaf: "sqlx.Open", kind: DataStoreKind::Sql, label: "sqlx" }, - DriverRule { leaf: "redis.NewClient", kind: DataStoreKind::KeyValue, label: "go-redis" }, - DriverRule { leaf: "mongo.Connect", kind: DataStoreKind::Document, label: "MongoDB (go-driver)" }, - + DriverRule { + leaf: "sql.Open", + kind: DataStoreKind::Sql, + label: "database/sql", + }, + DriverRule { + leaf: "gorm.Open", + kind: DataStoreKind::Sql, + label: "GORM", + }, + DriverRule { + leaf: "sqlx.Connect", + kind: DataStoreKind::Sql, + label: "sqlx", + }, + DriverRule { + leaf: "sqlx.Open", + kind: DataStoreKind::Sql, + label: "sqlx", + }, + DriverRule { + leaf: "redis.NewClient", + kind: DataStoreKind::KeyValue, + label: "go-redis", + }, + DriverRule { + leaf: "mongo.Connect", + kind: DataStoreKind::Document, + label: "MongoDB (go-driver)", + }, // PHP — Eloquent / PDO - DriverRule { leaf: "PDO", kind: DataStoreKind::Sql, label: "PDO" }, - DriverRule { leaf: "Eloquent::find", kind: DataStoreKind::Sql, label: "Laravel Eloquent" }, - DriverRule { leaf: "Eloquent::where", kind: DataStoreKind::Sql, label: "Laravel Eloquent" }, - DriverRule { leaf: "DB::connection", kind: DataStoreKind::Sql, label: "Laravel DB" }, - DriverRule { leaf: "Doctrine", kind: DataStoreKind::Sql, label: "Doctrine ORM" }, - + DriverRule { + leaf: "PDO", + kind: DataStoreKind::Sql, + label: "PDO", + }, + DriverRule { + leaf: "Eloquent::find", + kind: DataStoreKind::Sql, + label: "Laravel Eloquent", + }, + DriverRule { + leaf: "Eloquent::where", + kind: DataStoreKind::Sql, + label: "Laravel Eloquent", + }, + DriverRule { + leaf: "DB::connection", + kind: DataStoreKind::Sql, + label: "Laravel DB", + }, + DriverRule { + leaf: "Doctrine", + kind: DataStoreKind::Sql, + label: "Doctrine ORM", + }, // Ruby — ActiveRecord - DriverRule { leaf: "ActiveRecord::Base.connection", kind: DataStoreKind::Sql, label: "ActiveRecord" }, - DriverRule { leaf: "ActiveRecord::Base.find", kind: DataStoreKind::Sql, label: "ActiveRecord" }, - DriverRule { leaf: ".find_by_sql", kind: DataStoreKind::Sql, label: "ActiveRecord raw SQL" }, - + DriverRule { + leaf: "ActiveRecord::Base.connection", + kind: DataStoreKind::Sql, + label: "ActiveRecord", + }, + DriverRule { + leaf: "ActiveRecord::Base.find", + kind: DataStoreKind::Sql, + label: "ActiveRecord", + }, + DriverRule { + leaf: ".find_by_sql", + kind: DataStoreKind::Sql, + label: "ActiveRecord raw SQL", + }, // Rust — sqlx / diesel - DriverRule { leaf: "sqlx::query", kind: DataStoreKind::Sql, label: "sqlx" }, - DriverRule { leaf: "sqlx::query_as", kind: DataStoreKind::Sql, label: "sqlx" }, - DriverRule { leaf: "diesel::sql_query", kind: DataStoreKind::Sql, label: "Diesel" }, - DriverRule { leaf: "PgConnection::establish", kind: DataStoreKind::Sql, label: "Diesel" }, - + DriverRule { + leaf: "sqlx::query", + kind: DataStoreKind::Sql, + label: "sqlx", + }, + DriverRule { + leaf: "sqlx::query_as", + kind: DataStoreKind::Sql, + label: "sqlx", + }, + 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" }, - + 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" }, + DriverRule { + leaf: "open", + kind: DataStoreKind::Filesystem, + label: "Filesystem", + }, ]; /// Walk every function summary's callee list and emit one @@ -127,7 +355,9 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec { 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()); + let typed = summaries + .get_ssa(key) + .map(|s| s.typed_call_receivers.as_slice()); for callee in &summary.callees { let rule = match_rule(&callee.name).or_else(|| { typed @@ -136,11 +366,7 @@ pub fn detect_data_stores(summaries: &GlobalSummaries) -> Vec { }); let Some(rule) = rule else { continue }; let location = call_site_location(summary, callee); - let dedup = ( - location.file.clone(), - location.line, - rule.label.to_string(), - ); + let dedup = (location.file.clone(), location.line, rule.label.to_string()); if !seen.insert(dedup) { continue; } @@ -170,7 +396,10 @@ fn qualify(container: &str, callee_name: &str) -> String { /// `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()) + typed + .iter() + .find(|(o, _)| *o == ordinal) + .map(|(_, c)| c.as_str()) } fn match_rule(callee: &str) -> Option<&'static DriverRule> { @@ -285,11 +514,8 @@ mod tests { #[test] fn dedup_collapses_repeats_in_same_file() { let mut gs = GlobalSummaries::new(); - let (k, s) = summary_with_callees( - "init", - "app.py", - &["psycopg2.connect", "psycopg2.connect"], - ); + let (k, s) = + summary_with_callees("init", "app.py", &["psycopg2.connect", "psycopg2.connect"]); gs.insert(k, s); let nodes = detect_data_stores(&gs); assert_eq!(nodes.len(), 1); @@ -352,14 +578,12 @@ mod tests { 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 - }, - ], + 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); diff --git a/src/surface/external.rs b/src/surface/external.rs index 11d7175f..b3e75b67 100644 --- a/src/surface/external.rs +++ b/src/surface/external.rs @@ -19,81 +19,307 @@ struct ClientRule { const CLIENT_RULES: &[ClientRule] = &[ // HTTP - ClientRule { leaf: "requests.get", kind: ExternalServiceKind::HttpApi, label: "requests (Python)" }, - ClientRule { leaf: "requests.post", kind: ExternalServiceKind::HttpApi, label: "requests (Python)" }, - ClientRule { leaf: "httpx.get", kind: ExternalServiceKind::HttpApi, label: "httpx (Python)" }, - ClientRule { leaf: "httpx.post", kind: ExternalServiceKind::HttpApi, label: "httpx (Python)" }, - ClientRule { leaf: "urllib.request.urlopen", kind: ExternalServiceKind::HttpApi, label: "urllib" }, - ClientRule { leaf: "fetch", kind: ExternalServiceKind::HttpApi, label: "fetch (JS)" }, - ClientRule { leaf: "axios.get", kind: ExternalServiceKind::HttpApi, label: "axios" }, - ClientRule { leaf: "axios.post", kind: ExternalServiceKind::HttpApi, label: "axios" }, - ClientRule { leaf: "http.request", kind: ExternalServiceKind::HttpApi, label: "node http" }, - ClientRule { leaf: "got", kind: ExternalServiceKind::HttpApi, label: "got (JS)" }, - ClientRule { leaf: "HttpClient.send", kind: ExternalServiceKind::HttpApi, label: "Java HttpClient" }, - ClientRule { leaf: "HttpClient.execute", kind: ExternalServiceKind::HttpApi, label: "Java HttpClient" }, - ClientRule { leaf: "RestTemplate.exchange", kind: ExternalServiceKind::HttpApi, label: "Spring RestTemplate" }, - ClientRule { leaf: "RestTemplate.getForObject", kind: ExternalServiceKind::HttpApi, label: "Spring RestTemplate" }, - ClientRule { leaf: "OkHttpClient.newCall", kind: ExternalServiceKind::HttpApi, label: "OkHttp" }, - ClientRule { leaf: "http.Get", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" }, - ClientRule { leaf: "http.Post", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" }, - ClientRule { leaf: "http.NewRequest", kind: ExternalServiceKind::HttpApi, label: "net/http (Go)" }, - ClientRule { leaf: "client.Do", kind: ExternalServiceKind::HttpApi, label: "go http client" }, - ClientRule { leaf: "reqwest::get", kind: ExternalServiceKind::HttpApi, label: "reqwest (Rust)" }, - ClientRule { leaf: "reqwest::Client", kind: ExternalServiceKind::HttpApi, label: "reqwest (Rust)" }, - ClientRule { leaf: "Net::HTTP", kind: ExternalServiceKind::HttpApi, label: "Net::HTTP (Ruby)" }, - ClientRule { leaf: "HTTParty.get", kind: ExternalServiceKind::HttpApi, label: "HTTParty" }, - ClientRule { leaf: "Faraday", kind: ExternalServiceKind::HttpApi, label: "Faraday (Ruby)" }, - ClientRule { leaf: "curl_exec", kind: ExternalServiceKind::HttpApi, label: "PHP curl" }, - ClientRule { leaf: "file_get_contents", kind: ExternalServiceKind::HttpApi, label: "PHP file_get_contents" }, - ClientRule { leaf: "Guzzle", kind: ExternalServiceKind::HttpApi, label: "Guzzle (PHP)" }, - + ClientRule { + leaf: "requests.get", + kind: ExternalServiceKind::HttpApi, + label: "requests (Python)", + }, + ClientRule { + leaf: "requests.post", + kind: ExternalServiceKind::HttpApi, + label: "requests (Python)", + }, + ClientRule { + leaf: "httpx.get", + kind: ExternalServiceKind::HttpApi, + label: "httpx (Python)", + }, + ClientRule { + leaf: "httpx.post", + kind: ExternalServiceKind::HttpApi, + label: "httpx (Python)", + }, + ClientRule { + leaf: "urllib.request.urlopen", + kind: ExternalServiceKind::HttpApi, + label: "urllib", + }, + ClientRule { + leaf: "fetch", + kind: ExternalServiceKind::HttpApi, + label: "fetch (JS)", + }, + ClientRule { + leaf: "axios.get", + kind: ExternalServiceKind::HttpApi, + label: "axios", + }, + ClientRule { + leaf: "axios.post", + kind: ExternalServiceKind::HttpApi, + label: "axios", + }, + ClientRule { + leaf: "http.request", + kind: ExternalServiceKind::HttpApi, + label: "node http", + }, + ClientRule { + leaf: "got", + kind: ExternalServiceKind::HttpApi, + label: "got (JS)", + }, + ClientRule { + leaf: "HttpClient.send", + kind: ExternalServiceKind::HttpApi, + label: "Java HttpClient", + }, + ClientRule { + leaf: "HttpClient.execute", + kind: ExternalServiceKind::HttpApi, + label: "Java HttpClient", + }, + ClientRule { + leaf: "RestTemplate.exchange", + kind: ExternalServiceKind::HttpApi, + label: "Spring RestTemplate", + }, + ClientRule { + leaf: "RestTemplate.getForObject", + kind: ExternalServiceKind::HttpApi, + label: "Spring RestTemplate", + }, + ClientRule { + leaf: "OkHttpClient.newCall", + kind: ExternalServiceKind::HttpApi, + label: "OkHttp", + }, + ClientRule { + leaf: "http.Get", + kind: ExternalServiceKind::HttpApi, + label: "net/http (Go)", + }, + ClientRule { + leaf: "http.Post", + kind: ExternalServiceKind::HttpApi, + label: "net/http (Go)", + }, + ClientRule { + leaf: "http.NewRequest", + kind: ExternalServiceKind::HttpApi, + label: "net/http (Go)", + }, + ClientRule { + leaf: "client.Do", + kind: ExternalServiceKind::HttpApi, + label: "go http client", + }, + ClientRule { + leaf: "reqwest::get", + kind: ExternalServiceKind::HttpApi, + label: "reqwest (Rust)", + }, + ClientRule { + leaf: "reqwest::Client", + kind: ExternalServiceKind::HttpApi, + label: "reqwest (Rust)", + }, + ClientRule { + leaf: "Net::HTTP", + kind: ExternalServiceKind::HttpApi, + label: "Net::HTTP (Ruby)", + }, + ClientRule { + leaf: "HTTParty.get", + kind: ExternalServiceKind::HttpApi, + label: "HTTParty", + }, + ClientRule { + leaf: "Faraday", + kind: ExternalServiceKind::HttpApi, + label: "Faraday (Ruby)", + }, + ClientRule { + leaf: "curl_exec", + kind: ExternalServiceKind::HttpApi, + label: "PHP curl", + }, + ClientRule { + leaf: "file_get_contents", + kind: ExternalServiceKind::HttpApi, + label: "PHP file_get_contents", + }, + ClientRule { + leaf: "Guzzle", + kind: ExternalServiceKind::HttpApi, + label: "Guzzle (PHP)", + }, // Message brokers - ClientRule { leaf: "kafka.send", kind: ExternalServiceKind::MessageBroker, label: "Kafka" }, - ClientRule { leaf: "KafkaProducer.send", kind: ExternalServiceKind::MessageBroker, label: "Kafka" }, - ClientRule { leaf: "rabbitmq.publish", kind: ExternalServiceKind::MessageBroker, label: "RabbitMQ" }, - ClientRule { leaf: "amqp.publish", kind: ExternalServiceKind::MessageBroker, label: "AMQP" }, - ClientRule { leaf: "sqs.send_message", kind: ExternalServiceKind::MessageBroker, label: "AWS SQS" }, - ClientRule { leaf: "sns.publish", kind: ExternalServiceKind::MessageBroker, label: "AWS SNS" }, - + ClientRule { + leaf: "kafka.send", + kind: ExternalServiceKind::MessageBroker, + label: "Kafka", + }, + ClientRule { + leaf: "KafkaProducer.send", + kind: ExternalServiceKind::MessageBroker, + label: "Kafka", + }, + ClientRule { + leaf: "rabbitmq.publish", + kind: ExternalServiceKind::MessageBroker, + label: "RabbitMQ", + }, + ClientRule { + leaf: "amqp.publish", + kind: ExternalServiceKind::MessageBroker, + label: "AMQP", + }, + ClientRule { + leaf: "sqs.send_message", + kind: ExternalServiceKind::MessageBroker, + label: "AWS SQS", + }, + ClientRule { + leaf: "sns.publish", + kind: ExternalServiceKind::MessageBroker, + label: "AWS SNS", + }, // Search indices - ClientRule { leaf: "Elasticsearch", kind: ExternalServiceKind::SearchIndex, label: "Elasticsearch" }, - ClientRule { leaf: "elasticsearch.search", kind: ExternalServiceKind::SearchIndex, label: "Elasticsearch" }, - ClientRule { leaf: "OpenSearch", kind: ExternalServiceKind::SearchIndex, label: "OpenSearch" }, - ClientRule { leaf: "Algolia", kind: ExternalServiceKind::SearchIndex, label: "Algolia" }, - + ClientRule { + leaf: "Elasticsearch", + kind: ExternalServiceKind::SearchIndex, + label: "Elasticsearch", + }, + ClientRule { + leaf: "elasticsearch.search", + kind: ExternalServiceKind::SearchIndex, + label: "Elasticsearch", + }, + ClientRule { + leaf: "OpenSearch", + kind: ExternalServiceKind::SearchIndex, + label: "OpenSearch", + }, + ClientRule { + leaf: "Algolia", + kind: ExternalServiceKind::SearchIndex, + label: "Algolia", + }, // Auth providers - ClientRule { leaf: "auth0", kind: ExternalServiceKind::AuthProvider, label: "Auth0" }, - ClientRule { leaf: "passport.authenticate", kind: ExternalServiceKind::AuthProvider, label: "Passport.js" }, - ClientRule { leaf: "OAuth2Client", kind: ExternalServiceKind::AuthProvider, label: "OAuth2 client" }, - ClientRule { leaf: "google.oauth2", kind: ExternalServiceKind::AuthProvider, label: "Google OAuth2" }, - + ClientRule { + leaf: "auth0", + kind: ExternalServiceKind::AuthProvider, + label: "Auth0", + }, + ClientRule { + leaf: "passport.authenticate", + kind: ExternalServiceKind::AuthProvider, + label: "Passport.js", + }, + ClientRule { + leaf: "OAuth2Client", + kind: ExternalServiceKind::AuthProvider, + label: "OAuth2 client", + }, + ClientRule { + leaf: "google.oauth2", + kind: ExternalServiceKind::AuthProvider, + label: "Google OAuth2", + }, // SMTP - ClientRule { leaf: "smtplib.SMTP", kind: ExternalServiceKind::HttpApi, label: "SMTP (Python)" }, - ClientRule { leaf: "Mail::send", kind: ExternalServiceKind::HttpApi, label: "Laravel Mail" }, - ClientRule { leaf: "ActionMailer", kind: ExternalServiceKind::HttpApi, label: "Rails ActionMailer" }, - + ClientRule { + leaf: "smtplib.SMTP", + kind: ExternalServiceKind::HttpApi, + label: "SMTP (Python)", + }, + ClientRule { + leaf: "Mail::send", + kind: ExternalServiceKind::HttpApi, + label: "Laravel Mail", + }, + ClientRule { + leaf: "ActionMailer", + kind: ExternalServiceKind::HttpApi, + label: "Rails ActionMailer", + }, // DNS - 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" }, - + 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" }, + 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 @@ -109,10 +335,11 @@ const CLIENT_RULES: &[ClientRule] = &[ /// 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(); + let mut seen: std::collections::HashSet<(String, 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()); + let typed = summaries + .get_ssa(key) + .map(|s| s.typed_call_receivers.as_slice()); for callee in &summary.callees { let rule = match_rule(&callee.name).or_else(|| { typed @@ -161,7 +388,10 @@ fn qualify(container: &str, callee_name: &str) -> String { } fn container_for_ordinal(typed: &[(u32, String)], ordinal: u32) -> Option<&str> { - typed.iter().find(|(o, _)| *o == ordinal).map(|(_, c)| c.as_str()) + typed + .iter() + .find(|(o, _)| *o == ordinal) + .map(|(_, c)| c.as_str()) } fn match_rule(callee: &str) -> Option<&'static ClientRule> { diff --git a/src/surface/lang/common.rs b/src/surface/lang/common.rs index 22ef07da..a97d72b4 100644 --- a/src/surface/lang/common.rs +++ b/src/surface/lang/common.rs @@ -106,10 +106,7 @@ pub fn python_imports_any(bytes: &[u8], modules: &[&str]) -> bool { let pkg = if let Some(rest) = line.strip_prefix("from ") { rest.split_whitespace().next().unwrap_or("") } else if let Some(rest) = line.strip_prefix("import ") { - rest.split([',', ' ', ';']) - .next() - .unwrap_or("") - .trim() + rest.split([',', ' ', ';']).next().unwrap_or("").trim() } else { continue; }; @@ -237,7 +234,10 @@ mod tests { #[test] fn leaf_matches_handles_dot_and_colon_paths() { - assert!(leaf_matches("flask_login.login_required", &["login_required"])); + assert!(leaf_matches( + "flask_login.login_required", + &["login_required"] + )); assert!(leaf_matches("Auth::JwtRequired", &["JwtRequired"])); assert!(!leaf_matches("OtherDecorator", &["login_required"])); } @@ -246,7 +246,10 @@ mod tests { fn python_imports_any_matches_actual_imports() { assert!(python_imports_any(b"from flask import Flask\n", &["flask"])); assert!(python_imports_any(b"import flask\n", &["flask"])); - assert!(python_imports_any(b"from flask.app import Flask\n", &["flask"])); + assert!(python_imports_any( + b"from flask.app import Flask\n", + &["flask"] + )); assert!(python_imports_any(b"import django.urls\n", &["django"])); // Comment-only mention must not match. assert!(!python_imports_any(b"# flask is great\n", &["flask"])); @@ -260,10 +263,7 @@ mod tests { fn rust_uses_any_matches_use_statements() { assert!(rust_uses_any(b"use actix_web::web;\n", &["actix_web"])); assert!(rust_uses_any(b"use actix_web;\n", &["actix_web"])); - assert!(rust_uses_any( - b"pub use axum::Router;\n", - &["axum"] - )); + assert!(rust_uses_any(b"pub use axum::Router;\n", &["axum"])); assert!(rust_uses_any( b"pub(crate) use axum::extract::Path;\n", &["axum"] diff --git a/src/surface/lang/go_gin.rs b/src/surface/lang/go_gin.rs index a2614964..db27c4ba 100644 --- a/src/surface/lang/go_gin.rs +++ b/src/surface/lang/go_gin.rs @@ -21,8 +21,8 @@ use tree_sitter::{Node, Tree}; pub use crate::auth_analysis::auth_markers::GIN_MIDDLEWARES as AUTH_MIDDLEWARES; const VERBS: &[&str] = &[ - "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "Any", - "Get", "Post", "Put", "Delete", "Patch", "Options", "Head", + "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "Any", "Get", "Post", "Put", + "Delete", "Patch", "Options", "Head", ]; pub fn detect_gin_routes( @@ -73,7 +73,9 @@ fn match_gin_call(call: Node, bytes: &[u8], file_rel: &str) -> Option bool { } if let Some(name) = annotation_name(ann, bytes) { let leaf = name.rsplit('.').next().unwrap_or(&name); - if AUTH_ANNOTATIONS.iter().any(|a| leaf.eq_ignore_ascii_case(a)) { + if AUTH_ANNOTATIONS + .iter() + .any(|a| leaf.eq_ignore_ascii_case(a)) + { return true; } } @@ -149,7 +152,11 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool { false } -fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpMethod, String, bool)> { +fn method_mapping( + method: Node, + bytes: &[u8], + class_path: &str, +) -> Option<(HttpMethod, String, bool)> { let modifiers = crate::surface::lang::common::child_or_named(method, "modifiers")?; let mut cursor = modifiers.walk(); let mut verb: Option = None; @@ -163,7 +170,10 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM continue; }; let leaf = name.rsplit('.').next().unwrap_or(&name); - if let Some((_, m)) = JAXRS_VERBS.iter().find(|(n, _)| n.eq_ignore_ascii_case(leaf)) { + if let Some((_, m)) = JAXRS_VERBS + .iter() + .find(|(n, _)| n.eq_ignore_ascii_case(leaf)) + { verb = Some(*m); } if leaf == "Path" @@ -171,7 +181,10 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM { method_path = p; } - if AUTH_ANNOTATIONS.iter().any(|a| leaf.eq_ignore_ascii_case(a)) { + if AUTH_ANNOTATIONS + .iter() + .any(|a| leaf.eq_ignore_ascii_case(a)) + { auth = true; } } @@ -181,7 +194,11 @@ fn method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpM } else if method_path.is_empty() { class_path.to_string() } else { - format!("{}/{}", class_path.trim_end_matches('/'), method_path.trim_start_matches('/')) + format!( + "{}/{}", + class_path.trim_end_matches('/'), + method_path.trim_start_matches('/') + ) }; Some((v, combined, auth)) } @@ -258,7 +275,8 @@ public class GreetResource { } "#; let (tree, bytes) = parse(src); - let nodes = detect_quarkus_routes(&tree, &bytes, &PathBuf::from("GreetResource.java"), None); + let nodes = + detect_quarkus_routes(&tree, &bytes, &PathBuf::from("GreetResource.java"), None); assert_eq!(nodes.len(), 1); let SurfaceNode::EntryPoint(ep) = &nodes[0] else { panic!() diff --git a/src/surface/lang/java_servlet.rs b/src/surface/lang/java_servlet.rs index 1a48e42a..00a0f9f0 100644 --- a/src/surface/lang/java_servlet.rs +++ b/src/surface/lang/java_servlet.rs @@ -139,7 +139,10 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool { } if let Some(name) = annotation_name(ann, bytes) && AUTH_ANNOTATIONS.iter().any(|a| { - name.rsplit('.').next().unwrap_or(&name).eq_ignore_ascii_case(a) + name.rsplit('.') + .next() + .unwrap_or(&name) + .eq_ignore_ascii_case(a) }) { return true; @@ -148,7 +151,11 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool { false } -fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option<(HttpMethod, String, bool)> { +fn jaxrs_method_mapping( + method: Node, + bytes: &[u8], + class_path: &str, +) -> Option<(HttpMethod, String, bool)> { let modifiers = crate::surface::lang::common::child_or_named(method, "modifiers")?; let mut cursor = modifiers.walk(); let mut verb: Option = None; @@ -162,7 +169,10 @@ fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option< continue; }; let leaf = name.rsplit('.').next().unwrap_or(&name); - if let Some((_, m)) = JAXRS_VERBS.iter().find(|(n, _)| n.eq_ignore_ascii_case(leaf)) { + if let Some((_, m)) = JAXRS_VERBS + .iter() + .find(|(n, _)| n.eq_ignore_ascii_case(leaf)) + { verb = Some(*m); } if leaf == "Path" @@ -183,7 +193,11 @@ fn jaxrs_method_mapping(method: Node, bytes: &[u8], class_path: &str) -> Option< } else if method_path.is_empty() { class_path.to_string() } else { - format!("{}/{}", class_path.trim_end_matches('/'), method_path.trim_start_matches('/')) + format!( + "{}/{}", + class_path.trim_end_matches('/'), + method_path.trim_start_matches('/') + ) }; Some((v, combined, auth)) } @@ -255,7 +269,8 @@ public class UsersResource { } "#; let (tree, bytes) = parse(src); - let nodes = detect_servlet_routes(&tree, &bytes, &PathBuf::from("UsersResource.java"), None); + let nodes = + detect_servlet_routes(&tree, &bytes, &PathBuf::from("UsersResource.java"), None); assert!(!nodes.is_empty()); let SurfaceNode::EntryPoint(ep) = &nodes[0] else { panic!() diff --git a/src/surface/lang/java_spring.rs b/src/surface/lang/java_spring.rs index 9d85379a..03f4479b 100644 --- a/src/surface/lang/java_spring.rs +++ b/src/surface/lang/java_spring.rs @@ -46,9 +46,7 @@ pub fn detect_spring_routes( if member.kind() != "method_declaration" { continue; } - if let Some((method, route_path, auth)) = - method_mapping(member, bytes, &class_path) - { + if let Some((method, route_path, auth)) = method_mapping(member, bytes, &class_path) { let auth_required = class_auth || auth; let handler_name = method_name(member, bytes).unwrap_or_default(); out.push(SurfaceNode::EntryPoint(EntryPoint { @@ -114,9 +112,7 @@ fn class_has_auth_annotation(class: Node, bytes: &[u8]) -> bool { continue; } if let Some((name, _)) = annotation_name_and_args(ann, bytes) - && AUTH_ANNOTATIONS - .iter() - .any(|a| leaf_matches(&name, &[a])) + && AUTH_ANNOTATIONS.iter().any(|a| leaf_matches(&name, &[a])) { return true; } @@ -140,10 +136,7 @@ fn method_mapping( let Some((name, args_text)) = annotation_name_and_args(ann, bytes) else { continue; }; - if AUTH_ANNOTATIONS - .iter() - .any(|a| leaf_matches(&name, &[a])) - { + if AUTH_ANNOTATIONS.iter().any(|a| leaf_matches(&name, &[a])) { auth = true; } if found.is_some() { @@ -156,7 +149,11 @@ fn method_mapping( // Class-only mapping; method has no path. method_route = class_path.to_string(); } else if !class_path.is_empty() { - method_route = format!("{}/{}", class_path.trim_end_matches('/'), method_route.trim_start_matches('/')); + method_route = format!( + "{}/{}", + class_path.trim_end_matches('/'), + method_route.trim_start_matches('/') + ); } let method = default_method .or_else(|| extract_request_method_from_args(&args_text)) @@ -171,10 +168,7 @@ fn method_mapping( } fn is_annotation(node: Node) -> bool { - matches!( - node.kind(), - "annotation" | "marker_annotation" - ) + matches!(node.kind(), "annotation" | "marker_annotation") } /// Returns `(annotation_name, raw_args_text)` for an annotation node. @@ -253,7 +247,8 @@ public class UserController { } "#; let (tree, bytes) = parse(src); - let nodes = detect_spring_routes(&tree, &bytes, &PathBuf::from("UserController.java"), None); + let nodes = + detect_spring_routes(&tree, &bytes, &PathBuf::from("UserController.java"), None); assert_eq!(nodes.len(), 1); let SurfaceNode::EntryPoint(ep) = &nodes[0] else { panic!() diff --git a/src/surface/lang/js_express.rs b/src/surface/lang/js_express.rs index 725891a5..791e05c1 100644 --- a/src/surface/lang/js_express.rs +++ b/src/surface/lang/js_express.rs @@ -153,10 +153,7 @@ fn arg_is_auth_marker(node: Node, bytes: &[u8]) -> bool { fn receiver_is_express(object: Node, bytes: &[u8], has_express_witness: bool) -> bool { fn name_matches_strong(text: &str) -> bool { let lower = text.to_ascii_lowercase(); - lower == "app" - || lower == "server" - || lower.ends_with("_app") - || lower.ends_with("api") + lower == "app" || lower == "server" || lower.ends_with("_app") || lower.ends_with("api") } fn name_matches_router(text: &str) -> bool { let lower = text.to_ascii_lowercase(); @@ -239,7 +236,10 @@ mod tests { let src = "const Router = require('@koa/router');\nconst router = new Router();\nrouter.get('/users', async ctx => {});\n"; let (tree, bytes) = parse(src); let nodes = detect_express_routes(&tree, &bytes, &PathBuf::from("server.js"), None); - assert!(nodes.is_empty(), "express probe FP'd on koa-only file: {nodes:?}"); + assert!( + nodes.is_empty(), + "express probe FP'd on koa-only file: {nodes:?}" + ); } #[test] diff --git a/src/surface/lang/mod.rs b/src/surface/lang/mod.rs index 864ea3b5..243a317c 100644 --- a/src/surface/lang/mod.rs +++ b/src/surface/lang/mod.rs @@ -12,26 +12,26 @@ pub mod common; -pub mod python_flask; -pub mod python_fastapi; pub mod python_django; +pub mod python_fastapi; +pub mod python_flask; pub mod js_express; pub mod js_koa; pub mod ts_next; -pub mod java_spring; -pub mod java_servlet; pub mod java_quarkus; +pub mod java_servlet; +pub mod java_spring; -pub mod go_http; pub mod go_gin; +pub mod go_http; pub mod php_laravel; pub mod php_slim; -pub mod ruby_sinatra; pub mod ruby_rails; +pub mod ruby_sinatra; pub mod rust_actix; pub mod rust_axum; diff --git a/src/surface/lang/php_laravel.rs b/src/surface/lang/php_laravel.rs index 924ca3d5..3e172384 100644 --- a/src/surface/lang/php_laravel.rs +++ b/src/surface/lang/php_laravel.rs @@ -119,7 +119,9 @@ fn check_chained_middleware(call: Node, bytes: &[u8]) -> bool { && name_text == "middleware" && let Some(args) = p.child_by_field_name("arguments") && let Ok(args_text) = args.utf8_text(bytes) - && (args_text.contains("auth") || args_text.contains("jwt") || args_text.contains("authenticated")) + && (args_text.contains("auth") + || args_text.contains("jwt") + || args_text.contains("authenticated")) { return true; } diff --git a/src/surface/lang/python_django.rs b/src/surface/lang/python_django.rs index c81226b4..ea8d68f9 100644 --- a/src/surface/lang/python_django.rs +++ b/src/surface/lang/python_django.rs @@ -60,7 +60,13 @@ pub fn detect_django_routes( let file_rel = rel_file(path, scan_root); let mut out = Vec::new(); let function_index = collect_function_definitions(tree.root_node(), bytes); - detect_url_dispatch(tree.root_node(), bytes, &file_rel, &function_index, &mut out); + detect_url_dispatch( + tree.root_node(), + bytes, + &file_rel, + &function_index, + &mut out, + ); detect_class_based_views(tree.root_node(), bytes, &file_rel, &mut out); out } @@ -178,16 +184,9 @@ fn parse_url_call(call: Node, bytes: &[u8]) -> Option<(String, String)> { Some((route?, handler?)) } -fn detect_class_based_views( - root: Node, - bytes: &[u8], - file_rel: &str, - out: &mut Vec, -) { +fn detect_class_based_views(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec) { fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec) { - if node.kind() == "class_definition" - && class_is_django_view(node, bytes) - { + if node.kind() == "class_definition" && class_is_django_view(node, bytes) { let class_auth = class_has_auth_permission(node, bytes); // Walk the body for HTTP-named methods. if let Some(body) = node.child_by_field_name("body") { diff --git a/src/surface/lang/python_flask.rs b/src/surface/lang/python_flask.rs index acfb3b05..6e38e79b 100644 --- a/src/surface/lang/python_flask.rs +++ b/src/surface/lang/python_flask.rs @@ -17,9 +17,7 @@ use crate::entry_points::HttpMethod; use crate::surface::lang::common::python_imports_any; -use crate::surface::{ - EntryPoint, Framework, SourceLocation, SurfaceNode, relative_path_string, -}; +use crate::surface::{EntryPoint, Framework, SourceLocation, SurfaceNode, relative_path_string}; use std::path::Path; use tree_sitter::{Node, Tree}; @@ -273,9 +271,7 @@ fn decorator_is_auth_marker(decorator: Node, bytes: &[u8]) -> bool { return false; }; let leaf = text.rsplit('.').next().unwrap_or(text).trim(); - AUTH_DECORATORS - .iter() - .any(|d| leaf.eq_ignore_ascii_case(d)) + AUTH_DECORATORS.iter().any(|d| leaf.eq_ignore_ascii_case(d)) } /// Read the function name from a `function_definition` node. diff --git a/src/surface/lang/ruby_rails.rs b/src/surface/lang/ruby_rails.rs index cc2d8147..8e58321a 100644 --- a/src/surface/lang/ruby_rails.rs +++ b/src/surface/lang/ruby_rails.rs @@ -42,37 +42,35 @@ fn detect_routes_dsl(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec) { if matches!(node.kind(), "call" | "method_call") && let Some(method_node) = node.child_by_field_name("method") - && let Ok(method_text) = method_node.utf8_text(bytes) - && let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text) - { - let args_opt = node - .child_by_field_name("arguments") - .or_else(|| { - let mut c = node.walk(); - node.children(&mut c).find(|n| n.kind() == "argument_list") - }); - if let Some(args) = args_opt { - let mut cursor = args.walk(); - let positional: Vec = args.named_children(&mut cursor).collect(); - if let Some(route_node) = positional.first() - && let Some(route) = string_node_value(*route_node, bytes) - { - let handler_name = positional - .iter() - .find_map(|n| extract_to_handler(*n, bytes)) - .unwrap_or_default(); - out.push(SurfaceNode::EntryPoint(EntryPoint { - location: loc_for(node, file_rel), - framework: Framework::Rails, - method: *method, - route, - handler_name, - handler_location: loc_for(node, file_rel), - auth_required: false, - })); - } + && let Ok(method_text) = method_node.utf8_text(bytes) + && let Some((_, method)) = VERBS.iter().find(|(v, _)| *v == method_text) + { + let args_opt = node.child_by_field_name("arguments").or_else(|| { + let mut c = node.walk(); + node.children(&mut c).find(|n| n.kind() == "argument_list") + }); + if let Some(args) = args_opt { + let mut cursor = args.walk(); + let positional: Vec = args.named_children(&mut cursor).collect(); + if let Some(route_node) = positional.first() + && let Some(route) = string_node_value(*route_node, bytes) + { + let handler_name = positional + .iter() + .find_map(|n| extract_to_handler(*n, bytes)) + .unwrap_or_default(); + out.push(SurfaceNode::EntryPoint(EntryPoint { + location: loc_for(node, file_rel), + framework: Framework::Rails, + method: *method, + route, + handler_name, + handler_location: loc_for(node, file_rel), + auth_required: false, + })); } } + } let mut cursor = node.walk(); for child in node.children(&mut cursor) { recurse(child, bytes, file_rel, out); @@ -109,9 +107,7 @@ fn extract_to_handler(node: Node, bytes: &[u8]) -> Option { fn detect_controllers(root: Node, bytes: &[u8], file_rel: &str, out: &mut Vec) { fn recurse(node: Node, bytes: &[u8], file_rel: &str, out: &mut Vec) { - if node.kind() == "class" - && class_is_controller(node, bytes) - { + if node.kind() == "class" && class_is_controller(node, bytes) { let class_auth = class_has_before_authenticate(node, bytes); walk_methods(node, bytes, &mut |method_node, name| { out.push(SurfaceNode::EntryPoint(EntryPoint { diff --git a/src/surface/lang/ruby_sinatra.rs b/src/surface/lang/ruby_sinatra.rs index 8a083099..1623c344 100644 --- a/src/surface/lang/ruby_sinatra.rs +++ b/src/surface/lang/ruby_sinatra.rs @@ -50,24 +50,18 @@ fn walk_calls<'tree, F: FnMut(Node<'tree>)>(node: Node<'tree>, visit: &mut F) { fn match_sinatra_call(call: Node, bytes: &[u8], file_rel: &str) -> Option { let method_name_node = call.child_by_field_name("method")?; let method_text = method_name_node.utf8_text(bytes).ok()?; - let (_, method) = VERBS - .iter() - .find(|(v, _)| *v == method_text)?; + let (_, method) = VERBS.iter().find(|(v, _)| *v == method_text)?; // Must have a block to be a Sinatra route. - let block = call - .child_by_field_name("block") - .or_else(|| { - let mut c = call.walk(); - call.children(&mut c) - .find(|n| matches!(n.kind(), "do_block" | "block")) - })?; + let block = call.child_by_field_name("block").or_else(|| { + let mut c = call.walk(); + call.children(&mut c) + .find(|n| matches!(n.kind(), "do_block" | "block")) + })?; // Args: Sinatra accepts a string literal as the first positional arg. - let args = call - .child_by_field_name("arguments") - .or_else(|| { - let mut c = call.walk(); - call.children(&mut c).find(|n| n.kind() == "argument_list") - })?; + let args = call.child_by_field_name("arguments").or_else(|| { + let mut c = call.walk(); + call.children(&mut c).find(|n| n.kind() == "argument_list") + })?; let mut cursor = args.walk(); let route_node = args.named_children(&mut cursor).next()?; let route = string_node_value(route_node, bytes)?; diff --git a/src/surface/lang/rust_actix.rs b/src/surface/lang/rust_actix.rs index 13a6f802..51a553b0 100644 --- a/src/surface/lang/rust_actix.rs +++ b/src/surface/lang/rust_actix.rs @@ -68,9 +68,7 @@ fn match_actix_function(func: Node, bytes: &[u8], file_rel: &str) -> Option) -> String { if let Some(root) = scan_root - && let Ok(rel) = path.strip_prefix(root) { - return rel.to_string_lossy().replace('\\', "/"); - } + && let Ok(rel) = path.strip_prefix(root) + { + return rel.to_string_lossy().replace('\\', "/"); + } path.to_string_lossy().replace('\\', "/") } diff --git a/src/surface/reachability.rs b/src/surface/reachability.rs index 89ce3535..d57b0d15 100644 --- a/src/surface/reachability.rs +++ b/src/surface/reachability.rs @@ -77,9 +77,7 @@ pub fn populate_reaches_edges( .index .iter() .filter(|(k, _)| k.name == ep.handler_name) - .filter(|(k, _)| { - file_part_of_namespace(&k.namespace) == ep.handler_location.file - }) + .filter(|(k, _)| file_part_of_namespace(&k.namespace) == ep.handler_location.file) .map(|(_, idx)| *idx) .collect::>(); @@ -217,9 +215,6 @@ mod tests { "src/file.ts" ); // Last `::` wins, matching `namespace_with_package`'s shape. - assert_eq!( - file_part_of_namespace("@a/b::@c/d::lib/x.ts"), - "lib/x.ts" - ); + assert_eq!(file_part_of_namespace("@a/b::@c/d::lib/x.ts"), "lib/x.ts"); } } diff --git a/src/symbol/mod.rs b/src/symbol/mod.rs index ae2bb6b5..cbc3d730 100644 --- a/src/symbol/mod.rs +++ b/src/symbol/mod.rs @@ -115,9 +115,10 @@ impl Lang { /// CLI entry points and other extensionless / non-canonical files. pub fn from_path_or_content(path: &Path, head_bytes: &[u8]) -> Option { if let Some(ext) = path.extension().and_then(|e| e.to_str()) - && let Some(lang) = Self::from_extension(ext) { - return Some(lang); - } + && let Some(lang) = Self::from_extension(ext) + { + return Some(lang); + } if let Some(lang) = lang_from_shebang(head_bytes) { return Some(lang); } @@ -352,10 +353,7 @@ fn lang_from_shebang(head: &[u8]) -> Option { return None; } let cap = head.len().min(SNIFF_HEAD_LIMIT); - let line_end = head[..cap] - .iter() - .position(|&b| b == b'\n') - .unwrap_or(cap); + let line_end = head[..cap].iter().position(|&b| b == b'\n').unwrap_or(cap); let line = std::str::from_utf8(&head[..line_end]).ok()?; let line = line.trim_end_matches('\r').trim(); let rest = line.strip_prefix("#!")?.trim(); diff --git a/src/utils/redact.rs b/src/utils/redact.rs index f4e31b57..f61cf76b 100644 --- a/src/utils/redact.rs +++ b/src/utils/redact.rs @@ -74,16 +74,25 @@ static PATTERNS: &[Pattern] = &[ // AWS access key IDs: AKIA[A-Z0-9]{16} Pattern { prefix: "AKIA", - replace_fn: |s| replace_pattern(s, |c: &str| { - if let Some(start) = c.find("AKIA") { - let rest = &c[start + 4..]; - let end = rest.find(|ch: char| !ch.is_ascii_alphanumeric()).unwrap_or(rest.len()); - if end >= 12 { - return true; - } - } - false - }, "AKIA", 20), + replace_fn: |s| { + replace_pattern( + s, + |c: &str| { + if let Some(start) = c.find("AKIA") { + let rest = &c[start + 4..]; + let end = rest + .find(|ch: char| !ch.is_ascii_alphanumeric()) + .unwrap_or(rest.len()); + if end >= 12 { + return true; + } + } + false + }, + "AKIA", + 20, + ) + }, matches_fn: |s| akia_matches(s), }, // GitHub personal access tokens: ghp_, github_pat_, ghs_, ghr_ @@ -255,7 +264,9 @@ fn replace_pem_blocks(s: &str) -> String { fn akia_matches(s: &str) -> bool { if let Some(pos) = s.find("AKIA") { let rest = &s[pos + 4..]; - let end = rest.find(|ch: char| !ch.is_ascii_alphanumeric()).unwrap_or(rest.len()); + let end = rest + .find(|ch: char| !ch.is_ascii_alphanumeric()) + .unwrap_or(rest.len()); return end >= 12; } false @@ -266,7 +277,9 @@ fn contains_sk_token(s: &str) -> bool { let mut rest = s; while let Some(pos) = rest.find("sk-") { let after = &rest[pos + 3..]; - let end = after.find(|ch: char| !ch.is_ascii_alphanumeric() && ch != '-').unwrap_or(after.len()); + let end = after + .find(|ch: char| !ch.is_ascii_alphanumeric() && ch != '-') + .unwrap_or(after.len()); if end >= 20 { return true; } @@ -285,7 +298,9 @@ fn replace_pattern( let mut rest = s; while let Some(pos) = rest.find(prefix) { let after = &rest[pos + prefix.len()..]; - let end = after.find(|ch: char| !ch.is_ascii_alphanumeric()).unwrap_or(after.len()); + let end = after + .find(|ch: char| !ch.is_ascii_alphanumeric()) + .unwrap_or(after.len()); if end >= token_len - prefix.len() { out.push_str(&rest[..pos]); out.push_str(""); @@ -307,7 +322,10 @@ mod tests { fn redacts_aws_key() { let input = "key: AKIAFAKETEST00000000 in config"; let out = redact_str(input); - assert!(!out.contains("AKIAFAKETEST00000000"), "AWS key must be redacted"); + assert!( + !out.contains("AKIAFAKETEST00000000"), + "AWS key must be redacted" + ); assert!(out.contains("")); } @@ -338,7 +356,10 @@ mod tests { fn passthrough_clean_bytes() { let input = b"\x80\x81 normal text here"; let out = redact(input); - assert!(out.windows(b"normal text".len()).any(|w| w == b"normal text")); + assert!( + out.windows(b"normal text".len()) + .any(|w| w == b"normal text") + ); } #[test] @@ -349,7 +370,8 @@ mod tests { #[test] fn redacts_pem_block() { - let input = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQ\n-----END RSA PRIVATE KEY-----"; + let input = + "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQ\n-----END RSA PRIVATE KEY-----"; let out = redact_str(input); assert!(!out.contains("MIIEowIBAAKCAQ")); assert!(out.contains("")); diff --git a/tests/c_fixtures.rs b/tests/c_fixtures.rs index 19e52e37..d5e39426 100644 --- a/tests/c_fixtures.rs +++ b/tests/c_fixtures.rs @@ -15,7 +15,7 @@ mod common; #[cfg(feature = "dynamic")] mod c_fixture_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -64,7 +64,16 @@ mod c_fixture_tests { slot: PayloadSlot, ) -> Option { run_shape_fixture_lang_or_skip( - CC_REQ, Lang::C, "c", shape, file, func, cap, sink_line, kind, slot, + CC_REQ, + Lang::C, + "c", + shape, + file, + func, + cap, + sink_line, + kind, + slot, ) } @@ -73,18 +82,32 @@ mod c_fixture_tests { #[test] fn main_argv_vuln_is_confirmed() { let Some(r) = run( - "main_argv", "vuln.c", "nyx_entry_main", Cap::CODE_EXEC, 23, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), - ) else { return; }; + "main_argv", + "vuln.c", + "nyx_entry_main", + Cap::CODE_EXEC, + 23, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), + ) else { + return; + }; assert_confirmed("main_argv", &r); } #[test] fn main_argv_benign_not_confirmed() { let Some(r) = run( - "main_argv", "benign.c", "nyx_entry_main", Cap::CODE_EXEC, 11, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), - ) else { return; }; + "main_argv", + "benign.c", + "nyx_entry_main", + Cap::CODE_EXEC, + 11, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), + ) else { + return; + }; assert_not_confirmed("main_argv", &r); } @@ -93,18 +116,32 @@ mod c_fixture_tests { #[test] fn libfuzzer_vuln_is_confirmed() { let Some(r) = run( - "libfuzzer", "vuln.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 16, - EntryKind::LibraryApi, PayloadSlot::Param(0), - ) else { return; }; + "libfuzzer", + "vuln.c", + "LLVMFuzzerTestOneInput", + Cap::CODE_EXEC, + 16, + EntryKind::LibraryApi, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("libfuzzer", &r); } #[test] fn libfuzzer_benign_not_confirmed() { let Some(r) = run( - "libfuzzer", "benign.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10, - EntryKind::LibraryApi, PayloadSlot::Param(0), - ) else { return; }; + "libfuzzer", + "benign.c", + "LLVMFuzzerTestOneInput", + Cap::CODE_EXEC, + 10, + EntryKind::LibraryApi, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("libfuzzer", &r); } @@ -113,18 +150,32 @@ mod c_fixture_tests { #[test] fn free_fn_vuln_is_confirmed() { let Some(r) = run( - "free_fn", "vuln.c", "run", Cap::CODE_EXEC, 15, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "free_fn", + "vuln.c", + "run", + Cap::CODE_EXEC, + 15, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("free_fn", &r); } #[test] fn free_fn_benign_not_confirmed() { let Some(r) = run( - "free_fn", "benign.c", "run", Cap::CODE_EXEC, 10, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "free_fn", + "benign.c", + "run", + Cap::CODE_EXEC, + 10, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("free_fn", &r); } } diff --git a/tests/chain_edges.rs b/tests/chain_edges.rs index 05e80301..bbfe1918 100644 --- a/tests/chain_edges.rs +++ b/tests/chain_edges.rs @@ -82,10 +82,7 @@ fn single_edge(diag: Diag, surface: &SurfaceMap) -> ChainEdge { #[test] fn rule_cmdi_alone_maps_to_rce() { let surface = synthetic_surface("app.py", "/run"); - let edge = single_edge( - diag_with_caps("app.py", 12, Cap::CODE_EXEC), - &surface, - ); + let edge = single_edge(diag_with_caps("app.py", 12, Cap::CODE_EXEC), &surface); assert_eq!(edge.primary_cap, Cap::CODE_EXEC); assert!(matches!(edge.reach, Reach::Reachable { .. })); assert_eq!( @@ -97,10 +94,7 @@ fn rule_cmdi_alone_maps_to_rce() { #[test] fn rule_deserialize_alone_maps_to_rce() { let surface = synthetic_surface("app.py", "/load"); - let edge = single_edge( - diag_with_caps("app.py", 7, Cap::DESERIALIZE), - &surface, - ); + let edge = single_edge(diag_with_caps("app.py", 7, Cap::DESERIALIZE), &surface); assert_eq!(edge.primary_cap, Cap::DESERIALIZE); assert_eq!( lookup_impact(edge.primary_cap, None), @@ -111,10 +105,7 @@ fn rule_deserialize_alone_maps_to_rce() { #[test] fn rule_ssrf_alone_maps_to_internal_network_access() { let surface = synthetic_surface("fetch.py", "/proxy"); - let edge = single_edge( - diag_with_caps("fetch.py", 4, Cap::SSRF), - &surface, - ); + let edge = single_edge(diag_with_caps("fetch.py", 4, Cap::SSRF), &surface); assert_eq!(edge.primary_cap, Cap::SSRF); assert_eq!( lookup_impact(edge.primary_cap, None), @@ -186,9 +177,6 @@ fn finding_in_file_with_no_entry_point_is_unreachable() { #[test] fn feasibility_defaults_to_unverified() { let surface = synthetic_surface("app.py", "/"); - let edge = single_edge( - diag_with_caps("app.py", 1, Cap::CODE_EXEC), - &surface, - ); + let edge = single_edge(diag_with_caps("app.py", 1, Cap::CODE_EXEC), &surface); assert_eq!(edge.feasibility, Feasibility::Unverified); } diff --git a/tests/chain_emission.rs b/tests/chain_emission.rs index 762282e8..9501c2ce 100644 --- a/tests/chain_emission.rs +++ b/tests/chain_emission.rs @@ -88,7 +88,12 @@ fn fixture_findings() -> Vec { d }; vec![ - mk(10, "cfg-cors-allow-all", Cap::HEADER_INJECTION, Severity::Medium), + mk( + 10, + "cfg-cors-allow-all", + Cap::HEADER_INJECTION, + Severity::Medium, + ), mk(15, "cfg-auth-gap", Cap::UNAUTHORIZED_ID, Severity::Medium), mk(25, "taint-shell-exec", Cap::CODE_EXEC, Severity::High), ] @@ -129,7 +134,11 @@ fn cors_plus_noauth_plus_websocket_emits_one_critical_chain() { min_score: 0.0, }, ); - assert_eq!(chains.len(), 1, "expected exactly one chain, got {chains:?}"); + assert_eq!( + chains.len(), + 1, + "expected exactly one chain, got {chains:?}" + ); let chain = &chains[0]; assert_eq!(chain.implied_impact, ImpactCategory::BrowserToLocalRce); assert_eq!(chain.severity, ChainSeverity::Critical); @@ -213,11 +222,7 @@ fn sarif_output_validates_against_v210_shape() { min_score: 0.0, }, ); - let sarif = build_sarif_with_chains( - &findings, - &chains, - std::path::Path::new("."), - ); + let sarif = build_sarif_with_chains(&findings, &chains, std::path::Path::new(".")); // Surface-level v2.1.0 invariants — the SARIF schema requires // these fields and we want a tripwire if any disappear. diff --git a/tests/chain_emission_e2e.rs b/tests/chain_emission_e2e.rs index 432e698d..e7fc890c 100644 --- a/tests/chain_emission_e2e.rs +++ b/tests/chain_emission_e2e.rs @@ -311,8 +311,8 @@ fn flask_eval_chain_dynamic_verdict_is_null_when_verify_disabled() { .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 value: Value = + serde_json::from_str(&stdout).expect("nyx scan --format json produced invalid JSON"); let chains = value .get("chains") diff --git a/tests/chain_reverify.rs b/tests/chain_reverify.rs index 3e0ef1f2..77a47361 100644 --- a/tests/chain_reverify.rs +++ b/tests/chain_reverify.rs @@ -193,8 +193,14 @@ fn compose_chain_step_threads_prev_output_for_every_emitter() { "{lang:?} emitter must thread NYX_PREV_OUTPUT via extra_env; got {:?}", step.extra_env ); - assert!(!step.source.is_empty(), "{lang:?} step source must be non-empty"); - assert!(!step.command.is_empty(), "{lang:?} step command must be non-empty"); + assert!( + !step.source.is_empty(), + "{lang:?} step source must be non-empty" + ); + assert!( + !step.command.is_empty(), + "{lang:?} step command must be non-empty" + ); assert!( !step.source.contains(ChainStepHarness::SINK_HIT_SENTINEL), "{lang:?} non-terminal step must NOT carry the sink-hit sentinel; got source:\n{}", diff --git a/tests/class_method_corpus.rs b/tests/class_method_corpus.rs index 4cbc587c..47fb34e4 100644 --- a/tests/class_method_corpus.rs +++ b/tests/class_method_corpus.rs @@ -16,7 +16,7 @@ use nyx_scanner::dynamic::lang; use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot}; -use nyx_scanner::dynamic::stubs::{mock_source, MockKind}; +use nyx_scanner::dynamic::stubs::{MockKind, mock_source}; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; diff --git a/tests/cli_unsafe_sandbox.rs b/tests/cli_unsafe_sandbox.rs index 91e70dd3..c4e806fc 100644 --- a/tests/cli_unsafe_sandbox.rs +++ b/tests/cli_unsafe_sandbox.rs @@ -28,11 +28,9 @@ mod dynamic_sandbox_cli { fn unsafe_sandbox_with_docker_backend_is_rejected() { let mut cmd = scan_cmd_with_fresh_env(); cmd.args(["--unsafe-sandbox", "--backend", "docker"]); - cmd.assert() - .failure() - .stderr(predicate::str::contains( - "--unsafe-sandbox and --backend docker are mutually exclusive", - )); + cmd.assert().failure().stderr(predicate::str::contains( + "--unsafe-sandbox and --backend docker are mutually exclusive", + )); } /// `--unsafe-sandbox` alone (no explicit --backend) must NOT trigger the diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs index 0fdaf543..9f19101e 100644 --- a/tests/common/fixture_harness.rs +++ b/tests/common/fixture_harness.rs @@ -14,10 +14,10 @@ //! failure, prompting an explicit golden update. use nyx_scanner::commands::scan::Diag; -use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; +use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ - Confidence, EntryKind, Evidence, FlowStep, FlowStepKind, InconclusiveReason, - UnsupportedReason, VerifyResult, VerifyStatus, + Confidence, EntryKind, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, + VerifyResult, VerifyStatus, }; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; @@ -187,10 +187,7 @@ pub fn check_prerequisites(reqs: &[Prerequisite]) -> Result<(), SkipReason> { Err(_) => return Err(SkipReason::MissingStaticLib(lib)), }; use std::io::Write; - let mut handle = match std::fs::OpenOptions::new() - .write(true) - .open(probe.path()) - { + let mut handle = match std::fs::OpenOptions::new().write(true).open(probe.path()) { Ok(h) => h, Err(_) => return Err(SkipReason::MissingStaticLib(lib)), }; @@ -207,7 +204,9 @@ pub fn check_prerequisites(reqs: &[Prerequisite]) -> Result<(), SkipReason> { }; let status = std::process::Command::new("cc") .args([ - "-x", "c", "-static", + "-x", + "c", + "-static", probe.path().to_str().unwrap_or(""), "-o", out.to_str().unwrap_or(""), @@ -327,9 +326,8 @@ pub fn run_fixture_and_compare_to_golden(spec: &FixtureSpec<'_>) { current_json.push('\n'); if std::env::var("NYX_UPDATE_GOLDENS").is_ok_and(|v| v == "1") { - std::fs::write(&golden_path, ¤t_json).unwrap_or_else(|e| { - panic!("write golden {}: {e}", golden_path.display()) - }); + std::fs::write(&golden_path, ¤t_json) + .unwrap_or_else(|e| panic!("write golden {}: {e}", golden_path.display())); return; } @@ -365,7 +363,9 @@ fn fixture_dir(lang_dir: &str) -> PathBuf { fn stage_fixture(src: &Path, tmp: &TempDir, copy: CopyStrategy) -> PathBuf { match copy { CopyStrategy::PreserveName => { - let dst = tmp.path().join(src.file_name().expect("fixture has filename")); + let dst = tmp + .path() + .join(src.file_name().expect("fixture has filename")); std::fs::copy(src, &dst).expect("copy fixture into tempdir"); dst } @@ -435,7 +435,7 @@ pub fn run_shape_fixture_lang( entry_kind: EntryKind, payload_slot: nyx_scanner::dynamic::spec::PayloadSlot, ) -> VerifyResult { - use nyx_scanner::dynamic::runner::{run_spec, RunError}; + use nyx_scanner::dynamic::runner::{RunError, run_spec}; use nyx_scanner::dynamic::sandbox::SandboxOptions; use nyx_scanner::dynamic::spec::{HarnessSpec, SpecDerivationStrategy}; @@ -801,9 +801,8 @@ pub fn run_harness_snapshot_lang( .replace(file, ""); if std::env::var("NYX_UPDATE_GOLDENS").is_ok_and(|v| v == "1") { - std::fs::write(&snapshot_path, &normalised).unwrap_or_else(|e| { - panic!("write harness snapshot {}: {e}", snapshot_path.display()) - }); + std::fs::write(&snapshot_path, &normalised) + .unwrap_or_else(|e| panic!("write harness snapshot {}: {e}", snapshot_path.display())); return; } diff --git a/tests/cpp_fixtures.rs b/tests/cpp_fixtures.rs index ee430863..3f2b1229 100644 --- a/tests/cpp_fixtures.rs +++ b/tests/cpp_fixtures.rs @@ -15,7 +15,7 @@ mod common; #[cfg(feature = "dynamic")] mod cpp_fixture_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -64,7 +64,16 @@ mod cpp_fixture_tests { slot: PayloadSlot, ) -> Option { run_shape_fixture_lang_or_skip( - CXX_REQ, Lang::Cpp, "cpp", shape, file, func, cap, sink_line, kind, slot, + CXX_REQ, + Lang::Cpp, + "cpp", + shape, + file, + func, + cap, + sink_line, + kind, + slot, ) } @@ -73,18 +82,32 @@ mod cpp_fixture_tests { #[test] fn main_argv_vuln_is_confirmed() { let Some(r) = run( - "main_argv", "vuln.cpp", "nyx_entry_main", Cap::CODE_EXEC, 16, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), - ) else { return; }; + "main_argv", + "vuln.cpp", + "nyx_entry_main", + Cap::CODE_EXEC, + 16, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), + ) else { + return; + }; assert_confirmed("main_argv", &r); } #[test] fn main_argv_benign_not_confirmed() { let Some(r) = run( - "main_argv", "benign.cpp", "nyx_entry_main", Cap::CODE_EXEC, 11, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), - ) else { return; }; + "main_argv", + "benign.cpp", + "nyx_entry_main", + Cap::CODE_EXEC, + 11, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), + ) else { + return; + }; assert_not_confirmed("main_argv", &r); } @@ -93,18 +116,32 @@ mod cpp_fixture_tests { #[test] fn libfuzzer_vuln_is_confirmed() { let Some(r) = run( - "libfuzzer", "vuln.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 15, - EntryKind::LibraryApi, PayloadSlot::Param(0), - ) else { return; }; + "libfuzzer", + "vuln.cpp", + "LLVMFuzzerTestOneInput", + Cap::CODE_EXEC, + 15, + EntryKind::LibraryApi, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("libfuzzer", &r); } #[test] fn libfuzzer_benign_not_confirmed() { let Some(r) = run( - "libfuzzer", "benign.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10, - EntryKind::LibraryApi, PayloadSlot::Param(0), - ) else { return; }; + "libfuzzer", + "benign.cpp", + "LLVMFuzzerTestOneInput", + Cap::CODE_EXEC, + 10, + EntryKind::LibraryApi, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("libfuzzer", &r); } @@ -113,18 +150,32 @@ mod cpp_fixture_tests { #[test] fn free_fn_vuln_is_confirmed() { let Some(r) = run( - "free_fn", "vuln.cpp", "run", Cap::CODE_EXEC, 12, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "free_fn", + "vuln.cpp", + "run", + Cap::CODE_EXEC, + 12, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("free_fn", &r); } #[test] fn free_fn_benign_not_confirmed() { let Some(r) = run( - "free_fn", "benign.cpp", "run", Cap::CODE_EXEC, 10, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "free_fn", + "benign.cpp", + "run", + Cap::CODE_EXEC, + 10, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("free_fn", &r); } } diff --git a/tests/crypto_corpus.rs b/tests/crypto_corpus.rs index 43a1a79a..a5c50172 100644 --- a/tests/crypto_corpus.rs +++ b/tests/crypto_corpus.rs @@ -13,20 +13,14 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; -use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; use std::time::Duration; -const LANGS: &[Lang] = &[ - Lang::Java, - Lang::Python, - Lang::Php, - Lang::Go, - Lang::Rust, -]; +const LANGS: &[Lang] = &[Lang::Java, Lang::Python, Lang::Php, Lang::Go, Lang::Rust]; fn outcome() -> SandboxOutcome { SandboxOutcome { @@ -72,19 +66,17 @@ fn corpus_registers_crypto_for_each_supported_lang() { fn crypto_payloads_pair_benign_controls_per_lang() { for lang in LANGS { let slice = payloads_for_lang(Cap::CRYPTO, *lang); - let vuln = slice - .iter() - .find(|p| !p.is_benign) - .expect("vuln payload"); - let resolved = resolve_benign_control_lang(vuln, Cap::CRYPTO, *lang) - .expect("benign control resolves"); + let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln payload"); + let resolved = + resolve_benign_control_lang(vuln, Cap::CRYPTO, *lang).expect("benign control resolves"); assert!(resolved.is_benign); match &vuln.oracle { Oracle::SinkProbe { predicates } => { - assert!(predicates.iter().any(|p| matches!( - p, - ProbePredicate::WeakKeyEntropy { max_bits: 16 } - ))); + assert!( + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::WeakKeyEntropy { max_bits: 16 })) + ); } other => panic!("expected SinkProbe, got {other:?}"), } @@ -119,7 +111,13 @@ fn weak_key_entropy_clears_with_no_probe() { #[test] fn crypto_unsupported_for_other_langs() { - for lang in [Lang::C, Lang::Cpp, Lang::Ruby, Lang::JavaScript, Lang::TypeScript] { + for lang in [ + Lang::C, + Lang::Cpp, + Lang::Ruby, + Lang::JavaScript, + Lang::TypeScript, + ] { assert!( payloads_for_lang(Cap::CRYPTO, lang).is_empty(), "CRYPTO has unexpected payloads for {lang:?}", diff --git a/tests/data_exfil_corpus.rs b/tests/data_exfil_corpus.rs index a70d1915..cd180d10 100644 --- a/tests/data_exfil_corpus.rs +++ b/tests/data_exfil_corpus.rs @@ -14,7 +14,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; -use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::labels::Cap; @@ -76,10 +76,11 @@ fn data_exfil_payloads_pair_benign_per_lang() { .expect("benign control resolves"); assert!(resolved.is_benign); match &vuln.oracle { - Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!( - p, - ProbePredicate::OutboundHostNotIn { .. } - ))), + Oracle::SinkProbe { predicates } => assert!( + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::OutboundHostNotIn { .. })) + ), other => panic!("expected SinkProbe, got {other:?}"), } } diff --git a/tests/deserialize_corpus.rs b/tests/deserialize_corpus.rs index 98b16d8d..bb798f0f 100644 --- a/tests/deserialize_corpus.rs +++ b/tests/deserialize_corpus.rs @@ -13,8 +13,8 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; @@ -105,7 +105,9 @@ fn payload_oracle_carries_deserialize_predicate() { assert!( predicates.iter().any(|p| matches!( p, - ProbePredicate::DeserializeGadgetInvoked { require_invoked: true } + ProbePredicate::DeserializeGadgetInvoked { + require_invoked: true + } )), "{lang:?} vuln payload missing DeserializeGadgetInvoked predicate", ); @@ -166,8 +168,8 @@ fn lang_emitter_dispatches_to_deserialize_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("NYX_GADGET_CLASS:"), "{lang:?} deserialize harness must parse NYX_GADGET_CLASS marker", @@ -187,10 +189,19 @@ fn framework_adapters_detect_deserialize_sink() { // EntryKind::Function binding when the fixture contains the // canonical sink call. for (lang, fixture) in [ - (Lang::Java, "tests/dynamic_fixtures/deserialize/java/Vuln.java"), - (Lang::Python, "tests/dynamic_fixtures/deserialize/python/vuln.py"), + ( + Lang::Java, + "tests/dynamic_fixtures/deserialize/java/Vuln.java", + ), + ( + Lang::Python, + "tests/dynamic_fixtures/deserialize/python/vuln.py", + ), (Lang::Php, "tests/dynamic_fixtures/deserialize/php/vuln.php"), - (Lang::Ruby, "tests/dynamic_fixtures/deserialize/ruby/vuln.rb"), + ( + Lang::Ruby, + "tests/dynamic_fixtures/deserialize/ruby/vuln.rb", + ), ] { let bytes = std::fs::read(fixture).expect("fixture exists"); let ts_lang = ts_language_for(lang); @@ -204,19 +215,15 @@ fn framework_adapters_detect_deserialize_sink() { ..Default::default() }; let registry_slice = adapters_for(lang); - assert!( - !registry_slice.is_empty(), - "{lang:?} adapter slice empty", - ); + assert!(!registry_slice.is_empty(), "{lang:?} adapter slice empty",); let binding = nyx_scanner::dynamic::framework::detect_binding( &summary, tree.root_node(), &bytes, lang, ); - let b = binding.unwrap_or_else(|| { - panic!("{lang:?} adapter must detect the deserialize sink fixture") - }); + let b = binding + .unwrap_or_else(|| panic!("{lang:?} adapter must detect the deserialize sink fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -262,10 +269,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_03 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::SandboxOptions; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -383,7 +390,9 @@ mod e2e_phase_03 { /// an allow-listed class name and writes no probe). #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Java DESERIALIZE vuln must Confirm via run_spec; got {outcome:?}", @@ -401,7 +410,9 @@ mod e2e_phase_03 { #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Python DESERIALIZE vuln must Confirm via run_spec; got {outcome:?}", @@ -415,7 +426,9 @@ mod e2e_phase_03 { #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "PHP DESERIALIZE vuln must Confirm via run_spec; got {outcome:?}", @@ -429,7 +442,9 @@ mod e2e_phase_03 { #[test] fn ruby_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Ruby DESERIALIZE vuln must Confirm via run_spec; got {outcome:?}", diff --git a/tests/determinism_audit.rs b/tests/determinism_audit.rs index 0d3652a5..3fbd449f 100644 --- a/tests/determinism_audit.rs +++ b/tests/determinism_audit.rs @@ -15,7 +15,7 @@ use nyx_scanner::commands::scan::Diag; use nyx_scanner::dynamic::telemetry::{self, SamplingPolicy}; -use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; +use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{Confidence, Evidence, VerifyStatus}; use nyx_scanner::patterns::{FindingCategory, Severity}; use serde_json::Value; @@ -99,10 +99,7 @@ fn ten_runs_produce_byte_identical_telemetry_minus_timestamps() { // Drop `differential` and any future timestamped field by // round-tripping through serde; structural equality is the // contract. - verdict_jsons.insert( - serde_json::to_string(&result) - .expect("VerifyResult serialises"), - ); + verdict_jsons.insert(serde_json::to_string(&result).expect("VerifyResult serialises")); } assert_eq!( verdict_jsons.len(), @@ -243,10 +240,7 @@ fn confirmed_run_is_byte_identical_across_runs() { // every run reads + writes the same absolute paths (the per-run path // would otherwise leak into VerifyResult and break determinism). unsafe { - std::env::set_var( - "NYX_REPRO_BASE", - tmp.path().join("repro").to_str().unwrap(), - ); + std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap()); std::env::set_var( "NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap(), @@ -370,10 +364,7 @@ fn policy_deny_excerpt_is_stable_across_runs() { .inconclusive_reason .expect("expected PolicyDeniedDynamic on deny path") { - nyx_scanner::evidence::InconclusiveReason::PolicyDeniedDynamic { - excerpt, - .. - } => { + nyx_scanner::evidence::InconclusiveReason::PolicyDeniedDynamic { excerpt, .. } => { excerpts.insert(excerpt); } other => panic!("expected PolicyDeniedDynamic, got {other:?}"), diff --git a/tests/dynamic_parity.rs b/tests/dynamic_parity.rs index ffb0ea07..a7ed8c46 100644 --- a/tests/dynamic_parity.rs +++ b/tests/dynamic_parity.rs @@ -16,8 +16,8 @@ #[cfg(feature = "dynamic")] mod parity_tests { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; @@ -118,8 +118,11 @@ mod parity_tests { } /// Assert two verdicts agree on status (and on reason for non-Confirmed). - fn assert_parity(fixture: &str, process_result: &nyx_scanner::evidence::VerifyResult, - docker_result: &nyx_scanner::evidence::VerifyResult) { + fn assert_parity( + fixture: &str, + process_result: &nyx_scanner::evidence::VerifyResult, + docker_result: &nyx_scanner::evidence::VerifyResult, + ) { // Docker reachability fluctuates per host: `docker info` may exit 0 // (daemon listening) while the sandbox's container-start path still // fails (image not pulled, socket gated by Docker Desktop's @@ -128,16 +131,20 @@ mod parity_tests { // where the error surfaces, so the skip predicate looks at the // reason text, not the verdict status. if let Some(ref r) = docker_result.reason - && format!("{r:?}").contains("BackendUnavailable") { - return; // Docker absent — skip comparison. - } + && format!("{r:?}").contains("BackendUnavailable") + { + return; // Docker absent — skip comparison. + } assert_eq!( - process_result.status, docker_result.status, + process_result.status, + docker_result.status, "fixture {fixture}: status mismatch: process={:?} docker={:?}\n\ process detail: {:?}\ndocker detail: {:?}", - process_result.status, docker_result.status, - process_result.detail, docker_result.detail, + process_result.status, + docker_result.status, + process_result.detail, + docker_result.detail, ); // For non-Confirmed statuses, the reason must also match. @@ -154,7 +161,9 @@ mod parity_tests { /// Helper: run a fixture through both backends and assert parity. fn parity_check(fixture: &str, function: &str, sink_line: u32, cap: Cap) { - if !docker_available() { return; } + if !docker_available() { + return; + } let diag = python_diag(fixture, function, sink_line, cap); let process_result = verify_finding(&diag, &process_opts()); @@ -266,7 +275,9 @@ mod parity_tests { /// Rust finding (lang unsupported) must return same status on both backends. #[test] fn parity_rust_lang_unsupported() { - if !docker_available() { return; } + if !docker_available() { + return; + } let diag = python_diag("src/handler.rs", "handle_request", 10, Cap::SQL_QUERY); let process_result = verify_finding(&diag, &process_opts()); diff --git a/tests/dynamic_sandbox_escape.rs b/tests/dynamic_sandbox_escape.rs index db92c59f..40ee5403 100644 --- a/tests/dynamic_sandbox_escape.rs +++ b/tests/dynamic_sandbox_escape.rs @@ -115,7 +115,8 @@ mod escape_tests { let stdout = std::str::from_utf8(&outcome.stdout).unwrap_or(""); let stderr = std::str::from_utf8(&outcome.stderr).unwrap_or(""); assert!( - !stdout.contains("NYX_ESCAPE_SUCCESS") && !stderr.contains("NYX_ESCAPE_SUCCESS"), + !stdout.contains("NYX_ESCAPE_SUCCESS") + && !stderr.contains("NYX_ESCAPE_SUCCESS"), "fixture {fixture}: escape succeeded!\nstdout: {stdout}\nstderr: {stderr}" ); @@ -143,7 +144,9 @@ mod escape_tests { ($name:ident, $fixture:literal) => { #[test] fn $name() { - if !docker_available() { return; } + if !docker_available() { + return; + } let (_tmpdir, harness) = harness_for_fixture($fixture); let result = sandbox::run(&harness, &noop_payload(), &escape_opts()); assert_no_escape(result, $fixture, None); @@ -157,7 +160,9 @@ mod escape_tests { #[cfg(target_os = "linux")] #[test] fn $name() { - if !docker_available() { return; } + if !docker_available() { + return; + } let (_tmpdir, harness) = harness_for_fixture($fixture); let result = sandbox::run(&harness, &noop_payload(), &escape_opts()); assert_no_escape(result, $fixture, None); @@ -166,7 +171,9 @@ mod escape_tests { ($name:ident, $fixture:literal, marker = $marker:expr) => { #[test] fn $name() { - if !docker_available() { return; } + if !docker_available() { + return; + } let marker: PathBuf = PathBuf::from($marker); // Remove stale marker before test. let _ = fs::remove_file(&marker); @@ -181,7 +188,9 @@ mod escape_tests { #[cfg(target_os = "linux")] #[test] fn $name() { - if !docker_available() { return; } + if !docker_available() { + return; + } let marker: PathBuf = PathBuf::from($marker); let _ = fs::remove_file(&marker); let (_tmpdir, harness) = harness_for_fixture($fixture); @@ -236,20 +245,20 @@ mod escape_tests { /// Skips gracefully when Docker is unavailable or `rust:slim` is not pulled. #[test] fn escape_rust_malicious_build_rs() { - if !docker_available() { return; } + if !docker_available() { + return; + } let tmpdir = tempfile::TempDir::new().expect("temp dir"); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/dynamic_fixtures/escape/rust_build_rs"); - copy_dir_recursive(&fixture, tmpdir.path()) - .expect("copy rust_build_rs fixture"); + copy_dir_recursive(&fixture, tmpdir.path()).expect("copy rust_build_rs fixture"); let marker: PathBuf = PathBuf::from("/tmp/pwned_build_rs"); let _ = fs::remove_file(&marker); // Run Docker-isolated cargo build. Returns Err if Docker/image unavailable. - let result = - nyx_scanner::dynamic::build_sandbox::prepare_rust_in_docker(tmpdir.path()); + let result = nyx_scanner::dynamic::build_sandbox::prepare_rust_in_docker(tmpdir.path()); if result.is_err() { // Docker or rust:slim unavailable — no container ran. return; @@ -274,19 +283,19 @@ mod escape_tests { /// Skips gracefully when Docker is unavailable or `node:20-slim` is not pulled. #[test] fn escape_npm_malicious_lifecycle() { - if !docker_available() { return; } + if !docker_available() { + return; + } let tmpdir = tempfile::TempDir::new().expect("temp dir"); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/dynamic_fixtures/escape/npm_malicious_lifecycle"); - copy_dir_recursive(&fixture, tmpdir.path()) - .expect("copy npm_malicious_lifecycle fixture"); + copy_dir_recursive(&fixture, tmpdir.path()).expect("copy npm_malicious_lifecycle fixture"); let marker: PathBuf = PathBuf::from("/tmp/pwned_npm_lifecycle"); let _ = fs::remove_file(&marker); - let result = - nyx_scanner::dynamic::build_sandbox::prepare_node_in_docker(tmpdir.path()); + let result = nyx_scanner::dynamic::build_sandbox::prepare_node_in_docker(tmpdir.path()); if result.is_err() { return; } @@ -310,20 +319,20 @@ mod escape_tests { /// Skips gracefully when Docker is unavailable or `golang:1.21-slim` is not pulled. #[test] fn escape_go_malicious_init() { - if !docker_available() { return; } + if !docker_available() { + return; + } let tmpdir = tempfile::TempDir::new().expect("temp dir"); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/dynamic_fixtures/escape/go_malicious_init_main"); - copy_dir_recursive(&fixture, tmpdir.path()) - .expect("copy go_malicious_init_main fixture"); + copy_dir_recursive(&fixture, tmpdir.path()).expect("copy go_malicious_init_main fixture"); let marker: PathBuf = PathBuf::from("/tmp/pwned_go_init"); let _ = fs::remove_file(&marker); // Docker-isolated go build: init() does not run during compilation. - let result = - nyx_scanner::dynamic::build_sandbox::prepare_go_in_docker(tmpdir.path()); + let result = nyx_scanner::dynamic::build_sandbox::prepare_go_in_docker(tmpdir.path()); if result.is_err() { return; } @@ -346,19 +355,19 @@ mod escape_tests { /// Skips gracefully when Docker is unavailable or the Maven image is not pulled. #[test] fn escape_maven_malicious_plugin() { - if !docker_available() { return; } + if !docker_available() { + return; + } let tmpdir = tempfile::TempDir::new().expect("temp dir"); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/dynamic_fixtures/escape/maven_malicious_plugin"); - copy_dir_recursive(&fixture, tmpdir.path()) - .expect("copy maven_malicious_plugin fixture"); + copy_dir_recursive(&fixture, tmpdir.path()).expect("copy maven_malicious_plugin fixture"); let marker: PathBuf = PathBuf::from("/tmp/pwned_maven_plugin"); let _ = fs::remove_file(&marker); - let result = - nyx_scanner::dynamic::build_sandbox::prepare_java_in_docker(tmpdir.path()); + let result = nyx_scanner::dynamic::build_sandbox::prepare_java_in_docker(tmpdir.path()); if result.is_err() { return; } @@ -380,7 +389,9 @@ mod escape_tests { /// Skips gracefully when Docker is unavailable or `composer:2` is not pulled. #[test] fn escape_composer_malicious_postinstall() { - if !docker_available() { return; } + if !docker_available() { + return; + } let tmpdir = tempfile::TempDir::new().expect("temp dir"); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -391,8 +402,7 @@ mod escape_tests { let marker: PathBuf = PathBuf::from("/tmp/pwned_composer_postinstall"); let _ = fs::remove_file(&marker); - let result = - nyx_scanner::dynamic::build_sandbox::prepare_php_in_docker(tmpdir.path()); + let result = nyx_scanner::dynamic::build_sandbox::prepare_php_in_docker(tmpdir.path()); if result.is_err() { return; } @@ -434,12 +444,17 @@ mod escape_tests { let container_name = format!("nyx-posctl-{}", std::process::id()); let status = std::process::Command::new("docker") .args([ - "run", "-d", "--rm", - "--name", &container_name, + "run", + "-d", + "--rm", + "--name", + &container_name, "--cap-add=SYS_ADMIN", - "--network", "none", + "--network", + "none", "python:3-slim", - "sleep", "60", + "sleep", + "60", ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) @@ -470,8 +485,10 @@ mod escape_tests { // Run the fixture and capture output. let out = std::process::Command::new("docker") .args([ - "exec", &container_name, - "python3", "/workdir/cap_sys_admin_positive_control.py", + "exec", + &container_name, + "python3", + "/workdir/cap_sys_admin_positive_control.py", ]) .output() .expect("docker exec positive control"); @@ -503,7 +520,9 @@ mod escape_tests { /// the container registry holds one entry (started once, reused once). #[test] fn docker_exec_reuse_for_same_workdir() { - if !docker_available() { return; } + if !docker_available() { + return; + } let (_tmpdir, harness) = harness_for_fixture("dns_leak.py"); let opts = escape_opts(); @@ -524,7 +543,9 @@ mod escape_tests { // Verify the container is still running (not torn down between calls). // Container name is derived from the workdir path. - let spec_hash = _tmpdir.path().file_name() + let spec_hash = _tmpdir + .path() + .file_name() .and_then(|n| n.to_str()) .unwrap_or(""); let container_name = format!("nyx-{spec_hash}"); @@ -535,10 +556,7 @@ mod escape_tests { match out { Ok(o) if o.status.success() => { - let running = std::str::from_utf8(&o.stdout) - .unwrap_or("") - .trim() - == "true"; + let running = std::str::from_utf8(&o.stdout).unwrap_or("").trim() == "true"; // Container should still be running (exec reuse kept it alive). assert!( running, diff --git a/tests/dynamic_verify_e2e.rs b/tests/dynamic_verify_e2e.rs index f6cf84ab..a61127a1 100644 --- a/tests/dynamic_verify_e2e.rs +++ b/tests/dynamic_verify_e2e.rs @@ -18,8 +18,10 @@ #[cfg(feature = "dynamic")] mod verify_e2e { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, UnsupportedReason, VerifyStatus}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{ + Confidence, Evidence, FlowStep, FlowStepKind, UnsupportedReason, VerifyStatus, + }; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; @@ -243,9 +245,15 @@ mod verify_e2e { let v: serde_json::Value = serde_json::from_str(&json).expect("must be valid JSON"); assert!(v.get("status").is_some(), "status field must be present"); - assert!(v.get("triggered_payload").is_none(), "triggered_payload must be absent"); + assert!( + v.get("triggered_payload").is_none(), + "triggered_payload must be absent" + ); assert!(v.get("detail").is_none(), "detail must be absent"); - assert!(v.get("attempts").is_none(), "attempts must be absent (empty vec skipped)"); + assert!( + v.get("attempts").is_none(), + "attempts must be absent (empty vec skipped)" + ); assert!(v["finding_id"].is_string()); } } diff --git a/tests/env_capture_flask.rs b/tests/env_capture_flask.rs index 76541290..75c5ca93 100644 --- a/tests/env_capture_flask.rs +++ b/tests/env_capture_flask.rs @@ -23,8 +23,8 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::environment::{ - capture_project_dependencies, capture_project_dependencies_with_context, - stage_workdir_full, MAX_WORKDIR_BYTES, + MAX_WORKDIR_BYTES, capture_project_dependencies, capture_project_dependencies_with_context, + stage_workdir_full, }; use nyx_scanner::dynamic::lang::materialize_runtime; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy}; @@ -108,7 +108,11 @@ fn capture_returns_three_deps_plus_flask() { assert!(!captured.toolchain.toolchain_drift); // Manifests resolved: requirements.txt and pyproject.toml. - assert!(captured.lockfile.is_some(), "lockfile = {:?}", captured.lockfile); + assert!( + captured.lockfile.is_some(), + "lockfile = {:?}", + captured.lockfile + ); let manifest_names: Vec = captured .manifests .iter() @@ -255,7 +259,7 @@ fn callgraph_context_extends_source_closure() { // reverse-edge walk discovered (here just one file because the // fixture is single-file). use nyx_scanner::ast::analyse_file_fused; - use nyx_scanner::callgraph::{build_call_graph}; + use nyx_scanner::callgraph::build_call_graph; use nyx_scanner::summary::GlobalSummaries; use nyx_scanner::utils::config::{AnalysisMode, Config}; @@ -268,8 +272,8 @@ fn callgraph_context_extends_source_closure() { let root = fixture_root(); let app = root.join("app.py"); let bytes = std::fs::read(&app).unwrap(); - let result = analyse_file_fused(&bytes, &app, &cfg, None, Some(&root)) - .expect("analyse fixture"); + let result = + analyse_file_fused(&bytes, &app, &cfg, None, Some(&root)).expect("analyse fixture"); let root_str = root.to_string_lossy(); let mut gs = GlobalSummaries::new(); for s in result.summaries { diff --git a/tests/fix_validation_e2e.rs b/tests/fix_validation_e2e.rs index fdfce344..393b90fb 100644 --- a/tests/fix_validation_e2e.rs +++ b/tests/fix_validation_e2e.rs @@ -13,8 +13,8 @@ mod common; use nyx_scanner::baseline::{ - check_gate, compute_verdict_diff, diags_to_baseline_entries, load_baseline, write_baseline, - BaselineEntry, Transition, GATE_NO_NEW_CONFIRMED, GATE_RESOLVE_ALL_CONFIRMED, + BaselineEntry, GATE_NO_NEW_CONFIRMED, GATE_RESOLVE_ALL_CONFIRMED, Transition, check_gate, + compute_verdict_diff, diags_to_baseline_entries, load_baseline, write_baseline, }; use nyx_scanner::commands::scan::compute_stable_hash; use nyx_scanner::evidence::{Evidence, VerifyResult, VerifyStatus}; @@ -32,10 +32,7 @@ fn scan_with_hashes(dir: &Path) -> Vec { } /// Attach a simulated dynamic verdict to every finding in the list. -fn set_verdict( - diags: &mut [nyx_scanner::commands::scan::Diag], - status: VerifyStatus, -) { +fn set_verdict(diags: &mut [nyx_scanner::commands::scan::Diag], status: VerifyStatus) { for d in diags.iter_mut() { let fid = format!("{:016x}", d.stable_hash); let ev = d.evidence.get_or_insert_with(Evidence::default); @@ -89,7 +86,10 @@ fn fix_resolves_confirmed_finding() { // Step 1: scan vulnerable, simulate Confirmed verdict. let mut vuln_diags = scan_with_hashes(vuln_path); - assert!(!vuln_diags.is_empty(), "Need at least one SQL injection finding"); + assert!( + !vuln_diags.is_empty(), + "Need at least one SQL injection finding" + ); set_verdict(&mut vuln_diags, VerifyStatus::Confirmed); // Step 2: write stripped baseline. @@ -260,7 +260,6 @@ fn load_baseline_accepts_full_diag_json() { // Hashes must round-trip. let loaded_hashes: std::collections::HashSet = loaded.iter().map(|e| e.stable_hash).collect(); - let diag_hashes: std::collections::HashSet = - diags.iter().map(|d| d.stable_hash).collect(); + let diag_hashes: std::collections::HashSet = diags.iter().map(|d| d.stable_hash).collect(); assert_eq!(loaded_hashes, diag_hashes); } diff --git a/tests/go_fixtures.rs b/tests/go_fixtures.rs index 6d5697ef..b70e02a3 100644 --- a/tests/go_fixtures.rs +++ b/tests/go_fixtures.rs @@ -14,7 +14,7 @@ mod common; #[cfg(feature = "dynamic")] mod go_fixture_tests { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, VerifyStatus, @@ -456,7 +456,7 @@ mod go_fixture_tests { #[cfg(feature = "dynamic")] mod phase15_shape_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -506,7 +506,15 @@ mod phase15_shape_tests { // return; };`. run_shape_fixture_lang_or_skip( &[Prerequisite::CommandAvailable("go")], - Lang::Go, "go", shape, file, func, cap, sink_line, kind, slot, + Lang::Go, + "go", + shape, + file, + func, + cap, + sink_line, + kind, + slot, ) } @@ -515,8 +523,13 @@ mod phase15_shape_tests { #[test] fn handler_func_vuln_is_confirmed() { let Some(r) = run( - "handler_func", "vuln.go", "Handle", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "handler_func", + "vuln.go", + "Handle", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -526,8 +539,13 @@ mod phase15_shape_tests { #[test] fn handler_func_benign_not_confirmed() { let Some(r) = run( - "handler_func", "benign.go", "Handle", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "handler_func", + "benign.go", + "Handle", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -539,8 +557,13 @@ mod phase15_shape_tests { #[test] fn gin_handler_vuln_is_confirmed() { let Some(r) = run( - "gin_handler", "vuln.go", "Handle", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "gin_handler", + "vuln.go", + "Handle", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -550,8 +573,13 @@ mod phase15_shape_tests { #[test] fn gin_handler_benign_not_confirmed() { let Some(r) = run( - "gin_handler", "benign.go", "Handle", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "gin_handler", + "benign.go", + "Handle", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -563,8 +591,13 @@ mod phase15_shape_tests { #[test] fn flag_cli_vuln_is_confirmed() { let Some(r) = run( - "flag_cli", "vuln.go", "Run", Cap::CODE_EXEC, 19, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "flag_cli", + "vuln.go", + "Run", + Cap::CODE_EXEC, + 19, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -574,8 +607,13 @@ mod phase15_shape_tests { #[test] fn flag_cli_benign_not_confirmed() { let Some(r) = run( - "flag_cli", "benign.go", "Run", Cap::CODE_EXEC, 15, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "flag_cli", + "benign.go", + "Run", + Cap::CODE_EXEC, + 15, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -587,8 +625,13 @@ mod phase15_shape_tests { #[test] fn fuzz_variadic_vuln_is_confirmed() { let Some(r) = run( - "fuzz_variadic", "vuln.go", "FuzzHandle", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), + "fuzz_variadic", + "vuln.go", + "FuzzHandle", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; @@ -598,8 +641,13 @@ mod phase15_shape_tests { #[test] fn fuzz_variadic_benign_not_confirmed() { let Some(r) = run( - "fuzz_variadic", "benign.go", "FuzzHandle", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), + "fuzz_variadic", + "benign.go", + "FuzzHandle", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; diff --git a/tests/go_frameworks_corpus.rs b/tests/go_frameworks_corpus.rs index cd1f905b..5dcddcb3 100644 --- a/tests/go_frameworks_corpus.rs +++ b/tests/go_frameworks_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod}; +use nyx_scanner::dynamic::framework::{HttpMethod, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; diff --git a/tests/header_injection_corpus.rs b/tests/header_injection_corpus.rs index 6cd67e0a..f84d51c2 100644 --- a/tests/header_injection_corpus.rs +++ b/tests/header_injection_corpus.rs @@ -16,12 +16,12 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; -use nyx_scanner::dynamic::oracle::{oracle_fired, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; @@ -311,8 +311,8 @@ fn lang_emitter_dispatches_to_header_injection_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("HeaderEmit"), "{lang:?} header harness must carry the HeaderEmit probe kind", @@ -396,8 +396,8 @@ fn framework_adapters_detect_header_sink() { &bytes, lang, ); - let b = binding - .unwrap_or_else(|| panic!("{lang:?} adapter must detect the header fixture")); + let b = + binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the header fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -457,10 +457,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_08 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -588,43 +588,57 @@ mod e2e_phase_08 { #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert_confirmed(Lang::Java, &outcome); } #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert_confirmed(Lang::Python, &outcome); } #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert_confirmed(Lang::Php, &outcome); } #[test] fn ruby_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert_confirmed(Lang::Ruby, &outcome); } #[test] fn js_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { + return; + }; assert_confirmed(Lang::JavaScript, &outcome); } #[test] fn go_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else { return }; + let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else { + return; + }; assert_confirmed(Lang::Go, &outcome); } #[test] fn rust_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else { return }; + let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else { + return; + }; assert_confirmed(Lang::Rust, &outcome); } } diff --git a/tests/java_fixtures.rs b/tests/java_fixtures.rs index e173d61a..3b392665 100644 --- a/tests/java_fixtures.rs +++ b/tests/java_fixtures.rs @@ -22,7 +22,7 @@ mod common; #[cfg(feature = "dynamic")] mod java_fixture_tests { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, VerifyStatus, @@ -464,7 +464,7 @@ mod java_fixture_tests { #[cfg(feature = "dynamic")] mod phase14_shape_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -517,7 +517,15 @@ mod phase14_shape_tests { Prerequisite::CommandAvailable("javac"), Prerequisite::CommandAvailable("java"), ], - Lang::Java, "java", shape, file, func, cap, sink_line, kind, slot, + Lang::Java, + "java", + shape, + file, + func, + cap, + sink_line, + kind, + slot, ) } @@ -526,8 +534,13 @@ mod phase14_shape_tests { #[test] fn static_method_vuln_is_confirmed() { let Some(r) = run( - "static_method", "Vuln.java", "processInput", Cap::CODE_EXEC, 12, - EntryKind::Function, PayloadSlot::Param(0), + "static_method", + "Vuln.java", + "processInput", + Cap::CODE_EXEC, + 12, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; @@ -537,8 +550,13 @@ mod phase14_shape_tests { #[test] fn static_method_benign_not_confirmed() { let Some(r) = run( - "static_method", "Benign.java", "processInput", Cap::CODE_EXEC, 13, - EntryKind::Function, PayloadSlot::Param(0), + "static_method", + "Benign.java", + "processInput", + Cap::CODE_EXEC, + 13, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; @@ -550,8 +568,13 @@ mod phase14_shape_tests { #[test] fn static_main_vuln_is_confirmed() { let Some(r) = run( - "static_main", "Vuln.java", "main", Cap::CODE_EXEC, 13, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "static_main", + "Vuln.java", + "main", + Cap::CODE_EXEC, + 13, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -561,8 +584,13 @@ mod phase14_shape_tests { #[test] fn static_main_benign_not_confirmed() { let Some(r) = run( - "static_main", "Benign.java", "main", Cap::CODE_EXEC, 12, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "static_main", + "Benign.java", + "main", + Cap::CODE_EXEC, + 12, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -574,8 +602,13 @@ mod phase14_shape_tests { #[test] fn servlet_doget_vuln_is_confirmed() { let Some(r) = run( - "servlet_doget", "Vuln.java", "doGet", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "servlet_doget", + "Vuln.java", + "doGet", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -585,8 +618,13 @@ mod phase14_shape_tests { #[test] fn servlet_doget_benign_not_confirmed() { let Some(r) = run( - "servlet_doget", "Benign.java", "doGet", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()), + "servlet_doget", + "Benign.java", + "doGet", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("payload".into()), ) else { return; }; @@ -598,8 +636,13 @@ mod phase14_shape_tests { #[test] fn servlet_dopost_vuln_is_confirmed() { let Some(r) = run( - "servlet_dopost", "Vuln.java", "doPost", Cap::CODE_EXEC, 13, - EntryKind::HttpRoute, PayloadSlot::HttpBody, + "servlet_dopost", + "Vuln.java", + "doPost", + Cap::CODE_EXEC, + 13, + EntryKind::HttpRoute, + PayloadSlot::HttpBody, ) else { return; }; @@ -609,8 +652,13 @@ mod phase14_shape_tests { #[test] fn servlet_dopost_benign_not_confirmed() { let Some(r) = run( - "servlet_dopost", "Benign.java", "doPost", Cap::CODE_EXEC, 12, - EntryKind::HttpRoute, PayloadSlot::HttpBody, + "servlet_dopost", + "Benign.java", + "doPost", + Cap::CODE_EXEC, + 12, + EntryKind::HttpRoute, + PayloadSlot::HttpBody, ) else { return; }; @@ -622,8 +670,13 @@ mod phase14_shape_tests { #[test] fn spring_controller_vuln_is_confirmed() { let Some(r) = run( - "spring_controller", "Vuln.java", "run", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "spring_controller", + "Vuln.java", + "run", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -633,8 +686,13 @@ mod phase14_shape_tests { #[test] fn spring_controller_benign_not_confirmed() { let Some(r) = run( - "spring_controller", "Benign.java", "run", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "spring_controller", + "Benign.java", + "run", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -646,8 +704,13 @@ mod phase14_shape_tests { #[test] fn junit_test_vuln_is_confirmed() { let Some(r) = run( - "junit_test", "Vuln.java", "testRun", Cap::CODE_EXEC, 17, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "junit_test", + "Vuln.java", + "testRun", + Cap::CODE_EXEC, + 17, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -657,8 +720,13 @@ mod phase14_shape_tests { #[test] fn junit_test_benign_not_confirmed() { let Some(r) = run( - "junit_test", "Benign.java", "testRun", Cap::CODE_EXEC, 15, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "junit_test", + "Benign.java", + "testRun", + Cap::CODE_EXEC, + 15, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -670,8 +738,13 @@ mod phase14_shape_tests { #[test] fn quarkus_route_vuln_is_confirmed() { let Some(r) = run( - "quarkus_route", "Vuln.java", "run", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "quarkus_route", + "Vuln.java", + "run", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -681,8 +754,13 @@ mod phase14_shape_tests { #[test] fn quarkus_route_benign_not_confirmed() { let Some(r) = run( - "quarkus_route", "Benign.java", "run", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "quarkus_route", + "Benign.java", + "run", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; diff --git a/tests/java_frameworks_corpus.rs b/tests/java_frameworks_corpus.rs index 5b87c49e..8aa4db7e 100644 --- a/tests/java_frameworks_corpus.rs +++ b/tests/java_frameworks_corpus.rs @@ -16,7 +16,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; @@ -143,10 +143,12 @@ fn servlet_doget_vuln_fixture_binds_route() { // path defaults to `"/"`. assert_eq!(route.path, "/"); // The (req, resp) pair should classify as Implicit. - assert!(binding - .request_params - .iter() - .all(|p| matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .all(|p| matches!(p.source, ParamSource::Implicit)) + ); } #[test] diff --git a/tests/javascript_fixtures.rs b/tests/javascript_fixtures.rs index c88c9744..3904243e 100644 --- a/tests/javascript_fixtures.rs +++ b/tests/javascript_fixtures.rs @@ -18,7 +18,7 @@ mod common; #[cfg(feature = "dynamic")] mod javascript_fixture_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -89,9 +89,16 @@ mod javascript_fixture_tests { fn commonjs_export_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "commonjs_export", "vuln.js", "runPing", Cap::CODE_EXEC, 11, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "commonjs_export", + "vuln.js", + "runPing", + Cap::CODE_EXEC, + 11, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("commonjs_export", &r); } @@ -99,9 +106,16 @@ mod javascript_fixture_tests { fn commonjs_export_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "commonjs_export", "benign.js", "runPing", Cap::CODE_EXEC, 11, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "commonjs_export", + "benign.js", + "runPing", + Cap::CODE_EXEC, + 11, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("commonjs_export", &r); } @@ -111,9 +125,16 @@ mod javascript_fixture_tests { fn async_function_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "async_function", "vuln.js", "runPing", Cap::CODE_EXEC, 15, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "async_function", + "vuln.js", + "runPing", + Cap::CODE_EXEC, + 15, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("async_function", &r); } @@ -121,9 +142,16 @@ mod javascript_fixture_tests { fn async_function_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "async_function", "benign.js", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "async_function", + "benign.js", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("async_function", &r); } @@ -133,9 +161,16 @@ mod javascript_fixture_tests { fn esm_default_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "esm_default", "vuln.js", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "esm_default", + "vuln.js", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("esm_default", &r); } @@ -143,9 +178,16 @@ mod javascript_fixture_tests { fn esm_default_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "esm_default", "benign.js", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "esm_default", + "benign.js", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("esm_default", &r); } @@ -158,9 +200,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("express"), ], - "express", "vuln.js", "ping", Cap::CODE_EXEC, 15, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "express", + "vuln.js", + "ping", + Cap::CODE_EXEC, + 15, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("express", &r); } @@ -171,9 +220,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("express"), ], - "express", "benign.js", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "express", + "benign.js", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("express", &r); } @@ -186,9 +242,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("koa"), ], - "koa", "vuln.js", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "koa", + "vuln.js", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("koa", &r); } @@ -199,9 +262,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("koa"), ], - "koa", "benign.js", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "koa", + "benign.js", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("koa", &r); } @@ -214,9 +284,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("next"), ], - "next_route", "vuln.js", "handler", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "next_route", + "vuln.js", + "handler", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("next_route", &r); } @@ -227,9 +304,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("next"), ], - "next_route", "benign.js", "handler", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "next_route", + "benign.js", + "handler", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("next_route", &r); } @@ -242,9 +326,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("jsdom"), ], - "browser_event", "vuln.js", "clickHandler", Cap::HTML_ESCAPE, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "browser_event", + "vuln.js", + "clickHandler", + Cap::HTML_ESCAPE, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("browser_event", &r); } @@ -255,9 +346,16 @@ mod javascript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("jsdom"), ], - "browser_event", "benign.js", "clickHandler", Cap::HTML_ESCAPE, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "browser_event", + "benign.js", + "clickHandler", + Cap::HTML_ESCAPE, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("browser_event", &r); } } diff --git a/tests/js_fixtures.rs b/tests/js_fixtures.rs index 490ec3e5..2ce0e3cb 100644 --- a/tests/js_fixtures.rs +++ b/tests/js_fixtures.rs @@ -12,7 +12,7 @@ #[cfg(feature = "dynamic")] mod js_fixture_tests { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, VerifyStatus, diff --git a/tests/js_frameworks_corpus.rs b/tests/js_frameworks_corpus.rs index fc35111d..48d70ecc 100644 --- a/tests/js_frameworks_corpus.rs +++ b/tests/js_frameworks_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; @@ -45,10 +45,12 @@ fn express_vuln_fixture_binds_route() { let route = binding.route.as_ref().expect("route"); assert_eq!(route.path, "/run"); assert_eq!(route.method, HttpMethod::GET); - assert!(binding - .request_params - .iter() - .any(|p| p.name == "req" && matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "req" && matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -77,10 +79,12 @@ fn koa_vuln_fixture_binds_router_route() { let route = binding.route.as_ref().expect("route"); assert_eq!(route.path, "/run"); assert_eq!(route.method, HttpMethod::GET); - assert!(binding - .request_params - .iter() - .any(|p| p.name == "ctx" && matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "ctx" && matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -107,14 +111,18 @@ fn fastify_vuln_fixture_binds_route() { let route = binding.route.as_ref().expect("route"); assert_eq!(route.path, "/run"); assert_eq!(route.method, HttpMethod::GET); - assert!(binding - .request_params - .iter() - .any(|p| p.name == "request" && matches!(p.source, ParamSource::Implicit))); - assert!(binding - .request_params - .iter() - .any(|p| p.name == "reply" && matches!(p.source, ParamSource::Implicit))); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "request" && matches!(p.source, ParamSource::Implicit)) + ); + assert!( + binding + .request_params + .iter() + .any(|p| p.name == "reply" && matches!(p.source, ParamSource::Implicit)) + ); } #[test] @@ -176,7 +184,6 @@ fn express_adapter_runs_before_fastify_for_express_files() { app.get('/x', h);\n"; let tree = parse_js(src); let summary = summary_for("h", "synthetic.js"); - let binding = - detect_binding(&summary, tree.root_node(), src, Lang::JavaScript).expect("fires"); + let binding = detect_binding(&summary, tree.root_node(), src, Lang::JavaScript).expect("fires"); assert_eq!(binding.adapter, "js-express"); } diff --git a/tests/json_parse_corpus.rs b/tests/json_parse_corpus.rs index 44be649c..c73a3410 100644 --- a/tests/json_parse_corpus.rs +++ b/tests/json_parse_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; -use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::labels::Cap; @@ -68,7 +68,9 @@ fn json_parse_pairs_benign_per_lang_via_canary_predicate() { match &vuln.oracle { Oracle::SinkProbe { predicates } => assert!(predicates.iter().any(|p| matches!( p, - ProbePredicate::PrototypeCanaryTouched { canary: "__nyx_canary" } + ProbePredicate::PrototypeCanaryTouched { + canary: "__nyx_canary" + } ))), other => panic!("expected SinkProbe, got {other:?}"), } @@ -82,8 +84,16 @@ fn canary_predicate_fires_only_on_canary_property() { canary: "__nyx_canary", }], }; - assert!(oracle_fired(&oracle, &outcome(), &[canary_probe("__nyx_canary")])); - assert!(!oracle_fired(&oracle, &outcome(), &[canary_probe("__data__")])); + assert!(oracle_fired( + &oracle, + &outcome(), + &[canary_probe("__nyx_canary")] + )); + assert!(!oracle_fired( + &oracle, + &outcome(), + &[canary_probe("__data__")] + )); assert!(!oracle_fired(&oracle, &outcome(), &[])); } diff --git a/tests/json_snapshot.rs b/tests/json_snapshot.rs index bd0fa9de..83774012 100644 --- a/tests/json_snapshot.rs +++ b/tests/json_snapshot.rs @@ -6,9 +6,7 @@ //! `skip_serializing_if = "Option::is_none"`). use nyx_scanner::commands::scan::Diag; -use nyx_scanner::evidence::{ - AttemptSummary, Evidence, VerifyResult, VerifyStatus, -}; +use nyx_scanner::evidence::{AttemptSummary, Evidence, VerifyResult, VerifyStatus}; use nyx_scanner::patterns::{FindingCategory, Severity}; fn base_diag() -> Diag { diff --git a/tests/ldap_corpus.rs b/tests/ldap_corpus.rs index dfd58ac5..c2e9d9b4 100644 --- a/tests/ldap_corpus.rs +++ b/tests/ldap_corpus.rs @@ -16,8 +16,8 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; @@ -57,7 +57,10 @@ fn make_spec(lang: Lang, entry_file: &str, entry_name: &str) -> HarnessSpec { fn corpus_registers_ldap_for_every_supported_lang() { for lang in LANGS { let slice = payloads_for_lang(Cap::LDAP_INJECTION, *lang); - assert!(!slice.is_empty(), "LDAP_INJECTION has no payloads for {lang:?}"); + assert!( + !slice.is_empty(), + "LDAP_INJECTION has no payloads for {lang:?}" + ); let has_vuln = slice.iter().any(|p| !p.is_benign); let has_benign = slice.iter().any(|p| p.is_benign); assert!(has_vuln, "{lang:?} LDAP missing vuln payload"); @@ -104,10 +107,9 @@ fn payload_oracle_carries_ldap_result_count_predicate() { match &vuln.oracle { Oracle::SinkProbe { predicates } => { assert!( - predicates.iter().any(|p| matches!( - p, - ProbePredicate::QueryResultCountGreaterThan { n: 1 } - )), + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::QueryResultCountGreaterThan { n: 1 })), "{lang:?} vuln payload missing QueryResultCountGreaterThan {{ n: 1 }}", ); } @@ -146,7 +148,9 @@ fn marker_collisions_clean_with_phase_06_additions() { #[test] fn probe_kind_ldap_serdes() { - let original = ProbeKind::Ldap { entries_returned: 3 }; + let original = ProbeKind::Ldap { + entries_returned: 3, + }; let json = serde_json::to_string(&original).unwrap(); assert!(json.contains("Ldap")); assert!(json.contains("entries_returned")); @@ -181,8 +185,8 @@ fn lang_emitter_dispatches_to_ldap_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("entries_returned"), "{lang:?} ldap harness must carry the entries_returned probe field", @@ -246,8 +250,7 @@ fn framework_adapters_detect_ldap_sink() { &bytes, lang, ); - let b = binding - .unwrap_or_else(|| panic!("{lang:?} adapter must detect the LDAP fixture")); + let b = binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the LDAP fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -279,7 +282,10 @@ fn stub_ldap_server_returns_three_for_wildcard_filter() { let stub = LdapStub::start().expect("ldap stub starts"); let mal = LdapStub::evaluate("(|(uid=alice)(uid=*))"); let benign = LdapStub::evaluate("(uid=alice)"); - assert!(mal.len() > 1, "malicious filter must match > 1 entry, got {mal:?}"); + assert!( + mal.len() > 1, + "malicious filter must match > 1 entry, got {mal:?}" + ); assert_eq!(benign.len(), 1, "benign filter must match exactly 1 entry"); assert_eq!(stub.kind(), StubKind::Ldap); } @@ -302,10 +308,10 @@ fn stub_kind_for_cap_routes_ldap_injection() { mod e2e_phase_06 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -413,7 +419,9 @@ mod e2e_phase_06 { #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Java LDAP vuln must Confirm via run_spec; got {outcome:?}", @@ -427,7 +435,9 @@ mod e2e_phase_06 { #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Python LDAP vuln must Confirm via run_spec; got {outcome:?}", @@ -441,7 +451,9 @@ mod e2e_phase_06 { #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "PHP LDAP vuln must Confirm via run_spec; got {outcome:?}", diff --git a/tests/marker_uniqueness.rs b/tests/marker_uniqueness.rs index a85e1d76..5bda20f2 100644 --- a/tests/marker_uniqueness.rs +++ b/tests/marker_uniqueness.rs @@ -95,7 +95,9 @@ fn no_marker_is_substring_of_another_caps_payload() { continue; } for payload in payloads_for(cap).iter().filter(|p| !p.is_benign) { - let payload_contains_marker = payload.bytes.windows(marker_bytes.len()) + let payload_contains_marker = payload + .bytes + .windows(marker_bytes.len()) .any(|w| w == marker_bytes); if payload_contains_marker { @@ -215,7 +217,8 @@ fn all_vuln_payloads_have_non_empty_oracle_marker() { assert!( marker.len() >= 4, "payload {:?} for {cap:?} has very short marker {:?} (< 4 chars) — collision risk", - payload.label, marker + payload.label, + marker ); } } diff --git a/tests/message_handler_corpus.rs b/tests/message_handler_corpus.rs index ff9f678c..dfa7a89c 100644 --- a/tests/message_handler_corpus.rs +++ b/tests/message_handler_corpus.rs @@ -17,7 +17,7 @@ mod common; use nyx_scanner::dynamic::framework::registry::adapters_for; -use nyx_scanner::dynamic::framework::{detect_binding, FrameworkBinding}; +use nyx_scanner::dynamic::framework::{FrameworkBinding, detect_binding}; use nyx_scanner::dynamic::lang; use nyx_scanner::dynamic::spec::{EntryKind, EntryKindTag, HarnessSpec, PayloadSlot}; use nyx_scanner::labels::Cap; @@ -32,13 +32,7 @@ const SUPPORTED_LANGS: &[Lang] = &[ Lang::Go, ]; -const UNSUPPORTED_LANGS: &[Lang] = &[ - Lang::Php, - Lang::Ruby, - Lang::Rust, - Lang::C, - Lang::Cpp, -]; +const UNSUPPORTED_LANGS: &[Lang] = &[Lang::Php, Lang::Ruby, Lang::Rust, Lang::C, Lang::Cpp]; fn entry_file(broker_lang: &str) -> &'static str { // Phase 20 fixtures live at tests/dynamic_fixtures/message_handler/{broker_lang}/{vuln,benign}. @@ -222,29 +216,29 @@ fn kafka_python_adapter_binds_message_handler_kind() { #[test] fn kafka_java_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Java, entry_file("kafka_java"), "onMessage") - .expect("kafka-java detect"); + let b = + detect_for(Lang::Java, entry_file("kafka_java"), "onMessage").expect("kafka-java detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } #[test] fn sqs_python_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Python, entry_file("sqs_python"), "handler") - .expect("sqs-python detect"); + let b = + detect_for(Lang::Python, entry_file("sqs_python"), "handler").expect("sqs-python detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } #[test] fn sqs_java_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Java, entry_file("sqs_java"), "handleMessage") - .expect("sqs-java detect"); + let b = + detect_for(Lang::Java, entry_file("sqs_java"), "handleMessage").expect("sqs-java detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } #[test] fn sqs_node_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::JavaScript, entry_file("sqs_node"), "handler") - .expect("sqs-node detect"); + let b = + detect_for(Lang::JavaScript, entry_file("sqs_node"), "handler").expect("sqs-node detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } @@ -257,8 +251,7 @@ fn pubsub_python_adapter_binds_message_handler_kind() { #[test] fn pubsub_go_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Go, entry_file("pubsub_go"), "OnMessage") - .expect("pubsub-go detect"); + let b = detect_for(Lang::Go, entry_file("pubsub_go"), "OnMessage").expect("pubsub-go detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } @@ -271,24 +264,20 @@ fn rabbit_python_adapter_binds_message_handler_kind() { #[test] fn rabbit_java_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Java, entry_file("rabbit_java"), "onMessage") - .expect("rabbit-java detect"); + let b = + detect_for(Lang::Java, entry_file("rabbit_java"), "onMessage").expect("rabbit-java detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } #[test] fn nats_go_adapter_binds_message_handler_kind() { - let b = detect_for(Lang::Go, entry_file("nats_go"), "OnMessage") - .expect("nats-go detect"); + let b = detect_for(Lang::Go, entry_file("nats_go"), "OnMessage").expect("nats-go detect"); assert!(matches!(b.kind, EntryKind::MessageHandler { .. })); } #[test] fn registry_slices_include_phase_20_adapters() { - let java_names: Vec<&'static str> = adapters_for(Lang::Java) - .iter() - .map(|a| a.name()) - .collect(); + let java_names: Vec<&'static str> = adapters_for(Lang::Java).iter().map(|a| a.name()).collect(); assert!(java_names.contains(&"kafka-java")); assert!(java_names.contains(&"sqs-java")); assert!(java_names.contains(&"rabbit-java")); @@ -302,10 +291,7 @@ fn registry_slices_include_phase_20_adapters() { assert!(python_names.contains(&"pubsub-python")); assert!(python_names.contains(&"rabbit-python")); - let go_names: Vec<&'static str> = adapters_for(Lang::Go) - .iter() - .map(|a| a.name()) - .collect(); + let go_names: Vec<&'static str> = adapters_for(Lang::Go).iter().map(|a| a.name()).collect(); assert!(go_names.contains(&"pubsub-go")); assert!(go_names.contains(&"nats-go")); @@ -327,10 +313,10 @@ fn registry_slices_include_phase_20_adapters() { mod e2e_phase_20 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::SandboxOptions; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -468,9 +454,7 @@ mod e2e_phase_20 { ); None } - Err(e) => panic!( - "run_spec({lang:?} {fixture_dir}/{fixture_file}) errored: {e:?}", - ), + Err(e) => panic!("run_spec({lang:?} {fixture_dir}/{fixture_file}) errored: {e:?}",), } } @@ -497,8 +481,7 @@ mod e2e_phase_20 { #[test] fn sqs_python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "sqs_python", "vuln.py", "handler", "jobs") - else { + let Some(outcome) = run(Lang::Python, "sqs_python", "vuln.py", "handler", "jobs") else { return; }; assert!(outcome.triggered_by.is_some()); @@ -540,8 +523,7 @@ mod e2e_phase_20 { #[test] fn sqs_node_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "sqs_node", "vuln.js", "handler", "jobs") - else { + let Some(outcome) = run(Lang::JavaScript, "sqs_node", "vuln.js", "handler", "jobs") else { return; }; assert!( diff --git a/tests/network_policy.rs b/tests/network_policy.rs index 2c68aaf0..e61fd2bb 100644 --- a/tests/network_policy.rs +++ b/tests/network_policy.rs @@ -59,7 +59,9 @@ fn oob_outbound_carries_listener() { return; }; let listener = Arc::new(listener); - let p = NetworkPolicy::OobOutbound { listener: Arc::clone(&listener) }; + let p = NetworkPolicy::OobOutbound { + listener: Arc::clone(&listener), + }; assert!(p.allows_network()); let got = p.oob_listener().expect("listener present"); assert!( diff --git a/tests/open_redirect_corpus.rs b/tests/open_redirect_corpus.rs index 200faa91..e8da6f52 100644 --- a/tests/open_redirect_corpus.rs +++ b/tests/open_redirect_corpus.rs @@ -16,12 +16,12 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; -use nyx_scanner::dynamic::oracle::{oracle_fired, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; @@ -72,10 +72,7 @@ fn corpus_registers_open_redirect_for_every_supported_lang() { let has_vuln = slice.iter().any(|p| !p.is_benign); let has_benign = slice.iter().any(|p| p.is_benign); assert!(has_vuln, "{lang:?} OPEN_REDIRECT missing vuln payload"); - assert!( - has_benign, - "{lang:?} OPEN_REDIRECT missing benign control" - ); + assert!(has_benign, "{lang:?} OPEN_REDIRECT missing benign control"); } } @@ -94,8 +91,8 @@ fn benign_control_resolves_within_lang_slice() { for lang in LANGS { let slice = payloads_for_lang(Cap::OPEN_REDIRECT, *lang); let vuln = slice.iter().find(|p| !p.is_benign).unwrap(); - let resolved = resolve_benign_control_lang(vuln, Cap::OPEN_REDIRECT, *lang) - .expect("paired control"); + let resolved = + resolve_benign_control_lang(vuln, Cap::OPEN_REDIRECT, *lang).expect("paired control"); assert!(resolved.is_benign); let direct = benign_payload_for_lang(Cap::OPEN_REDIRECT, *lang).unwrap(); assert_eq!(direct.label, resolved.label); @@ -110,10 +107,9 @@ fn payload_oracle_carries_redirect_host_not_in_predicate() { match &vuln.oracle { Oracle::SinkProbe { predicates } => { assert!( - predicates.iter().any(|p| matches!( - p, - ProbePredicate::RedirectHostNotIn { .. } - )), + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::RedirectHostNotIn { .. })), "{lang:?} vuln payload missing RedirectHostNotIn predicate", ); } @@ -275,8 +271,8 @@ fn lang_emitter_dispatches_to_open_redirect_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("Redirect"), "{lang:?} redirect harness must carry the Redirect probe kind", @@ -361,8 +357,8 @@ fn framework_adapters_detect_redirect_sink() { &bytes, lang, ); - let b = binding - .unwrap_or_else(|| panic!("{lang:?} adapter must detect the redirect fixture")); + let b = + binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the redirect fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -423,10 +419,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_09 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -554,43 +550,57 @@ mod e2e_phase_09 { #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert_confirmed(Lang::Java, &outcome); } #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert_confirmed(Lang::Python, &outcome); } #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert_confirmed(Lang::Php, &outcome); } #[test] fn ruby_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert_confirmed(Lang::Ruby, &outcome); } #[test] fn js_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { + return; + }; assert_confirmed(Lang::JavaScript, &outcome); } #[test] fn go_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else { return }; + let Some(outcome) = run(Lang::Go, "vuln.go", "Run") else { + return; + }; assert_confirmed(Lang::Go, &outcome); } #[test] fn rust_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else { return }; + let Some(outcome) = run(Lang::Rust, "vuln.rs", "run") else { + return; + }; assert_confirmed(Lang::Rust, &outcome); } } diff --git a/tests/oracle_differential.rs b/tests/oracle_differential.rs index 210010a6..0fd739b6 100644 --- a/tests/oracle_differential.rs +++ b/tests/oracle_differential.rs @@ -81,7 +81,11 @@ fn sample_probe(callee: &str, arg: &str, label: &str) -> SinkProbe { #[test] fn build_outcome_confirmed_carries_both_traces() { - let vuln = vec![sample_probe("os.system", "; echo NYX_PWN_CMDI", "cmdi-echo-marker")]; + let vuln = vec![sample_probe( + "os.system", + "; echo NYX_PWN_CMDI", + "cmdi-echo-marker", + )]; let benign = vec![sample_probe("os.system", "benign_safe_cmdi", "cmdi-benign")]; let outcome = build_outcome( "cmdi-echo-marker", @@ -106,7 +110,10 @@ fn build_outcome_oracle_collision_keeps_both_traces() { let vuln = vec![sample_probe("os.system", "a", "v")]; let benign = vec![sample_probe("os.system", "b", "b")]; let outcome = build_outcome("v", true, &vuln, "b", true, &benign); - assert_eq!(outcome.verdict, DifferentialVerdict::OracleCollisionSuspected); + assert_eq!( + outcome.verdict, + DifferentialVerdict::OracleCollisionSuspected + ); assert_eq!(outcome.vuln_probes.len(), 1); assert_eq!(outcome.benign_probes.len(), 1); } diff --git a/tests/oracle_sink_crash.rs b/tests/oracle_sink_crash.rs index 0ea8837d..5a53e93c 100644 --- a/tests/oracle_sink_crash.rs +++ b/tests/oracle_sink_crash.rs @@ -21,13 +21,9 @@ mod common; -use nyx_scanner::dynamic::oracle::{ - oracle_fired, probe_crash_signal, Oracle, Signal, SignalSet, -}; +use nyx_scanner::dynamic::oracle::{Oracle, Signal, SignalSet, oracle_fired, probe_crash_signal}; use nyx_scanner::dynamic::policy; -use nyx_scanner::dynamic::probe::{ - ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe, -}; +use nyx_scanner::dynamic::probe::{ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::evidence::InconclusiveReason; use std::time::Duration; @@ -116,7 +112,10 @@ fn case_b_outside_sink_crash_does_not_fire_and_is_unrelated() { let dir = TempDir::new().unwrap(); let channel = ProbeChannel::for_workdir(dir.path()).unwrap(); let probes = channel.drain(); - assert!(probes.is_empty(), "no probe written from outside-sink abort"); + assert!( + probes.is_empty(), + "no probe written from outside-sink abort" + ); let oracle = Oracle::SinkCrash { signals: SignalSet::all(), @@ -131,8 +130,7 @@ fn case_b_outside_sink_crash_does_not_fire_and_is_unrelated() { // outcome + no probe with a crash signal. Lock the predicate // here so the runner's wiring in src/dynamic/runner.rs stays in // sync with what the test labels expect. - let process_crashed = - crashed_outcome().exit_code.is_none() && !crashed_outcome().timed_out; + let process_crashed = crashed_outcome().exit_code.is_none() && !crashed_outcome().timed_out; let has_sink_crash_probe = probes.iter().any(|p| probe_crash_signal(p).is_some()); let is_sink_crash_oracle = matches!(oracle, Oracle::SinkCrash { .. }); assert!(is_sink_crash_oracle && process_crashed && !has_sink_crash_probe); @@ -209,7 +207,10 @@ fn case_c_witness_capture_is_bounded_and_scrubbed() { assert_eq!(witness.cwd, "/tmp/nyx-run-1"); assert_eq!(witness.callee, "exec"); - assert_eq!(witness.args_repr, vec!["arg0".to_owned(), "arg1".to_owned()]); + assert_eq!( + witness.args_repr, + vec!["arg0".to_owned(), "arg1".to_owned()] + ); } #[test] @@ -266,13 +267,11 @@ fn signal_wire_format_accepts_canonical_and_short_aliases() { // The per-language shims write SIGSEGV / SIGABRT / etc. as the // signal value; downstream JSON consumers and the host-side oracle // both need to deserialise the same wire format. - let canonical = - serde_json::from_str::("\"SIGSEGV\"").expect("canonical SIG name"); + let canonical = serde_json::from_str::("\"SIGSEGV\"").expect("canonical SIG name"); assert_eq!(canonical, Signal::Sigsegv); let short = serde_json::from_str::("\"SEGV\"").expect("short alias"); assert_eq!(short, Signal::Sigsegv); - let title = - serde_json::from_str::("\"Sigsegv\"").expect("derive-default alias"); + let title = serde_json::from_str::("\"Sigsegv\"").expect("derive-default alias"); assert_eq!(title, Signal::Sigsegv); } @@ -310,10 +309,10 @@ fn signal_set_const_construction_is_order_independent() { mod e2e_phase_08 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::SandboxOptions; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; @@ -387,7 +386,9 @@ mod e2e_phase_08 { #[test] fn setup_fault_routes_to_unrelated_crash() { - let Some(outcome) = run("setup_fault.c") else { return }; + let Some(outcome) = run("setup_fault.c") else { + return; + }; assert!( outcome.triggered_by.is_none(), "setup_fault must not Confirm — handler is never installed: {outcome:?}", @@ -408,7 +409,9 @@ mod e2e_phase_08 { #[test] fn sink_fault_confirms_via_sink_crash_probe() { - let Some(outcome) = run("sink_fault.c") else { return }; + let Some(outcome) = run("sink_fault.c") else { + return; + }; assert!( outcome.triggered_by.is_some(), "sink_fault must Confirm via SinkCrash + differential: {outcome:?}", diff --git a/tests/oracle_sink_probe.rs b/tests/oracle_sink_probe.rs index ba1b911b..68c6ed12 100644 --- a/tests/oracle_sink_probe.rs +++ b/tests/oracle_sink_probe.rs @@ -17,9 +17,9 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ - ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe, PROBE_PATH_ENV, + PROBE_PATH_ENV, ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe, }; use std::time::Duration; use tempfile::TempDir; @@ -59,7 +59,9 @@ fn synthetic_harness_fires_probe( kind: ProbeKind::Normal, witness: ProbeWitness::empty(), }; - channel.write(&probe).expect("synthetic harness probe write"); + channel + .write(&probe) + .expect("synthetic harness probe write"); } /// "Control" harness — runs the same way but does NOT write a probe. diff --git a/tests/phase21_corpus.rs b/tests/phase21_corpus.rs index 6c5503e6..492a2629 100644 --- a/tests/phase21_corpus.rs +++ b/tests/phase21_corpus.rs @@ -121,12 +121,7 @@ fn graphql_resolver_supported_in_target_langs() { #[test] fn websocket_supported_in_target_langs() { - for lang in [ - Lang::Python, - Lang::JavaScript, - Lang::TypeScript, - Lang::Ruby, - ] { + for lang in [Lang::Python, Lang::JavaScript, Lang::TypeScript, Lang::Ruby] { assert!( lang::entry_kinds_supported(lang).contains(&EntryKindTag::WebSocket), "{lang:?} must advertise WebSocket after Phase 21", diff --git a/tests/php_fixtures.rs b/tests/php_fixtures.rs index 5e2ef65c..c2ca4db8 100644 --- a/tests/php_fixtures.rs +++ b/tests/php_fixtures.rs @@ -14,7 +14,7 @@ mod common; #[cfg(feature = "dynamic")] mod php_fixture_tests { use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, VerifyStatus, @@ -456,7 +456,7 @@ mod php_fixture_tests { #[cfg(feature = "dynamic")] mod phase15_shape_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -506,7 +506,15 @@ mod phase15_shape_tests { // return; };`. run_shape_fixture_lang_or_skip( &[Prerequisite::CommandAvailable("php")], - Lang::Php, "php", shape, file, func, cap, sink_line, kind, slot, + Lang::Php, + "php", + shape, + file, + func, + cap, + sink_line, + kind, + slot, ) } @@ -515,8 +523,13 @@ mod phase15_shape_tests { #[test] fn route_closure_vuln_is_confirmed() { let Some(r) = run( - "route_closure", "vuln.php", "run", Cap::CODE_EXEC, 10, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "route_closure", + "vuln.php", + "run", + Cap::CODE_EXEC, + 10, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -526,8 +539,13 @@ mod phase15_shape_tests { #[test] fn route_closure_benign_not_confirmed() { let Some(r) = run( - "route_closure", "benign.php", "run", Cap::CODE_EXEC, 11, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "route_closure", + "benign.php", + "run", + Cap::CODE_EXEC, + 11, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -539,8 +557,13 @@ mod phase15_shape_tests { #[test] fn cli_script_vuln_is_confirmed() { let Some(r) = run( - "cli_script", "vuln.php", "main", Cap::CODE_EXEC, 8, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "cli_script", + "vuln.php", + "main", + Cap::CODE_EXEC, + 8, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -550,8 +573,13 @@ mod phase15_shape_tests { #[test] fn cli_script_benign_not_confirmed() { let Some(r) = run( - "cli_script", "benign.php", "main", Cap::CODE_EXEC, 11, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "cli_script", + "benign.php", + "main", + Cap::CODE_EXEC, + 11, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -563,8 +591,13 @@ mod phase15_shape_tests { #[test] fn top_level_script_vuln_is_confirmed() { let Some(r) = run( - "top_level_script", "vuln.php", "", Cap::CODE_EXEC, 8, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "top_level_script", + "vuln.php", + "", + Cap::CODE_EXEC, + 8, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -574,8 +607,13 @@ mod phase15_shape_tests { #[test] fn top_level_script_benign_not_confirmed() { let Some(r) = run( - "top_level_script", "benign.php", "", Cap::CODE_EXEC, 10, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "top_level_script", + "benign.php", + "", + Cap::CODE_EXEC, + 10, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; diff --git a/tests/php_frameworks_corpus.rs b/tests/php_frameworks_corpus.rs index 4d899a2a..bdc62cbb 100644 --- a/tests/php_frameworks_corpus.rs +++ b/tests/php_frameworks_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; diff --git a/tests/policy_deny.rs b/tests/policy_deny.rs index d7f1ddf3..4c21173a 100644 --- a/tests/policy_deny.rs +++ b/tests/policy_deny.rs @@ -12,7 +12,7 @@ use nyx_scanner::commands::scan::Diag; use nyx_scanner::dynamic::policy::{self, DenyRule, PolicyDecision}; -use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; +use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, SpanEvidence, VerifyStatus, }; @@ -78,9 +78,7 @@ fn allow_returns_for_diag_without_secrets() { fn credentials_rule_fires_on_aws_key_in_flow_step_snippet() { let mut diag = empty_diag(); let mut ev = Evidence::default(); - ev.flow_steps = vec![flow_step_with_snippet( - "key=AKIAFAKETEST00000000", - )]; + ev.flow_steps = vec![flow_step_with_snippet("key=AKIAFAKETEST00000000")]; diag.evidence = Some(ev); match policy::evaluate(&diag) { PolicyDecision::Deny { @@ -116,9 +114,7 @@ fn credentials_rule_fires_on_bearer_header_note() { fn private_key_rule_fires_on_pem_block_in_snippet() { let mut diag = empty_diag(); let mut ev = Evidence::default(); - ev.source = Some(span_with_snippet( - "-----BEGIN OPENSSH PRIVATE KEY-----", - )); + ev.source = Some(span_with_snippet("-----BEGIN OPENSSH PRIVATE KEY-----")); diag.evidence = Some(ev); match policy::evaluate(&diag) { PolicyDecision::Deny { rule, .. } => { @@ -185,9 +181,7 @@ fn credentials_rule_fires_before_other_rules() { // endpoint name. Order asserted by the policy.evaluate impl. let mut diag = empty_diag(); let mut ev = Evidence::default(); - ev.notes = vec![ - "deploying key=AKIAFAKETEST00000000 to api.prod.example.com".to_owned(), - ]; + ev.notes = vec!["deploying key=AKIAFAKETEST00000000 to api.prod.example.com".to_owned()]; diag.evidence = Some(ev); match policy::evaluate(&diag) { PolicyDecision::Deny { rule, .. } => { diff --git a/tests/prototype_pollution_corpus.rs b/tests/prototype_pollution_corpus.rs index 07dea6cc..f3e995d9 100644 --- a/tests/prototype_pollution_corpus.rs +++ b/tests/prototype_pollution_corpus.rs @@ -16,12 +16,12 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; -use nyx_scanner::dynamic::oracle::{oracle_fired, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; @@ -63,7 +63,10 @@ fn corpus_registers_prototype_pollution_for_js_and_ts() { ); let has_vuln = slice.iter().any(|p| !p.is_benign); let has_benign = slice.iter().any(|p| p.is_benign); - assert!(has_vuln, "{lang:?} PROTOTYPE_POLLUTION missing vuln payload"); + assert!( + has_vuln, + "{lang:?} PROTOTYPE_POLLUTION missing vuln payload" + ); assert!( has_benign, "{lang:?} PROTOTYPE_POLLUTION missing benign control" @@ -111,10 +114,9 @@ fn payload_oracle_carries_prototype_canary_predicate() { match &vuln.oracle { Oracle::SinkProbe { predicates } => { assert!( - predicates.iter().any(|p| matches!( - p, - ProbePredicate::PrototypeCanaryTouched { .. } - )), + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::PrototypeCanaryTouched { .. })), "{lang:?} vuln payload missing PrototypeCanaryTouched predicate", ); } @@ -246,7 +248,9 @@ fn lang_emitter_dispatches_to_prototype_pollution_harness() { "{lang:?} harness must reference the canary property name", ); assert!( - harness.source.contains("Object.defineProperty(Object.prototype"), + harness + .source + .contains("Object.defineProperty(Object.prototype"), "{lang:?} harness must install the canary trap on Object.prototype", ); assert!( @@ -408,10 +412,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_10 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -523,13 +527,17 @@ mod e2e_phase_10 { #[test] fn js_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { + return; + }; assert_confirmed(Lang::JavaScript, &outcome); } #[test] fn ts_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::TypeScript, "vuln.ts", "run") else { return }; + let Some(outcome) = run(Lang::TypeScript, "vuln.ts", "run") else { + return; + }; assert_confirmed(Lang::TypeScript, &outcome); } } diff --git a/tests/python_fixtures.rs b/tests/python_fixtures.rs index 74ed8c34..8a94f5bb 100644 --- a/tests/python_fixtures.rs +++ b/tests/python_fixtures.rs @@ -14,15 +14,14 @@ mod common; #[cfg(feature = "dynamic")] mod python_fixture_tests { use crate::common::fixture_harness::{ - run_fixture_and_compare_to_golden, run_harness_snapshot, run_shape_fixture, - CopyStrategy, FixtureSpec, Prerequisite, + CopyStrategy, FixtureSpec, Prerequisite, run_fixture_and_compare_to_golden, + run_harness_snapshot, run_shape_fixture, }; use nyx_scanner::commands::scan::Diag; use nyx_scanner::dynamic::spec::PayloadSlot; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ - Confidence, EntryKind, Evidence, FlowStep, FlowStepKind, UnsupportedReason, - VerifyStatus, + Confidence, EntryKind, Evidence, FlowStep, FlowStepKind, UnsupportedReason, VerifyStatus, }; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; @@ -39,7 +38,12 @@ mod python_fixture_tests { .unwrap_or(false) } - fn spec(fixture: &'static str, func: &'static str, cap: Cap, sink_line: u32) -> FixtureSpec<'static> { + fn spec( + fixture: &'static str, + func: &'static str, + cap: Cap, + sink_line: u32, + ) -> FixtureSpec<'static> { FixtureSpec { lang_dir: "python", fixture, @@ -82,13 +86,19 @@ mod python_fixture_tests { #[test] fn sqli_positive_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec("sqli_positive.py", "login", Cap::SQL_QUERY, 17)); } #[test] fn sqli_negative_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec("sqli_negative.py", "login", Cap::SQL_QUERY, 12)); } @@ -104,22 +114,46 @@ mod python_fixture_tests { #[test] fn sqli_adversarial_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("sqli_adversarial.py", "get_value", Cap::SQL_QUERY, 999)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "sqli_adversarial.py", + "get_value", + Cap::SQL_QUERY, + 999, + )); } // ── Command injection ──────────────────────────────────────────────────── #[test] fn cmdi_positive_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("cmdi_positive.py", "run_ping", Cap::CODE_EXEC, 13)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "cmdi_positive.py", + "run_ping", + Cap::CODE_EXEC, + 13, + )); } #[test] fn cmdi_negative_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("cmdi_negative.py", "run_ping", Cap::CODE_EXEC, 17)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "cmdi_negative.py", + "run_ping", + Cap::CODE_EXEC, + 17, + )); } #[test] @@ -134,7 +168,10 @@ mod python_fixture_tests { #[test] fn cmdi_adversarial_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec( "cmdi_adversarial.py", "process_input", @@ -147,14 +184,30 @@ mod python_fixture_tests { #[test] fn fileio_positive_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("fileio_positive.py", "read_file", Cap::FILE_IO, 11)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "fileio_positive.py", + "read_file", + Cap::FILE_IO, + 11, + )); } #[test] fn fileio_negative_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("fileio_negative.py", "read_file", Cap::FILE_IO, 18)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "fileio_negative.py", + "read_file", + Cap::FILE_IO, + 18, + )); } #[test] @@ -169,21 +222,35 @@ mod python_fixture_tests { #[test] fn fileio_adversarial_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("fileio_adversarial.py", "read_file", Cap::FILE_IO, 999)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "fileio_adversarial.py", + "read_file", + Cap::FILE_IO, + 999, + )); } // ── SSRF ───────────────────────────────────────────────────────────────── #[test] fn ssrf_positive_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec("ssrf_positive.py", "fetch_url", Cap::SSRF, 11)); } #[test] fn ssrf_negative_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec("ssrf_negative.py", "fetch_url", Cap::SSRF, 26)); } @@ -194,15 +261,26 @@ mod python_fixture_tests { #[test] fn ssrf_adversarial_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } - run_fixture_and_compare_to_golden(&spec("ssrf_adversarial.py", "fetch_url", Cap::SSRF, 999)); + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } + run_fixture_and_compare_to_golden(&spec( + "ssrf_adversarial.py", + "fetch_url", + Cap::SSRF, + 999, + )); } // ── XSS ────────────────────────────────────────────────────────────────── #[test] fn xss_positive_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec( "xss_positive.py", "render_comment", @@ -213,7 +291,10 @@ mod python_fixture_tests { #[test] fn xss_negative_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec( "xss_negative.py", "render_comment", @@ -234,7 +315,10 @@ mod python_fixture_tests { #[test] fn xss_adversarial_matches_golden() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } run_fixture_and_compare_to_golden(&spec( "xss_adversarial.py", "render_comment", @@ -342,20 +426,36 @@ mod python_fixture_tests { #[test] fn generic_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "generic", "vuln.py", "run_ping", Cap::CODE_EXEC, 12, - EntryKind::Function, PayloadSlot::Param(0), + "generic", + "vuln.py", + "run_ping", + Cap::CODE_EXEC, + 12, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_confirmed("generic", &r); } #[test] fn generic_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "generic", "benign.py", "run_ping", Cap::CODE_EXEC, 20, - EntryKind::Function, PayloadSlot::Param(0), + "generic", + "benign.py", + "run_ping", + Cap::CODE_EXEC, + 20, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_not_confirmed("generic", &r); } @@ -363,8 +463,13 @@ mod python_fixture_tests { #[test] fn generic_harness_snapshot_matches_golden() { run_harness_snapshot( - "generic", "vuln.py", "run_ping", Cap::CODE_EXEC, 12, - EntryKind::Function, PayloadSlot::Param(0), + "generic", + "vuln.py", + "run_ping", + Cap::CODE_EXEC, + 12, + EntryKind::Function, + PayloadSlot::Param(0), ); } @@ -372,20 +477,36 @@ mod python_fixture_tests { #[test] fn cli_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "cli", "vuln.py", "main", Cap::CODE_EXEC, 14, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "cli", + "vuln.py", + "main", + Cap::CODE_EXEC, + 14, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ); assert_confirmed("cli", &r); } #[test] fn cli_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "cli", "benign.py", "main", Cap::CODE_EXEC, 11, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "cli", + "benign.py", + "main", + Cap::CODE_EXEC, + 11, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ); assert_not_confirmed("cli", &r); } @@ -393,8 +514,13 @@ mod python_fixture_tests { #[test] fn cli_harness_snapshot_matches_golden() { run_harness_snapshot( - "cli", "vuln.py", "main", Cap::CODE_EXEC, 14, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "cli", + "vuln.py", + "main", + Cap::CODE_EXEC, + 14, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ); } @@ -402,20 +528,36 @@ mod python_fixture_tests { #[test] fn pytest_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "pytest", "vuln.py", "test_run_ping", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "pytest", + "vuln.py", + "test_run_ping", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ); assert_confirmed("pytest", &r); } #[test] fn pytest_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "pytest", "benign.py", "test_run_ping", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "pytest", + "benign.py", + "test_run_ping", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ); assert_not_confirmed("pytest", &r); } @@ -423,8 +565,13 @@ mod python_fixture_tests { #[test] fn pytest_harness_snapshot_matches_golden() { run_harness_snapshot( - "pytest", "vuln.py", "test_run_ping", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "pytest", + "vuln.py", + "test_run_ping", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ); } @@ -432,20 +579,36 @@ mod python_fixture_tests { #[test] fn async_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "async", "vuln.py", "run_ping", Cap::CODE_EXEC, 13, - EntryKind::Function, PayloadSlot::Param(0), + "async", + "vuln.py", + "run_ping", + Cap::CODE_EXEC, + 13, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_confirmed("async", &r); } #[test] fn async_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } let r = run_shape_fixture( - "async", "benign.py", "run_ping", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), + "async", + "benign.py", + "run_ping", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_not_confirmed("async", &r); } @@ -453,8 +616,13 @@ mod python_fixture_tests { #[test] fn async_harness_snapshot_matches_golden() { run_harness_snapshot( - "async", "vuln.py", "run_ping", Cap::CODE_EXEC, 13, - EntryKind::Function, PayloadSlot::Param(0), + "async", + "vuln.py", + "run_ping", + Cap::CODE_EXEC, + 13, + EntryKind::Function, + PayloadSlot::Param(0), ); } @@ -462,28 +630,44 @@ mod python_fixture_tests { #[test] fn celery_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("celery") { eprintln!("SKIP: celery not importable"); return; } let r = run_shape_fixture( - "celery", "vuln.py", "run_job", Cap::CODE_EXEC, 17, - EntryKind::Function, PayloadSlot::Param(0), + "celery", + "vuln.py", + "run_job", + Cap::CODE_EXEC, + 17, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_confirmed("celery", &r); } #[test] fn celery_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("celery") { eprintln!("SKIP: celery not importable"); return; } let r = run_shape_fixture( - "celery", "benign.py", "run_job", Cap::CODE_EXEC, 17, - EntryKind::Function, PayloadSlot::Param(0), + "celery", + "benign.py", + "run_job", + Cap::CODE_EXEC, + 17, + EntryKind::Function, + PayloadSlot::Param(0), ); assert_not_confirmed("celery", &r); } @@ -491,8 +675,13 @@ mod python_fixture_tests { #[test] fn celery_harness_snapshot_matches_golden() { run_harness_snapshot( - "celery", "vuln.py", "run_job", Cap::CODE_EXEC, 17, - EntryKind::Function, PayloadSlot::Param(0), + "celery", + "vuln.py", + "run_job", + Cap::CODE_EXEC, + 17, + EntryKind::Function, + PayloadSlot::Param(0), ); } @@ -500,28 +689,44 @@ mod python_fixture_tests { #[test] fn flask_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("flask") { eprintln!("SKIP: flask not importable"); return; } let r = run_shape_fixture( - "flask", "vuln.py", "ping", Cap::CODE_EXEC, 18, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "flask", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 18, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_confirmed("flask", &r); } #[test] fn flask_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("flask") { eprintln!("SKIP: flask not importable"); return; } let r = run_shape_fixture( - "flask", "benign.py", "ping", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "flask", + "benign.py", + "ping", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_not_confirmed("flask", &r); } @@ -529,8 +734,13 @@ mod python_fixture_tests { #[test] fn flask_harness_snapshot_matches_golden() { run_harness_snapshot( - "flask", "vuln.py", "ping", Cap::CODE_EXEC, 18, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "flask", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 18, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); } @@ -538,28 +748,44 @@ mod python_fixture_tests { #[test] fn fastapi_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("fastapi") { eprintln!("SKIP: fastapi not importable"); return; } let r = run_shape_fixture( - "fastapi", "vuln.py", "ping", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "fastapi", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_confirmed("fastapi", &r); } #[test] fn fastapi_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("fastapi") { eprintln!("SKIP: fastapi not importable"); return; } let r = run_shape_fixture( - "fastapi", "benign.py", "ping", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "fastapi", + "benign.py", + "ping", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_not_confirmed("fastapi", &r); } @@ -567,8 +793,13 @@ mod python_fixture_tests { #[test] fn fastapi_harness_snapshot_matches_golden() { run_harness_snapshot( - "fastapi", "vuln.py", "ping", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "fastapi", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); } @@ -576,28 +807,44 @@ mod python_fixture_tests { #[test] fn django_vuln_is_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("django") { eprintln!("SKIP: django not importable"); return; } let r = run_shape_fixture( - "django", "vuln.py", "ping", Cap::CODE_EXEC, 15, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "django", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 15, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_confirmed("django", &r); } #[test] fn django_benign_not_confirmed() { - if !python3_available() { eprintln!("SKIP: python3 not available"); return; } + if !python3_available() { + eprintln!("SKIP: python3 not available"); + return; + } if !python_module_available("django") { eprintln!("SKIP: django not importable"); return; } let r = run_shape_fixture( - "django", "benign.py", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "django", + "benign.py", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); assert_not_confirmed("django", &r); } @@ -605,8 +852,13 @@ mod python_fixture_tests { #[test] fn django_harness_snapshot_matches_golden() { run_harness_snapshot( - "django", "vuln.py", "ping", Cap::CODE_EXEC, 15, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), + "django", + "vuln.py", + "ping", + Cap::CODE_EXEC, + 15, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), ); } diff --git a/tests/python_frameworks_corpus.rs b/tests/python_frameworks_corpus.rs index a0b96efa..33e14234 100644 --- a/tests/python_frameworks_corpus.rs +++ b/tests/python_frameworks_corpus.rs @@ -21,7 +21,7 @@ mod common; -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; @@ -193,10 +193,10 @@ fn fastapi_adapter_runs_before_starlette_for_fastapi_files() { #[cfg(feature = "dynamic")] mod e2e_phase_12 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::SandboxOptions; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -277,7 +277,9 @@ mod e2e_phase_12 { } fn assert_confirmed(fixture_subdir: &str) { - let Some(outcome) = run(fixture_subdir) else { return }; + let Some(outcome) = run(fixture_subdir) else { + return; + }; assert!( outcome.triggered_by.is_some(), "{fixture_subdir} CODE_EXEC vuln must Confirm via run_spec; got {outcome:?}", diff --git a/tests/repro_determinism.rs b/tests/repro_determinism.rs index 16d409d3..3f8c5757 100644 --- a/tests/repro_determinism.rs +++ b/tests/repro_determinism.rs @@ -90,16 +90,19 @@ mod repro_determinism_tests { // Write repro bundle (first time). let artifact1 = repro::write( - &spec, &opts, &outcome, &verdict, + &spec, + &opts, + &outcome, + &verdict, "# harness source v1\n", "def login(x): pass\n", b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("first repro write must succeed"); + ) + .expect("first repro write must succeed"); - let outcome_json_1 = - std::fs::read_to_string(artifact1.root.join("expected/outcome.json")) + let outcome_json_1 = std::fs::read_to_string(artifact1.root.join("expected/outcome.json")) .expect("outcome.json must exist after first write"); // Write repro bundle (second time, same inputs). @@ -107,16 +110,19 @@ mod repro_determinism_tests { std::fs::remove_dir_all(&artifact1.root).unwrap(); let artifact2 = repro::write( - &spec, &opts, &outcome, &verdict, + &spec, + &opts, + &outcome, + &verdict, "# harness source v1\n", "def login(x): pass\n", b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("second repro write must succeed"); + ) + .expect("second repro write must succeed"); - let outcome_json_2 = - std::fs::read_to_string(artifact2.root.join("expected/outcome.json")) + let outcome_json_2 = std::fs::read_to_string(artifact2.root.join("expected/outcome.json")) .expect("outcome.json must exist after second write"); assert_eq!( @@ -141,9 +147,17 @@ mod repro_determinism_tests { let verdict = make_confirmed_verdict("determinism00002"); let artifact = repro::write( - &spec, &opts, &outcome, &verdict, - "# harness", "# entry", b"payload", "label", None, - ).expect("repro write must succeed"); + &spec, + &opts, + &outcome, + &verdict, + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .expect("repro write must succeed"); let outcome_json = std::fs::read_to_string(artifact.root.join("expected/outcome.json")).unwrap(); @@ -262,8 +276,7 @@ fn main() { None, ) .expect("first Rust repro write"); - let json1 = - std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); + let json1 = std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); std::fs::remove_dir_all(&artifact1.root).unwrap(); @@ -279,8 +292,7 @@ fn main() { None, ) .expect("second Rust repro write"); - let json2 = - std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); + let json2 = std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); assert_eq!( json1, json2, @@ -325,24 +337,39 @@ fn main() { let entry_src = "function login(username) { console.log(username); }\n"; let artifact1 = repro::write( - &spec, &opts, &outcome, &verdict, - "// harness js\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("first JS repro write"); - let json1 = - std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// harness js\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("first JS repro write"); + let json1 = std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); std::fs::remove_dir_all(&artifact1.root).unwrap(); let artifact2 = repro::write( - &spec, &opts, &outcome, &verdict, - "// harness js\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("second JS repro write"); - let json2 = - std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// harness js\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("second JS repro write"); + let json2 = std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); - assert_eq!(json1, json2, "JS outcome.json must be byte-identical across two writes"); + assert_eq!( + json1, json2, + "JS outcome.json must be byte-identical across two writes" + ); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } @@ -382,24 +409,39 @@ fn main() { let entry_src = "package entry\nfunc Login(username string) {}\n"; let artifact1 = repro::write( - &spec, &opts, &outcome, &verdict, - "// harness go\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("first Go repro write"); - let json1 = - std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// harness go\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("first Go repro write"); + let json1 = std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); std::fs::remove_dir_all(&artifact1.root).unwrap(); let artifact2 = repro::write( - &spec, &opts, &outcome, &verdict, - "// harness go\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("second Go repro write"); - let json2 = - std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// harness go\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("second Go repro write"); + let json2 = std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); - assert_eq!(json1, json2, "Go outcome.json must be byte-identical across two writes"); + assert_eq!( + json1, json2, + "Go outcome.json must be byte-identical across two writes" + ); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } @@ -439,24 +481,39 @@ fn main() { let entry_src = "public class Entry { public static void login(String u) {} }\n"; let artifact1 = repro::write( - &spec, &opts, &outcome, &verdict, - "// NyxHarness.java\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("first Java repro write"); - let json1 = - std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// NyxHarness.java\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("first Java repro write"); + let json1 = std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap(); std::fs::remove_dir_all(&artifact1.root).unwrap(); let artifact2 = repro::write( - &spec, &opts, &outcome, &verdict, - "// NyxHarness.java\n", entry_src, - b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None, - ).expect("second Java repro write"); - let json2 = - std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); + &spec, + &opts, + &outcome, + &verdict, + "// NyxHarness.java\n", + entry_src, + b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", + "sqli-union-nyx", + None, + ) + .expect("second Java repro write"); + let json2 = std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap(); - assert_eq!(json1, json2, "Java outcome.json must be byte-identical across two writes"); + assert_eq!( + json1, json2, + "Java outcome.json must be byte-identical across two writes" + ); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } @@ -496,24 +553,39 @@ fn main() { let entry_src = " VerifyResult { reason: None, inconclusive_reason: None, detail: Some( - "flask_eval chain composer fixture: eval(NYX_PAYLOAD) under python-3.11" - .into(), + "flask_eval chain composer fixture: eval(NYX_PAYLOAD) under python-3.11".into(), ), attempts: vec![AttemptSummary { payload_label: FLASK_EVAL_PAYLOAD_LABEL.into(), @@ -167,10 +166,8 @@ fn flask_eval_bundle_root() -> PathBuf { } fn read_json(path: &Path) -> serde_json::Value { - let bytes = std::fs::read(path) - .unwrap_or_else(|e| panic!("read {}: {e}", path.display())); - serde_json::from_slice(&bytes) - .unwrap_or_else(|e| panic!("parse {}: {e}", path.display())) + let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", path.display())) } /// Regenerate the committed flask_eval bundle. Run with `--ignored` to @@ -206,8 +203,7 @@ fn regen_python_3_11_flask_eval_bundle() { } assert_eq!( - artifact.root, - bundle_root, + artifact.root, bundle_root, "bundle wrote to unexpected path", ); } diff --git a/tests/repro_hermetic.rs b/tests/repro_hermetic.rs index 1ca052c2..d81905be 100644 --- a/tests/repro_hermetic.rs +++ b/tests/repro_hermetic.rs @@ -29,7 +29,7 @@ #[cfg(feature = "dynamic")] mod repro_hermetic_tests { use nyx_scanner::dynamic::repro; - use nyx_scanner::dynamic::repro::{replay_bundle, ReplayResult}; + use nyx_scanner::dynamic::repro::{ReplayResult, replay_bundle}; use nyx_scanner::dynamic::sandbox::{SandboxOptions, SandboxOutcome}; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; use nyx_scanner::evidence::{AttemptSummary, VerifyResult, VerifyStatus}; @@ -110,7 +110,8 @@ mod repro_hermetic_tests { b"' OR 1=1-- NYX", "sqli-or-1", None, - ).unwrap(); + ) + .unwrap(); let lock_path = artifact.root.join("toolchain.lock"); assert!(lock_path.exists(), "toolchain.lock missing from bundle"); @@ -135,10 +136,16 @@ mod repro_hermetic_tests { b"' OR 1=1-- NYX", "sqli-or-1", None, - ).unwrap(); - let lock2: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(artifact2.root.join("toolchain.lock")).unwrap()).unwrap(); - assert_eq!(lock["files"], lock2["files"], "lock file hashes must be deterministic"); + ) + .unwrap(); + let lock2: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(artifact2.root.join("toolchain.lock")).unwrap(), + ) + .unwrap(); + assert_eq!( + lock["files"], lock2["files"], + "lock file hashes must be deterministic" + ); unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } @@ -162,7 +169,8 @@ mod repro_hermetic_tests { b"payload", "label", None, - ).unwrap(); + ) + .unwrap(); // Simulate "no language toolchain installed" by stripping PATH // down to /usr/bin (where `sh`, `grep`, `cat` live) before @@ -188,15 +196,14 @@ mod repro_hermetic_tests { // running the (broken) harness. Detect that and skip — Phase // 28 acceptance is about the refusal path, not the host-has-it // path. - let host_has_python = - std::process::Command::new("sh") - .arg("-c") - .arg("command -v python3") - .env_clear() - .env("PATH", &minimal_path) - .output() - .map(|o| o.status.success()) - .unwrap_or(false); + let host_has_python = std::process::Command::new("sh") + .arg("-c") + .arg("command -v python3") + .env_clear() + .env("PATH", &minimal_path) + .output() + .map(|o| o.status.success()) + .unwrap_or(false); if host_has_python { eprintln!("skip: host has python3 in minimal PATH; cannot simulate clean CI image"); return; @@ -234,14 +241,16 @@ mod repro_hermetic_tests { std::fs::write( bundle.join("reproduce.sh"), "#!/bin/sh\necho 'host toolchain missing' >&2\nexit 3\n", - ).unwrap(); + ) + .unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions( bundle.join("reproduce.sh"), std::fs::Permissions::from_mode(0o755), - ).unwrap(); + ) + .unwrap(); } assert_eq!(replay_bundle(&bundle, &[]), ReplayResult::ToolchainMismatch); } @@ -254,14 +263,16 @@ mod repro_hermetic_tests { std::fs::write( bundle.join("reproduce.sh"), "#!/bin/sh\necho 'PASS: simulated green'\nexit 0\n", - ).unwrap(); + ) + .unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions( bundle.join("reproduce.sh"), std::fs::Permissions::from_mode(0o755), - ).unwrap(); + ) + .unwrap(); } assert_eq!(replay_bundle(&bundle, &[]), ReplayResult::Pass); } @@ -284,11 +295,15 @@ mod repro_hermetic_tests { &SandboxOptions::default(), &make_outcome(), &make_verdict(), - "# harness", "# entry", b"payload", "label", None, - ).unwrap(); + "# harness", + "# entry", + b"payload", + "label", + None, + ) + .unwrap(); - let pinned = - nyx_scanner::dynamic::toolchain::pinned_image_ref(&spec.toolchain_id); + let pinned = nyx_scanner::dynamic::toolchain::pinned_image_ref(&spec.toolchain_id); if pinned.is_some() { assert!( artifact.root.join("docker_pull.sh").exists(), diff --git a/tests/ruby_fixtures.rs b/tests/ruby_fixtures.rs index 93c94a43..18a0dcdb 100644 --- a/tests/ruby_fixtures.rs +++ b/tests/ruby_fixtures.rs @@ -13,7 +13,7 @@ mod common; #[cfg(feature = "dynamic")] mod phase15_shape_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -77,8 +77,13 @@ mod phase15_shape_tests { #[test] fn sinatra_route_vuln_is_confirmed() { let Some(r) = run( - "sinatra_route", "vuln.rb", "run", Cap::CODE_EXEC, 7, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "sinatra_route", + "vuln.rb", + "run", + Cap::CODE_EXEC, + 7, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -88,8 +93,13 @@ mod phase15_shape_tests { #[test] fn sinatra_route_benign_not_confirmed() { let Some(r) = run( - "sinatra_route", "benign.rb", "run", Cap::CODE_EXEC, 10, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "sinatra_route", + "benign.rb", + "run", + Cap::CODE_EXEC, + 10, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -101,8 +111,13 @@ mod phase15_shape_tests { #[test] fn rails_action_vuln_is_confirmed() { let Some(r) = run( - "rails_action", "vuln.rb", "index", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "rails_action", + "vuln.rb", + "index", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -112,8 +127,13 @@ mod phase15_shape_tests { #[test] fn rails_action_benign_not_confirmed() { let Some(r) = run( - "rails_action", "benign.rb", "index", Cap::CODE_EXEC, 20, - EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "rails_action", + "benign.rb", + "index", + Cap::CODE_EXEC, + 20, + EntryKind::HttpRoute, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -125,8 +145,13 @@ mod phase15_shape_tests { #[test] fn rack_middleware_vuln_is_confirmed() { let Some(r) = run( - "rack_middleware", "vuln.rb", "call", Cap::CODE_EXEC, 9, - EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "rack_middleware", + "vuln.rb", + "call", + Cap::CODE_EXEC, + 9, + EntryKind::HttpRoute, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -136,8 +161,13 @@ mod phase15_shape_tests { #[test] fn rack_middleware_benign_not_confirmed() { let Some(r) = run( - "rack_middleware", "benign.rb", "call", Cap::CODE_EXEC, 11, - EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()), + "rack_middleware", + "benign.rb", + "call", + Cap::CODE_EXEC, + 11, + EntryKind::HttpRoute, + PayloadSlot::EnvVar("NYX_PAYLOAD".into()), ) else { return; }; @@ -149,8 +179,13 @@ mod phase15_shape_tests { #[test] fn controller_method_vuln_is_confirmed() { let Some(r) = run( - "controller_method", "vuln.rb", "authenticate", Cap::CODE_EXEC, 7, - EntryKind::Function, PayloadSlot::Param(0), + "controller_method", + "vuln.rb", + "authenticate", + Cap::CODE_EXEC, + 7, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; @@ -160,8 +195,13 @@ mod phase15_shape_tests { #[test] fn controller_method_benign_not_confirmed() { let Some(r) = run( - "controller_method", "benign.rb", "authenticate", Cap::CODE_EXEC, 10, - EntryKind::Function, PayloadSlot::Param(0), + "controller_method", + "benign.rb", + "authenticate", + Cap::CODE_EXEC, + 10, + EntryKind::Function, + PayloadSlot::Param(0), ) else { return; }; diff --git a/tests/ruby_frameworks_corpus.rs b/tests/ruby_frameworks_corpus.rs index 01b51c31..f8c7de19 100644 --- a/tests/ruby_frameworks_corpus.rs +++ b/tests/ruby_frameworks_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; @@ -155,8 +155,8 @@ fn sinatra_does_not_fire_on_rails_controller() { let bytes = std::fs::read(path).expect("rails vuln fixture exists"); let tree = parse_ruby(&bytes); let summary = summary_for("index", path); - let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby) - .expect("adapter binds"); + let binding = + detect_binding(&summary, tree.root_node(), &bytes, Lang::Ruby).expect("adapter binds"); // First-match-wins ordering must produce `ruby-rails`, not // `ruby-sinatra`, even if both adapters could in theory match. assert_eq!(binding.adapter, "ruby-rails"); diff --git a/tests/rust_fixtures.rs b/tests/rust_fixtures.rs index 7e39de51..1637a3c4 100644 --- a/tests/rust_fixtures.rs +++ b/tests/rust_fixtures.rs @@ -12,18 +12,21 @@ mod common; #[cfg(feature = "dynamic")] mod rust_fixture_tests { use crate::common::fixture_harness::{ - run_fixture_and_compare_to_golden, CopyStrategy, FixtureSpec, Prerequisite, + CopyStrategy, FixtureSpec, Prerequisite, run_fixture_and_compare_to_golden, }; use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{ - Confidence, Evidence, FlowStep, FlowStepKind, - }; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; use std::path::{Path, PathBuf}; - fn spec(fixture: &'static str, func: &'static str, cap: Cap, sink_line: u32) -> FixtureSpec<'static> { + fn spec( + fixture: &'static str, + func: &'static str, + cap: Cap, + sink_line: u32, + ) -> FixtureSpec<'static> { FixtureSpec { lang_dir: "rust", fixture, @@ -290,7 +293,7 @@ mod rust_fixture_tests { #[cfg(feature = "dynamic")] mod phase16_shape_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -357,8 +360,13 @@ mod phase16_shape_tests { #[test] fn actix_route_vuln_is_confirmed() { let Some(r) = run( - "actix_route", "vuln.rs", "handler", Cap::CODE_EXEC, 16, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "actix_route", + "vuln.rs", + "handler", + Cap::CODE_EXEC, + 16, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -368,8 +376,13 @@ mod phase16_shape_tests { #[test] fn actix_route_benign_not_confirmed() { let Some(r) = run( - "actix_route", "benign.rs", "handler", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "actix_route", + "benign.rs", + "handler", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -381,8 +394,13 @@ mod phase16_shape_tests { #[test] fn axum_handler_vuln_is_confirmed() { let Some(r) = run( - "axum_handler", "vuln.rs", "handler", Cap::CODE_EXEC, 15, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "axum_handler", + "vuln.rs", + "handler", + Cap::CODE_EXEC, + 15, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -392,8 +410,13 @@ mod phase16_shape_tests { #[test] fn axum_handler_benign_not_confirmed() { let Some(r) = run( - "axum_handler", "benign.rs", "handler", Cap::CODE_EXEC, 13, - EntryKind::HttpRoute, PayloadSlot::Param(0), + "axum_handler", + "benign.rs", + "handler", + Cap::CODE_EXEC, + 13, + EntryKind::HttpRoute, + PayloadSlot::Param(0), ) else { return; }; @@ -405,8 +428,13 @@ mod phase16_shape_tests { #[test] fn clap_cli_vuln_is_confirmed() { let Some(r) = run( - "clap_cli", "vuln.rs", "run", Cap::CODE_EXEC, 17, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "clap_cli", + "vuln.rs", + "run", + Cap::CODE_EXEC, + 17, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -416,8 +444,13 @@ mod phase16_shape_tests { #[test] fn clap_cli_benign_not_confirmed() { let Some(r) = run( - "clap_cli", "benign.rs", "run", Cap::CODE_EXEC, 13, - EntryKind::CliSubcommand, PayloadSlot::Argv(0), + "clap_cli", + "benign.rs", + "run", + Cap::CODE_EXEC, + 13, + EntryKind::CliSubcommand, + PayloadSlot::Argv(0), ) else { return; }; @@ -429,8 +462,13 @@ mod phase16_shape_tests { #[test] fn libfuzzer_target_vuln_is_confirmed() { let Some(r) = run( - "libfuzzer_target", "vuln.rs", "fuzz_target", Cap::CODE_EXEC, 15, - EntryKind::LibraryApi, PayloadSlot::Param(0), + "libfuzzer_target", + "vuln.rs", + "fuzz_target", + Cap::CODE_EXEC, + 15, + EntryKind::LibraryApi, + PayloadSlot::Param(0), ) else { return; }; @@ -440,8 +478,13 @@ mod phase16_shape_tests { #[test] fn libfuzzer_target_benign_not_confirmed() { let Some(r) = run( - "libfuzzer_target", "benign.rs", "fuzz_target", Cap::CODE_EXEC, 13, - EntryKind::LibraryApi, PayloadSlot::Param(0), + "libfuzzer_target", + "benign.rs", + "fuzz_target", + Cap::CODE_EXEC, + 13, + EntryKind::LibraryApi, + PayloadSlot::Param(0), ) else { return; }; diff --git a/tests/rust_frameworks_corpus.rs b/tests/rust_frameworks_corpus.rs index d6eab037..a62900fb 100644 --- a/tests/rust_frameworks_corpus.rs +++ b/tests/rust_frameworks_corpus.rs @@ -11,7 +11,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod}; +use nyx_scanner::dynamic::framework::{HttpMethod, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; diff --git a/tests/sandbox_docker.rs b/tests/sandbox_docker.rs index 18dfe1a9..343dfe85 100644 --- a/tests/sandbox_docker.rs +++ b/tests/sandbox_docker.rs @@ -16,8 +16,8 @@ use nyx_scanner::dynamic::harness::BuiltHarness; use nyx_scanner::dynamic::sandbox::docker::{ - ensure_image_pulled, image_reference_for_toolchain, network_args, stub_mount_args, - toolchain_is_pinned, workdir_mount_args, STUB_MOUNT_ROOT, WORK_MOUNT_PATH, + STUB_MOUNT_ROOT, WORK_MOUNT_PATH, ensure_image_pulled, image_reference_for_toolchain, + network_args, stub_mount_args, toolchain_is_pinned, workdir_mount_args, }; use nyx_scanner::dynamic::sandbox::{ self, HostPort, NetworkPolicy, SandboxBackend, SandboxOptions, @@ -87,12 +87,20 @@ fn stub_mount_args_uses_indexed_fixed_paths() { #[test] fn network_args_translate_every_policy() { - assert!(network_args(&NetworkPolicy::None).iter().any(|a| a == "none")); + assert!( + network_args(&NetworkPolicy::None) + .iter() + .any(|a| a == "none") + ); let stubs = NetworkPolicy::StubsOnly { allow: vec![HostPort::new("sql", 5432)], }; let stubs_args = network_args(&stubs); - assert!(stubs_args.iter().any(|a| a == "--add-host=sql:host-gateway")); + assert!( + stubs_args + .iter() + .any(|a| a == "--add-host=sql:host-gateway") + ); let open = network_args(&NetworkPolicy::Open); assert!(open.iter().any(|a| a == "bridge")); assert!(!open.iter().any(|a| a.starts_with("--add-host="))); @@ -117,9 +125,15 @@ fn toolchain_pinning_state_is_observable() { let pinned = toolchain_is_pinned("python-3.11"); let r = image_reference_for_toolchain("python-3.11").unwrap(); if pinned { - assert!(r.contains("@sha256:"), "pinned ref must carry digest, got {r}"); + assert!( + r.contains("@sha256:"), + "pinned ref must carry digest, got {r}" + ); } else { - assert!(!r.contains("@sha256:"), "unpinned ref must not carry digest, got {r}"); + assert!( + !r.contains("@sha256:"), + "unpinned ref must not carry digest, got {r}" + ); } } @@ -131,8 +145,8 @@ fn ensure_image_pulled_returns_true_for_python_slim() { eprintln!("docker unavailable — skipping"); return; } - let r = image_reference_for_toolchain("python-3.11") - .expect("python-3.11 must be in the catalogue"); + let r = + image_reference_for_toolchain("python-3.11").expect("python-3.11 must be in the catalogue"); assert!( ensure_image_pulled(r), "ensure_image_pulled must succeed for `{r}` when docker is available", @@ -170,8 +184,7 @@ fn harness_workdir_is_mounted_at_fixed_work_path() { return; } let tmp = tempfile::TempDir::new().expect("tempdir"); - std::fs::write(tmp.path().join("token.txt"), "phase-19-mount-token\n") - .expect("write fixture"); + std::fs::write(tmp.path().join("token.txt"), "phase-19-mount-token\n").expect("write fixture"); write_harness_script( tmp.path(), // Read from the fixed /work mount path — this passes only when the diff --git a/tests/sandbox_escape_suite.rs b/tests/sandbox_escape_suite.rs index 76dff77e..f9430238 100644 --- a/tests/sandbox_escape_suite.rs +++ b/tests/sandbox_escape_suite.rs @@ -155,7 +155,10 @@ mod escape_suite { unsafe { std::env::set_var(format!("NYX_ESCAPE_DYN_{technique}_{variant}"), "1") }; } - builds().lock().unwrap().insert(key.clone(), Some(out_bin.clone())); + builds() + .lock() + .unwrap() + .insert(key.clone(), Some(out_bin.clone())); Some(out_bin) } @@ -291,34 +294,58 @@ mod escape_suite { // keep the build dependency-free. #[test] - fn chmod_4755_benign() { let _ = assert_contained("chmod_4755", "benign"); } + fn chmod_4755_benign() { + let _ = assert_contained("chmod_4755", "benign"); + } #[test] - fn chmod_4755_vuln() { let _ = assert_contained("chmod_4755", "vuln"); } + fn chmod_4755_vuln() { + let _ = assert_contained("chmod_4755", "vuln"); + } #[test] - fn etc_write_benign() { let _ = assert_contained("etc_write", "benign"); } + fn etc_write_benign() { + let _ = assert_contained("etc_write", "benign"); + } #[test] - fn etc_write_vuln() { let _ = assert_contained("etc_write", "vuln"); } + fn etc_write_vuln() { + let _ = assert_contained("etc_write", "vuln"); + } #[test] - fn dlopen_outside_chroot_benign() { let _ = assert_contained("dlopen_outside_chroot", "benign"); } + fn dlopen_outside_chroot_benign() { + let _ = assert_contained("dlopen_outside_chroot", "benign"); + } #[test] - fn dlopen_outside_chroot_vuln() { let _ = assert_contained("dlopen_outside_chroot", "vuln"); } + fn dlopen_outside_chroot_vuln() { + let _ = assert_contained("dlopen_outside_chroot", "vuln"); + } #[test] - fn proc_root_passwd_benign() { let _ = assert_contained("proc_root_passwd", "benign"); } + fn proc_root_passwd_benign() { + let _ = assert_contained("proc_root_passwd", "benign"); + } #[test] - fn proc_root_passwd_vuln() { let _ = assert_contained("proc_root_passwd", "vuln"); } + fn proc_root_passwd_vuln() { + let _ = assert_contained("proc_root_passwd", "vuln"); + } #[test] - fn raw_socket_bind_benign() { let _ = assert_contained("raw_socket_bind", "benign"); } + fn raw_socket_bind_benign() { + let _ = assert_contained("raw_socket_bind", "benign"); + } #[test] - fn raw_socket_bind_vuln() { let _ = assert_contained("raw_socket_bind", "vuln"); } + fn raw_socket_bind_vuln() { + let _ = assert_contained("raw_socket_bind", "vuln"); + } #[test] - fn setuid_zero_benign() { let _ = assert_contained("setuid_zero", "benign"); } + fn setuid_zero_benign() { + let _ = assert_contained("setuid_zero", "benign"); + } #[test] - fn setuid_zero_vuln() { let _ = assert_contained("setuid_zero", "vuln"); } + fn setuid_zero_vuln() { + let _ = assert_contained("setuid_zero", "vuln"); + } // ── Track-B regression tripwire ────────────────────────────────────────── diff --git a/tests/sandbox_hardening_linux.rs b/tests/sandbox_hardening_linux.rs index 0998cc47..f06f2d0a 100644 --- a/tests/sandbox_hardening_linux.rs +++ b/tests/sandbox_hardening_linux.rs @@ -27,9 +27,9 @@ mod hardening_tests { self, HardeningRecord, ProcessHardeningProfile, SandboxBackend, SandboxOptions, }; - fn linux_outcome(out: &sandbox::SandboxOutcome) - -> Option - { + fn linux_outcome( + out: &sandbox::SandboxOutcome, + ) -> Option { match out.hardening_outcome.as_ref()? { HardeningRecord::Linux(o) => Some(*o), #[allow(unreachable_patterns)] @@ -43,9 +43,7 @@ mod hardening_tests { static PROBE_BINARY: OnceLock> = OnceLock::new(); fn probe_path() -> Option<&'static Path> { - PROBE_BINARY - .get_or_init(|| build_probe_once()) - .as_deref() + PROBE_BINARY.get_or_init(|| build_probe_once()).as_deref() } fn build_probe_once() -> Option { @@ -310,7 +308,9 @@ mod hardening_tests { fn chroot_blocks_etc_passwd() { let Some(_) = probe_path() else { return }; if !probe_is_static() { - eprintln!("SKIP: probe is dynamically linked — chroot would block its loader before main()"); + eprintln!( + "SKIP: probe is dynamically linked — chroot would block its loader before main()" + ); return; } let tmp = workdir(); @@ -372,7 +372,8 @@ mod hardening_tests { "sink hit should be absent on a traversal-blocked run" ); assert!( - stdout.contains("chroot blocked") || stdout.contains("chroot:blocked") + stdout.contains("chroot blocked") + || stdout.contains("chroot:blocked") || stdout.contains("traverse:blocked"), "expected `chroot blocked` marker in probe stdout; got:\n{stdout}" ); @@ -505,10 +506,8 @@ mod hardening_tests { } use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{ - Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus, - }; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; use nyx_scanner::utils::config::Config; @@ -521,10 +520,7 @@ mod hardening_tests { std::fs::copy(&fixture_src, &dst).expect("stage fixture into tempdir"); unsafe { - std::env::set_var( - "NYX_REPRO_BASE", - tmp.path().join("repro").to_str().unwrap(), - ); + std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap()); std::env::set_var( "NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap(), @@ -688,10 +684,8 @@ mod hardening_tests { } use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{ - Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus, - }; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; use nyx_scanner::utils::config::Config; @@ -704,10 +698,7 @@ mod hardening_tests { std::fs::copy(&fixture_src, &dst).expect("stage fixture into tempdir"); unsafe { - std::env::set_var( - "NYX_REPRO_BASE", - tmp.path().join("repro").to_str().unwrap(), - ); + std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap()); std::env::set_var( "NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap(), @@ -871,4 +862,3 @@ mod non_linux_placeholder { ); } } - diff --git a/tests/sandbox_hardening_macos.rs b/tests/sandbox_hardening_macos.rs index 4363490a..f8fdc87f 100644 --- a/tests/sandbox_hardening_macos.rs +++ b/tests/sandbox_hardening_macos.rs @@ -21,17 +21,16 @@ mod hardening_tests { use nyx_scanner::dynamic::harness::BuiltHarness; use nyx_scanner::dynamic::sandbox::process_macos::{ - clear_profile_path_cache_for_tests, profile_for_caps, profile_path, - sandbox_exec_available, HardeningLevel, SANDBOX_EXEC_BIN_ENV, SB_DENY_DEFAULT_ENV, - SB_SEED_DIR_ENV, + HardeningLevel, SANDBOX_EXEC_BIN_ENV, SB_DENY_DEFAULT_ENV, SB_SEED_DIR_ENV, + clear_profile_path_cache_for_tests, profile_for_caps, profile_path, sandbox_exec_available, }; use nyx_scanner::dynamic::sandbox::{ self, HardeningRecord, ProcessHardeningProfile, SandboxBackend, SandboxOptions, }; - fn macos_outcome(out: &sandbox::SandboxOutcome) - -> Option<&nyx_scanner::dynamic::sandbox::process_macos::HardeningOutcome> - { + fn macos_outcome( + out: &sandbox::SandboxOutcome, + ) -> Option<&nyx_scanner::dynamic::sandbox::process_macos::HardeningOutcome> { match out.hardening_outcome.as_ref()? { HardeningRecord::Macos(o) => Some(o), #[allow(unreachable_patterns)] @@ -120,8 +119,7 @@ except Exception as exc: /// the harness workdir at run time so the sandbox-exec narrow /// `/Users//Library/...` denies cannot accidentally shadow a /// home-relative script-load path. - const XXE_PROBE_SOURCE: &str = - include_str!("dynamic_fixtures/hardening/xxe_probe.py"); + const XXE_PROBE_SOURCE: &str = include_str!("dynamic_fixtures/hardening/xxe_probe.py"); fn write_xxe_probe(workdir: &Path) -> PathBuf { let path = workdir.join("xxe_probe.py"); @@ -427,10 +425,8 @@ except Exception as exc: } use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{ - Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus, - }; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; use nyx_scanner::utils::config::Config; @@ -443,10 +439,7 @@ except Exception as exc: std::fs::copy(&fixture_src, &dst).expect("stage fixture into tempdir"); unsafe { - std::env::set_var( - "NYX_REPRO_BASE", - tmp.path().join("repro").to_str().unwrap(), - ); + std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap()); std::env::set_var( "NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap(), @@ -562,10 +555,8 @@ except Exception as exc: } use nyx_scanner::commands::scan::Diag; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; - use nyx_scanner::evidence::{ - Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus, - }; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; + use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind, VerifyStatus}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; use nyx_scanner::utils::config::Config; @@ -578,10 +569,7 @@ except Exception as exc: std::fs::copy(&fixture_src, &dst).expect("stage fixture into tempdir"); unsafe { - std::env::set_var( - "NYX_REPRO_BASE", - tmp.path().join("repro").to_str().unwrap(), - ); + std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap()); std::env::set_var( "NYX_TELEMETRY_PATH", tmp.path().join("events.jsonl").to_str().unwrap(), @@ -786,7 +774,7 @@ except Exception as exc: /// downstream replay reads the same fields back. #[test] fn hardening_summary_round_trips_through_json() { - use nyx_scanner::evidence::{HardeningSummary, HardeningPrimitive}; + use nyx_scanner::evidence::{HardeningPrimitive, HardeningSummary}; let summary = HardeningSummary { backend: "macos-process".into(), level: "sandboxed".into(), diff --git a/tests/sarif_dynamic_verdict_tests.rs b/tests/sarif_dynamic_verdict_tests.rs index 18db29fd..27686236 100644 --- a/tests/sarif_dynamic_verdict_tests.rs +++ b/tests/sarif_dynamic_verdict_tests.rs @@ -82,8 +82,7 @@ fn sarif_confirmed_verdict_sets_partial_fingerprint() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["partialFingerprints"]["dynamic_verdict_status"], - "Confirmed", + result["partialFingerprints"]["dynamic_verdict_status"], "Confirmed", "partialFingerprints.dynamic_verdict_status must be 'Confirmed'" ); assert!( @@ -92,8 +91,7 @@ fn sarif_confirmed_verdict_sets_partial_fingerprint() { result["properties"]["nyx_dynamic_verdict"] ); assert_eq!( - result["properties"]["nyx_dynamic_verdict"]["status"], - "Confirmed", + result["properties"]["nyx_dynamic_verdict"]["status"], "Confirmed", "nyx_dynamic_verdict.status must be 'Confirmed'" ); } @@ -118,8 +116,7 @@ fn sarif_not_confirmed_verdict_sets_partial_fingerprint() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["partialFingerprints"]["dynamic_verdict_status"], - "NotConfirmed", + result["partialFingerprints"]["dynamic_verdict_status"], "NotConfirmed", "partialFingerprints.dynamic_verdict_status must be 'NotConfirmed'" ); assert!( @@ -148,8 +145,7 @@ fn sarif_unsupported_verdict_sets_partial_fingerprint() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["partialFingerprints"]["dynamic_verdict_status"], - "Unsupported", + result["partialFingerprints"]["dynamic_verdict_status"], "Unsupported", "partialFingerprints.dynamic_verdict_status must be 'Unsupported'" ); assert!( @@ -157,8 +153,7 @@ fn sarif_unsupported_verdict_sets_partial_fingerprint() { "properties.nyx_dynamic_verdict must be an object" ); assert_eq!( - result["properties"]["nyx_dynamic_verdict"]["reason"], - "NoPayloadsForCap", + result["properties"]["nyx_dynamic_verdict"]["reason"], "NoPayloadsForCap", "nyx_dynamic_verdict must carry the unsupported reason" ); } @@ -183,8 +178,7 @@ fn sarif_inconclusive_verdict_sets_partial_fingerprint() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["partialFingerprints"]["dynamic_verdict_status"], - "Inconclusive", + result["partialFingerprints"]["dynamic_verdict_status"], "Inconclusive", "partialFingerprints.dynamic_verdict_status must be 'Inconclusive'" ); assert!( @@ -192,8 +186,7 @@ fn sarif_inconclusive_verdict_sets_partial_fingerprint() { "properties.nyx_dynamic_verdict must be an object" ); assert_eq!( - result["properties"]["nyx_dynamic_verdict"]["inconclusive_reason"], - "BuildFailed", + result["properties"]["nyx_dynamic_verdict"]["inconclusive_reason"], "BuildFailed", "nyx_dynamic_verdict must carry the inconclusive reason" ); } @@ -204,12 +197,14 @@ fn sarif_no_dynamic_verdict_omits_both_keys() { let result = sarif_result(diag); assert!( - result["partialFingerprints"].is_null() || result["partialFingerprints"] == serde_json::Value::Null, + result["partialFingerprints"].is_null() + || result["partialFingerprints"] == serde_json::Value::Null, "partialFingerprints must be absent when no dynamic verdict: {}", result["partialFingerprints"] ); assert!( - result["properties"]["nyx_dynamic_verdict"].is_null() || result["properties"]["nyx_dynamic_verdict"] == serde_json::Value::Null, + result["properties"]["nyx_dynamic_verdict"].is_null() + || result["properties"]["nyx_dynamic_verdict"] == serde_json::Value::Null, "properties.nyx_dynamic_verdict must be absent when no dynamic verdict" ); } @@ -234,8 +229,7 @@ fn sarif_confirmed_verdict_nyx_dynamic_verdict_contains_triggered_payload() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["properties"]["nyx_dynamic_verdict"]["triggered_payload"], - "cmd-injection-semicolon", + result["properties"]["nyx_dynamic_verdict"]["triggered_payload"], "cmd-injection-semicolon", "triggered_payload must appear in nyx_dynamic_verdict" ); } @@ -268,8 +262,7 @@ fn sarif_all_four_statuses_produce_partial_fingerprint() { let result = sarif_result(diag_with_verdict(verdict)); assert_eq!( - result["partialFingerprints"]["dynamic_verdict_status"], - expected_str, + result["partialFingerprints"]["dynamic_verdict_status"], expected_str, "status {expected_str}: partialFingerprints.dynamic_verdict_status mismatch" ); assert!( diff --git a/tests/scrubber_pii.rs b/tests/scrubber_pii.rs index e8da1bca..16041329 100644 --- a/tests/scrubber_pii.rs +++ b/tests/scrubber_pii.rs @@ -8,7 +8,7 @@ #[cfg(feature = "dynamic")] mod scrubber_pii_tests { - use nyx_scanner::dynamic::policy::{Scrubber, SCRUB_HASH_PREFIX}; + use nyx_scanner::dynamic::policy::{SCRUB_HASH_PREFIX, Scrubber}; use nyx_scanner::dynamic::probe::ProbeWitness; #[test] @@ -68,7 +68,8 @@ mod scrubber_pii_tests { #[test] fn scrubber_recognises_pem_block() { let s = Scrubber::project_default(); - let value = "-----BEGIN RSA PRIVATE KEY-----\nMIIEoQIBAAKCAQ\n-----END RSA PRIVATE KEY-----"; + let value = + "-----BEGIN RSA PRIVATE KEY-----\nMIIEoQIBAAKCAQ\n-----END RSA PRIVATE KEY-----"; assert!(s.matches_any(value)); let out = s.scrub_string(value); assert!(!out.contains("MIIEoQIBAAKCAQ")); @@ -126,10 +127,14 @@ mod scrubber_pii_tests { ); let serialised = serde_json::to_string(&witness).unwrap(); - assert!(!serialised.contains("deadbeef-feedface"), - "raw secret leaked into serialised witness: {serialised}"); - assert!(serialised.contains(SCRUB_HASH_PREFIX), - "expected scrubbed-hash marker; got {serialised}"); + assert!( + !serialised.contains("deadbeef-feedface"), + "raw secret leaked into serialised witness: {serialised}" + ); + assert!( + serialised.contains(SCRUB_HASH_PREFIX), + "expected scrubbed-hash marker; got {serialised}" + ); } #[test] @@ -137,12 +142,9 @@ mod scrubber_pii_tests { // An env var keyed past the deny-list (so scrub_env keeps the // value verbatim) but whose textual value contains a secret // pattern must still be hashed by the Phase 28 scrubber pass. - let env: Vec<(String, String)> = vec![ - ("USER_DATA".to_owned(), "AKIAFAKETEST00000000".to_owned()), - ]; - let witness = ProbeWitness::from_inputs( - env, "/x", b"", "fn", vec![], - ); + let env: Vec<(String, String)> = + vec![("USER_DATA".to_owned(), "AKIAFAKETEST00000000".to_owned())]; + let witness = ProbeWitness::from_inputs(env, "/x", b"", "fn", vec![]); let value = witness.env_snapshot.get("USER_DATA").unwrap(); assert!(value.starts_with(SCRUB_HASH_PREFIX), "got {value}"); } diff --git a/tests/secret_derivation.rs b/tests/secret_derivation.rs index b8bd8231..de7ec305 100644 --- a/tests/secret_derivation.rs +++ b/tests/secret_derivation.rs @@ -22,7 +22,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::environment::{ - build_secret_bag, derive_secret, extract_env_var_references, SECRET_VALUE_PREFIX, + SECRET_VALUE_PREFIX, build_secret_bag, derive_secret, extract_env_var_references, }; use nyx_scanner::symbol::Lang; use std::path::{Path, PathBuf}; @@ -228,7 +228,10 @@ fn flask_fixture_boots_with_derived_secret_env() { // Spawn python3 in the fixture directory, env-clear, layer the bag // on top, and confirm the module imports without raising. let mut cmd = std::process::Command::new("python3"); - cmd.args(["-c", "import sys; sys.path.insert(0, '.'); import app; print('OK')"]); + cmd.args([ + "-c", + "import sys; sys.path.insert(0, '.'); import app; print('OK')", + ]); cmd.current_dir(&fixture); cmd.env_clear(); // PATH is required so python3 can re-locate its stdlib; the diff --git a/tests/sound_oracle_unavailable.rs b/tests/sound_oracle_unavailable.rs index 21265e1e..ae7ddbc8 100644 --- a/tests/sound_oracle_unavailable.rs +++ b/tests/sound_oracle_unavailable.rs @@ -13,7 +13,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::corpus::registry::{ - sound_oracle_unavailable_hint, CORPUS_SOUND_ORACLE_UNAVAILABLE, + CORPUS_SOUND_ORACLE_UNAVAILABLE, sound_oracle_unavailable_hint, }; use nyx_scanner::labels::Cap; diff --git a/tests/spec_callgraph_resolution.rs b/tests/spec_callgraph_resolution.rs index dae4b695..808dbc6c 100644 --- a/tests/spec_callgraph_resolution.rs +++ b/tests/spec_callgraph_resolution.rs @@ -15,11 +15,9 @@ #![cfg(feature = "dynamic")] use nyx_scanner::ast::analyse_file_fused; -use nyx_scanner::callgraph::{analyse, build_call_graph, CallGraph, CallGraphAnalysis}; +use nyx_scanner::callgraph::{CallGraph, CallGraphAnalysis, analyse, build_call_graph}; use nyx_scanner::commands::scan::Diag; -use nyx_scanner::dynamic::spec::{ - is_entry_point, EntryKind, HarnessSpec, SpecDerivationStrategy, -}; +use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, SpecDerivationStrategy, is_entry_point}; use nyx_scanner::evidence::{Confidence, Evidence, FlowStep, FlowStepKind}; use nyx_scanner::labels::Cap; use nyx_scanner::patterns::{FindingCategory, Severity}; @@ -50,8 +48,7 @@ fn build_context(file: &Path) -> (GlobalSummaries, CallGraph, CallGraphAnalysis) let root = file.parent().unwrap(); let root_str = root.to_string_lossy(); let bytes = std::fs::read(file).expect("read fixture"); - let result = analyse_file_fused(&bytes, file, &cfg, None, Some(root)) - .expect("analyse fixture"); + let result = analyse_file_fused(&bytes, file, &cfg, None, Some(root)).expect("analyse fixture"); let mut gs = GlobalSummaries::new(); for s in result.summaries { let key = s.func_key(Some(&root_str)); diff --git a/tests/spec_derivation_strategies.rs b/tests/spec_derivation_strategies.rs index 9b7931b1..ac342724 100644 --- a/tests/spec_derivation_strategies.rs +++ b/tests/spec_derivation_strategies.rs @@ -20,10 +20,10 @@ mod spec_strategies { use nyx_scanner::commands::scan::Diag; use nyx_scanner::dynamic::spec::{ - derive_from_callgraph_entry, derive_from_func_summary, derive_from_rule_namespace, EntryKind, EntryKindTag, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + derive_from_callgraph_entry, derive_from_func_summary, derive_from_rule_namespace, }; - use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions}; + use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, VerifyStatus, @@ -98,7 +98,10 @@ mod spec_strategies { ); let mut ev = Evidence::default(); ev.flow_steps = vec![ - source_step("tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py", "handle_request"), + source_step( + "tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py", + "handle_request", + ), sink_step("tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py"), ]; ev.sink_caps = Cap::SHELL_ESCAPE.bits(); @@ -132,11 +135,7 @@ mod spec_strategies { #[test] fn from_rule_namespace_called_directly_returns_some() { - let mut diag = make_diag( - "java.deser.readobject", - "src/Main.java", - 12, - ); + let mut diag = make_diag("java.deser.readobject", "src/Main.java", 12); let mut ev = Evidence::default(); ev.sink_caps = Cap::DESERIALIZE.bits(); diag.evidence = Some(ev.clone()); @@ -212,9 +211,8 @@ mod spec_strategies { hierarchy_edges: vec![], entry_kind: None, }; - let spec = - derive_from_func_summary(&diag, diag.evidence.as_ref().unwrap(), Some(&summary)) - .expect("summary strategy must succeed"); + let spec = derive_from_func_summary(&diag, diag.evidence.as_ref().unwrap(), Some(&summary)) + .expect("summary strategy must succeed"); assert_eq!(spec.derivation, SpecDerivationStrategy::FromFuncSummaryWalk); assert!(matches!(spec.payload_slot, PayloadSlot::Param(1))); assert_eq!(spec.entry_name, "read_path"); @@ -240,11 +238,7 @@ mod spec_strategies { #[test] fn from_callgraph_entry_called_directly_returns_some() { - let mut diag = make_diag( - "rs.cli.subcommand_parse", - "src/main.rs", - 10, - ); + let mut diag = make_diag("rs.cli.subcommand_parse", "src/main.rs", 10); let mut ev = Evidence::default(); ev.sink_caps = Cap::SHELL_ESCAPE.bits(); diag.evidence = Some(ev.clone()); @@ -305,7 +299,10 @@ mod spec_strategies { ); let mut ev = Evidence::default(); ev.flow_steps = vec![ - source_step("tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py", "handle_request"), + source_step( + "tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py", + "handle_request", + ), sink_step("tests/dynamic_fixtures/spec_strategies/flow_steps_taint.py"), ]; ev.sink_caps = Cap::SHELL_ESCAPE.bits(); @@ -379,9 +376,7 @@ mod spec_strategies { "hint must name the attempted entry kind; got {hint:?}" ); } - other => panic!( - "expected InconclusiveReason::EntryKindUnsupported, got {other:?}" - ), + other => panic!("expected InconclusiveReason::EntryKindUnsupported, got {other:?}"), } } } diff --git a/tests/ssti_corpus.rs b/tests/ssti_corpus.rs index 8ce8d770..dba0581c 100644 --- a/tests/ssti_corpus.rs +++ b/tests/ssti_corpus.rs @@ -14,12 +14,12 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; -use nyx_scanner::dynamic::oracle::{oracle_fired, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; use nyx_scanner::labels::Cap; @@ -72,13 +72,7 @@ fn corpus_registers_ssti_for_every_supported_lang() { fn ssti_unsupported_caps_unchanged_for_other_langs() { // Phase 04 only fills Python/Ruby/PHP/Java/JS — TypeScript / Rust / // C / Cpp / Go remain empty. - for lang in [ - Lang::Rust, - Lang::C, - Lang::Cpp, - Lang::Go, - Lang::TypeScript, - ] { + for lang in [Lang::Rust, Lang::C, Lang::Cpp, Lang::Go, Lang::TypeScript] { assert!( payloads_for_lang(Cap::SSTI, lang).is_empty(), "unexpected SSTI payloads registered for {lang:?}", @@ -91,8 +85,7 @@ fn benign_control_resolves_within_lang_slice() { for lang in LANGS { let slice = payloads_for_lang(Cap::SSTI, *lang); let vuln = slice.iter().find(|p| !p.is_benign).unwrap(); - let resolved = - resolve_benign_control_lang(vuln, Cap::SSTI, *lang).expect("paired control"); + let resolved = resolve_benign_control_lang(vuln, Cap::SSTI, *lang).expect("paired control"); assert!(resolved.is_benign); let direct = benign_payload_for_lang(Cap::SSTI, *lang).unwrap(); assert_eq!(direct.label, resolved.label); @@ -106,9 +99,9 @@ fn payload_oracle_carries_template_eval_predicate() { let vuln = slice.iter().find(|p| !p.is_benign).unwrap(); match &vuln.oracle { Oracle::SinkProbe { predicates } => { - let has_predicate = predicates.iter().any(|p| { - matches!(p, ProbePredicate::TemplateEvalEqual { expected: 49 }) - }); + let has_predicate = predicates + .iter() + .any(|p| matches!(p, ProbePredicate::TemplateEvalEqual { expected: 49 })); assert!( has_predicate, "{lang:?} vuln payload missing TemplateEvalEqual{{expected:49}}", @@ -205,8 +198,8 @@ fn lang_emitter_dispatches_to_ssti_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains(marker), "{lang:?} ssti harness must splice {marker:?}", @@ -277,10 +270,13 @@ fn framework_adapters_detect_ssti_sink() { .push(nyx_scanner::summary::CalleeSite::bare(sink_callee)); let registry_slice = adapters_for(lang); assert!(!registry_slice.is_empty(), "{lang:?} adapter slice empty"); - let binding = - nyx_scanner::dynamic::framework::detect_binding(&summary, tree.root_node(), &bytes, lang); - let b = - binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the SSTI fixture")); + let binding = nyx_scanner::dynamic::framework::detect_binding( + &summary, + tree.root_node(), + &bytes, + lang, + ); + let b = binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the SSTI fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -292,9 +288,7 @@ fn ts_language_for(lang: Lang) -> tree_sitter::Language { Lang::Ruby => tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE), Lang::Php => tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP), Lang::Java => tree_sitter::Language::from(tree_sitter_java::LANGUAGE), - Lang::JavaScript => { - tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE) - } + Lang::JavaScript => tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE), other => panic!("unsupported test lang {other:?}"), } } @@ -338,10 +332,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_04 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -454,7 +448,9 @@ mod e2e_phase_04 { #[test] fn python_jinja2_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Python Jinja2 SSTI vuln must Confirm via run_spec; got {outcome:?}", @@ -468,7 +464,9 @@ mod e2e_phase_04 { #[test] fn ruby_erb_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Ruby ERB SSTI vuln must Confirm via run_spec; got {outcome:?}", @@ -482,7 +480,9 @@ mod e2e_phase_04 { #[test] fn php_twig_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "PHP Twig SSTI vuln must Confirm via run_spec; got {outcome:?}", @@ -496,7 +496,9 @@ mod e2e_phase_04 { #[test] fn js_handlebars_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "JS Handlebars SSTI vuln must Confirm via run_spec; got {outcome:?}", @@ -510,7 +512,9 @@ mod e2e_phase_04 { #[test] fn java_thymeleaf_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "vuln.java", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Java Thymeleaf SSTI vuln must Confirm via run_spec; got {outcome:?}", diff --git a/tests/stubs_e2e_per_lang.rs b/tests/stubs_e2e_per_lang.rs index 7bfc1db6..182ae5d0 100644 --- a/tests/stubs_e2e_per_lang.rs +++ b/tests/stubs_e2e_per_lang.rs @@ -862,8 +862,8 @@ fn go_http_stub_captures_attempted_outbound_via_shim_recorder() { // Go fragments need wrapping: the file under tests/dynamic_fixtures // is a body-only fragment, not a standalone program. - let fragment = std::fs::read_to_string(fixture_path("go/http/vuln/main.go")) - .expect("read go fragment"); + let fragment = + std::fs::read_to_string(fixture_path("go/http/vuln/main.go")).expect("read go fragment"); let combined = wrap_go_fragment(&fragment, go_probe_shim()); let script_path = workdir.path().join("driver_http.go"); @@ -918,8 +918,8 @@ fn go_http_shim_recorder_is_noop_without_log_env() { let stub = HttpStub::start(workdir.path()).expect("HttpStub::start"); let endpoint = stub.endpoint(); - let fragment = std::fs::read_to_string(fixture_path("go/http/vuln/main.go")) - .expect("read go fragment"); + let fragment = + std::fs::read_to_string(fixture_path("go/http/vuln/main.go")).expect("read go fragment"); let combined = wrap_go_fragment(&fragment, go_probe_shim()); let script_path = workdir.path().join("driver_http_no_log.go"); @@ -1589,8 +1589,11 @@ fn rust_http_shim_recorder_is_noop_without_log_env() { let crate_dir = workdir.path().join("driver_no_log"); std::fs::create_dir_all(&crate_dir).expect("create crate dir"); - std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("http_no_log")) - .expect("write Cargo.toml"); + std::fs::write( + crate_dir.join("Cargo.toml"), + rust_stub_cargo_toml("http_no_log"), + ) + .expect("write Cargo.toml"); std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs"); let output = Command::new("cargo") @@ -1702,8 +1705,11 @@ fn rust_sql_shim_recorder_is_noop_without_log_env() { let crate_dir = workdir.path().join("driver_sql_no_log"); std::fs::create_dir_all(&crate_dir).expect("create crate dir"); - std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("sql_no_log")) - .expect("write Cargo.toml"); + std::fs::write( + crate_dir.join("Cargo.toml"), + rust_stub_cargo_toml("sql_no_log"), + ) + .expect("write Cargo.toml"); std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs"); let output = Command::new("cargo") diff --git a/tests/stubs_per_cap.rs b/tests/stubs_per_cap.rs index 26c9bb45..1e5e21e6 100644 --- a/tests/stubs_per_cap.rs +++ b/tests/stubs_per_cap.rs @@ -18,9 +18,7 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::oracle::{ - oracle_fired_with_stubs, Oracle, ProbePredicate, -}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired_with_stubs}; use nyx_scanner::dynamic::probe::{ProbeArg, ProbeChannel, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::stubs::{ @@ -77,7 +75,10 @@ fn sql_stub_vuln_fixture_confirms_with_captured_query() { // Synthetic harness: read the vuln fixture, record the executed // query against the stub, then evaluate the oracle. let payload = extract_payload(&read_fixture("sql", "vuln.txt")); - assert!(payload.contains("OR 1=1"), "vuln fixture must carry a tautology"); + assert!( + payload.contains("OR 1=1"), + "vuln fixture must carry a tautology" + ); stub.record_query(&payload).unwrap(); let oracle = Oracle::StubEvent { @@ -85,7 +86,11 @@ fn sql_stub_vuln_fixture_confirms_with_captured_query() { needle: "OR 1=1", }; let events = stub.drain_events(); - assert_eq!(events.len(), 1, "stub must have captured the executed query"); + assert_eq!( + events.len(), + 1, + "stub must have captured the executed query" + ); assert!( events[0].summary.contains("OR 1=1"), "captured query must be visible in probe output: {:?}", @@ -103,7 +108,10 @@ fn sql_stub_benign_fixture_does_not_confirm() { let stub = SqlStub::start(dir.path()).unwrap(); let payload = extract_payload(&read_fixture("sql", "benign.txt")); - assert!(!payload.contains("OR 1=1"), "benign control must lack tautology"); + assert!( + !payload.contains("OR 1=1"), + "benign control must lack tautology" + ); stub.record_query(&payload).unwrap(); let oracle = Oracle::StubEvent { @@ -161,7 +169,10 @@ fn http_stub_vuln_fixture_confirms_recorded_request() { let workdir = TempDir::new().unwrap(); let stub = HttpStub::start(workdir.path()).unwrap(); let payload = extract_payload(&read_fixture("http", "vuln.txt")); - assert!(payload.contains("169.254"), "vuln fixture must carry metadata host"); + assert!( + payload.contains("169.254"), + "vuln fixture must carry metadata host" + ); stub.record(payload.clone()); let events = stub.drain_events(); @@ -172,7 +183,12 @@ fn http_stub_vuln_fixture_confirms_recorded_request() { kind: StubKind::Http, needle: "169.254", }; - assert!(oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } #[test] @@ -187,7 +203,12 @@ fn http_stub_benign_fixture_does_not_confirm() { kind: StubKind::Http, needle: "169.254", }; - assert!(!oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(!oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } // ── Redis stub ─────────────────────────────────────────────────────── @@ -204,7 +225,12 @@ fn redis_stub_vuln_fixture_confirms_destructive_command() { kind: StubKind::Redis, needle: "FLUSHALL", }; - assert!(oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } #[test] @@ -221,7 +247,12 @@ fn redis_stub_benign_fixture_does_not_confirm() { kind: StubKind::Redis, needle: "FLUSHALL", }; - assert!(!oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(!oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } // ── Filesystem stub ────────────────────────────────────────────────── @@ -239,7 +270,12 @@ fn filesystem_stub_vuln_fixture_confirms_path_traversal() { kind: StubKind::Filesystem, needle: "/etc/passwd", }; - assert!(oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } #[test] @@ -255,7 +291,12 @@ fn filesystem_stub_benign_fixture_does_not_confirm() { kind: StubKind::Filesystem, needle: "/etc/passwd", }; - assert!(!oracle_fired_with_stubs(&oracle, &empty_outcome(), &[], &events)); + assert!(!oracle_fired_with_stubs( + &oracle, + &empty_outcome(), + &[], + &events + )); } // ── Performance invariant ──────────────────────────────────────────── diff --git a/tests/surface_cli.rs b/tests/surface_cli.rs index db89d9f2..c15eb921 100644 --- a/tests/surface_cli.rs +++ b/tests/surface_cli.rs @@ -9,8 +9,8 @@ use nyx_scanner::callgraph::CallGraph; use nyx_scanner::commands::surface::{load_or_build, render_dot, render_text}; use nyx_scanner::summary::GlobalSummaries; use nyx_scanner::surface::{ - build::{build_surface_map, SurfaceBuildInputs}, SurfaceMap, + build::{SurfaceBuildInputs, build_surface_map}, }; use nyx_scanner::utils::config::Config; use std::path::{Path, PathBuf}; @@ -78,7 +78,8 @@ fn text_output_matches_golden_for_flask_fixture() { let expected = std::fs::read_to_string(GOLDEN_PATH) .expect("read tests/dynamic_fixtures/surface/cli_output.golden.txt"); assert_eq!( - actual, expected, + actual, + expected, "render_text output drifted from golden; re-run with UPDATE_GOLDEN=1 if intentional.\nfixture: {}", dir.display() ); @@ -98,7 +99,10 @@ fn json_output_round_trips_byte_identical() { let bytes = map.to_json().expect("canonical JSON"); let mut rt = SurfaceMap::from_json(&bytes).expect("from_json"); let rt_bytes = rt.to_json().expect("re-serialise"); - assert_eq!(bytes, rt_bytes, "canonical JSON must round-trip identically"); + assert_eq!( + bytes, rt_bytes, + "canonical JSON must round-trip identically" + ); } #[test] diff --git a/tests/surface_cross_lang.rs b/tests/surface_cross_lang.rs index aaaa2a91..9fc931eb 100644 --- a/tests/surface_cross_lang.rs +++ b/tests/surface_cross_lang.rs @@ -15,7 +15,7 @@ use nyx_scanner::callgraph::CallGraph; use nyx_scanner::summary::GlobalSummaries; use nyx_scanner::surface::{ Framework, SurfaceMap, SurfaceNode, - build::{build_surface_map, SurfaceBuildInputs}, + build::{SurfaceBuildInputs, build_surface_map}, }; use nyx_scanner::utils::config::Config; use std::path::{Path, PathBuf}; diff --git a/tests/surface_flask.rs b/tests/surface_flask.rs index d71a9774..09e90ddd 100644 --- a/tests/surface_flask.rs +++ b/tests/surface_flask.rs @@ -101,7 +101,11 @@ fn surface_map_captures_five_flask_routes() { let ep = map.entry_for_route(method, route).unwrap_or_else(|| { panic!("missing route {method:?} {route}; map = {entries:#?}"); }); - assert_eq!(ep.framework, Framework::Flask, "framework mismatch on {route}"); + assert_eq!( + ep.framework, + Framework::Flask, + "framework mismatch on {route}" + ); assert_eq!(ep.handler_name, handler, "handler mismatch on {route}"); assert_eq!( ep.auth_required, auth, diff --git a/tests/telemetry_schema.rs b/tests/telemetry_schema.rs index 7f290e65..808ede94 100644 --- a/tests/telemetry_schema.rs +++ b/tests/telemetry_schema.rs @@ -13,11 +13,11 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::telemetry::{ - self, RankDeltaEvent, SamplingPolicy, TelemetryEvent, TelemetryReadError, CORPUS_VERSION, - NYX_VERSION, SCHEMA_VERSION, -}; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy}; +use nyx_scanner::dynamic::telemetry::{ + self, CORPUS_VERSION, NYX_VERSION, RankDeltaEvent, SCHEMA_VERSION, SamplingPolicy, + TelemetryEvent, TelemetryReadError, +}; use nyx_scanner::evidence::VerifyStatus; use nyx_scanner::labels::Cap; use nyx_scanner::symbol::Lang; diff --git a/tests/ts_frameworks_corpus.rs b/tests/ts_frameworks_corpus.rs index 00ca432b..92071a32 100644 --- a/tests/ts_frameworks_corpus.rs +++ b/tests/ts_frameworks_corpus.rs @@ -9,15 +9,14 @@ #![cfg(feature = "dynamic")] -use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource}; +use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding}; use nyx_scanner::evidence::EntryKind; use nyx_scanner::summary::FuncSummary; use nyx_scanner::symbol::Lang; fn parse_ts(src: &[u8]) -> tree_sitter::Tree { let mut parser = tree_sitter::Parser::new(); - let lang = - tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT); + let lang = tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT); parser.set_language(&lang).unwrap(); parser.parse(src, None).unwrap() } diff --git a/tests/typescript_fixtures.rs b/tests/typescript_fixtures.rs index 2e54029a..2493ed3c 100644 --- a/tests/typescript_fixtures.rs +++ b/tests/typescript_fixtures.rs @@ -10,7 +10,7 @@ mod common; #[cfg(feature = "dynamic")] mod typescript_fixture_tests { - use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite}; + use crate::common::fixture_harness::{Prerequisite, run_shape_fixture_lang_or_skip}; use nyx_scanner::dynamic::spec::PayloadSlot; use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus}; use nyx_scanner::labels::Cap; @@ -79,9 +79,16 @@ mod typescript_fixture_tests { fn commonjs_export_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "commonjs_export", "vuln.ts", "runPing", Cap::CODE_EXEC, 11, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "commonjs_export", + "vuln.ts", + "runPing", + Cap::CODE_EXEC, + 11, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("commonjs_export", &r); } @@ -89,9 +96,16 @@ mod typescript_fixture_tests { fn commonjs_export_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "commonjs_export", "benign.ts", "runPing", Cap::CODE_EXEC, 11, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "commonjs_export", + "benign.ts", + "runPing", + Cap::CODE_EXEC, + 11, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("commonjs_export", &r); } @@ -101,9 +115,16 @@ mod typescript_fixture_tests { fn async_function_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "async_function", "vuln.ts", "runPing", Cap::CODE_EXEC, 15, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "async_function", + "vuln.ts", + "runPing", + Cap::CODE_EXEC, + 15, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("async_function", &r); } @@ -111,9 +132,16 @@ mod typescript_fixture_tests { fn async_function_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "async_function", "benign.ts", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "async_function", + "benign.ts", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("async_function", &r); } @@ -123,9 +151,16 @@ mod typescript_fixture_tests { fn esm_default_vuln_is_confirmed() { let Some(r) = run( NODE_REQ, - "esm_default", "vuln.ts", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "esm_default", + "vuln.ts", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("esm_default", &r); } @@ -133,9 +168,16 @@ mod typescript_fixture_tests { fn esm_default_benign_not_confirmed() { let Some(r) = run( NODE_REQ, - "esm_default", "benign.ts", "runPing", Cap::CODE_EXEC, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "esm_default", + "benign.ts", + "runPing", + Cap::CODE_EXEC, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("esm_default", &r); } @@ -148,9 +190,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("express"), ], - "express", "vuln.ts", "ping", Cap::CODE_EXEC, 15, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "express", + "vuln.ts", + "ping", + Cap::CODE_EXEC, + 15, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("express", &r); } @@ -161,9 +210,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("express"), ], - "express", "benign.ts", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "express", + "benign.ts", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("express", &r); } @@ -176,9 +232,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("koa"), ], - "koa", "vuln.ts", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "koa", + "vuln.ts", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("koa", &r); } @@ -189,9 +252,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("koa"), ], - "koa", "benign.ts", "ping", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "koa", + "benign.ts", + "ping", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("koa", &r); } @@ -204,9 +274,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("next"), ], - "next_route", "vuln.ts", "handler", Cap::CODE_EXEC, 17, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "next_route", + "vuln.ts", + "handler", + Cap::CODE_EXEC, + 17, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_confirmed("next_route", &r); } @@ -217,9 +294,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("next"), ], - "next_route", "benign.ts", "handler", Cap::CODE_EXEC, 14, - EntryKind::HttpRoute, PayloadSlot::QueryParam("host".into()), - ) else { return; }; + "next_route", + "benign.ts", + "handler", + Cap::CODE_EXEC, + 14, + EntryKind::HttpRoute, + PayloadSlot::QueryParam("host".into()), + ) else { + return; + }; assert_not_confirmed("next_route", &r); } @@ -232,9 +316,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("jsdom"), ], - "browser_event", "vuln.ts", "clickHandler", Cap::HTML_ESCAPE, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "browser_event", + "vuln.ts", + "clickHandler", + Cap::HTML_ESCAPE, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_confirmed("browser_event", &r); } @@ -245,9 +336,16 @@ mod typescript_fixture_tests { Prerequisite::CommandAvailable("node"), Prerequisite::NodeModuleAvailable("jsdom"), ], - "browser_event", "benign.ts", "clickHandler", Cap::HTML_ESCAPE, 14, - EntryKind::Function, PayloadSlot::Param(0), - ) else { return; }; + "browser_event", + "benign.ts", + "clickHandler", + Cap::HTML_ESCAPE, + 14, + EntryKind::Function, + PayloadSlot::Param(0), + ) else { + return; + }; assert_not_confirmed("browser_event", &r); } } diff --git a/tests/unauthorized_id_corpus.rs b/tests/unauthorized_id_corpus.rs index 440a6edc..8a1b040a 100644 --- a/tests/unauthorized_id_corpus.rs +++ b/tests/unauthorized_id_corpus.rs @@ -12,7 +12,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::corpus::{payloads_for_lang, resolve_benign_control_lang}; -use nyx_scanner::dynamic::oracle::{oracle_fired, Oracle, ProbePredicate}; +use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::labels::Cap; @@ -60,10 +60,7 @@ fn idor_probe(caller: &str, owner: &str) -> SinkProbe { fn corpus_registers_unauthorized_id_for_each_supported_lang() { for lang in LANGS { let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang); - assert!( - !slice.is_empty(), - "UNAUTHORIZED_ID missing for {lang:?}" - ); + assert!(!slice.is_empty(), "UNAUTHORIZED_ID missing for {lang:?}"); assert!(slice.iter().any(|p| !p.is_benign)); assert!(slice.iter().any(|p| p.is_benign)); } @@ -74,9 +71,8 @@ fn idor_payloads_pair_benign_per_lang() { for lang in LANGS { let slice = payloads_for_lang(Cap::UNAUTHORIZED_ID, *lang); let vuln = slice.iter().find(|p| !p.is_benign).expect("vuln"); - let resolved = - resolve_benign_control_lang(vuln, Cap::UNAUTHORIZED_ID, *lang) - .expect("benign control resolves"); + let resolved = resolve_benign_control_lang(vuln, Cap::UNAUTHORIZED_ID, *lang) + .expect("benign control resolves"); assert!(resolved.is_benign); match &vuln.oracle { Oracle::SinkProbe { predicates } => assert!( @@ -94,7 +90,11 @@ fn idor_predicate_fires_on_boundary_crossing() { let oracle = Oracle::SinkProbe { predicates: &[ProbePredicate::IdorBoundaryCrossed], }; - assert!(oracle_fired(&oracle, &outcome(), &[idor_probe("alice", "bob")])); + assert!(oracle_fired( + &oracle, + &outcome(), + &[idor_probe("alice", "bob")] + )); assert!(!oracle_fired( &oracle, &outcome(), diff --git a/tests/xpath_corpus.rs b/tests/xpath_corpus.rs index d2604766..2e6b615f 100644 --- a/tests/xpath_corpus.rs +++ b/tests/xpath_corpus.rs @@ -17,14 +17,12 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; -use nyx_scanner::dynamic::oracle::{ - oracle_fired, ProbePredicate, SignalSet, -}; +use nyx_scanner::dynamic::oracle::{ProbePredicate, SignalSet, oracle_fired}; use nyx_scanner::dynamic::probe::{ProbeKind, ProbeWitness, SinkProbe}; use nyx_scanner::dynamic::sandbox::SandboxOutcome; use nyx_scanner::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot}; @@ -63,7 +61,10 @@ fn make_spec(lang: Lang, entry_file: &str, entry_name: &str) -> HarnessSpec { fn corpus_registers_xpath_for_every_supported_lang() { for lang in LANGS { let slice = payloads_for_lang(Cap::XPATH_INJECTION, *lang); - assert!(!slice.is_empty(), "XPATH_INJECTION has no payloads for {lang:?}"); + assert!( + !slice.is_empty(), + "XPATH_INJECTION has no payloads for {lang:?}" + ); let has_vuln = slice.iter().any(|p| !p.is_benign); let has_benign = slice.iter().any(|p| p.is_benign); assert!(has_vuln, "{lang:?} XPath missing vuln payload"); @@ -109,10 +110,9 @@ fn payload_oracle_carries_query_result_count_predicate() { match &vuln.oracle { Oracle::SinkProbe { predicates } => { assert!( - predicates.iter().any(|p| matches!( - p, - ProbePredicate::QueryResultCountGreaterThan { n: 1 } - )), + predicates + .iter() + .any(|p| matches!(p, ProbePredicate::QueryResultCountGreaterThan { n: 1 })), "{lang:?} vuln payload missing QueryResultCountGreaterThan {{ n: 1 }}", ); } @@ -221,7 +221,9 @@ fn query_result_count_predicate_also_matches_ldap_probe() { args: vec![], captured_at_ns: 1, payload_id: "phase07".into(), - kind: ProbeKind::Ldap { entries_returned: 3 }, + kind: ProbeKind::Ldap { + entries_returned: 3, + }, witness: ProbeWitness::empty(), }]; let outcome = SandboxOutcome { @@ -269,8 +271,8 @@ fn lang_emitter_dispatches_to_xpath_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("nodes_returned"), "{lang:?} xpath harness must carry the nodes_returned probe field", @@ -354,8 +356,7 @@ fn framework_adapters_detect_xpath_sink() { &bytes, lang, ); - let b = binding - .unwrap_or_else(|| panic!("{lang:?} adapter must detect the XPath fixture")); + let b = binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the XPath fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -407,10 +408,10 @@ fn staged_corpus_carries_three_users() { mod e2e_phase_07 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -520,7 +521,9 @@ mod e2e_phase_07 { #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Java XPath vuln must Confirm via run_spec; got {outcome:?}", @@ -534,7 +537,9 @@ mod e2e_phase_07 { #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Python XPath vuln must Confirm via run_spec; got {outcome:?}", @@ -548,7 +553,9 @@ mod e2e_phase_07 { #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "PHP XPath vuln must Confirm via run_spec; got {outcome:?}", @@ -562,7 +569,9 @@ mod e2e_phase_07 { #[test] fn javascript_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { return }; + let Some(outcome) = run(Lang::JavaScript, "vuln.js", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "JavaScript XPath vuln must Confirm via run_spec; got {outcome:?}", diff --git a/tests/xxe_corpus.rs b/tests/xxe_corpus.rs index fd6b7260..42c4fbc4 100644 --- a/tests/xxe_corpus.rs +++ b/tests/xxe_corpus.rs @@ -14,8 +14,8 @@ mod common; use nyx_scanner::dynamic::corpus::{ - audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, - resolve_benign_control_lang, Oracle, + Oracle, audit_marker_collisions, benign_payload_for_lang, payloads_for_lang, + resolve_benign_control_lang, }; use nyx_scanner::dynamic::framework::registry::adapters_for; use nyx_scanner::dynamic::lang; @@ -89,8 +89,7 @@ fn benign_control_resolves_within_lang_slice() { .iter() .find(|p| !p.is_benign && !p.oob_nonce_slot) .unwrap(); - let resolved = - resolve_benign_control_lang(vuln, Cap::XXE, *lang).expect("paired control"); + let resolved = resolve_benign_control_lang(vuln, Cap::XXE, *lang).expect("paired control"); assert!(resolved.is_benign); let direct = benign_payload_for_lang(Cap::XXE, *lang).unwrap(); assert_eq!(direct.label, resolved.label); @@ -113,7 +112,9 @@ fn payload_oracle_carries_xxe_entity_expanded_predicate() { assert!( predicates.iter().any(|p| matches!( p, - ProbePredicate::XxeEntityExpanded { require_expanded: true } + ProbePredicate::XxeEntityExpanded { + require_expanded: true + } )), "{lang:?} vuln payload missing XxeEntityExpanded{{require_expanded:true}}", ); @@ -208,8 +209,8 @@ fn lang_emitter_dispatches_to_xxe_harness() { ), ] { let spec = make_spec(lang, entry_file, entry_name); - let harness = lang::emit(&spec) - .unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); + let harness = + lang::emit(&spec).unwrap_or_else(|e| panic!("emit failed for {lang:?}: {e:?}")); assert!( harness.source.contains("entity_expanded"), "{lang:?} xxe harness must carry the entity_expanded probe field", @@ -250,11 +251,7 @@ fn framework_adapters_detect_xxe_sink() { "tests/dynamic_fixtures/xxe/php/vuln.php", "simplexml_load_string", ), - ( - Lang::Ruby, - "tests/dynamic_fixtures/xxe/ruby/vuln.rb", - "new", - ), + (Lang::Ruby, "tests/dynamic_fixtures/xxe/ruby/vuln.rb", "new"), ( Lang::Go, "tests/dynamic_fixtures/xxe/go/vuln.go", @@ -283,8 +280,7 @@ fn framework_adapters_detect_xxe_sink() { &bytes, lang, ); - let b = binding - .unwrap_or_else(|| panic!("{lang:?} adapter must detect the XXE fixture")); + let b = binding.unwrap_or_else(|| panic!("{lang:?} adapter must detect the XXE fixture")); assert_eq!(b.kind, EntryKind::Function); assert!(!b.adapter.is_empty()); } @@ -344,10 +340,10 @@ fn slug(lang: Lang) -> &'static str { mod e2e_phase_05 { use crate::common::fixture_harness::FIXTURE_LOCK; - use nyx_scanner::dynamic::runner::{run_spec, RunError, RunOutcome}; + use nyx_scanner::dynamic::runner::{RunError, RunOutcome, run_spec}; use nyx_scanner::dynamic::sandbox::{SandboxBackend, SandboxOptions}; use nyx_scanner::dynamic::spec::{ - default_toolchain_id, EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, + EntryKind, HarnessSpec, PayloadSlot, SpecDerivationStrategy, default_toolchain_id, }; use nyx_scanner::evidence::DifferentialVerdict; use nyx_scanner::labels::Cap; @@ -454,9 +450,7 @@ mod e2e_phase_05 { match run_spec(&spec, &opts) { Ok(outcome) => { if is_jvm_cwd_flake(&outcome) && attempt < 2 { - eprintln!( - "RETRY {lang:?} {fixture}: JVM cwd flake on attempt {attempt}", - ); + eprintln!("RETRY {lang:?} {fixture}: JVM cwd flake on attempt {attempt}",); std::thread::sleep(std::time::Duration::from_millis(200)); continue; } @@ -485,7 +479,9 @@ mod e2e_phase_05 { #[test] fn java_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run(Lang::Java, "Vuln.java", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Java XXE vuln must Confirm via run_spec; got {outcome:?}", @@ -499,7 +495,9 @@ mod e2e_phase_05 { #[test] fn python_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run(Lang::Python, "vuln.py", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Python XXE vuln must Confirm via run_spec; got {outcome:?}", @@ -513,7 +511,9 @@ mod e2e_phase_05 { #[test] fn php_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run(Lang::Php, "vuln.php", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "PHP XXE vuln must Confirm via run_spec; got {outcome:?}", @@ -527,7 +527,9 @@ mod e2e_phase_05 { #[test] fn ruby_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Ruby XXE vuln must Confirm via run_spec; got {outcome:?}", @@ -541,7 +543,9 @@ mod e2e_phase_05 { #[test] fn go_vuln_confirms_via_run_spec() { - let Some(outcome) = run(Lang::Go, "vuln.go", "run") else { return }; + let Some(outcome) = run(Lang::Go, "vuln.go", "run") else { + return; + }; assert!( outcome.triggered_by.is_some(), "Go XXE vuln must Confirm via run_spec; got {outcome:?}", @@ -657,31 +661,41 @@ mod e2e_phase_05 { #[test] fn python_xxe_oob_loopback_records_callback() { - let Some(outcome) = run_oob(Lang::Python, "vuln.py", "run") else { return }; + let Some(outcome) = run_oob(Lang::Python, "vuln.py", "run") else { + return; + }; assert_oob_recorded(&outcome, "xxe-python-oob-nonce"); } #[test] fn java_xxe_oob_loopback_records_callback() { - let Some(outcome) = run_oob(Lang::Java, "Vuln.java", "run") else { return }; + let Some(outcome) = run_oob(Lang::Java, "Vuln.java", "run") else { + return; + }; assert_oob_recorded(&outcome, "xxe-java-oob-nonce"); } #[test] fn php_xxe_oob_loopback_records_callback() { - let Some(outcome) = run_oob(Lang::Php, "vuln.php", "run") else { return }; + let Some(outcome) = run_oob(Lang::Php, "vuln.php", "run") else { + return; + }; assert_oob_recorded(&outcome, "xxe-php-oob-nonce"); } #[test] fn ruby_xxe_oob_loopback_records_callback() { - let Some(outcome) = run_oob(Lang::Ruby, "vuln.rb", "run") else { return }; + let Some(outcome) = run_oob(Lang::Ruby, "vuln.rb", "run") else { + return; + }; assert_oob_recorded(&outcome, "xxe-ruby-oob-nonce"); } #[test] fn go_xxe_oob_loopback_records_callback() { - let Some(outcome) = run_oob(Lang::Go, "vuln.go", "run") else { return }; + let Some(outcome) = run_oob(Lang::Go, "vuln.go", "run") else { + return; + }; assert_oob_recorded(&outcome, "xxe-go-oob-nonce"); } } diff --git a/tools/image-builder/main.rs b/tools/image-builder/main.rs index c2a4ab30..20806146 100644 --- a/tools/image-builder/main.rs +++ b/tools/image-builder/main.rs @@ -103,13 +103,20 @@ fn cmd_list(toml_path: &Path) -> ExitCode { let entries = match read_catalogue(toml_path) { Ok(v) => v, Err(e) => { - eprintln!("nyx-image-builder: cannot read {}: {e}", toml_path.display()); + eprintln!( + "nyx-image-builder: cannot read {}: {e}", + toml_path.display() + ); return ExitCode::FAILURE; } }; for e in &entries { - let digest = if e.digest.is_empty() { "" } else { &e.digest }; + let digest = if e.digest.is_empty() { + "" + } else { + &e.digest + }; println!("{:<20} {:<40} {}", e.toolchain_id, e.base, digest); } ExitCode::SUCCESS @@ -119,7 +126,10 @@ fn cmd_build(toml_path: &Path, args: &[String]) -> ExitCode { let entries = match read_catalogue(toml_path) { Ok(v) => v, Err(e) => { - eprintln!("nyx-image-builder: cannot read {}: {e}", toml_path.display()); + eprintln!( + "nyx-image-builder: cannot read {}: {e}", + toml_path.display() + ); return ExitCode::FAILURE; } }; @@ -172,7 +182,10 @@ fn cmd_build(toml_path: &Path, args: &[String]) -> ExitCode { let original = match std::fs::read_to_string(toml_path) { Ok(s) => s, Err(e) => { - eprintln!("nyx-image-builder build: cannot read {}: {e}", toml_path.display()); + eprintln!( + "nyx-image-builder build: cannot read {}: {e}", + toml_path.display() + ); return ExitCode::FAILURE; } }; @@ -185,9 +198,16 @@ fn cmd_build(toml_path: &Path, args: &[String]) -> ExitCode { ); return ExitCode::FAILURE; } - eprintln!("==> updated {} ({} entries)", toml_path.display(), updates.len()); + eprintln!( + "==> updated {} ({} entries)", + toml_path.display(), + updates.len() + ); } else { - eprintln!("==> {} unchanged (digests already pinned)", toml_path.display()); + eprintln!( + "==> {} unchanged (digests already pinned)", + toml_path.display() + ); } } @@ -202,7 +222,10 @@ fn cmd_verify(toml_path: &Path) -> ExitCode { let entries = match read_catalogue(toml_path) { Ok(v) => v, Err(e) => { - eprintln!("nyx-image-builder: cannot read {}: {e}", toml_path.display()); + eprintln!( + "nyx-image-builder: cannot read {}: {e}", + toml_path.display() + ); return ExitCode::FAILURE; } }; @@ -212,7 +235,10 @@ fn cmd_verify(toml_path: &Path) -> ExitCode { for entry in &entries { if entry.digest.is_empty() { - eprintln!("MISS {}: digest unpinned in {}", entry.toolchain_id, IMAGES_TOML); + eprintln!( + "MISS {}: digest unpinned in {}", + entry.toolchain_id, IMAGES_TOML + ); unpinned += 1; continue; } @@ -272,11 +298,7 @@ fn docker_pull(image: &str) -> bool { fn resolve_image_digest(image: &str) -> Option { // Try RepoDigests first. let repo = Command::new(docker_bin()) - .args([ - "inspect", - "--format={{index .RepoDigests 0}}", - image, - ]) + .args(["inspect", "--format={{index .RepoDigests 0}}", image]) .output() .ok()?; if repo.status.success() { @@ -350,8 +372,12 @@ fn parse_catalogue(src: &str) -> Vec { } continue; } - let Some(slot) = current.as_mut() else { continue }; - let Some((key, value)) = line.split_once('=') else { continue }; + let Some(slot) = current.as_mut() else { + continue; + }; + let Some((key, value)) = line.split_once('=') else { + continue; + }; let key = key.trim(); let value = value.trim().trim_matches('"').trim_matches('\''); match key {