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());
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
180 let config_files = security_report.findings.iter()
182 .filter_map(|f| f.file_path.as_ref())
183 .collect::<std::collections::HashSet<_>>()
184 .len();
185 score_box.add_line("Files Analyzed:", &config_files.max(1).to_string().green(), true);
186 score_box.add_line("Scan Mode:", &format!("{:?}", scan_mode).green(), true);
187
188 format!("\n{}\n", score_box.draw())
189}
190
191fn format_security_findings_box(
192 security_report: &SecurityReport,
193 project_path: &std::path::Path,
194) -> String {
195 let terminal_width = if let Some((width, _)) = term_size::dimensions() {
197 width.saturating_sub(10) } else {
199 120 };
201
202 let mut findings_box = BoxDrawer::new("Security Findings");
203
204 for (i, finding) in security_report.findings.iter().enumerate() {
205 let severity_color = match finding.severity {
206 TurboSecuritySeverity::Critical => "bright_red",
207 TurboSecuritySeverity::High => "red",
208 TurboSecuritySeverity::Medium => "yellow",
209 TurboSecuritySeverity::Low => "blue",
210 TurboSecuritySeverity::Info => "green",
211 };
212
213 let file_display = calculate_relative_path(finding.file_path.as_ref(), project_path);
215
216 let gitignore_status = determine_gitignore_status(&finding.description);
218
219 let finding_type = determine_finding_type(&finding.title);
221
222 let position_display = format_position(finding.line_number, finding.column_number);
224
225 format_file_path(&mut findings_box, i + 1, &file_display, terminal_width);
227
228 findings_box.add_value_only(&format!(" {} {} | {} {} | {} {} | {} {}",
229 "Type:".dimmed(),
230 finding_type.yellow(),
231 "Severity:".dimmed(),
232 format!("{:?}", finding.severity).color(severity_color).bold(),
233 "Position:".dimmed(),
234 position_display.bright_cyan(),
235 "Status:".dimmed(),
236 gitignore_status
237 ));
238
239 if i < security_report.findings.len() - 1 {
241 findings_box.add_value_only("");
242 }
243 }
244
245 format!("\n{}\n", findings_box.draw())
246}
247
248fn calculate_relative_path(file_path: Option<&PathBuf>, project_path: &std::path::Path) -> String {
249 if let Some(file_path) = file_path {
250 let canonical_file = file_path.canonicalize().unwrap_or_else(|_| file_path.clone());
252 let canonical_project = project_path.canonicalize().unwrap_or_else(|_| project_path.to_path_buf());
253
254 if let Ok(relative_path) = canonical_file.strip_prefix(&canonical_project) {
256 let relative_str = relative_path.to_string_lossy().replace('\\', "/");
258 format!("./{}", relative_str)
259 } else {
260 format_fallback_path(file_path, project_path)
262 }
263 } else {
264 "N/A".to_string()
265 }
266}
267
268fn format_fallback_path(file_path: &PathBuf, project_path: &std::path::Path) -> String {
269 let path_str = file_path.to_string_lossy();
270 if path_str.starts_with('/') {
271 if let Some(project_name) = project_path.file_name().and_then(|n| n.to_str()) {
273 if let Some(project_idx) = path_str.rfind(project_name) {
274 let relative_part = &path_str[project_idx + project_name.len()..];
275 if relative_part.starts_with('/') {
276 format!(".{}", relative_part)
277 } else if !relative_part.is_empty() {
278 format!("./{}", relative_part)
279 } else {
280 format!("./{}", file_path.file_name().unwrap_or_default().to_string_lossy())
281 }
282 } else {
283 path_str.to_string()
284 }
285 } else {
286 path_str.to_string()
287 }
288 } else {
289 if path_str.starts_with("./") {
291 path_str.to_string()
292 } else {
293 format!("./{}", path_str)
294 }
295 }
296}
297
298fn determine_gitignore_status(description: &str) -> ColoredString {
299 if description.contains("is tracked by git") {
300 "TRACKED".bright_red().bold()
301 } else if description.contains("is NOT in .gitignore") {
302 "EXPOSED".yellow().bold()
303 } else if description.contains("is protected") || description.contains("properly ignored") {
304 "SAFE".bright_green().bold()
305 } else if description.contains("appears safe") {
306 "OK".bright_blue().bold()
307 } else {
308 "UNKNOWN".dimmed()
309 }
310}
311
312fn determine_finding_type(title: &str) -> &'static str {
313 if title.contains("Environment Variable") {
314 "ENV VAR"
315 } else if title.contains("Secret File") {
316 "SECRET FILE"
317 } else if title.contains("API Key") || title.contains("Stripe") || title.contains("Firebase") {
318 "API KEY"
319 } else if title.contains("Configuration") {
320 "CONFIG"
321 } else {
322 "OTHER"
323 }
324}
325
326fn format_position(line_number: Option<usize>, column_number: Option<usize>) -> String {
327 match (line_number, column_number) {
328 (Some(line), Some(col)) => format!("{}:{}", line, col),
329 (Some(line), None) => format!("{}", line),
330 _ => "â".to_string(),
331 }
332}
333
334fn format_file_path(findings_box: &mut BoxDrawer, index: usize, file_display: &str, terminal_width: usize) {
335 let box_margin = 6; let available_width = terminal_width.saturating_sub(box_margin);
337 let max_path_width = available_width.saturating_sub(20); if file_display.len() + 3 <= max_path_width {
340 findings_box.add_value_only(&format!("{}. {}",
342 format!("{}", index).bright_white().bold(),
343 file_display.cyan().bold()
344 ));
345 } else if file_display.len() <= available_width.saturating_sub(4) {
346 findings_box.add_value_only(&format!("{}.",
348 format!("{}", index).bright_white().bold()
349 ));
350 findings_box.add_value_only(&format!(" {}",
351 file_display.cyan().bold()
352 ));
353 } else {
354 format_long_path(findings_box, index, file_display, available_width);
356 }
357}
358
359fn format_long_path(findings_box: &mut BoxDrawer, index: usize, file_display: &str, available_width: usize) {
360 findings_box.add_value_only(&format!("{}.",
361 format!("{}", index).bright_white().bold()
362 ));
363
364 let wrap_width = available_width.saturating_sub(4);
366 let mut remaining = file_display;
367 let mut first_line = true;
368
369 while !remaining.is_empty() {
370 let prefix = if first_line { " " } else { " " };
371 let line_width = wrap_width.saturating_sub(prefix.len());
372
373 if remaining.len() <= line_width {
374 findings_box.add_value_only(&format!("{}{}",
376 prefix, remaining.cyan().bold()
377 ));
378 break;
379 } else {
380 let chunk = &remaining[..line_width];
382 let break_point = chunk.rfind('/').unwrap_or(line_width.saturating_sub(1));
383
384 findings_box.add_value_only(&format!("{}{}",
385 prefix, chunk[..break_point].cyan().bold()
386 ));
387 remaining = &remaining[break_point..];
388 if remaining.starts_with('/') {
389 remaining = &remaining[1..]; }
391 }
392 first_line = false;
393 }
394}
395
396fn format_gitignore_legend() -> String {
397 let mut legend_box = BoxDrawer::new("Git Status Legend");
398 legend_box.add_line(&"TRACKED:".bright_red().bold().to_string(), "File is tracked by git - CRITICAL RISK", false);
399 legend_box.add_line(&"EXPOSED:".yellow().bold().to_string(), "File contains secrets but not in .gitignore", false);
400 legend_box.add_line(&"SAFE:".bright_green().bold().to_string(), "File is properly ignored by .gitignore", false);
401 legend_box.add_line(&"OK:".bright_blue().bold().to_string(), "File appears safe for version control", false);
402 format!("\n{}\n", legend_box.draw())
403}
404
405fn format_no_findings_box() -> String {
406 let mut no_findings_box = BoxDrawer::new("Security Status");
407 no_findings_box.add_value_only(&"â
No security issues detected".green());
408 no_findings_box.add_value_only("đĄ Regular security scanning recommended");
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}