use serde_json::Value;
const MAX_CANDIDATE_CHARS: usize = 8000;
const CREDENTIAL_PATTERNS: &[&str] = &[
"api_key",
"api-key",
"apikey",
"access_key",
"access-key",
"secret_key",
"secret-key",
"private_key",
"private-key",
"password=",
"passwd=",
"token=",
"bearer ",
"akia",
];
fn contains_credential(content: &str) -> bool {
let lower = content.to_lowercase();
if let Some(idx) = content.find("eyJ") {
let after_eyj = &content[idx..];
let after_lower = &lower[idx.min(lower.len())..];
let dots = after_lower.chars().filter(|&c| c == '.').count();
if dots >= 2 && after_eyj.len() >= 20 {
return true;
}
}
for window in lower.as_bytes().windows(20) {
if &window[0..4] == b"akia" && window[4..].iter().all(|b| b.is_ascii_alphanumeric()) {
return true;
}
}
for pattern in CREDENTIAL_PATTERNS {
if lower.contains(pattern) {
return true;
}
}
false
}
pub fn extract_candidate(value: &Value) -> Option<String> {
let raw = stringify_value(value)?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.chars().count() > MAX_CANDIDATE_CHARS {
return None;
}
if contains_credential(trimmed) {
return None;
}
Some(trimmed.to_string())
}
fn stringify_value(value: &Value) -> Option<String> {
match value {
Value::Null => None,
Value::String(s) => {
if s.trim().is_empty() {
None
} else {
Some(s.clone())
}
}
Value::Object(_) | Value::Array(_) => serde_json::to_string(value).ok(),
Value::Number(n) => Some(n.to_string()),
Value::Bool(b) => Some(b.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extract_string_value() {
let v = Value::String("hello world".to_string());
assert_eq!(extract_candidate(&v), Some("hello world".to_string()));
}
#[test]
fn extract_null_returns_none() {
assert_eq!(extract_candidate(&Value::Null), None);
}
#[test]
fn extract_empty_string_returns_none() {
assert_eq!(extract_candidate(&Value::String(" ".to_string())), None);
}
#[test]
fn extract_object_stringifies_compact() {
let v = json!({"command": "ls -la"});
let result = extract_candidate(&v).expect("object should stringify");
assert!(result.contains("\"command\":\"ls -la\""));
}
#[test]
fn extract_array_joins_elements() {
let v = json!([{"role": "user", "content": "hi"}]);
let result = extract_candidate(&v).expect("array should stringify");
assert!(result.contains("hi"));
}
#[test]
fn extract_long_content_dropped() {
let long = "a".repeat(MAX_CANDIDATE_CHARS + 1);
assert_eq!(extract_candidate(&Value::String(long)), None);
}
#[test]
fn block_jwt_three_segments() {
let jwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozj9NabvVH5hU0R3qTc";
let v = Value::String(format!("token is {jwt} here"));
assert_eq!(
extract_candidate(&v),
None,
"JWT-bearing content must be hard-blocked"
);
}
#[test]
fn block_aws_access_key() {
let v = Value::String("key=AKIAIOSFODNN7EXAMPLE end".to_string());
assert_eq!(
extract_candidate(&v),
None,
"AWS access key ID must be hard-blocked"
);
}
#[test]
fn block_bearer_token() {
let v = Value::String("Authorization: Bearer abc123def456".to_string());
assert_eq!(
extract_candidate(&v),
None,
"Bearer token must be hard-blocked"
);
}
#[test]
fn block_api_key_pattern() {
let v = Value::String("my api_key=supersecret123".to_string());
assert_eq!(extract_candidate(&v), None, "api_key= must be hard-blocked");
}
#[test]
fn block_password_pattern() {
let v = Value::String("login password=hunter2".to_string());
assert_eq!(
extract_candidate(&v),
None,
"password= must be hard-blocked"
);
}
#[test]
fn no_false_positive_for_eyj_without_dots() {
let v = Value::String("the user said eyJhello there".to_string());
assert!(
extract_candidate(&v).is_some(),
"eyJ without enough structure must not block"
);
}
#[test]
fn no_false_positive_for_normal_text() {
let v = Value::String("Alice works at Microsoft and uses Rust".to_string());
assert_eq!(
extract_candidate(&v),
Some("Alice works at Microsoft and uses Rust".to_string())
);
}
}