bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Secret redaction shared by human diagnostics and machine lifecycle reporting.
//!
//! Redaction is deliberately conservative and text-based: it masks proxy user information,
//! common key/value secrets, and environment-mutation values whose names look sensitive. It is a
//! last output-boundary safeguard, not a replacement for keeping secrets out of event payloads.

/// Mask proxy credentials and common key/value secrets in output-bound text.
pub(crate) fn mask_secrets(value: &str) -> String {
    let mut masked = mask_proxy_secrets(value);
    for key in [
        "token",
        "password",
        "authorization",
        "api_key",
        "apikey",
        "secret",
        "credential",
    ] {
        masked = mask_key_value(&masked, key);
    }
    let mut lines = Vec::new();
    let mut sensitive_variable = false;
    for line in masked.lines() {
        let trimmed = line.trim_start();
        let upper = trimmed.to_ascii_uppercase();
        if trimmed.starts_with("name")
            && ["TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "API_KEY"]
                .iter()
                .any(|marker| upper.contains(marker))
        {
            sensitive_variable = true;
            lines.push(line.to_string());
        } else if sensitive_variable && trimmed.starts_with("value") {
            if let Some(separator) = line.find('=') {
                lines.push(format!("{}= \"***\"", &line[..separator]));
            } else {
                lines.push(line.to_string());
            }
            sensitive_variable = false;
        } else {
            lines.push(line.to_string());
        }
    }
    lines.join("\n")
}

fn mask_key_value(value: &str, key: &str) -> String {
    let lower = value.to_ascii_lowercase();
    let Some(start) = lower.find(key) else {
        return value.to_string();
    };
    let suffix = &value[start + key.len()..];
    let Some(separator) = suffix.find(['=', ':']) else {
        return value.to_string();
    };
    let value_start = start + key.len() + separator + 1;
    let end = value[value_start..]
        .find([',', ' ', '\n', '\r'])
        .map_or(value.len(), |offset| value_start + offset);
    format!("{}***{}", &value[..value_start], &value[end..])
}

fn mask_proxy_secrets(value: &str) -> String {
    let Some(scheme_end) = value.find("://") else {
        return value.to_string();
    };
    let auth_start = scheme_end + 3;
    let Some(at_offset) = value[auth_start..].find('@') else {
        return value.to_string();
    };
    let at = auth_start + at_offset;
    let Some(host_end_offset) = value[auth_start..].find(['/', '?', '#', ' ', '"', '\'']) else {
        return format!("{}***@{}", &value[..auth_start], &value[at + 1..]);
    };
    let host_end = auth_start + host_end_offset;
    if at > host_end {
        return value.to_string();
    }
    format!("{}***@{}", &value[..auth_start], &value[at + 1..])
}

#[cfg(test)]
mod tests {
    use crate::redaction::{mask_proxy_secrets, mask_secrets};

    #[test]
    fn masks_proxy_credentials() {
        assert_eq!(
            mask_proxy_secrets("http://user:pass@127.0.0.1:7890"),
            "http://***@127.0.0.1:7890"
        );
        assert_eq!(
            mask_proxy_secrets("proxy = \"http://user:pass@example.com:8080\""),
            "proxy = \"http://***@example.com:8080\""
        );
    }

    #[test]
    fn masks_common_secret_keys() {
        assert_eq!(mask_secrets("token=secret next"), "token=*** next");
        assert_eq!(
            mask_secrets("Authorization: Bearer-secret"),
            "Authorization:*** Bearer-secret"
        );
        assert_eq!(
            mask_secrets("https://user:pass@example.com/path"),
            "https://***@example.com/path"
        );
    }
}