mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-21 21:31:03 +02:00
Feat/full cfg (#30)
* feat: Enhance control flow analysis with function summaries and taint analysis * feat: Update taint analysis to utilize function summaries for enhanced tracking * Refactor `walk.rs` batch processing and override handling: - Renamed `Batcher` to `BatchSender` for clarity. - Added `BatchSender::new` constructor for cleaner initialization. - Simplified batch size management in `BatchSender`. - Extracted `build_overrides` function for reusable override construction. - Improved error handling and validation in override building. - Enhanced performance with directory and file type filtering in `walk`. * Improve logging and streamline directory walk process: - Added detailed `tracing` logs for debugging batch flushes, override construction, and walk initialization/completion. - Optimized and simplified `filter_entry` logic for directory and file type filters. - Improved metadata checks and max file size enforcement during the scan. * Refactor and optimize taint tracking, label rules, and directory walk process: - Replaced `DefaultHasher` with `blake3::Hasher` for improved taint hashing. - Enhanced sorting and hashing logic in `taint.rs` for consistency and efficiency. - Removed unused `set_hash` function and redundant imports across files. - Improved batch sender logic in `walk.rs`, renaming key components for clarity. - Unified `spawn_senders` and `spawn_file_walker` with thread handling and channel tuple return. - Expanded label rules with additional matchers for sources, sanitizers, and sinks. - Deprecated `dump_cfg` and specific logging utilities in `cfg.rs` for code cleanup. * fix: fixed let chains error in walk.rs * fix: updated dependencies * fix: updated dependencies * chore: Remove standard error in scan.rs * feat: Introduce function summaries for enhanced taint and control flow analysis * feat: Enhance taint analysis with interop support and function summaries * feat: Add configuration analysis module and enhance matcher rules * feat: Add arity column to function_summaries and handle schema migration * fix: fixed clippy &PathBuf warnings * chore: Update dependencies and versioning in Cargo files * docs: Update README to enhance clarity and detail on features and analysis modes * chore: Update CHANGELOG for version 0.2.0 with new features, changes, and fixes * docs: Update SECURITY.md to clarify version support status --------- Co-authored-by: elipeter <eli.peter@es.fcm.travel>
This commit is contained in:
parent
8cbbec7d90
commit
f96a89e7c1
87 changed files with 11505 additions and 1099 deletions
208
src/cfg_analysis/guards.rs
Normal file
208
src/cfg_analysis/guards.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
use super::dominators::{self, dominates};
|
||||
use super::rules;
|
||||
use super::{AnalysisContext, CfgAnalysis, CfgFinding, Confidence, is_entry_point_func};
|
||||
use crate::cfg::StmtKind;
|
||||
use crate::labels::{Cap, DataLabel};
|
||||
use crate::patterns::Severity;
|
||||
use petgraph::graph::NodeIndex;
|
||||
|
||||
pub struct UnguardedSink;
|
||||
|
||||
/// Find all nodes in the CFG that are calls to guard functions.
|
||||
fn find_guard_nodes(ctx: &AnalysisContext) -> Vec<(NodeIndex, Cap)> {
|
||||
let guard_rules = rules::guard_rules(ctx.lang);
|
||||
let mut result = Vec::new();
|
||||
|
||||
for idx in ctx.cfg.node_indices() {
|
||||
let info = &ctx.cfg[idx];
|
||||
if info.kind != StmtKind::Call {
|
||||
continue;
|
||||
}
|
||||
if let Some(callee) = &info.callee {
|
||||
let callee_lower = callee.to_ascii_lowercase();
|
||||
for rule in guard_rules {
|
||||
let matched = rule.matchers.iter().any(|m| {
|
||||
let ml = m.to_ascii_lowercase();
|
||||
if ml.ends_with('_') {
|
||||
callee_lower.starts_with(&ml)
|
||||
} else {
|
||||
callee_lower.ends_with(&ml)
|
||||
}
|
||||
});
|
||||
if matched {
|
||||
result.push((idx, rule.applies_to_sink_caps));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check whether taint analysis confirmed unsanitized flow to this sink node.
|
||||
fn taint_confirms_sink(ctx: &AnalysisContext, sink: NodeIndex) -> bool {
|
||||
ctx.taint_findings.iter().any(|f| f.sink == sink)
|
||||
}
|
||||
|
||||
/// Check whether any variable used by the sink is directly derived from a
|
||||
/// Source node in the same function (via simple def-use chain).
|
||||
fn sink_arg_is_source_derived(ctx: &AnalysisContext, sink: NodeIndex) -> bool {
|
||||
let sink_info = &ctx.cfg[sink];
|
||||
let sink_func = sink_info.enclosing_func.as_deref();
|
||||
|
||||
// Collect all variables the sink reads
|
||||
let sink_uses = &sink_info.uses;
|
||||
if sink_uses.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Walk all nodes in the same function looking for Source nodes that define
|
||||
// one of the variables the sink uses.
|
||||
for idx in ctx.cfg.node_indices() {
|
||||
let info = &ctx.cfg[idx];
|
||||
if info.enclosing_func.as_deref() != sink_func {
|
||||
continue;
|
||||
}
|
||||
if !matches!(info.label, Some(DataLabel::Source(_))) {
|
||||
continue;
|
||||
}
|
||||
// Source node defines a variable that the sink reads → source-derived
|
||||
if let Some(def) = &info.defines
|
||||
&& sink_uses.iter().any(|u| u == def)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether the sink's arguments are *only* function parameters
|
||||
/// (i.e. this function is a thin wrapper around the sink).
|
||||
fn sink_arg_is_parameter_only(ctx: &AnalysisContext, sink: NodeIndex) -> bool {
|
||||
let sink_info = &ctx.cfg[sink];
|
||||
let sink_func = sink_info.enclosing_func.as_deref();
|
||||
|
||||
let sink_uses = &sink_info.uses;
|
||||
if sink_uses.is_empty() {
|
||||
// No identifiable arguments — could be a constant call like Command::new("ls")
|
||||
return true; // treat as non-dangerous (constant arg)
|
||||
}
|
||||
|
||||
// Collect parameter names for the enclosing function from FuncSummaries
|
||||
let param_names: Vec<&str> = ctx
|
||||
.func_summaries
|
||||
.values()
|
||||
.filter(|s| {
|
||||
// Match by function entry being in the same function
|
||||
ctx.cfg[s.entry].enclosing_func.as_deref() == sink_func
|
||||
})
|
||||
.flat_map(|s| s.param_names.iter().map(|p| p.as_str()))
|
||||
.collect();
|
||||
|
||||
if param_names.is_empty() {
|
||||
return false; // can't determine params
|
||||
}
|
||||
|
||||
// Check if ALL sink uses are parameters
|
||||
sink_uses.iter().all(|u| param_names.contains(&u.as_str()))
|
||||
}
|
||||
|
||||
/// Check if the enclosing function qualifies as an entrypoint.
|
||||
fn sink_in_entrypoint(ctx: &AnalysisContext, sink: NodeIndex) -> bool {
|
||||
let sink_info = &ctx.cfg[sink];
|
||||
if let Some(func_name) = &sink_info.enclosing_func {
|
||||
is_entry_point_func(func_name, ctx.lang)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl CfgAnalysis for UnguardedSink {
|
||||
fn name(&self) -> &'static str {
|
||||
"unguarded-sink"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &AnalysisContext) -> Vec<CfgFinding> {
|
||||
let doms = dominators::compute_dominators(ctx.cfg, ctx.entry);
|
||||
let sink_nodes = dominators::find_sink_nodes(ctx.cfg);
|
||||
let guard_nodes = find_guard_nodes(ctx);
|
||||
|
||||
let mut findings = Vec::new();
|
||||
|
||||
for sink in &sink_nodes {
|
||||
let sink_info = &ctx.cfg[*sink];
|
||||
let sink_caps = match sink_info.label {
|
||||
Some(DataLabel::Sink(caps)) => caps,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let sink_func = sink_info.enclosing_func.as_deref();
|
||||
|
||||
// Check: does any applicable guard dominate this sink?
|
||||
// Guards must be in the same function to be relevant.
|
||||
let is_guarded = guard_nodes.iter().any(|(guard_idx, guard_caps)| {
|
||||
let guard_func = ctx.cfg[*guard_idx].enclosing_func.as_deref();
|
||||
(*guard_caps & sink_caps) != Cap::empty()
|
||||
&& guard_func == sink_func
|
||||
&& dominates(&doms, *guard_idx, *sink)
|
||||
});
|
||||
|
||||
// Also check if an inline sanitizer dominates this sink (same function).
|
||||
let has_sanitizer = ctx.cfg.node_indices().any(|idx| {
|
||||
let node_func = ctx.cfg[idx].enclosing_func.as_deref();
|
||||
if let Some(DataLabel::Sanitizer(san_caps)) = ctx.cfg[idx].label {
|
||||
(san_caps & sink_caps) != Cap::empty()
|
||||
&& node_func == sink_func
|
||||
&& dominates(&doms, idx, *sink)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if is_guarded || has_sanitizer {
|
||||
continue;
|
||||
}
|
||||
|
||||
let callee_desc = sink_info.callee.as_deref().unwrap_or("(unknown sink)");
|
||||
|
||||
// ── Severity classification ───────────────────────────────
|
||||
//
|
||||
// HIGH: taint confirms flow OR source directly feeds sink
|
||||
// MEDIUM: structural finding without taint confirmation
|
||||
// LOW: wrapper function (param-only, non-entrypoint)
|
||||
|
||||
let has_taint = taint_confirms_sink(ctx, *sink);
|
||||
let source_derived = sink_arg_is_source_derived(ctx, *sink);
|
||||
let param_only = sink_arg_is_parameter_only(ctx, *sink);
|
||||
let in_entrypoint = sink_in_entrypoint(ctx, *sink);
|
||||
|
||||
let (severity, confidence) = if has_taint || source_derived {
|
||||
// Taint-confirmed or directly source-derived → HIGH
|
||||
(Severity::High, Confidence::High)
|
||||
} else if param_only && !in_entrypoint {
|
||||
// Wrapper function consuming only parameters → LOW
|
||||
(Severity::Low, Confidence::Low)
|
||||
} else if in_entrypoint && !param_only {
|
||||
// Entrypoint with non-parameter args but no taint confirmation → MEDIUM
|
||||
(Severity::Medium, Confidence::Medium)
|
||||
} else {
|
||||
// Generic structural finding → MEDIUM
|
||||
(Severity::Medium, Confidence::Medium)
|
||||
};
|
||||
|
||||
findings.push(CfgFinding {
|
||||
rule_id: "cfg-unguarded-sink".to_string(),
|
||||
title: "Unguarded sink".to_string(),
|
||||
severity,
|
||||
confidence,
|
||||
span: sink_info.span,
|
||||
message: format!("Sink `{callee_desc}` has no dominating guard or sanitizer"),
|
||||
evidence: vec![*sink],
|
||||
score: None,
|
||||
});
|
||||
}
|
||||
|
||||
findings
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue