use super::{ScanContext, ScanPhase};
use crate::{Args, Result};
use async_trait::async_trait;
pub struct FingerprintPhase;
impl FingerprintPhase {
pub fn new() -> Self {
Self
}
async fn capture_ja3(&self, context: &mut ScanContext) -> Result<()> {
use crate::fingerprint::{ClientHelloNetworkCapture, Ja3Database};
let target = context.target();
let capture = ClientHelloNetworkCapture::new(target);
let (client_hello, ja3) = capture.capture_and_fingerprint().await?;
let fingerprints = context.results.fingerprints_mut();
fingerprints.ja3_fingerprint = Some(ja3.clone());
let db = if let Some(ref db_path) = context.args.fingerprint.ja3_database {
Ja3Database::from_file(db_path).unwrap_or_default()
} else {
Ja3Database::default()
};
if let Some(signature) = db.match_fingerprint(&ja3.ja3_hash) {
fingerprints.ja3_match = Some(signature.clone());
}
if context.args.fingerprint.client_hello {
fingerprints.client_hello_raw = Some(client_hello.to_bytes());
}
Ok(())
}
async fn capture_ja3s(&self, context: &mut ScanContext) -> Result<()> {
use crate::fingerprint::{Ja3sDatabase, Ja3sFingerprint, ServerHelloNetworkCapture};
let target = context.target();
let capturer = ServerHelloNetworkCapture::new(target);
let server_hello = capturer.capture()?;
let server_hello_raw = server_hello.to_bytes();
let ja3s = Ja3sFingerprint::from_server_hello(&server_hello);
let ja3s_db = match Ja3sDatabase::load_default() {
Ok(db) => db,
Err(e) => {
eprintln!("Warning: Failed to load JA3S database: {}", e);
eprintln!("Continuing without JA3S signature matching");
Ja3sDatabase::empty()
}
};
let ja3s_match = ja3s_db.match_fingerprint(&ja3s.ja3s_hash).cloned();
let header_map: Option<std::collections::HashMap<String, String>> = context
.results
.http
.as_ref()
.and_then(|http| http.http_headers.as_ref())
.map(|http_headers| {
http_headers
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
});
if let Some(header_map) = header_map {
let advanced = context.results.advanced_mut();
advanced.cdn_detection = Some(crate::fingerprint::CdnDetection::from_ja3s_and_headers(
&ja3s,
ja3s_match.as_ref(),
&header_map,
));
advanced.load_balancer_info =
Some(crate::fingerprint::LoadBalancerInfo::from_ja3s_and_headers(
ja3s_match.as_ref(),
&header_map,
));
}
let fingerprints = context.results.fingerprints_mut();
fingerprints.ja3s_fingerprint = Some(ja3s);
fingerprints.ja3s_match = ja3s_match;
if context.args.fingerprint.server_hello {
fingerprints.server_hello_raw = Some(server_hello_raw);
}
Ok(())
}
async fn capture_jarm(&self, context: &mut ScanContext) -> Result<()> {
use crate::fingerprint::{JarmDatabase, JarmFingerprinter};
use std::time::Duration;
let database = if let Some(ref db_path) = context.args.fingerprint.jarm_database {
JarmDatabase::from_file(
db_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid JARM database path"))?,
)
.unwrap_or_else(|e| {
eprintln!("Warning: Failed to load custom JARM database: {}", e);
eprintln!("Falling back to builtin database");
JarmDatabase::builtin()
})
} else {
JarmDatabase::builtin()
};
let timeout = Duration::from_secs(context.args.connection.socket_timeout.unwrap_or(5));
let fingerprinter = JarmFingerprinter::with_database(timeout, database);
let ip = context
.target
.ip_addresses
.first()
.ok_or_else(|| anyhow::anyhow!("No IP address resolved for target"))?;
let addr = std::net::SocketAddr::new(*ip, context.target.port);
let hostname = context.target.hostname.clone();
let jarm = fingerprinter.fingerprint(addr, &hostname).await?;
context.results.fingerprints_mut().jarm_fingerprint = Some(jarm);
Ok(())
}
}
impl Default for FingerprintPhase {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ScanPhase for FingerprintPhase {
fn name(&self) -> &'static str {
"Capturing TLS Fingerprints"
}
fn should_run(&self, args: &Args) -> bool {
args.fingerprint.ja3 || args.fingerprint.ja3s || args.fingerprint.jarm
}
async fn execute(&self, context: &mut ScanContext) -> Result<()> {
if context.args.fingerprint.ja3
&& let Err(e) = self.capture_ja3(context).await {
eprintln!(" Failed to generate JA3 fingerprint: {}", e);
}
if context.args.fingerprint.ja3s
&& let Err(e) = self.capture_ja3s(context).await {
eprintln!(" Failed to generate JA3S fingerprint: {}", e);
}
if context.args.fingerprint.jarm
&& let Err(e) = self.capture_jarm(context).await {
eprintln!(" Failed to generate JARM fingerprint: {}", e);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_fingerprint_phase_should_run() {
let phase = FingerprintPhase::new();
let mut args = Args::default();
args.fingerprint.ja3 = true;
assert!(phase.should_run(&args));
let mut args = Args::default();
args.fingerprint.ja3s = true;
assert!(phase.should_run(&args));
let mut args = Args::default();
args.fingerprint.jarm = true;
assert!(phase.should_run(&args));
let mut args = Args::default();
args.scan.all = true;
assert!(!phase.should_run(&args));
let args = Args::default();
assert!(!phase.should_run(&args));
}
#[test]
fn test_fingerprint_phase_name() {
let phase = FingerprintPhase::new();
assert_eq!(phase.name(), "Capturing TLS Fingerprints");
}
}