runx_runtime/
redaction.rs1pub fn redact_sensitive_text(input: &str) -> String {
2 redact_urls(&redact_bearer_tokens(&redact_prefixed_secret(
3 input, "SECRET_",
4 )))
5}
6
7#[cfg(feature = "thread-outbox-provider")]
8pub(crate) fn trim_ascii_whitespace(bytes: &[u8]) -> &[u8] {
9 let start = bytes
10 .iter()
11 .position(|byte| !byte.is_ascii_whitespace())
12 .unwrap_or(bytes.len());
13 let end = bytes
14 .iter()
15 .rposition(|byte| !byte.is_ascii_whitespace())
16 .map_or(start, |index| index + 1);
17 &bytes[start..end]
18}
19
20fn redact_prefixed_secret(input: &str, prefix: &str) -> String {
21 let mut output = String::with_capacity(input.len());
22 let mut index = 0;
23 while let Some(relative_start) = input[index..].find(prefix) {
24 let start = index + relative_start;
25 output.push_str(&input[index..start]);
26 let mut end = start + prefix.len();
27 for character in input[end..].chars() {
28 if character.is_ascii_alphanumeric() || matches!(character, '_' | '-') {
29 end += character.len_utf8();
30 } else {
31 break;
32 }
33 }
34 output.push_str("[redacted]");
35 index = end;
36 }
37 output.push_str(&input[index..]);
38 output
39}
40
41fn redact_bearer_tokens(input: &str) -> String {
42 let mut output = String::with_capacity(input.len());
43 let mut index = 0;
44 while let Some(relative_start) = input[index..].find("Bearer ") {
45 let start = index + relative_start;
46 output.push_str(&input[index..start]);
47 output.push_str("Bearer [redacted]");
48 let mut end = start + "Bearer ".len();
49 for character in input[end..].chars() {
50 if character.is_whitespace() {
51 break;
52 }
53 end += character.len_utf8();
54 }
55 index = end;
56 }
57 output.push_str(&input[index..]);
58 output
59}
60
61fn redact_urls(input: &str) -> String {
62 let mut output = String::with_capacity(input.len());
63 let mut index = 0;
64 while let Some(relative_start) = find_next_url(&input[index..]) {
65 let start = index + relative_start;
66 output.push_str(&input[index..start]);
67 output.push_str("[redacted-url]");
68 let mut end = start;
69 for character in input[end..].chars() {
70 if character.is_whitespace() || matches!(character, '"' | '\'' | ')' | ']') {
71 break;
72 }
73 end += character.len_utf8();
74 }
75 index = end;
76 }
77 output.push_str(&input[index..]);
78 output
79}
80
81fn find_next_url(input: &str) -> Option<usize> {
82 match (input.find("https://"), input.find("http://")) {
83 (Some(https), Some(http)) => Some(https.min(http)),
84 (Some(https), None) => Some(https),
85 (None, Some(http)) => Some(http),
86 (None, None) => None,
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::redact_sensitive_text;
93
94 #[test]
95 fn redacts_sentinel_and_bearer_values() {
96 let redacted = redact_sensitive_text(
97 "failed SECRET_PROVIDER_ACCESS_TOKEN_DO_NOT_LEAK Bearer abc.def SECRET_AUTHORIZE_QUERY_DO_NOT_LEAK https://auth.example/authorize?code=abc",
98 );
99
100 assert!(!redacted.contains("SECRET_PROVIDER_ACCESS_TOKEN_DO_NOT_LEAK"));
101 assert!(!redacted.contains("abc.def"));
102 assert!(!redacted.contains("SECRET_AUTHORIZE_QUERY_DO_NOT_LEAK"));
103 assert!(!redacted.contains("auth.example"));
104 assert_eq!(
105 redacted,
106 "failed [redacted] Bearer [redacted] [redacted] [redacted-url]"
107 );
108 }
109}