mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
Phase 1 (#33)
* chore: Exclude CLAUDE.md from Cargo.toml * feat: add callgraph module and integrate into main analysis flow * feat: enhance CLI with new severity filtering and analysis modes * feat: update CHANGELOG with recent enhancements and fixes to severity filtering and output handling * feat: implement state-model dataflow analysis for resource lifecycle and auth state * feat: enhance diagnostic output formatting and add evidence structure * feat: implement attack surface ranking for diagnostics with scoring and sorting * feat: add comprehensive documentation for installation, usage, and rules reference * feat: add multiple language support for command execution and evaluation endpoints * feat: implement inline suppression for findings using `nyx:ignore` comments * feat: add confidence levels to AST patterns and update output structure * feat: implement low-noise prioritization system with category filtering, rollup grouping, and configurable budgets * feat: bump version to 0.4.0 and update changelog with new features and improvements * feat: add dead code allowances to various functions in mod.rs and real_world_tests.rs
This commit is contained in:
parent
19b578c5c4
commit
1bbe4b1cfb
456 changed files with 25628 additions and 1228 deletions
288
src/state/engine.rs
Normal file
288
src/state/engine.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use super::lattice::Lattice;
|
||||
use crate::cfg::{Cfg, EdgeKind, NodeInfo};
|
||||
use petgraph::graph::NodeIndex;
|
||||
use petgraph::visit::EdgeRef;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
/// Maximum tracked variables per function (guarded degradation).
|
||||
pub const MAX_TRACKED_VARS: usize = 64;
|
||||
|
||||
/// Default worklist iteration budget.
|
||||
pub const MAX_WORKLIST_ITERATIONS: usize = 100_000;
|
||||
|
||||
/// Generic transfer function trait for forward dataflow analysis.
|
||||
///
|
||||
/// Domains implement this to define how abstract state flows through
|
||||
/// CFG nodes and what events (findings) are emitted.
|
||||
pub trait Transfer<S: Lattice> {
|
||||
/// Side-channel events emitted during transfer (e.g., findings, violations).
|
||||
type Event: Clone;
|
||||
|
||||
/// Apply the transfer function to a node, returning the output state
|
||||
/// and any events.
|
||||
fn apply(
|
||||
&self,
|
||||
node: NodeIndex,
|
||||
info: &NodeInfo,
|
||||
edge: Option<EdgeKind>,
|
||||
state: S,
|
||||
) -> (S, Vec<Self::Event>);
|
||||
|
||||
/// Per-domain iteration budget. Defaults to [`MAX_WORKLIST_ITERATIONS`].
|
||||
fn iteration_budget(&self) -> usize {
|
||||
MAX_WORKLIST_ITERATIONS
|
||||
}
|
||||
|
||||
/// Called when the budget is exhausted. Returns true if the engine
|
||||
/// should continue with the current (non-converged) state, false to bail.
|
||||
fn on_budget_exceeded(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of running the forward dataflow engine.
|
||||
pub struct DataflowResult<S, E> {
|
||||
/// Converged state at the entry of each node.
|
||||
pub states: HashMap<NodeIndex, S>,
|
||||
/// Events emitted during Phase 2 transfer over converged states.
|
||||
pub events: Vec<E>,
|
||||
/// Whether the analysis converged (false if budget was hit).
|
||||
#[allow(dead_code)]
|
||||
pub converged: bool,
|
||||
}
|
||||
|
||||
/// Run a forward worklist dataflow analysis over the CFG.
|
||||
///
|
||||
/// Two-phase design:
|
||||
/// - Phase 1: fixed-point iteration to converge states (no event collection).
|
||||
/// - Phase 2: single pass over converged states to collect events.
|
||||
///
|
||||
/// Termination is guaranteed by lattice finiteness + iteration budget.
|
||||
pub fn run_forward<S: Lattice, T: Transfer<S>>(
|
||||
cfg: &Cfg,
|
||||
entry: NodeIndex,
|
||||
transfer: &T,
|
||||
initial: S,
|
||||
) -> DataflowResult<S, T::Event> {
|
||||
let mut states: HashMap<NodeIndex, S> = HashMap::new();
|
||||
let budget = transfer.iteration_budget();
|
||||
|
||||
// Initialize entry node
|
||||
states.insert(entry, initial);
|
||||
|
||||
// ── Phase 1: fixed-point iteration (compute converged states) ─────
|
||||
let mut worklist: VecDeque<NodeIndex> = VecDeque::new();
|
||||
worklist.push_back(entry);
|
||||
|
||||
let mut iterations: usize = 0;
|
||||
let mut converged = true;
|
||||
|
||||
while let Some(node) = worklist.pop_front() {
|
||||
iterations += 1;
|
||||
if iterations > budget {
|
||||
converged = !transfer.on_budget_exceeded();
|
||||
if !converged {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let node_state = match states.get(&node) {
|
||||
Some(s) => s.clone(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let edges: Vec<_> = cfg.edges(node).map(|e| (*e.weight(), e.target())).collect();
|
||||
|
||||
// No outgoing edges — nothing to propagate (exit/dead end).
|
||||
if edges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (edge_kind, target) in edges {
|
||||
let info = &cfg[node];
|
||||
let (out_state, _events) =
|
||||
transfer.apply(node, info, Some(edge_kind), node_state.clone());
|
||||
|
||||
// Join into target's state
|
||||
let target_state = states.get(&target);
|
||||
let new_target = match target_state {
|
||||
Some(existing) => existing.join(&out_state),
|
||||
None => out_state,
|
||||
};
|
||||
|
||||
let changed = target_state.is_none_or(|existing| *existing != new_target);
|
||||
if changed {
|
||||
states.insert(target, new_target);
|
||||
if !worklist.contains(&target) {
|
||||
worklist.push_back(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: single pass over converged states to collect events ──
|
||||
let mut events: Vec<T::Event> = Vec::new();
|
||||
let mut seen_edges: std::collections::HashSet<(NodeIndex, NodeIndex)> =
|
||||
std::collections::HashSet::new();
|
||||
|
||||
for node in states.keys().copied().collect::<Vec<_>>() {
|
||||
let node_state = match states.get(&node) {
|
||||
Some(s) => s.clone(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let edges: Vec<_> = cfg.edges(node).map(|e| (*e.weight(), e.target())).collect();
|
||||
|
||||
if edges.is_empty() {
|
||||
// Exit / dead end — apply transfer for event collection.
|
||||
let info = &cfg[node];
|
||||
let (_out_state, new_events) = transfer.apply(node, info, None, node_state);
|
||||
events.extend(new_events);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (edge_kind, target) in edges {
|
||||
if !seen_edges.insert((node, target)) {
|
||||
continue;
|
||||
}
|
||||
let info = &cfg[node];
|
||||
let (_out_state, new_events) =
|
||||
transfer.apply(node, info, Some(edge_kind), node_state.clone());
|
||||
events.extend(new_events);
|
||||
}
|
||||
}
|
||||
|
||||
DataflowResult {
|
||||
states,
|
||||
events,
|
||||
converged,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::cfg::{EdgeKind, NodeInfo, StmtKind};
|
||||
use crate::cfg_analysis::rules;
|
||||
use crate::state::domain::ResourceLifecycle;
|
||||
use crate::state::symbol::SymbolInterner;
|
||||
use crate::state::transfer::DefaultTransfer;
|
||||
use crate::symbol::Lang;
|
||||
use petgraph::Graph;
|
||||
|
||||
fn make_node(kind: StmtKind) -> NodeInfo {
|
||||
NodeInfo {
|
||||
kind,
|
||||
span: (0, 0),
|
||||
label: None,
|
||||
defines: None,
|
||||
uses: vec![],
|
||||
callee: None,
|
||||
enclosing_func: None,
|
||||
call_ordinal: 0,
|
||||
condition_text: None,
|
||||
condition_vars: vec![],
|
||||
condition_negated: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_cfg_converges() {
|
||||
use crate::state::domain::ProductState;
|
||||
|
||||
// Entry → fopen(f) → fclose(f) → Exit
|
||||
let mut cfg: Cfg = Graph::new();
|
||||
let entry = cfg.add_node(make_node(StmtKind::Entry));
|
||||
let open_node = cfg.add_node(NodeInfo {
|
||||
kind: StmtKind::Call,
|
||||
defines: Some("f".into()),
|
||||
callee: Some("fopen".into()),
|
||||
..make_node(StmtKind::Call)
|
||||
});
|
||||
let close_node = cfg.add_node(NodeInfo {
|
||||
kind: StmtKind::Call,
|
||||
uses: vec!["f".into()],
|
||||
callee: Some("fclose".into()),
|
||||
..make_node(StmtKind::Call)
|
||||
});
|
||||
let exit = cfg.add_node(make_node(StmtKind::Exit));
|
||||
|
||||
cfg.add_edge(entry, open_node, EdgeKind::Seq);
|
||||
cfg.add_edge(open_node, close_node, EdgeKind::Seq);
|
||||
cfg.add_edge(close_node, exit, EdgeKind::Seq);
|
||||
|
||||
let interner = SymbolInterner::from_cfg(&cfg);
|
||||
let transfer = DefaultTransfer {
|
||||
lang: Lang::C,
|
||||
resource_pairs: rules::resource_pairs(Lang::C),
|
||||
interner: &interner,
|
||||
};
|
||||
|
||||
let result = run_forward(&cfg, entry, &transfer, ProductState::initial());
|
||||
|
||||
// No events (clean open→close)
|
||||
assert!(result.events.is_empty());
|
||||
assert!(result.converged);
|
||||
|
||||
// At exit, f should be CLOSED
|
||||
let sym_f = interner.get("f").unwrap();
|
||||
let exit_state = result.states.get(&exit).unwrap();
|
||||
assert_eq!(exit_state.resource.get(sym_f), ResourceLifecycle::CLOSED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diamond_cfg_joins_states() {
|
||||
use crate::state::domain::ProductState;
|
||||
|
||||
// Entry
|
||||
// |
|
||||
// fopen(f)
|
||||
// |
|
||||
// If
|
||||
// / \
|
||||
// fclose(f) (no close)
|
||||
// \ /
|
||||
// Exit
|
||||
let mut cfg: Cfg = Graph::new();
|
||||
let entry = cfg.add_node(make_node(StmtKind::Entry));
|
||||
let open_node = cfg.add_node(NodeInfo {
|
||||
kind: StmtKind::Call,
|
||||
defines: Some("f".into()),
|
||||
callee: Some("fopen".into()),
|
||||
..make_node(StmtKind::Call)
|
||||
});
|
||||
let if_node = cfg.add_node(make_node(StmtKind::If));
|
||||
let close_node = cfg.add_node(NodeInfo {
|
||||
kind: StmtKind::Call,
|
||||
uses: vec!["f".into()],
|
||||
callee: Some("fclose".into()),
|
||||
..make_node(StmtKind::Call)
|
||||
});
|
||||
let no_close = cfg.add_node(make_node(StmtKind::Seq));
|
||||
let exit = cfg.add_node(make_node(StmtKind::Exit));
|
||||
|
||||
cfg.add_edge(entry, open_node, EdgeKind::Seq);
|
||||
cfg.add_edge(open_node, if_node, EdgeKind::Seq);
|
||||
cfg.add_edge(if_node, close_node, EdgeKind::True);
|
||||
cfg.add_edge(if_node, no_close, EdgeKind::False);
|
||||
cfg.add_edge(close_node, exit, EdgeKind::Seq);
|
||||
cfg.add_edge(no_close, exit, EdgeKind::Seq);
|
||||
|
||||
let interner = SymbolInterner::from_cfg(&cfg);
|
||||
let transfer = DefaultTransfer {
|
||||
lang: Lang::C,
|
||||
resource_pairs: rules::resource_pairs(Lang::C),
|
||||
interner: &interner,
|
||||
};
|
||||
|
||||
let result = run_forward(&cfg, entry, &transfer, ProductState::initial());
|
||||
|
||||
// At exit, f should be OPEN | CLOSED (may-leak)
|
||||
let sym_f = interner.get("f").unwrap();
|
||||
let exit_state = result.states.get(&exit).unwrap();
|
||||
assert_eq!(
|
||||
exit_state.resource.get(sym_f),
|
||||
ResourceLifecycle::OPEN | ResourceLifecycle::CLOSED
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue