use crate::{TechCategory, Technology};
#[derive(Debug, Clone)]
pub struct BehaviorFingerprint {
pub server: Option<String>,
pub confidence: u8,
pub probes: Vec<ProbeResult>,
}
#[derive(Debug, Clone)]
pub struct ProbeResult {
pub probe_name: &'static str,
pub status: u16,
pub body_prefix: String,
pub has_server_header: bool,
pub content_type: Option<String>,
}
const SIGNATURES: &[(&str, u16, &str, &str, &str)] = &[
("invalid_method", 405, "<center>nginx", "", "nginx"),
(
"invalid_method",
501,
"Method Not Implemented",
"",
"apache",
),
(
"invalid_method",
405,
"Method Not Allowed",
"text/html",
"iis",
),
("invalid_method", 405, "", "", "caddy"),
("invalid_method", 404, "Cannot ", "", "express"),
("overlong_uri", 414, "Request-URI Too Large", "", "nginx"),
("overlong_uri", 414, "Request-URI Too Long", "", "apache"),
("missing_host", 400, "400 Bad Request", "", "nginx"),
("missing_host", 400, "Bad Request", "text/html", "apache"),
];
pub async fn identify(
client: &reqwest::Client,
base_url: &str,
technologies: &mut Vec<Technology>,
) -> anyhow::Result<()> {
let mut probes_results = Vec::new();
for (probe_name, method_str, url, extra_headers) in probes(base_url) {
let method = reqwest::Method::from_bytes(method_str.as_bytes())?;
let mut req = client.request(method, &url);
for (k, v) in extra_headers {
req = req.header(k, v);
}
match req.send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let has_server_header = resp.headers().contains_key("server");
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let body = resp.text().await.unwrap_or_default();
let body_prefix = body.chars().take(512).collect();
probes_results.push(ProbeResult {
probe_name,
status,
body_prefix,
has_server_header,
content_type,
});
}
Err(_) => {
probes_results.push(ProbeResult {
probe_name,
status: 0,
body_prefix: String::new(),
has_server_header: false,
content_type: None,
});
}
}
}
if let Some(tech) = identify_from_probes(&probes_results) {
technologies.push(tech);
}
Ok(())
}
fn identify_from_probes(probes: &[ProbeResult]) -> Option<Technology> {
let mut scores: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();
for probe in probes {
for &(probe_name, status, body_sig, ct_sig, server) in SIGNATURES {
if probe.probe_name == probe_name
&& probe.status == status
&& (body_sig.is_empty() || probe.body_prefix.contains(body_sig))
&& (ct_sig.is_empty()
|| probe.content_type.as_deref().unwrap_or("").contains(ct_sig))
{
*scores.entry(server).or_insert(0) += 1;
}
}
}
let (best, &count) = scores.iter().max_by_key(|(_, &v)| v)?;
let total_probes = probes.len() as u32;
let confidence = ((count as f64 / total_probes.max(1) as f64) * 100.0).min(100.0) as u8;
if confidence < 30 {
return None;
}
Some(Technology {
name: best.to_string(),
version: None,
category: TechCategory::Server,
confidence,
})
}
pub fn probes(
base_url: &str,
) -> Vec<(
&'static str,
&'static str,
String,
Vec<(&'static str, &'static str)>,
)> {
vec![
("invalid_method", "XYZMETHOD", base_url.to_string(), vec![]),
(
"overlong_uri",
"GET",
format!("{}/{}", base_url, "A".repeat(8192)),
vec![],
),
("trace_method", "TRACE", base_url.to_string(), vec![]),
]
}