1use crate::{
2 analyzer::display::BoxDrawer,
3 analyzer::security::SecuritySeverity as TurboSecuritySeverity,
4 analyzer::security::turbo::results::SecurityReport,
5 analyzer::security::{ScanMode, TurboConfig, TurboSecurityAnalyzer},
6 cli::{OutputFormat, SecurityScanMode},
7};
8use colored::*;
9use std::path::PathBuf;
10
11pub fn handle_security(
12 path: PathBuf,
13 mode: SecurityScanMode,
14 include_low: bool,
15 no_secrets: bool,
16 no_code_patterns: bool,
17 _no_infrastructure: bool,
18 _no_compliance: bool,
19 _frameworks: Vec<String>,
20 format: OutputFormat,
21 output: Option<PathBuf>,
22 fail_on_findings: bool,
23 quiet: bool,
24) -> crate::Result<String> {
25 let project_path = path.canonicalize().unwrap_or_else(|_| path.clone());
26
27 let mut result_output = String::new();
29
30 if !quiet {
32 println!(
33 "đĄī¸ Running security analysis on: {}",
34 project_path.display()
35 );
36 result_output.push_str(&format!(
37 "đĄī¸ Running security analysis on: {}\n",
38 project_path.display()
39 ));
40 }
41
42 let scan_mode = determine_scan_mode(mode, include_low, no_secrets, no_code_patterns);
44
45 let config = create_turbo_config(scan_mode, fail_on_findings, no_secrets);
47
48 let analyzer = TurboSecurityAnalyzer::new(config).map_err(|e| {
50 crate::error::IaCGeneratorError::Analysis(crate::error::AnalysisError::InvalidStructure(
51 format!("Failed to create turbo security analyzer: {}", e),
52 ))
53 })?;
54
55 let start_time = std::time::Instant::now();
56 let security_report = analyzer.analyze_project(&project_path).map_err(|e| {
57 crate::error::IaCGeneratorError::Analysis(crate::error::AnalysisError::InvalidStructure(
58 format!("Turbo security analysis failed: {}", e),
59 ))
60 })?;
61 let scan_duration = start_time.elapsed();
62
63 if !quiet {
65 println!("⥠Scan completed in {:.2}s", scan_duration.as_secs_f64());
66 result_output.push_str(&format!(
67 "⥠Scan completed in {:.2}s\n",
68 scan_duration.as_secs_f64()
69 ));
70 }
71
72 let output_string = match format {
74 OutputFormat::Table => format_security_table(&security_report, scan_mode, &path),
75 OutputFormat::Json => serde_json::to_string_pretty(&security_report)?,
76 };
77
78 result_output.push_str(&output_string);
80
81 if let Some(output_path) = output {
83 std::fs::write(&output_path, &output_string)?;
84 if !quiet {
85 println!("Security report saved to: {}", output_path.display());
86 }
87 result_output.push_str(&format!(
88 "\nSecurity report saved to: {}\n",
89 output_path.display()
90 ));
91 } else if !quiet {
92 print!("{}", output_string);
93 }
94
95 if fail_on_findings && security_report.total_findings > 0 {
97 handle_exit_codes(&security_report);
98 }
99
100 Ok(result_output)
101}
102
103fn determine_scan_mode(
104 mode: SecurityScanMode,
105 include_low: bool,
106 no_secrets: bool,
107 no_code_patterns: bool,
108) -> ScanMode {
109 if no_secrets && no_code_patterns {
110 ScanMode::Lightning
112 } else if include_low {
113 ScanMode::Paranoid
115 } else {
116 match mode {
118 SecurityScanMode::Lightning => ScanMode::Lightning,
119 SecurityScanMode::Fast => ScanMode::Fast,
120 SecurityScanMode::Balanced => ScanMode::Balanced,
121 SecurityScanMode::Thorough => ScanMode::Thorough,
122 SecurityScanMode::Paranoid => ScanMode::Paranoid,
123 }
124 }
125}
126
127fn create_turbo_config(
128 scan_mode: ScanMode,
129 fail_on_findings: bool,
130 no_secrets: bool,
131) -> TurboConfig {
132 TurboConfig {
133 scan_mode,
134 max_file_size: 10 * 1024 * 1024, worker_threads: 0, use_mmap: true,
137 enable_cache: true,
138 cache_size_mb: 100,
139 max_critical_findings: if fail_on_findings { Some(1) } else { None },
140 timeout_seconds: Some(60),
141 skip_gitignored: true,
142 priority_extensions: vec![
143 "env".to_string(),
144 "key".to_string(),
145 "pem".to_string(),
146 "json".to_string(),
147 "yml".to_string(),
148 "yaml".to_string(),
149 "toml".to_string(),
150 "ini".to_string(),
151 "conf".to_string(),
152 "config".to_string(),
153 "js".to_string(),
154 "ts".to_string(),
155 "py".to_string(),
156 "rs".to_string(),
157 "go".to_string(),
158 ],
159 pattern_sets: if no_secrets {
160 vec![]
161 } else {
162 vec!["default".to_string(), "aws".to_string(), "gcp".to_string()]
163 },
164 }
165}
166
167fn format_security_table(
168 security_report: &SecurityReport,
169 scan_mode: ScanMode,
170 path: &std::path::Path,
171) -> String {
172 let mut output = String::new();
173
174 output.push_str(&format!(
176 "\n{}\n",
177 "đĄī¸ Security Analysis Results".bright_white().bold()
178 ));
179 output.push_str(&format!("{}\n", "â".repeat(80).bright_blue()));
180
181 output.push_str(&format_security_summary_box(security_report, scan_mode));
183
184 if !security_report.findings.is_empty() {
186 output.push_str(&format_security_findings_box(security_report, path));
187 output.push_str(&format_gitignore_legend());
188 } else {
189 output.push_str(&format_no_findings_box(security_report.files_scanned));
190 }
191
192 output.push_str(&format_recommendations_box(security_report));
194
195 output
196}
197
198fn format_security_summary_box(security_report: &SecurityReport, scan_mode: ScanMode) -> String {
199 let mut score_box = BoxDrawer::new("Security Summary");
200 score_box.add_line(
201 "Overall Score:",
202 &format!("{:.0}/100", security_report.overall_score).bright_yellow(),
203 true,
204 );
205 score_box.add_line(
206 "Risk Level:",
207 &format!("{:?}", security_report.risk_level).color(match security_report.risk_level {
208 TurboSecuritySeverity::Critical => "bright_red",
209 TurboSecuritySeverity::High => "red",
210 TurboSecuritySeverity::Medium => "yellow",
211 TurboSecuritySeverity::Low => "green",
212 TurboSecuritySeverity::Info => "blue",
213 }),
214 true,
215 );
216 score_box.add_line(
217 "Total Findings:",
218 &security_report.total_findings.to_string().cyan(),
219 true,
220 );
221 score_box.add_line(
222 "Files Scanned:",
223 &security_report.files_scanned.to_string().green(),
224 true,
225 );
226 score_box.add_line("Scan Mode:", &format!("{:?}", scan_mode).green(), true);
227
228 format!("\n{}\n", score_box.draw())
229}
230
231fn format_security_findings_box(
232 security_report: &SecurityReport,
233 project_path: &std::path::Path,
234) -> String {
235 let terminal_width = if let Some((width, _)) = term_size::dimensions() {
237 width.saturating_sub(10) } else {
239 120 };
241
242 let mut findings_box = BoxDrawer::new("Security Findings");
243
244 for (i, finding) in security_report.findings.iter().enumerate() {
245 let severity_color = match finding.severity {
246 TurboSecuritySeverity::Critical => "bright_red",
247 TurboSecuritySeverity::High => "red",
248 TurboSecuritySeverity::Medium => "yellow",
249 TurboSecuritySeverity::Low => "blue",
250 TurboSecuritySeverity::Info => "green",
251 };
252
253 let file_display = calculate_relative_path(finding.file_path.as_ref(), project_path);
255
256 let gitignore_status = determine_gitignore_status(&finding.description);
258
259 let finding_type = determine_finding_type(&finding.title);
261
262 let position_display = format_position(finding.line_number, finding.column_number);
264
265 format_file_path(&mut findings_box, i + 1, &file_display, terminal_width);
267
268 findings_box.add_value_only(&format!(
269 " {} {} | {} {} | {} {} | {} {}",
270 "Type:".dimmed(),
271 finding_type.yellow(),
272 "Severity:".dimmed(),
273 format!("{:?}", finding.severity)
274 .color(severity_color)
275 .bold(),
276 "Position:".dimmed(),
277 position_display.bright_cyan(),
278 "Status:".dimmed(),
279 gitignore_status
280 ));
281
282 if i < security_report.findings.len() - 1 {
284 findings_box.add_value_only("");
285 }
286 }
287
288 format!("\n{}\n", findings_box.draw())
289}
290
291fn calculate_relative_path(file_path: Option<&PathBuf>, project_path: &std::path::Path) -> String {
292 if let Some(file_path) = file_path {
293 let canonical_file = file_path
295 .canonicalize()
296 .unwrap_or_else(|_| file_path.clone());
297 let canonical_project = project_path
298 .canonicalize()
299 .unwrap_or_else(|_| project_path.to_path_buf());
300
301 if let Ok(relative_path) = canonical_file.strip_prefix(&canonical_project) {
303 let relative_str = relative_path.to_string_lossy().replace('\\', "/");
305 format!("./{}", relative_str)
306 } else {
307 format_fallback_path(file_path, project_path)
309 }
310 } else {
311 "N/A".to_string()
312 }
313}
314
315fn format_fallback_path(file_path: &std::path::Path, project_path: &std::path::Path) -> String {
316 let path_str = file_path.to_string_lossy();
317 if path_str.starts_with('/') {
318 if let Some(project_name) = project_path.file_name().and_then(|n| n.to_str()) {
320 if let Some(project_idx) = path_str.rfind(project_name) {
321 let relative_part = &path_str[project_idx + project_name.len()..];
322 if relative_part.starts_with('/') {
323 format!(".{}", relative_part)
324 } else if !relative_part.is_empty() {
325 format!("./{}", relative_part)
326 } else {
327 format!(
328 "./{}",
329 file_path.file_name().unwrap_or_default().to_string_lossy()
330 )
331 }
332 } else {
333 path_str.to_string()
334 }
335 } else {
336 path_str.to_string()
337 }
338 } else {
339 if path_str.starts_with("./") {
341 path_str.to_string()
342 } else {
343 format!("./{}", path_str)
344 }
345 }
346}
347
348fn determine_gitignore_status(description: &str) -> ColoredString {
349 if description.contains("is tracked by git") {
350 "TRACKED".bright_red().bold()
351 } else if description.contains("is NOT in .gitignore") {
352 "EXPOSED".yellow().bold()
353 } else if description.contains("is protected") || description.contains("properly ignored") {
354 "SAFE".bright_green().bold()
355 } else if description.contains("appears safe") {
356 "OK".bright_blue().bold()
357 } else {
358 "UNKNOWN".dimmed()
359 }
360}
361
362fn determine_finding_type(title: &str) -> &'static str {
363 if title.contains("Environment Variable") {
364 "ENV VAR"
365 } else if title.contains("Secret File") {
366 "SECRET FILE"
367 } else if title.contains("API Key") || title.contains("Stripe") || title.contains("Firebase") {
368 "API KEY"
369 } else if title.contains("Configuration") {
370 "CONFIG"
371 } else {
372 "OTHER"
373 }
374}
375
376fn format_position(line_number: Option<usize>, column_number: Option<usize>) -> String {
377 match (line_number, column_number) {
378 (Some(line), Some(col)) => format!("{}:{}", line, col),
379 (Some(line), None) => format!("{}", line),
380 _ => "â".to_string(),
381 }
382}
383
384fn format_file_path(
385 findings_box: &mut BoxDrawer,
386 index: usize,
387 file_display: &str,
388 terminal_width: usize,
389) {
390 let box_margin = 6; let available_width = terminal_width.saturating_sub(box_margin);
392 let max_path_width = available_width.saturating_sub(20); if file_display.len() + 3 <= max_path_width {
395 findings_box.add_value_only(&format!(
397 "{}. {}",
398 format!("{}", index).bright_white().bold(),
399 file_display.cyan().bold()
400 ));
401 } else if file_display.len() <= available_width.saturating_sub(4) {
402 findings_box.add_value_only(&format!("{}.", format!("{}", index).bright_white().bold()));
404 findings_box.add_value_only(&format!(" {}", file_display.cyan().bold()));
405 } else {
406 format_long_path(findings_box, index, file_display, available_width);
408 }
409}
410
411fn format_long_path(
412 findings_box: &mut BoxDrawer,
413 index: usize,
414 file_display: &str,
415 available_width: usize,
416) {
417 findings_box.add_value_only(&format!("{}.", format!("{}", index).bright_white().bold()));
418
419 let wrap_width = available_width.saturating_sub(4);
421 let mut remaining = file_display;
422 let mut first_line = true;
423
424 while !remaining.is_empty() {
425 let prefix = if first_line { " " } else { " " };
426 let line_width = wrap_width.saturating_sub(prefix.len());
427
428 if remaining.len() <= line_width {
429 findings_box.add_value_only(&format!("{}{}", prefix, remaining.cyan().bold()));
431 break;
432 } else {
433 let chunk = &remaining[..line_width];
435 let break_point = chunk.rfind('/').unwrap_or(line_width.saturating_sub(1));
436
437 findings_box.add_value_only(&format!(
438 "{}{}",
439 prefix,
440 chunk[..break_point].cyan().bold()
441 ));
442 remaining = &remaining[break_point..];
443 if remaining.starts_with('/') {
444 remaining = &remaining[1..]; }
446 }
447 first_line = false;
448 }
449}
450
451fn format_gitignore_legend() -> String {
452 let mut legend_box = BoxDrawer::new("Git Status Legend");
453 legend_box.add_line(
454 &"TRACKED:".bright_red().bold().to_string(),
455 "File is tracked by git - CRITICAL RISK",
456 false,
457 );
458 legend_box.add_line(
459 &"EXPOSED:".yellow().bold().to_string(),
460 "File contains secrets but not in .gitignore",
461 false,
462 );
463 legend_box.add_line(
464 &"SAFE:".bright_green().bold().to_string(),
465 "File is properly ignored by .gitignore",
466 false,
467 );
468 legend_box.add_line(
469 &"OK:".bright_blue().bold().to_string(),
470 "File appears safe for version control",
471 false,
472 );
473 format!("\n{}\n", legend_box.draw())
474}
475
476fn format_no_findings_box(files_scanned: usize) -> String {
477 let mut no_findings_box = BoxDrawer::new("Security Status");
478 if files_scanned == 0 {
479 no_findings_box.add_value_only(&"â ī¸ No files were scanned".yellow());
480 no_findings_box.add_value_only(
481 "This may indicate that all files were filtered out or the scan failed.",
482 );
483 no_findings_box.add_value_only(
484 "đĄ Try running with --mode thorough or --mode paranoid for a deeper scan",
485 );
486 } else {
487 no_findings_box.add_value_only(&"â
No security issues detected".green());
488 no_findings_box.add_value_only("đĄ Regular security scanning recommended");
489 }
490 format!("\n{}\n", no_findings_box.draw())
491}
492
493fn format_recommendations_box(security_report: &SecurityReport) -> String {
494 let mut rec_box = BoxDrawer::new("Key Recommendations");
495 if !security_report.recommendations.is_empty() {
496 for (i, rec) in security_report.recommendations.iter().take(5).enumerate() {
497 let clean_rec = rec.replace(
499 "Add these patterns to your .gitignore:",
500 "Add to .gitignore:",
501 );
502 rec_box.add_value_only(&format!("{}. {}", i + 1, clean_rec));
503 }
504 if security_report.recommendations.len() > 5 {
505 rec_box.add_value_only(
506 &format!(
507 "... and {} more recommendations",
508 security_report.recommendations.len() - 5
509 )
510 .dimmed(),
511 );
512 }
513 } else {
514 rec_box.add_value_only("â
No immediate security concerns detected");
515 rec_box.add_value_only("đĄ Consider implementing dependency scanning");
516 rec_box.add_value_only("đĄ Review environment variable security practices");
517 }
518 format!("\n{}\n", rec_box.draw())
519}
520
521fn handle_exit_codes(security_report: &SecurityReport) -> ! {
522 let critical_count = security_report
523 .findings_by_severity
524 .get(&TurboSecuritySeverity::Critical)
525 .unwrap_or(&0);
526 let high_count = security_report
527 .findings_by_severity
528 .get(&TurboSecuritySeverity::High)
529 .unwrap_or(&0);
530
531 if *critical_count > 0 {
532 eprintln!("â Critical security issues found. Please address immediately.");
533 std::process::exit(1);
534 } else if *high_count > 0 {
535 eprintln!("â ī¸ High severity security issues found. Review recommended.");
536 std::process::exit(2);
537 } else {
538 eprintln!("âšī¸ Security issues found but none are critical or high severity.");
539 std::process::exit(3);
540 }
541}