mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
139 lines
5 KiB
Rust
139 lines
5 KiB
Rust
//! Phase 24 — exploit-chain composer scaffolding (Track G.1).
|
|
//!
|
|
//! A `ChainGraph` is the small intermediate representation the chain
|
|
//! composer walks between two pre-existing artefacts: the flat list of
|
|
//! per-finding [`Diag`](crate::commands::scan::Diag)s produced by the
|
|
//! static analyser and the [`SurfaceMap`](crate::surface::SurfaceMap)
|
|
//! produced by Track F.
|
|
//!
|
|
//! Phase 24 ships the types only. The implicit-attacker node and the
|
|
//! bounded DFS that walks edges into [`ChainFinding`]s land in Phase 25
|
|
//! (`src/chain/search.rs`); composite re-verification lands in Phase 26
|
|
//! (`src/chain/reverify.rs`).
|
|
//!
|
|
//! # Storage shape
|
|
//!
|
|
//! Two parallel `Vec`s — `nodes` and `edges` — mirroring `SurfaceMap`'s
|
|
//! shape. Determinism is the caller's responsibility: edges are
|
|
//! produced in the order the source [`Diag`] slice presents, and
|
|
//! `findings_to_edges` does not sort the input. Phase 25 will fold
|
|
//! these into a `petgraph::DiGraph` for path search.
|
|
//!
|
|
//! # Lattice exhaustiveness
|
|
//!
|
|
//! [`impact`] keeps a `IMPACT_LATTICE_COVERED | IMPACT_LATTICE_UNCOVERED
|
|
//! == Cap::all().bits()` const assertion, mirroring the
|
|
//! `CORPUS_SUPPORTED | CORPUS_UNSUPPORTED == Cap::all().bits()` pattern
|
|
//! in [`crate::dynamic::corpus`]. Adding a new `Cap` bit without
|
|
//! updating the lattice fails to compile.
|
|
|
|
use crate::entry_points::HttpMethod;
|
|
use crate::labels::Cap;
|
|
use crate::surface::SourceLocation;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub mod edges;
|
|
pub mod feasibility;
|
|
pub mod finding;
|
|
pub mod impact;
|
|
#[cfg(feature = "dynamic")]
|
|
pub mod reverify;
|
|
pub mod score;
|
|
pub mod search;
|
|
|
|
pub use edges::{ChainEdge, FindingRef, findings_to_edges, findings_to_edges_with_reach};
|
|
pub use feasibility::Feasibility;
|
|
pub use finding::{ChainFinding, ChainMember, ChainSeverity, ChainSink};
|
|
pub use impact::{IMPACT_LATTICE, ImpactCategory, ImpactRule, lookup_impact};
|
|
#[cfg(feature = "dynamic")]
|
|
pub use reverify::{
|
|
ChainReverifyResult, CompositeReverifier, DefaultCompositeReverifier, reverify_chain,
|
|
reverify_chain_with, reverify_top_chains, reverify_top_chains_with,
|
|
};
|
|
pub use score::{ChainScoreConfig, category_weight, min_score_default, score_path};
|
|
pub use search::{ChainSearchConfig, find_chains, find_chains_with_reach};
|
|
|
|
/// One node in a [`ChainGraph`].
|
|
///
|
|
/// `Entry` and `Sink` nodes are translated 1:1 from the SurfaceMap's
|
|
/// [`crate::surface::SurfaceNode::EntryPoint`] and
|
|
/// [`crate::surface::SurfaceNode::DangerousLocal`] variants. `Finding`
|
|
/// nodes wrap a static [`Diag`](crate::commands::scan::Diag) so a path
|
|
/// from an entry to a sink can pin which finding witnesses each hop.
|
|
/// Phase 25's path search treats the implicit attacker as a virtual
|
|
/// predecessor of every `Entry`; there is no explicit `Attacker`
|
|
/// variant on this enum.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "node", rename_all = "snake_case")]
|
|
pub enum ChainNode {
|
|
/// A web entry-point lifted from the SurfaceMap.
|
|
Entry {
|
|
location: SourceLocation,
|
|
method: HttpMethod,
|
|
route: String,
|
|
auth_required: bool,
|
|
},
|
|
/// A static finding produced by the analyser.
|
|
Finding(FindingRef),
|
|
/// A dangerous-local sink lifted from the SurfaceMap.
|
|
Sink {
|
|
location: SourceLocation,
|
|
function_name: String,
|
|
cap_bits: u32,
|
|
},
|
|
}
|
|
|
|
impl ChainNode {
|
|
/// Source location of this node. Used for byte-deterministic
|
|
/// ordering and for the `nyx surface`-style human display.
|
|
pub fn location(&self) -> &SourceLocation {
|
|
match self {
|
|
ChainNode::Entry { location, .. } => location,
|
|
ChainNode::Finding(f) => &f.location,
|
|
ChainNode::Sink { location, .. } => location,
|
|
}
|
|
}
|
|
|
|
/// Cap bitmask carried by this node, or `0` for entry nodes. Used
|
|
/// by Phase 25 to discriminate which [`ImpactRule`] a path matches.
|
|
pub fn cap_bits(&self) -> u32 {
|
|
match self {
|
|
ChainNode::Entry { .. } => 0,
|
|
ChainNode::Finding(f) => f.cap_bits,
|
|
ChainNode::Sink { cap_bits, .. } => *cap_bits,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The full chain graph. Phase 24 only exposes the types; the
|
|
/// composer that fills the vectors lands in Phase 25.
|
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct ChainGraph {
|
|
pub nodes: Vec<ChainNode>,
|
|
pub edges: Vec<ChainEdge>,
|
|
}
|
|
|
|
impl ChainGraph {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn node_count(&self) -> usize {
|
|
self.nodes.len()
|
|
}
|
|
|
|
pub fn edge_count(&self) -> usize {
|
|
self.edges.len()
|
|
}
|
|
}
|
|
|
|
/// Convert a primary [`Cap`] bit into the closest matching impact
|
|
/// category in isolation (no adjacency). Returns `None` when the cap
|
|
/// has no terminal interpretation on its own — chain composition needs
|
|
/// an additional cap or surface property to lift it.
|
|
///
|
|
/// Phase 25's path-search code calls this as a fast-path before
|
|
/// consulting the full [`IMPACT_LATTICE`].
|
|
pub fn standalone_impact(cap: Cap) -> Option<ImpactCategory> {
|
|
lookup_impact(cap, None)
|
|
}
|