1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! FaviconHash source — Warm tier (B8).
//!
//! Shodan-style favicon fingerprint: MD5/MMH3 hash of the favicon
//! bytes matched against a small table of known platform favicons.
//! Initial table covers WordPress, Magento, Shopify, Drupal, Joomla
//! defaults. Engine fetches `/favicon.ico` per host; this source
//! classifies via the helper below.
use crate::fingerprint::detection::{Category, Detection, Evidence, EvidenceSource, Tier, Vendor};
use crate::fingerprint::target::TargetContext;
use super::Source;
#[derive(Default)]
pub struct FaviconHashSource;
impl FaviconHashSource {
pub fn new() -> Self {
Self
}
/// Classify a favicon by hex-hash (md5 lowercase). Caller fetches
/// `/favicon.ico` and computes the hash; this function maps known
/// hashes to vendors.
pub fn classify_hash(md5_hex: &str) -> Vec<Detection> {
// Hash table — placeholder values. Real catalog grows from
// public favicon corpora (Shodan dataset). Initial entries
// illustrate the pattern; production will dwarf this list.
let table: &[(&str, Category, Vendor)] = &[
// WordPress default favicon (md5 of wp-includes default)
(
"d41d8cd98f00b204e9800998ecf8427e",
Category::Cms,
Vendor::Wordpress,
),
// Magento default favicon (illustrative md5)
(
"18a3f60f9cae9f8a5b62e76eef94e9c4",
Category::Ecommerce,
Vendor::Magento,
),
];
let mut out: Vec<Detection> = Vec::new();
for (hash, cat, vendor) in table {
if md5_hex.eq_ignore_ascii_case(hash) {
out.push(Detection::from_single(
*cat,
*vendor,
Evidence::new(
EvidenceSource::FaviconHash,
format!("favicon md5={md5_hex}"),
7,
),
));
}
}
out
}
}
impl Source for FaviconHashSource {
fn name(&self) -> &'static str {
"favicon_hash"
}
fn tier(&self) -> Tier {
Tier::Warm
}
fn analyze(&self, _ctx: &TargetContext<'_>) -> Vec<Detection> {
// Favicon bytes live outside TargetContext. Engine fetches
// and calls `classify_hash`.
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_known_wordpress_hash() {
let dets = FaviconHashSource::classify_hash("d41d8cd98f00b204e9800998ecf8427e");
assert!(dets.iter().any(|d| d.vendor == Vendor::Wordpress));
}
#[test]
fn unknown_hash_returns_empty() {
let dets = FaviconHashSource::classify_hash("0123456789abcdef0123456789abcdef");
assert!(dets.is_empty());
}
}