pub(crate) const RFC7519_EXAMPLE_JWT_PREFIX: &str =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkw";
pub(crate) fn looks_like_dashed_serial_key(credential: &str) -> bool {
is_five_by_five_dash_shape(credential, |b| b.is_ascii_alphanumeric())
}
#[inline]
pub(super) fn is_canonical_hex_digest_length(len: usize) -> bool {
crate::hex_digest_policy::is_canonical_length(len)
}
pub(crate) fn is_structured_dotted_token(value: &str) -> bool {
if !value.contains('.') {
return false;
}
let mut parts = value.split('.');
let (Some(first), Some(second), Some(third), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
let segments = [first, second, third];
let is_jwt_like = crate::jwt::has_jwt_header_prefix(first)
&& segments.iter().all(|segment| {
segment.len() >= 4 && segment.bytes().all(crate::decode::is_base64_candidate_byte)
});
let is_discord_style = (23..=28).contains(&first.len())
&& (6..=8).contains(&second.len())
&& (27..=38).contains(&third.len())
&& segments.iter().all(|segment| {
segment
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
});
is_jwt_like || is_discord_style
}
crate::tier_b_list::tier_b_vec!(
pub(crate) HASH_ALGO_INTEGRITY_LABELS,
"hash-algo-labels.toml",
integrity_labels
);
pub(crate) static HASH_ALGO_COLON_LABELS: std::sync::LazyLock<Vec<Vec<u8>>> =
std::sync::LazyLock::new(|| {
#[derive(serde::Deserialize)]
struct HashAlgoLabels {
colon_labels: Vec<String>,
}
let raw = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/rules/hash-algo-labels.toml"
));
let parsed: HashAlgoLabels = toml::from_str(raw).unwrap_or_else(|error| {
panic!(
"rules/hash-algo-labels.toml is invalid: {error}. \
Fix the bundled Tier-B data file."
)
});
assert!(
!parsed.colon_labels.is_empty(),
"rules/hash-algo-labels.toml colon_labels is empty; refusing to run without the Tier-B list it owns."
);
parsed
.colon_labels
.into_iter()
.map(|s| s.into_bytes())
.collect()
});
pub(super) fn is_five_by_five_dash_shape(value: &str, body_byte_ok: impl Fn(u8) -> bool) -> bool {
let bytes = value.as_bytes();
if bytes.len() != 29 {
return false;
}
bytes.iter().enumerate().all(|(idx, &byte)| {
if matches!(idx, 5 | 11 | 17 | 23) {
byte == b'-'
} else {
body_byte_ok(byte)
}
})
}
pub(crate) fn looks_like_prefixed_hash_digest(credential: &str) -> bool {
let Some(body) = strip_hash_algo_prefix(credential) else {
return false;
};
(is_canonical_hex_digest_length(body.len()) && is_uniform_hex(body))
|| looks_like_base64_integrity_body(body)
}
pub(crate) fn looks_like_bare_hex_digest(credential: &str) -> bool {
crate::hex_digest_policy::is_bare_digest_length(credential.len()) && is_uniform_hex(credential)
}
crate::tier_b_list::tier_b_vec!(
AWS_IAM_ARN_PARTITIONS,
"hash-algo-labels.toml",
aws_iam_arn_partitions
);
fn strip_aws_iam_arn_body(value: &str, require_arn: bool) -> Option<&str> {
let rest = match value.strip_prefix("arn:") {
Some(rest) if require_arn => rest,
None if !require_arn => value,
_ => return None,
};
AWS_IAM_ARN_PARTITIONS.iter().find_map(|partition| {
rest.strip_prefix(partition.as_str())?
.strip_prefix(":iam::")
})
}
pub(crate) fn looks_like_aws_iam_arn(value: &str) -> bool {
strip_aws_iam_arn_body(value, true).is_some_and(aws_iam_arn_body_has_resource_target)
}
pub(crate) fn looks_like_trimmed_aws_iam_arn(value: &str) -> bool {
strip_aws_iam_arn_body(value, false).is_some_and(aws_iam_arn_body_has_resource_target)
}
crate::tier_b_list::tier_b_vec!(
AWS_IAM_ARN_RESOURCE_TARGETS,
"hash-algo-labels.toml",
aws_iam_arn_resource_targets
);
fn aws_iam_arn_body_has_resource_target(body: &str) -> bool {
AWS_IAM_ARN_RESOURCE_TARGETS
.iter()
.any(|target| body.contains(target.as_str()))
}
fn strip_hash_algo_prefix(credential: &str) -> Option<&str> {
let bytes = credential.as_bytes();
HASH_ALGO_COLON_LABELS
.iter()
.map(|v| v.as_slice())
.chain(
HASH_ALGO_INTEGRITY_LABELS
.iter()
.map(|label| label.as_bytes()),
)
.find_map(|label| {
crate::ascii_ci::ci_find_at(bytes, label).map(|idx| &credential[idx + label.len()..])
})
}
pub(crate) fn looks_like_base64_integrity_body(s: &str) -> bool {
if s.len() < 40 {
return false;
}
crate::decode::standard_base64_shape(s).is_some()
}
pub(crate) fn looks_like_standard_base64_blob(credential: &str) -> bool {
crate::decode_structure::is_random_base64_blob(credential, 40, 80, 32)
}
pub(crate) fn looks_like_random_byte_base64_blob(value: &str) -> bool {
if !(40..=80).contains(&value.len()) {
return false;
}
if value.bytes().any(|b| matches!(b, b'+' | b'/')) {
return false;
}
if !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'=')
{
return false;
}
let structure = crate::decode_structure::analyze(value);
if !structure.decodable {
return false;
}
if structure.magic.is_some() {
return true;
}
structure.printable_ratio < 0.85
}
pub(super) fn is_uniform_hex(s: &str) -> bool {
let bytes = s.as_bytes();
if bytes.is_empty() {
return false;
}
hex_bytes_are_uniform_case(bytes, &[])
}
fn hex_bytes_are_uniform_case(bytes: &[u8], skipped_positions: &[usize]) -> bool {
let mut saw_lower = false;
let mut saw_upper = false;
for (index, &b) in bytes.iter().enumerate() {
if skipped_positions.contains(&index) {
continue;
}
match b {
b'0'..=b'9' => {}
b'a'..=b'f' => saw_lower = true,
b'A'..=b'F' => saw_upper = true,
_ => return false,
}
}
!(saw_lower && saw_upper)
}
pub(crate) fn is_uuid_v4_shape(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 36 {
return false;
}
if b[8] != b'-' || b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
return false;
}
hex_bytes_are_uniform_case(b, &[8, 13, 18, 23])
}
pub(crate) fn looks_like_truncated_uuid_v4_suffix(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 34 {
return false;
}
if b[6] != b'-' || b[11] != b'-' || b[16] != b'-' || b[21] != b'-' {
return false;
}
if b[12] != b'4' {
return false;
}
if !matches!(b[17], b'8' | b'9' | b'a' | b'b' | b'A' | b'B') {
return false;
}
hex_bytes_are_uniform_case(b, &[6, 11, 16, 21])
}
const TEMPLATE_PLACEHOLDER_MAX_LEN: usize = 80;
pub(crate) fn looks_like_bracketed_template_placeholder(value: &str) -> bool {
let bracketed = (value.starts_with('{') && value.ends_with('}'))
|| (value.starts_with('<') && value.ends_with('>'))
|| (value.starts_with("${") && value.ends_with('}'));
bracketed && value.len() <= TEMPLATE_PLACEHOLDER_MAX_LEN
}
pub(crate) fn looks_like_shell_variable_reference(value: &str) -> bool {
let Some(name) = value.strip_prefix('$') else {
return false;
};
let mut bytes = name.bytes();
let Some(first) = bytes.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == b'_')
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}
fn byte_runs(bytes: &[u8]) -> impl Iterator<Item = (u8, usize)> + '_ {
let mut i = 0;
std::iter::from_fn(move || {
let b = *bytes.get(i)?;
let mut run = 1usize;
while i + run < bytes.len() && bytes[i + run] == b {
run += 1;
}
i += run;
Some((b, run))
})
}
pub(crate) fn has_three_or_more_consecutive_identical(s: &str) -> bool {
byte_runs(s.as_bytes()).any(|(_, run)| run >= 3)
}
crate::tier_b_list::tier_b_vec!(MASK_PREFIXES, "mask-sequences.toml", mask_prefixes);
crate::tier_b_list::tier_b_vec!(MASK_DIGIT_RUNS, "mask-sequences.toml", digit_runs);
crate::tier_b_list::tier_b_vec!(MASK_ALPHA_RUNS, "mask-sequences.toml", alpha_runs);
pub(crate) fn looks_like_prefixed_masked_sequence(body: &str) -> bool {
if body.ends_with("...") || body.ends_with('…') {
return true;
}
use crate::ascii_ci::{ci_find, starts_with_ignore_ascii_case};
let bytes = body.as_bytes();
let starts_with_mask = MASK_PREFIXES
.iter()
.any(|p| starts_with_ignore_ascii_case(bytes, p.as_bytes()));
if !starts_with_mask {
return false;
}
MASK_DIGIT_RUNS
.iter()
.any(|run| ci_find(bytes, run.as_bytes()))
|| MASK_ALPHA_RUNS
.iter()
.any(|run| ci_find(bytes, run.as_bytes()))
}
pub(crate) fn has_repeated_block_mask(s: &str) -> bool {
let bytes = s.as_bytes();
let long_runs = byte_runs(bytes)
.filter(|&(b, run)| run >= 4 && b.is_ascii_alphanumeric())
.take(3)
.count();
long_runs >= 3 || has_repeated_full_block(bytes)
}
fn has_repeated_full_block(bytes: &[u8]) -> bool {
if bytes.len() < 24 || !bytes.iter().any(|b| b.is_ascii_alphanumeric()) {
return false;
}
let max_block_len = (bytes.len() / 2).min(64);
for block_len in 3..=max_block_len {
let first = &bytes[..block_len];
if first.iter().all(|b| !b.is_ascii_alphanumeric()) {
continue;
}
if bytes[block_len..]
.iter()
.enumerate()
.all(|(index, byte)| *byte == first[index % block_len])
{
return true;
}
}
false
}
pub(crate) fn has_n_or_more_consecutive_identical(s: &str, n: usize) -> bool {
byte_runs(s.as_bytes()).any(|(b, run)| run >= n && b != b'-')
}
pub(crate) fn is_single_byte_mask(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() >= 4 && bytes[0] != b'-' && bytes.iter().all(|byte| *byte == bytes[0])
}
pub(crate) fn is_dash_segmented_alnum_decoy(value: &str) -> bool {
let randomness = crate::suppression::token_randomness::TokenRandomness::for_candidate(value);
is_dash_segmented_alnum_decoy_with_randomness(value, &randomness)
}
pub(crate) fn is_dash_segmented_alnum_decoy_with_randomness(
value: &str,
randomness: &crate::suppression::token_randomness::TokenRandomness<'_>,
) -> bool {
if !value.contains('-') {
return false;
}
if !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
{
return false;
}
let mut group_count = 0usize;
let mut all_len5_upperdigit = true;
let mut all_alpha = true;
for group in value.split('-') {
if group.is_empty() {
return false;
}
group_count += 1;
if group.len() != 5
|| !group
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
{
all_len5_upperdigit = false;
}
if !group.bytes().all(|b| b.is_ascii_alphabetic()) {
all_alpha = false;
}
}
if group_count >= 3 && all_len5_upperdigit {
return true;
}
group_count >= 3 && all_alpha && !randomness.is_random_token(value)
}
#[cfg(test)]
#[path = "../../../tests/unit/suppression_shape_canonical.rs"]
mod tests;