syncable_cli/analyzer/security/turbo/
mod.rs1use std::path::Path;
7use std::sync::Arc;
8use std::time::Instant;
9
10use crossbeam::channel::bounded;
11
12use log::{debug, info, trace};
13use rayon::prelude::*;
14
15pub mod cache;
16pub mod file_discovery;
17pub mod pattern_engine;
18pub mod results;
19pub mod scanner;
20
21use cache::SecurityCache;
22use file_discovery::{DiscoveryConfig, FileDiscovery, FileMetadata};
23use pattern_engine::PatternEngine;
24use results::{ResultAggregator, SecurityReport};
25use scanner::{FileScanner, ScanResult, ScanTask};
26
27use crate::analyzer::security::SecurityFinding;
28
29#[derive(Debug, Clone)]
31pub struct TurboConfig {
32 pub scan_mode: ScanMode,
34
35 pub max_file_size: usize,
37
38 pub worker_threads: usize,
40
41 pub use_mmap: bool,
43
44 pub enable_cache: bool,
46 pub cache_size_mb: usize,
47
48 pub max_critical_findings: Option<usize>,
50 pub timeout_seconds: Option<u64>,
51
52 pub skip_gitignored: bool,
54 pub priority_extensions: Vec<String>,
55
56 pub pattern_sets: Vec<String>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ScanMode {
63 Lightning,
65
66 Fast,
68
69 Balanced,
71
72 Thorough,
74
75 Paranoid,
77}
78
79impl Default for TurboConfig {
80 fn default() -> Self {
81 Self {
82 scan_mode: ScanMode::Balanced,
83 max_file_size: 10 * 1024 * 1024, worker_threads: 0, use_mmap: true,
86 enable_cache: true,
87 cache_size_mb: 100,
88 max_critical_findings: None,
89 timeout_seconds: None,
90 skip_gitignored: true,
91 priority_extensions: vec![
92 "env".to_string(),
93 "key".to_string(),
94 "pem".to_string(),
95 "json".to_string(),
96 "yml".to_string(),
97 "yaml".to_string(),
98 "toml".to_string(),
99 "ini".to_string(),
100 "conf".to_string(),
101 "config".to_string(),
102 ],
103 pattern_sets: vec!["default".to_string()],
104 }
105 }
106}
107
108pub struct TurboSecurityAnalyzer {
110 config: TurboConfig,
111 pattern_engine: Arc<PatternEngine>,
112 cache: Arc<SecurityCache>,
113 file_discovery: Arc<FileDiscovery>,
114}
115
116impl TurboSecurityAnalyzer {
117 pub fn new(config: TurboConfig) -> Result<Self, SecurityError> {
119 let start = Instant::now();
120
121 let pattern_engine = Arc::new(PatternEngine::new(&config)?);
123 info!(
124 "Pattern engine initialized with {} patterns in {:?}",
125 pattern_engine.pattern_count(),
126 start.elapsed()
127 );
128
129 let cache = Arc::new(SecurityCache::new(config.cache_size_mb));
131
132 let discovery_config = DiscoveryConfig {
134 use_git: config.skip_gitignored,
135 max_file_size: config.max_file_size,
136 priority_extensions: config.priority_extensions.clone(),
137 scan_mode: config.scan_mode,
138 };
139 let file_discovery = Arc::new(FileDiscovery::new(discovery_config));
140
141 Ok(Self {
142 config,
143 pattern_engine,
144 cache,
145 file_discovery,
146 })
147 }
148
149 pub fn analyze_project(&self, project_root: &Path) -> Result<SecurityReport, SecurityError> {
151 let start = Instant::now();
152 info!(
153 "🚀 Starting turbo security analysis for: {}",
154 project_root.display()
155 );
156
157 let discovery_start = Instant::now();
159 let files = self.file_discovery.discover_files(project_root)?;
160 info!(
161 "📁 Discovered {} files in {:?}",
162 files.len(),
163 discovery_start.elapsed()
164 );
165
166 if files.is_empty() {
168 return Ok(SecurityReport::empty());
169 }
170
171 let filtered_files = self.filter_and_prioritize_files(files);
173 info!(
174 "🎯 Filtered to {} high-priority files",
175 filtered_files.len()
176 );
177
178 let scan_start = Instant::now();
180 let (findings, files_scanned) = self.parallel_scan(filtered_files)?;
181 info!(
182 "🔍 Scanned files in {:?}, found {} findings",
183 scan_start.elapsed(),
184 findings.len()
185 );
186
187 let report = ResultAggregator::aggregate(findings, start.elapsed(), files_scanned);
189
190 info!("✅ Turbo analysis completed in {:?}", start.elapsed());
191 Ok(report)
192 }
193
194 fn filter_and_prioritize_files(&self, files: Vec<FileMetadata>) -> Vec<FileMetadata> {
196 use ScanMode::*;
197
198 let mut filtered: Vec<FileMetadata> = match self.config.scan_mode {
199 Lightning => {
200 files.into_iter()
202 .filter(|f| f.is_critical())
203 .take(100) .collect()
205 }
206 Fast => {
207 let (priority, others): (Vec<_>, Vec<_>) =
209 files.into_iter().partition(|f| f.is_priority());
210
211 let mut result = priority;
212 let sample_size = others.len() / 5;
214 result.extend(others.into_iter().take(sample_size));
215 result
216 }
217 Balanced => {
218 let (priority, others): (Vec<_>, Vec<_>) =
220 files.into_iter().partition(|f| f.is_priority());
221
222 let mut result = priority;
223 let sample_size = others.len() / 2;
224 result.extend(others.into_iter().take(sample_size));
225 result
226 }
227 Thorough => {
228 files
230 .into_iter()
231 .filter(|f| f.size < self.config.max_file_size)
232 .collect()
233 }
234 Paranoid => {
235 files
237 }
238 };
239
240 filtered.par_sort_by_key(|f| std::cmp::Reverse(f.priority_score()));
242 filtered
243 }
244
245 fn parallel_scan(
247 &self,
248 files: Vec<FileMetadata>,
249 ) -> Result<(Vec<SecurityFinding>, usize), SecurityError> {
250 let thread_count = if self.config.worker_threads == 0 {
251 num_cpus::get()
252 } else {
253 self.config.worker_threads
254 };
255
256 let (task_sender, task_receiver) = bounded::<ScanTask>(thread_count * 10);
258 let (result_sender, result_receiver) = bounded::<ScanResult>(thread_count * 10);
259
260 let critical_count = Arc::new(parking_lot::Mutex::new(0));
262 let should_terminate = Arc::new(parking_lot::RwLock::new(false));
263
264 let scanner_handles: Vec<_> = (0..thread_count)
266 .map(|thread_id| {
267 let scanner = FileScanner::new(
268 thread_id,
269 Arc::clone(&self.pattern_engine),
270 Arc::clone(&self.cache),
271 self.config.use_mmap,
272 );
273
274 let task_receiver = task_receiver.clone();
275 let result_sender = result_sender.clone();
276 let critical_count = Arc::clone(&critical_count);
277 let should_terminate = Arc::clone(&should_terminate);
278 let max_critical = self.config.max_critical_findings;
279
280 std::thread::spawn(move || {
281 scanner.run(
282 task_receiver,
283 result_sender,
284 critical_count,
285 should_terminate,
286 max_critical,
287 )
288 })
289 })
290 .collect();
291
292 drop(task_receiver);
294
295 let task_sender_thread = {
297 let task_sender = task_sender.clone();
298 let should_terminate = Arc::clone(&should_terminate);
299
300 std::thread::spawn(move || {
301 for (idx, file) in files.into_iter().enumerate() {
302 if *should_terminate.read() {
304 debug!("Early termination triggered, stopping task distribution");
305 break;
306 }
307
308 let task = ScanTask {
309 id: idx,
310 file,
311 quick_reject: idx > 1000, };
313
314 if task_sender.send(task).is_err() {
315 break; }
317 }
318 })
319 };
320
321 drop(task_sender);
323 drop(result_sender);
324
325 let mut all_findings = Vec::new();
327 let mut files_scanned = 0;
328 let mut files_skipped = 0;
329
330 while let Ok(result) = result_receiver.recv() {
331 match result {
332 ScanResult::Findings(findings) => {
333 all_findings.extend(findings);
334 files_scanned += 1;
335 }
336 ScanResult::Skipped => {
337 files_skipped += 1;
338 }
339 ScanResult::Error(err) => {
340 debug!("Scan error: {}", err);
341 }
342 }
343
344 if (files_scanned + files_skipped) % 100 == 0 {
346 trace!(
347 "Progress: {} scanned, {} skipped",
348 files_scanned, files_skipped
349 );
350 }
351 }
352
353 task_sender_thread.join().unwrap();
355 for handle in scanner_handles {
356 handle.join().unwrap();
357 }
358
359 info!(
360 "Scan complete: {} files scanned, {} skipped, {} findings",
361 files_scanned,
362 files_skipped,
363 all_findings.len()
364 );
365
366 Ok((all_findings, files_scanned))
367 }
368}
369
370#[derive(Debug, thiserror::Error)]
371pub enum SecurityError {
372 #[error("Pattern engine error: {0}")]
373 PatternEngine(String),
374
375 #[error("File discovery error: {0}")]
376 FileDiscovery(String),
377
378 #[error("IO error: {0}")]
379 Io(#[from] std::io::Error),
380
381 #[error("Cache error: {0}")]
382 Cache(String),
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use std::fs;
389 use tempfile::TempDir;
390
391 #[test]
392 fn test_turbo_analyzer_creation() {
393 let config = TurboConfig::default();
394 let analyzer = TurboSecurityAnalyzer::new(config);
395 assert!(analyzer.is_ok());
396 }
397
398 #[test]
399 #[ignore] fn test_scan_modes() {
401 let temp_dir = TempDir::new().unwrap();
402
403 fs::write(temp_dir.path().join(".env"), "API_KEY=secret123").unwrap();
405 fs::write(temp_dir.path().join("config.json"), r#"{"key": "value"}"#).unwrap();
406 fs::write(temp_dir.path().join("main.rs"), "fn main() {}").unwrap();
407
408 let mut config = TurboConfig::default();
410 config.scan_mode = ScanMode::Lightning;
411
412 let analyzer = TurboSecurityAnalyzer::new(config).unwrap();
413 let report = analyzer.analyze_project(temp_dir.path()).unwrap();
414
415 assert!(report.total_findings > 0);
417 }
418}