mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] sweep after phase 24: 2 deferred items resolved
This commit is contained in:
parent
c9e7342ad3
commit
a3ab1215f1
2 changed files with 84 additions and 4 deletions
|
|
@ -20,6 +20,7 @@ use crate::surface::{SourceLocation, SurfaceMap, SurfaceNode};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::feasibility::Feasibility;
|
||||
use super::impact::lookup_impact;
|
||||
|
||||
/// Compact reference to a static finding embedded in a [`ChainEdge`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
@ -66,9 +67,13 @@ pub enum Reach {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ChainEdge {
|
||||
pub finding: FindingRef,
|
||||
/// Primary cap classification. Picked deterministically as the
|
||||
/// lowest set bit of [`FindingRef::cap_bits`] so two scans of the
|
||||
/// same source produce identical edges.
|
||||
/// Primary cap classification. Picked via [`pick_chain_cap`]: when
|
||||
/// several cap bits are set, prefers a bit that has a standalone
|
||||
/// rule in [`crate::chain::impact::IMPACT_LATTICE`] over the
|
||||
/// lowest bit so a `SQL_QUERY | CODE_EXEC` finding lands on the
|
||||
/// chain-relevant cap (`CODE_EXEC`). Falls back to the lowest set
|
||||
/// bit when no bit has a standalone rule, keeping single-cap
|
||||
/// findings deterministic.
|
||||
pub primary_cap: Cap,
|
||||
/// Where the finding sits relative to the surface.
|
||||
pub reach: Reach,
|
||||
|
|
@ -101,7 +106,7 @@ fn build_edge(diag: &Diag, surface: &SurfaceMap) -> Option<ChainEdge> {
|
|||
return None;
|
||||
}
|
||||
let cap_bits = evidence.sink_caps;
|
||||
let primary_cap = lowest_cap(cap_bits)?;
|
||||
let primary_cap = pick_chain_cap(cap_bits)?;
|
||||
let location = SourceLocation::new(diag.path.clone(), diag.line as u32, diag.col as u32);
|
||||
let reach = locate_reach(&location, surface);
|
||||
let feasibility = Feasibility::for_finding(diag);
|
||||
|
|
@ -130,6 +135,35 @@ pub fn lowest_cap(bits: u32) -> Option<Cap> {
|
|||
Cap::from_bits(lowest)
|
||||
}
|
||||
|
||||
/// Pick the chain-relevant [`Cap`] from a sink-cap bitmask.
|
||||
///
|
||||
/// When multiple caps are set, prefer one that has a standalone rule in
|
||||
/// [`crate::chain::impact::IMPACT_LATTICE`] (e.g. `CODE_EXEC`,
|
||||
/// `DESERIALIZE`, `SSRF`) over the lowest set bit. A finding with
|
||||
/// `sink_caps = SQL_QUERY | CODE_EXEC` previously resolved to
|
||||
/// `SQL_QUERY` (the lowest bit) and missed the `CODE_EXEC → Rce`
|
||||
/// lattice rule; this helper resolves it to `CODE_EXEC` instead.
|
||||
///
|
||||
/// Iterates bits low to high so ties between caps with standalone
|
||||
/// rules stay deterministic. Falls back to [`lowest_cap`] when no
|
||||
/// bit has a standalone rule, preserving single-cap behaviour.
|
||||
pub fn pick_chain_cap(bits: u32) -> Option<Cap> {
|
||||
if bits == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut remaining = bits;
|
||||
while remaining != 0 {
|
||||
let bit = 1u32 << remaining.trailing_zeros();
|
||||
if let Some(cap) = Cap::from_bits(bit) {
|
||||
if lookup_impact(cap, None).is_some() {
|
||||
return Some(cap);
|
||||
}
|
||||
}
|
||||
remaining &= !bit;
|
||||
}
|
||||
lowest_cap(bits)
|
||||
}
|
||||
|
||||
fn locate_reach(loc: &SourceLocation, surface: &SurfaceMap) -> Reach {
|
||||
for node in &surface.nodes {
|
||||
if let SurfaceNode::EntryPoint(ep) = node {
|
||||
|
|
@ -175,6 +209,29 @@ mod tests {
|
|||
assert_eq!(lowest_cap(combined.bits()), Some(Cap::FILE_IO));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_chain_cap_prefers_standalone_rule_cap() {
|
||||
// SQL_QUERY (bit 7) has no standalone lattice rule; CODE_EXEC
|
||||
// (bit 10) does. Lowest-bit alone would pick SQL_QUERY.
|
||||
let combined = Cap::SQL_QUERY | Cap::CODE_EXEC;
|
||||
assert_eq!(pick_chain_cap(combined.bits()), Some(Cap::CODE_EXEC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_chain_cap_falls_back_to_lowest_when_no_standalone_rule() {
|
||||
// SQL_QUERY + FILE_IO: neither has a standalone rule, fall
|
||||
// back to lowest_cap behaviour.
|
||||
let combined = Cap::SQL_QUERY | Cap::FILE_IO;
|
||||
assert_eq!(pick_chain_cap(combined.bits()), Some(Cap::FILE_IO));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_chain_cap_single_bit_unchanged() {
|
||||
assert_eq!(pick_chain_cap(Cap::CODE_EXEC.bits()), Some(Cap::CODE_EXEC));
|
||||
assert_eq!(pick_chain_cap(Cap::SQL_QUERY.bits()), Some(Cap::SQL_QUERY));
|
||||
assert_eq!(pick_chain_cap(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_findings_without_cap_bits() {
|
||||
let mut d = diag_with_cap("a.py", 1, Cap::CODE_EXEC);
|
||||
|
|
|
|||
|
|
@ -161,6 +161,29 @@ const _: () = assert!(
|
|||
"Cap bit appears in both IMPACT_LATTICE_COVERED and IMPACT_LATTICE_UNCOVERED",
|
||||
);
|
||||
|
||||
/// Union of every cap bit referenced by an [`IMPACT_LATTICE`] rule, as
|
||||
/// `source_cap` or `adjacent_cap`. Computed at compile time.
|
||||
const fn rule_coverage_bits() -> u32 {
|
||||
let mut acc: u32 = 0;
|
||||
let mut i = 0;
|
||||
while i < IMPACT_LATTICE.len() {
|
||||
let rule = IMPACT_LATTICE[i];
|
||||
acc |= rule.source_cap.bits();
|
||||
acc |= match rule.adjacent_cap {
|
||||
Some(a) => a.bits(),
|
||||
None => 0,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
const _: () = assert!(
|
||||
rule_coverage_bits() == IMPACT_LATTICE_COVERED,
|
||||
"IMPACT_LATTICE_COVERED claims a cap bit that no IMPACT_LATTICE rule references; \
|
||||
drop it from IMPACT_LATTICE_COVERED or add a rule that consumes it",
|
||||
);
|
||||
|
||||
/// Look up an [`ImpactCategory`] for a (source, adjacent) cap pair.
|
||||
///
|
||||
/// `adjacent` is `None` when the caller has not yet found a partner
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue