cha_core/plugins/
hardcoded_secret.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2use regex::Regex;
3use std::sync::LazyLock;
4
5pub struct HardcodedSecretAnalyzer;
6
7impl Default for HardcodedSecretAnalyzer {
8 fn default() -> Self {
9 Self
10 }
11}
12
13static PATTERNS: LazyLock<Vec<(&str, Regex)>> = LazyLock::new(|| {
14 [
15 ("AWS Access Key", r#"(?i)(aws_access_key_id|aws_secret_access_key)\s*[=:]\s*["']?[A-Za-z0-9/+=]{20,}"#),
16 ("Generic Secret", r#"(?i)(secret|password|passwd|token|api_key|apikey|auth_token|access_token)\s*[=:]\s*["'][^"']{8,}["']"#),
17 ("Private Key", r#"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"#),
18 ("GitHub Token", r#"gh[ps]_[A-Za-z0-9_]{36,}"#),
19 ("Slack Token", r#"xox[bpors]-[A-Za-z0-9-]{10,}"#),
20 ("JWT", r#"eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"#),
21 ("Hex Secret (32+)", r#"(?i)(secret|key|token|password)\s*[=:]\s*["'][0-9a-f]{32,}["']"#),
22 ]
23 .iter()
24 .map(|(name, pat)| (*name, Regex::new(pat).unwrap()))
25 .collect()
26});
27
28impl Plugin for HardcodedSecretAnalyzer {
29 fn name(&self) -> &str {
30 "hardcoded_secret"
31 }
32
33 fn description(&self) -> &str {
34 "Hardcoded API keys, tokens, passwords"
35 }
36
37 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
38 let mut findings = Vec::new();
39 for (line_num, line) in ctx.file.content.lines().enumerate() {
40 let ln = line_num + 1;
41 if is_skip_line(line) {
42 continue;
43 }
44 for (label, re) in PATTERNS.iter() {
45 if re.is_match(line) {
46 let col = re.find(line).map(|m| m.start()).unwrap_or(0);
47 let end_col = re.find(line).map(|m| m.end()).unwrap_or(0);
48 findings.push(Finding {
49 smell_name: "hardcoded_secret".into(),
50 category: SmellCategory::Security,
51 severity: Severity::Warning,
52 location: Location {
53 path: ctx.file.path.clone(),
54 start_line: ln,
55 start_col: col,
56 end_line: ln,
57 end_col,
58 name: Some(label.to_string()),
59 },
60 message: format!("Possible hardcoded {label} detected"),
61 suggested_refactorings: vec![
62 "Use environment variables".into(),
63 "Use a secrets manager".into(),
64 ],
65 ..Default::default()
66 });
67 break; }
69 }
70 }
71 findings
72 }
73}
74
75fn is_skip_line(line: &str) -> bool {
76 let trimmed = line.trim();
77 trimmed.starts_with("//")
78 || trimmed.starts_with('#')
79 || trimmed.starts_with("/*")
80 || trimmed.starts_with('*')
81 || trimmed.contains("example")
82 || trimmed.contains("placeholder")
83 || trimmed.contains("xxx")
84 || trimmed.contains("TODO")
85}