assay-lua 0.19.0

General-purpose enhanced Lua runtime. Batteries-included scripting, automation, and web services.
Documentation
use serde_json::Value;

pub fn redact_json_text(text: &str, keys: &[String]) -> Option<String> {
    if keys.is_empty() {
        return None;
    }
    let mut value: Value = serde_json::from_str(text).ok()?;
    let lowered: Vec<String> = keys.iter().map(|k| k.to_ascii_lowercase()).collect();
    strip(&mut value, &lowered);
    serde_json::to_string(&value).ok()
}

pub fn is_redacted_header(name: &str, keys: &[String]) -> bool {
    let name = name.to_ascii_lowercase();
    keys.iter().any(|k| k.to_ascii_lowercase() == name)
}

fn strip(value: &mut Value, keys: &[String]) {
    match value {
        Value::Object(map) => {
            for (_, v) in map.iter_mut() {
                strip(v, keys);
            }
            for key in map.keys().cloned().collect::<Vec<_>>() {
                if keys.contains(&key.to_ascii_lowercase()) {
                    map.insert(key, Value::String("[redacted]".to_string()));
                }
            }
        }
        Value::Array(items) => {
            for item in items.iter_mut() {
                strip(item, keys);
            }
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn keys() -> Vec<String> {
        vec!["password".into(), "token".into()]
    }

    #[test]
    fn nested_and_arrayed_secrets_are_replaced_not_removed() {
        let out = redact_json_text(
            r#"{"user":{"name":"a","password":"hunter2"},"items":[{"token":"t"}]}"#,
            &keys(),
        )
        .expect("redacted");
        let parsed: Value = serde_json::from_str(&out).expect("valid json");
        assert_eq!(
            parsed["user"]["password"],
            Value::String("[redacted]".into())
        );
        assert_eq!(
            parsed["items"][0]["token"],
            Value::String("[redacted]".into())
        );
        assert_eq!(parsed["user"]["name"], Value::String("a".into()));
    }

    #[test]
    fn matching_is_case_insensitive() {
        let out = redact_json_text(r#"{"Password":"hunter2"}"#, &keys()).expect("redacted");
        assert!(!out.contains("hunter2"));
    }

    #[test]
    fn non_json_and_empty_keys_pass_through_untouched() {
        assert!(redact_json_text("not json at all", &keys()).is_none());
        assert!(redact_json_text(r#"{"password":"x"}"#, &[]).is_none());
    }

    #[test]
    fn header_names_match_case_insensitively() {
        assert!(is_redacted_header("X-Auth-Token", &["x-auth-token".into()]));
        assert!(!is_redacted_header("content-type", &keys()));
    }
}