use crate::filters::{
BenignNameConfig, BenignNameFilter, ConsequenceAnalyzer, ConsequenceConfig, RhsAnalyzer,
RhsConfig, ScopeFilter, ScopeFilterConfig,
};
use crate::models::{AnalysisContext, Comparison, ScoreFactor, SuspectValue};
#[derive(Debug, Clone, Default)]
pub struct FilterPipelineConfig {
pub name_config: BenignNameConfig,
pub scope_config: ScopeFilterConfig,
pub consequence_config: ConsequenceConfig,
pub rhs_config: RhsConfig,
pub enable_name_filter: bool,
pub enable_scope_filter: bool,
pub enable_consequence_filter: bool,
pub enable_rhs_filter: bool,
}
impl FilterPipelineConfig {
pub fn new() -> Self {
Self {
name_config: BenignNameConfig::default(),
scope_config: ScopeFilterConfig::default(),
consequence_config: ConsequenceConfig::default(),
rhs_config: RhsConfig::default(),
enable_name_filter: true,
enable_scope_filter: true,
enable_consequence_filter: true,
enable_rhs_filter: true,
}
}
}
#[derive(Debug)]
pub struct FilterResult {
pub killed: bool,
pub kill_reason: Option<String>,
pub factors: Vec<ScoreFactor>,
}
impl FilterResult {
pub fn killed(reason: impl Into<String>) -> Self {
Self {
killed: true,
kill_reason: Some(reason.into()),
factors: Vec::new(),
}
}
pub fn passed(factors: Vec<ScoreFactor>) -> Self {
Self {
killed: false,
kill_reason: None,
factors,
}
}
pub fn total_score(&self) -> i32 {
self.factors.iter().map(|f| f.contribution).sum()
}
}
pub struct FilterPipeline {
name_filter: Option<BenignNameFilter>,
scope_filter: Option<ScopeFilter>,
consequence_analyzer: Option<ConsequenceAnalyzer>,
rhs_analyzer: Option<RhsAnalyzer>,
}
impl FilterPipeline {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
Self::with_config(FilterPipelineConfig::new())
}
pub fn with_config(config: FilterPipelineConfig) -> Result<Self, Box<dyn std::error::Error>> {
let name_filter = if config.enable_name_filter {
Some(BenignNameFilter::new(config.name_config)?)
} else {
None
};
let scope_filter = if config.enable_scope_filter {
Some(ScopeFilter::new(config.scope_config)?)
} else {
None
};
let consequence_analyzer = if config.enable_consequence_filter {
Some(ConsequenceAnalyzer::new(config.consequence_config))
} else {
None
};
let rhs_analyzer = if config.enable_rhs_filter {
Some(RhsAnalyzer::new(config.rhs_config))
} else {
None
};
Ok(Self {
name_filter,
scope_filter,
consequence_analyzer,
rhs_analyzer,
})
}
pub fn analyze(
&self,
suspect: &SuspectValue,
comparison: Option<&Comparison>,
context: &AnalysisContext,
) -> FilterResult {
let mut all_factors = Vec::new();
if let Some(scope_filter) = &self.scope_filter {
if scope_filter.should_skip(context) {
return FilterResult::killed("Finding is in ignored scope (test/example/bench)");
}
all_factors.extend(scope_filter.analyze(context));
}
if let Some(name_filter) = &self.name_filter {
if let Some(name) = suspect.name() {
let name_factors = name_filter.analyze(name);
for factor in &name_factors {
if factor.contribution <= -100 {
return FilterResult::killed(format!(
"Name '{}' is benign: {}",
name, factor.reason
));
}
}
all_factors.extend(name_factors);
}
}
if let Some(consequence_analyzer) = &self.consequence_analyzer {
if let Some(comp) = comparison {
if let Some(consequence) = &comp.consequence {
all_factors.extend(consequence_analyzer.score(consequence));
}
}
}
if let Some(rhs_analyzer) = &self.rhs_analyzer {
if let Some(comp) = comparison {
if let Some(variable) = comp.variable_side() {
all_factors.extend(rhs_analyzer.analyze(variable));
}
}
}
FilterResult::passed(all_factors)
}
pub fn is_definitely_benign(&self, name: &str) -> bool {
if let Some(name_filter) = &self.name_filter {
name_filter.is_definitely_benign(name)
} else {
false
}
}
pub fn should_skip_context(&self, context: &AnalysisContext) -> bool {
if let Some(scope_filter) = &self.scope_filter {
scope_filter.should_skip(context)
} else {
false
}
}
pub fn looks_like_routing(&self, comparison: &Comparison) -> bool {
if let Some(rhs_analyzer) = &self.rhs_analyzer {
if let Some(variable) = comparison.variable_side() {
return rhs_analyzer.looks_like_routing(variable);
}
}
false
}
pub fn looks_like_auth(&self, comparison: &Comparison) -> bool {
if let Some(rhs_analyzer) = &self.rhs_analyzer {
if let Some(variable) = comparison.variable_side() {
return rhs_analyzer.looks_like_auth(variable);
}
}
false
}
}
impl Default for FilterPipeline {
fn default() -> Self {
Self::new().expect("Failed to create default FilterPipeline")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_context() -> AnalysisContext {
AnalysisContext::new(PathBuf::from("src/lib.rs"))
}
fn test_context_in_tests() -> AnalysisContext {
let mut ctx = AnalysisContext::new(PathBuf::from("tests/auth_test.rs"));
ctx.in_test_context = true;
ctx
}
#[test]
fn test_version_constant_is_killed() {
let pipeline = FilterPipeline::new().unwrap();
let suspect = SuspectValue::Constant {
name: "VERSION".into(),
value: "1.0.0".into(),
type_annotation: None,
};
let result = pipeline.analyze(&suspect, None, &test_context());
assert!(result.killed, "VERSION should be killed");
}
#[test]
fn test_auth_token_not_killed() {
let pipeline = FilterPipeline::new().unwrap();
let suspect = SuspectValue::Constant {
name: "AUTH_TOKEN".into(),
value: "secret123".into(),
type_annotation: None,
};
let result = pipeline.analyze(&suspect, None, &test_context());
assert!(!result.killed, "AUTH_TOKEN should not be killed");
assert!(
result.total_score() > 0,
"AUTH_TOKEN should have positive score"
);
}
#[test]
fn test_test_context_kills() {
let pipeline = FilterPipeline::new().unwrap();
let suspect = SuspectValue::Constant {
name: "SECRET_KEY".into(),
value: "test_secret".into(),
type_annotation: None,
};
let result = pipeline.analyze(&suspect, None, &test_context_in_tests());
assert!(result.killed, "Test context should kill finding");
}
#[test]
fn test_definitely_benign_check() {
let pipeline = FilterPipeline::new().unwrap();
assert!(pipeline.is_definitely_benign("VERSION"));
assert!(pipeline.is_definitely_benign("MIME_TYPE"));
assert!(!pipeline.is_definitely_benign("SECRET_KEY"));
}
}