use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use crossbeam::channel::bounded;
use log::{debug, info, trace};
use rayon::prelude::*;
pub mod cache;
pub mod file_discovery;
pub mod pattern_engine;
pub mod results;
pub mod scanner;
use cache::SecurityCache;
use file_discovery::{DiscoveryConfig, FileDiscovery, FileMetadata};
use pattern_engine::PatternEngine;
use results::{ResultAggregator, SecurityReport};
use scanner::{FileScanner, ScanResult, ScanTask};
use crate::analyzer::security::SecurityFinding;
#[derive(Debug, Clone)]
pub struct TurboConfig {
pub scan_mode: ScanMode,
pub max_file_size: usize,
pub worker_threads: usize,
pub use_mmap: bool,
pub enable_cache: bool,
pub cache_size_mb: usize,
pub max_critical_findings: Option<usize>,
pub timeout_seconds: Option<u64>,
pub skip_gitignored: bool,
pub priority_extensions: Vec<String>,
pub pattern_sets: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanMode {
Lightning,
Fast,
Balanced,
Thorough,
Paranoid,
}
impl Default for TurboConfig {
fn default() -> Self {
Self {
scan_mode: ScanMode::Balanced,
max_file_size: 10 * 1024 * 1024, worker_threads: 0, use_mmap: true,
enable_cache: true,
cache_size_mb: 100,
max_critical_findings: None,
timeout_seconds: None,
skip_gitignored: true,
priority_extensions: vec![
"env".to_string(),
"key".to_string(),
"pem".to_string(),
"json".to_string(),
"yml".to_string(),
"yaml".to_string(),
"toml".to_string(),
"ini".to_string(),
"conf".to_string(),
"config".to_string(),
],
pattern_sets: vec!["default".to_string()],
}
}
}
pub struct TurboSecurityAnalyzer {
config: TurboConfig,
pattern_engine: Arc<PatternEngine>,
cache: Arc<SecurityCache>,
file_discovery: Arc<FileDiscovery>,
}
impl TurboSecurityAnalyzer {
pub fn new(config: TurboConfig) -> Result<Self, SecurityError> {
let start = Instant::now();
let pattern_engine = Arc::new(PatternEngine::new(&config)?);
info!(
"Pattern engine initialized with {} patterns in {:?}",
pattern_engine.pattern_count(),
start.elapsed()
);
let cache = Arc::new(SecurityCache::new(config.cache_size_mb));
let discovery_config = DiscoveryConfig {
use_git: config.skip_gitignored,
max_file_size: config.max_file_size,
priority_extensions: config.priority_extensions.clone(),
scan_mode: config.scan_mode,
};
let file_discovery = Arc::new(FileDiscovery::new(discovery_config));
Ok(Self {
config,
pattern_engine,
cache,
file_discovery,
})
}
pub fn analyze_project(&self, project_root: &Path) -> Result<SecurityReport, SecurityError> {
let start = Instant::now();
info!(
"🚀 Starting turbo security analysis for: {}",
project_root.display()
);
let discovery_start = Instant::now();
let files = self.file_discovery.discover_files(project_root)?;
info!(
"📁 Discovered {} files in {:?}",
files.len(),
discovery_start.elapsed()
);
if files.is_empty() {
return Ok(SecurityReport::empty());
}
let filtered_files = self.filter_and_prioritize_files(files);
info!(
"🎯 Filtered to {} high-priority files",
filtered_files.len()
);
let scan_start = Instant::now();
let (findings, files_scanned) = self.parallel_scan(filtered_files)?;
info!(
"🔍 Scanned files in {:?}, found {} findings",
scan_start.elapsed(),
findings.len()
);
let report = ResultAggregator::aggregate(findings, start.elapsed(), files_scanned);
info!("✅ Turbo analysis completed in {:?}", start.elapsed());
Ok(report)
}
fn filter_and_prioritize_files(&self, files: Vec<FileMetadata>) -> Vec<FileMetadata> {
use ScanMode::*;
let mut filtered: Vec<FileMetadata> = match self.config.scan_mode {
Lightning => {
files.into_iter()
.filter(|f| f.is_critical())
.take(100) .collect()
}
Fast => {
let (priority, others): (Vec<_>, Vec<_>) =
files.into_iter().partition(|f| f.is_priority());
let mut result = priority;
let sample_size = others.len() / 5;
result.extend(others.into_iter().take(sample_size));
result
}
Balanced => {
let (priority, others): (Vec<_>, Vec<_>) =
files.into_iter().partition(|f| f.is_priority());
let mut result = priority;
let sample_size = others.len() / 2;
result.extend(others.into_iter().take(sample_size));
result
}
Thorough => {
files
.into_iter()
.filter(|f| f.size < self.config.max_file_size)
.collect()
}
Paranoid => {
files
}
};
filtered.par_sort_by_key(|f| std::cmp::Reverse(f.priority_score()));
filtered
}
fn parallel_scan(
&self,
files: Vec<FileMetadata>,
) -> Result<(Vec<SecurityFinding>, usize), SecurityError> {
let thread_count = if self.config.worker_threads == 0 {
num_cpus::get()
} else {
self.config.worker_threads
};
let (task_sender, task_receiver) = bounded::<ScanTask>(thread_count * 10);
let (result_sender, result_receiver) = bounded::<ScanResult>(thread_count * 10);
let critical_count = Arc::new(parking_lot::Mutex::new(0));
let should_terminate = Arc::new(parking_lot::RwLock::new(false));
let scanner_handles: Vec<_> = (0..thread_count)
.map(|thread_id| {
let scanner = FileScanner::new(
thread_id,
Arc::clone(&self.pattern_engine),
Arc::clone(&self.cache),
self.config.use_mmap,
);
let task_receiver = task_receiver.clone();
let result_sender = result_sender.clone();
let critical_count = Arc::clone(&critical_count);
let should_terminate = Arc::clone(&should_terminate);
let max_critical = self.config.max_critical_findings;
std::thread::spawn(move || {
scanner.run(
task_receiver,
result_sender,
critical_count,
should_terminate,
max_critical,
)
})
})
.collect();
drop(task_receiver);
let task_sender_thread = {
let task_sender = task_sender.clone();
let should_terminate = Arc::clone(&should_terminate);
std::thread::spawn(move || {
for (idx, file) in files.into_iter().enumerate() {
if *should_terminate.read() {
debug!("Early termination triggered, stopping task distribution");
break;
}
let task = ScanTask {
id: idx,
file,
quick_reject: idx > 1000, };
if task_sender.send(task).is_err() {
break; }
}
})
};
drop(task_sender);
drop(result_sender);
let mut all_findings = Vec::new();
let mut files_scanned = 0;
let mut files_skipped = 0;
while let Ok(result) = result_receiver.recv() {
match result {
ScanResult::Findings(findings) => {
all_findings.extend(findings);
files_scanned += 1;
}
ScanResult::Skipped => {
files_skipped += 1;
}
ScanResult::Error(err) => {
debug!("Scan error: {}", err);
}
}
if (files_scanned + files_skipped) % 100 == 0 {
trace!(
"Progress: {} scanned, {} skipped",
files_scanned, files_skipped
);
}
}
task_sender_thread.join().unwrap();
for handle in scanner_handles {
handle.join().unwrap();
}
info!(
"Scan complete: {} files scanned, {} skipped, {} findings",
files_scanned,
files_skipped,
all_findings.len()
);
Ok((all_findings, files_scanned))
}
}
#[derive(Debug, thiserror::Error)]
pub enum SecurityError {
#[error("Pattern engine error: {0}")]
PatternEngine(String),
#[error("File discovery error: {0}")]
FileDiscovery(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Cache error: {0}")]
Cache(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_turbo_analyzer_creation() {
let config = TurboConfig::default();
let analyzer = TurboSecurityAnalyzer::new(config);
assert!(analyzer.is_ok());
}
#[test]
#[ignore] fn test_scan_modes() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join(".env"), "API_KEY=secret123").unwrap();
fs::write(temp_dir.path().join("config.json"), r#"{"key": "value"}"#).unwrap();
fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();
let mut config = TurboConfig::default();
config.scan_mode = ScanMode::Lightning;
let analyzer = TurboSecurityAnalyzer::new(config).unwrap();
let report = analyzer.analyze_project(temp_dir.path()).unwrap();
assert!(report.total_findings > 0);
}
}