use crate::error::SecretSet;
use crate::json::OValue;
pub const REDACTED: &str = "[REDACTED]";
pub const MIN_REGISTERED: usize = 8;
pub const UNREGISTERED_KEYS: [&str; 5] =
["client_id", "clientid", "grant_type", "scope", "audience"];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Reg {
Strong,
Weak,
Never,
}
impl Reg {
fn under(self, key: &str) -> Self {
let lower = key.to_ascii_lowercase();
if self == Self::Never || UNREGISTERED_KEYS.contains(&lower.as_str()) {
Self::Never
} else if secret_key(key) {
Self::Strong
} else {
self
}
}
fn admits(self, value: &str) -> bool {
match self {
Self::Strong => true,
Self::Weak => value.len() >= MIN_REGISTERED,
Self::Never => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Shape {
Target,
Adapter,
ErrorLog,
Generic,
}
fn scheme_credential(value: &str) -> Option<&str> {
let (scheme, rest) = value.trim().split_once(' ')?;
let rest = rest.trim();
(!scheme.is_empty()
&& scheme.len() <= 16
&& scheme.bytes().all(|byte| byte.is_ascii_alphabetic())
&& rest.len() >= 4)
.then_some(rest)
}
pub fn redact(document: &mut OValue, shape: Shape, secrets: &SecretSet) {
let mut sink = |value: &str, reg: Reg| {
if !reg.admits(value) {
return;
}
secrets.add(value);
if let Some(credential) = scheme_credential(value)
&& reg.admits(credential)
{
secrets.add(credential);
}
};
match shape {
Shape::Target => target(document, &mut sink),
Shape::Adapter => adapter(document, &mut sink),
Shape::ErrorLog => error_log(document, &mut sink),
Shape::Generic => catch_all(document, &mut sink),
}
}
type Sink<'a> = dyn FnMut(&str, Reg) + 'a;
const SENTINEL: &str = "\u{0}airs-withheld\u{0}";
fn is_mask(text: &str) -> bool {
text.len() >= 4 && text.bytes().all(|byte| byte == b'*')
}
fn mark_withheld(value: &mut OValue) {
match value {
OValue::String(text) if text == REDACTED || is_mask(text) => {
*text = SENTINEL.to_owned();
}
OValue::Array(items) => items.iter_mut().for_each(mark_withheld),
OValue::Object(entries) => entries
.iter_mut()
.for_each(|(_, value)| mark_withheld(value)),
_ => {}
}
}
pub fn withheld(document: &OValue) -> bool {
let mut copy = document.clone();
mark_withheld(&mut copy);
let mut found = false;
target(&mut copy, &mut |value: &str, _| found |= value == SENTINEL);
let overrides = ["adapter_variable_overrides", "adapterVariableOverrides"]
.iter()
.filter_map(|key| document.get(key).and_then(OValue::as_array))
.flatten();
for entry in overrides {
let secret = entry.get("type").and_then(OValue::as_str) == Some("SECRET");
let valueless = match entry.get("value") {
None | Some(OValue::Null) => true,
Some(OValue::String(text)) => text.is_empty(),
Some(_) => false,
};
found |= secret && valueless;
}
found
}
pub fn carries_credentials(document: &OValue) -> bool {
let mut copy = document.clone();
mark_withheld(&mut copy);
let mut found = false;
target(&mut copy, &mut |value: &str, reg| {
let masked = |text: &str| text == SENTINEL || text == REDACTED || is_mask(text);
found |=
reg != Reg::Never && !masked(value) && !scheme_credential(value).is_some_and(masked);
});
found
}
fn replace(value: &mut OValue, reg: Reg, sink: &mut Sink<'_>) {
if let OValue::String(text) = value
&& !text.is_empty()
&& text != REDACTED
{
sink(text, reg);
*text = REDACTED.to_owned();
}
}
fn all_strings(
value: &mut OValue,
kept: &[&str],
url_keys: &[&str],
reg: Reg,
sink: &mut Sink<'_>,
) {
match value {
OValue::String(_) => replace(value, reg, sink),
OValue::Array(items) => {
for item in items {
all_strings(item, kept, url_keys, reg, sink);
}
}
OValue::Object(entries) => {
for (key, value) in entries {
if kept.contains(&key.as_str()) {
continue;
}
if url_keys.contains(&key.as_str()) {
url(value, sink);
continue;
}
all_strings(value, kept, url_keys, reg.under(key), sink);
}
}
OValue::Null | OValue::Bool(_) | OValue::Number(_) => {}
}
}
fn url(value: &mut OValue, sink: &mut Sink<'_>) {
if let OValue::String(text) = value
&& let Some(redacted) = crate::tenant::config::redact_url(text)
{
sink(text, Reg::Never);
url_credentials(text, sink);
*text = redacted;
}
}
fn secret_param(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
secret_key(name) || lower == "sig" || lower == "auth" || lower.ends_with("signature")
}
fn param_credentials(text: &str, sink: &mut Sink<'_>) {
for piece in text.split('&') {
let Some((name, raw)) = piece.split_once('=') else {
continue;
};
let name: String = url::form_urlencoded::parse(name.as_bytes())
.map(|(key, _)| key.into_owned())
.next()
.unwrap_or_default();
if raw.is_empty() || !secret_param(&name) {
continue;
}
sink(raw, Reg::Strong);
let decoded: String = url::form_urlencoded::parse(piece.as_bytes())
.map(|(_, value)| value.into_owned())
.next()
.unwrap_or_default();
if !decoded.is_empty() && decoded != raw {
sink(&decoded, Reg::Strong);
}
}
}
fn url_credentials(text: &str, sink: &mut Sink<'_>) {
if let Some(start) = text.find("://").map(|index| index + 3) {
let rest = &text[start..];
let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
if let Some((userinfo, _)) = rest[..end].rsplit_once('@') {
let (user, password) = userinfo.split_once(':').unwrap_or((userinfo, ""));
let decode = |part: &str| -> String {
url::form_urlencoded::parse(format!("x={part}").as_bytes())
.map(|(_, value)| value.into_owned())
.next()
.unwrap_or_default()
};
for (part, reg) in [(user, Reg::Weak), (password, Reg::Strong)] {
if part.is_empty() {
continue;
}
sink(part, reg);
let decoded = decode(part);
if !decoded.is_empty() && decoded != part {
sink(&decoded, reg);
}
}
}
}
let (before_fragment, fragment) = text.split_once('#').unwrap_or((text, ""));
if let Some((_, query)) = before_fragment.split_once('?') {
param_credentials(query, sink);
}
param_credentials(fragment, sink);
}
fn shell_words(command: &str) -> Vec<String> {
let mut words = Vec::new();
let mut word = String::new();
let mut in_word = false;
let mut chars = command.chars().peekable();
while let Some(c) = chars.next() {
match c {
c if c.is_whitespace() => {
if in_word {
words.push(std::mem::take(&mut word));
in_word = false;
}
}
'\\' => match chars.next() {
Some('\n') | None => {}
Some(next) => {
word.push(next);
in_word = true;
}
},
'\'' => {
in_word = true;
for next in chars.by_ref() {
if next == '\'' {
break;
}
word.push(next);
}
}
'$' if chars.peek() == Some(&'\'') => {
chars.next();
in_word = true;
while let Some(next) = chars.next() {
match next {
'\'' => break,
'\\' => {
if let Some(escaped) = chars.next() {
word.push(escaped);
}
}
other => word.push(other),
}
}
}
'"' => {
in_word = true;
while let Some(next) = chars.next() {
match next {
'"' => break,
'\\' => match chars.next() {
Some('\n') | None => {}
Some(escaped @ ('"' | '\\' | '$' | '`')) => word.push(escaped),
Some(other) => {
word.push('\\');
word.push(other);
}
},
other => word.push(other),
}
}
}
other => {
word.push(other);
in_word = true;
}
}
}
if in_word {
words.push(word);
}
words
}
fn curl_credentials(command: &str, sink: &mut Sink<'_>) {
const HEADER: [&str; 3] = ["-H", "--header", "--proxy-header"];
const USER: [&str; 4] = ["-u", "--user", "-U", "--proxy-user"];
const COOKIE: [&str; 2] = ["-b", "--cookie"];
const BEARER: [&str; 1] = ["--oauth2-bearer"];
const BODY: [&str; 9] = [
"-d",
"--data",
"--data-raw",
"--data-binary",
"--data-ascii",
"--data-urlencode",
"--json",
"-F",
"--form",
];
const URL: [&str; 1] = ["--url"];
let takes_value = |flag: &str| {
[&HEADER[..], &USER, &COOKIE, &BEARER, &BODY, &URL]
.iter()
.any(|flags| flags.contains(&flag))
};
let words = shell_words(command);
let mut index = 0;
while index < words.len() {
let word = words[index].as_str();
index += 1;
let (flag, value) = if let Some((flag, value)) = word
.split_once('=')
.filter(|(flag, _)| flag.starts_with("--") && takes_value(flag))
{
(flag, Some(value.to_owned()))
} else if word.len() > 2
&& word.is_char_boundary(2)
&& !word.starts_with("--")
&& takes_value(&word[..2])
{
(&word[..2], Some(word[2..].to_owned()))
} else if takes_value(word) {
let value = words.get(index).cloned();
index += 1;
(word, value)
} else {
if word.contains("://") {
url_credentials(word, sink);
}
continue;
};
let Some(value) = value.filter(|value| !value.is_empty()) else {
continue;
};
if HEADER.contains(&flag) {
if let Some((name, header)) = value.split_once(':') {
let header = header.trim();
if secret_key(name.trim()) && !header.is_empty() {
sink(header, Reg::Strong);
}
}
} else if USER.contains(&flag) {
if let Some((_, password)) = value.split_once(':')
&& !password.is_empty()
{
sink(password, Reg::Strong);
sink(
&crate::error::base64_standard(value.as_bytes()),
Reg::Strong,
);
}
} else if COOKIE.contains(&flag) {
for cookie in value.split(';') {
if let Some((_, cookie)) = cookie.split_once('=') {
sink(cookie.trim(), Reg::Weak);
}
}
} else if BEARER.contains(&flag) {
sink(&value, Reg::Strong);
} else if BODY.contains(&flag) {
body_credentials(&value, sink);
} else {
url_credentials(&value, sink);
}
}
}
fn body_credentials(body: &str, sink: &mut Sink<'_>) {
if let Ok(mut document) = crate::json::parse(body)
&& matches!(document, OValue::Object(_) | OValue::Array(_))
{
catch_all(&mut document, sink);
return;
}
param_credentials(body, sink);
}
fn secret_key(key: &str) -> bool {
let key = key.to_ascii_lowercase();
if matches!(
key.as_str(),
"response_key" | "response_stop_key" | "oauth2_token_response_key"
) {
return false;
}
["key", "secret", "token", "password"]
.iter()
.any(|suffix| key.ends_with(suffix))
|| ["authorization", "cookie", "credential", "passwd"]
.iter()
.any(|part| key.contains(part))
}
fn catch_all(value: &mut OValue, sink: &mut Sink<'_>) {
catch_all_in(value, false, sink);
}
fn catch_all_in(value: &mut OValue, variable_entry: bool, sink: &mut Sink<'_>) {
match value {
OValue::Array(items) => {
for item in items {
catch_all_in(item, false, sink);
}
}
OValue::Object(entries) => {
for (key, value) in entries.iter_mut() {
if variable_entry && key == "key" {
continue;
}
if secret_key(key) {
if let OValue::String(_) = value {
replace(value, Reg::Strong, sink);
continue;
}
all_strings(value, &[], &[], Reg::Strong, sink);
continue;
}
if is_variables_key(key)
&& let OValue::Array(items) = value
{
for item in items {
catch_all_in(item, true, sink);
}
continue;
}
catch_all_in(value, false, sink);
}
}
OValue::Null | OValue::Bool(_) | OValue::Number(_) | OValue::String(_) => {}
}
}
fn is_variables_key(key: &str) -> bool {
matches!(
key,
"variables" | "adapter_variable_overrides" | "adapterVariableOverrides"
)
}
fn field<'a>(value: &'a mut OValue, names: &[&str]) -> Vec<&'a mut OValue> {
match value {
OValue::Object(entries) => entries
.iter_mut()
.filter(|(key, _)| names.contains(&key.as_str()))
.map(|(_, value)| value)
.collect(),
_ => Vec::new(),
}
}
fn variables(value: &mut OValue, sink: &mut Sink<'_>) {
let OValue::Array(items) = value else {
return;
};
for item in items {
let secret = item.get("type").and_then(OValue::as_str) != Some("VAR");
if secret {
for value in field(item, &["value"]) {
replace(value, Reg::Strong, sink);
}
}
}
}
fn target(document: &mut OValue, sink: &mut Sink<'_>) {
for auth in field(document, &["auth_config", "authConfig"]) {
all_strings(
auth,
&["basic_auth_location", "oauth2_token_response_key"],
&["oauth2_token_url"],
Reg::Weak,
sink,
);
}
for params in field(document, &["connection_params", "connectionParams"]) {
connection_params(params, sink);
}
for overrides in field(
document,
&["adapter_variable_overrides", "adapterVariableOverrides"],
) {
variables(overrides, sink);
}
catch_all(document, sink);
}
fn connection_params(params: &mut OValue, sink: &mut Sink<'_>) {
let OValue::Object(entries) = params else {
return;
};
for (key, value) in entries.iter_mut() {
match key.as_str() {
"request_headers" => {
if let OValue::Object(headers) = value {
for (name, value) in headers.iter_mut() {
let kept = ["accept", "content-type", "user-agent"]
.iter()
.any(|allowed| name.eq_ignore_ascii_case(allowed));
if !kept {
all_strings(value, &[], &[], Reg::Weak.under(name), sink);
}
}
}
}
"curl" => match value {
OValue::String(command) => {
let command = command.clone();
replace(value, Reg::Never, sink);
curl_credentials(&command, sink);
}
other => all_strings(other, &[], &[], Reg::Never, sink),
},
"target_connection_config" => all_strings(
value,
&["model_name", "model_id", "region", "auth_type"],
&["workspace_url"],
Reg::Weak,
sink,
),
name if name == "api_endpoint" || name.ends_with("_url") => url(value, sink),
_ => {}
}
}
}
fn adapter(document: &mut OValue, sink: &mut Sink<'_>) {
for list in field(document, &["variables"]) {
variables(list, sink);
}
catch_all(document, sink);
}
fn error_log(document: &mut OValue, sink: &mut Sink<'_>) {
for object in field(document, &["targetObject", "target_object"]) {
target(object, sink);
}
catch_all(document, sink);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::parse;
#[test]
fn withheld_credentials_are_found_by_the_target_rules() {
let check = |text: &str| withheld(&parse(text).unwrap());
assert!(!check(
r#"{"name":"t","auth_config":{"auth_header":{"Authorization":"Bearer S1"}}}"#
));
assert!(check(
r#"{"name":"t","auth_config":{"auth_header":{"Authorization":"[REDACTED]"}}}"#
));
assert!(check(
r#"{"connection_params":{"request_headers":{"apikey":"********"}}}"#
));
assert!(check(
r#"{"connection_params":{"target_connection_config":{"api_key":"****"}}}"#
));
assert!(check(r#"{"extra_info":{"token":"[REDACTED]"}}"#));
assert!(!check(
r#"{"description":"[REDACTED]","connection_params":{"api_endpoint":"[REDACTED]","request_headers":{"Accept":"****"}}}"#
));
assert!(check(
r#"{"adapter_variable_overrides":[{"key":"k","type":"SECRET"}]}"#
));
assert!(check(
r#"{"adapter_variable_overrides":[{"key":"k","value":null,"type":"SECRET"}]}"#
));
assert!(check(
r#"{"adapter_variable_overrides":[{"key":"k","value":"**********","type":"SECRET"}]}"#
));
assert!(!check(
r#"{"adapter_variable_overrides":[{"key":"k","value":"v","type":"SECRET"},{"key":"e","type":"VAR"}]}"#
));
}
#[test]
fn returned_credentials_are_told_from_withheld_ones() {
let check = |text: &str| carries_credentials(&parse(text).unwrap());
assert!(check(
r#"{"auth_config":{"auth_header":{"Authorization":"Bearer S1"}}}"#
));
assert!(check(
r#"{"adapter_variable_overrides":[{"key":"k","value":"v","type":"SECRET"}]}"#
));
assert!(check(
r#"{"connection_params":{"request_headers":{"apikey":"********","X-Key":"real"}}}"#
));
assert!(!check(
r#"{"name":"t","description":"no credentials","connection_params":{"api_endpoint":"https://x.invalid/v1","request_headers":{"Content-Type":"application/json"}}}"#
));
assert!(!check(
r#"{"auth_config":{"auth_header":{"Authorization":"Bearer ********"}},"connection_params":{"request_headers":{"apikey":"[REDACTED]"},"target_connection_config":{"api_key":"****"}}}"#
));
assert!(!check(
r#"{"adapter_variable_overrides":[{"key":"k","type":"SECRET"},{"key":"m","value":"*****","type":"SECRET"}]}"#
));
assert!(!check(
r#"{"auth_config":{"oauth2_token_url":"https://auth.invalid/t","client_id":"app"}}"#
));
}
fn redacted(text: &str, shape: Shape) -> (String, SecretSet) {
let secrets = SecretSet::default();
let mut value = parse(text).unwrap();
redact(&mut value, shape, &secrets);
(crate::json::to_compact(&value).unwrap(), secrets)
}
#[test]
fn target_rules_cover_every_credential_path() {
let (text, secrets) = redacted(
r#"{"name":"t","auth_config":{"auth_header":{"Authorization":"Bearer S1"},
"basic_auth_location":"HEADER","oauth2_token_response_key":"access_token",
"oauth2_token_url":"https://auth.invalid/t?client_secret=S2","oauth2_expiry_minutes":60},
"connection_params":{"api_endpoint":"https://x.invalid/v1?key=S3",
"request_headers":{"Content-Type":"application/json","ACCEPT":"*/*","apikey":"S4","n":{"deep":"S5-DEEP-VALUE"}},
"curl":"curl -H 'X-Api-Key: S6'","response_key":"choices.0","base_url":"https://u:S7@h.invalid/p",
"target_connection_config":{"api_key":"S8","model_name":"m","region":"r","workspace_url":"https://w.invalid/?t=S9","secret":"S10"},
"request_json":{"messages":[]}},
"adapter_variable_overrides":[{"key":"endpoint","value":"http://a","type":"VAR"},{"key":"api_token","value":"S11","type":"SECRET"},{"key":"x","value":null,"type":"SECRET"}],
"extra_info":{"note":"plain","token":"S12","key":"S13","count":3},
"description":"","password":""}"#,
Shape::Target,
);
assert_eq!(
text,
concat!(
r#"{"name":"t","auth_config":{"auth_header":{"Authorization":"[REDACTED]"},"#,
r#""basic_auth_location":"HEADER","oauth2_token_response_key":"access_token","#,
r#""oauth2_token_url":"https://auth.invalid/t?[REDACTED]","oauth2_expiry_minutes":60},"#,
r#""connection_params":{"api_endpoint":"https://x.invalid/v1?[REDACTED]","#,
r#""request_headers":{"Content-Type":"application/json","ACCEPT":"*/*","apikey":"[REDACTED]","n":{"deep":"[REDACTED]"}},"#,
r#""curl":"[REDACTED]","response_key":"choices.0","base_url":"https://[REDACTED]@h.invalid/p","#,
r#""target_connection_config":{"api_key":"[REDACTED]","model_name":"m","region":"r","workspace_url":"https://w.invalid/?[REDACTED]","secret":"[REDACTED]"},"#,
r#""request_json":{"messages":[]}},"#,
r#""adapter_variable_overrides":[{"key":"endpoint","value":"http://a","type":"VAR"},{"key":"api_token","value":"[REDACTED]","type":"SECRET"},{"key":"x","value":null,"type":"SECRET"}],"#,
r#""extra_info":{"note":"plain","token":"[REDACTED]","key":"[REDACTED]","count":3},"#,
r#""description":"","password":""}"#
)
);
for value in [
"Bearer S1",
"S3",
"S4",
"S5-DEEP-VALUE",
"S6",
"S7",
"S8",
"S10",
"S11",
"S12",
] {
assert!(secrets.taints(value), "{value} joined the secret set");
}
assert!(secrets.taints("https://x.invalid/v1?key=S3"));
assert!(!secrets.taints("choices.0"));
for text in ["curl -H", "X-Api-Key", "x.invalid/v1", "h.invalid"] {
assert!(!secrets.taints(text), "{text} is not withheld");
}
}
#[test]
fn short_and_non_credential_values_are_redacted_but_not_registered() {
let (text, secrets) = redacted(
r#"{"name":"prod chatbot v1","description":"Uses client_credentials grant",
"auth_config":{"client_id":"cid","oauth2_body_params":{"grant_type":"client_credentials","scope":"read:all"},
"client_secret":"s3","username":"svc-account-7"},
"connection_params":{"request_headers":{"X-Env":"prod","X-Api-Version":"1","X-Api-Key":"k1","X-Trace":"trace-long-value"}}}"#,
Shape::Target,
);
assert_eq!(
text,
concat!(
r#"{"name":"prod chatbot v1","description":"Uses client_credentials grant","#,
r#""auth_config":{"client_id":"[REDACTED]","oauth2_body_params":{"grant_type":"[REDACTED]","scope":"[REDACTED]"},"#,
r#""client_secret":"[REDACTED]","username":"[REDACTED]"},"#,
r#""connection_params":{"request_headers":{"X-Env":"[REDACTED]","X-Api-Version":"[REDACTED]","X-Api-Key":"[REDACTED]","X-Trace":"[REDACTED]"}}}"#
),
"every credential-path value is still redacted"
);
for kept in [
"prod chatbot v1",
"Uses client_credentials grant",
"Acid target",
"read:all",
] {
assert!(!secrets.taints(kept), "{kept} is not withheld");
}
for value in ["s3", "k1", "svc-account-7", "trace-long-value"] {
assert!(secrets.taints(value), "{value} joined the secret set");
}
let (_, secrets) = redacted(
r#"{"variables":[{"key":"pin","value":"42","type":"SECRET"}]}"#,
Shape::Adapter,
);
assert!(secrets.taints("42"), "a SECRET value always joins");
}
#[test]
fn normalised_targets_and_error_logs_use_the_camel_case_keys() {
let (text, _) = redacted(
r#"{"authConfig":{"auth_header":{"Authorization":"Bearer S1"}},"connectionParams":{"curl":"c S2"}}"#,
Shape::Target,
);
assert_eq!(
text,
r#"{"authConfig":{"auth_header":{"Authorization":"[REDACTED]"}},"connectionParams":{"curl":"[REDACTED]"}}"#
);
let (text, _) = redacted(
r#"{"errorMessage":"m","targetObject":{"connection_params":{"request_headers":{"apikey":"S1"}}},"extraInfo":{"api_key":"S2","x":"y"}}"#,
Shape::ErrorLog,
);
assert_eq!(
text,
r#"{"errorMessage":"m","targetObject":{"connection_params":{"request_headers":{"apikey":"[REDACTED]"}}},"extraInfo":{"api_key":"[REDACTED]","x":"y"}}"#
);
}
#[test]
fn adapter_secret_values_are_always_redacted() {
let (text, secrets) = redacted(
r#"{"name":"a","scriptB64":"cHJpbnQ=","variables":[{"key":"endpoint","value":"http://e","type":"VAR","isRedacted":false},{"key":"api_token","value":"**********","type":"SECRET","isRedacted":true},{"key":"raw","value":"S1","type":"SECRET"},{"key":"gone","value":null,"type":"SECRET"}]}"#,
Shape::Adapter,
);
assert_eq!(
text,
r#"{"name":"a","scriptB64":"cHJpbnQ=","variables":[{"key":"endpoint","value":"http://e","type":"VAR","isRedacted":false},{"key":"api_token","value":"[REDACTED]","type":"SECRET","isRedacted":true},{"key":"raw","value":"[REDACTED]","type":"SECRET"},{"key":"gone","value":null,"type":"SECRET"}]}"#
);
assert!(secrets.taints("S1"));
}
#[test]
fn scheme_values_register_the_bare_credential() {
let (_, secrets) = redacted(
r#"{"auth_config":{"auth_header":{"Authorization":"Bearer S1-TOKEN"}}}"#,
Shape::Target,
);
assert!(secrets.taints("echo S1-TOKEN"));
assert_eq!(scheme_credential("Basic dXNlcjpw"), Some("dXNlcjpw"));
assert_eq!(scheme_credential("no-space"), None);
assert_eq!(
scheme_credential("Bearer x"),
None,
"too short to be a credential"
);
assert_eq!(scheme_credential("two words here"), Some("words here"));
}
#[test]
fn a_curl_command_registers_only_its_credentials() {
let curl = concat!(
r#"curl -sS -X POST 'https://chat.example.invalid/v1/chat/completions?api-version=2024-06-01&api_key=QK-SECRET-1' \"#,
"\n",
r#" -H 'Content-Type: application/json' -H "Authorization: Bearer TOK-SECRET-2" \"#,
"\n",
r#" --header=X-Api-Key:K3-SECRET -H 'X-Model: gpt-4o-mini' -u svc-user:PW-SECRET-4 \"#,
"\n",
r#" -b 'session=COOKIE-SECRET-5; consent=1' --oauth2-bearer OB-SECRET-6 \"#,
"\n",
r#" -d '{"model":"gpt-4o-mini","client_secret":"BODY-SECRET-7","messages":[{"role":"system","content":"You are Rest Target One, the fixture REST target"}]}'"#,
);
let document = format!(
r#"{{"name":"Rest Target One","connection_params":{{"curl":{}}}}}"#,
serde_json::to_string(curl).unwrap()
);
let (text, secrets) = redacted(&document, Shape::Target);
assert_eq!(
text,
r#"{"name":"Rest Target One","connection_params":{"curl":"[REDACTED]"}}"#
);
for secret in [
"Bearer TOK-SECRET-2",
"TOK-SECRET-2",
"QK-SECRET-1",
"K3-SECRET",
"PW-SECRET-4",
"COOKIE-SECRET-5",
"OB-SECRET-6",
"BODY-SECRET-7",
] {
assert!(secrets.taints(&format!("echo {secret}")), "{secret}");
}
assert!(
secrets.taints(&crate::error::base64_standard(b"svc-user:PW-SECRET-4")),
"the Basic credential -u forms"
);
for printable in [
"Rest Target One",
"the fixture REST target",
"gpt-4o-mini",
"chat.example.invalid",
"completions",
"2024-06-01",
"svc-user",
"application/json",
"curl -sS",
"1",
] {
assert!(!secrets.taints(printable), "{printable} is not withheld");
}
assert!(withheld(
&parse(r#"{"connection_params":{"curl":"[REDACTED]"}}"#).unwrap()
));
}
#[test]
fn urls_register_their_credentials_not_their_host_or_path() {
let (text, secrets) = redacted(
r#"{"name":"gpt-4o","connection_params":{"api_endpoint":"https://contoso.openai.invalid/openai/deployments/gpt-4o/chat/completions?api-version=2024-06-01&sig=SIG%2BSECRET#access_token=FRAG-SECRET",
"base_url":"https://apikey-as-user-0001:PASS-SECRET@h.invalid/p"}}"#,
Shape::Target,
);
assert_eq!(
text,
r#"{"name":"gpt-4o","connection_params":{"api_endpoint":"https://contoso.openai.invalid/openai/deployments/gpt-4o/chat/completions?[REDACTED]#[REDACTED]","base_url":"https://[REDACTED]@h.invalid/p"}}"#
);
for secret in [
"SIG%2BSECRET",
"SIG+SECRET",
"FRAG-SECRET",
"PASS-SECRET",
"apikey-as-user-0001",
] {
assert!(secrets.taints(&format!("x {secret} y")), "{secret}");
}
for printable in [
"gpt-4o",
"contoso",
"deployments",
"2024-06-01",
"h.invalid",
] {
assert!(!secrets.taints(printable), "{printable} is not withheld");
}
}
#[test]
fn shell_words_follow_posix_quoting() {
assert_eq!(
shell_words("curl -H 'A: b c' \"d\\\"e\" f\\ g \\\n h $'i\\'j'"),
["curl", "-H", "A: b c", "d\"e", "f g", "h", "i'j"]
);
assert_eq!(shell_words("a 'unterminated"), ["a", "unterminated"]);
}
#[test]
fn the_catch_all_rule_names() {
for key in [
"api_key",
"x-api-key",
"ApiKey",
"client_secret",
"access_token",
"Password",
"Authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"credentials",
"passwd_file",
"key",
] {
assert!(secret_key(key), "{key}");
}
for key in [
"response_key",
"response_stop_key",
"oauth2_token_response_key",
"name",
"keyboard",
"tokens_used",
"model_name",
] {
assert!(!secret_key(key), "{key}");
}
}
}