diff --git a/chrs-shhh/Cargo.toml b/chrs-shhh/Cargo.toml new file mode 100644 index 00000000..e2a437d1 --- /dev/null +++ b/chrs-shhh/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "chrs-shhh" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +regex = "1.10" +lazy_static = "1.4" diff --git a/chrs-shhh/src/lib.rs b/chrs-shhh/src/lib.rs new file mode 100644 index 00000000..aba01985 --- /dev/null +++ b/chrs-shhh/src/lib.rs @@ -0,0 +1,82 @@ +use regex::Regex; +use lazy_static::lazy_static; + +pub struct RedactionRule { + pub name: String, + pub pattern: Regex, + pub replacement: String, +} + +pub struct SecretSentinel { + rules: Vec, +} + +lazy_static! { + static ref OPENAI_KEY: Regex = Regex::new(r"sk-[a-zA-Z0-9]{48}").unwrap(); + static ref AWS_KEY: Regex = Regex::new(r"AKIA[0-9A-Z]{16}").unwrap(); + static ref GENERIC_SECRET: Regex = Regex::new(r"(?i)(password|secret|key|token)\s*[:=]\s*[^\s]+").unwrap(); +} + +impl SecretSentinel { + pub fn new_default() -> Self { + let rules = vec![ + RedactionRule { + name: "OpenAI API Key".into(), + pattern: OPENAI_KEY.clone(), + replacement: "[REDACTED OPENAI KEY]".into(), + }, + RedactionRule { + name: "AWS Access Key".into(), + pattern: AWS_KEY.clone(), + replacement: "[REDACTED AWS KEY]".into(), + }, + RedactionRule { + name: "Generic Secret".into(), + pattern: GENERIC_SECRET.clone(), + replacement: "$1: [REDACTED]".into(), + }, + ]; + Self { rules } + } + + pub fn scrub_text(&self, input: &str) -> String { + let mut scrubbed = input.to_string(); + for rule in &self.rules { + scrubbed = rule.pattern.replace_all(&scrubbed, &rule.replacement).to_string(); + } + scrubbed + } + + pub fn contains_secrets(&self, input: &str) -> bool { + self.rules.iter().any(|rule| rule.pattern.is_match(input)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_scrub_openai_key() { + let sentinel = SecretSentinel::new_default(); + let input = "My key is sk-1234567890abcdef1234567890abcdef1234567890abcdef"; + let output = sentinel.scrub_text(input); + assert!(output.contains("[REDACTED OPENAI KEY]")); + assert!(!output.contains("sk-1234567890")); + } + + #[test] + fn test_scrub_generic_password() { + let sentinel = SecretSentinel::new_default(); + let input = "login with password: my-secret-password now"; + let output = sentinel.scrub_text(input); + assert!(output.contains("password: [REDACTED]")); + } + + #[test] + fn test_contains_secrets() { + let sentinel = SecretSentinel::new_default(); + assert!(sentinel.contains_secrets("AKIAIOSFODNN7EXAMPLE")); + assert!(!sentinel.contains_secrets("nothing sensitive here")); + } +}