assemblyline_models/types/
ja4.rs

1
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/// Parse a set of sample JA4 fingerprints from https://github.com/FoxIO-LLC/ja4
49/// saved locally as ja4plus-mapping-2025-02-11.csv
50#[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}