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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Detection criteria for a WAF
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WafDetection {
/// Header patterns to match
pub headers: HashMap<String, String>,
/// Body patterns to match
pub body_patterns: Vec<String>,
/// Status codes that indicate this WAF
pub status_codes: Vec<u16>,
/// Cookie patterns
pub cookies: Vec<String>,
}
/// WAF signature definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WafSignature {
/// Name of the WAF
pub name: String,
/// Vendor/manufacturer
pub vendor: String,
/// Detection criteria
pub detection: WafDetection,
}
/// WAF detector for identifying web application firewalls
pub struct WafDetector {
signatures: Vec<WafSignature>,
}
impl WafDetector {
/// Create a new detector with embedded signatures
pub fn new() -> crate::error::Result<Self> {
const SIGNATURES: &str = include_str!("../fingerprints/waf-signatures.json");
let signatures: Vec<WafSignature> = serde_json::from_str(SIGNATURES).map_err(|e| {
crate::error::ScanError::InvalidPayload(format!(
"Failed to parse WAF signatures: {}",
e
))
})?;
tracing::info!("Loaded {} WAF signatures", signatures.len());
Ok(Self { signatures })
}
/// Detect WAF from an HTTP response
/// Returns the best matching WAF with highest confidence score
pub fn detect(&self, response: &DetectionResponse) -> Option<String> {
let mut best_match: Option<(String, i32, usize)> = None; // (name, score, specificity)
for signature in &self.signatures {
let (score, specificity) = self.calculate_match_score(response, signature);
if score >= 2 { // Minimum threshold for detection
match &mut best_match {
None => {
best_match = Some((signature.name.clone(), score, specificity));
}
Some((_, best_score, best_specificity)) => {
// Prefer higher score, or higher specificity if scores are equal
if score > *best_score || (score == *best_score && specificity > *best_specificity) {
best_match = Some((signature.name.clone(), score, specificity));
}
}
}
}
}
if let Some((name, score, _)) = best_match {
tracing::info!("Detected: {} (confidence score: {})", name, score);
Some(name)
} else {
tracing::debug!("No WAF detected");
None
}
}
/// Calculate match score and specificity for a signature
/// Returns (score, specificity) where higher specificity means more unique/specific detection
fn calculate_match_score(&self, response: &DetectionResponse, signature: &WafSignature) -> (i32, usize) {
let mut score = 0;
let mut header_matches = 0;
let total_criteria = signature.detection.headers.len()
+ signature.detection.body_patterns.len()
+ signature.detection.status_codes.len()
+ signature.detection.cookies.len();
// Check headers - require specific header values for high confidence
for (header_name, pattern) in &signature.detection.headers {
if let Some(header_value) = response.headers.get(&header_name.to_lowercase()) {
// Exact match or pattern match
if pattern == ".*" {
score += 1; // Generic header presence
header_matches += 1;
} else if header_value.to_lowercase().contains(&pattern.to_lowercase()) {
score += 3; // Specific header value match (stronger indicator)
header_matches += 1;
}
}
}
// Check body patterns
for pattern in &signature.detection.body_patterns {
if response.body.to_lowercase().contains(&pattern.to_lowercase()) {
score += 2; // Body patterns are good indicators
break; // Only count once
}
}
// Check status codes
for status in &signature.detection.status_codes {
if *status == response.status_code {
score += 1; // Status codes are weak indicators alone
break;
}
}
// Check cookies
for cookie_pattern in &signature.detection.cookies {
for cookie in &response.cookies {
if cookie.contains(cookie_pattern) {
score += 2; // Cookies are good indicators
break;
}
}
}
// Specificity: number of criteria defined + matched headers
let specificity = total_criteria + header_matches;
(score, specificity)
}
/// Check if response matches a signature (legacy method for compatibility)
#[allow(dead_code)]
fn matches_signature(&self, response: &DetectionResponse, signature: &WafSignature) -> bool {
let (score, _) = self.calculate_match_score(response, signature);
score >= 2
}
/// Get all loaded signatures
pub fn signatures(&self) -> &[WafSignature] {
&self.signatures
}
}
impl Default for WafDetector {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self {
signatures: Vec::new(),
})
}
}
/// Response data for WAF detection
#[derive(Debug, Clone)]
pub struct DetectionResponse {
/// HTTP status code
pub status_code: u16,
/// Response headers (lowercase keys)
pub headers: HashMap<String, String>,
/// Response body
pub body: String,
/// Cookie names
pub cookies: Vec<String>,
}
impl DetectionResponse {
/// Create a new detection response
pub fn new(
status_code: u16,
headers: HashMap<String, String>,
body: String,
cookies: Vec<String>,
) -> Self {
Self {
status_code,
headers,
body,
cookies,
}
}
}