use std::sync::LazyLock;
use regex::Regex;
use super::patterns::Confidence;
pub(crate) const JS_SPACE_CLASS: &str =
r"\t\n\x0B\f\r \u{a0}\u{1680}\u{2000}-\u{200a}\u{2028}\u{2029}\u{202f}\u{205f}\u{3000}\u{feff}";
pub(crate) fn is_js_space(character: char) -> bool {
if matches!(character, '\u{2000}'..='\u{200a}') {
return true;
}
matches!(
character,
'\t' | '\n'
| '\u{b}'
| '\u{c}'
| '\r'
| ' '
| '\u{a0}'
| '\u{1680}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}
pub(crate) fn js_trim_start(text: &str) -> &str {
text.trim_start_matches(is_js_space)
}
pub(crate) fn js_trim_end(text: &str) -> &str {
text.trim_end_matches(is_js_space)
}
fn is_single_repeated_character(value: &str) -> bool {
let mut characters = value.chars();
let Some(first) = characters.next() else {
return false;
};
characters.all(|character| character == first) && value.chars().count() >= 2
}
pub(crate) fn looks_like_placeholder(value: &str) -> bool {
if value.contains("${") || value.contains("{{") {
return true;
}
if value.starts_with('<') && value.ends_with('>') {
return true;
}
value.encode_utf16().count() >= 4 && is_single_repeated_character(value)
}
pub(crate) fn confidence_by_length(value: &str, high_at: usize, medium_at: usize) -> Confidence {
let length = value.encode_utf16().count();
if length >= high_at {
return Confidence::High;
}
if length >= medium_at {
return Confidence::Medium;
}
Confidence::Low
}
static JWT_SHAPE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$")
.expect("a constant pattern compiles")
});
pub(crate) fn is_jwt_shaped(value: &str) -> bool {
JWT_SHAPE.is_match(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interpolations_and_angle_brackets_are_placeholders() {
assert!(looks_like_placeholder("${DATABASE_PASSWORD}"));
assert!(looks_like_placeholder("{{secret}}"));
assert!(looks_like_placeholder("<your-key-here>"));
}
#[test]
fn a_run_of_one_character_is_a_placeholder() {
assert!(looks_like_placeholder("xxxxxxxx"));
assert!(looks_like_placeholder("********"));
assert!(looks_like_placeholder("...."));
}
#[test]
fn a_short_run_is_not_a_placeholder() {
assert!(!looks_like_placeholder("xxx"));
assert!(!looks_like_placeholder("xx"));
}
#[test]
fn a_real_value_is_not_a_placeholder() {
assert!(!looks_like_placeholder("hunter2hunter2"));
assert!(!looks_like_placeholder("AKIAIOSFODNN7EXAMPLE"));
assert!(!looks_like_placeholder(""));
}
#[test]
fn confidence_tiers_on_length() {
assert_eq!(
confidence_by_length("a".repeat(32).as_str(), 32, 20),
Confidence::High
);
assert_eq!(
confidence_by_length("a".repeat(20).as_str(), 32, 20),
Confidence::Medium
);
assert_eq!(
confidence_by_length("a".repeat(19).as_str(), 32, 20),
Confidence::Low
);
}
#[test]
fn the_spelled_out_space_set_matches_the_class_the_engine_gets() {
let class = Regex::new(&format!("^[{JS_SPACE_CLASS}]$")).expect("compiles");
for code in 0..=0x3100_u32 {
let Some(character) = char::from_u32(code) else {
continue;
};
assert_eq!(
is_js_space(character),
class.is_match(&character.to_string()),
"U+{code:04X} is in one spelling of JavaScript's \\s and not the other"
);
}
}
#[test]
fn a_byte_order_mark_is_space_to_javascript_and_a_next_line_is_not() {
assert_eq!(js_trim_end(js_trim_start("\u{feff}abc\u{feff}")), "abc");
assert!(!"\u{feff}".trim().is_empty(), "Rust's own trim keeps it");
assert_eq!(js_trim_start("\u{85}abc"), "\u{85}abc");
assert!("\u{85}".trim().is_empty(), "Rust's own trim drops it");
assert_eq!(js_trim_end(js_trim_start(" abc\t\n")), "abc");
}
#[test]
fn only_a_base64_json_header_counts_as_a_jwt() {
assert!(is_jwt_shaped("eyJhbGciOi.eyJzdWIi.dozjgNryP4"));
assert!(!is_jwt_shaped("1.2.3"));
assert!(!is_jwt_shaped("my.host.name"));
assert!(!is_jwt_shaped("src.utils.helper"));
assert!(!is_jwt_shaped("abc.def.ghi"));
}
}