1use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum SignalCategory {
17 AuthToken,
19 Device,
21 Network,
23 Behavioral,
25}
26
27impl std::fmt::Display for SignalCategory {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 SignalCategory::AuthToken => write!(f, "auth_token"),
31 SignalCategory::Device => write!(f, "device"),
32 SignalCategory::Network => write!(f, "network"),
33 SignalCategory::Behavioral => write!(f, "behavioral"),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum SignalType {
42 Jwt,
44 ApiKey,
45 SessionCookie,
46 Bearer,
47 Basic,
48 CustomAuth,
49
50 HttpFingerprint,
52 HeaderOrder,
53 ClientHints,
54 AcceptPattern,
55
56 Ip,
58 TlsFingerprint,
59 Asn,
60 Geo,
61 Ja4,
62 Ja4h,
63
64 Timing,
66 Navigation,
67 RequestPattern,
68 DlpMatch,
69}
70
71impl SignalType {
72 pub fn category(&self) -> SignalCategory {
74 match self {
75 SignalType::Jwt
76 | SignalType::ApiKey
77 | SignalType::SessionCookie
78 | SignalType::Bearer
79 | SignalType::Basic
80 | SignalType::CustomAuth => SignalCategory::AuthToken,
81
82 SignalType::HttpFingerprint
83 | SignalType::HeaderOrder
84 | SignalType::ClientHints
85 | SignalType::AcceptPattern => SignalCategory::Device,
86
87 SignalType::Ip
88 | SignalType::TlsFingerprint
89 | SignalType::Asn
90 | SignalType::Geo
91 | SignalType::Ja4
92 | SignalType::Ja4h => SignalCategory::Network,
93
94 SignalType::Timing
95 | SignalType::Navigation
96 | SignalType::RequestPattern
97 | SignalType::DlpMatch => SignalCategory::Behavioral,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Signal {
109 pub id: String,
111 pub timestamp: i64,
113 pub category: SignalCategory,
115 pub signal_type: SignalType,
117 pub value: String,
119 pub entity_id: String,
121 pub session_id: Option<String>,
123 pub metadata: SignalMetadata,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(untagged)]
130pub enum SignalMetadata {
131 AuthToken(AuthTokenMetadata),
132 Device(DeviceMetadata),
133 Network(NetworkMetadata),
134 Behavioral(BehavioralMetadata),
135}
136
137impl Default for SignalMetadata {
138 fn default() -> Self {
139 SignalMetadata::Behavioral(BehavioralMetadata::default())
140 }
141}
142
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145pub struct AuthTokenMetadata {
146 pub header_name: String,
148 pub token_prefix: Option<String>,
150 pub token_hash: String,
152 pub jwt_claims: Option<JwtClaims>,
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158pub struct JwtClaims {
159 pub sub: Option<String>,
160 pub iss: Option<String>,
161 pub exp: Option<i64>,
162 pub iat: Option<i64>,
163 pub aud: Option<String>,
164}
165
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct DeviceMetadata {
169 pub user_agent: String,
170 pub accept_language: Option<String>,
171 pub header_count: usize,
172 pub client_hints: Option<ClientHints>,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct ClientHints {
178 pub brands: Vec<String>,
179 pub mobile: Option<bool>,
180 pub platform: Option<String>,
181 pub platform_version: Option<String>,
182 pub architecture: Option<String>,
183 pub model: Option<String>,
184 pub bitness: Option<String>,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct NetworkMetadata {
190 pub ip: String,
191 pub tls_version: Option<String>,
192 pub tls_cipher: Option<String>,
193 pub alpn_protocol: Option<String>,
194 pub tls_fingerprint: Option<String>,
195 pub ja4: Option<String>,
197 pub ja4h: Option<String>,
198 pub ja4_combined: Option<String>,
199 pub ja4_tls_version: Option<u8>,
200 pub ja4_http_version: Option<u8>,
201 pub ja4_protocol: Option<String>,
202 pub ja4_bot_match: Option<String>,
203 pub ja4_bot_category: Option<String>,
204 pub ja4_bot_risk: Option<u8>,
205}
206
207#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct BehavioralMetadata {
210 pub time_since_last_request: Option<i64>,
212 pub requests_per_minute: Option<f64>,
214 pub path_pattern: Option<String>,
216 pub method_sequence: Vec<String>,
218 pub referer_pattern: Option<String>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct SignalBucketData {
229 pub timestamp: i64,
231 pub end_timestamp: i64,
233 pub signals: Vec<Signal>,
235 pub summary: BucketSummary,
237}
238
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241pub struct BucketSummary {
242 pub total_count: usize,
243 pub by_category: HashMap<SignalCategory, CategorySummary>,
244}
245
246#[derive(Debug, Clone, Default, Serialize, Deserialize)]
248pub struct CategorySummary {
249 pub count: usize,
250 pub unique_values: HashSet<String>,
251 pub unique_entities: HashSet<String>,
252 pub by_type: HashMap<SignalType, usize>,
253}
254
255#[derive(Debug, Clone, Default)]
261pub struct TrendQueryOptions {
262 pub category: Option<SignalCategory>,
263 pub signal_type: Option<SignalType>,
264 pub from: Option<i64>,
265 pub to: Option<i64>,
266 pub resolution: Option<TrendResolution>,
267 pub entity_id: Option<String>,
268 pub limit: Option<usize>,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum TrendResolution {
274 Minute,
275 Hour,
276 Day,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct SignalTrend {
282 pub signal_type: SignalType,
283 pub category: SignalCategory,
284 pub count: usize,
285 pub unique_values: usize,
286 pub unique_entities: usize,
287 pub first_seen: i64,
288 pub last_seen: i64,
289 pub histogram: Vec<TrendHistogramBucket>,
290 pub change_rate: f64,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct TrendHistogramBucket {
297 pub timestamp: i64,
298 pub count: usize,
299 pub unique_values: usize,
300 pub unique_entities: usize,
301}
302
303#[derive(Debug, Clone, Default, Serialize, Deserialize)]
305pub struct TrendsSummary {
306 pub time_range: TimeRange,
307 pub total_signals: usize,
308 pub by_category: HashMap<SignalCategory, CategoryTrendSummary>,
309 pub top_signal_types: Vec<TopSignalType>,
310 pub anomaly_count: usize,
311}
312
313#[derive(Debug, Clone, Default, Serialize, Deserialize)]
315pub struct TimeRange {
316 pub from: i64,
317 pub to: i64,
318}
319
320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
322pub struct CategoryTrendSummary {
323 pub count: usize,
324 pub unique_values: usize,
325 pub unique_entities: usize,
326 pub change_rate: f64,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct TopSignalType {
332 pub signal_type: SignalType,
333 pub category: SignalCategory,
334 pub count: usize,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
343#[serde(rename_all = "snake_case")]
344pub enum AnomalyType {
345 FingerprintChange,
347 SessionSharing,
349 VelocitySpike,
351 ImpossibleTravel,
353 TokenReuse,
355 RotationPattern,
357 TimingAnomaly,
359 Ja4RotationPattern,
362 Ja4IpCluster,
364 Ja4BrowserSpoofing,
366 Ja4hChange,
368 OversizedRequest,
371 OversizedResponse,
373 BandwidthSpike,
375 ExfiltrationPattern,
377 UploadPattern,
379}
380
381impl std::fmt::Display for AnomalyType {
382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 match self {
384 AnomalyType::FingerprintChange => write!(f, "fingerprint_change"),
385 AnomalyType::SessionSharing => write!(f, "session_sharing"),
386 AnomalyType::VelocitySpike => write!(f, "velocity_spike"),
387 AnomalyType::ImpossibleTravel => write!(f, "impossible_travel"),
388 AnomalyType::TokenReuse => write!(f, "token_reuse"),
389 AnomalyType::RotationPattern => write!(f, "rotation_pattern"),
390 AnomalyType::TimingAnomaly => write!(f, "timing_anomaly"),
391 AnomalyType::Ja4RotationPattern => write!(f, "ja4_rotation_pattern"),
392 AnomalyType::Ja4IpCluster => write!(f, "ja4_ip_cluster"),
393 AnomalyType::Ja4BrowserSpoofing => write!(f, "ja4_browser_spoofing"),
394 AnomalyType::Ja4hChange => write!(f, "ja4h_change"),
395 AnomalyType::OversizedRequest => write!(f, "oversized_request"),
396 AnomalyType::OversizedResponse => write!(f, "oversized_response"),
397 AnomalyType::BandwidthSpike => write!(f, "bandwidth_spike"),
398 AnomalyType::ExfiltrationPattern => write!(f, "exfiltration_pattern"),
399 AnomalyType::UploadPattern => write!(f, "upload_pattern"),
400 }
401 }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
406#[serde(rename_all = "lowercase")]
407pub enum AnomalySeverity {
408 Low,
409 Medium,
410 High,
411 Critical,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct Anomaly {
417 pub id: String,
418 pub detected_at: i64,
419 pub category: SignalCategory,
420 pub anomaly_type: AnomalyType,
421 pub severity: AnomalySeverity,
422 pub description: String,
423 pub signals: Vec<Signal>,
424 pub entities: Vec<String>,
425 pub metadata: AnomalyMetadata,
426 pub risk_applied: Option<u32>,
428}
429
430#[derive(Debug, Clone, Default, Serialize, Deserialize)]
432pub struct AnomalyMetadata {
433 pub previous_value: Option<String>,
434 pub new_value: Option<String>,
435 pub ip_count: Option<usize>,
436 pub change_count: Option<usize>,
437 pub time_delta: Option<i64>,
438 pub threshold: Option<f64>,
439 pub actual: Option<f64>,
440 pub template: Option<String>,
442 pub source: Option<String>,
443 pub unique_ip_count: Option<usize>,
445 pub ips: Option<Vec<String>>,
446 pub time_delta_ms: Option<i64>,
447 pub time_delta_minutes: Option<f64>,
448 pub token_hash_prefix: Option<String>,
449 pub detection_method: Option<String>,
450}
451
452#[derive(Debug, Clone, Default)]
454pub struct AnomalyQueryOptions {
455 pub severity: Option<AnomalySeverity>,
456 pub anomaly_type: Option<AnomalyType>,
457 pub category: Option<SignalCategory>,
458 pub from: Option<i64>,
459 pub to: Option<i64>,
460 pub entity_id: Option<String>,
461 pub limit: Option<usize>,
462 pub include_resolved: bool,
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn test_signal_type_category() {
471 assert_eq!(SignalType::Jwt.category(), SignalCategory::AuthToken);
472 assert_eq!(SignalType::Ja4.category(), SignalCategory::Network);
473 assert_eq!(SignalType::Timing.category(), SignalCategory::Behavioral);
474 assert_eq!(SignalType::HeaderOrder.category(), SignalCategory::Device);
475 }
476
477 #[test]
478 fn test_anomaly_type_display() {
479 assert_eq!(
480 AnomalyType::FingerprintChange.to_string(),
481 "fingerprint_change"
482 );
483 assert_eq!(
484 AnomalyType::Ja4RotationPattern.to_string(),
485 "ja4_rotation_pattern"
486 );
487 }
488
489 #[test]
490 fn test_severity_ordering() {
491 assert!(AnomalySeverity::Low < AnomalySeverity::Medium);
492 assert!(AnomalySeverity::Medium < AnomalySeverity::High);
493 assert!(AnomalySeverity::High < AnomalySeverity::Critical);
494 }
495}