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:
Eli Peter 2026-02-24 23:44:07 -05:00 committed by GitHub
parent 8cbbec7d90
commit f96a89e7c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 11505 additions and 1099 deletions

208
src/cfg_analysis/guards.rs Normal file
View 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
}
}