auditaur-core 0.1.2

Shared models, configuration, redaction, and local discovery types for Auditaur.
Documentation
use serde_json::Value;
use std::collections::HashSet;

pub const DEFAULT_REDACTION_KEYS: &[&str] = &[
    "password",
    "passwd",
    "pwd",
    "secret",
    "token",
    "access_token",
    "refresh_token",
    "id_token",
    "authorization",
    "api_key",
    "apikey",
    "key",
    "cookie",
    "set-cookie",
    "session",
    "credential",
    "connection_string",
];

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedactionResult {
    pub value: Value,
    pub redacted: bool,
}

pub fn redact_json(value: &Value, extra_keys: &[String]) -> RedactionResult {
    redact_json_with_options(value, true, extra_keys)
}

pub fn redact_json_with_options(
    value: &Value,
    redact_defaults: bool,
    extra_keys: &[String],
) -> RedactionResult {
    let keys = redaction_key_set(extra_keys);
    let keys = if redact_defaults {
        keys
    } else {
        extra_keys
            .iter()
            .map(|key| key.to_ascii_lowercase())
            .collect()
    };
    let mut redacted = false;
    let value = redact_value(value, &keys, &mut redacted);
    RedactionResult { value, redacted }
}

fn redaction_key_set(extra_keys: &[String]) -> HashSet<String> {
    DEFAULT_REDACTION_KEYS
        .iter()
        .map(|key| key.to_ascii_lowercase())
        .chain(extra_keys.iter().map(|key| key.to_ascii_lowercase()))
        .collect()
}

fn redact_value(value: &Value, keys: &HashSet<String>, redacted: &mut bool) -> Value {
    match value {
        Value::Object(map) => Value::Object(
            map.iter()
                .map(|(key, value)| {
                    if keys.contains(&key.to_ascii_lowercase()) {
                        *redacted = true;
                        (key.clone(), Value::String("[REDACTED]".to_string()))
                    } else {
                        (key.clone(), redact_value(value, keys, redacted))
                    }
                })
                .collect(),
        ),
        Value::Array(items) => Value::Array(
            items
                .iter()
                .map(|item| redact_value(item, keys, redacted))
                .collect(),
        ),
        _ => value.clone(),
    }
}

#[cfg(test)]
mod tests {
    use super::redact_json;
    use serde_json::json;

    #[test]
    fn redacts_matching_keys_recursively() {
        let input = json!({
            "name": "auditaur",
            "nested": {
                "token": "abc",
                "items": [{ "api_key": "def" }]
            }
        });

        let result = redact_json(&input, &[]);

        assert!(result.redacted);
        assert_eq!(result.value["name"], "auditaur");
        assert_eq!(result.value["nested"]["token"], "[REDACTED]");
        assert_eq!(result.value["nested"]["items"][0]["api_key"], "[REDACTED]");
    }

    #[test]
    fn can_use_extra_keys_without_default_keys() {
        let input = json!({
            "token": "kept",
            "custom_secret": "hidden"
        });

        let result = super::redact_json_with_options(&input, false, &["custom_secret".to_string()]);

        assert!(result.redacted);
        assert_eq!(result.value["token"], "kept");
        assert_eq!(result.value["custom_secret"], "[REDACTED]");
    }
}