Skip to main content

cha_core/plugins/
hardcoded_secret.rs

1use 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 smells(&self) -> Vec<String> {
34        vec!["hardcoded_secret".into()]
35    }
36
37    fn description(&self) -> &str {
38        "Hardcoded API keys, tokens, passwords"
39    }
40
41    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
42        let mut findings = Vec::new();
43        for (line_num, line) in ctx.file.content.lines().enumerate() {
44            let ln = line_num + 1;
45            if is_skip_line(line) {
46                continue;
47            }
48            for (label, re) in PATTERNS.iter() {
49                if re.is_match(line) {
50                    let col = re.find(line).map(|m| m.start()).unwrap_or(0);
51                    let end_col = re.find(line).map(|m| m.end()).unwrap_or(0);
52                    findings.push(Finding {
53                        smell_name: "hardcoded_secret".into(),
54                        category: SmellCategory::Security,
55                        severity: Severity::Warning,
56                        location: Location {
57                            path: ctx.file.path.clone(),
58                            start_line: ln,
59                            start_col: col,
60                            end_line: ln,
61                            end_col,
62                            name: Some(label.to_string()),
63                        },
64                        message: format!("Possible hardcoded {label} detected"),
65                        suggested_refactorings: vec![
66                            "Use environment variables".into(),
67                            "Use a secrets manager".into(),
68                        ],
69                        ..Default::default()
70                    });
71                    break; // one finding per line
72                }
73            }
74        }
75        findings
76    }
77}
78
79fn is_skip_line(line: &str) -> bool {
80    let trimmed = line.trim();
81    trimmed.starts_with("//")
82        || trimmed.starts_with('#')
83        || trimmed.starts_with("/*")
84        || trimmed.starts_with('*')
85        || trimmed.contains("example")
86        || trimmed.contains("placeholder")
87        || trimmed.contains("xxx")
88        || trimmed.contains("TODO")
89}