use crate::fingerprint::detection::{Category, Detection, Evidence, EvidenceSource, Tier, Vendor};
use crate::fingerprint::target::TargetContext;
use super::Source;
#[derive(Default)]
pub struct WellKnownSource;
impl WellKnownSource {
pub fn new() -> Self {
Self
}
pub fn classify(path: &str, status: u16, body_has_content: bool) -> Vec<Detection> {
if status != 200 || !body_has_content {
return Vec::new();
}
let mut out: Vec<Detection> = Vec::new();
if path.ends_with("/security.txt") {
out.push(Detection::from_single(
Category::Other,
Vendor::Generic,
Evidence::new(
EvidenceSource::WellKnown,
"/.well-known/security.txt present",
3,
),
));
}
if path.ends_with("/openid-configuration") {
out.push(Detection::from_single(
Category::Auth,
Vendor::Unknown,
Evidence::new(
EvidenceSource::WellKnown,
"/.well-known/openid-configuration present (OIDC issuer)",
8,
),
));
}
out
}
}
impl Source for WellKnownSource {
fn name(&self) -> &'static str {
"well_known"
}
fn tier(&self) -> Tier {
Tier::Warm
}
fn analyze(&self, _ctx: &TargetContext<'_>) -> Vec<Detection> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_security_txt() {
let dets = WellKnownSource::classify("/.well-known/security.txt", 200, true);
assert_eq!(dets.len(), 1);
}
#[test]
fn classifies_openid_configuration() {
let dets = WellKnownSource::classify("/.well-known/openid-configuration", 200, true);
assert!(dets.iter().any(|d| d.category == Category::Auth));
}
#[test]
fn skips_404() {
let dets = WellKnownSource::classify("/.well-known/security.txt", 404, false);
assert!(dets.is_empty());
}
}