mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
[pitboss] sweep after phase 26: 4 deferred items resolved
This commit is contained in:
parent
8a801953e2
commit
ea722dc9ca
8 changed files with 320 additions and 130 deletions
|
|
@ -217,9 +217,25 @@ fn compose_chain(
|
|||
let sink_cap = sole_cap(sink.cap_bits)?;
|
||||
let (impact, member_impacts) =
|
||||
resolve_impact(&path, sink_cap, entry, local_listener_present)?;
|
||||
Some(build_chain(entry, sink, &path, impact, &member_impacts))
|
||||
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
|
||||
// exposes a loopback listener, the chain is more concrete than the
|
||||
// bare lattice match — lift the score so it ranks above SSRF chains
|
||||
// without a corroborating in-process target.
|
||||
if impact == ImpactCategory::InternalNetworkAccess && local_listener_present {
|
||||
chain.score *= LOCAL_LISTENER_BOOST;
|
||||
}
|
||||
Some(chain)
|
||||
}
|
||||
|
||||
/// Score multiplier applied when an `InternalNetworkAccess` chain has
|
||||
/// a corroborating loopback listener in the SurfaceMap. Calibrated to
|
||||
/// lift the chain above an otherwise-identical SSRF chain that lacks
|
||||
/// the listener context, without overtaking strictly more severe
|
||||
/// categories.
|
||||
const LOCAL_LISTENER_BOOST: f64 = 1.5;
|
||||
|
||||
/// Pick the lowest-bit single [`Cap`] from `bits`, or `None` when no
|
||||
/// bit is set. Sinks in the SurfaceMap may carry multi-bit
|
||||
/// `cap_bits`; the DFS terminates against the lowest single bit so
|
||||
|
|
@ -557,6 +573,61 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssrf_with_local_listener_scores_higher_than_without() {
|
||||
use crate::surface::{DataStore, DataStoreKind};
|
||||
let edge = || -> ChainEdge {
|
||||
edge_with(
|
||||
"app.py",
|
||||
10,
|
||||
"taint-ssrf",
|
||||
Cap::SSRF,
|
||||
"/fetch",
|
||||
HttpMethod::POST,
|
||||
Feasibility::Confirmed,
|
||||
)
|
||||
};
|
||||
let mut surface_no_listener = SurfaceMap::new();
|
||||
surface_no_listener.nodes.push(entry("app.py", "/fetch", false));
|
||||
surface_no_listener
|
||||
.nodes
|
||||
.push(sink("app.py", 20, "requests.get", Cap::SSRF));
|
||||
let baseline = find_chains(
|
||||
&[edge()],
|
||||
&surface_no_listener,
|
||||
ChainSearchConfig {
|
||||
max_depth: 4,
|
||||
min_score: 0.0,
|
||||
},
|
||||
);
|
||||
assert_eq!(baseline.len(), 1);
|
||||
assert_eq!(baseline[0].implied_impact, ImpactCategory::InternalNetworkAccess);
|
||||
|
||||
let mut surface_with_listener = surface_no_listener.clone();
|
||||
surface_with_listener
|
||||
.nodes
|
||||
.push(SurfaceNode::DataStore(DataStore {
|
||||
location: loc("app.py", 5),
|
||||
kind: DataStoreKind::KeyValue,
|
||||
label: "redis://127.0.0.1:6379".into(),
|
||||
}));
|
||||
let boosted = find_chains(
|
||||
&[edge()],
|
||||
&surface_with_listener,
|
||||
ChainSearchConfig {
|
||||
max_depth: 4,
|
||||
min_score: 0.0,
|
||||
},
|
||||
);
|
||||
assert_eq!(boosted.len(), 1);
|
||||
assert_eq!(boosted[0].implied_impact, ImpactCategory::InternalNetworkAccess);
|
||||
let ratio = boosted[0].score / baseline[0].score;
|
||||
assert!(
|
||||
(ratio - LOCAL_LISTENER_BOOST).abs() < 1e-9,
|
||||
"expected ×{LOCAL_LISTENER_BOOST} boost, got ratio={ratio}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_threshold_drops_low_score_chains() {
|
||||
let mut surface = SurfaceMap::new();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue