1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(
15 Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
16)]
17#[serde(rename_all = "snake_case")]
18pub enum TargetClass {
19 Api,
21 ContentSite,
23 HighSecurity,
25 #[default]
27 Unknown,
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct BlockedRatioSlo {
36 pub target_class: TargetClass,
38 pub acceptable: f64,
40 pub warning: f64,
42 pub critical: f64,
44}
45
46impl BlockedRatioSlo {
47 #[must_use]
49 pub const fn api() -> Self {
50 Self {
51 target_class: TargetClass::Api,
52 acceptable: 0.05,
53 warning: 0.10,
54 critical: 0.15,
55 }
56 }
57
58 #[must_use]
60 pub const fn content_site() -> Self {
61 Self {
62 target_class: TargetClass::ContentSite,
63 acceptable: 0.15,
64 warning: 0.25,
65 critical: 0.40,
66 }
67 }
68
69 #[must_use]
71 pub const fn high_security() -> Self {
72 Self {
73 target_class: TargetClass::HighSecurity,
74 acceptable: 0.30,
75 warning: 0.50,
76 critical: 0.70,
77 }
78 }
79
80 #[must_use]
82 pub const fn unknown() -> Self {
83 Self {
84 target_class: TargetClass::Unknown,
85 acceptable: 0.05, warning: 0.10,
87 critical: 0.15,
88 }
89 }
90
91 #[must_use]
93 pub const fn for_class(class: TargetClass) -> Self {
94 match class {
95 TargetClass::Api => Self::api(),
96 TargetClass::ContentSite => Self::content_site(),
97 TargetClass::HighSecurity => Self::high_security(),
98 TargetClass::Unknown => Self::unknown(),
99 }
100 }
101
102 #[must_use]
106 pub fn assess(&self, blocked_ratio: f64) -> (bool, bool, bool) {
107 (
108 blocked_ratio <= self.acceptable,
109 blocked_ratio > self.acceptable && blocked_ratio <= self.warning,
110 blocked_ratio > self.critical,
111 )
112 }
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
117pub struct TransactionView {
118 pub url: String,
120 pub status: u16,
122 pub response_headers: BTreeMap<String, String>,
124 pub response_body_snippet: Option<String>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
130pub enum AntiBotProvider {
131 DataDome,
133 Cloudflare,
135 Akamai,
137 PerimeterX,
139 Kasada,
141 FingerprintCom,
143 Unknown,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct Detection {
150 pub provider: AntiBotProvider,
152 pub confidence: f64,
154 pub markers: Vec<String>,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct ProviderScore {
161 pub provider: AntiBotProvider,
163 pub score: u32,
165 pub markers: Vec<String>,
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct HarRequestSummary {
172 pub url: String,
174 pub status: u16,
176 pub resource_type: Option<String>,
178 pub detection: Detection,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct HarClassificationReport {
185 pub page_title: Option<String>,
187 pub aggregate: Detection,
189 pub requests: Vec<HarRequestSummary>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct MarkerCount {
196 pub marker: String,
198 pub count: u64,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct HostSummary {
205 pub host: String,
207 pub total_requests: u64,
209 pub blocked_requests: u64,
211}
212
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub struct InvestigationReport {
216 pub page_title: Option<String>,
218 pub total_requests: u64,
220 pub blocked_requests: u64,
222 pub status_histogram: BTreeMap<u16, u64>,
224 pub resource_type_histogram: BTreeMap<String, u64>,
226 pub provider_histogram: BTreeMap<AntiBotProvider, u64>,
228 pub marker_histogram: BTreeMap<String, u64>,
230 pub top_markers: Vec<MarkerCount>,
232 pub hosts: Vec<HostSummary>,
234 pub suspicious_requests: Vec<HarRequestSummary>,
236 pub aggregate: Detection,
238 #[serde(default)]
240 pub target_class: Option<TargetClass>,
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct InvestigationDiff {
246 pub baseline_total_requests: u64,
248 pub candidate_total_requests: u64,
250 pub baseline_blocked_requests: u64,
252 pub candidate_blocked_requests: u64,
254 pub blocked_ratio_delta: f64,
256 pub likely_regression: bool,
258 pub provider_delta: BTreeMap<AntiBotProvider, i64>,
260 pub new_markers: Vec<String>,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
266pub enum RequirementLevel {
267 Low,
269 Medium,
271 High,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct AntiBotRequirement {
278 pub id: String,
280 pub title: String,
282 pub why: String,
284 pub evidence: Vec<String>,
286 pub level: RequirementLevel,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292pub enum AdapterStrategy {
293 DirectHttp,
295 BrowserStealth,
297 StickyProxy,
299 SessionWarmup,
301 InvestigateOnly,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307pub struct IntegrationRecommendation {
308 pub strategy: AdapterStrategy,
310 pub rationale: String,
312 pub required_stygian_features: Vec<String>,
314 pub config_hints: BTreeMap<String, String>,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct RequirementsProfile {
321 pub provider: AntiBotProvider,
323 pub confidence: f64,
325 pub requirements: Vec<AntiBotRequirement>,
327 pub recommendation: IntegrationRecommendation,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(rename_all = "snake_case")]
334pub enum ExecutionMode {
335 Http,
337 Browser,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
343#[serde(rename_all = "snake_case")]
344pub enum SessionMode {
345 Stateless,
347 Sticky,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum TelemetryLevel {
355 Basic,
357 Standard,
359 Deep,
361}
362
363#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
365pub struct RuntimePolicy {
366 pub execution_mode: ExecutionMode,
368 pub session_mode: SessionMode,
370 pub telemetry_level: TelemetryLevel,
372 pub rate_limit_rps: f64,
374 pub max_retries: u32,
376 pub backoff_base_ms: u64,
378 pub enable_warmup: bool,
380 pub enforce_webrtc_proxy_only: bool,
382 pub sticky_session_ttl_secs: Option<u64>,
384 pub required_stygian_features: Vec<String>,
386 pub config_hints: BTreeMap<String, String>,
388 pub risk_score: f64,
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub struct InvestigationBundle {
395 pub report: InvestigationReport,
397 pub requirements: RequirementsProfile,
399 pub policy: RuntimePolicy,
401}
402
403#[cfg(test)]
404#[allow(
405 clippy::unwrap_used,
406 clippy::expect_used,
407 clippy::panic,
408 clippy::indexing_slicing
409)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn test_blocked_ratio_slo_api_thresholds() {
415 let slo = BlockedRatioSlo::api();
416 assert_eq!(slo.target_class, TargetClass::Api);
417 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
418 assert!((slo.warning - 0.10).abs() < f64::EPSILON);
419 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
420 }
421
422 #[test]
423 fn test_blocked_ratio_slo_content_site_thresholds() {
424 let slo = BlockedRatioSlo::content_site();
425 assert_eq!(slo.target_class, TargetClass::ContentSite);
426 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
427 assert!((slo.warning - 0.25).abs() < f64::EPSILON);
428 assert!((slo.critical - 0.40).abs() < f64::EPSILON);
429 }
430
431 #[test]
432 fn test_blocked_ratio_slo_high_security_thresholds() {
433 let slo = BlockedRatioSlo::high_security();
434 assert_eq!(slo.target_class, TargetClass::HighSecurity);
435 assert!((slo.acceptable - 0.30).abs() < f64::EPSILON);
436 assert!((slo.warning - 0.50).abs() < f64::EPSILON);
437 assert!((slo.critical - 0.70).abs() < f64::EPSILON);
438 }
439
440 #[test]
441 fn test_blocked_ratio_slo_unknown_defaults_to_api() {
442 let slo = BlockedRatioSlo::unknown();
443 assert_eq!(slo.target_class, TargetClass::Unknown);
444 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON); assert!((slo.warning - 0.10).abs() < f64::EPSILON);
446 assert!((slo.critical - 0.15).abs() < f64::EPSILON);
447 }
448
449 #[test]
450 fn test_blocked_ratio_slo_for_class_api() {
451 let slo = BlockedRatioSlo::for_class(TargetClass::Api);
452 assert_eq!(slo.target_class, TargetClass::Api);
453 assert!((slo.acceptable - 0.05).abs() < f64::EPSILON);
454 }
455
456 #[test]
457 fn test_blocked_ratio_slo_for_class_content_site() {
458 let slo = BlockedRatioSlo::for_class(TargetClass::ContentSite);
459 assert_eq!(slo.target_class, TargetClass::ContentSite);
460 assert!((slo.acceptable - 0.15).abs() < f64::EPSILON);
461 }
462
463 #[test]
464 fn test_blocked_ratio_slo_assess_below_acceptable() {
465 let slo = BlockedRatioSlo::api();
466 let (acceptable, warning, critical) = slo.assess(0.02);
467 assert!(acceptable);
468 assert!(!warning);
469 assert!(!critical);
470 }
471
472 #[test]
473 fn test_blocked_ratio_slo_assess_at_acceptable() {
474 let slo = BlockedRatioSlo::api();
475 let (acceptable, warning, critical) = slo.assess(0.05);
476 assert!(acceptable);
477 assert!(!warning);
478 assert!(!critical);
479 }
480
481 #[test]
482 fn test_blocked_ratio_slo_assess_in_warning_zone() {
483 let slo = BlockedRatioSlo::api();
484 let (acceptable, warning, critical) = slo.assess(0.075);
485 assert!(!acceptable);
486 assert!(warning);
487 assert!(!critical);
488 }
489
490 #[test]
491 fn test_blocked_ratio_slo_assess_at_warning() {
492 let slo = BlockedRatioSlo::api();
493 let (acceptable, warning, critical) = slo.assess(0.10);
494 assert!(!acceptable);
497 assert!(warning); assert!(!critical);
499 }
500
501 #[test]
502 fn test_blocked_ratio_slo_assess_between_warning_and_critical() {
503 let slo = BlockedRatioSlo::api();
504 let (acceptable, warning, critical) = slo.assess(0.125);
505 assert!(!acceptable);
506 assert!(!warning);
507 assert!(!critical);
508 }
509
510 #[test]
511 fn test_blocked_ratio_slo_assess_above_critical() {
512 let slo = BlockedRatioSlo::api();
513 let (acceptable, warning, critical) = slo.assess(0.20);
514 assert!(!acceptable);
515 assert!(!warning);
516 assert!(critical);
517 }
518
519 #[test]
520 fn test_blocked_ratio_slo_content_site_assessment() {
521 let slo = BlockedRatioSlo::content_site();
522
523 let (acc, warn, crit) = slo.assess(0.10);
525 assert!(acc && !warn && !crit);
526
527 let (acc, warn, crit) = slo.assess(0.20);
529 assert!(!acc && warn && !crit);
530
531 let (acc, warn, crit) = slo.assess(0.45);
533 assert!(!acc && !warn && crit);
534
535 let (acc, warn, crit) = slo.assess(0.40);
537 assert!(!acc && !warn && !crit); }
539
540 #[test]
541 fn test_target_class_derives() {
542 let api1 = TargetClass::Api;
544 let api2 = TargetClass::Api;
545 let content = TargetClass::ContentSite;
546
547 assert_eq!(api1, api2);
548 assert_ne!(api1, content);
549 }
550
551 #[test]
552 fn test_blocked_ratio_slo_serialization() {
553 let slo = BlockedRatioSlo::content_site();
554 let json = serde_json::to_string(&slo).unwrap_or_default();
555 if let Ok(deserialized) = serde_json::from_str::<BlockedRatioSlo>(&json) {
556 assert_eq!(slo, deserialized);
557 }
558 }
559
560 #[test]
561 fn test_target_class_serialization() {
562 let target = TargetClass::HighSecurity;
563 let json = serde_json::to_string(&target).unwrap_or_default();
564 if let Ok(deserialized) = serde_json::from_str::<TargetClass>(&json) {
565 assert_eq!(target, deserialized);
566 }
567 }
568}