1use chrono::Utc;
19use regex::Regex;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::time::{Duration, Instant};
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
30pub enum Severity {
31 #[serde(rename = "info")]
32 Info,
33 #[serde(rename = "low")]
34 Low,
35 #[serde(rename = "medium")]
36 Medium,
37 #[serde(rename = "high")]
38 High,
39 #[serde(rename = "critical")]
40 Critical,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct AttackEvent {
46 pub event_id: String,
48 pub timestamp: String,
50 pub category: String,
52 pub subcategory: String,
54 pub matched_payload: String,
56 pub full_payload: String,
58 pub method: String,
60 pub path: String,
62 pub severity: Severity,
64 pub mitre_id: Option<String>,
66 pub simulated_response: u16,
68 pub attacker_ip: String,
70 pub user_agent: String,
72 pub headers: HashMap<String, String>,
74 pub session_id: Option<String>,
76 pub confidence: f64,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AttackerProfile {
83 pub profile_id: String,
85 pub ip: String,
87 pub country: Option<String>,
89 pub asn: Option<String>,
91 pub is_tor: bool,
93 pub is_cloud: bool,
95 pub is_proxy: bool,
97 pub user_agent: String,
99 pub browser_fingerprint: Option<BrowserFingerprint>,
101 pub first_seen: String,
103 pub last_seen: String,
105 pub total_requests: u64,
107 pub attack_categories: HashMap<String, u64>,
109 pub techniques_used: Vec<String>,
111 pub avg_request_interval: f64,
113 pub is_automated: bool,
115 pub risk_score: f64,
117 pub targets: Vec<String>,
119 pub event_timeline: Vec<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct BrowserFingerprint {
126 pub browser: String,
127 pub browser_version: String,
128 pub os: String,
129 pub os_version: String,
130 pub engine: String,
131 pub device_type: String,
132 pub is_headless: bool,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct HoneypotConfig {
138 pub max_payload_store: usize,
140 pub realistic_timing: bool,
142 pub min_delay_ms: u64,
144 pub max_delay_ms: u64,
146 pub fake_rsc_responses: bool,
148 pub session_tracking: bool,
150 pub session_cookie: String,
152 pub log_all_requests: bool,
154 pub detection_threshold: f64,
156 pub progressive_sizing: bool,
158}
159
160impl Default for HoneypotConfig {
161 fn default() -> Self {
162 Self {
163 max_payload_store: 8192,
164 realistic_timing: true,
165 min_delay_ms: 20,
166 max_delay_ms: 180,
167 fake_rsc_responses: true,
168 session_tracking: true,
169 session_cookie: "__Host-RSC-ID".to_string(),
170 log_all_requests: false,
171 detection_threshold: 0.5,
172 progressive_sizing: true,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct HoneypotState {
180 pub config: HoneypotConfig,
181 pub total_requests: u64,
182 pub total_attacks_detected: u64,
183 pub unique_attackers: usize,
184 pub attack_events: Vec<AttackEvent>,
185 pub attacker_profiles: HashMap<String, AttackerProfile>,
186 pub uptime_seconds: f64,
187 pub requests_per_minute: f64,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct DetectionResult {
193 pub detections: Vec<AttackEvent>,
195 pub simulated_status: u16,
197 pub simulated_body: String,
199 pub content_type: String,
201 pub should_block: bool,
203 pub suggested_delay_ms: u64,
205}
206
207#[derive(Debug, Clone)]
209pub struct RawRequest {
210 pub method: String,
211 pub path: String,
212 pub query_string: String,
213 pub body: String,
214 pub headers: HashMap<String, String>,
215 pub ip: String,
216 pub timestamp: chrono::DateTime<Utc>,
217}
218
219struct AttackVector {
225 category: &'static str,
226 subcategory: &'static str,
227 patterns: &'static [&'static str],
228 severity: Severity,
229 mitre_id: &'static str,
230 search_location: &'static str,
232 context_keywords: &'static [&'static str],
234}
235
236struct CompiledVector {
238 category: &'static str,
239 subcategory: &'static str,
240 patterns: Vec<Regex>,
241 severity: Severity,
242 mitre_id: &'static str,
243 search_location: &'static str,
244 context_keywords: &'static [&'static str],
245}
246
247fn attack_vectors() -> &'static [AttackVector] {
249 use Severity::*;
250 static VECTORS: std::sync::OnceLock<Vec<AttackVector>> = std::sync::OnceLock::new();
251 VECTORS.get_or_init(|| vec![
252 AttackVector {
254 category: "sqli", subcategory: "classic_tautology", severity: Critical,
255 mitre_id: "T1190",
256 search_location: "all",
257 context_keywords: &[],
258 patterns: &[
259 r"(?i)('|\%27)\s*(OR|AND)\s*('|\%27)?\s*\d+\s*=\s*\d+",
260 r"(?i)('|\%27)\s*(OR|AND)\s*('|\%27)?\s*'\d+'\s*=\s*'\d+'",
261 r"(?i)(OR|AND)\s+\d+\s*=\s*\d+\s*--",
262 r"(?i)admin'\s*(--|#|/\*)",
263 r#"(?i)['"]\s*OR\s+1\s*=\s*1\s*--"#,
264 r"(?i)'\s*OR\s+'1'\s*=\s*'1",
265 ],
266 },
267 AttackVector {
268 category: "sqli", subcategory: "union_select", severity: Critical,
269 mitre_id: "T1190",
270 search_location: "all",
271 context_keywords: &[],
272 patterns: &[
273 r"(?i)UNION\s+(ALL\s+)?SELECT\s+(NULL|@@|\d+|user\b|database\b)",
274 r"(?i)UNION\s+(ALL\s+)?SELECT\s+(NULL|@@|\d+|user\b|database\b).*--",
275 r"(?i)'\s*UNION\s+SELECT\s+.*FROM\s+",
276 ],
277 },
278 AttackVector {
279 category: "sqli", subcategory: "blind_time", severity: Critical,
280 mitre_id: "T1190",
281 search_location: "all",
282 context_keywords: &[],
283 patterns: &[
284 r"(?i)(SLEEP|pg_sleep|WAITFOR\s+DELAY|dbms_lock\.sleep|benchmark)\s*\(.*\d+",
285 r"(?i)AND\s+(SLEEP|pg_sleep|WAITFOR)\s*\(\s*\d+\s*\)",
286 r"(?i)'\s*AND\s+(SELECT\s+.*FROM\s+.*SLEEP)",
287 ],
288 },
289 AttackVector {
290 category: "sqli", subcategory: "error_based", severity: Critical,
291 mitre_id: "T1190",
292 search_location: "all",
293 context_keywords: &[],
294 patterns: &[
295 r"(?i)extractvalue\s*\(\s*\d+\s*,\s*concat\s*\(",
296 r"(?i)updatexml\s*\(\s*\d+\s*,\s*concat\s*\(",
297 r"(?i)convert\s*\(.*using\s+",
298 r"(?i)AND\s+1\s*=\s*CONVERT\s*\(int",
299 ],
300 },
301 AttackVector {
302 category: "sqli", subcategory: "stacked", severity: Critical,
303 mitre_id: "T1190",
304 search_location: "all",
305 context_keywords: &[],
306 patterns: &[
307 r"(?i);\s*(DROP|INSERT|UPDATE|DELETE|ALTER|CREATE|EXEC|TRUNCATE|SHUTDOWN)\s+",
308 r"(?i)';\s*(DROP|INSERT|UPDATE|DELETE)\s+",
309 r"(?i);\s*EXEC\s+(sp_|xp_)",
310 ],
311 },
312 AttackVector {
314 category: "nosqli", subcategory: "mongodb", severity: Critical,
315 mitre_id: "T1190",
316 search_location: "all",
317 context_keywords: &[],
318 patterns: &[
319 r#"(?i)\{\s*"\$ne"\s*:\s*""#,
320 r#"(?i)\{\s*"\$gt"\s*:\s*""#,
321 r#"(?i)\{\s*"\$regex"\s*:\s*".*"\s*\}"#,
322 r#"(?i)\{\s*"\$where"\s*:\s*""#,
323 r#"(?i)"\$(eq|ne|gt|gte|lt|lte|in|nin|regex|exists|type|mod|text|search|where)"\s*:"#,
324 r"(?i)\{\s*'\$ne'\s*:\s*",
325 ],
326 },
327 AttackVector {
328 category: "nosqli", subcategory: "redis_injection", severity: High,
329 mitre_id: "T1190",
330 search_location: "all",
331 context_keywords: &[],
332 patterns: &[
333 r"(?i)(\r\n|\n)\s*(CONFIG|SET|GET|FLUSHALL|KEYS|SAVE|SHUTDOWN|SLAVEOF)\s",
334 r"(?i)%0[dD]%0[aA]\s*(CONFIG|SET|FLUSHALL)",
335 ],
336 },
337 AttackVector {
339 category: "xss", subcategory: "reflected", severity: High,
340 mitre_id: "T1059.007",
341 search_location: "all",
342 context_keywords: &[],
343 patterns: &[
344 r"(?i)<script[^>]*>.*</script>",
345 r"(?i)<script[^>]*>.*",
346 r#"(?i)javascript\s*:\s*(alert|prompt|confirm)\s*\("#,
347 r#"(?i)"><script[^>]*>alert\("#,
348 r#"(?i)<img[^>]+onerror\s*=\s*[`'\"]?\w+"#,
349 r#"(?i)<svg[^>]+onload\s*=\s*[`'\"]?\w+"#,
350 ],
351 },
352 AttackVector {
353 category: "xss", subcategory: "polyglot", severity: High,
354 mitre_id: "T1059.007",
355 search_location: "all",
356 context_keywords: &[],
357 patterns: &[
358 r#"(?i)jaVasCript:/*-/*`/*\`/*'/*"/**/(\s*/\*\s*/.*\)\s*;)"#,
359 r#"(?i)"\s*;\s*alert\s*\(.*\)\s*//"#,
360 ],
361 },
362 AttackVector {
363 category: "xss", subcategory: "stored_payload", severity: High,
364 mitre_id: "T1059.007",
365 search_location: "all",
366 context_keywords: &[],
367 patterns: &[
368 r#"(?i)<iframe[^>]*srcdoc\s*=\s*[`'\"]?\s*<script"#,
369 r#"(?i)<object[^>]*data\s*=\s*[`'\"]?data:text/html"#,
370 r#"(?i)<embed[^>]*src\s*=\s*[`'\"]?data:text/html"#,
371 ],
372 },
373 AttackVector {
375 category: "cmdi", subcategory: "unix_pipe", severity: Critical,
376 mitre_id: "T1059.004",
377 search_location: "all",
378 context_keywords: &[],
379 patterns: &[
380 r"(?m)[\|\;`]\s*(id|whoami|ls|cat|pwd|uname|hostname)\s*$",
381 r"(?m)\$\(\s*(id|whoami|ls|cat|wget|curl)\s*",
382 r"(?m)`\s*(id|whoami|ls|cat)\s*`",
383 r"(?m)\|\|\s*(id|whoami|ls|cat|ping)\s",
384 r"(?m)&&\s*(id|whoami|ls|cat|ping)\s",
385 r"(?m);\s*(id|whoami|ls|cat|ping|sleep)\s",
386 ],
387 },
388 AttackVector {
389 category: "cmdi", subcategory: "unix_advanced", severity: Critical,
390 mitre_id: "T1059.004",
391 search_location: "all",
392 context_keywords: &[],
393 patterns: &[
394 r"(?m)(/usr/bin/|/bin/|/sbin/)(id|whoami|ls|cat|bash|sh|nc|wget|curl)",
395 r"(?m)\|\s*(nc|ncat|netcat)\s",
396 r"(?m)\|\s*(wget|curl)\s+http",
397 r"(?m);\s*/bin/(bash|sh|dash)\s+-[ci]",
398 r"(?m);\s*(chmod|chown)\s",
399 ],
400 },
401 AttackVector {
402 category: "cmdi", subcategory: "windows", severity: Critical,
403 mitre_id: "T1059.003",
404 search_location: "all",
405 context_keywords: &[],
406 patterns: &[
407 r"(?i)[\|\;`]\s*(whoami|systeminfo|ipconfig|net\s+user|tasklist)\b",
408 r"(?i)cmd\.exe\s+/[cCkK]\s+",
409 r"(?i)powershell\.exe\s+-[eE][xX]",
410 r"(?i)%(COMSPEC|SystemRoot|WINDIR)%",
411 r"(?i)certutil\s+-urlcache\s+-split\s+-f\s+http",
412 ],
413 },
414 AttackVector {
415 category: "cmdi", subcategory: "blind_oob", severity: Critical,
416 mitre_id: "T1059.004",
417 search_location: "all",
418 context_keywords: &[],
419 patterns: &[
420 r"(?m)\|\s*(nslookup|dig|host)\s+[a-zA-Z0-9]",
421 r"(?m)ping\s+-[cnt]\s+\d+\s+[a-zA-Z0-9]",
422 r"(?m);\s*(nslookup|dig|host|ping)\s+\$\{",
423 ],
424 },
425 AttackVector {
427 category: "path_traversal", subcategory: "dot_dot_slash", severity: High,
428 mitre_id: "T1083",
429 search_location: "all",
430 context_keywords: &[],
431 patterns: &[
432 r"(\.\./){2,}(etc|var|proc|sys|home|root|tmp|windows|winnt)",
433 r"(\.\.\\){2,}(windows|winnt|system32|boot\.ini)",
434 r"\.\./\.\./\.\./.*(passwd|shadow|hosts|\.ini|\.conf)",
435 r"\.%2e/\.%2e/",
436 r"\.%252e/\.%252e/",
437 r"\.\.%2f\.\.%2f",
438 r"\.\.%5c\.\.%5c",
439 r"file:///(etc|proc|sys|var|home)/",
440 ],
441 },
442 AttackVector {
443 category: "path_traversal", subcategory: "absolute_path", severity: High,
444 mitre_id: "T1083",
445 search_location: "all",
446 context_keywords: &[],
447 patterns: &[
448 r"^/(etc|proc|sys|var|root|home)/.*(passwd|shadow|hosts|\.conf)",
449 r"^(C:|D:)\\(windows|winnt|system32)\\.*",
450 ],
451 },
452 AttackVector {
454 category: "lfi", subcategory: "local_include", severity: Critical,
455 mitre_id: "T1190",
456 search_location: "all",
457 context_keywords: &[],
458 patterns: &[
459 r"(?i)(file|page|path|include|require|document|folder|dir|template|module|load)\s*=\s*(\.\./|/etc/|/proc/)",
460 r"(?i)/etc/(passwd|shadow|hosts|group|sudoers|resolv\.conf)",
461 r"(?i)/proc/(self|version|cpuinfo|meminfo|cmdline)/?",
462 r"(?i)/var/log/(apache|nginx|syslog|messages|auth\.log)",
463 r"(?i)C:\\windows\\(system32|win\.ini|boot\.ini|repair\\sam)",
464 r"(?i)php://filter/convert\.base64-encode/resource=",
465 r"(?i)php://filter/read=convert\.base64-encode/resource=",
466 r"(?i)php://input",
467 r"(?i)expect://(id|whoami|ls)",
468 r"(?i)data://text/plain;base64,",
469 ],
470 },
471 AttackVector {
472 category: "rfi", subcategory: "remote_include", severity: Critical,
473 mitre_id: "T1190",
474 search_location: "all",
475 context_keywords: &[],
476 patterns: &[
477 r"(?i)(https?|ftp)://[^/\s]+/[^?\s]+\.(php|txt|jpg|png|gif)\?",
478 r"(?i)(https?|ftp)://.*(shell|backdoor|r57|c99|web-shell)",
479 r"(?i)(https?|ftp)://.*/.*\.(txt|php|asp|jsp)\?",
480 ],
481 },
482 AttackVector {
484 category: "ssrf", subcategory: "cloud_metadata", severity: Critical,
485 mitre_id: "T1190",
486 search_location: "all",
487 context_keywords: &[],
488 patterns: &[
489 r"(?i)(169\.254\.\d+\.\d+|metadata\.google\.internal|100\.100\.\d+\.\d+)",
490 r"(?i)(/latest/meta-data|/metadata/v1|/openstack)",
491 r"(?i)instance-data\.ec2\.internal",
492 r"(?i)/latest/(meta-data|dynamic|user-data)",
493 r"(?i)kubernetes\.default\.svc",
494 r"(?i)\.compute\.internal",
495 ],
496 },
497 AttackVector {
498 category: "ssrf", subcategory: "internal_ports", severity: High,
499 mitre_id: "T1190",
500 search_location: "all",
501 context_keywords: &[],
502 patterns: &[
503 r"(?i)(http://|https://)(localhost|127\.\d+\.\d+\.\d+|0\.0\.0\.0|\[::1\])\s*[/:]",
504 r"(?i)(http://|https://)(10\.\d+\.\d+\.\d+|172\.1[6-9]\.\d+\.\d+|172\.2\d\.\d+\.\d+|172\.3[01]\.\d+\.\d+|192\.168\.\d+\.\d+)",
505 ],
506 },
507 AttackVector {
508 category: "ssrf", subcategory: "dns_rebinding", severity: Medium,
509 mitre_id: "T1190",
510 search_location: "all",
511 context_keywords: &[],
512 patterns: &[
513 r"(?i)([a-z0-9]+\.){2,}(1zero|rbndr|nip\.io|xip\.io|sslip\.io)",
514 r"(?i)(nslookup|dig|host)\s+[a-z0-9]+\.[a-z]+\.[a-z]+",
515 ],
516 },
517 AttackVector {
519 category: "xxe", subcategory: "external_entity", severity: Critical,
520 mitre_id: "T1190",
521 search_location: "body",
522 context_keywords: &[],
523 patterns: &[
524 r#"<!ENTITY\s+\w+\s+(SYSTEM|PUBLIC)\s+['\"]"#,
525 r#"<!ENTITY\s+%\s+\w+\s+SYSTEM\s+['\"]"#,
526 r"<!DOCTYPE\s+\w+\s+\[\s*<!ENTITY",
527 r"<xml[^>]*>\s*<!DOCTYPE",
528 r#"<\?xml[^?]*\?>\s*<!DOCTYPE\s+\w+\s+\["#,
529 ],
530 },
531 AttackVector {
532 category: "xxe", subcategory: "billion_laughs", severity: Critical,
533 mitre_id: "T1499.002",
534 search_location: "body",
535 context_keywords: &[],
536 patterns: &[
537 r#"<!ENTITY\s+\w+\s+['\"]<!ENTITY"#,
538 r"&(lol|lolz|lol1|lol2|laugh|boom|ha|haha);",
539 ],
540 },
541 AttackVector {
543 category: "ssti", subcategory: "jinja2", severity: Critical,
544 mitre_id: "T1190",
545 search_location: "all",
546 context_keywords: &[],
547 patterns: &[
548 r"\{\{\s*(\d+\s*[\*\+\-]\s*\d+|\w+\.\w+)",
549 r"\{\{\s*config\s*\}\}",
550 r"\{\{\s*self\s*\}\}",
551 r"\{\{\s*''\.__class__\.__mro__",
552 r"\{\{\s*lipsum\.__globals__",
553 r"\{\{\s*request\.application\.__globals__",
554 r"\{\%\s*(import|extends|include|set|for|if)\s+",
555 r"\{\{\s*cycler\.__init__\.__globals__",
556 ],
557 },
558 AttackVector {
559 category: "ssti", subcategory: "twig", severity: Critical,
560 mitre_id: "T1190",
561 search_location: "all",
562 context_keywords: &[],
563 patterns: &[
564 r"\{\{\s*_self\.env\.registerUndefinedFilterCallback",
565 r#"\{\{\s*['\"].*['\"]\s*\|\s*map\("#,
566 r#"\{\{\s*['\"].*['\"]\s*\|\s*filter\("#,
567 ],
568 },
569 AttackVector {
570 category: "ssti", subcategory: "freemarker", severity: Critical,
571 mitre_id: "T1190",
572 search_location: "all",
573 context_keywords: &[],
574 patterns: &[
575 r"\$\{.*\.class\.forName\(",
576 r#"<\#assign\s+ex\s*=\s*['\"]freemarker"#,
577 r"\$\{(.*\?)?new\s+java\.\w+\(",
578 ],
579 },
580 AttackVector {
582 category: "deserialization", subcategory: "java", severity: Critical,
583 mitre_id: "T1190",
584 search_location: "all",
585 context_keywords: &[],
586 patterns: &[
587 r"(?i)(ac ed 00 05|rO0AB|aced0005)",
588 r"(?i)(com\.sun\.org\.apache\.xalan|org\.apache\.commons\.collections)",
589 r"(?i)(java\.lang\.Runtime|java\.lang\.ProcessBuilder)",
590 r"(?i)(org\.springframework\.beans\.factory)",
591 ],
592 },
593 AttackVector {
594 category: "deserialization", subcategory: "php", severity: Critical,
595 mitre_id: "T1190",
596 search_location: "all",
597 context_keywords: &[],
598 patterns: &[
599 r#"(?i)(O:\d+:['\"][A-Za-z0-9_\\]+['\"]:\d+:)"#,
600 r"(?i)(a:\d+:\{.*s:\d+:)",
601 r#"(?i)(C:\d+:['\"][A-Za-z0-9_\\]+['\"]:\d+:)"#,
602 ],
603 },
604 AttackVector {
605 category: "deserialization", subcategory: "python_pickle", severity: Critical,
606 mitre_id: "T1190",
607 search_location: "all",
608 context_keywords: &[],
609 patterns: &[
610 r"(?i)(cos\\nsystem|c__builtin__\\neval|csubprocess\\nPopen)",
611 r"(?i)(__reduce__|__reduce_ex__)",
612 r"(?i)(S'((import|exec|eval)\b|__import__)",
613 ],
614 },
615 AttackVector {
616 category: "deserialization", subcategory: "nodejs", severity: Critical,
617 mitre_id: "T1190",
618 search_location: "all",
619 context_keywords: &[],
620 patterns: &[
621 r#"(?i)\{"_bsontype":"Code","code":"[^"]*require\(['"]child_process"#,
622 r#"(?i)\{"type":"Function","body":"[^"]*require\("#,
623 ],
624 },
625 AttackVector {
627 category: "jwt", subcategory: "none_algorithm", severity: Critical,
628 mitre_id: "T1557",
629 search_location: "all",
630 context_keywords: &[],
631 patterns: &[
632 r#"(?i)"alg"\s*:\s*"none""#,
633 r"(?i)ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\.(?:$|\s|&)",
634 ],
635 },
636 AttackVector {
637 category: "jwt", subcategory: "key_confusion", severity: High,
638 mitre_id: "T1557",
639 search_location: "all",
640 context_keywords: &[],
641 patterns: &[
642 r#"(?i)"alg"\s*:\s*"HS256"[^}]*"k"\s*:"#,
643 r#"(?i)"jwk"\s*:\s*\{[^}]*"kty"\s*:"#,
644 ],
645 },
646 AttackVector {
648 category: "graphql", subcategory: "introspection", severity: Medium,
649 mitre_id: "T1190",
650 search_location: "body",
651 context_keywords: &[],
652 patterns: &[
653 r"__schema\s*\{\s*types\s*\{",
654 r#"__type\s*\(\s*name\s*:\s*\"\""#,
655 r"query\s*\{\s*__schema\{",
656 r"fragment\s+FullType\s+on\s+__Type\s*\{",
657 ],
658 },
659 AttackVector {
660 category: "graphql", subcategory: "batch_attack", severity: High,
661 mitre_id: "T1190",
662 search_location: "body",
663 context_keywords: &[],
664 patterns: &[
665 r#"\[\s*\{\s*\"query\""#,
666 r#"\"batch\"\s*:\s*\["#,
667 ],
668 },
669 AttackVector {
671 category: "prototype_pollution", subcategory: "javascript", severity: High,
672 mitre_id: "T1059.007",
673 search_location: "all",
674 context_keywords: &[],
675 patterns: &[
676 r#"(?i)(__proto__|constructor|prototype)\s*=\s*["']"#,
677 r#"(?i)"__proto__"\s*:\s*\{[^}]*\}"#,
678 r#"(?i)"constructor"\s*:\s*\{[^}]*"prototype"\s*:"#,
679 r#"(?i)\[\[__proto__\]\]\s*=\s*"#,
680 ],
681 },
682 AttackVector {
684 category: "crlf", subcategory: "response_splitting", severity: High,
685 mitre_id: "T1190",
686 search_location: "all",
687 context_keywords: &[],
688 patterns: &[
689 r"(\r\n|\%0[dD]\%0[aA])\s*Content-(Type|Length|Disposition):",
690 r"(\r\n|\%0[dD]\%0[aA])\s*Set-Cookie\s*:",
691 r"(\r\n|\%0[dD]\%0[aA])\s*(HTTP/|Location\s*:)",
692 r"(\r\n|\%0[dD]\%0[aA])\s*X-XSS-Protection\s*:",
693 ],
694 },
695 AttackVector {
696 category: "crlf", subcategory: "header_injection", severity: Medium,
697 mitre_id: "T1190",
698 search_location: "all",
699 context_keywords: &[],
700 patterns: &[
701 r"(\r\n|\%0[dD]\%0[aA])\s*[A-Za-z0-9\-]+\s*:\s*[^\n]+\r?\n",
702 ],
703 },
704 AttackVector {
706 category: "http_smuggling", subcategory: "cl_te", severity: High,
707 mitre_id: "T1190",
708 search_location: "headers",
709 context_keywords: &[],
710 patterns: &[
711 r"(?i)^\s*Transfer-Encoding\s*:\s*[\x0b]",
712 r"(?i)^\s*Transfer-Encoding\s*:.*\x0b",
713 r"(?i)^\s*Content-Length\s*:\s*\d+\s*\n\s*Content-Length",
714 ],
715 },
716 AttackVector {
717 category: "host_attack", subcategory: "host_injection", severity: High,
718 mitre_id: "T1190",
719 search_location: "headers",
720 context_keywords: &[],
721 patterns: &[
722 r"(?i)^\s*Host\s*:\s*(evil|attacker|malware|hack|bugbounty|pwned)\.(com|net|org|io)",
723 r"(?i)^\s*Host\s*:\s*(127\.0\.0\.1|localhost|0\.0\.0\.0)",
724 r"(?i)^\s*X-Forwarded-Host\s*:\s*(evil|attacker|127\.0\.0\.1)",
725 ],
726 },
727 AttackVector {
729 category: "file_upload", subcategory: "malicious_extension", severity: Critical,
730 mitre_id: "T1190",
731 search_location: "all",
732 context_keywords: &[],
733 patterns: &[
734 r#"(?i)filename\s*=\s*["'][^"']+\.(php|jsp|asp|aspx|phtml|php5|php7|shtml|cgi|pl|war|jspx)['"]#"#,
735 r#"(?i)Content-Disposition:.*filename=\\*['"][^'"]+\.(php|jsp|asp)['"]#"#,
736 r"(?i)\.php\d*\.(jpg|png|gif|pdf)",
737 r"(?i)\.(php|jsp|asp)\s*%00",
738 ],
739 },
740 AttackVector {
742 category: "open_redirect", subcategory: "url_param", severity: Medium,
743 mitre_id: "T1204.001",
744 search_location: "all",
745 context_keywords: &[],
746 patterns: &[
747 r"(?i)(redirect|url|next|return|goto|target|dest|continue|back)\s*=\s*(https?://|//)[^&\s]+",
748 r"(?i)(redirect|url|next|return|goto)\s*=\s*(evil|attacker|phish|malw)",
749 r#"(?i)"(redirect|url|next)"\s*:\s*"(https?://|//)"#,
750 ],
751 },
752 AttackVector {
754 category: "cookie_attack", subcategory: "injection", severity: Medium,
755 mitre_id: "T1539",
756 search_location: "headers",
757 context_keywords: &[],
758 patterns: &[
759 r"(?i)Cookie\s*:\s*.*(<script|alert|onerror|javascript:)",
760 r"(?i)Cookie\s*:\s*.*(../|\.\.\\\\)",
761 r"(?i)Cookie\s*:\s*.*(SELECT|UNION)",
762 ],
763 },
764 AttackVector {
766 category: "cache_poisoning", subcategory: "header_probe", severity: High,
767 mitre_id: "T1499",
768 search_location: "headers",
769 context_keywords: &[],
770 patterns: &[
771 r"(?i)^\s*X-Forwarded-(Scheme|Proto|Host|Port|Prefix)\s*:\s*(https?://)?[a-z]+",
772 r"(?i)^\s*X-Original-URL\s*:\s*",
773 r"(?i)^\s*X-Rewrite-URL\s*:\s*",
774 r"(?i)^\s*X-HTTP-Method-Override\s*:\s*",
775 r"(?i)^\s*X-Method-Override\s*:\s*",
776 ],
777 },
778 AttackVector {
780 category: "auth_bypass", subcategory: "header_forgery", severity: Critical,
781 mitre_id: "T1548",
782 search_location: "headers",
783 context_keywords: &[],
784 patterns: &[
785 r"(?i)^\s*X-Forwarded-For\s*:\s*(127\.0\.0\.1|localhost|::1)",
786 r"(?i)^\s*X-Remote-IP\s*:\s*(127\.0\.0\.1|10\.\d+\.\d+\.\d+)",
787 r"(?i)^\s*X-Originating-IP\s*:\s*(127\.0\.0\.1)",
788 r"(?i)^\s*X-Real-IP\s*:\s*(127\.0\.0\.1|10\.\d+\.\d+\.\d+)",
789 r"(?i)^\s*Authorization\s*:\s*Basic\s+[A-Za-z0-9+/=]+={0,2}",
790 ],
791 },
792 AttackVector {
794 category: "hpp", subcategory: "duplicate_params", severity: Medium,
795 mitre_id: "T1190",
796 search_location: "query",
797 context_keywords: &[],
798 patterns: &[
799 r#"(?i)([?&])[^?&=]+=[^?&=]+&[^?&=]+=&"#,
800 r"(?i)([?&])[^?&=]+=[^?&=]+&(same_param)=[^?&=]+",
801 ],
802 },
803 AttackVector {
805 category: "method_tamper", subcategory: "method_override", severity: Medium,
806 mitre_id: "T1190",
807 search_location: "all",
808 context_keywords: &[],
809 patterns: &[
810 r"(?i)_method\s*=\s*(PUT|DELETE|PATCH|OPTIONS|TRACE|CONNECT)",
811 r"(?i)X-HTTP-Method\s*:\s*(PUT|DELETE)",
812 ],
813 },
814 AttackVector {
816 category: "null_byte", subcategory: "termination", severity: High,
817 mitre_id: "T1190",
818 search_location: "all",
819 context_keywords: &[],
820 patterns: &[
821 r"(?i)%00\.(php|jsp|asp|html|txt|conf)",
822 r"(?i)\.php%00",
823 r"\x00[^\x00]*\.(php|jsp|asp)",
824 ],
825 },
826 AttackVector {
828 category: "cors", subcategory: "origin_spoof", severity: Medium,
829 mitre_id: "T1190",
830 search_location: "headers",
831 context_keywords: &[],
832 patterns: &[
833 r"(?i)^\s*Origin\s*:\s*https?://(evil|attacker|null|127\.0\.0\.1)",
834 r"(?i)^\s*Origin\s*:\s*null",
835 ],
836 },
837 AttackVector {
839 category: "brute_force", subcategory: "multi_attempt", severity: High,
840 mitre_id: "T1110",
841 search_location: "body",
842 context_keywords: &[],
843 patterns: &[
844 r#"(?i)(password|passwd|pwd|pin|secret|token)\s*=\s*['\"][^'\"]{1,20}['\"]"#,
845 r#"(?i)\{"(email|username|user|login)"\s*:\s*"[^"]+"\s*,\s*"(password|passwd|pwd)"\s*:\s*""#,
846 ],
847 },
848 AttackVector {
850 category: "format_string", subcategory: "printf_injection", severity: High,
851 mitre_id: "T1190",
852 search_location: "all",
853 context_keywords: &[],
854 patterns: &[
855 r"(%[0-9]*\$)?%([xXndsSph]|p[rd]){1,2}",
856 r"%[0-9]{1,2}\$[xdspnXDSPN]",
857 ],
858 },
859 AttackVector {
861 category: "race_condition", subcategory: "concurrent", severity: Medium,
862 mitre_id: "T1499",
863 search_location: "all",
864 context_keywords: &[],
865 patterns: &[
866 r"(?i)(race|parallel|concurrent|thread)\s*=\s*(true|1|yes)",
867 ],
868 },
869 AttackVector {
871 category: "clickjacking", subcategory: "frame_probe", severity: Low,
872 mitre_id: "T1499",
873 search_location: "all",
874 context_keywords: &[],
875 patterns: &[
876 r#"(?i)<iframe[^>]*style\s*=\s*['\"]opacity\s*:\s*0"#,
877 r#"(?i)<iframe[^>]*width\s*=\s*['\"]\d+['\"][^>]*height\s*=\s*['\"]\d+"#,
878 ],
879 },
880 AttackVector {
882 category: "source_leak", subcategory: "sourcemap_probe", severity: Low,
883 mitre_id: "T1213",
884 search_location: "all",
885 context_keywords: &[],
886 patterns: &[
887 r"(?i)(\.js\.map|\.css\.map|//#\s*sourceMappingURL)",
888 r"(?i)/_next/static/.*\.map$",
889 ],
890 },
891 AttackVector {
893 category: "rsc_attack", subcategory: "flight_injection", severity: Critical,
894 mitre_id: "T1190",
895 search_location: "body",
896 context_keywords: &[],
897 patterns: &[
898 r#"(?i)\[\["\$","@\w+",null,\{"#,
899 r#"(?i)"type"\s*:\s*"blob_handler""#,
900 r#"(?i)"dispatch"\s*:\s*"dynamic""#,
901 r#"(?i)"method"\s*:\s*"child_process\.exec""#,
902 ],
903 },
904 AttackVector {
905 category: "rsc_attack", subcategory: "server_action_probe", severity: High,
906 mitre_id: "T1190",
907 search_location: "headers",
908 context_keywords: &[],
909 patterns: &[
910 r"(?i)^\s*Next-Action\s*:",
911 r"(?i)^\s*RSC\s*:\s*1",
912 r"(?i)^\s*Content-Type\s*:\s*text/x-component",
913 r"(?i)^\s*Next-Router-State-Tree\s*:",
914 ],
915 },
916 AttackVector {
917 category: "nextjs_probe", subcategory: "internal_route", severity: Medium,
918 mitre_id: "T1190",
919 search_location: "path",
920 context_keywords: &[],
921 patterns: &[
922 r"^/_next/.*(webpack-hmr|__nextjs_|middleware)",
923 r"^/_next/data/",
924 r"^/_next/image\?url=",
925 ],
926 },
927 AttackVector {
929 category: "websocket", subcategory: "injection", severity: High,
930 mitre_id: "T1190",
931 search_location: "all",
932 context_keywords: &[],
933 patterns: &[
934 r"(?i)ws://(evil|attacker|localhost|127\.0\.0\.1)",
935 r"(?i)Sec-WebSocket-Key\s*:\s*[A-Za-z0-9+/=]+",
936 ],
937 },
938 AttackVector {
940 category: "dns_exfil", subcategory: "tunnel_probe", severity: High,
941 mitre_id: "T1048.001",
942 search_location: "all",
943 context_keywords: &[],
944 patterns: &[
945 r"(?i)(nslookup|dig|host)\s+\w{20,}\.[a-z]+\.[a-z]+",
946 r"(?i)\.(burpcollaborator|interact\.sh|canarytokens|oastify)\.(com|net|io|pro|live|site|online|fun)",
947 ],
948 },
949 AttackVector {
951 category: "content_type", subcategory: "mismatch_attack", severity: Medium,
952 mitre_id: "T1190",
953 search_location: "all",
954 context_keywords: &[],
955 patterns: &[
956 r"(?i)Content-Type\s*:\s*text/html.*\{.*\}.*Content-Type\s*:\s*application/json",
957 ],
958 },
959 AttackVector {
961 category: "encoding_attack", subcategory: "charset_confusion", severity: Medium,
962 mitre_id: "T1190",
963 search_location: "all",
964 context_keywords: &[],
965 patterns: &[
966 r"(?i)%u[0-9a-fA-F]{4}",
967 r"(?i)&#x[0-9a-fA-F]+;",
968 r"(?i)&#\d{2,};",
969 r"(?i)[\\]x[0-9a-fA-F]{2}",
970 ],
971 },
972 AttackVector {
974 category: "user_agent", subcategory: "fake_crawler", severity: Low,
975 mitre_id: "T1592",
976 search_location: "headers",
977 context_keywords: &[],
978 patterns: &[
979 r"(?i)User-Agent\s*:\s*.*(sqlmap|nikto|nmap|burp|nessus|wpscan|dirbuster|gobuster|hydra)",
980 r"(?i)User-Agent\s*:\s*.*(curl|wget|python|go-http|libwww|axios|node-fetch)",
981 ],
982 },
983 AttackVector {
985 category: "credential_probe", subcategory: "token_brute", severity: High,
986 mitre_id: "T1110.001",
987 search_location: "all",
988 context_keywords: &[],
989 patterns: &[
990 r"(?i)(Authorization|X-API-Key|X-Auth-Token|Bearer)\s*:\s*[A-Za-z0-9\-_\.]{20,}",
991 ],
992 },
993 AttackVector {
995 category: "session_fixation", subcategory: "cookie_set", severity: Medium,
996 mitre_id: "T1539",
997 search_location: "headers",
998 context_keywords: &[],
999 patterns: &[
1000 r"(?i)Cookie\s*:\s*(SESSID|JSESSIONID|PHPSESSID|session_id|sid|connect\.sid)\s*=\s*[A-Za-z0-9]+",
1001 ],
1002 },
1003 AttackVector {
1005 category: "css_injection", subcategory: "data_exfil", severity: Medium,
1006 mitre_id: "T1213",
1007 search_location: "all",
1008 context_keywords: &[],
1009 patterns: &[
1010 r#"(?i)@import\s+url\s*\(\s*['\"]?https?://"#,
1011 r#"(?i)background(-image)?\s*:\s*url\s*\(\s*['\"]?https?://"#,
1012 r#"input\[type\s*=\s*["']password["']\][^{]*\{[^}]*background"#,
1013 ],
1014 },
1015 ])
1016}
1017
1018fn compiled_vectors() -> &'static [CompiledVector] {
1020 static COMPILED: std::sync::OnceLock<Vec<CompiledVector>> = std::sync::OnceLock::new();
1021 COMPILED.get_or_init(|| {
1022 attack_vectors()
1023 .iter()
1024 .map(|av| {
1025 let patterns: Vec<Regex> = av
1026 .patterns
1027 .iter()
1028 .filter_map(|p| Regex::new(p).ok())
1029 .collect();
1030 CompiledVector {
1031 category: av.category,
1032 subcategory: av.subcategory,
1033 patterns,
1034 severity: av.severity.clone(),
1035 mitre_id: av.mitre_id,
1036 search_location: av.search_location,
1037 context_keywords: av.context_keywords,
1038 }
1039 })
1040 .collect()
1041 })
1042}
1043
1044pub struct HoneypotEngine {
1050 config: HoneypotConfig,
1051 state: HoneypotState,
1052 request_times: HashMap<String, Vec<Instant>>,
1054 rsc_endpoints: Vec<String>,
1056}
1057
1058impl HoneypotEngine {
1059 pub fn new() -> Self {
1061 Self::with_config(HoneypotConfig::default())
1062 }
1063
1064 pub fn with_config(config: HoneypotConfig) -> Self {
1066 Self {
1067 config: config.clone(),
1068 state: HoneypotState {
1069 config,
1070 total_requests: 0,
1071 total_attacks_detected: 0,
1072 unique_attackers: 0,
1073 attack_events: Vec::new(),
1074 attacker_profiles: HashMap::new(),
1075 uptime_seconds: 0.0,
1076 requests_per_minute: 0.0,
1077 },
1078 request_times: HashMap::new(),
1079 rsc_endpoints: vec![
1080 "/_rsc/__PAGE__".to_string(),
1081 "/api/graphql".to_string(),
1082 "/api/auth/callback".to_string(),
1083 "/api/chat".to_string(),
1084 "/api/upload".to_string(),
1085 "/api/search".to_string(),
1086 "/api/admin/settings".to_string(),
1087 "/dashboard".to_string(),
1088 ],
1089 }
1090 }
1091
1092 pub fn process_request(&mut self, req: &RawRequest) -> DetectionResult {
1094 let _start = Instant::now();
1095 self.state.total_requests += 1;
1096
1097 self.state.uptime_seconds = (Utc::now().timestamp_millis() as f64) / 1000.0;
1099 if self.state.uptime_seconds > 0.0 {
1100 self.state.requests_per_minute =
1101 (self.state.total_requests as f64 / self.state.uptime_seconds) * 60.0;
1102 }
1103
1104 let profile_id = self.get_or_create_profile_id(req);
1105 self.update_request_times(&profile_id);
1106
1107 let detections = self.detect_attacks(req, &profile_id);
1109
1110 let attack_count = detections.len() as u64;
1111 if attack_count > 0 {
1112 self.state.total_attacks_detected += attack_count;
1113 for det in &detections {
1114 self.state.attack_events.push(det.clone());
1115 if self.state.attack_events.len() > 10000 {
1117 self.state.attack_events.drain(0..1000);
1118 }
1119 }
1120 } else if self.config.log_all_requests {
1121 let event = AttackEvent {
1123 event_id: Self::generate_event_id(),
1124 timestamp: Utc::now().to_rfc3339(),
1125 category: "clean".to_string(),
1126 subcategory: "passive".to_string(),
1127 matched_payload: String::new(),
1128 full_payload: String::new(),
1129 method: req.method.clone(),
1130 path: req.path.clone(),
1131 severity: Severity::Info,
1132 mitre_id: None,
1133 simulated_response: 200,
1134 attacker_ip: req.ip.clone(),
1135 user_agent: req.headers.get("user-agent").cloned().unwrap_or_default(),
1136 headers: req.headers.clone(),
1137 session_id: Some(profile_id.clone()),
1138 confidence: 0.0,
1139 };
1140 self.state.attack_events.push(event);
1141 if self.state.attack_events.len() > 10000 {
1142 self.state.attack_events.drain(0..1000);
1143 }
1144 }
1145
1146 self.update_attacker_profile(req, &profile_id, &detections);
1148
1149 if self.state.attacker_profiles.len() > 10000 {
1151 self.evict_profiles();
1152 }
1153
1154 let simulated_status = self.simulate_status(&detections);
1156 let simulated_body = self.simulate_body(req, &detections);
1157 let content_type = self.simulate_content_type(req);
1158 let should_block = self.should_block_request(&detections);
1159 let suggested_delay = self.calculate_delay(&detections);
1160
1161 DetectionResult {
1162 detections,
1163 simulated_status,
1164 simulated_body,
1165 content_type,
1166 should_block,
1167 suggested_delay_ms: suggested_delay.as_millis() as u64,
1168 }
1169 }
1170
1171 fn detect_attacks(&mut self, req: &RawRequest, profile_id: &str) -> Vec<AttackEvent> {
1173 let mut events = Vec::new();
1174
1175 for vector in compiled_vectors().iter() {
1176 let search_text = match vector.search_location {
1177 "body" => &req.body,
1178 "query" => &req.query_string,
1179 "path" => &req.path,
1180 "headers" => &self.headers_as_string(&req.headers),
1181 "all" => &self.all_request_text(req),
1182 _ => &self.all_request_text(req),
1183 };
1184
1185 if search_text.is_empty() {
1186 continue;
1187 }
1188
1189 if !vector.context_keywords.is_empty() {
1191 let has_context = vector
1192 .context_keywords
1193 .iter()
1194 .any(|kw| search_text.to_lowercase().contains(&kw.to_lowercase()));
1195 if !has_context {
1196 continue;
1197 }
1198 }
1199
1200 for re in &vector.patterns {
1201 if let Some(m) = re.find(search_text) {
1202 let matched = m.as_str().to_string();
1203 let confidence = self.calculate_confidence(vector, &matched, search_text);
1204
1205 if confidence >= self.config.detection_threshold {
1206 events.push(AttackEvent {
1207 event_id: Self::generate_event_id(),
1208 timestamp: Utc::now().to_rfc3339(),
1209 category: vector.category.to_string(),
1210 subcategory: vector.subcategory.to_string(),
1211 matched_payload: Self::truncate_str(&matched, 500),
1212 full_payload: Self::truncate_str(
1213 search_text,
1214 self.config.max_payload_store,
1215 ),
1216 method: req.method.clone(),
1217 path: req.path.clone(),
1218 severity: vector.severity.clone(),
1219 mitre_id: Some(vector.mitre_id.to_string()),
1220 simulated_response: 0, attacker_ip: req.ip.clone(),
1222 user_agent: req.headers.get("user-agent").cloned().unwrap_or_default(),
1223 headers: req.headers.clone(),
1224 session_id: Some(profile_id.to_string()),
1225 confidence,
1226 });
1227 break; }
1229 }
1230 }
1231 }
1232
1233 for event in &mut events {
1235 event.simulated_response = self.simulate_status_for_event(event);
1236 }
1237
1238 events
1239 }
1240
1241 fn calculate_confidence(
1243 &self,
1244 _vector: &CompiledVector,
1245 matched: &str,
1246 full_text: &str,
1247 ) -> f64 {
1248 let mut confidence = 0.5; let specificity_bonus = (matched.len() as f64 / 30.0).min(0.3);
1252 confidence += specificity_bonus;
1253
1254 let keyword_signals = [
1257 ("eval", 0.05),
1258 ("exec", 0.05),
1259 ("system", 0.05),
1260 ("import", 0.03),
1261 ("require", 0.03),
1262 ("base64", 0.04),
1263 ("fromCharCode", 0.05),
1264 ("String.fromCharCode", 0.06),
1265 ("atob", 0.03),
1266 ("charCodeAt", 0.03),
1267 ("document.cookie", 0.06),
1268 ("window.location", 0.04),
1269 ("XMLHttpRequest", 0.03),
1270 ("fetch(", 0.02),
1271 ("curl", 0.04),
1272 ("wget", 0.04),
1273 ("nc ", 0.05),
1274 ("/bin/bash", 0.06),
1275 ("/bin/sh", 0.06),
1276 ("cmd.exe", 0.06),
1277 ("powershell", 0.06),
1278 ("reverse", 0.04),
1279 ("shell", 0.05),
1280 ("backdoor", 0.06),
1281 ("trojan", 0.06),
1282 ("exploit", 0.05),
1283 ];
1284
1285 let lower = full_text.to_lowercase();
1286 for (signal, bonus) in keyword_signals {
1287 if lower.contains(signal) {
1288 confidence += bonus;
1289 }
1290 }
1291
1292 confidence.min(1.0)
1293 }
1294
1295 fn simulate_status(&self, detections: &[AttackEvent]) -> u16 {
1297 if detections.is_empty() {
1298 return 200;
1299 }
1300 let has_critical = detections.iter().any(|d| d.severity == Severity::Critical);
1301 let has_high = detections.iter().any(|d| d.severity == Severity::High);
1302
1303 if has_critical {
1304 500 } else if has_high {
1306 400 } else {
1308 200 }
1310 }
1311
1312 fn simulate_status_for_event(&self, event: &AttackEvent) -> u16 {
1313 match event.severity {
1314 Severity::Critical => 500,
1315 Severity::High => 400,
1316 Severity::Medium | Severity::Low | Severity::Info => 200,
1317 }
1318 }
1319
1320 fn is_rsc_request(&self, req: &RawRequest) -> bool {
1322 self.rsc_endpoints.iter().any(|ep| req.path.starts_with(ep))
1323 || req
1324 .headers
1325 .get("content-type")
1326 .map(|ct| ct.contains("text/x-component"))
1327 .unwrap_or(false)
1328 || req.headers.contains_key("next-action")
1329 }
1330
1331 fn simulate_body(&self, req: &RawRequest, _detections: &[AttackEvent]) -> String {
1333 if !self.config.fake_rsc_responses {
1334 return String::new();
1335 }
1336
1337 let is_rsc = self.is_rsc_request(req);
1338 let mut body = if is_rsc {
1339 self.generate_fake_rsc_response(req)
1340 } else if req.path.contains("/api/") {
1341 self.generate_fake_api_response(req)
1342 } else {
1343 self.generate_fake_html_response(req)
1344 };
1345
1346 if self.config.progressive_sizing {
1349 let extra_bytes = 128 + (Utc::now().timestamp_millis() as usize % 1024);
1350 body.push_str(&" ".repeat(extra_bytes / 32));
1351 }
1352
1353 body
1354 }
1355
1356 fn generate_fake_rsc_response(&self, _req: &RawRequest) -> String {
1358 let responses = [
1359 r#"0:["$","@2",null,{"id":"__PAGE__","children":[["$","@3",null,{"name":"Page","props":{}}]]}]
13601:{"status":"resolved","data":{"pageProps":{"title":"Dashboard","user":{"name":"Admin User","role":"administrator","email":"admin@internal.local"}}}}
13612:["$","div",null,{"className":"page-wrapper","children":[["$","header",null,{"children":"Dashboard"}],["$","main",null,{"children":["$","p",null,{"children":"Welcome back, Admin User"},"$","@4",null,{}]}]]
13623:{"status":"pending","chunks":["@5","@6"]}"#,
1363 r#"0:["$","@2",null,{"id":"__PAGE__"}]
13641:{"status":"resolved","data":{"pageProps":{"items":[{"id":1,"name":"Project Alpha","status":"active","owner":"admin"},{"id":2,"name":"Project Beta","status":"inactive","owner":"user2"},{"id":3,"name":"API Gateway","status":"active","owner":"admin"}]}}}
13652:"$6eb96e9c8e4a3f1b2d5c7a8e9f0a1b2c""#,
1366 r#"0:["$","@2",null,{"id":"__PAGE__","children":[["$","@3",null,{"name":"ErrorBoundary"}]],"fallback":["$","@4",null,{"name":"SuspenseFallback"}]}]
13671:{"status":"pending","chunks":["@5"]}
13682:["$","div",null,{"className":"layout","children":["$","nav",null,{"children":[["$","a",null,{"href":"/dashboard","children":"Dashboard"}],["$","a",null,{"href":"/settings","children":"Settings"}]]}]}"#,
1369 ];
1370
1371 let idx = (Utc::now().timestamp_millis() as usize) % responses.len();
1372 responses[idx].to_string()
1373 }
1374
1375 fn generate_fake_api_response(&self, req: &RawRequest) -> String {
1377 if req.path.contains("/graphql") {
1378 r#"{"data":{"__typename":"Query","node":{"id":"UHJvamVjdDox","name":"Internal Project","owner":{"login":"admin","email":"admin@internal.local"}}}}"#.to_string()
1379 } else {
1380 r#"{"success":true,"data":{"id":"67f1a2b3c4d5","status":"ok","timestamp":"2026-05-06T12:00:00Z","message":"Operation completed"}}"#.to_string()
1381 }
1382 }
1383
1384 fn generate_fake_html_response(&self, _req: &RawRequest) -> String {
1386 format!(
1387 r#"<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="generator" content="Next.js 15.2.3"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Internal Dashboard</title><link rel="preload" href="/_next/static/chunks/app/layout-{}.js" as="script"></head><body><div id="__next"><div class="app-shell"><header class="topbar"><nav><a href="/dashboard">Dashboard</a><a href="/admin">Admin</a><a href="/api/docs">API</a></nav><div class="user-menu">Signed in as <strong>Admin User</strong></div></header><main><!--$?--><template id="B:0"></template><div class="skeleton-loader"><div class="skeleton-card"></div><div class="skeleton-row"></div><div class="skeleton-row short"></div></div><!--/$--></main></div></div><script src="/_next/static/chunks/main-app-{}.js" async></script></body></html>"#,
1388 Self::random_hex(16),
1389 Self::random_hex(16)
1390 )
1391 }
1392
1393 fn simulate_content_type(&self, req: &RawRequest) -> String {
1394 let is_rsc = self.is_rsc_request(req);
1395
1396 if is_rsc {
1397 "text/x-component; charset=utf-8".to_string()
1398 } else if req.path.contains("/api/") {
1399 "application/json; charset=utf-8".to_string()
1400 } else {
1401 "text/html; charset=utf-8".to_string()
1402 }
1403 }
1404
1405 fn should_block_request(&self, detections: &[AttackEvent]) -> bool {
1407 detections
1408 .iter()
1409 .any(|d| d.severity == Severity::Critical || d.confidence > 0.9)
1410 }
1411
1412 fn calculate_delay(&self, _detections: &[AttackEvent]) -> Duration {
1414 if !self.config.realistic_timing {
1415 return Duration::from_millis(0);
1416 }
1417 use std::collections::hash_map::DefaultHasher;
1418 use std::hash::{Hash, Hasher};
1419 let mut hasher = DefaultHasher::new();
1420 Utc::now()
1421 .timestamp_nanos_opt()
1422 .unwrap_or(0)
1423 .hash(&mut hasher);
1424 let hash = hasher.finish();
1425 let jitter = hash % (self.config.max_delay_ms - self.config.min_delay_ms + 1);
1426 Duration::from_millis(self.config.min_delay_ms + jitter)
1427 }
1428
1429 fn get_or_create_profile_id(&mut self, req: &RawRequest) -> String {
1433 let fingerprint = self.build_fingerprint(req);
1434 let hashed = Self::hash_str(&fingerprint);
1435 let profile_id = format!("prof_{}", &hashed[..16]);
1436
1437 if !self.state.attacker_profiles.contains_key(&profile_id) {
1438 self.state.attacker_profiles.insert(
1439 profile_id.clone(),
1440 AttackerProfile {
1441 profile_id: profile_id.clone(),
1442 ip: req.ip.clone(),
1443 country: None,
1444 asn: None,
1445 is_tor: false,
1446 is_cloud: false,
1447 is_proxy: false,
1448 user_agent: req.headers.get("user-agent").cloned().unwrap_or_default(),
1449 browser_fingerprint: self.parse_user_agent(
1450 req.headers
1451 .get("user-agent")
1452 .map(|s| s.as_str())
1453 .unwrap_or(""),
1454 ),
1455 first_seen: Utc::now().to_rfc3339(),
1456 last_seen: Utc::now().to_rfc3339(),
1457 total_requests: 0,
1458 attack_categories: HashMap::new(),
1459 techniques_used: Vec::new(),
1460 avg_request_interval: 0.0,
1461 is_automated: false,
1462 risk_score: 0.0,
1463 targets: Vec::new(),
1464 event_timeline: Vec::new(),
1465 },
1466 );
1467 self.state.unique_attackers = self.state.attacker_profiles.len();
1468 }
1469
1470 profile_id
1471 }
1472
1473 fn build_fingerprint(&self, req: &RawRequest) -> String {
1475 let ua = req
1476 .headers
1477 .get("user-agent")
1478 .map(|s| s.as_str())
1479 .unwrap_or("");
1480 let accept = req.headers.get("accept").map(|s| s.as_str()).unwrap_or("");
1481 let accept_lang = req
1482 .headers
1483 .get("accept-language")
1484 .map(|s| s.as_str())
1485 .unwrap_or("");
1486 let accept_enc = req
1487 .headers
1488 .get("accept-encoding")
1489 .map(|s| s.as_str())
1490 .unwrap_or("");
1491
1492 format!(
1493 "{}|{}|{}|{}|{}",
1494 req.ip, ua, accept, accept_lang, accept_enc
1495 )
1496 }
1497
1498 fn update_request_times(&mut self, profile_id: &str) {
1500 let times = self
1501 .request_times
1502 .entry(profile_id.to_string())
1503 .or_default();
1504 times.push(Instant::now());
1505 if times.len() > 100 {
1507 times.drain(0..times.len() - 100);
1508 }
1509
1510 if let Some(profile) = self.state.attacker_profiles.get_mut(profile_id) {
1512 profile.total_requests += 1;
1513 profile.last_seen = Utc::now().to_rfc3339();
1514 if times.len() >= 3 {
1515 let intervals: Vec<f64> = times
1516 .windows(2)
1517 .map(|w| w[1].duration_since(w[0]).as_secs_f64())
1518 .collect();
1519 profile.avg_request_interval =
1520 intervals.iter().sum::<f64>() / intervals.len() as f64;
1521
1522 if profile.total_requests >= 10 {
1524 let std_dev = Self::std_dev(&intervals, profile.avg_request_interval);
1525 profile.is_automated = profile.avg_request_interval < 0.1
1526 || (profile.avg_request_interval < 0.5 && std_dev < 0.05);
1527 }
1528 }
1529 }
1530 }
1531
1532 fn update_attacker_profile(
1534 &mut self,
1535 req: &RawRequest,
1536 profile_id: &str,
1537 detections: &[AttackEvent],
1538 ) {
1539 if let Some(profile) = self.state.attacker_profiles.get_mut(profile_id) {
1540 for det in detections {
1541 *profile
1542 .attack_categories
1543 .entry(det.category.clone())
1544 .or_insert(0) += 1;
1545 if !profile.techniques_used.contains(&det.subcategory) {
1546 profile.techniques_used.push(det.subcategory.clone());
1547 }
1548 profile.event_timeline.push(format!(
1549 "{} | {}:{} | {} | conf={:.2}",
1550 &det.timestamp[..19],
1551 det.category,
1552 det.subcategory,
1553 det.severity.clone().serde_name().unwrap_or("unknown"),
1554 det.confidence
1555 ));
1556 }
1557 if !profile.targets.contains(&req.path) {
1558 profile.targets.push(req.path.clone());
1559 }
1560
1561 profile.risk_score = Self::calculate_risk_score(profile);
1563 }
1564 }
1565
1566 fn evict_profiles(&mut self) {
1569 let mut sorted: Vec<(String, f64)> = self
1570 .state
1571 .attacker_profiles
1572 .iter()
1573 .map(|(id, p)| (id.clone(), p.risk_score))
1574 .collect();
1575 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1576 let keep_ids: std::collections::HashSet<String> =
1577 sorted.iter().take(5000).map(|(id, _)| id.clone()).collect();
1578 self.state
1579 .attacker_profiles
1580 .retain(|id, _| keep_ids.contains(id));
1581 self.state.unique_attackers = self.state.attacker_profiles.len();
1582 }
1583
1584 fn calculate_risk_score(profile: &AttackerProfile) -> f64 {
1586 let mut score = 0.0;
1587
1588 let severity_weights: HashMap<&str, f64> = [
1590 ("sqli", 15.0),
1591 ("cmdi", 15.0),
1592 ("rce", 15.0),
1593 ("deserialization", 15.0),
1594 ("xxe", 14.0),
1595 ("ssti", 14.0),
1596 ("lfi", 13.0),
1597 ("rfi", 13.0),
1598 ("ssrf", 12.0),
1599 ("nosqli", 12.0),
1600 ("xss", 8.0),
1601 ("path_traversal", 8.0),
1602 ("file_upload", 10.0),
1603 ("crlf", 7.0),
1604 ("http_smuggling", 9.0),
1605 ("jwt", 9.0),
1606 ("auth_bypass", 10.0),
1607 ("prototype_pollution", 8.0),
1608 ("rsc_attack", 14.0),
1609 ("dns_exfil", 6.0),
1610 ]
1611 .into_iter()
1612 .collect();
1613
1614 for (cat, count) in &profile.attack_categories {
1615 let weight = severity_weights.get(cat.as_str()).copied().unwrap_or(3.0);
1616 score += weight * (*count as f64).min(3.0); }
1618
1619 let technique_bonus = (profile.techniques_used.len() as f64 * 2.0).min(20.0);
1621 score += technique_bonus;
1622
1623 if profile.is_automated {
1625 score += 10.0;
1626 }
1627
1628 if profile.total_requests > 100 {
1630 score += 5.0;
1631 }
1632 if profile.total_requests > 500 {
1633 score += 5.0;
1634 }
1635
1636 score.min(100.0)
1637 }
1638
1639 fn parse_user_agent(&self, ua: &str) -> Option<BrowserFingerprint> {
1643 if ua.is_empty() {
1644 return None;
1645 }
1646
1647 let ua_lower = ua.to_lowercase();
1648
1649 let (browser, browser_version) = if ua_lower.contains("firefox") {
1651 ("Firefox", Self::extract_version(ua, "Firefox/"))
1652 } else if ua_lower.contains("edg") {
1653 ("Edge", Self::extract_version(ua, "Edg/"))
1654 } else if ua_lower.contains("chrome") && !ua_lower.contains("chromium") {
1655 ("Chrome", Self::extract_version(ua, "Chrome/"))
1656 } else if ua_lower.contains("safari") && !ua_lower.contains("chrome") {
1657 ("Safari", Self::extract_version(ua, "Version/"))
1658 } else if ua_lower.contains("opera") || ua_lower.contains("opr") {
1659 ("Opera", Self::extract_version(ua, "OPR/"))
1660 } else if ua_lower.contains("msie") || ua_lower.contains("trident") {
1661 ("Internet Explorer", Self::extract_version(ua, "MSIE "))
1662 } else {
1663 ("Unknown", "0.0".to_string())
1664 };
1665
1666 let (os, os_version) = if ua_lower.contains("windows nt 10") {
1668 ("Windows", "10/11".to_string())
1669 } else if ua_lower.contains("windows nt 6.3") {
1670 ("Windows", "8.1".to_string())
1671 } else if ua_lower.contains("windows nt 6.1") {
1672 ("Windows", "7".to_string())
1673 } else if ua_lower.contains("mac os x") {
1674 ("macOS", Self::extract_version(ua, "Mac OS X "))
1675 } else if ua_lower.contains("android") {
1676 ("Android", Self::extract_version(ua, "Android "))
1677 } else if ua_lower.contains("iphone") || ua_lower.contains("ipad") {
1678 ("iOS", Self::extract_version(ua, "OS "))
1679 } else if ua_lower.contains("linux") {
1680 ("Linux", "".to_string())
1681 } else {
1682 ("Unknown", "".to_string())
1683 };
1684
1685 let engine = if ua_lower.contains("webkit") {
1686 "WebKit"
1687 } else if ua_lower.contains("gecko") {
1688 "Gecko"
1689 } else if ua_lower.contains("trident") {
1690 "Trident"
1691 } else {
1692 "Unknown"
1693 };
1694
1695 let device_type = if ua_lower.contains("mobile") || ua_lower.contains("android") {
1696 "Mobile"
1697 } else if ua_lower.contains("tablet") || ua_lower.contains("ipad") {
1698 "Tablet"
1699 } else {
1700 "Desktop"
1701 };
1702
1703 let is_headless = ua_lower.contains("headless")
1704 || ua_lower.contains("phantom")
1705 || ua_lower.contains("puppeteer")
1706 || ua_lower.contains("playwright")
1707 || ua_lower.contains("selenium");
1708
1709 Some(BrowserFingerprint {
1710 browser: browser.to_string(),
1711 browser_version,
1712 os: os.to_string(),
1713 os_version,
1714 engine: engine.to_string(),
1715 device_type: device_type.to_string(),
1716 is_headless,
1717 })
1718 }
1719
1720 fn extract_version(ua: &str, prefix: &str) -> String {
1721 if let Some(pos) = ua.find(prefix) {
1722 let start = pos + prefix.len();
1723 let end = ua[start..]
1724 .find([' ', ';', ')'])
1725 .map(|p| start + p)
1726 .unwrap_or(ua.len());
1727 let version = &ua[start..end];
1728 version.split('.').take(2).collect::<Vec<_>>().join(".")
1730 } else {
1731 "0.0".to_string()
1732 }
1733 }
1734
1735 fn all_request_text(&self, req: &RawRequest) -> String {
1738 format!(
1739 "{} {}?{} {} {}",
1740 req.method,
1741 req.path,
1742 req.query_string,
1743 req.body,
1744 self.headers_as_string(&req.headers)
1745 )
1746 }
1747
1748 fn headers_as_string(&self, headers: &HashMap<String, String>) -> String {
1749 headers
1750 .iter()
1751 .map(|(k, v)| format!("{}: {}", k, v))
1752 .collect::<Vec<_>>()
1753 .join("\n")
1754 }
1755
1756 fn generate_event_id() -> String {
1757 format!(
1758 "evt_{}_{}",
1759 Utc::now().timestamp_millis(),
1760 Self::random_hex(8)
1761 )
1762 }
1763
1764 fn truncate_str(s: &str, max_len: usize) -> String {
1765 if s.len() <= max_len {
1766 s.to_string()
1767 } else {
1768 format!(
1769 "{}...[truncated {} bytes]",
1770 &s[..max_len],
1771 s.len() - max_len
1772 )
1773 }
1774 }
1775
1776 fn random_hex(len: usize) -> String {
1777 use std::collections::hash_map::DefaultHasher;
1778 use std::hash::{Hash, Hasher};
1779 let mut hasher = DefaultHasher::new();
1780 Utc::now()
1781 .timestamp_nanos_opt()
1782 .unwrap_or(0)
1783 .hash(&mut hasher);
1784 format!("{:016x}", hasher.finish())[..len].to_string()
1785 }
1786
1787 fn hash_str(s: &str) -> String {
1788 use std::collections::hash_map::DefaultHasher;
1789 use std::hash::{Hash, Hasher};
1790 let mut hasher = DefaultHasher::new();
1791 s.hash(&mut hasher);
1792 format!("{:016x}", hasher.finish())
1793 }
1794
1795 fn std_dev(values: &[f64], mean: f64) -> f64 {
1796 if values.len() < 2 {
1797 return 0.0;
1798 }
1799 let variance =
1800 values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
1801 variance.sqrt()
1802 }
1803
1804 pub fn get_state(&self) -> &HoneypotState {
1808 &self.state
1809 }
1810
1811 pub fn get_profiles(&self) -> Vec<&AttackerProfile> {
1813 self.state.attacker_profiles.values().collect()
1814 }
1815
1816 pub fn get_profile(&self, profile_id: &str) -> Option<&AttackerProfile> {
1818 self.state.attacker_profiles.get(profile_id)
1819 }
1820
1821 pub fn get_top_threats(&self, n: usize) -> Vec<&AttackerProfile> {
1823 let mut profiles: Vec<&AttackerProfile> = self.state.attacker_profiles.values().collect();
1824 profiles.sort_by(|a, b| {
1825 b.risk_score
1826 .partial_cmp(&a.risk_score)
1827 .unwrap_or(std::cmp::Ordering::Equal)
1828 });
1829 profiles.truncate(n);
1830 profiles
1831 }
1832
1833 pub fn export_json(&self) -> serde_json::Result<String> {
1835 serde_json::to_string_pretty(&self.state)
1836 }
1837
1838 pub fn reset(&mut self) {
1840 self.state = HoneypotState {
1841 config: self.config.clone(),
1842 total_requests: 0,
1843 total_attacks_detected: 0,
1844 unique_attackers: 0,
1845 attack_events: Vec::new(),
1846 attacker_profiles: HashMap::new(),
1847 uptime_seconds: 0.0,
1848 requests_per_minute: 0.0,
1849 };
1850 self.request_times.clear();
1851 }
1852}
1853
1854impl Default for HoneypotEngine {
1855 fn default() -> Self {
1856 Self::new()
1857 }
1858}
1859
1860impl Severity {
1861 fn serde_name(&self) -> Option<&str> {
1862 match self {
1863 Severity::Info => Some("info"),
1864 Severity::Low => Some("low"),
1865 Severity::Medium => Some("medium"),
1866 Severity::High => Some("high"),
1867 Severity::Critical => Some("critical"),
1868 }
1869 }
1870}
1871
1872#[cfg(test)]
1877mod tests {
1878 use super::*;
1879
1880 fn make_request(
1881 method: &str,
1882 path: &str,
1883 body: &str,
1884 headers: Vec<(&str, &str)>,
1885 ) -> RawRequest {
1886 let mut h = HashMap::new();
1887 for (k, v) in headers {
1888 h.insert(k.to_lowercase(), v.to_string());
1889 }
1890 RawRequest {
1891 method: method.to_string(),
1892 path: path.to_string(),
1893 query_string: String::new(),
1894 body: body.to_string(),
1895 headers: h,
1896 ip: "192.168.1.100".to_string(),
1897 timestamp: Utc::now(),
1898 }
1899 }
1900
1901 #[test]
1902 fn test_sqli_detection() {
1903 let mut engine = HoneypotEngine::new();
1904 let req = make_request(
1905 "GET",
1906 "/login",
1907 "username=admin' OR '1'='1&password=test",
1908 vec![],
1909 );
1910 let result = engine.process_request(&req);
1911 let sqli = result
1912 .detections
1913 .iter()
1914 .filter(|d| d.category == "sqli")
1915 .collect::<Vec<_>>();
1916 assert!(!sqli.is_empty(), "Should detect SQL injection");
1917 }
1918
1919 #[test]
1920 fn test_xss_detection() {
1921 let mut engine = HoneypotEngine::new();
1922 let req = make_request("GET", "/search", "q=<script>alert('XSS')</script>", vec![]);
1923 let result = engine.process_request(&req);
1924 let xss = result
1925 .detections
1926 .iter()
1927 .filter(|d| d.category == "xss")
1928 .collect::<Vec<_>>();
1929 assert!(!xss.is_empty(), "Should detect XSS");
1930 }
1931
1932 #[test]
1933 fn test_cmdi_detection() {
1934 let mut engine = HoneypotEngine::new();
1935 let req = make_request("POST", "/api/exec", "cmd=;id", vec![]);
1936 let result = engine.process_request(&req);
1937 let cmdi = result
1938 .detections
1939 .iter()
1940 .filter(|d| d.category == "cmdi")
1941 .collect::<Vec<_>>();
1942 assert!(!cmdi.is_empty(), "Should detect command injection");
1943 }
1944
1945 #[test]
1946 fn test_path_traversal_detection() {
1947 let mut engine = HoneypotEngine::new();
1948 let req = make_request("GET", "/download", "file=../../../etc/passwd", vec![]);
1949 let result = engine.process_request(&req);
1950 let pt = result
1951 .detections
1952 .iter()
1953 .filter(|d| d.category == "path_traversal")
1954 .collect::<Vec<_>>();
1955 assert!(!pt.is_empty(), "Should detect path traversal");
1956 }
1957
1958 #[test]
1959 fn test_ssti_detection() {
1960 let mut engine = HoneypotEngine::new();
1961 let req = make_request("POST", "/contact", "name={{7*7}}", vec![]);
1962 let result = engine.process_request(&req);
1963 let ssti = result
1964 .detections
1965 .iter()
1966 .filter(|d| d.category == "ssti")
1967 .collect::<Vec<_>>();
1968 assert!(!ssti.is_empty(), "Should detect SSTI ({{7*7}})");
1969 }
1970
1971 #[test]
1972 fn test_lfi_detection() {
1973 let mut engine = HoneypotEngine::new();
1974 let req = make_request("GET", "/view", "page=/etc/passwd", vec![]);
1975 let result = engine.process_request(&req);
1976 let lfi = result
1977 .detections
1978 .iter()
1979 .filter(|d| d.category == "lfi")
1980 .collect::<Vec<_>>();
1981 assert!(!lfi.is_empty(), "Should detect LFI (/etc/passwd)");
1982 }
1983
1984 #[test]
1985 fn test_ssrf_metadata_detection() {
1986 let mut engine = HoneypotEngine::new();
1987 let req = make_request(
1988 "POST",
1989 "/api/fetch",
1990 "url=http://169.254.169.254/latest/meta-data/",
1991 vec![],
1992 );
1993 let result = engine.process_request(&req);
1994 let ssrf = result
1995 .detections
1996 .iter()
1997 .filter(|d| d.category == "ssrf")
1998 .collect::<Vec<_>>();
1999 assert!(!ssrf.is_empty(), "Should detect SSRF cloud metadata probe");
2000 }
2001
2002 #[test]
2003 fn test_rsc_flight_detection() {
2004 let mut engine = HoneypotEngine::new();
2005 let req = make_request(
2006 "POST",
2007 "/",
2008 r#"0:[["$","@1",null,{"id":"malicious_component","chunks":[]}]]"#,
2009 vec![
2010 ("Content-Type", "text/x-component"),
2011 ("Next-Action", "exploit"),
2012 ],
2013 );
2014 let result = engine.process_request(&req);
2015 let rsc = result
2016 .detections
2017 .iter()
2018 .filter(|d| d.category == "rsc_attack")
2019 .collect::<Vec<_>>();
2020 assert!(!rsc.is_empty(), "Should detect RSC/Flight protocol attack");
2021 }
2022
2023 #[test]
2024 fn test_attacker_profiling() {
2025 let mut engine = HoneypotEngine::new();
2026
2027 for i in 0..5 {
2029 let body = format!("cmd=;id_{}", i);
2030 let req = make_request(
2031 "POST",
2032 "/api/exec",
2033 &body,
2034 vec![("User-Agent", "sqlmap/1.0")],
2035 );
2036 engine.process_request(&req);
2037 }
2038
2039 let profiles = engine.get_profiles();
2040 assert!(!profiles.is_empty(), "Should have at least one profile");
2041 let profile = &profiles[0];
2042 assert!(profile.total_requests >= 5);
2043 assert!(!profile.techniques_used.is_empty());
2044 }
2045
2046 #[test]
2047 fn test_ua_parsing() {
2048 let engine = HoneypotEngine::new();
2049 let fp = engine.parse_user_agent(
2050 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
2051 );
2052 let fp = fp.unwrap();
2053 assert_eq!(fp.browser, "Chrome");
2054 assert_eq!(fp.os, "Windows");
2055 assert_eq!(fp.engine, "WebKit");
2056 }
2057
2058 #[test]
2059 fn test_no_detection_on_clean_request() {
2060 let mut engine = HoneypotEngine::new();
2061 let req = make_request("GET", "/", "", vec![("User-Agent", "Mozilla/5.0")]);
2062 let result = engine.process_request(&req);
2063 let critical = result
2065 .detections
2066 .iter()
2067 .filter(|d| d.severity >= Severity::High)
2068 .count();
2069 assert_eq!(
2070 critical, 0,
2071 "Clean request should not trigger high-severity detections"
2072 );
2073 }
2074
2075 #[test]
2076 fn test_risk_score() {
2077 let mut profile = AttackerProfile {
2078 profile_id: "test".to_string(),
2079 ip: "10.0.0.1".to_string(),
2080 country: None,
2081 asn: None,
2082 is_tor: false,
2083 is_cloud: false,
2084 is_proxy: false,
2085 user_agent: String::new(),
2086 browser_fingerprint: None,
2087 first_seen: String::new(),
2088 last_seen: String::new(),
2089 total_requests: 200,
2090 attack_categories: [
2091 ("sqli".to_string(), 5),
2092 ("xss".to_string(), 3),
2093 ("cmdi".to_string(), 2),
2094 ]
2095 .into_iter()
2096 .collect(),
2097 techniques_used: vec![
2098 "union_select".to_string(),
2099 "stacked".to_string(),
2100 "reflected".to_string(),
2101 ],
2102 avg_request_interval: 0.05,
2103 is_automated: true,
2104 risk_score: 0.0,
2105 targets: vec!["/login".to_string(), "/api/exec".to_string()],
2106 event_timeline: vec![],
2107 };
2108
2109 let score = HoneypotEngine::calculate_risk_score(&profile);
2110 profile.risk_score = score;
2111
2112 assert!(
2113 score > 60.0,
2114 "Risk score should be high for diverse attacks: got {}",
2115 score
2116 );
2117 assert!(score <= 100.0, "Risk score should not exceed 100");
2118 }
2119
2120 #[test]
2121 fn test_export_json() {
2122 let mut engine = HoneypotEngine::new();
2123 let req = make_request(
2124 "POST",
2125 "/api/login",
2126 "user=admin'--",
2127 vec![("User-Agent", "Mozilla/5.0")],
2128 );
2129 engine.process_request(&req);
2130
2131 let json = engine.export_json().unwrap();
2132 assert!(json.contains("sqli"));
2133 assert!(json.contains("admin"));
2134 }
2135
2136 #[test]
2137 fn test_fake_rsc_response() {
2138 let engine = HoneypotEngine::new();
2139 let req = make_request(
2140 "POST",
2141 "/dashboard",
2142 "[]",
2143 vec![
2144 ("Content-Type", "text/x-component"),
2145 ("Next-Action", "test"),
2146 ],
2147 );
2148 let body = engine.generate_fake_rsc_response(&req);
2149 assert!(!body.is_empty());
2150 assert!(body.contains("$") || body.contains("pageProps") || body.contains("status"));
2151 }
2152
2153 #[test]
2154 fn test_fake_html_response() {
2155 let engine = HoneypotEngine::new();
2156 let req = make_request("GET", "/", "", vec![]);
2157 let body = engine.generate_fake_html_response(&req);
2158 assert!(body.contains("<html"));
2159 assert!(body.contains("Next.js"));
2160 assert!(body.contains("__next"));
2161 }
2162}