1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum TargetClass {
17 Api,
19 ContentSite,
21 HighSecurity,
23 Unknown,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct BlockedRatioSlo {
33 pub target_class: TargetClass,
35 pub acceptable: f64,
37 pub warning: f64,
39 pub critical: f64,
41}
42
43impl BlockedRatioSlo {
44 #[must_use]
46 pub const fn api() -> Self {
47 Self {
48 target_class: TargetClass::Api,
49 acceptable: 0.05,
50 warning: 0.10,
51 critical: 0.15,
52 }
53 }
54
55 #[must_use]
57 pub const fn content_site() -> Self {
58 Self {
59 target_class: TargetClass::ContentSite,
60 acceptable: 0.15,
61 warning: 0.25,
62 critical: 0.40,
63 }
64 }
65
66 #[must_use]
68 pub const fn high_security() -> Self {
69 Self {
70 target_class: TargetClass::HighSecurity,
71 acceptable: 0.30,
72 warning: 0.50,
73 critical: 0.70,
74 }
75 }
76
77 #[must_use]
79 pub const fn unknown() -> Self {
80 Self {
81 target_class: TargetClass::Unknown,
82 acceptable: 0.05, warning: 0.10,
84 critical: 0.15,
85 }
86 }
87
88 #[must_use]
90 pub const fn for_class(class: TargetClass) -> Self {
91 match class {
92 TargetClass::Api => Self::api(),
93 TargetClass::ContentSite => Self::content_site(),
94 TargetClass::HighSecurity => Self::high_security(),
95 TargetClass::Unknown => Self::unknown(),
96 }
97 }
98
99 #[must_use]
103 pub fn assess(&self, blocked_ratio: f64) -> (bool, bool, bool) {
104 (
105 blocked_ratio <= self.acceptable,
106 blocked_ratio > self.acceptable && blocked_ratio <= self.warning,
107 blocked_ratio > self.critical,
108 )
109 }
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
114pub struct TransactionView {
115 pub url: String,
117 pub status: u16,
119 pub response_headers: BTreeMap<String, String>,
121 pub response_body_snippet: Option<String>,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
127pub enum AntiBotProvider {
128 DataDome,
130 Cloudflare,
132 Akamai,
134 PerimeterX,
136 Kasada,
138 FingerprintCom,
140 Unknown,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct Detection {
147 pub provider: AntiBotProvider,
149 pub confidence: f64,
151 pub markers: Vec<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct ProviderScore {
158 pub provider: AntiBotProvider,
160 pub score: u32,
162 pub markers: Vec<String>,
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct HarRequestSummary {
169 pub url: String,
171 pub status: u16,
173 pub resource_type: Option<String>,
175 pub detection: Detection,
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct HarClassificationReport {
182 pub page_title: Option<String>,
184 pub aggregate: Detection,
186 pub requests: Vec<HarRequestSummary>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct MarkerCount {
193 pub marker: String,
195 pub count: u64,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct HostSummary {
202 pub host: String,
204 pub total_requests: u64,
206 pub blocked_requests: u64,
208}
209
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct InvestigationReport {
213 pub page_title: Option<String>,
215 pub total_requests: u64,
217 pub blocked_requests: u64,
219 pub status_histogram: BTreeMap<u16, u64>,
221 pub resource_type_histogram: BTreeMap<String, u64>,
223 pub provider_histogram: BTreeMap<AntiBotProvider, u64>,
225 pub marker_histogram: BTreeMap<String, u64>,
227 pub top_markers: Vec<MarkerCount>,
229 pub hosts: Vec<HostSummary>,
231 pub suspicious_requests: Vec<HarRequestSummary>,
233 pub aggregate: Detection,
235 #[serde(default)]
237 pub target_class: Option<TargetClass>,
238}
239
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct InvestigationDiff {
243 pub baseline_total_requests: u64,
245 pub candidate_total_requests: u64,
247 pub baseline_blocked_requests: u64,
249 pub candidate_blocked_requests: u64,
251 pub blocked_ratio_delta: f64,
253 pub likely_regression: bool,
255 pub provider_delta: BTreeMap<AntiBotProvider, i64>,
257 pub new_markers: Vec<String>,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
263pub enum RequirementLevel {
264 Low,
266 Medium,
268 High,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
274pub struct AntiBotRequirement {
275 pub id: String,
277 pub title: String,
279 pub why: String,
281 pub evidence: Vec<String>,
283 pub level: RequirementLevel,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289pub enum AdapterStrategy {
290 DirectHttp,
292 BrowserStealth,
294 StickyProxy,
296 SessionWarmup,
298 InvestigateOnly,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct IntegrationRecommendation {
305 pub strategy: AdapterStrategy,
307 pub rationale: String,
309 pub required_stygian_features: Vec<String>,
311 pub config_hints: BTreeMap<String, String>,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
317pub struct RequirementsProfile {
318 pub provider: AntiBotProvider,
320 pub confidence: f64,
322 pub requirements: Vec<AntiBotRequirement>,
324 pub recommendation: IntegrationRecommendation,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(rename_all = "snake_case")]
331pub enum ExecutionMode {
332 Http,
334 Browser,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(rename_all = "snake_case")]
341pub enum SessionMode {
342 Stateless,
344 Sticky,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350#[serde(rename_all = "snake_case")]
351pub enum TelemetryLevel {
352 Basic,
354 Standard,
356 Deep,
358}
359
360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362pub struct RuntimePolicy {
363 pub execution_mode: ExecutionMode,
365 pub session_mode: SessionMode,
367 pub telemetry_level: TelemetryLevel,
369 pub rate_limit_rps: f64,
371 pub max_retries: u32,
373 pub backoff_base_ms: u64,
375 pub enable_warmup: bool,
377 pub enforce_webrtc_proxy_only: bool,
379 pub sticky_session_ttl_secs: Option<u64>,
381 pub required_stygian_features: Vec<String>,
383 pub config_hints: BTreeMap<String, String>,
385 pub risk_score: f64,
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391pub struct InvestigationBundle {
392 pub report: InvestigationReport,
394 pub requirements: RequirementsProfile,
396 pub policy: RuntimePolicy,
398}
399
400#[cfg(test)]
401#[allow(
402 clippy::unwrap_used,
403 clippy::expect_used,
404 clippy::panic,
405 clippy::indexing_slicing
406)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_blocked_ratio_slo_api_thresholds() {
412 let slo = BlockedRatioSlo::api();
413 assert_eq!(slo.target_class, TargetClass::Api);
414 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
415 assert!((slo.warning - 0.10).abs() < f64::EPSILON);
416 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
417 }
418
419 #[test]
420 fn test_blocked_ratio_slo_content_site_thresholds() {
421 let slo = BlockedRatioSlo::content_site();
422 assert_eq!(slo.target_class, TargetClass::ContentSite);
423 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
424 assert!((slo.warning - 0.25).abs() < f64::EPSILON);
425 assert!((slo.critical - 0.40).abs() < f64::EPSILON);
426 }
427
428 #[test]
429 fn test_blocked_ratio_slo_high_security_thresholds() {
430 let slo = BlockedRatioSlo::high_security();
431 assert_eq!(slo.target_class, TargetClass::HighSecurity);
432 assert!((slo.acceptable - 0.30).abs() < f64::EPSILON);
433 assert!((slo.warning - 0.50).abs() < f64::EPSILON);
434 assert!((slo.critical - 0.70).abs() < f64::EPSILON);
435 }
436
437 #[test]
438 fn test_blocked_ratio_slo_unknown_defaults_to_api() {
439 let slo = BlockedRatioSlo::unknown();
440 assert_eq!(slo.target_class, TargetClass::Unknown);
441 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON); assert!((slo.warning - 0.10).abs() < f64::EPSILON);
443 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
444 }
445
446 #[test]
447 fn test_blocked_ratio_slo_for_class_api() {
448 let slo = BlockedRatioSlo::for_class(TargetClass::Api);
449 assert_eq!(slo.target_class, TargetClass::Api);
450 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
451 }
452
453 #[test]
454 fn test_blocked_ratio_slo_for_class_content_site() {
455 let slo = BlockedRatioSlo::for_class(TargetClass::ContentSite);
456 assert_eq!(slo.target_class, TargetClass::ContentSite);
457 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
458 }
459
460 #[test]
461 fn test_blocked_ratio_slo_assess_below_acceptable() {
462 let slo = BlockedRatioSlo::api();
463 let (acceptable, warning, critical) = slo.assess(0.02);
464 assert!(acceptable);
465 assert!(!warning);
466 assert!(!critical);
467 }
468
469 #[test]
470 fn test_blocked_ratio_slo_assess_at_acceptable() {
471 let slo = BlockedRatioSlo::api();
472 let (acceptable, warning, critical) = slo.assess(0.05);
473 assert!(acceptable);
474 assert!(!warning);
475 assert!(!critical);
476 }
477
478 #[test]
479 fn test_blocked_ratio_slo_assess_in_warning_zone() {
480 let slo = BlockedRatioSlo::api();
481 let (acceptable, warning, critical) = slo.assess(0.075);
482 assert!(!acceptable);
483 assert!(warning);
484 assert!(!critical);
485 }
486
487 #[test]
488 fn test_blocked_ratio_slo_assess_at_warning() {
489 let slo = BlockedRatioSlo::api();
490 let (acceptable, warning, critical) = slo.assess(0.10);
491 assert!(!acceptable);
494 assert!(warning); assert!(!critical);
496 }
497
498 #[test]
499 fn test_blocked_ratio_slo_assess_between_warning_and_critical() {
500 let slo = BlockedRatioSlo::api();
501 let (acceptable, warning, critical) = slo.assess(0.125);
502 assert!(!acceptable);
503 assert!(!warning);
504 assert!(!critical);
505 }
506
507 #[test]
508 fn test_blocked_ratio_slo_assess_above_critical() {
509 let slo = BlockedRatioSlo::api();
510 let (acceptable, warning, critical) = slo.assess(0.20);
511 assert!(!acceptable);
512 assert!(!warning);
513 assert!(critical);
514 }
515
516 #[test]
517 fn test_blocked_ratio_slo_content_site_assessment() {
518 let slo = BlockedRatioSlo::content_site();
519
520 let (acc, warn, crit) = slo.assess(0.10);
522 assert!(acc && !warn && !crit);
523
524 let (acc, warn, crit) = slo.assess(0.20);
526 assert!(!acc && warn && !crit);
527
528 let (acc, warn, crit) = slo.assess(0.45);
530 assert!(!acc && !warn && crit);
531
532 let (acc, warn, crit) = slo.assess(0.40);
534 assert!(!acc && !warn && !crit); }
536
537 #[test]
538 fn test_target_class_derives() {
539 let api1 = TargetClass::Api;
541 let api2 = TargetClass::Api;
542 let content = TargetClass::ContentSite;
543
544 assert_eq!(api1, api2);
545 assert_ne!(api1, content);
546 }
547
548 #[test]
549 fn test_blocked_ratio_slo_serialization() {
550 let slo = BlockedRatioSlo::content_site();
551 let json = serde_json::to_string(&slo).unwrap_or_default();
552 if let Ok(deserialized) = serde_json::from_str::<BlockedRatioSlo>(&json) {
553 assert_eq!(slo, deserialized);
554 }
555 }
556
557 #[test]
558 fn test_target_class_serialization() {
559 let target = TargetClass::HighSecurity;
560 let json = serde_json::to_string(&target).unwrap_or_default();
561 if let Ok(deserialized) = serde_json::from_str::<TargetClass>(&json) {
562 assert_eq!(target, deserialized);
563 }
564 }
565}