Skip to main content

runx_runtime/
redaction.rs

1pub 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}