use serde::{Deserialize, Serialize};
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrandImpersonationRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_timeout() -> u64 { 10 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrandMatch {
pub brand: String, pub brand_domain: String, pub edit_distance: u32, pub match_type: String, pub risk: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrandImpersonationResult {
pub domain: String,
pub is_impersonating: bool,
pub matches: Vec<BrandMatch>,
pub error: Option<String>,
}
const BRANDS: &[(&str, &str)] = &[
("google", "google.com"),
("apple", "apple.com"),
("microsoft", "microsoft.com"),
("amazon", "amazon.com"),
("paypal", "paypal.com"),
("facebook", "facebook.com"),
("twitter", "twitter.com"),
("instagram", "instagram.com"),
("netflix", "netflix.com"),
("spotify", "spotify.com"),
("linkedin", "linkedin.com"),
("github", "github.com"),
("dropbox", "dropbox.com"),
("adobe", "adobe.com"),
("chase", "chase.com"),
("wellsfargo", "wellsfargo.com"),
("bankofamerica", "bankofamerica.com"),
("citibank", "citibank.com"),
("dhl", "dhl.com"),
("fedex", "fedex.com"),
("ups", "ups.com"),
("usps", "usps.com"),
("steam", "steampowered.com"),
("discord", "discord.com"),
("cloudflare", "cloudflare.com"),
("stripe", "stripe.com"),
("shopify", "shopify.com"),
("wordpress", "wordpress.com"),
("zoom", "zoom.us"),
("slack", "slack.com"),
];
const SUSPICIOUS_TLDS: &[&str] = &[".xyz", ".tk", ".ml", ".ga", ".cf", ".pw", ".top", ".click", ".info", ".biz"];
fn levenshtein_distance(s1: &str, s2: &str) -> u32 {
let s1_chars: Vec<char> = s1.chars().collect();
let s2_chars: Vec<char> = s2.chars().collect();
let len1 = s1_chars.len();
let len2 = s2_chars.len();
if len1 == 0 {
return len2 as u32;
}
if len2 == 0 {
return len1 as u32;
}
let mut matrix = vec![vec![0u32; len2 + 1]; len1 + 1];
for i in 0..=len1 {
matrix[i][0] = i as u32;
}
for j in 0..=len2 {
matrix[0][j] = j as u32;
}
for i in 1..=len1 {
for j in 1..=len2 {
let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
matrix[i][j] = std::cmp::min(
std::cmp::min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1),
matrix[i - 1][j - 1] + cost,
);
}
}
matrix[len1][len2]
}
fn extract_sld(domain: &str) -> String {
let parts: Vec<&str> = domain.split('.').collect();
if parts.len() >= 2 {
parts[parts.len() - 2].to_lowercase()
} else {
domain.to_lowercase()
}
}
fn get_tld(domain: &str) -> String {
let parts: Vec<&str> = domain.split('.').collect();
if parts.len() >= 2 {
format!(".{}", parts[parts.len() - 1])
} else {
String::new()
}
}
pub async fn check_brand_impersonation(req: &BrandImpersonationRequest) -> Result<BrandImpersonationResult> {
let domain_lower = req.domain.to_lowercase();
let sld = extract_sld(&domain_lower);
let tld = get_tld(&domain_lower);
let mut matches = Vec::new();
for (brand_name, brand_domain) in BRANDS {
let brand_sld = extract_sld(brand_domain);
if sld == brand_sld {
if !tld.is_empty() && SUSPICIOUS_TLDS.iter().any(|st| tld.ends_with(st)) {
matches.push(BrandMatch {
brand: brand_name.to_string(),
brand_domain: brand_domain.to_string(),
edit_distance: 0,
match_type: "tld_variant".to_string(),
risk: "medium".to_string(),
});
} else {
matches.push(BrandMatch {
brand: brand_name.to_string(),
brand_domain: brand_domain.to_string(),
edit_distance: 0,
match_type: "exact".to_string(),
risk: "high".to_string(),
});
}
continue;
}
let dist = levenshtein_distance(&sld, &brand_sld);
if dist <= 2 && dist > 0 {
matches.push(BrandMatch {
brand: brand_name.to_string(),
brand_domain: brand_domain.to_string(),
edit_distance: dist,
match_type: "close".to_string(),
risk: "high".to_string(),
});
continue;
}
if sld.contains(&brand_sld) && sld != brand_sld {
matches.push(BrandMatch {
brand: brand_name.to_string(),
brand_domain: brand_domain.to_string(),
edit_distance: 0,
match_type: "contains".to_string(),
risk: "medium".to_string(),
});
continue;
}
}
let is_impersonating = !matches.is_empty();
Ok(BrandImpersonationResult {
domain: req.domain.clone(),
is_impersonating,
matches,
error: None,
})
}