use super::values::{
has_private_key, looks_like_credential, looks_like_header_name, looks_like_password,
registers_whole, scheme_credentials, scheme_words,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Class {
Plain,
Fingerprint,
HeaderMap,
Count,
Generic,
Credential,
Member,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Pattern {
Exact(&'static [&'static str]),
Prefix(&'static [&'static str]),
Suffix(&'static [&'static str]),
Joined(&'static [&'static str]),
Word(&'static [&'static str]),
Within(&'static [&'static str], &'static [&'static str]),
Last(&'static [&'static str]),
LastAfter(&'static [&'static str], &'static [&'static str]),
}
fn ends_with_words(name: &str, word: &str) -> bool {
name.strip_suffix(word)
.is_some_and(|rest| rest.is_empty() || rest.ends_with('_'))
}
impl Pattern {
#[cfg_attr(
not(test),
allow(dead_code, reason = "the harness's corpus generates names from it")
)]
pub const fn words(&self) -> &'static [&'static str] {
match self {
Self::Exact(words)
| Self::Prefix(words)
| Self::Suffix(words)
| Self::Joined(words)
| Self::Word(words)
| Self::Within(words, _)
| Self::Last(words)
| Self::LastAfter(words, _) => words,
}
}
pub fn matches(&self, name: &str) -> bool {
match self {
Self::Exact(words) => words.contains(&name),
Self::Prefix(words) => words.iter().any(|word| name.starts_with(word)),
Self::Suffix(words) => words.iter().any(|word| name.ends_with(word)),
Self::Joined(words) => {
let joined = name.replace('_', "");
words.iter().any(|word| joined.contains(word))
}
Self::Word(words) => name.split('_').any(|part| words.contains(&part)),
Self::Within(words, except) => {
let kept: String = name
.split('_')
.filter(|part| !except.contains(part))
.collect();
words.iter().any(|word| kept.contains(word))
}
Self::Last(words) => words.iter().any(|word| ends_with_words(name, word)),
Self::LastAfter(words, within) => words.iter().any(|word| {
name.strip_suffix(word)
.and_then(|rest| rest.strip_suffix('_'))
.is_some_and(|rest| {
let joined = rest.replace('_', "");
within.iter().any(|fragment| joined.contains(fragment))
})
}),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NameRule {
pub pattern: Pattern,
pub located: bool,
pub class: Class,
pub choices: &'static str,
pub reason: &'static str,
}
pub const LOCATION_NAMES: &[&str] = &["secret_key"];
pub const LOCATION_ANCHORS: &[&str] = &["secret_path", "secret_reference_id"];
pub const PRINTED_PREFIXES: &[&str] = &["masked_"];
pub const PRINTED_NAMES: &[&str] = &[
"api_key_id",
"api_key_ids",
"api_key_defaults_id",
"header_key",
"max_token_age",
"max_tokens",
"metadata_key",
"metadata_keys",
"rotate_auth",
"secret_mappings",
"secret_path",
"secret_reference_id",
"secrets_categories",
"total_tokens",
"unique_metadata_keys",
"unique_virtual_keys",
"value_key",
"virtual_key",
"virtual_keys",
];
pub const GENERIC_NAMES: &[&str] = &["key", "keys", "token"];
pub const CREDENTIAL_MEMBERS: &[&str] = &[
"vertex_service_account_json",
"oauth_metadata",
"model_config",
"auth_config",
];
pub const CREDENTIAL_IDS: &[&str] = &["secret_id"];
pub const URL_WORDS: &[&str] = &["url", "uri", "endpoint"];
pub const SECRET_URL_FRAGMENTS: &[&str] = &[
"secret",
"password",
"passwd",
"pwd",
"passphrase",
"passcode",
"credential",
"cookie",
"privatekey",
"connectionstring",
];
pub const FINGERPRINT_WORDS: &[&str] = &["prefix", "last4", "last8", "last_four", "hint"];
pub const METADATA_WORDS: &[&str] = &[
"count",
"counts",
"limit",
"limits",
"length",
"size",
"max",
"min",
"ttl",
"period",
"interval",
"duration",
"ms",
"seconds",
"minutes",
"hours",
"days",
"at",
"date",
"timestamp",
"expiry",
"expiration",
"expires",
"expires_in",
"enabled",
"disabled",
"required",
"type",
"types",
"kind",
"mode",
"format",
"name",
"names",
"id",
"ids",
"version",
"status",
"location",
"path",
"url",
"uri",
"endpoint",
];
pub const CREDENTIAL_NAMES: &[&str] = &[
"auth",
"jwt",
"bearer",
"auth_header",
"authheader",
"accesskey",
"masterkey",
"accountkey",
"shared_access_signature",
"sharedaccesssignature",
"sig",
"signature",
];
pub const CREDENTIAL_SUFFIXES: &[&str] = &["_key", "_keys", "_auth", "_jwt", "_signature"];
pub const CREDENTIAL_FRAGMENTS: &[&str] = &[
"secret",
"password",
"passwd",
"pwd",
"passphrase",
"passcode",
"credential",
"apikey",
"authorization",
"cookie",
"clientauth",
"authcode",
"privatekey",
"connectionstring",
];
pub const CREDENTIAL_WORDS: &[&str] = &["pass", "pat", "otp"];
pub const TOKEN_WORDS: &[&str] = &["token"];
pub const TOKEN_EXCEPTIONS: &[&str] = &["tokenizer", "tokenizers"];
pub const HEADER_MAPS: &[&str] = &["headers"];
pub const HEADER_MAP_SUFFIXES: &[&str] = &["_headers"];
pub const NAME_RULES: &[NameRule] = &[
NameRule {
pattern: Pattern::Exact(LOCATION_NAMES),
located: true,
class: Class::Plain,
choices: "164",
reason: "a key name inside the referenced secret, beside `secret_path` or `secret_reference_id`",
},
NameRule {
pattern: Pattern::Prefix(PRINTED_PREFIXES),
located: false,
class: Class::Plain,
choices: "ยง18.20 c5, 157",
reason: "the service's fingerprints",
},
NameRule {
pattern: Pattern::Exact(PRINTED_NAMES),
located: false,
class: Class::Plain,
choices: "157, 165, 170, 176, 188",
reason: "identifiers, references, text-capable counts, guardrail settings, key names, a request flag",
},
NameRule {
pattern: Pattern::Exact(GENERIC_NAMES),
located: false,
class: Class::Generic,
choices: "120, 141, 176",
reason: "a tag name or a word as often as a credential",
},
NameRule {
pattern: Pattern::Exact(CREDENTIAL_MEMBERS),
located: false,
class: Class::Member,
choices: "136",
reason: "the SDK's credential members",
},
NameRule {
pattern: Pattern::Last(CREDENTIAL_IDS),
located: false,
class: Class::Credential,
choices: "187",
reason: "a Vault AppRole SecretID, a credential that ends in `id`",
},
NameRule {
pattern: Pattern::LastAfter(URL_WORDS, SECRET_URL_FRAGMENTS),
located: false,
class: Class::Credential,
choices: "187",
reason: "a capability URL named after a stored secret",
},
NameRule {
pattern: Pattern::Last(FINGERPRINT_WORDS),
located: false,
class: Class::Fingerprint,
choices: "187",
reason: "a credential's fingerprint, withheld only where it looks like a credential",
},
NameRule {
pattern: Pattern::Last(METADATA_WORDS),
located: false,
class: Class::Plain,
choices: "187",
reason: "what describes a credential: a count, a lifetime, a timestamp, a type, a name, an id, a location",
},
NameRule {
pattern: Pattern::Exact(CREDENTIAL_NAMES),
located: false,
class: Class::Credential,
choices: "170, 181, 183, 187",
reason: "a credential named by its kind",
},
NameRule {
pattern: Pattern::Suffix(CREDENTIAL_SUFFIXES),
located: false,
class: Class::Credential,
choices: "157, 170, 176, 181, 187",
reason: "a key, an authentication, a token or a signature of something",
},
NameRule {
pattern: Pattern::Joined(CREDENTIAL_FRAGMENTS),
located: false,
class: Class::Credential,
choices: "131, 157, 170, 176, 187",
reason: "a credential word anywhere in the name",
},
NameRule {
pattern: Pattern::Word(CREDENTIAL_WORDS),
located: false,
class: Class::Credential,
choices: "176, 187",
reason: "a credential word that is ordinary inside a word",
},
NameRule {
pattern: Pattern::Within(TOKEN_WORDS, TOKEN_EXCEPTIONS),
located: false,
class: Class::Count,
choices: "163, 165, 171, 176",
reason: "a token or a token count",
},
NameRule {
pattern: Pattern::Exact(HEADER_MAPS),
located: false,
class: Class::HeaderMap,
choices: "138",
reason: "a header map",
},
NameRule {
pattern: Pattern::Suffix(HEADER_MAP_SUFFIXES),
located: false,
class: Class::HeaderMap,
choices: "138, 177",
reason: "a header map of something",
},
];
pub fn normalised(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
let mut out = String::with_capacity(name.len() + 4);
let mut pending = false;
for (at, &c) in chars.iter().enumerate() {
if !c.is_alphanumeric() {
pending = !out.is_empty();
continue;
}
if c.is_uppercase() && at > 0 {
let before = chars[at - 1];
let lower_after = chars.get(at + 1).is_some_and(|next| next.is_lowercase());
if before.is_lowercase()
|| before.is_ascii_digit()
|| (before.is_uppercase() && lower_after)
{
pending = !out.is_empty();
}
}
if pending {
out.push('_');
pending = false;
}
out.extend(c.to_lowercase());
}
out
}
pub fn rule_of(name: &str, located: bool) -> Option<&'static NameRule> {
let name = normalised(name);
NAME_RULES
.iter()
.find(|rule| (located || !rule.located) && rule.pattern.matches(&name))
}
pub fn member_class(name: &str, located: bool) -> Class {
rule_of(name, located).map_or(Class::Plain, |rule| rule.class)
}
pub fn classify(name: &str) -> Class {
member_class(name, false)
}
pub fn is_secret_location<'a>(names: impl IntoIterator<Item = &'a str>) -> bool {
names
.into_iter()
.any(|name| LOCATION_ANCHORS.contains(&normalised(name).as_str()))
}
pub fn is_credential_name(name: &str) -> bool {
classify(name) >= Class::Count
}
pub fn is_header_map(name: &str) -> bool {
classify(name) == Class::HeaderMap
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Shape {
Null,
Bool,
Number,
CredentialLike,
String,
TextFree,
Container,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Leaf {
Path,
Credential,
Generic,
Inner,
Describing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
Read,
Headers,
Keep,
Withhold(Leaf),
}
pub const fn action(class: Class, shape: Shape) -> Action {
match (class, shape) {
(Class::Plain, _) => Action::Read,
(Class::Fingerprint, Shape::CredentialLike) => Action::Withhold(Leaf::Credential),
(Class::Fingerprint, _) => Action::Read,
(_, Shape::Null | Shape::Bool) => Action::Keep,
(Class::HeaderMap, Shape::Number) => Action::Keep,
(Class::HeaderMap, _) => Action::Headers,
(Class::Count, Shape::Number | Shape::TextFree) => Action::Keep,
(Class::Count, _) => Action::Withhold(Leaf::Credential),
(Class::Generic, _) => Action::Withhold(Leaf::Generic),
(Class::Credential | Class::Member, _) => Action::Withhold(Leaf::Credential),
}
}
pub const SAFE_HEADERS: &[&str] = &[
"accept",
"accept_encoding",
"accept_language",
"cache_control",
"connection",
"content_encoding",
"content_length",
"content_type",
"host",
"user_agent",
"x_request_id",
"x_portkey_provider",
];
pub const HEADER_CREDENTIAL_WORDS: &[&str] = &[
"auth",
"authorization",
"key",
"keys",
"apikey",
"token",
"secret",
"password",
"passwd",
"pwd",
"cookie",
"credential",
"credentials",
"signature",
];
pub fn is_safe_header(name: &str) -> bool {
SAFE_HEADERS.contains(&normalised(name).as_str()) && !is_credential_name(name)
}
pub fn header_class(name: &str) -> Class {
if is_safe_header(name) {
return Class::Plain;
}
let class = classify(name);
if matches!(class, Class::Count | Class::Credential | Class::Member) {
class
} else if normalised(name)
.split('_')
.any(|word| HEADER_CREDENTIAL_WORDS.contains(&word))
{
Class::Credential
} else {
Class::Generic
}
}
#[cfg(test)]
pub fn is_credential_header(name: &str) -> bool {
matches!(
header_class(name),
Class::Count | Class::Credential | Class::Member
)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pair {
pub names: Vec<String>,
}
pub const PAIR_NAMES: &[&str] = &["key", "name", "header"];
pub const PAIR_VALUE: &str = "value";
impl Pair {
pub fn of<'a>(members: impl IntoIterator<Item = (&'a str, Option<&'a str>)>) -> Option<Self> {
let mut has_value = false;
let mut found: [Option<String>; PAIR_NAMES.len()] = Default::default();
for (member, text) in members {
if member == PAIR_VALUE {
has_value = true;
} else if let Some(at) = PAIR_NAMES.iter().position(|name| *name == member) {
found[at] = text.map(str::to_owned);
}
}
let names: Vec<String> = found.into_iter().flatten().collect();
(has_value && !names.is_empty()).then_some(Self { names })
}
pub fn names_member(&self, member: &str, text: Option<&str>) -> bool {
PAIR_NAMES.contains(&member)
&& text.is_some_and(|text| self.names.iter().any(|n| n == text))
}
pub fn class(&self) -> Class {
self.names
.iter()
.map(|name| header_class(name))
.max()
.unwrap_or(Class::Generic)
}
pub fn class_outside_headers(&self) -> Class {
match self.class() {
class @ (Class::Count | Class::Credential | Class::Member) => class,
_ => Class::Plain,
}
}
}
pub const fn header_action(class: Class, shape: Shape) -> Action {
match (class, shape) {
(Class::Generic, Shape::Null | Shape::Bool | Shape::Number) => Action::Keep,
(Class::Generic, _) => Action::Withhold(Leaf::Generic),
(Class::Count, _) => action(Class::Credential, shape),
_ => action(class, shape),
}
}
pub fn header_item_action(text: Option<&str>, shape: Shape) -> Action {
if text.is_some_and(looks_like_header_name) {
Action::Read
} else {
header_action(header_class(""), shape)
}
}
pub const MIN_REGISTERED: usize = 4;
pub const PLACEHOLDER_WORDS: &[&str] = &[
"true",
"false",
"null",
"none",
"nil",
"undefined",
"empty",
"unset",
"redacted",
"[redacted]",
];
pub const MODE_WORDS: &[&str] = &[
"enabled",
"disabled",
"required",
"optional",
"mandatory",
"anonymous",
"unauthenticated",
"inherited",
"clientcredentials",
"oauth2clientcredentials",
"authorizationcode",
"oauth2authorizationcode",
"refreshtoken",
"devicecode",
"jwtbearer",
"implicit",
"oauth2implicit",
];
pub fn is_mode_word(text: &str) -> bool {
let trimmed = text.trim();
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return false;
}
let folded: String = trimmed
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect();
MODE_WORDS.contains(&folded.as_str())
}
pub const MODE_MEMBER_WORDS: &[&str] = &["type", "mode"];
pub fn mode_values<'a>(
members: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
) -> Vec<&'a str> {
members
.into_iter()
.filter(|(name, _)| Pattern::Last(MODE_MEMBER_WORDS).matches(&normalised(name)))
.filter_map(|(_, text)| text)
.collect()
}
pub fn repeats_a_mode(text: &str, modes: &[&str]) -> bool {
text.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
&& is_credential_name(text)
&& modes.iter().any(|mode| mode.eq_ignore_ascii_case(text))
}
pub const MASK_CHARACTERS: &[char] = &['*', 'โข', 'โ', 'ยท', 'x', 'X', '#'];
pub fn registrable(text: &str) -> bool {
let trimmed = text.trim();
trimmed.chars().count() >= MIN_REGISTERED
&& !text.contains(super::WITHHELD)
&& !PLACEHOLDER_WORDS
.iter()
.any(|word| trimmed.eq_ignore_ascii_case(word))
&& !is_mode_word(trimmed)
&& !trimmed.chars().all(|c| MASK_CHARACTERS.contains(&c))
}
pub fn registered(leaf: Leaf, text: &str) -> Vec<String> {
let whole = has_private_key(text)
|| match leaf {
Leaf::Path => true,
Leaf::Credential => registers_whole(text),
Leaf::Generic | Leaf::Describing => looks_like_credential(text),
Leaf::Inner => looks_like_credential(text) || looks_like_password(text),
};
let mut out = Vec::new();
if whole {
out.push(text.to_owned());
}
if leaf == Leaf::Describing {
return out;
}
let mut words: Vec<&str> = text
.split_whitespace()
.filter(|word| *word != text && looks_like_credential(word))
.collect();
let schemes = if matches!(leaf, Leaf::Path | Leaf::Credential) {
scheme_words(text)
} else {
scheme_credentials(text)
};
for word in schemes {
if word != text && !words.contains(&word) {
words.push(word);
}
}
out.extend(words.into_iter().map(str::to_owned));
out.retain(|found| registrable(found));
out
}
pub fn registered_number(leaf: Leaf, text: &str) -> Option<String> {
let enough = match leaf {
Leaf::Path | Leaf::Credential => text.chars().count() >= super::values::PASSWORD_LOOK,
Leaf::Generic | Leaf::Inner => looks_like_credential(text),
Leaf::Describing => false,
};
(enough && registrable(text)).then(|| text.to_owned())
}
pub const SCHEMA_TYPES: &[&str] = &[
"array", "boolean", "integer", "null", "number", "object", "string",
];
pub const SCHEMA_KEYWORDS: &[&str] = &[
"$comment",
"$id",
"$ref",
"$schema",
"contentEncoding",
"contentMediaType",
"deprecated",
"description",
"encrypted",
"exclusiveMaximum",
"exclusiveMinimum",
"format",
"hidden",
"label",
"maxItems",
"maxLength",
"maxProperties",
"maximum",
"minItems",
"minLength",
"minProperties",
"minimum",
"multipleOf",
"nullable",
"pattern",
"readOnly",
"required",
"title",
"type",
"uniqueItems",
"writeOnly",
];
pub const SCHEMA_VALUE_KEYWORDS: &[&str] = &["const", "default", "enum", "examples"];
pub const SUBSCHEMA_KEYWORDS: &[&str] = &[
"additionalProperties",
"allOf",
"anyOf",
"contains",
"else",
"if",
"items",
"not",
"oneOf",
"then",
];
pub const AUDIT_FIELDS: &[&str] = &["request_body", "request_headers", "query_params"];
#[cfg(test)]
pub fn rules_table() -> String {
let class = |class: Class| match class {
Class::Plain => "printed",
Class::Fingerprint => "fingerprint",
Class::HeaderMap => "header map",
Class::Count => "count",
Class::Generic => "generic",
Class::Credential => "credential",
Class::Member => "credential member",
};
let mut out = String::from(
"| # | A normalised name that | Class | Choices | Why |\n|---|---|---|---|---|\n",
);
for (at, rule) in NAME_RULES.iter().enumerate() {
let words = rule
.pattern
.words()
.iter()
.map(|word| format!("`{word}`"))
.collect::<Vec<_>>()
.join(", ");
let matches = match rule.pattern {
Pattern::Exact(_) => format!("is {words}"),
Pattern::Prefix(_) => format!("starts with {words}"),
Pattern::Suffix(_) => format!("ends with {words}"),
Pattern::Joined(_) => format!("contains, with `_` removed, {words}"),
Pattern::Word(_) => format!("has a `_` word that is {words}"),
Pattern::Within(_, except) => {
let except = except
.iter()
.map(|word| format!("`{word}`"))
.collect::<Vec<_>>()
.join(", ");
format!("contains, with the `_` words {except} and `_` removed, {words}")
}
Pattern::Last(_) => format!("ends with the `_` word(s) {words}"),
Pattern::LastAfter(_, within) => {
let within = within
.iter()
.map(|word| format!("`{word}`"))
.collect::<Vec<_>>()
.join(", ");
format!(
"ends with the `_` word {words} after a part that, with `_` removed, contains {within}"
)
}
};
let place = if rule.located {
" in a secret mapping or reference"
} else {
""
};
out.push_str(&format!(
"| {} | {matches}{place} | {} | {} | {} |\n",
at + 1,
class(rule.class),
rule.choices,
rule.reason
));
}
out.push_str(&format!(
"| {} | matches no row | printed | | |\n",
NAME_RULES.len() + 1
));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_normalised_across_case_and_separators() {
for (name, expected) in [
("clientSecret", "client_secret"),
("awsSessionToken", "aws_session_token"),
("webhookURL", "webhook_url"),
("X-API-Key", "x_api_key"),
("APIKey", "api_key"),
("oauth2Token", "oauth2_token"),
("SESSION_TOKEN", "session_token"),
("Content-Type", "content_type"),
("API KEY", "api_key"),
("api.key", "api_key"),
("Api Key", "api_key"),
(" api -- key ", "api_key"),
("_secret", "secret"),
("metadata.key", "metadata_key"),
("x/api:key", "x_api_key"),
] {
assert_eq!(normalised(name), expected, "{name}");
}
}
#[test]
fn the_table_classifies_every_kind_of_name() {
for (name, class) in [
("region", Class::Plain),
("key_id", Class::Plain),
("tokenizer", Class::Plain),
("model_tokenizers", Class::Plain),
("passthrough", Class::Plain),
("passthrough_header", Class::Plain),
("compass", Class::Plain),
("bypass", Class::Plain),
("rules_passed", Class::Plain),
("path", Class::Plain),
("data_pattern", Class::Plain),
("virtual_keys", Class::Plain),
("api_key_ids", Class::Plain),
("monkey", Class::Plain),
("auth_type", Class::Plain),
("auth_settings", Class::Plain),
("authority", Class::Plain),
("masked_api_key", Class::Plain),
("api_key_id", Class::Plain),
("headerKey", Class::Plain),
("metadataKey", Class::Plain),
("virtual_key", Class::Plain),
("rotate_auth", Class::Plain),
("custom_headers", Class::HeaderMap),
("extra_headers", Class::HeaderMap),
("forwardHeaders", Class::HeaderMap),
("prompt_tokens", Class::Count),
("access_token", Class::Count),
("bearertoken", Class::Count),
("tokenvalue", Class::Count),
("tokenhash", Class::Count),
("accesstokenvalue", Class::Count),
("TOKENVALUE", Class::Count),
("TOKENvalue", Class::Count),
("ToKen", Class::Count),
("key", Class::Generic),
("keys", Class::Generic),
("token", Class::Generic),
("model_config", Class::Member),
("auth", Class::Credential),
("pwd", Class::Credential),
("db_pwd", Class::Credential),
("passphrase", Class::Credential),
("keyPassphrase", Class::Credential),
("account_key", Class::Credential),
("master_key", Class::Credential),
("encryption_key", Class::Credential),
("API KEY", Class::Credential),
("api.key", Class::Credential),
("basic_auth", Class::Credential),
("secret_key", Class::Credential),
("clientSecret", Class::Credential),
("adminpassword", Class::Credential),
("secret_tokens", Class::Credential),
("db_pass", Class::Credential),
("smtpPass", Class::Credential),
("DB_PASS", Class::Credential),
("pass", Class::Credential),
("GITHUB_PAT", Class::Credential),
("access_keys", Class::Credential),
("secretKeys", Class::Credential),
("connectionString", Class::Credential),
("AZURE_STORAGE_CONNECTION_STRING", Class::Credential),
] {
assert_eq!(classify(name), class, "{name}");
}
assert_eq!(member_class("secret_key", true), Class::Plain);
assert!(is_secret_location(["target_field", "secretReferenceId"]));
}
#[test]
fn the_action_table_prints_only_what_carries_no_credential() {
use Shape::*;
for class in [
Class::Count,
Class::Generic,
Class::Credential,
Class::Member,
] {
assert_eq!(action(class, Null), Action::Keep);
assert_eq!(action(class, Bool), Action::Keep);
assert!(matches!(action(class, String), Action::Withhold(_)));
assert!(matches!(action(class, CredentialLike), Action::Withhold(_)));
assert!(matches!(action(class, Container), Action::Withhold(_)));
}
for class in [Class::Credential, Class::Member] {
assert_eq!(
action(class, Number),
Action::Withhold(Leaf::Credential),
"{class:?}"
);
assert_eq!(
header_action(class, Number),
Action::Withhold(Leaf::Credential),
"{class:?}"
);
}
assert_eq!(
action(Class::Generic, Number),
Action::Withhold(Leaf::Generic)
);
for class in [Class::HeaderMap, Class::Count] {
assert_eq!(action(class, Number), Action::Keep, "{class:?}");
}
assert_eq!(header_action(Class::Generic, Number), Action::Keep);
assert_eq!(
header_action(Class::Count, Number),
Action::Withhold(Leaf::Credential)
);
assert_eq!(
header_action(Class::Count, TextFree),
Action::Withhold(Leaf::Credential)
);
assert_eq!(action(Class::Plain, Number), Action::Read);
assert_eq!(action(Class::Fingerprint, Number), Action::Read);
assert_eq!(action(Class::Fingerprint, String), Action::Read);
assert_eq!(
action(Class::Fingerprint, CredentialLike),
Action::Withhold(Leaf::Credential)
);
assert_eq!(action(Class::Count, TextFree), Action::Keep);
for class in [Class::Generic, Class::Credential, Class::Member] {
assert!(matches!(action(class, TextFree), Action::Withhold(_)));
}
assert_eq!(action(Class::Plain, String), Action::Read);
assert_eq!(action(Class::HeaderMap, Container), Action::Headers);
}
#[test]
fn headers_and_pairs_take_the_strongest_name() {
assert_eq!(header_class("Accept"), Class::Plain);
assert_eq!(header_class("x-env"), Class::Generic);
assert_eq!(header_class("X-Functions-Key"), Class::Credential);
assert_eq!(header_class("token"), Class::Credential);
assert_eq!(header_class("x-amz-security-token"), Class::Count);
assert!(!is_credential_header("x-portkey-workspace"));
let pair =
|members: &[(&'static str, Option<&'static str>)]| Pair::of(members.iter().copied());
let env = pair(&[("name", Some("OPENAI_API_KEY")), ("value", None)]).unwrap();
assert_eq!(env.class_outside_headers(), Class::Credential);
let tag = pair(&[("key", Some("region")), ("value", Some("us"))]).unwrap();
assert_eq!(tag.class_outside_headers(), Class::Plain);
let both = pair(&[
("key", Some("accept")),
("name", Some("Authorization")),
("value", None),
])
.unwrap();
assert_eq!(both.class(), Class::Credential);
let header = pair(&[("header", Some("Authorization")), ("value", None)]).unwrap();
assert_eq!(header.class_outside_headers(), Class::Credential);
assert!(header.names_member("header", Some("Authorization")));
let keys = pair(&[("key", Some("keys")), ("value", None)]).unwrap();
assert_eq!(keys.class_outside_headers(), Class::Credential);
assert!(pair(&[("name", Some("n"))]).is_none());
assert!(pair(&[("name", None), ("value", None)]).is_none());
assert_eq!(
header_action(Class::Generic, Shape::String),
Action::Withhold(Leaf::Generic)
);
assert_eq!(header_action(Class::Generic, Shape::Number), Action::Keep);
assert_eq!(
header_action(Class::Credential, Shape::Number),
Action::Withhold(Leaf::Credential)
);
assert_eq!(header_action(Class::Plain, Shape::Bool), Action::Read);
assert_eq!(
header_item_action(Some("x-trace"), Shape::String),
Action::Read
);
for item in ["sk-proj-ab12cd34ef56gh78", "hunter2x", "Bearer x", ""] {
assert_eq!(
header_item_action(Some(item), Shape::String),
Action::Withhold(Leaf::Generic),
"{item}"
);
}
}
#[test]
fn registration_follows_the_leaf() {
assert_eq!(
registered(Leaf::Credential, "Bearer hunter2x"),
["Bearer hunter2x", "hunter2x"]
);
assert!(registered(Leaf::Generic, "prod").is_empty());
assert_eq!(registered(Leaf::Inner, "hunter2x!"), ["hunter2x!"]);
let pem = "-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----";
assert_eq!(registered(Leaf::Describing, pem), [pem]);
assert_eq!(
registered_number(Leaf::Credential, "12345678"),
Some("12345678".into())
);
assert_eq!(registered_number(Leaf::Credential, "2026"), None);
assert_eq!(registered_number(Leaf::Generic, "12345678"), None);
}
#[test]
fn placeholder_words_and_masks_are_never_registered() {
for text in [
"none",
"None",
"NULL",
"true",
" none ",
"****",
"โขโขโขโขโขโขโขโข",
"xxxxxxxx",
"abc",
"[withheld]",
"Bearer [withheld]",
"[REDACTED]",
] {
assert!(!registrable(text), "{text}");
assert!(registered(Leaf::Credential, text).is_empty(), "{text}");
}
for text in ["hunter", "sk-****abcd", "nonexistent", "prod"] {
assert!(registrable(text), "{text}");
}
assert_eq!(registered(Leaf::Credential, "Token none"), ["Token none"]);
}
#[test]
fn mode_words_are_never_registered() {
for text in [
"disabled",
"Disabled",
" required ",
"optional",
"anonymous",
"oauth2_client_credentials",
"client-credentials",
"CLIENT_CREDENTIALS",
"authorization_code",
] {
assert!(!registrable(text), "{text}");
assert!(registered(Leaf::Credential, text).is_empty(), "{text}");
assert!(registered(Leaf::Path, text).is_empty(), "{text}");
}
for text in ["disabled!", "disabled2x", "password", "dragonfly"] {
assert!(registrable(text), "{text}");
}
let modes = mode_values([
("auth_type", Some("password")),
("mode", Some("api_key")),
("status", Some("hunter2x")),
("region", Some("dragonfly")),
("type", None),
]);
assert_eq!(modes, ["password", "api_key"]);
assert!(repeats_a_mode("password", &modes));
assert!(repeats_a_mode("PASSWORD", &modes));
assert!(repeats_a_mode("api_key", &modes));
assert!(!repeats_a_mode("password", &[]));
assert!(
!repeats_a_mode("hunter2x", &["hunter2x"]),
"names no credential"
);
assert!(!repeats_a_mode("dragonfly", &modes));
}
#[test]
fn key_name_lists_are_printed() {
for name in [
"metadataKeys",
"metadata_keys",
"unique_metadata_keys",
"unique_virtual_keys",
] {
assert_eq!(classify(name), Class::Plain, "{name}");
}
for name in ["unique_api_keys", "api_keys", "access_keys"] {
assert_eq!(classify(name), Class::Credential, "{name}");
}
}
#[test]
fn metadata_names_describe_a_credential() {
for name in [
"user_api_key_count",
"userApiKeyMaxTtl",
"user_api_key_rotation_period",
"token_expires_at",
"secret_updated_at",
"auth_token_url",
] {
assert_eq!(classify(name), Class::Plain, "{name}");
}
for name in [
"api_key_last_four",
"api_key_hint",
"token_prefix",
"apiKeyLast8",
"secret_last4",
] {
assert_eq!(classify(name), Class::Fingerprint, "{name}");
assert!(!is_credential_name(name), "{name}");
}
for (name, class) in [
("secret_id", Class::Credential),
("vault_secret_id", Class::Credential),
("secret_url", Class::Credential),
("db_password_uri", Class::Credential),
("api_key", Class::Credential),
("user_api_key", Class::Credential),
("x_api_key", Class::Credential),
("api_key_value", Class::Credential),
("format_token", Class::Count),
("id_token", Class::Count),
("atlas_secret", Class::Credential),
] {
assert_eq!(classify(name), class, "{name}");
}
assert!(Pattern::Last(&["at"]).matches("expires_at"));
assert!(!Pattern::Last(&["at"]).matches("format"));
assert!(Pattern::Last(&["last_four"]).matches("api_key_last_four"));
assert!(!Pattern::Last(&["last_four"]).matches("api_key_four"));
assert!(Pattern::LastAfter(&["url"], &["secret"]).matches("client_secret_url"));
assert!(!Pattern::LastAfter(&["url"], &["secret"]).matches("secret_url_name"));
assert!(!Pattern::LastAfter(&["url"], &["secret"]).matches("url"));
}
#[test]
fn a_credential_name_registers_only_credential_shaped_values() {
for text in [
"Bearer",
"hunter2",
"secret",
"2026-10-19T00:00:00.000Z",
"2026-10-19T00:00:00+05:00",
] {
assert!(registered(Leaf::Credential, text).is_empty(), "{text}");
assert_eq!(registered(Leaf::Path, text), [text], "{text}");
}
for text in ["********", "[REDACTED]", "undefined", " none "] {
assert!(registered(Leaf::Credential, text).is_empty(), "{text}");
}
for text in [
"dragonfly",
"sunshine",
"hunter2x",
"p@ss",
"correct horse battery",
"sk-proj-0123456789abcdef",
] {
assert_eq!(registered(Leaf::Credential, text)[0], text, "{text}");
}
}
}