diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 767fb596..8c1b1c30 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -109,6 +109,10 @@ jobs:
- name: monitor-the-situation-dashboard-frontend
type: biome
path: PROJECTS/advanced/monitor-the-situation-dashboard/frontend
+ # Frontend (biome + stylelint + tsc)
+ - name: ja3-ja4-tls-fingerprinting-frontend
+ type: frontend
+ path: PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend
# Go
- name: simple-vulnerability-scanner
type: go
@@ -142,6 +146,9 @@ jobs:
- name: binary-analysis-tool-backend
type: rust
path: PROJECTS/intermediate/binary-analysis-tool/backend
+ - name: ja3-ja4-tls-fingerprinting
+ type: rust
+ path: PROJECTS/intermediate/ja3-ja4-tls-fingerprinting
# Crystal
- name: credential-rotation-enforcer
type: crystal
@@ -184,21 +191,21 @@ jobs:
if: matrix.type == 'ruff'
run: pip install ruff
- # Biome Setup
+ # Biome / Frontend Setup
- name: Setup Node.js
- if: matrix.type == 'biome'
+ if: matrix.type == 'biome' || matrix.type == 'frontend'
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup pnpm
- if: matrix.type == 'biome'
+ if: matrix.type == 'biome' || matrix.type == 'frontend'
uses: pnpm/action-setup@v4
with:
version: 10
- name: Cache pnpm store
- if: matrix.type == 'biome'
+ if: matrix.type == 'biome' || matrix.type == 'frontend'
uses: actions/cache@v4
with:
path: ~/.local/share/pnpm/store/v10
@@ -208,7 +215,7 @@ jobs:
${{ runner.os }}-pnpm-
- name: Install frontend dependencies
- if: matrix.type == 'biome'
+ if: matrix.type == 'biome' || matrix.type == 'frontend'
run: pnpm install --frozen-lockfile
# Go Setup
@@ -333,6 +340,41 @@ jobs:
cat biome-output.txt
continue-on-error: true
+ # Frontend Linting (biome + stylelint + tsc)
+ - name: Run Biome, Stylelint, and tsc
+ if: matrix.type == 'frontend'
+ id: frontend
+ run: |
+ FRONTEND_OK=true
+ echo "Running Biome check..."
+ if pnpm biome check . > frontend-output.txt 2>&1; then
+ echo "biome: no issues"
+ else
+ FRONTEND_OK=false
+ echo "biome: issues found"
+ fi
+ echo "Running Stylelint..."
+ if pnpm stylelint "**/*.scss" >> frontend-output.txt 2>&1; then
+ echo "stylelint: no issues"
+ else
+ FRONTEND_OK=false
+ echo "stylelint: issues found"
+ fi
+ echo "Running tsc --noEmit..."
+ if pnpm tsc --noEmit >> frontend-output.txt 2>&1; then
+ echo "tsc: no issues"
+ else
+ FRONTEND_OK=false
+ echo "tsc: type errors found"
+ fi
+ if [[ "$FRONTEND_OK" == "true" ]]; then
+ echo "FRONTEND_PASSED=true" >> $GITHUB_ENV
+ else
+ echo "FRONTEND_PASSED=false" >> $GITHUB_ENV
+ fi
+ cat frontend-output.txt
+ continue-on-error: true
+
# Go Linting
- name: Run golangci-lint
if: matrix.type == 'go'
@@ -589,6 +631,35 @@ jobs:
fi
} >> $GITHUB_STEP_SUMMARY
+ # Create Summary for Frontend
+ - name: Create Frontend Lint Summary
+ if: matrix.type == 'frontend'
+ run: |
+ {
+ echo "## Lint Results: ${{ matrix.name }}"
+ echo ''
+ if [[ "${{ env.FRONTEND_PASSED }}" == "true" ]]; then
+ echo '### Biome + Stylelint + tsc: **Passed**'
+ echo 'No frontend issues found.'
+ else
+ echo '### Biome + Stylelint + tsc: **Issues Found**'
+ echo 'View output
'
+ echo ''
+ echo '```'
+ head -100 frontend-output.txt
+ echo '```'
+ echo ' '
+ fi
+ echo ''
+ if [[ "${{ env.FRONTEND_PASSED }}" == "true" ]]; then
+ echo '---'
+ echo '### All checks passed!'
+ else
+ echo '---'
+ echo '### Review the issues above'
+ fi
+ } >> $GITHUB_STEP_SUMMARY
+
# Create Summary for Go
- name: Create Go Lint Summary
if: matrix.type == 'go'
@@ -809,6 +880,11 @@ jobs:
echo "Biome lint checks failed"
exit 1
fi
+ elif [[ "${{ matrix.type }}" == "frontend" ]]; then
+ if [[ "${{ env.FRONTEND_PASSED }}" == "false" ]]; then
+ echo "Frontend lint checks failed"
+ exit 1
+ fi
elif [[ "${{ matrix.type }}" == "go" ]]; then
if [[ "${{ env.GOLANGCI_PASSED }}" == "false" ]]; then
echo "Go lint checks failed"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7d7ab794..f1716f02 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -258,6 +258,33 @@ repos:
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/
pass_filenames: false
+ - id: biome-ja3-ja4-tls-fingerprinting
+ name: biome check (ja3-ja4-tls-fingerprinting frontend)
+ entry: bash -c 'cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend && pnpm biome check .'
+ language: system
+ files: ^PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/
+ pass_filenames: false
+
+ # Stylelint SCSS Checks
+ - repo: local
+ hooks:
+ - id: stylelint-ja3-ja4-tls-fingerprinting
+ name: stylelint (ja3-ja4-tls-fingerprinting frontend)
+ entry: bash -c 'cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend && pnpm stylelint "**/*.scss"'
+ language: system
+ files: ^PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/
+ pass_filenames: false
+
+ # TypeScript tsc Checks
+ - repo: local
+ hooks:
+ - id: tsc-ja3-ja4-tls-fingerprinting
+ name: tsc typecheck (ja3-ja4-tls-fingerprinting frontend)
+ entry: bash -c 'cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend && pnpm tsc --noEmit'
+ language: system
+ files: ^PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/
+ pass_filenames: false
+
# Nim nph Checks
- repo: local
hooks:
@@ -287,6 +314,22 @@ repos:
types: [rust]
pass_filenames: false
+ - id: cargo-fmt-ja3-ja4-tls-fingerprinting
+ name: cargo fmt (ja3-ja4-tls-fingerprinting)
+ entry: bash -c 'cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting && cargo fmt --all -- --check'
+ language: system
+ files: ^PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/
+ types: [rust]
+ pass_filenames: false
+
+ - id: cargo-clippy-ja3-ja4-tls-fingerprinting
+ name: cargo clippy (ja3-ja4-tls-fingerprinting)
+ entry: bash -c 'cd PROJECTS/intermediate/ja3-ja4-tls-fingerprinting && cargo clippy --workspace --all-targets -- -D warnings'
+ language: system
+ files: ^PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/
+ types: [rust]
+ pass_filenames: false
+
# Crystal Checks (format + ameba + compile)
- repo: local
hooks:
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs
index 7041b881..eabcf7a4 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/der.rs
@@ -8,6 +8,7 @@ pub mod tag {
pub const OBJECT_IDENTIFIER: u8 = 0x06;
pub const SEQUENCE: u8 = 0x30;
pub const SET: u8 = 0x31;
+ pub const CONTEXT_0: u8 = 0xa0;
pub const CONTEXT_3: u8 = 0xa3;
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs
index 7cdc7609..71b4a0a7 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4t.rs
@@ -62,7 +62,7 @@ mod tests {
}
#[test]
- fn foxio_linux_default_vector() {
+ fn foxio_windows_default_vector() {
let i = input(64240, &[2, 1, 3, 1, 1, 4], 1460, 8);
assert_eq!(ja4t(&i), "64240_2-1-3-1-1-4_1460_8");
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs
index df2af350..2d28b03d 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/ja4x.rs
@@ -32,6 +32,15 @@ pub fn ja4x(cert_der: &[u8]) -> Result {
type OidList = SmallVec<[String; 8]>;
+/// Walks a certificate down to the issuer Name, the subject Name, and the
+/// extension block, the three field groups JA4X reads.
+///
+/// The TBSCertificate fields sit in a fixed order (RFC 5280): an optional
+/// explicit version tagged context zero, then the serial number, signature
+/// algorithm, issuer, validity, subject, and public key, with the extensions
+/// tagged context three at the end. The walk names every field it steps over and
+/// bounds checks each one, so a certificate truncated before the public key or
+/// the extensions is an error rather than a panic.
fn certificate_oids(cert_der: &[u8]) -> Result<(OidList, OidList, OidList)> {
let mut outer = Der::new(cert_der);
let (_, certificate) = outer.read_tlv()?;
@@ -44,19 +53,24 @@ fn certificate_oids(cert_der: &[u8]) -> Result<(OidList, OidList, OidList)> {
fields.push(walker.read_tlv()?);
}
- let mut idx = 0;
- if fields.first().is_some_and(|(t, _)| *t == 0xa0) {
- idx += 1;
+ let mut cursor = 0usize;
+ if fields.first().is_some_and(|(t, _)| *t == tag::CONTEXT_0) {
+ cursor += 1;
}
- idx += 2;
- let issuer = field_content(&fields, idx)?;
- idx += 1;
- idx += 1;
- let subject = field_content(&fields, idx)?;
- idx += 1;
- idx += 1;
+ let _serial = field_content(&fields, cursor)?;
+ cursor += 1;
+ let _signature = field_content(&fields, cursor)?;
+ cursor += 1;
+ let issuer = field_content(&fields, cursor)?;
+ cursor += 1;
+ let _validity = field_content(&fields, cursor)?;
+ cursor += 1;
+ let subject = field_content(&fields, cursor)?;
+ cursor += 1;
- let extensions = fields[idx..]
+ let extensions = fields
+ .get(cursor..)
+ .unwrap_or(&[])
.iter()
.find(|(t, _)| *t == tag::CONTEXT_3)
.map(|(_, c)| *c);
@@ -156,6 +170,31 @@ mod tests {
assert_eq!(oids.as_slice(), &["550406", "55040a", "55040b", "550403"]);
}
+ #[test]
+ fn certificate_ending_after_subject_is_handled_without_panic() {
+ let rdn = tlv(
+ 0x31,
+ &tlv(0x30, &{
+ let mut atv = oid(&[0x55, 0x04, 0x03]);
+ atv.extend(tlv(0x13, b"x"));
+ atv
+ }),
+ );
+ let tbs = tlv(
+ 0x30,
+ &[
+ tlv(0x02, &[0x01]),
+ tlv(0x30, &[]),
+ tlv(0x30, &rdn),
+ tlv(0x30, &[]),
+ tlv(0x30, &rdn),
+ ]
+ .concat(),
+ );
+ let cert = tlv(0x30, &tbs);
+ assert!(super::ja4x(&cert).is_ok());
+ }
+
#[test]
fn extracts_extension_oids() {
let mut sequence = Vec::new();
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs
index 5c204b33..35046d5f 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/decode.rs
@@ -372,7 +372,7 @@ mod tests {
}
#[test]
- fn ja4t_walk_reproduces_the_linux_default_vector() {
+ fn ja4t_walk_reproduces_the_windows_default_vector() {
let options = [
0x02, 0x04, 0x05, 0xb4, 0x01, 0x03, 0x03, 0x08, 0x01, 0x01, 0x04, 0x02,
];
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs
index 989b8cd6..03faa82b 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/pipeline/mod.rs
@@ -399,7 +399,7 @@ impl Pipeline {
let opened = if let Some(keys) = flow.keys.as_ref() {
packet.open(keys, flow.largest_pn).ok()
} else {
- let candidate = InitialKeys::client(packet.dcid);
+ let candidate = packet.client_keys();
match packet.open(&candidate, None) {
Ok(opened) => {
flow.keys = Some(candidate);
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/quic.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/quic.rs
index d5d3c69c..4981b7bc 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/quic.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/src/quic.rs
@@ -49,6 +49,9 @@ use crate::registry::handshake_type;
/// The QUIC version 1 code point, from RFC 9000.
pub const VERSION_1: u32 = 0x0000_0001;
+/// The QUIC version 2 code point, from RFC 9369.
+pub const VERSION_2: u32 = 0x6b33_43cf;
+
/// The version 1 Initial salt, from RFC 9001 Section 5.2. This constant is
/// what binds a set of Initial keys to one QUIC version: a different version
/// uses a different salt, so keys derived under the wrong salt simply fail
@@ -58,6 +61,15 @@ const INITIAL_SALT_V1: [u8; 20] = [
0xcc, 0xbb, 0x7f, 0x0a,
];
+/// The version 2 Initial salt, from RFC 9369 Section 3.3.1. QUIC v2 deliberately
+/// changes the salt, the key derivation labels, and the long header type code
+/// for Initial away from their v1 values, so an observer must implement the
+/// version on purpose rather than assume v1 framing.
+const INITIAL_SALT_V2: [u8; 20] = [
+ 0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb,
+ 0xf9, 0xbd, 0x2e, 0xd9,
+];
+
const KEY_LEN: usize = 16;
const IV_LEN: usize = 12;
const HP_LEN: usize = 16;
@@ -79,10 +91,11 @@ const PN_SAMPLE_OFFSET: usize = 4;
const MAX_CID_LEN: usize = 20;
/// The high bit of the first byte marks a long header; the next bit is the
-/// version independent fixed bit. Initial is long header packet type `0b00`.
+/// version independent fixed bit. The two type bits identify an Initial, but
+/// their value differs by version, `0b00` in v1 and `0b01` in v2, so the type
+/// code is carried per version rather than as one constant.
const LONG_HEADER_FORM: u8 = 0x80;
const PACKET_TYPE_MASK: u8 = 0x30;
-const PACKET_TYPE_INITIAL: u8 = 0x00;
/// Header protection masks the low four bits of a long header's first byte.
const LONG_HEADER_PN_MASK: u8 = 0x0f;
@@ -174,6 +187,51 @@ fn hkdf_expand_label(secret: &[u8], label: &[u8], out: &mut [u8]) {
.expect("Initial output never exceeds the HKDF limit");
}
+/// The version specific inputs to Initial key derivation: the HKDF salt, the
+/// key, IV, and header protection labels, and the long header type code that
+/// marks an Initial packet.
+///
+/// QUIC v1 (RFC 9001) and v2 (RFC 9369) differ in all of these. Binding them to
+/// the version is what makes keys derived under the wrong version fail the AEAD
+/// tag rather than silently misread a packet, and the `client in` secret label
+/// is shared because it comes from the TLS 1.3 derivation, which v2 left alone.
+#[derive(Clone, Copy)]
+struct QuicVersion {
+ salt: &'static [u8; 20],
+ key_label: &'static [u8],
+ iv_label: &'static [u8],
+ hp_label: &'static [u8],
+ initial_type: u8,
+}
+
+impl QuicVersion {
+ const V1: QuicVersion = QuicVersion {
+ salt: &INITIAL_SALT_V1,
+ key_label: b"quic key",
+ iv_label: b"quic iv",
+ hp_label: b"quic hp",
+ initial_type: 0x00,
+ };
+
+ const V2: QuicVersion = QuicVersion {
+ salt: &INITIAL_SALT_V2,
+ key_label: b"quicv2 key",
+ iv_label: b"quicv2 iv",
+ hp_label: b"quicv2 hp",
+ initial_type: 0x10,
+ };
+
+ /// Returns the parameters for a known QUIC version code, or `None` for a
+ /// version this build has no Initial salt for.
+ fn from_code(code: u32) -> Option {
+ match code {
+ VERSION_1 => Some(Self::V1),
+ VERSION_2 => Some(Self::V2),
+ _ => None,
+ }
+ }
+}
+
/// The AEAD and header protection keys for one direction of one QUIC Initial.
pub struct InitialKeys {
key: [u8; KEY_LEN],
@@ -182,16 +240,16 @@ pub struct InitialKeys {
}
impl InitialKeys {
- /// Derives the client's Initial keys from a Destination Connection ID,
- /// for QUIC version 1.
+ /// Derives the client's Initial keys from a Destination Connection ID, under
+ /// the salt and labels of the given QUIC version.
///
/// A passive observer derives these from the connection ID alone, with no
/// secret input, which is exactly why the AEAD tag rather than secrecy is
/// what tells a client Initial apart from a server one: only a packet the
/// client actually protected under these keys will verify.
#[must_use]
- pub fn client(dcid: &[u8]) -> Self {
- let (initial_secret, _) = Hkdf::::extract(Some(&INITIAL_SALT_V1), dcid);
+ fn client(dcid: &[u8], version: QuicVersion) -> Self {
+ let (initial_secret, _) = Hkdf::::extract(Some(version.salt), dcid);
let mut client_secret = [0u8; 32];
hkdf_expand_label(&initial_secret, b"client in", &mut client_secret);
@@ -200,9 +258,9 @@ impl InitialKeys {
iv: [0u8; IV_LEN],
hp: [0u8; HP_LEN],
};
- hkdf_expand_label(&client_secret, b"quic key", &mut keys.key);
- hkdf_expand_label(&client_secret, b"quic iv", &mut keys.iv);
- hkdf_expand_label(&client_secret, b"quic hp", &mut keys.hp);
+ hkdf_expand_label(&client_secret, version.key_label, &mut keys.key);
+ hkdf_expand_label(&client_secret, version.iv_label, &mut keys.iv);
+ hkdf_expand_label(&client_secret, version.hp_label, &mut keys.hp);
keys
}
}
@@ -218,6 +276,9 @@ pub struct InitialPacket<'pkt> {
datagram: &'pkt [u8],
/// The Destination Connection ID, which seeds key derivation.
pub dcid: &'pkt [u8],
+ /// The QUIC version this packet declared, selecting the salt and labels its
+ /// client keys derive under.
+ version: QuicVersion,
/// Absolute offset of this packet's first byte within the datagram.
start: usize,
/// Absolute offset of the packet number field within the datagram.
@@ -250,16 +311,16 @@ impl<'pkt> InitialPacket<'pkt> {
return Err(ParseError::NotQuicInitial);
}
- let version = u32::from_be_bytes(
+ let version_code = u32::from_be_bytes(
buf.get(1..5)
.ok_or(ParseError::NotQuicInitial)?
.try_into()
.expect("a four byte slice is four bytes"),
);
- if version != VERSION_1 {
- return Err(ParseError::UnsupportedQuicVersion(version));
- }
- if first & PACKET_TYPE_MASK != PACKET_TYPE_INITIAL {
+ let Some(version) = QuicVersion::from_code(version_code) else {
+ return Err(ParseError::UnsupportedQuicVersion(version_code));
+ };
+ if first & PACKET_TYPE_MASK != version.initial_type {
return Err(ParseError::NotQuicInitial);
}
@@ -291,6 +352,7 @@ impl<'pkt> InitialPacket<'pkt> {
Ok(Self {
datagram,
dcid,
+ version,
start,
pn_offset,
end,
@@ -298,6 +360,13 @@ impl<'pkt> InitialPacket<'pkt> {
})
}
+ /// Derives the client Initial keys for this packet, from its own Destination
+ /// Connection ID under the salt and labels of the version it declared.
+ #[must_use]
+ pub fn client_keys(&self) -> InitialKeys {
+ InitialKeys::client(self.dcid, self.version)
+ }
+
/// Removes header protection and decrypts the payload, returning the
/// cleartext QUIC frames.
///
@@ -622,8 +691,8 @@ pub enum ClientHelloState<'a> {
#[cfg(test)]
mod tests {
use super::{
- ClientHelloState, CryptoAssembler, InitialKeys, InitialPacket, decode_packet_number,
- hkdf_expand_label, read_varint, walk_crypto_frames,
+ ClientHelloState, CryptoAssembler, InitialKeys, InitialPacket, QuicVersion,
+ decode_packet_number, hkdf_expand_label, read_varint, walk_crypto_frames,
};
/// RFC 9000 Appendix A.1 sample variable length integer decodings.
@@ -654,12 +723,23 @@ mod tests {
#[test]
fn client_initial_keys_match_rfc9001_appendix_a1() {
let dcid = hex("8394c8f03e515708");
- let keys = InitialKeys::client(&dcid);
+ let keys = InitialKeys::client(&dcid, QuicVersion::V1);
assert_eq!(keys.key.to_vec(), hex("1f369613dd76d5467730efcbe3b1a22d"));
assert_eq!(keys.iv.to_vec(), hex("fa044b2f42a3fd3b46fb255c"));
assert_eq!(keys.hp.to_vec(), hex("9f50449e04a0e810283a1e9933adedd2"));
}
+ /// RFC 9369 Appendix A derives the QUIC v2 client Initial keys from the same
+ /// sample Destination Connection ID, under the v2 salt and labels.
+ #[test]
+ fn client_initial_keys_match_rfc9369_appendix_a() {
+ let dcid = hex("8394c8f03e515708");
+ let keys = InitialKeys::client(&dcid, QuicVersion::V2);
+ assert_eq!(keys.key.to_vec(), hex("8b1a0bc121284290a29e0971b5cd045d"));
+ assert_eq!(keys.iv.to_vec(), hex("91f73e2351d8fa91660e909f"));
+ assert_eq!(keys.hp.to_vec(), hex("45b95e15235d6f45a6b19cbcb0294ba9"));
+ }
+
/// RFC 9001 Appendix A.1 also pins the intermediate expand label output.
#[test]
fn expand_label_reproduces_rfc9001_client_secret() {
@@ -683,7 +763,7 @@ mod tests {
let packet = InitialPacket::parse(&datagram, 0).unwrap();
assert_eq!(packet.dcid, &hex("8394c8f03e515708")[..]);
- let keys = InitialKeys::client(packet.dcid);
+ let keys = packet.client_keys();
let opened = packet.open(&keys, None).unwrap();
assert_eq!(opened.packet_number, 2);
@@ -732,6 +812,30 @@ mod tests {
assert!(InitialPacket::parse(udp, 0).is_err());
}
+ /// A QUIC v2 long header Initial is recognized by its version code and its
+ /// remapped Initial type bits, `0b01`, where the v1 Initial code `0b00` is
+ /// not an Initial in v2. This proves the version specific parse logic; the v2
+ /// key schedule is pinned by the keys KAT above, and the AEAD decryption path
+ /// is the one the RFC 9001 Appendix A.2 test already exercises unchanged.
+ #[test]
+ fn parses_a_quic_v2_initial_header() {
+ let mut packet = vec![0xd3];
+ packet.extend_from_slice(&super::VERSION_2.to_be_bytes());
+ packet.push(8);
+ packet.extend_from_slice(&hex("8394c8f03e515708"));
+ packet.push(0);
+ packet.push(0);
+ packet.push(0x10);
+ packet.extend_from_slice(&[0u8; 16]);
+
+ let parsed = InitialPacket::parse(&packet, 0).unwrap();
+ assert_eq!(parsed.dcid, &hex("8394c8f03e515708")[..]);
+
+ let mut v1_type = packet.clone();
+ v1_type[0] = 0xc3;
+ assert!(InitialPacket::parse(&v1_type, 0).is_err());
+ }
+
fn hex(s: &str) -> Vec {
::hex::decode(s).unwrap()
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4x.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4x.rs
new file mode 100644
index 00000000..aeea7d37
--- /dev/null
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-core/tests/ja4x.rs
@@ -0,0 +1,21 @@
+// ©AngelaMos | 2026
+// ja4x.rs
+
+//! Fuzz harness proving the X.509 certificate walk behind JA4X never panics.
+//!
+//! JA4X reads certificate bytes a passive observer takes straight off a cleartext
+//! TLS 1.2 handshake, so a malformed or hostile certificate must be an ordinary
+//! error rather than a crash, the same contract the TLS parser and the stream
+//! reassembler hold and fuzz. This pairs with the constructed truncated
+//! certificate unit test: arbitrary bytes in, never a panic out.
+
+use proptest::prelude::*;
+
+proptest! {
+ #[test]
+ fn ja4x_never_panics_on_arbitrary_bytes(
+ bytes in proptest::collection::vec(any::(), 0..4096),
+ ) {
+ let _ = tlsfp_core::ja4x(&bytes);
+ }
+}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/detect.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/detect.rs
index fdbb768b..2ad9c1f8 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/detect.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/detect.rs
@@ -11,7 +11,9 @@
//!
//! The rules, in the order they are evaluated:
//!
-//! - known bad: the fingerprint matches malicious or suspicious intelligence.
+//! - known bad: the fingerprint matches malicious or suspicious intelligence,
+//! raised the first time an address presents it so a repeating beacon from one
+//! host is one alert rather than one per flow.
//! - User-Agent mismatch: a request calls itself a browser while the same
//! client's TLS fingerprint is a script or a tool. This is the headline, the
//! lie that a fingerprint catches and a User-Agent string cannot tell.
@@ -256,8 +258,10 @@ pub(crate) fn run(
let observation_id = insert_observation(conn, ts, &ip_text, kind, &value, &summary, &context)?;
- if let Some(alert) = known_bad(conn, ts, &ip_text, &reports, kind, &value)? {
- alerts.push(alert);
+ if !ip_fp_known {
+ if let Some(alert) = known_bad(conn, ts, &ip_text, &reports, kind, &value)? {
+ alerts.push(alert);
+ }
}
if !fp_known {
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/model.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/model.rs
index aecaeb92..14e49800 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/model.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/model.rs
@@ -12,6 +12,25 @@ use std::fmt;
use serde::Serialize;
+/// The threat score at or above which a fingerprint is judged malicious.
+const MALICIOUS_THRESHOLD: f64 = 0.8;
+
+/// The threat score at or below which a fingerprint is judged benign; between
+/// the two thresholds it is suspicious.
+const BENIGN_THRESHOLD: f64 = 0.2;
+
+/// How much a suspicious, dual use hit counts toward the threat score, half a
+/// malicious vote.
+const SUSPICIOUS_VOTE: f64 = 0.5;
+
+/// The share of confidence the best match earns before corroboration, so a lone
+/// exact hit is already fairly trusted and agreeing hits raise it the rest of
+/// the way without ever exceeding the best match weight.
+const CONFIDENCE_FLOOR: f64 = 0.6;
+
+/// The remaining share of confidence that corroborating, aligned hits supply.
+const CONFIDENCE_CORROBORATION: f64 = 0.4;
+
/// Which fingerprint algorithm a stored value belongs to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
@@ -284,11 +303,11 @@ impl MatchReport {
}
}
let total = malicious + suspicious + benign;
- let threat_score = (malicious + 0.5 * suspicious) / total;
+ let threat_score = (malicious + SUSPICIOUS_VOTE * suspicious) / total;
- let verdict = if threat_score >= 0.8 {
+ let verdict = if threat_score >= MALICIOUS_THRESHOLD {
Verdict::Malicious
- } else if threat_score <= 0.2 {
+ } else if threat_score <= BENIGN_THRESHOLD {
Verdict::Benign
} else {
Verdict::Suspicious
@@ -299,7 +318,7 @@ impl MatchReport {
.filter(|hit| verdict_aligns(verdict, hit.category.severity()))
.count();
let corroboration = 1.0 - 1.0 / (1.0 + count_to_f64(aligned));
- let confidence = best * (0.6 + 0.4 * corroboration);
+ let confidence = best * (CONFIDENCE_FLOOR + CONFIDENCE_CORROBORATION * corroboration);
Self {
kind,
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/signal.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/signal.rs
index 404b91f8..4acb65a0 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/signal.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/src/signal.rs
@@ -28,6 +28,16 @@
use crate::model::Category;
+/// Whether `needle` appears in `haystack` as a whole word, bounded by characters
+/// that are not ASCII alphanumeric. Short, ambiguous tokens like "tor" and
+/// "java" are matched this way so a label such as "Factory" is not read as Tor
+/// and a User-Agent mentioning "javascript" is not read as Java.
+fn contains_word(haystack: &str, needle: &str) -> bool {
+ haystack
+ .split(|c: char| !c.is_ascii_alphanumeric())
+ .any(|word| word == needle)
+}
+
/// A coarse operating-system class, the resolution the SYN signature can carry
/// without guessing. Finer naming from one packet is not reliable, so the
/// mismatch rule works at this granularity on purpose.
@@ -216,7 +226,7 @@ pub fn ua_family(user_agent: &str) -> Option {
if ua.contains("okhttp") {
return Some(Family::OkHttp);
}
- if ua.contains("java") {
+ if contains_word(&ua, "java") {
return Some(Family::Java);
}
None
@@ -263,10 +273,10 @@ pub fn label_family(label: &str) -> Option {
if label.contains("okhttp") {
return Some(Family::OkHttp);
}
- if label.contains("java") {
+ if contains_word(&label, "java") {
return Some(Family::Java);
}
- if label.contains("tor") {
+ if contains_word(&label, "tor") {
return Some(Family::Tor);
}
None
@@ -414,4 +424,13 @@ mod tests {
assert!(!label_is_non_browser("Google Chrome", Category::Benign));
assert!(!label_is_non_browser("unrecognised", Category::Benign));
}
+
+ #[test]
+ fn ambiguous_substrings_do_not_misclassify() {
+ assert_eq!(label_family("Factory C2"), None);
+ assert_eq!(label_family("Tor Browser"), Some(Family::Tor));
+ assert_eq!(label_family("JavaScript Runtime"), None);
+ assert_eq!(label_family("Java"), Some(Family::Java));
+ assert_eq!(ua_family("Mozilla/5.0 javascript engine"), None);
+ }
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/tests/detect.rs b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/tests/detect.rs
index 5143341b..c4446508 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/tests/detect.rs
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp-intel/tests/detect.rs
@@ -105,6 +105,33 @@ fn known_bad_fires_on_malicious_fingerprint() {
assert_eq!(known_bad.score, Some(1.0));
}
+#[test]
+fn known_bad_does_not_refire_for_the_same_address_and_fingerprint() {
+ let mut store = store();
+ let config = cfg();
+ let first = store
+ .detect_with(
+ &client_hello("10.0.0.1", 1_000_000_000, MALWARE_JA4),
+ &config,
+ )
+ .unwrap();
+ let second = store
+ .detect_with(
+ &client_hello("10.0.0.1", 2_000_000_000, MALWARE_JA4),
+ &config,
+ )
+ .unwrap();
+ let other = store
+ .detect_with(
+ &client_hello("10.0.0.99", 3_000_000_000, MALWARE_JA4),
+ &config,
+ )
+ .unwrap();
+ assert!(fires(&first, Rule::KnownBad));
+ assert!(!fires(&second, Rule::KnownBad));
+ assert!(fires(&other, Rule::KnownBad));
+}
+
#[test]
fn ua_mismatch_browser_claim_over_tool_handshake() {
let mut store = store();
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/.gitignore b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/.gitignore
index 61cb0c2e..fe24ba6d 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/.gitignore
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/.gitignore
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
+.pnpm-store
dist
dist-ssr
*.local
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/biome.json b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/biome.json
index c7ff0a84..11d1a0b0 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/biome.json
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/biome.json
@@ -1,12 +1,21 @@
{
- "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
- "includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
+ "includes": [
+ "**/*.ts",
+ "**/*.tsx",
+ "**/*.js",
+ "**/*.jsx",
+ "**/*.json",
+ "!**/node_modules",
+ "!**/.pnpm-store",
+ "!**/dist"
+ ]
},
"formatter": {
"enabled": true,
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.jpg b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.jpg
deleted file mode 100644
index 48eb545c..00000000
Binary files a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.jpg and /dev/null differ
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.png b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.png
new file mode 100644
index 00000000..ec0c774f
Binary files /dev/null and b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-butterfly.png differ
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.png b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.png
new file mode 100644
index 00000000..2f642be3
Binary files /dev/null and b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.png differ
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.webp b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.webp
deleted file mode 100644
index c1ac3dd8..00000000
Binary files a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/public/assets/objects/xray-lily.webp and /dev/null differ
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/alert-feed.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/alert-feed.module.scss
index da60beb4..8ad934a6 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/alert-feed.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/alert-feed.module.scss
@@ -40,7 +40,7 @@
border-left-color: var(--accent);
background: linear-gradient(
90deg,
- rgba(255, 77, 18, 0.07),
+ rgb(255, 77, 18, 7%),
transparent 50%
);
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/anomaly.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/anomaly.module.scss
index d7de6e8f..f5c41ba8 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/anomaly.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/anomaly.module.scss
@@ -40,7 +40,7 @@
position: relative;
z-index: 1;
padding: $space-3;
- background: rgba(8, 9, 10, 0.82);
+ background: rgb(8, 9, 10, 82%);
border: 1px solid $line;
border-left: 2px solid var(--accent);
box-shadow: 0 0 20px -10px var(--accent);
@@ -61,7 +61,7 @@
line-height: 1.1;
letter-spacing: -0.01em;
color: $chalk;
- text-shadow: 0 0 11px rgba(255, 77, 18, 0.18);
+ text-shadow: 0 0 11px rgb(255, 77, 18, 18%);
}
.subject {
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/atoms.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/atoms.module.scss
index d1b804c6..9ccd970f 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/atoms.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/atoms.module.scss
@@ -23,7 +23,7 @@
padding: 0 6px;
color: var(--accent);
border: 1px solid var(--accent);
- background: rgba(0, 0, 0, 0.35);
+ background: rgb(0, 0, 0, 35%);
}
.cat {
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/distribution.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/distribution.module.scss
index afdda2a1..37c9aea9 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/distribution.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/distribution.module.scss
@@ -55,8 +55,8 @@
90deg,
transparent 0,
transparent 7px,
- rgba(255, 255, 255, 0.03) 7px,
- rgba(255, 255, 255, 0.03) 8px
+ rgb(255, 255, 255, 3%) 7px,
+ rgb(255, 255, 255, 3%) 8px
),
$ink-700;
border: 1px solid $line-faint;
@@ -68,7 +68,7 @@
background: var(--accent);
box-shadow:
0 0 7px -4px var(--accent),
- inset -1px 0 0 rgba(255, 255, 255, 0.3);
+ inset -1px 0 0 rgb(255, 255, 255, 30%);
transition: width $duration-slow $ease-out;
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/live-stream.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/live-stream.module.scss
index 944376c6..e5953e5e 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/live-stream.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/components/live-stream.module.scss
@@ -48,8 +48,7 @@
.scroll {
position: absolute;
inset: 0;
- overflow-y: auto;
- overflow-x: hidden;
+ overflow: hidden auto;
}
.row {
@@ -159,14 +158,14 @@
border-left: 2px solid $fire;
background: linear-gradient(
90deg,
- rgba(255, 77, 18, 0.14),
+ rgb(255, 77, 18, 14%),
transparent 55%
);
&:hover {
background: linear-gradient(
90deg,
- rgba(255, 77, 18, 0.2),
+ rgb(255, 77, 18, 20%),
transparent 55%
);
}
@@ -176,7 +175,7 @@
border-left: 2px solid var(--accent);
background: linear-gradient(
90deg,
- rgba(255, 174, 58, 0.08),
+ rgb(255, 174, 58, 8%),
transparent 60%
);
}
@@ -222,7 +221,7 @@
from {
opacity: 0;
transform: translateY(-7px);
- background: rgba(47, 123, 255, 0.09);
+ background: rgb(47, 123, 255, 9%);
}
to {
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/core/app/shell.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/core/app/shell.module.scss
index bea39181..4c4e7737 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/core/app/shell.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/core/app/shell.module.scss
@@ -21,7 +21,7 @@
align-items: center;
gap: $space-4;
padding: $space-4 $space-7;
- background: rgba(5, 5, 6, 0.86);
+ background: rgb(5, 5, 6, 86%);
backdrop-filter: blur(10px);
border-bottom: 1px solid $line;
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/intel/intel.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/intel/intel.module.scss
index 0e5e5dc0..ebefb5ac 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/intel/intel.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/intel/intel.module.scss
@@ -31,7 +31,7 @@
transform: translateY(-50%);
mix-blend-mode: screen;
opacity: 0.5;
- filter: drop-shadow(0 0 40px rgba(47, 123, 255, 0.3));
+ filter: drop-shadow(0 0 40px rgb(47, 123, 255, 30%));
pointer-events: none;
z-index: 0;
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/landing/landing.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/landing/landing.module.scss
index 8f3a49a5..970a8ce9 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/landing/landing.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/landing/landing.module.scss
@@ -26,7 +26,7 @@
transform: translateY(-50%);
mix-blend-mode: screen;
opacity: 0.92;
- filter: drop-shadow(0 0 60px rgba(47, 123, 255, 0.28));
+ filter: drop-shadow(0 0 60px rgb(47, 123, 255, 28%));
pointer-events: none;
z-index: 0;
@@ -103,7 +103,7 @@
letter-spacing: 0.34em;
text-transform: uppercase;
color: $fire;
- text-shadow: 0 0 10px rgba(255, 77, 18, 0.28);
+ text-shadow: 0 0 10px rgb(255, 77, 18, 28%);
margin-bottom: $space-5;
}
@@ -114,7 +114,7 @@
letter-spacing: 0.01em;
color: $chalk;
margin: 0 0 $space-5;
- text-shadow: 0 0 48px rgba(47, 123, 255, 0.16);
+ text-shadow: 0 0 48px rgb(47, 123, 255, 16%);
}
// The mono tag reads as a tight stamp under the masthead, so it is kept short
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/scope/scope.module.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/scope/scope.module.scss
index 6c70c8d1..ab27d518 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/scope/scope.module.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/pages/scope/scope.module.scss
@@ -83,22 +83,20 @@
.grid {
flex: 1;
display: grid;
- grid-template-columns: minmax(0, 1.62fr) minmax(0, 1fr);
- grid-template-rows: minmax(300px, auto) minmax(260px, auto) auto;
- grid-template-areas:
- 'live anomaly'
- 'live dist'
- 'alerts alerts';
+ grid-template:
+ 'live anomaly' minmax(300px, auto)
+ 'live dist' minmax(260px, auto)
+ 'alerts alerts' auto
+ / minmax(0, 1.62fr) minmax(0, 1fr);
gap: $space-4;
@include breakpoint-down('lg') {
- grid-template-columns: 1fr;
- grid-template-rows: none;
- grid-template-areas:
+ grid-template:
'live'
'anomaly'
'dist'
- 'alerts';
+ 'alerts'
+ / 1fr;
}
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles.scss
index d0abeb9b..1687c4fe 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles.scss
@@ -49,7 +49,7 @@
}
@font-face {
- font-family: 'Anton';
+ font-family: Anton;
font-style: normal;
font-weight: 400;
font-display: swap;
@@ -66,24 +66,25 @@ body {
font-family: $font-grotesk;
font-size: $font-size-sm;
letter-spacing: 0.01em;
+
// A breath of grain over the whole field so the black reads as a printed
// surface, not a switched-off screen.
background-image:
radial-gradient(
ellipse 120% 80% at 50% -10%,
- rgba(47, 123, 255, 0.05),
+ rgb(47, 123, 255, 5%),
transparent 60%
),
radial-gradient(
ellipse 90% 60% at 100% 110%,
- rgba(255, 77, 18, 0.04),
+ rgb(255, 77, 18, 4%),
transparent 55%
);
background-attachment: fixed;
}
::selection {
- background: rgba(255, 77, 18, 0.85);
+ background: rgb(255, 77, 18, 85%);
color: $void;
}
@@ -135,15 +136,19 @@ body {
[data-sev='info'] {
--accent: #{$sev-info};
}
+
[data-sev='low'] {
--accent: #{$sev-low};
}
+
[data-sev='medium'] {
--accent: #{$sev-medium};
}
+
[data-sev='high'] {
--accent: #{$sev-high};
}
+
[data-sev='critical'] {
--accent: #{$sev-critical};
}
@@ -151,12 +156,15 @@ body {
[data-verdict='malicious'] {
--accent: #{$verdict-malicious};
}
+
[data-verdict='suspicious'] {
--accent: #{$verdict-suspicious};
}
+
[data-verdict='benign'] {
--accent: #{$verdict-benign};
}
+
[data-verdict='unknown'] {
--accent: #{$verdict-unknown};
}
@@ -164,18 +172,23 @@ body {
[data-cat='malware'] {
--accent: #{$cat-malware};
}
+
[data-cat='c2'] {
--accent: #{$cat-c2};
}
+
[data-cat='tool'] {
--accent: #{$cat-tool};
}
+
[data-cat='benign'] {
--accent: #{$cat-benign};
}
+
[data-cat='os'] {
--accent: #{$cat-os};
}
+
[data-cat='unknown'] {
--accent: #{$cat-unknown};
}
@@ -183,18 +196,23 @@ body {
[data-rule='known_bad'] {
--accent: #{$blood};
}
+
[data-rule='ua_mismatch'] {
--accent: #{$fire};
}
+
[data-rule='os_mismatch'] {
--accent: #{$magma};
}
+
[data-rule='fp_rotation'] {
--accent: #{$volt};
}
+
[data-rule='monoculture'] {
--accent: #{$orchid};
}
+
[data-rule='first_seen'] {
--accent: #{$thermal};
}
diff --git a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles/_anti.scss b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles/_anti.scss
index 5bea6c23..77e4d706 100644
--- a/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles/_anti.scss
+++ b/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/frontend/src/styles/_anti.scss
@@ -9,7 +9,7 @@
// the instrument itself. Neutral type sits between.
// The void and the panels lifted just off it.
-$void: #000000;
+$void: #000;
$ink-1000: #050506;
$ink-900: #0a0b0d;
$ink-800: #0f1113;
@@ -18,7 +18,7 @@ $ink-600: #1c2024;
$ink-500: #262b30;
// Bone white for the artifacts, chalk for the few things that must shout.
-$chalk: #ffffff;
+$chalk: #fff;
$bone: #f2f0e9;
$paper: #d8d6cd;
@@ -28,10 +28,10 @@ $dust: #5a5f65;
$soot: #34383d;
// Hairlines. The grid the specimens are pinned to.
-$line-faint: rgba(255, 255, 255, 0.06);
-$line: rgba(255, 255, 255, 0.11);
-$line-strong: rgba(255, 255, 255, 0.2);
-$line-hot: rgba(255, 77, 18, 0.55);
+$line-faint: rgb(255, 255, 255, 6%);
+$line: rgb(255, 255, 255, 11%);
+$line-strong: rgb(255, 255, 255, 20%);
+$line-hot: rgb(255, 77, 18, 55%);
// The hot channels: what the x-ray exposes, what bleeds through the black.
$fire: #ff4d12; // the butterfly's fire, the snake; primary anomaly