Skip to main content

safe_shell_scanner/
scanner.rs

1use crate::rules::{built_in_rules, Rule};
2
3#[derive(Debug, Clone)]
4pub struct Finding {
5    pub rule_id: String,
6    pub description: String,
7    pub matched: String,
8    pub start: usize,
9    pub end: usize,
10}
11
12pub struct Scanner {
13    rules: Vec<Rule>,
14}
15
16impl Scanner {
17    pub fn new() -> Self {
18        Self {
19            rules: built_in_rules(),
20        }
21    }
22
23    pub fn scan(&self, text: &str) -> Vec<Finding> {
24        let mut findings = Vec::new();
25        for rule in &self.rules {
26            for mat in rule.pattern.find_iter(text) {
27                findings.push(Finding {
28                    rule_id: rule.id.clone(),
29                    description: rule.description.clone(),
30                    matched: mat.as_str().to_string(),
31                    start: mat.start(),
32                    end: mat.end(),
33                });
34            }
35        }
36        findings
37    }
38
39    pub fn contains_secret(&self, text: &str) -> bool {
40        self.rules.iter().any(|rule| rule.pattern.is_match(text))
41    }
42}
43
44impl Default for Scanner {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn detects_aws_access_key() {
56        let scanner = Scanner::new();
57        assert!(scanner.contains_secret("AKIAIOSFODNN7EXAMPLE"));
58    }
59
60    #[test]
61    fn detects_github_token() {
62        let scanner = Scanner::new();
63        assert!(scanner.contains_secret("ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"));
64    }
65
66    #[test]
67    fn detects_private_key() {
68        let scanner = Scanner::new();
69        assert!(scanner.contains_secret("-----BEGIN RSA PRIVATE KEY-----"));
70    }
71
72    #[test]
73    fn scan_returns_findings() {
74        let scanner = Scanner::new();
75        let findings = scanner.scan("my key is AKIAIOSFODNN7EXAMPLE ok");
76        assert!(!findings.is_empty());
77        assert_eq!(findings[0].rule_id, "aws-access-key");
78    }
79
80    #[test]
81    fn scan_multiple_secrets() {
82        let scanner = Scanner::new();
83        let text = "aws=AKIAIOSFODNN7EXAMPLE and key=-----BEGIN RSA PRIVATE KEY-----";
84        let findings = scanner.scan(text);
85        assert!(findings.len() >= 2);
86    }
87
88    #[test]
89    fn ignores_safe_text() {
90        let scanner = Scanner::new();
91        assert!(!scanner.contains_secret("hello world"));
92        assert!(!scanner.contains_secret("npm install express"));
93        assert!(!scanner.contains_secret("PATH=/usr/bin:/usr/local/bin"));
94    }
95}