1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::fs;
6use std::collections::HashMap;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9#[derive(Debug, Clone)]
10pub struct SecretScannerTool;
11#[derive(Debug, Clone, Serialize, Deserialize)]
12struct SecretScanReport {
13 files_scanned: usize,
14 secrets_found: usize,
15 findings: Vec<SecretFinding>,
16 false_positives: Vec<FalsePositive>,
17 statistics: ScanStatistics,
18 recommendations: Vec<String>,
19 timestamp: String,
20}
21#[derive(Debug, Clone, Serialize, Deserialize)]
22struct SecretFinding {
23 file_path: String,
24 line_number: usize,
25 secret_type: String,
26 confidence: String,
27 context: String,
28 secret_value: String,
29 masked_secret: String,
30 severity: String,
31 recommendation: String,
32}
33#[derive(Debug, Clone, Serialize, Deserialize)]
34struct FalsePositive {
35 file_path: String,
36 line_number: usize,
37 pattern_matched: String,
38 reason: String,
39}
40#[derive(Debug, Clone, Serialize, Deserialize)]
41struct ScanStatistics {
42 high_confidence: usize,
43 medium_confidence: usize,
44 low_confidence: usize,
45 false_positives: usize,
46 files_with_secrets: usize,
47 most_common_type: String,
48}
49impl SecretScannerTool {
50 pub fn new() -> Self {
51 Self
52 }
53 fn get_secret_patterns(&self) -> Vec<SecretPattern> {
54 vec![
55 SecretPattern { name : "AWS Access Key".to_string(), pattern :
56 r"AKIA[0-9A-Z]{16}".to_string(), confidence : "high".to_string(), description
57 : "AWS Access Key ID".to_string(), example : "AKIAIOSFODNN7EXAMPLE"
58 .to_string(), }, SecretPattern { name : "AWS Secret Key".to_string(), pattern
59 : r"[0-9a-zA-Z/+]{40}".to_string(), confidence : "medium".to_string(),
60 description : "AWS Secret Access Key".to_string(), example :
61 "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(), }, SecretPattern {
62 name : "GitHub Token".to_string(), pattern : r"ghp_[0-9a-zA-Z]{36}"
63 .to_string(), confidence : "high".to_string(), description :
64 "GitHub Personal Access Token".to_string(), example :
65 "ghp_1234567890abcdef1234567890abcdef1234".to_string(), }, SecretPattern {
66 name : "GitHub OAuth".to_string(), pattern : r"gho_[0-9a-zA-Z]{36}"
67 .to_string(), confidence : "high".to_string(), description :
68 "GitHub OAuth Access Token".to_string(), example :
69 "gho_1234567890abcdef1234567890abcdef1234".to_string(), }, SecretPattern {
70 name : "GitLab Token".to_string(), pattern : r"glpat-[0-9a-zA-Z\-_]{20}"
71 .to_string(), confidence : "high".to_string(), description :
72 "GitLab Personal Access Token".to_string(), example :
73 "glpat-1234567890abcdefghij".to_string(), }, SecretPattern { name :
74 "Slack Token".to_string(), pattern : r"xox[baprs]-[0-9a-zA-Z]{10,48}"
75 .to_string(), confidence : "high".to_string(), description :
76 "Slack API Token".to_string(), example :
77 "xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx".to_string(), },
78 SecretPattern { name : "Discord Token".to_string(), pattern :
79 r"[MN][A-Za-z\d]{23}\.[\w-]{6}\.[\w-]{27}".to_string(), confidence : "high"
80 .to_string(), description : "Discord Bot Token".to_string(), example :
81 "MTAwMDAwMDAwMDAwMDAwMDAw.GQ0wHm.example".to_string(), }, SecretPattern {
82 name : "Stripe Key".to_string(), pattern : r"[rs]k_live_[0-9a-zA-Z]{24}"
83 .to_string(), confidence : "high".to_string(), description : "Stripe API Key"
84 .to_string(), example : "sk_live_1234567890abcdefghijklmnopqrstuvwxyz"
85 .to_string(), }, SecretPattern { name : "Database URL".to_string(), pattern :
86 r"(?i)postgres://[^:\s]+:[^@\s]+@".to_string(), confidence : "high"
87 .to_string(), description : "PostgreSQL connection string".to_string(),
88 example : "postgres://username:password@host:port/database".to_string(), },
89 SecretPattern { name : "Generic API Key".to_string(), pattern :
90 r#"(?i)(api[_-]?key|apikey|secret|token|auth[_-]?token)\s*[:=]\s*['"]?.*['"]?"#
91 .to_string(), confidence : "medium".to_string(), description :
92 "Generic API key or token".to_string(), example :
93 "api_key=1234567890abcdef1234567890abcdef".to_string(), }, SecretPattern {
94 name : "Private Key".to_string(), pattern :
95 r"-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----".to_string(), confidence :
96 "high".to_string(), description : "RSA Private Key".to_string(), example :
97 "-----BEGIN PRIVATE KEY-----".to_string(), }, SecretPattern { name :
98 "SSH Private Key".to_string(), pattern :
99 r"-----BEGIN\s+OPENSSH\s+PRIVATE\s+KEY-----".to_string(), confidence : "high"
100 .to_string(), description : "SSH Private Key".to_string(), example :
101 "-----BEGIN OPENSSH PRIVATE KEY-----".to_string(), }, SecretPattern { name :
102 "JWT Token".to_string(), pattern :
103 r#"eyJ[A-Za-z0-9_.-]*\.[A-Za-z0-9_.-]*\.[A-Za-z0-9_.-]*"#.to_string(),
104 confidence : "medium".to_string(), description : "JSON Web Token"
105 .to_string(), example :
106 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
107 .to_string(), }, SecretPattern { name : "Password in Config".to_string(),
108 pattern : r#"(?i)(password|passwd|pwd)\s*[:=]\s*['"]?.*['"]?"#.to_string(),
109 confidence : "low".to_string(), description :
110 "Potential password in configuration".to_string(), example :
111 "password=secret123".to_string(), }, SecretPattern { name : "Bearer Token"
112 .to_string(), pattern : r#"(?i)bearer\s+[^'"\s]+"#.to_string(), confidence :
113 "medium".to_string(), description : "Bearer token in Authorization header"
114 .to_string(), example : "Authorization: Bearer abcdef1234567890".to_string(),
115 },
116 ]
117 }
118 fn scan_file_for_secrets(&self, file_path: &str) -> Result<Vec<SecretFinding>> {
119 let content = fs::read_to_string(file_path)?;
120 let mut findings = Vec::new();
121 let file_name = Path::new(file_path)
122 .file_name()
123 .unwrap_or_default()
124 .to_string_lossy();
125 if file_name.ends_with(".min.js") || file_name.ends_with(".min.css")
126 || file_name.contains("jquery") || file_name.contains("bootstrap")
127 {
128 return Ok(vec![]);
129 }
130 let patterns = self.get_secret_patterns();
131 for (line_number, line) in content.lines().enumerate() {
132 for pattern in &patterns {
133 if let Ok(regex) = Regex::new(&pattern.pattern) {
134 if let Some(captures) = regex.captures(line) {
135 if let Some(secret_match) = captures
136 .get(1)
137 .or_else(|| captures.get(0))
138 {
139 let secret_value = secret_match.as_str().to_string();
140 if self.is_false_positive(&secret_value, pattern, line) {
141 continue;
142 }
143 let masked_secret = self.mask_secret(&secret_value);
144 let context = self.extract_context(&content, line_number, 2);
145 let recommendation = self
146 .generate_recommendation(pattern, file_path);
147 findings
148 .push(SecretFinding {
149 file_path: file_path.to_string(),
150 line_number: line_number + 1,
151 secret_type: pattern.name.clone(),
152 confidence: pattern.confidence.clone(),
153 context,
154 secret_value: secret_value.clone(),
155 masked_secret,
156 severity: self
157 .calculate_severity(&pattern.confidence, &secret_value),
158 recommendation,
159 });
160 }
161 }
162 }
163 }
164 }
165 Ok(findings)
166 }
167 fn is_false_positive(
168 &self,
169 secret: &str,
170 pattern: &SecretPattern,
171 line: &str,
172 ) -> bool {
173 let false_positive_patterns = vec![
174 "example", "test", "demo", "sample", "placeholder", "your", "fake", "dummy",
175 "mock", "xxx", "1234567890",
176 ];
177 let lower_secret = secret.to_lowercase();
178 let lower_line = line.to_lowercase();
179 if lower_line.contains("//")
180 && lower_line.find("//").unwrap()
181 < lower_line.find(&lower_secret).unwrap_or(0)
182 {
183 return true;
184 }
185 for pattern in false_positive_patterns {
186 if lower_secret.contains(pattern) {
187 return true;
188 }
189 }
190 if secret.len() < 10 && secret.chars().all(|c| c.is_alphabetic()) {
191 return true;
192 }
193 false
194 }
195 fn mask_secret(&self, secret: &str) -> String {
196 if secret.len() <= 4 {
197 return "*".repeat(secret.len());
198 }
199 let visible_chars = 2;
200 let masked_chars = secret.len() - (visible_chars * 2);
201 format!(
202 "{}{}{}", & secret[0..visible_chars], "*".repeat(masked_chars), &
203 secret[secret.len() - visible_chars..]
204 )
205 }
206 fn extract_context(
207 &self,
208 content: &str,
209 line_number: usize,
210 context_lines: usize,
211 ) -> String {
212 let lines: Vec<&str> = content.lines().collect();
213 let start = line_number.saturating_sub(context_lines);
214 let end = std::cmp::min(line_number + context_lines + 1, lines.len());
215 lines[start..end].join("\n")
216 }
217 fn calculate_severity(&self, confidence: &str, secret: &str) -> String {
218 match confidence {
219 "high" => {
220 if secret.len() > 20 {
221 "critical".to_string()
222 } else {
223 "high".to_string()
224 }
225 }
226 "medium" => {
227 if secret.len() > 15 { "high".to_string() } else { "medium".to_string() }
228 }
229 "low" => {
230 if secret.len() > 20 { "medium".to_string() } else { "low".to_string() }
231 }
232 _ => "medium".to_string(),
233 }
234 }
235 fn generate_recommendation(
236 &self,
237 pattern: &SecretPattern,
238 file_path: &str,
239 ) -> String {
240 let base_recommendation = match pattern.name.as_str() {
241 "AWS Access Key" | "AWS Secret Key" => {
242 "Move to AWS IAM roles or use environment variables with proper access control"
243 }
244 "GitHub Token" | "GitHub OAuth" => {
245 "Use GitHub Actions secrets or environment variables"
246 }
247 "GitLab Token" => "Use GitLab CI/CD variables or environment variables",
248 "Slack Token" => "Use Slack app configuration or environment variables",
249 "Discord Token" => "Use environment variables or secure configuration files",
250 "Stripe Key" => "Use environment variables and never commit live keys",
251 "Database URL" => {
252 "Use environment variables or connection configuration files"
253 }
254 "Generic API Key" => {
255 "Use environment variables or secure configuration management"
256 }
257 "Private Key" | "SSH Private Key" => "Store in secure key management system",
258 "JWT Token" => "Use environment variables and proper token rotation",
259 "Password in Config" => {
260 "Use secure password hashing and environment variables"
261 }
262 "Bearer Token" => "Use environment variables and secure token storage",
263 _ => "Use environment variables or secure configuration management",
264 };
265 format!(
266 "{} (found in {})", base_recommendation, file_path.split('/').last()
267 .unwrap_or(file_path)
268 )
269 }
270 fn find_files_to_scan(&self, directory: &str) -> Result<Vec<String>> {
271 let mut files = Vec::new();
272 self.find_files_recursive(directory, &mut files)?;
273 Ok(files)
274 }
275 fn find_files_recursive(&self, dir: &str, files: &mut Vec<String>) -> Result<()> {
276 let path = Path::new(dir);
277 if !path.exists() {
278 return Ok(());
279 }
280 for entry in fs::read_dir(path)? {
281 let entry = entry?;
282 let path = entry.path();
283 if path.is_dir() {
284 let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
285 if !matches!(
286 dir_name.as_ref(), "target" | ".git" | "node_modules" | ".cargo" |
287 ".vscode" | ".idea" | ".DS_Store" | "dist" | "build" | "out"
288 ) {
289 self.find_files_recursive(&path.to_string_lossy(), files)?;
290 }
291 } else if let Some(ext) = path.extension() {
292 match ext.to_string_lossy().as_ref() {
293 "rs" | "toml" | "json" | "yaml" | "yml" | "env" | "config" | "ini"
294 | "cfg" | "properties" | "sh" | "bash" | "py" | "js" | "ts" | "php"
295 | "rb" | "go" | "java" | "kt" | "scala" => {
296 files.push(path.to_string_lossy().to_string());
297 }
298 _ => {}
299 }
300 } else if let Some(file_name) = path.file_name() {
301 let file_name_str = file_name.to_string_lossy();
302 if matches!(
303 file_name_str.as_ref(), ".env" | ".env.local" | ".env.production" |
304 "secrets" | "config" | "settings" | "credentials"
305 ) {
306 files.push(path.to_string_lossy().to_string());
307 }
308 }
309 }
310 Ok(())
311 }
312 fn calculate_statistics(&self, findings: &[SecretFinding]) -> ScanStatistics {
313 let mut high_confidence = 0;
314 let mut medium_confidence = 0;
315 let mut low_confidence = 0;
316 let mut files_with_secrets = std::collections::HashSet::new();
317 let mut type_counts = std::collections::HashMap::new();
318 for finding in findings {
319 files_with_secrets.insert(&finding.file_path);
320 match finding.confidence.as_str() {
321 "high" => high_confidence += 1,
322 "medium" => medium_confidence += 1,
323 "low" => low_confidence += 1,
324 _ => {}
325 }
326 *type_counts.entry(&finding.secret_type).or_insert(0) += 1;
327 }
328 let most_common_type = type_counts
329 .into_iter()
330 .max_by_key(|&(_, count)| count)
331 .map(|(ty, _)| ty.clone())
332 .unwrap_or_else(|| "None".to_string());
333 ScanStatistics {
334 high_confidence,
335 medium_confidence,
336 low_confidence,
337 false_positives: 0,
338 files_with_secrets: files_with_secrets.len(),
339 most_common_type,
340 }
341 }
342 fn generate_recommendations(&self, findings: &[SecretFinding]) -> Vec<String> {
343 let mut recommendations = Vec::new();
344 if !findings.is_empty() {
345 recommendations
346 .push(
347 "Use environment variables instead of hardcoding secrets".to_string(),
348 );
349 recommendations
350 .push(
351 "Store secrets in secure configuration management systems"
352 .to_string(),
353 );
354 recommendations
355 .push(
356 "Use .env files for local development (add to .gitignore)"
357 .to_string(),
358 );
359 recommendations
360 .push(
361 "Implement pre-commit hooks to prevent secret commits".to_string(),
362 );
363 recommendations.push("Rotate exposed secrets immediately".to_string());
364 recommendations.push("Use secret scanning in CI/CD pipelines".to_string());
365 }
366 let has_high_severity = findings.iter().any(|f| f.severity == "critical");
367 if has_high_severity {
368 recommendations
369 .insert(
370 0,
371 "šØ CRITICAL: High-severity secrets found - rotate immediately!"
372 .to_string(),
373 );
374 }
375 recommendations
376 }
377 fn display_report(
378 &self,
379 report: &SecretScanReport,
380 output_format: OutputFormat,
381 verbose: bool,
382 ) {
383 match output_format {
384 OutputFormat::Human => {
385 println!(
386 "\nš {} - Secret Scanner Report", "CargoMate SecretScanner".bold()
387 .blue()
388 );
389 println!("{}", "ā".repeat(60).blue());
390 println!("\nš Summary:");
391 println!(" ⢠Files Scanned: {}", report.files_scanned);
392 println!(" ⢠Secrets Found: {}", report.secrets_found);
393 println!(
394 " ⢠Files with Secrets: {}", report.statistics.files_with_secrets
395 );
396 println!(" ⢠High Confidence: {}", report.statistics.high_confidence);
397 println!(
398 " ⢠Medium Confidence: {}", report.statistics.medium_confidence
399 );
400 println!(" ⢠Low Confidence: {}", report.statistics.low_confidence);
401 println!(
402 " ⢠Most Common Type: {}", report.statistics.most_common_type
403 );
404 if !report.findings.is_empty() {
405 println!("\nšØ Secrets Found:");
406 for finding in &report.findings {
407 let severity_icon = match finding.severity.as_str() {
408 "critical" => "šØ",
409 "high" => "ā",
410 "medium" => "ā ļø",
411 "low" => "ā¹ļø",
412 _ => "ā¢",
413 };
414 let confidence_icon = match finding.confidence.as_str() {
415 "high" => "šÆ",
416 "medium" => "š",
417 "low" => "ā",
418 _ => "ā¢",
419 };
420 println!(
421 " {} {} {} - {} (Line {})", severity_icon, confidence_icon,
422 finding.secret_type.red(), finding.file_path.split('/')
423 .last().unwrap_or(& finding.file_path), finding.line_number
424 );
425 if verbose {
426 println!(
427 " š Secret: {}", finding.masked_secret.dimmed()
428 );
429 println!(" š Context:");
430 for line in finding.context.lines() {
431 if line.contains(&finding.secret_value) {
432 println!(" > {}", line.red());
433 } else {
434 println!(" > {}", line.dimmed());
435 }
436 }
437 println!(" š” {}", finding.recommendation.cyan());
438 }
439 }
440 }
441 if !report.recommendations.is_empty() {
442 println!("\nš” Recommendations:");
443 for rec in &report.recommendations {
444 println!(" ⢠{}", rec.cyan());
445 }
446 }
447 println!("\nā
Scan complete!");
448 if report.secrets_found == 0 {
449 println!(" No secrets found - good job!");
450 } else {
451 println!(
452 " Found {} potential secret(s) to review", report.secrets_found
453 );
454 }
455 }
456 OutputFormat::Json => {
457 let json = serde_json::to_string_pretty(report)
458 .unwrap_or_else(|_| "{}".to_string());
459 println!("{}", json);
460 }
461 OutputFormat::Table => {
462 println!(
463 "{:<35} {:<12} {:<15} {:<10} {:<20}", "File", "Line", "Type",
464 "Severity", "Masked Secret"
465 );
466 println!("{}", "ā".repeat(100));
467 for finding in &report.findings {
468 let file_name = finding
469 .file_path
470 .split('/')
471 .last()
472 .unwrap_or(&finding.file_path);
473 println!(
474 "{:<35} {:<12} {:<15} {:<10} {:<20}", file_name.chars().take(34)
475 .collect::< String > (), finding.line_number.to_string(), finding
476 .secret_type.chars().take(14).collect::< String > (), finding
477 .severity, finding.masked_secret.chars().take(19).collect::<
478 String > ()
479 );
480 }
481 }
482 }
483 }
484}
485#[derive(Debug, Clone)]
486struct SecretPattern {
487 name: String,
488 pattern: String,
489 confidence: String,
490 description: String,
491 example: String,
492}
493impl Tool for SecretScannerTool {
494 fn name(&self) -> &'static str {
495 "secret-scanner"
496 }
497 fn description(&self) -> &'static str {
498 "Scan for hardcoded secrets and API keys"
499 }
500 fn command(&self) -> Command {
501 Command::new(self.name())
502 .about(self.description())
503 .long_about(
504 "Scan your codebase for hardcoded secrets, API keys, tokens, and other \
505 sensitive information that should not be committed to version control.
506
507EXAMPLES:
508 cm tool secret-scanner --directory src/
509 cm tool secret-scanner --workspace --exclude-vendor
510 cm tool secret-scanner --format json --output secrets.json",
511 )
512 .args(
513 &[
514 Arg::new("directory")
515 .long("directory")
516 .short('d')
517 .help("Directory to scan (default: current)")
518 .default_value("."),
519 Arg::new("workspace")
520 .long("workspace")
521 .help("Scan entire workspace")
522 .action(clap::ArgAction::SetTrue),
523 Arg::new("exclude-vendor")
524 .long("exclude-vendor")
525 .help("Exclude vendor directories")
526 .action(clap::ArgAction::SetTrue),
527 Arg::new("include-tests")
528 .long("include-tests")
529 .help("Include test files in scan")
530 .action(clap::ArgAction::SetTrue),
531 Arg::new("confidence")
532 .long("confidence")
533 .short('c')
534 .help("Minimum confidence level")
535 .default_value("low")
536 .value_parser(["low", "medium", "high"]),
537 Arg::new("format")
538 .long("format")
539 .short('f')
540 .help("Output format for secrets")
541 .default_value("masked")
542 .value_parser(["masked", "full", "none"]),
543 Arg::new("output")
544 .long("output")
545 .short('o')
546 .help("Output file for results"),
547 Arg::new("ci-mode")
548 .long("ci-mode")
549 .help("CI-friendly output with exit codes")
550 .action(clap::ArgAction::SetTrue),
551 ],
552 )
553 .args(&common_options())
554 }
555 fn execute(&self, matches: &ArgMatches) -> Result<()> {
556 let directory = matches.get_one::<String>("directory").unwrap();
557 let workspace = matches.get_flag("workspace");
558 let exclude_vendor = matches.get_flag("exclude-vendor");
559 let include_tests = matches.get_flag("include-tests");
560 let min_confidence = matches.get_one::<String>("confidence").unwrap();
561 let format = matches.get_one::<String>("format").unwrap();
562 let output_file = matches.get_one::<String>("output");
563 let ci_mode = matches.get_flag("ci-mode");
564 let output_format = parse_output_format(matches);
565 let verbose = matches.get_flag("verbose");
566 println!(
567 "š {} - Scanning for Secrets", "CargoMate SecretScanner".bold().blue()
568 );
569 let scan_directory = if workspace { ".".to_string() } else { directory.clone() };
570 let files_to_scan = self.find_files_to_scan(&scan_directory)?;
571 if files_to_scan.is_empty() {
572 println!("{}", "No files found to scan".yellow());
573 return Ok(());
574 }
575 let mut all_findings = Vec::new();
576 for file_path in &files_to_scan {
577 match self.scan_file_for_secrets(file_path) {
578 Ok(findings) => {
579 for finding in findings {
580 let include_finding = match min_confidence.as_str() {
581 "high" => finding.confidence == "high",
582 "medium" => {
583 finding.confidence == "high"
584 || finding.confidence == "medium"
585 }
586 "low" => true,
587 _ => true,
588 };
589 if include_finding {
590 all_findings.push(finding);
591 }
592 }
593 }
594 Err(e) => {
595 if verbose {
596 println!("ā ļø Failed to scan {}: {}", file_path, e);
597 }
598 }
599 }
600 }
601 let final_findings = all_findings
602 .into_iter()
603 .filter(|finding| {
604 match format.as_str() {
605 "none" => false,
606 "masked" => true,
607 "full" => true,
608 _ => true,
609 }
610 })
611 .collect::<Vec<_>>();
612 let statistics = self.calculate_statistics(&final_findings);
613 let recommendations = self.generate_recommendations(&final_findings);
614 let report = SecretScanReport {
615 files_scanned: files_to_scan.len(),
616 secrets_found: final_findings.len(),
617 findings: final_findings,
618 false_positives: Vec::new(),
619 statistics,
620 recommendations,
621 timestamp: chrono::Utc::now().to_rfc3339(),
622 };
623 if let Some(output_path) = output_file {
624 let json = serde_json::to_string_pretty(&report)?;
625 fs::write(output_path, json)?;
626 println!("š Report saved to {}", output_path);
627 }
628 self.display_report(&report, output_format, verbose);
629 if ci_mode && !report.findings.is_empty() {
630 let critical_count = report
631 .findings
632 .iter()
633 .filter(|f| f.severity == "critical")
634 .count();
635 let high_count = report
636 .findings
637 .iter()
638 .filter(|f| f.severity == "high")
639 .count();
640 println!("::set-output name=secrets-found::{}", report.secrets_found);
641 println!("::set-output name=secrets-critical::{}", critical_count);
642 println!("::set-output name=secrets-high::{}", high_count);
643 if critical_count > 0 {
644 println!(
645 "::error title=Critical Secrets Found::Found {} critical secrets that must be addressed",
646 critical_count
647 );
648 }
649 if critical_count > 0 || high_count > 0 {
650 std::process::exit(1);
651 }
652 }
653 Ok(())
654 }
655}
656impl Default for SecretScannerTool {
657 fn default() -> Self {
658 Self::new()
659 }
660}