1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum TargetClass {
16 Api,
18 ContentSite,
20 HighSecurity,
22 Unknown,
24}
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct BlockedRatioSlo {
32 pub target_class: TargetClass,
34 pub acceptable: f64,
36 pub warning: f64,
38 pub critical: f64,
40}
41
42impl BlockedRatioSlo {
43 #[must_use]
45 pub const fn api() -> Self {
46 Self {
47 target_class: TargetClass::Api,
48 acceptable: 0.05,
49 warning: 0.10,
50 critical: 0.15,
51 }
52 }
53
54 #[must_use]
56 pub const fn content_site() -> Self {
57 Self {
58 target_class: TargetClass::ContentSite,
59 acceptable: 0.15,
60 warning: 0.25,
61 critical: 0.40,
62 }
63 }
64
65 #[must_use]
67 pub const fn high_security() -> Self {
68 Self {
69 target_class: TargetClass::HighSecurity,
70 acceptable: 0.30,
71 warning: 0.50,
72 critical: 0.70,
73 }
74 }
75
76 #[must_use]
78 pub const fn unknown() -> Self {
79 Self {
80 target_class: TargetClass::Unknown,
81 acceptable: 0.05, warning: 0.10,
83 critical: 0.15,
84 }
85 }
86
87 #[must_use]
89 pub const fn for_class(class: TargetClass) -> Self {
90 match class {
91 TargetClass::Api => Self::api(),
92 TargetClass::ContentSite => Self::content_site(),
93 TargetClass::HighSecurity => Self::high_security(),
94 TargetClass::Unknown => Self::unknown(),
95 }
96 }
97
98 #[must_use]
102 pub fn assess(&self, blocked_ratio: f64) -> (bool, bool, bool) {
103 (
104 blocked_ratio <= self.acceptable,
105 blocked_ratio > self.acceptable && blocked_ratio <= self.warning,
106 blocked_ratio > self.critical,
107 )
108 }
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
113pub struct TransactionView {
114 pub url: String,
116 pub status: u16,
118 pub response_headers: BTreeMap<String, String>,
120 pub response_body_snippet: Option<String>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
126pub enum AntiBotProvider {
127 DataDome,
129 Cloudflare,
131 Akamai,
133 PerimeterX,
135 Kasada,
137 FingerprintCom,
139 Unknown,
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct Detection {
146 pub provider: AntiBotProvider,
148 pub confidence: f64,
150 pub markers: Vec<String>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct ProviderScore {
157 pub provider: AntiBotProvider,
159 pub score: u32,
161 pub markers: Vec<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct HarRequestSummary {
168 pub url: String,
170 pub status: u16,
172 pub resource_type: Option<String>,
174 pub detection: Detection,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct HarClassificationReport {
181 pub page_title: Option<String>,
183 pub aggregate: Detection,
185 pub requests: Vec<HarRequestSummary>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct MarkerCount {
192 pub marker: String,
194 pub count: u64,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct HostSummary {
201 pub host: String,
203 pub total_requests: u64,
205 pub blocked_requests: u64,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct InvestigationReport {
212 pub page_title: Option<String>,
214 pub total_requests: u64,
216 pub blocked_requests: u64,
218 pub status_histogram: BTreeMap<u16, u64>,
220 pub resource_type_histogram: BTreeMap<String, u64>,
222 pub provider_histogram: BTreeMap<AntiBotProvider, u64>,
224 pub marker_histogram: BTreeMap<String, u64>,
226 pub top_markers: Vec<MarkerCount>,
228 pub hosts: Vec<HostSummary>,
230 pub suspicious_requests: Vec<HarRequestSummary>,
232 pub aggregate: Detection,
234 #[serde(default)]
236 pub target_class: Option<TargetClass>,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct InvestigationDiff {
242 pub baseline_total_requests: u64,
244 pub candidate_total_requests: u64,
246 pub baseline_blocked_requests: u64,
248 pub candidate_blocked_requests: u64,
250 pub blocked_ratio_delta: f64,
252 pub likely_regression: bool,
254 pub provider_delta: BTreeMap<AntiBotProvider, i64>,
256 pub new_markers: Vec<String>,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
262pub enum RequirementLevel {
263 Low,
265 Medium,
267 High,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct AntiBotRequirement {
274 pub id: String,
276 pub title: String,
278 pub why: String,
280 pub evidence: Vec<String>,
282 pub level: RequirementLevel,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288pub enum AdapterStrategy {
289 DirectHttp,
291 BrowserStealth,
293 StickyProxy,
295 SessionWarmup,
297 InvestigateOnly,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct IntegrationRecommendation {
304 pub strategy: AdapterStrategy,
306 pub rationale: String,
308 pub required_stygian_features: Vec<String>,
310 pub config_hints: BTreeMap<String, String>,
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub struct RequirementsProfile {
317 pub provider: AntiBotProvider,
319 pub confidence: f64,
321 pub requirements: Vec<AntiBotRequirement>,
323 pub recommendation: IntegrationRecommendation,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329pub enum ExecutionMode {
330 Http,
332 Browser,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
338pub enum SessionMode {
339 Stateless,
341 Sticky,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
347pub enum TelemetryLevel {
348 Basic,
350 Standard,
352 Deep,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358pub struct RuntimePolicy {
359 pub execution_mode: ExecutionMode,
361 pub session_mode: SessionMode,
363 pub telemetry_level: TelemetryLevel,
365 pub rate_limit_rps: f64,
367 pub max_retries: u32,
369 pub backoff_base_ms: u64,
371 pub enable_warmup: bool,
373 pub enforce_webrtc_proxy_only: bool,
375 pub sticky_session_ttl_secs: Option<u64>,
377 pub required_stygian_features: Vec<String>,
379 pub config_hints: BTreeMap<String, String>,
381 pub risk_score: f64,
383}
384
385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
387pub struct InvestigationBundle {
388 pub report: InvestigationReport,
390 pub requirements: RequirementsProfile,
392 pub policy: RuntimePolicy,
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_blocked_ratio_slo_api_thresholds() {
402 let slo = BlockedRatioSlo::api();
403 assert_eq!(slo.target_class, TargetClass::Api);
404 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
405 assert!((slo.warning - 0.10).abs() < f64::EPSILON);
406 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
407 }
408
409 #[test]
410 fn test_blocked_ratio_slo_content_site_thresholds() {
411 let slo = BlockedRatioSlo::content_site();
412 assert_eq!(slo.target_class, TargetClass::ContentSite);
413 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
414 assert!((slo.warning - 0.25).abs() < f64::EPSILON);
415 assert!((slo.critical - 0.40).abs() < f64::EPSILON);
416 }
417
418 #[test]
419 fn test_blocked_ratio_slo_high_security_thresholds() {
420 let slo = BlockedRatioSlo::high_security();
421 assert_eq!(slo.target_class, TargetClass::HighSecurity);
422 assert!((slo.acceptable - 0.30).abs() < f64::EPSILON);
423 assert!((slo.warning - 0.50).abs() < f64::EPSILON);
424 assert!((slo.critical - 0.70).abs() < f64::EPSILON);
425 }
426
427 #[test]
428 fn test_blocked_ratio_slo_unknown_defaults_to_api() {
429 let slo = BlockedRatioSlo::unknown();
430 assert_eq!(slo.target_class, TargetClass::Unknown);
431 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON); assert!((slo.warning - 0.10).abs() < f64::EPSILON);
433 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
434 }
435
436 #[test]
437 fn test_blocked_ratio_slo_for_class_api() {
438 let slo = BlockedRatioSlo::for_class(TargetClass::Api);
439 assert_eq!(slo.target_class, TargetClass::Api);
440 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
441 }
442
443 #[test]
444 fn test_blocked_ratio_slo_for_class_content_site() {
445 let slo = BlockedRatioSlo::for_class(TargetClass::ContentSite);
446 assert_eq!(slo.target_class, TargetClass::ContentSite);
447 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
448 }
449
450 #[test]
451 fn test_blocked_ratio_slo_assess_below_acceptable() {
452 let slo = BlockedRatioSlo::api();
453 let (acceptable, warning, critical) = slo.assess(0.02);
454 assert!(acceptable);
455 assert!(!warning);
456 assert!(!critical);
457 }
458
459 #[test]
460 fn test_blocked_ratio_slo_assess_at_acceptable() {
461 let slo = BlockedRatioSlo::api();
462 let (acceptable, warning, critical) = slo.assess(0.05);
463 assert!(acceptable);
464 assert!(!warning);
465 assert!(!critical);
466 }
467
468 #[test]
469 fn test_blocked_ratio_slo_assess_in_warning_zone() {
470 let slo = BlockedRatioSlo::api();
471 let (acceptable, warning, critical) = slo.assess(0.075);
472 assert!(!acceptable);
473 assert!(warning);
474 assert!(!critical);
475 }
476
477 #[test]
478 fn test_blocked_ratio_slo_assess_at_warning() {
479 let slo = BlockedRatioSlo::api();
480 let (acceptable, warning, critical) = slo.assess(0.10);
481 assert!(!acceptable);
484 assert!(warning); assert!(!critical);
486 }
487
488 #[test]
489 fn test_blocked_ratio_slo_assess_between_warning_and_critical() {
490 let slo = BlockedRatioSlo::api();
491 let (acceptable, warning, critical) = slo.assess(0.125);
492 assert!(!acceptable);
493 assert!(!warning);
494 assert!(!critical);
495 }
496
497 #[test]
498 fn test_blocked_ratio_slo_assess_above_critical() {
499 let slo = BlockedRatioSlo::api();
500 let (acceptable, warning, critical) = slo.assess(0.20);
501 assert!(!acceptable);
502 assert!(!warning);
503 assert!(critical);
504 }
505
506 #[test]
507 fn test_blocked_ratio_slo_content_site_assessment() {
508 let slo = BlockedRatioSlo::content_site();
509
510 let (acc, warn, crit) = slo.assess(0.10);
512 assert!(acc && !warn && !crit);
513
514 let (acc, warn, crit) = slo.assess(0.20);
516 assert!(!acc && warn && !crit);
517
518 let (acc, warn, crit) = slo.assess(0.45);
520 assert!(!acc && !warn && crit);
521
522 let (acc, warn, crit) = slo.assess(0.40);
524 assert!(!acc && !warn && !crit); }
526
527 #[test]
528 fn test_target_class_derives() {
529 let api1 = TargetClass::Api;
531 let api2 = TargetClass::Api;
532 let content = TargetClass::ContentSite;
533
534 assert_eq!(api1, api2);
535 assert_ne!(api1, content);
536 }
537
538 #[test]
539 fn test_blocked_ratio_slo_serialization() {
540 let slo = BlockedRatioSlo::content_site();
541 let json = serde_json::to_string(&slo).unwrap_or_default();
542 if let Ok(deserialized) = serde_json::from_str::<BlockedRatioSlo>(&json) {
543 assert_eq!(slo, deserialized);
544 }
545 }
546
547 #[test]
548 fn test_target_class_serialization() {
549 let target = TargetClass::HighSecurity;
550 let json = serde_json::to_string(&target).unwrap_or_default();
551 if let Ok(deserialized) = serde_json::from_str::<TargetClass>(&json) {
552 assert_eq!(target, deserialized);
553 }
554 }
555}