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