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