pub mod analyzer;
pub mod indexer;
use std::path::PathBuf;
use crate::models::Finding;
use crate::registry::SuspectRegistry;
use crate::scoring::{ScoringConfig, ScoringEngine};
pub use analyzer::Analyzer;
pub use indexer::Indexer;
#[derive(Debug, Clone, Default)]
pub struct ScannerConfig {
pub scoring: ScoringConfig,
pub skip_indexing: bool,
pub max_file_size: Option<usize>,
}
pub struct Scanner {
engine: ScoringEngine,
config: ScannerConfig,
}
impl Scanner {
pub fn new(config: ScannerConfig) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
engine: ScoringEngine::new(config.scoring.clone())?,
config,
})
}
pub fn scan(&self, files: Vec<PathBuf>) -> ScanResult {
let registry = if self.config.skip_indexing {
SuspectRegistry::new()
} else {
tracing::info!("Phase 1: Indexing {} files...", files.len());
let indexer = Indexer::new();
indexer.index_files(&files)
};
let stats = registry.stats();
tracing::info!(
"Indexed {} constants ({} public, {} suspicious)",
stats.total,
stats.public,
stats.suspicious
);
tracing::info!("Phase 2: Analyzing {} files...", files.len());
let analyzer = Analyzer::new(®istry, &self.engine);
let findings = analyzer.scan_files(&files);
tracing::info!("Found {} potential secrets", findings.len());
ScanResult {
findings,
files_scanned: files.len(),
constants_indexed: stats.total,
registry_stats: stats,
}
}
pub fn engine(&self) -> &ScoringEngine {
&self.engine
}
pub fn scan_content(&self, path: &std::path::Path, content: &str) -> Vec<Finding> {
let ast = match syn::parse_file(content) {
Ok(a) => a,
Err(_) => return Vec::new(),
};
let registry = SuspectRegistry::new();
let analyzer = Analyzer::new(®istry, &self.engine);
analyzer.scan_ast(path, &ast)
}
}
#[derive(Debug)]
pub struct ScanResult {
pub findings: Vec<Finding>,
pub files_scanned: usize,
pub constants_indexed: usize,
pub registry_stats: crate::registry::RegistryStats,
}
impl ScanResult {
pub fn sorted_by_severity(&self) -> Vec<&Finding> {
let mut findings: Vec<_> = self.findings.iter().collect();
findings.sort_by(|a, b| b.score.total.cmp(&a.score.total));
findings
}
pub fn above_threshold(&self, threshold: i32) -> Vec<&Finding> {
self.findings
.iter()
.filter(|f| f.score.total >= threshold)
.collect()
}
pub fn has_critical(&self) -> bool {
self.findings.iter().any(|f| f.score.total >= 100)
}
pub fn has_high(&self) -> bool {
self.findings.iter().any(|f| f.score.total >= 70)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_result_sorting() {
use crate::models::{FindingId, Score, SourceLocation, SuspectValue};
use std::path::PathBuf;
let findings = vec![
Finding {
id: FindingId::new("a"),
suspect: SuspectValue::Constant {
name: "LOW".into(),
value: "x".into(),
type_annotation: None,
},
location: SourceLocation::new(PathBuf::from("a.rs"), 1, 0),
usage: None,
context: crate::models::AnalysisContext::new(PathBuf::from("a.rs")),
score: Score::from_total(30),
explanation: String::new(),
remediation: None,
metadata: std::collections::HashMap::new(),
},
Finding {
id: FindingId::new("b"),
suspect: SuspectValue::Constant {
name: "HIGH".into(),
value: "y".into(),
type_annotation: None,
},
location: SourceLocation::new(PathBuf::from("b.rs"), 1, 0),
usage: None,
context: crate::models::AnalysisContext::new(PathBuf::from("b.rs")),
score: Score::from_total(90),
explanation: String::new(),
remediation: None,
metadata: std::collections::HashMap::new(),
},
];
let result = ScanResult {
findings,
files_scanned: 2,
constants_indexed: 2,
registry_stats: Default::default(),
};
let sorted = result.sorted_by_severity();
assert_eq!(sorted[0].suspect.name(), Some("HIGH"));
assert_eq!(sorted[1].suspect.name(), Some("LOW"));
}
}