the hallway is four steps longer at night. i've confirmed this.
This commit is contained in:
parent
4bb95fdb97
commit
56c2b0233c
|
|
@ -109,6 +109,10 @@ jobs:
|
||||||
- name: monitor-the-situation-dashboard-frontend
|
- name: monitor-the-situation-dashboard-frontend
|
||||||
type: biome
|
type: biome
|
||||||
path: PROJECTS/advanced/monitor-the-situation-dashboard/frontend
|
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
|
# Go
|
||||||
- name: simple-vulnerability-scanner
|
- name: simple-vulnerability-scanner
|
||||||
type: go
|
type: go
|
||||||
|
|
@ -142,6 +146,9 @@ jobs:
|
||||||
- name: binary-analysis-tool-backend
|
- name: binary-analysis-tool-backend
|
||||||
type: rust
|
type: rust
|
||||||
path: PROJECTS/intermediate/binary-analysis-tool/backend
|
path: PROJECTS/intermediate/binary-analysis-tool/backend
|
||||||
|
- name: ja3-ja4-tls-fingerprinting
|
||||||
|
type: rust
|
||||||
|
path: PROJECTS/intermediate/ja3-ja4-tls-fingerprinting
|
||||||
# Crystal
|
# Crystal
|
||||||
- name: credential-rotation-enforcer
|
- name: credential-rotation-enforcer
|
||||||
type: crystal
|
type: crystal
|
||||||
|
|
@ -184,21 +191,21 @@ jobs:
|
||||||
if: matrix.type == 'ruff'
|
if: matrix.type == 'ruff'
|
||||||
run: pip install ruff
|
run: pip install ruff
|
||||||
|
|
||||||
# Biome Setup
|
# Biome / Frontend Setup
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: matrix.type == 'biome'
|
if: matrix.type == 'biome' || matrix.type == 'frontend'
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
if: matrix.type == 'biome'
|
if: matrix.type == 'biome' || matrix.type == 'frontend'
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
|
|
||||||
- name: Cache pnpm store
|
- name: Cache pnpm store
|
||||||
if: matrix.type == 'biome'
|
if: matrix.type == 'biome' || matrix.type == 'frontend'
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.local/share/pnpm/store/v10
|
path: ~/.local/share/pnpm/store/v10
|
||||||
|
|
@ -208,7 +215,7 @@ jobs:
|
||||||
${{ runner.os }}-pnpm-
|
${{ runner.os }}-pnpm-
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
if: matrix.type == 'biome'
|
if: matrix.type == 'biome' || matrix.type == 'frontend'
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Go Setup
|
# Go Setup
|
||||||
|
|
@ -333,6 +340,41 @@ jobs:
|
||||||
cat biome-output.txt
|
cat biome-output.txt
|
||||||
continue-on-error: true
|
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
|
# Go Linting
|
||||||
- name: Run golangci-lint
|
- name: Run golangci-lint
|
||||||
if: matrix.type == 'go'
|
if: matrix.type == 'go'
|
||||||
|
|
@ -589,6 +631,35 @@ jobs:
|
||||||
fi
|
fi
|
||||||
} >> $GITHUB_STEP_SUMMARY
|
} >> $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 '<details><summary>View output</summary>'
|
||||||
|
echo ''
|
||||||
|
echo '```'
|
||||||
|
head -100 frontend-output.txt
|
||||||
|
echo '```'
|
||||||
|
echo '</details>'
|
||||||
|
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
|
# Create Summary for Go
|
||||||
- name: Create Go Lint Summary
|
- name: Create Go Lint Summary
|
||||||
if: matrix.type == 'go'
|
if: matrix.type == 'go'
|
||||||
|
|
@ -809,6 +880,11 @@ jobs:
|
||||||
echo "Biome lint checks failed"
|
echo "Biome lint checks failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
elif [[ "${{ matrix.type }}" == "go" ]]; then
|
||||||
if [[ "${{ env.GOLANGCI_PASSED }}" == "false" ]]; then
|
if [[ "${{ env.GOLANGCI_PASSED }}" == "false" ]]; then
|
||||||
echo "Go lint checks failed"
|
echo "Go lint checks failed"
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,33 @@ repos:
|
||||||
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/
|
files: ^PROJECTS/advanced/monitor-the-situation-dashboard/frontend/src/
|
||||||
pass_filenames: false
|
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
|
# Nim nph Checks
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
|
@ -287,6 +314,22 @@ repos:
|
||||||
types: [rust]
|
types: [rust]
|
||||||
pass_filenames: false
|
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)
|
# Crystal Checks (format + ameba + compile)
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub mod tag {
|
||||||
pub const OBJECT_IDENTIFIER: u8 = 0x06;
|
pub const OBJECT_IDENTIFIER: u8 = 0x06;
|
||||||
pub const SEQUENCE: u8 = 0x30;
|
pub const SEQUENCE: u8 = 0x30;
|
||||||
pub const SET: u8 = 0x31;
|
pub const SET: u8 = 0x31;
|
||||||
|
pub const CONTEXT_0: u8 = 0xa0;
|
||||||
pub const CONTEXT_3: u8 = 0xa3;
|
pub const CONTEXT_3: u8 = 0xa3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foxio_linux_default_vector() {
|
fn foxio_windows_default_vector() {
|
||||||
let i = input(64240, &[2, 1, 3, 1, 1, 4], 1460, 8);
|
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");
|
assert_eq!(ja4t(&i), "64240_2-1-3-1-1-4_1460_8");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,15 @@ pub fn ja4x(cert_der: &[u8]) -> Result<String> {
|
||||||
|
|
||||||
type OidList = SmallVec<[String; 8]>;
|
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)> {
|
fn certificate_oids(cert_der: &[u8]) -> Result<(OidList, OidList, OidList)> {
|
||||||
let mut outer = Der::new(cert_der);
|
let mut outer = Der::new(cert_der);
|
||||||
let (_, certificate) = outer.read_tlv()?;
|
let (_, certificate) = outer.read_tlv()?;
|
||||||
|
|
@ -44,19 +53,24 @@ fn certificate_oids(cert_der: &[u8]) -> Result<(OidList, OidList, OidList)> {
|
||||||
fields.push(walker.read_tlv()?);
|
fields.push(walker.read_tlv()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut idx = 0;
|
let mut cursor = 0usize;
|
||||||
if fields.first().is_some_and(|(t, _)| *t == 0xa0) {
|
if fields.first().is_some_and(|(t, _)| *t == tag::CONTEXT_0) {
|
||||||
idx += 1;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
idx += 2;
|
let _serial = field_content(&fields, cursor)?;
|
||||||
let issuer = field_content(&fields, idx)?;
|
cursor += 1;
|
||||||
idx += 1;
|
let _signature = field_content(&fields, cursor)?;
|
||||||
idx += 1;
|
cursor += 1;
|
||||||
let subject = field_content(&fields, idx)?;
|
let issuer = field_content(&fields, cursor)?;
|
||||||
idx += 1;
|
cursor += 1;
|
||||||
idx += 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()
|
.iter()
|
||||||
.find(|(t, _)| *t == tag::CONTEXT_3)
|
.find(|(t, _)| *t == tag::CONTEXT_3)
|
||||||
.map(|(_, c)| *c);
|
.map(|(_, c)| *c);
|
||||||
|
|
@ -156,6 +170,31 @@ mod tests {
|
||||||
assert_eq!(oids.as_slice(), &["550406", "55040a", "55040b", "550403"]);
|
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]
|
#[test]
|
||||||
fn extracts_extension_oids() {
|
fn extracts_extension_oids() {
|
||||||
let mut sequence = Vec::new();
|
let mut sequence = Vec::new();
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ja4t_walk_reproduces_the_linux_default_vector() {
|
fn ja4t_walk_reproduces_the_windows_default_vector() {
|
||||||
let options = [
|
let options = [
|
||||||
0x02, 0x04, 0x05, 0xb4, 0x01, 0x03, 0x03, 0x08, 0x01, 0x01, 0x04, 0x02,
|
0x02, 0x04, 0x05, 0xb4, 0x01, 0x03, 0x03, 0x08, 0x01, 0x01, 0x04, 0x02,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@ impl Pipeline {
|
||||||
let opened = if let Some(keys) = flow.keys.as_ref() {
|
let opened = if let Some(keys) = flow.keys.as_ref() {
|
||||||
packet.open(keys, flow.largest_pn).ok()
|
packet.open(keys, flow.largest_pn).ok()
|
||||||
} else {
|
} else {
|
||||||
let candidate = InitialKeys::client(packet.dcid);
|
let candidate = packet.client_keys();
|
||||||
match packet.open(&candidate, None) {
|
match packet.open(&candidate, None) {
|
||||||
Ok(opened) => {
|
Ok(opened) => {
|
||||||
flow.keys = Some(candidate);
|
flow.keys = Some(candidate);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ use crate::registry::handshake_type;
|
||||||
/// The QUIC version 1 code point, from RFC 9000.
|
/// The QUIC version 1 code point, from RFC 9000.
|
||||||
pub const VERSION_1: u32 = 0x0000_0001;
|
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
|
/// 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
|
/// 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
|
/// 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,
|
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 KEY_LEN: usize = 16;
|
||||||
const IV_LEN: usize = 12;
|
const IV_LEN: usize = 12;
|
||||||
const HP_LEN: usize = 16;
|
const HP_LEN: usize = 16;
|
||||||
|
|
@ -79,10 +91,11 @@ const PN_SAMPLE_OFFSET: usize = 4;
|
||||||
const MAX_CID_LEN: usize = 20;
|
const MAX_CID_LEN: usize = 20;
|
||||||
|
|
||||||
/// The high bit of the first byte marks a long header; the next bit is the
|
/// 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 LONG_HEADER_FORM: u8 = 0x80;
|
||||||
const PACKET_TYPE_MASK: u8 = 0x30;
|
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.
|
/// Header protection masks the low four bits of a long header's first byte.
|
||||||
const LONG_HEADER_PN_MASK: u8 = 0x0f;
|
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");
|
.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<QuicVersion> {
|
||||||
|
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.
|
/// The AEAD and header protection keys for one direction of one QUIC Initial.
|
||||||
pub struct InitialKeys {
|
pub struct InitialKeys {
|
||||||
key: [u8; KEY_LEN],
|
key: [u8; KEY_LEN],
|
||||||
|
|
@ -182,16 +240,16 @@ pub struct InitialKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InitialKeys {
|
impl InitialKeys {
|
||||||
/// Derives the client's Initial keys from a Destination Connection ID,
|
/// Derives the client's Initial keys from a Destination Connection ID, under
|
||||||
/// for QUIC version 1.
|
/// the salt and labels of the given QUIC version.
|
||||||
///
|
///
|
||||||
/// A passive observer derives these from the connection ID alone, with no
|
/// 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
|
/// 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
|
/// what tells a client Initial apart from a server one: only a packet the
|
||||||
/// client actually protected under these keys will verify.
|
/// client actually protected under these keys will verify.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn client(dcid: &[u8]) -> Self {
|
fn client(dcid: &[u8], version: QuicVersion) -> Self {
|
||||||
let (initial_secret, _) = Hkdf::<Sha256>::extract(Some(&INITIAL_SALT_V1), dcid);
|
let (initial_secret, _) = Hkdf::<Sha256>::extract(Some(version.salt), dcid);
|
||||||
let mut client_secret = [0u8; 32];
|
let mut client_secret = [0u8; 32];
|
||||||
hkdf_expand_label(&initial_secret, b"client in", &mut client_secret);
|
hkdf_expand_label(&initial_secret, b"client in", &mut client_secret);
|
||||||
|
|
||||||
|
|
@ -200,9 +258,9 @@ impl InitialKeys {
|
||||||
iv: [0u8; IV_LEN],
|
iv: [0u8; IV_LEN],
|
||||||
hp: [0u8; HP_LEN],
|
hp: [0u8; HP_LEN],
|
||||||
};
|
};
|
||||||
hkdf_expand_label(&client_secret, b"quic key", &mut keys.key);
|
hkdf_expand_label(&client_secret, version.key_label, &mut keys.key);
|
||||||
hkdf_expand_label(&client_secret, b"quic iv", &mut keys.iv);
|
hkdf_expand_label(&client_secret, version.iv_label, &mut keys.iv);
|
||||||
hkdf_expand_label(&client_secret, b"quic hp", &mut keys.hp);
|
hkdf_expand_label(&client_secret, version.hp_label, &mut keys.hp);
|
||||||
keys
|
keys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +276,9 @@ pub struct InitialPacket<'pkt> {
|
||||||
datagram: &'pkt [u8],
|
datagram: &'pkt [u8],
|
||||||
/// The Destination Connection ID, which seeds key derivation.
|
/// The Destination Connection ID, which seeds key derivation.
|
||||||
pub dcid: &'pkt [u8],
|
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.
|
/// Absolute offset of this packet's first byte within the datagram.
|
||||||
start: usize,
|
start: usize,
|
||||||
/// Absolute offset of the packet number field within the datagram.
|
/// Absolute offset of the packet number field within the datagram.
|
||||||
|
|
@ -250,16 +311,16 @@ impl<'pkt> InitialPacket<'pkt> {
|
||||||
return Err(ParseError::NotQuicInitial);
|
return Err(ParseError::NotQuicInitial);
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = u32::from_be_bytes(
|
let version_code = u32::from_be_bytes(
|
||||||
buf.get(1..5)
|
buf.get(1..5)
|
||||||
.ok_or(ParseError::NotQuicInitial)?
|
.ok_or(ParseError::NotQuicInitial)?
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("a four byte slice is four bytes"),
|
.expect("a four byte slice is four bytes"),
|
||||||
);
|
);
|
||||||
if version != VERSION_1 {
|
let Some(version) = QuicVersion::from_code(version_code) else {
|
||||||
return Err(ParseError::UnsupportedQuicVersion(version));
|
return Err(ParseError::UnsupportedQuicVersion(version_code));
|
||||||
}
|
};
|
||||||
if first & PACKET_TYPE_MASK != PACKET_TYPE_INITIAL {
|
if first & PACKET_TYPE_MASK != version.initial_type {
|
||||||
return Err(ParseError::NotQuicInitial);
|
return Err(ParseError::NotQuicInitial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,6 +352,7 @@ impl<'pkt> InitialPacket<'pkt> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
datagram,
|
datagram,
|
||||||
dcid,
|
dcid,
|
||||||
|
version,
|
||||||
start,
|
start,
|
||||||
pn_offset,
|
pn_offset,
|
||||||
end,
|
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
|
/// Removes header protection and decrypts the payload, returning the
|
||||||
/// cleartext QUIC frames.
|
/// cleartext QUIC frames.
|
||||||
///
|
///
|
||||||
|
|
@ -622,8 +691,8 @@ pub enum ClientHelloState<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
ClientHelloState, CryptoAssembler, InitialKeys, InitialPacket, decode_packet_number,
|
ClientHelloState, CryptoAssembler, InitialKeys, InitialPacket, QuicVersion,
|
||||||
hkdf_expand_label, read_varint, walk_crypto_frames,
|
decode_packet_number, hkdf_expand_label, read_varint, walk_crypto_frames,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// RFC 9000 Appendix A.1 sample variable length integer decodings.
|
/// RFC 9000 Appendix A.1 sample variable length integer decodings.
|
||||||
|
|
@ -654,12 +723,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn client_initial_keys_match_rfc9001_appendix_a1() {
|
fn client_initial_keys_match_rfc9001_appendix_a1() {
|
||||||
let dcid = hex("8394c8f03e515708");
|
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.key.to_vec(), hex("1f369613dd76d5467730efcbe3b1a22d"));
|
||||||
assert_eq!(keys.iv.to_vec(), hex("fa044b2f42a3fd3b46fb255c"));
|
assert_eq!(keys.iv.to_vec(), hex("fa044b2f42a3fd3b46fb255c"));
|
||||||
assert_eq!(keys.hp.to_vec(), hex("9f50449e04a0e810283a1e9933adedd2"));
|
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.
|
/// RFC 9001 Appendix A.1 also pins the intermediate expand label output.
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_label_reproduces_rfc9001_client_secret() {
|
fn expand_label_reproduces_rfc9001_client_secret() {
|
||||||
|
|
@ -683,7 +763,7 @@ mod tests {
|
||||||
let packet = InitialPacket::parse(&datagram, 0).unwrap();
|
let packet = InitialPacket::parse(&datagram, 0).unwrap();
|
||||||
assert_eq!(packet.dcid, &hex("8394c8f03e515708")[..]);
|
assert_eq!(packet.dcid, &hex("8394c8f03e515708")[..]);
|
||||||
|
|
||||||
let keys = InitialKeys::client(packet.dcid);
|
let keys = packet.client_keys();
|
||||||
let opened = packet.open(&keys, None).unwrap();
|
let opened = packet.open(&keys, None).unwrap();
|
||||||
assert_eq!(opened.packet_number, 2);
|
assert_eq!(opened.packet_number, 2);
|
||||||
|
|
||||||
|
|
@ -732,6 +812,30 @@ mod tests {
|
||||||
assert!(InitialPacket::parse(udp, 0).is_err());
|
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<u8> {
|
fn hex(s: &str) -> Vec<u8> {
|
||||||
::hex::decode(s).unwrap()
|
::hex::decode(s).unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::<u8>(), 0..4096),
|
||||||
|
) {
|
||||||
|
let _ = tlsfp_core::ja4x(&bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
//!
|
//!
|
||||||
//! The rules, in the order they are evaluated:
|
//! 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
|
//! - 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
|
//! 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.
|
//! 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)?;
|
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)? {
|
if !ip_fp_known {
|
||||||
alerts.push(alert);
|
if let Some(alert) = known_bad(conn, ts, &ip_text, &reports, kind, &value)? {
|
||||||
|
alerts.push(alert);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fp_known {
|
if !fp_known {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,25 @@ use std::fmt;
|
||||||
|
|
||||||
use serde::Serialize;
|
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.
|
/// Which fingerprint algorithm a stored value belongs to.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
@ -284,11 +303,11 @@ impl MatchReport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let total = malicious + suspicious + benign;
|
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
|
Verdict::Malicious
|
||||||
} else if threat_score <= 0.2 {
|
} else if threat_score <= BENIGN_THRESHOLD {
|
||||||
Verdict::Benign
|
Verdict::Benign
|
||||||
} else {
|
} else {
|
||||||
Verdict::Suspicious
|
Verdict::Suspicious
|
||||||
|
|
@ -299,7 +318,7 @@ impl MatchReport {
|
||||||
.filter(|hit| verdict_aligns(verdict, hit.category.severity()))
|
.filter(|hit| verdict_aligns(verdict, hit.category.severity()))
|
||||||
.count();
|
.count();
|
||||||
let corroboration = 1.0 - 1.0 / (1.0 + count_to_f64(aligned));
|
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 {
|
Self {
|
||||||
kind,
|
kind,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@
|
||||||
|
|
||||||
use crate::model::Category;
|
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
|
/// A coarse operating-system class, the resolution the SYN signature can carry
|
||||||
/// without guessing. Finer naming from one packet is not reliable, so the
|
/// without guessing. Finer naming from one packet is not reliable, so the
|
||||||
/// mismatch rule works at this granularity on purpose.
|
/// mismatch rule works at this granularity on purpose.
|
||||||
|
|
@ -216,7 +226,7 @@ pub fn ua_family(user_agent: &str) -> Option<Family> {
|
||||||
if ua.contains("okhttp") {
|
if ua.contains("okhttp") {
|
||||||
return Some(Family::OkHttp);
|
return Some(Family::OkHttp);
|
||||||
}
|
}
|
||||||
if ua.contains("java") {
|
if contains_word(&ua, "java") {
|
||||||
return Some(Family::Java);
|
return Some(Family::Java);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -263,10 +273,10 @@ pub fn label_family(label: &str) -> Option<Family> {
|
||||||
if label.contains("okhttp") {
|
if label.contains("okhttp") {
|
||||||
return Some(Family::OkHttp);
|
return Some(Family::OkHttp);
|
||||||
}
|
}
|
||||||
if label.contains("java") {
|
if contains_word(&label, "java") {
|
||||||
return Some(Family::Java);
|
return Some(Family::Java);
|
||||||
}
|
}
|
||||||
if label.contains("tor") {
|
if contains_word(&label, "tor") {
|
||||||
return Some(Family::Tor);
|
return Some(Family::Tor);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -414,4 +424,13 @@ mod tests {
|
||||||
assert!(!label_is_non_browser("Google Chrome", Category::Benign));
|
assert!(!label_is_non_browser("Google Chrome", Category::Benign));
|
||||||
assert!(!label_is_non_browser("unrecognised", 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,33 @@ fn known_bad_fires_on_malicious_fingerprint() {
|
||||||
assert_eq!(known_bad.score, Some(1.0));
|
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]
|
#[test]
|
||||||
fn ua_mismatch_browser_claim_over_tool_handshake() {
|
fn ua_mismatch_browser_claim_over_tool_handshake() {
|
||||||
let mut store = store();
|
let mut store = store();
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json"]
|
"includes": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.jsx",
|
||||||
|
"**/*.json",
|
||||||
|
"!**/node_modules",
|
||||||
|
"!**/.pnpm-store",
|
||||||
|
"!**/dist"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 709 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB |
|
|
@ -40,7 +40,7 @@
|
||||||
border-left-color: var(--accent);
|
border-left-color: var(--accent);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 77, 18, 0.07),
|
rgb(255, 77, 18, 7%),
|
||||||
transparent 50%
|
transparent 50%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: $space-3;
|
padding: $space-3;
|
||||||
background: rgba(8, 9, 10, 0.82);
|
background: rgb(8, 9, 10, 82%);
|
||||||
border: 1px solid $line;
|
border: 1px solid $line;
|
||||||
border-left: 2px solid var(--accent);
|
border-left: 2px solid var(--accent);
|
||||||
box-shadow: 0 0 20px -10px var(--accent);
|
box-shadow: 0 0 20px -10px var(--accent);
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: $chalk;
|
color: $chalk;
|
||||||
text-shadow: 0 0 11px rgba(255, 77, 18, 0.18);
|
text-shadow: 0 0 11px rgb(255, 77, 18, 18%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject {
|
.subject {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent);
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgb(0, 0, 0, 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cat {
|
.cat {
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@
|
||||||
90deg,
|
90deg,
|
||||||
transparent 0,
|
transparent 0,
|
||||||
transparent 7px,
|
transparent 7px,
|
||||||
rgba(255, 255, 255, 0.03) 7px,
|
rgb(255, 255, 255, 3%) 7px,
|
||||||
rgba(255, 255, 255, 0.03) 8px
|
rgb(255, 255, 255, 3%) 8px
|
||||||
),
|
),
|
||||||
$ink-700;
|
$ink-700;
|
||||||
border: 1px solid $line-faint;
|
border: 1px solid $line-faint;
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 7px -4px var(--accent),
|
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;
|
transition: width $duration-slow $ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@
|
||||||
.scroll {
|
.scroll {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
overflow-y: auto;
|
overflow: hidden auto;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
@ -159,14 +158,14 @@
|
||||||
border-left: 2px solid $fire;
|
border-left: 2px solid $fire;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 77, 18, 0.14),
|
rgb(255, 77, 18, 14%),
|
||||||
transparent 55%
|
transparent 55%
|
||||||
);
|
);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 77, 18, 0.2),
|
rgb(255, 77, 18, 20%),
|
||||||
transparent 55%
|
transparent 55%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +175,7 @@
|
||||||
border-left: 2px solid var(--accent);
|
border-left: 2px solid var(--accent);
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
90deg,
|
90deg,
|
||||||
rgba(255, 174, 58, 0.08),
|
rgb(255, 174, 58, 8%),
|
||||||
transparent 60%
|
transparent 60%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +221,7 @@
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-7px);
|
transform: translateY(-7px);
|
||||||
background: rgba(47, 123, 255, 0.09);
|
background: rgb(47, 123, 255, 9%);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $space-4;
|
gap: $space-4;
|
||||||
padding: $space-4 $space-7;
|
padding: $space-4 $space-7;
|
||||||
background: rgba(5, 5, 6, 0.86);
|
background: rgb(5, 5, 6, 86%);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-bottom: 1px solid $line;
|
border-bottom: 1px solid $line;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
mix-blend-mode: screen;
|
mix-blend-mode: screen;
|
||||||
opacity: 0.5;
|
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;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
mix-blend-mode: screen;
|
mix-blend-mode: screen;
|
||||||
opacity: 0.92;
|
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;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
letter-spacing: 0.34em;
|
letter-spacing: 0.34em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: $fire;
|
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;
|
margin-bottom: $space-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
color: $chalk;
|
color: $chalk;
|
||||||
margin: 0 0 $space-5;
|
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
|
// The mono tag reads as a tight stamp under the masthead, so it is kept short
|
||||||
|
|
|
||||||
|
|
@ -83,22 +83,20 @@
|
||||||
.grid {
|
.grid {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.62fr) minmax(0, 1fr);
|
grid-template:
|
||||||
grid-template-rows: minmax(300px, auto) minmax(260px, auto) auto;
|
'live anomaly' minmax(300px, auto)
|
||||||
grid-template-areas:
|
'live dist' minmax(260px, auto)
|
||||||
'live anomaly'
|
'alerts alerts' auto
|
||||||
'live dist'
|
/ minmax(0, 1.62fr) minmax(0, 1fr);
|
||||||
'alerts alerts';
|
|
||||||
gap: $space-4;
|
gap: $space-4;
|
||||||
|
|
||||||
@include breakpoint-down('lg') {
|
@include breakpoint-down('lg') {
|
||||||
grid-template-columns: 1fr;
|
grid-template:
|
||||||
grid-template-rows: none;
|
|
||||||
grid-template-areas:
|
|
||||||
'live'
|
'live'
|
||||||
'anomaly'
|
'anomaly'
|
||||||
'dist'
|
'dist'
|
||||||
'alerts';
|
'alerts'
|
||||||
|
/ 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Anton';
|
font-family: Anton;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|
@ -66,24 +66,25 @@ body {
|
||||||
font-family: $font-grotesk;
|
font-family: $font-grotesk;
|
||||||
font-size: $font-size-sm;
|
font-size: $font-size-sm;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
|
|
||||||
// A breath of grain over the whole field so the black reads as a printed
|
// A breath of grain over the whole field so the black reads as a printed
|
||||||
// surface, not a switched-off screen.
|
// surface, not a switched-off screen.
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(
|
radial-gradient(
|
||||||
ellipse 120% 80% at 50% -10%,
|
ellipse 120% 80% at 50% -10%,
|
||||||
rgba(47, 123, 255, 0.05),
|
rgb(47, 123, 255, 5%),
|
||||||
transparent 60%
|
transparent 60%
|
||||||
),
|
),
|
||||||
radial-gradient(
|
radial-gradient(
|
||||||
ellipse 90% 60% at 100% 110%,
|
ellipse 90% 60% at 100% 110%,
|
||||||
rgba(255, 77, 18, 0.04),
|
rgb(255, 77, 18, 4%),
|
||||||
transparent 55%
|
transparent 55%
|
||||||
);
|
);
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(255, 77, 18, 0.85);
|
background: rgb(255, 77, 18, 85%);
|
||||||
color: $void;
|
color: $void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,15 +136,19 @@ body {
|
||||||
[data-sev='info'] {
|
[data-sev='info'] {
|
||||||
--accent: #{$sev-info};
|
--accent: #{$sev-info};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sev='low'] {
|
[data-sev='low'] {
|
||||||
--accent: #{$sev-low};
|
--accent: #{$sev-low};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sev='medium'] {
|
[data-sev='medium'] {
|
||||||
--accent: #{$sev-medium};
|
--accent: #{$sev-medium};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sev='high'] {
|
[data-sev='high'] {
|
||||||
--accent: #{$sev-high};
|
--accent: #{$sev-high};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-sev='critical'] {
|
[data-sev='critical'] {
|
||||||
--accent: #{$sev-critical};
|
--accent: #{$sev-critical};
|
||||||
}
|
}
|
||||||
|
|
@ -151,12 +156,15 @@ body {
|
||||||
[data-verdict='malicious'] {
|
[data-verdict='malicious'] {
|
||||||
--accent: #{$verdict-malicious};
|
--accent: #{$verdict-malicious};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-verdict='suspicious'] {
|
[data-verdict='suspicious'] {
|
||||||
--accent: #{$verdict-suspicious};
|
--accent: #{$verdict-suspicious};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-verdict='benign'] {
|
[data-verdict='benign'] {
|
||||||
--accent: #{$verdict-benign};
|
--accent: #{$verdict-benign};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-verdict='unknown'] {
|
[data-verdict='unknown'] {
|
||||||
--accent: #{$verdict-unknown};
|
--accent: #{$verdict-unknown};
|
||||||
}
|
}
|
||||||
|
|
@ -164,18 +172,23 @@ body {
|
||||||
[data-cat='malware'] {
|
[data-cat='malware'] {
|
||||||
--accent: #{$cat-malware};
|
--accent: #{$cat-malware};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-cat='c2'] {
|
[data-cat='c2'] {
|
||||||
--accent: #{$cat-c2};
|
--accent: #{$cat-c2};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-cat='tool'] {
|
[data-cat='tool'] {
|
||||||
--accent: #{$cat-tool};
|
--accent: #{$cat-tool};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-cat='benign'] {
|
[data-cat='benign'] {
|
||||||
--accent: #{$cat-benign};
|
--accent: #{$cat-benign};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-cat='os'] {
|
[data-cat='os'] {
|
||||||
--accent: #{$cat-os};
|
--accent: #{$cat-os};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-cat='unknown'] {
|
[data-cat='unknown'] {
|
||||||
--accent: #{$cat-unknown};
|
--accent: #{$cat-unknown};
|
||||||
}
|
}
|
||||||
|
|
@ -183,18 +196,23 @@ body {
|
||||||
[data-rule='known_bad'] {
|
[data-rule='known_bad'] {
|
||||||
--accent: #{$blood};
|
--accent: #{$blood};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-rule='ua_mismatch'] {
|
[data-rule='ua_mismatch'] {
|
||||||
--accent: #{$fire};
|
--accent: #{$fire};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-rule='os_mismatch'] {
|
[data-rule='os_mismatch'] {
|
||||||
--accent: #{$magma};
|
--accent: #{$magma};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-rule='fp_rotation'] {
|
[data-rule='fp_rotation'] {
|
||||||
--accent: #{$volt};
|
--accent: #{$volt};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-rule='monoculture'] {
|
[data-rule='monoculture'] {
|
||||||
--accent: #{$orchid};
|
--accent: #{$orchid};
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-rule='first_seen'] {
|
[data-rule='first_seen'] {
|
||||||
--accent: #{$thermal};
|
--accent: #{$thermal};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
// the instrument itself. Neutral type sits between.
|
// the instrument itself. Neutral type sits between.
|
||||||
|
|
||||||
// The void and the panels lifted just off it.
|
// The void and the panels lifted just off it.
|
||||||
$void: #000000;
|
$void: #000;
|
||||||
$ink-1000: #050506;
|
$ink-1000: #050506;
|
||||||
$ink-900: #0a0b0d;
|
$ink-900: #0a0b0d;
|
||||||
$ink-800: #0f1113;
|
$ink-800: #0f1113;
|
||||||
|
|
@ -18,7 +18,7 @@ $ink-600: #1c2024;
|
||||||
$ink-500: #262b30;
|
$ink-500: #262b30;
|
||||||
|
|
||||||
// Bone white for the artifacts, chalk for the few things that must shout.
|
// Bone white for the artifacts, chalk for the few things that must shout.
|
||||||
$chalk: #ffffff;
|
$chalk: #fff;
|
||||||
$bone: #f2f0e9;
|
$bone: #f2f0e9;
|
||||||
$paper: #d8d6cd;
|
$paper: #d8d6cd;
|
||||||
|
|
||||||
|
|
@ -28,10 +28,10 @@ $dust: #5a5f65;
|
||||||
$soot: #34383d;
|
$soot: #34383d;
|
||||||
|
|
||||||
// Hairlines. The grid the specimens are pinned to.
|
// Hairlines. The grid the specimens are pinned to.
|
||||||
$line-faint: rgba(255, 255, 255, 0.06);
|
$line-faint: rgb(255, 255, 255, 6%);
|
||||||
$line: rgba(255, 255, 255, 0.11);
|
$line: rgb(255, 255, 255, 11%);
|
||||||
$line-strong: rgba(255, 255, 255, 0.2);
|
$line-strong: rgb(255, 255, 255, 20%);
|
||||||
$line-hot: rgba(255, 77, 18, 0.55);
|
$line-hot: rgb(255, 77, 18, 55%);
|
||||||
|
|
||||||
// The hot channels: what the x-ray exposes, what bleeds through the black.
|
// The hot channels: what the x-ray exposes, what bleeds through the black.
|
||||||
$fire: #ff4d12; // the butterfly's fire, the snake; primary anomaly
|
$fire: #ff4d12; // the butterfly's fire, the snake; primary anomaly
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue