lean_ctx/core/
redaction.rs1macro_rules! static_regex {
2 ($pattern:expr) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10pub fn redaction_enabled_for_active_role() -> bool {
11 let role = crate::core::roles::active_role();
12 if role.role.name == "admin" {
13 role.io.redact_outputs
14 } else {
15 true
17 }
18}
19
20pub fn redact_text_if_enabled(input: &str) -> String {
21 if !redaction_enabled_for_active_role() {
22 return input.to_string();
23 }
24 redact_text(input)
25}
26
27pub fn redact_text(input: &str) -> String {
28 let patterns: Vec<(&str, ®ex::Regex)> = vec![
29 (
30 "Bearer token",
31 static_regex!(r"(?i)(bearer\s+)[a-zA-Z0-9\-_\.]{8,}"),
32 ),
33 (
34 "Authorization header",
35 static_regex!(r"(?i)(authorization:\s*(?:basic|bearer|token)\s+)[^\s\r\n]+"),
36 ),
37 (
38 "API key param",
39 static_regex!(
40 r#"(?i)((?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|token|password|passwd|pwd|secret)\s*[=:]\s*)[^\s\r\n,;&"']+"#
41 ),
42 ),
43 ("AWS key", static_regex!(r"(AKIA[0-9A-Z]{12,})")),
44 (
45 "Private key block",
46 static_regex!(
47 r"(?s)(-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----).+?(-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----)"
48 ),
49 ),
50 (
51 "GitHub token",
52 static_regex!(r"(gh[pousr]_)[a-zA-Z0-9]{20,}"),
53 ),
54 (
55 "Generic long secret",
56 static_regex!(
57 r#"(?i)(?:key|token|secret|password|credential|auth)\s*[=:]\s*['"]?([a-zA-Z0-9+/=\-_]{32,})['"]?"#
58 ),
59 ),
60 ];
61
62 let mut out = input.to_string();
63 for (label, re) in &patterns {
64 out = re
65 .replace_all(&out, |caps: ®ex::Captures| {
66 if let Some(prefix) = caps.get(1) {
67 format!("{}[REDACTED:{}]", prefix.as_str(), label)
68 } else {
69 format!("[REDACTED:{label}]")
70 }
71 })
72 .to_string();
73 }
74 out
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn redacts_bearer_token() {
83 let s = "Authorization: Bearer abcdefghijklmnopqrstuvwxyz012345";
84 let out = redact_text(s);
85 assert!(out.contains("[REDACTED"));
86 assert!(!out.contains("abcdefghijklmnopqrstuvwxyz"));
87 }
88
89 #[test]
90 fn redacts_private_key_block() {
91 let s = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----";
92 let out = redact_text(s);
93 assert!(out.contains("[REDACTED"));
94 assert!(!out.contains("\nabc\n"));
95 }
96}