use crate::Technology;
use std::collections::HashMap;
pub fn apply(mut techs: Vec<Technology>, rules: &[crate::fingerprints::Rule]) -> Vec<Technology> {
let detected_names: std::collections::HashSet<String> =
techs.iter().map(|t| t.name.clone()).collect();
let mut requires_failed: std::collections::HashSet<String> = std::collections::HashSet::new();
for rule in rules {
if detected_names.contains(&rule.name) && !rule.requires.is_empty() {
let all_met = rule.requires.iter().all(|req| detected_names.contains(req));
if !all_met {
requires_failed.insert(rule.name.clone());
}
}
}
techs.retain(|t| !requires_failed.contains(&t.name));
let mut excluded: std::collections::HashSet<String> = std::collections::HashSet::new();
for rule in rules {
if techs.iter().any(|t| t.name == rule.name) {
for exc in &rule.excludes {
excluded.insert(exc.clone());
}
}
}
techs.retain(|t| !excluded.contains(&t.name));
let server_techs: Vec<&Technology> = techs
.iter()
.filter(|t| matches!(t.category, crate::TechCategory::Server))
.collect();
if server_techs.len() >= 2 {
}
let mut best: HashMap<String, Technology> = HashMap::new();
for tech in techs {
best.entry(tech.name.clone())
.and_modify(|existing| {
if tech.confidence > existing.confidence {
*existing = tech.clone();
}
})
.or_insert(tech);
}
let deduped: Vec<Technology> = best.into_values().collect();
let implied = crate::implied::expand(&deduped);
let mut final_set = deduped;
for imp in implied {
if !final_set.iter().any(|t| t.name == imp.name) {
final_set.push(imp);
}
}
final_set
}
pub fn is_spa_catch_all(baseline_hash: u64, probe_hash: u64, probe_status: u16) -> bool {
probe_status == 200 && baseline_hash == probe_hash && baseline_hash != 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fingerprints::Rule;
use crate::{TechCategory, Technology};
fn tech(name: &str, cat: TechCategory, confidence: u8) -> Technology {
Technology {
name: name.into(),
version: None,
category: cat,
confidence,
}
}
fn rule(name: &str, excludes: Vec<&str>, requires: Vec<&str>) -> Rule {
Rule {
name: name.into(),
version_header: None,
category: TechCategory::Server,
signals: vec![],
negative_signals: vec![],
excludes: excludes.into_iter().map(ToString::to_string).collect(),
requires: requires.into_iter().map(ToString::to_string).collect(),
min_signals: 1,
}
}
#[test]
fn excludes_removes_conflicting_tech() {
let techs = vec![
tech("Cloudflare", TechCategory::Cdn, 95),
tech("nginx", TechCategory::Server, 60),
];
let rules = vec![
rule("Cloudflare", vec!["nginx"], vec![]),
rule("nginx", vec![], vec![]),
];
let result = apply(techs, &rules);
assert!(!result.iter().any(|t| t.name == "nginx"));
assert!(result.iter().any(|t| t.name == "Cloudflare"));
}
#[test]
fn requires_removes_orphaned_plugins() {
let techs = vec![
tech("Yoast SEO", TechCategory::Other, 80),
];
let rules = vec![rule("Yoast SEO", vec![], vec!["WordPress"])];
let result = apply(techs, &rules);
assert!(result.is_empty());
}
#[test]
fn requires_keeps_when_dependency_present() {
let techs = vec![
tech("WordPress", TechCategory::Cms, 90),
tech("Yoast SEO", TechCategory::Other, 80),
];
let rules = vec![
rule("Yoast SEO", vec![], vec!["WordPress"]),
rule("WordPress", vec![], vec![]),
];
let result = apply(techs, &rules);
assert!(result.iter().any(|t| t.name == "Yoast SEO"));
}
#[test]
fn deduplicates_keeping_highest_confidence() {
let techs = vec![
tech("nginx", TechCategory::Server, 60),
tech("nginx", TechCategory::Server, 90),
];
let result = apply(techs, &[]);
assert_eq!(result.iter().filter(|t| t.name == "nginx").count(), 1);
assert_eq!(
result
.iter()
.find(|t| t.name == "nginx")
.unwrap()
.confidence,
90
);
}
#[test]
fn spa_catch_all_detected() {
assert!(is_spa_catch_all(12345, 12345, 200));
assert!(!is_spa_catch_all(12345, 67890, 200)); assert!(!is_spa_catch_all(12345, 12345, 404)); assert!(!is_spa_catch_all(0, 0, 200)); }
}