assemblyline_models/types/
ja4.rs1
2use std::sync::OnceLock;
3
4use serde_with::{DeserializeFromStr, SerializeDisplay};
5
6use struct_metadata::Described;
7
8use crate::ElasticMeta;
9
10
11#[derive(SerializeDisplay, DeserializeFromStr, Debug, Default, Described, Clone)]
12#[metadata_type(ElasticMeta)]
13pub struct JA4(String);
14
15impl std::fmt::Display for JA4 {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 f.write_str(&self.0)
18 }
19}
20
21static JA4_REGEX: OnceLock<regex::Regex> = OnceLock::new();
22
23pub fn is_ja4(value: &str) -> bool {
24 let regex = JA4_REGEX.get_or_init(|| {
25 regex::Regex::new(r"(t|q)([sd]|[0-3]){2}(d|i)\d{2}\d{2}\w{2}_[a-f0-9]{12}_[a-f0-9]{12}").unwrap()
26 });
27
28 regex.is_match(value)
29}
30
31impl std::str::FromStr for JA4 {
32 type Err = BadJA4;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 if is_ja4(s) {
36 Ok(Self(s.to_owned()))
37 } else {
38 Err(BadJA4(s.to_owned()))
39 }
40 }
41}
42
43
44#[derive(thiserror::Error, Debug)]
45#[error("Invalid JA4 fingerprint ({0})")]
46pub struct BadJA4(String);
47
48#[test]
51fn load_sample_ja4_fingerprints() {
52 let data = include_str!("ja4plus-mapping-2025-02-11.csv");
53 let mut hits = 0;
54 for row in data.split("\n").skip(1) {
55 for entry in row.split(",").skip(4).take(1) {
56 if entry.is_empty() { continue }
57 let _value: JA4 = entry.parse().unwrap();
58 hits += 1;
59 }
60 }
61 assert_eq!(hits, 30);
62}