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