Cybersecurity-Projects/PROJECTS/intermediate/ja3-ja4-tls-fingerprinting/crates/tlsfp/tests/report.rs

119 lines
3.6 KiB
Rust

// ©AngelaMos | 2026
// report.rs
//! End to end coverage of the forensic report mode.
//!
//! These drive the real binary against a vendored capture the way an analyst
//! would, then assert on what it prints. The capture is read with a database
//! path that does not exist, so the report runs as a pure fingerprint inventory
//! with no intelligence side effects and no dependence on a seeded store, which
//! keeps the test hermetic.
use std::path::PathBuf;
use std::process::Command;
/// The vendored capture used throughout: a browser session with TLS, QUIC, and
/// certificate handshakes, enough to exercise every section of the report.
fn capture() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../testdata/pcap/tls-handshake.pcapng")
}
/// A database path under the temp directory that is guaranteed not to exist, so
/// the report stays a pure inventory and never writes a store behind the test.
fn absent_db() -> PathBuf {
std::env::temp_dir().join(format!(
"tlsfp-report-test-{}-absent.db",
std::process::id()
))
}
fn run(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_tlsfp"))
.args(args)
.output()
.expect("running the tlsfp binary")
}
#[test]
fn text_report_has_every_section() {
let db = absent_db();
let output = run(&[
"pcap",
capture().to_str().unwrap(),
"--report",
"--db",
db.to_str().unwrap(),
]);
assert!(output.status.success(), "report run failed");
let text = String::from_utf8(output.stdout).expect("utf8 report");
for section in [
"== capture ==",
"== fingerprints by kind ==",
"== top client ja4 ==",
"== endpoints ==",
"== intelligence ==",
"== alerts ==",
"== coverage ==",
] {
assert!(text.contains(section), "missing section {section}");
}
assert!(text.contains("t13d1516h2_8daaf6152771_e5627efa2ab1"));
assert!(text.contains("q13d0310h3_55b375c5d22e_cd85d2d88918"));
assert!(text.contains("tls miss rate"));
assert!(text.contains("fp/s"));
assert!(!db.exists(), "report must not create a database");
}
#[test]
fn json_report_is_well_formed() {
let db = absent_db();
let output = run(&[
"pcap",
capture().to_str().unwrap(),
"--report",
"--json",
"--db",
db.to_str().unwrap(),
]);
assert!(output.status.success(), "json report run failed");
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("report is valid JSON");
assert!(value["capture"]["frames"].as_u64().unwrap() > 0);
assert!(
value["coverage"]["counters"]["tls_handshakes_fingerprinted"]
.as_u64()
.unwrap()
> 0
);
assert!(value["coverage"]["events_per_sec"].as_f64().unwrap() > 0.0);
assert!(
!value["distribution"]["top_ja4"]
.as_array()
.unwrap()
.is_empty()
);
let endpoints = value["endpoints"].as_array().unwrap();
assert!(endpoints.iter().any(|e| e["ip"] == "192.168.1.168"));
}
#[test]
fn top_flag_caps_ranked_rows() {
let db = absent_db();
let output = run(&[
"pcap",
capture().to_str().unwrap(),
"--report",
"--json",
"--top",
"2",
"--db",
db.to_str().unwrap(),
]);
assert!(output.status.success(), "capped report run failed");
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("report is valid JSON");
assert!(value["distribution"]["top_sni"].as_array().unwrap().len() <= 2);
}