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())
}
pub(super) const CANONICAL_HEX_DIGEST_LENGTHS: [usize; 4] = [32, 40, 64, 128];
#[inline]
pub(super) fn is_canonical_hex_digest_length(len: usize) -> bool {
CANONICAL_HEX_DIGEST_LENGTHS.contains(&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
}
pub(crate) const HASH_ALGO_INTEGRITY_LABELS: &[&str] = &["sha512-", "sha384-", "sha256-"];
pub(crate) const HASH_ALGO_COLON_LABELS: &[&[u8]] = &[b"sha256:", b"sha512:", b"sha1:", b"md5:"];
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 {
matches!(credential.len(), 32 | 40 | 48 | 56 | 64 | 72 | 128) && is_uniform_hex(credential)
}
const AWS_IAM_ARN_PARTITIONS: [&str; 3] = ["aws", "aws-cn", "aws-us-gov"];
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)?.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)
}
fn aws_iam_arn_body_has_resource_target(body: &str) -> bool {
[
":role/",
":user/",
":group/",
":policy/",
":instance-profile/",
]
.iter()
.any(|&target| body.contains(target))
}
fn strip_hash_algo_prefix(credential: &str) -> Option<&str> {
let bytes = credential.as_bytes();
HASH_ALGO_COLON_LABELS
.iter()
.copied()
.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
}
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)
}
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 = starts_with_ignore_ascii_case(bytes, b"xxx")
|| starts_with_ignore_ascii_case(bytes, b"***");
if !starts_with_mask {
return false;
}
ci_find(bytes, b"1234567890") || ci_find(bytes, b"0123456789") || ci_find(bytes, b"abcdefgh")
}
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_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;