[pitboss] sweep after phase 24: 2 deferred items resolved

This commit is contained in:
pitboss 2026-05-15 15:32:24 -05:00
parent c9e7342ad3
commit a3ab1215f1
2 changed files with 84 additions and 4 deletions

View file

@ -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);

View file

@ -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