mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
cargo fmt
This commit is contained in:
parent
bec7bbf96c
commit
3a35cd6c8f
294 changed files with 6809 additions and 3911 deletions
|
|
@ -182,30 +182,28 @@ pub fn pick_chain_cap(bits: u32) -> Option<Cap> {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -250,9 +250,10 @@ pub fn lookup_impact(source: Cap, adjacent: Option<Cap>) -> Option<ImpactCategor
|
|||
// try the standalone rule on adjacent_cap so a CODE_EXEC + UNRELATED
|
||||
// pair still reaches `Rce`.
|
||||
if let Some(adj) = adjacent
|
||||
&& let Some(cat) = standalone_lookup(adj) {
|
||||
return Some(cat);
|
||||
}
|
||||
&& let Some(cat) = standalone_lookup(adj)
|
||||
{
|
||||
return Some(cat);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,8 +264,8 @@ impl CompositeReverifier for DefaultCompositeReverifier {
|
|||
if result.cache_hit {
|
||||
cache_hits += 1;
|
||||
}
|
||||
total_build_ms = total_build_ms
|
||||
.saturating_add(result.duration.as_millis());
|
||||
total_build_ms =
|
||||
total_build_ms.saturating_add(result.duration.as_millis());
|
||||
built_steps.push((built_harness.workdir, spec));
|
||||
}
|
||||
Err(_) => build_errors += 1,
|
||||
|
|
@ -400,7 +400,11 @@ fn run_chain_steps(
|
|||
let mut prev_output: Option<Vec<u8>> = 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<crate::evidence::VerifyResult> {
|
||||
fn composite_dynamic_verdict(_path: &[ChainEdge]) -> Option<crate::evidence::VerifyResult> {
|
||||
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<u64> = 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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue