use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalCategory {
AuthToken,
Device,
Network,
Behavioral,
}
impl std::fmt::Display for SignalCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignalCategory::AuthToken => write!(f, "auth_token"),
SignalCategory::Device => write!(f, "device"),
SignalCategory::Network => write!(f, "network"),
SignalCategory::Behavioral => write!(f, "behavioral"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalType {
Jwt,
ApiKey,
SessionCookie,
Bearer,
Basic,
CustomAuth,
HttpFingerprint,
HeaderOrder,
ClientHints,
AcceptPattern,
Ip,
TlsFingerprint,
Asn,
Geo,
Ja4,
Ja4h,
Timing,
Navigation,
RequestPattern,
DlpMatch,
}
impl SignalType {
pub fn category(&self) -> SignalCategory {
match self {
SignalType::Jwt
| SignalType::ApiKey
| SignalType::SessionCookie
| SignalType::Bearer
| SignalType::Basic
| SignalType::CustomAuth => SignalCategory::AuthToken,
SignalType::HttpFingerprint
| SignalType::HeaderOrder
| SignalType::ClientHints
| SignalType::AcceptPattern => SignalCategory::Device,
SignalType::Ip
| SignalType::TlsFingerprint
| SignalType::Asn
| SignalType::Geo
| SignalType::Ja4
| SignalType::Ja4h => SignalCategory::Network,
SignalType::Timing
| SignalType::Navigation
| SignalType::RequestPattern
| SignalType::DlpMatch => SignalCategory::Behavioral,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub id: String,
pub timestamp: i64,
pub category: SignalCategory,
pub signal_type: SignalType,
pub value: String,
pub entity_id: String,
pub session_id: Option<String>,
pub metadata: SignalMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SignalMetadata {
AuthToken(AuthTokenMetadata),
Device(DeviceMetadata),
Network(NetworkMetadata),
Behavioral(BehavioralMetadata),
}
impl Default for SignalMetadata {
fn default() -> Self {
SignalMetadata::Behavioral(BehavioralMetadata::default())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuthTokenMetadata {
pub header_name: String,
pub token_prefix: Option<String>,
pub token_hash: String,
pub jwt_claims: Option<JwtClaims>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: Option<String>,
pub iss: Option<String>,
pub exp: Option<i64>,
pub iat: Option<i64>,
pub aud: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeviceMetadata {
pub user_agent: String,
pub accept_language: Option<String>,
pub header_count: usize,
pub client_hints: Option<ClientHints>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClientHints {
pub brands: Vec<String>,
pub mobile: Option<bool>,
pub platform: Option<String>,
pub platform_version: Option<String>,
pub architecture: Option<String>,
pub model: Option<String>,
pub bitness: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NetworkMetadata {
pub ip: String,
pub tls_version: Option<String>,
pub tls_cipher: Option<String>,
pub alpn_protocol: Option<String>,
pub tls_fingerprint: Option<String>,
pub ja4: Option<String>,
pub ja4h: Option<String>,
pub ja4_combined: Option<String>,
pub ja4_tls_version: Option<u8>,
pub ja4_http_version: Option<u8>,
pub ja4_protocol: Option<String>,
pub ja4_bot_match: Option<String>,
pub ja4_bot_category: Option<String>,
pub ja4_bot_risk: Option<u8>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BehavioralMetadata {
pub time_since_last_request: Option<i64>,
pub requests_per_minute: Option<f64>,
pub path_pattern: Option<String>,
pub method_sequence: Vec<String>,
pub referer_pattern: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalBucketData {
pub timestamp: i64,
pub end_timestamp: i64,
pub signals: Vec<Signal>,
pub summary: BucketSummary,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BucketSummary {
pub total_count: usize,
pub by_category: HashMap<SignalCategory, CategorySummary>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CategorySummary {
pub count: usize,
pub unique_values: HashSet<String>,
pub unique_entities: HashSet<String>,
pub by_type: HashMap<SignalType, usize>,
}
#[derive(Debug, Clone, Default)]
pub struct TrendQueryOptions {
pub category: Option<SignalCategory>,
pub signal_type: Option<SignalType>,
pub from: Option<i64>,
pub to: Option<i64>,
pub resolution: Option<TrendResolution>,
pub entity_id: Option<String>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrendResolution {
Minute,
Hour,
Day,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalTrend {
pub signal_type: SignalType,
pub category: SignalCategory,
pub count: usize,
pub unique_values: usize,
pub unique_entities: usize,
pub first_seen: i64,
pub last_seen: i64,
pub histogram: Vec<TrendHistogramBucket>,
pub change_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrendHistogramBucket {
pub timestamp: i64,
pub count: usize,
pub unique_values: usize,
pub unique_entities: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrendsSummary {
pub time_range: TimeRange,
pub total_signals: usize,
pub by_category: HashMap<SignalCategory, CategoryTrendSummary>,
pub top_signal_types: Vec<TopSignalType>,
pub anomaly_count: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TimeRange {
pub from: i64,
pub to: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CategoryTrendSummary {
pub count: usize,
pub unique_values: usize,
pub unique_entities: usize,
pub change_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopSignalType {
pub signal_type: SignalType,
pub category: SignalCategory,
pub count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnomalyType {
FingerprintChange,
SessionSharing,
VelocitySpike,
ImpossibleTravel,
TokenReuse,
RotationPattern,
TimingAnomaly,
Ja4RotationPattern,
Ja4IpCluster,
Ja4BrowserSpoofing,
Ja4hChange,
OversizedRequest,
OversizedResponse,
BandwidthSpike,
ExfiltrationPattern,
UploadPattern,
}
impl std::fmt::Display for AnomalyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AnomalyType::FingerprintChange => write!(f, "fingerprint_change"),
AnomalyType::SessionSharing => write!(f, "session_sharing"),
AnomalyType::VelocitySpike => write!(f, "velocity_spike"),
AnomalyType::ImpossibleTravel => write!(f, "impossible_travel"),
AnomalyType::TokenReuse => write!(f, "token_reuse"),
AnomalyType::RotationPattern => write!(f, "rotation_pattern"),
AnomalyType::TimingAnomaly => write!(f, "timing_anomaly"),
AnomalyType::Ja4RotationPattern => write!(f, "ja4_rotation_pattern"),
AnomalyType::Ja4IpCluster => write!(f, "ja4_ip_cluster"),
AnomalyType::Ja4BrowserSpoofing => write!(f, "ja4_browser_spoofing"),
AnomalyType::Ja4hChange => write!(f, "ja4h_change"),
AnomalyType::OversizedRequest => write!(f, "oversized_request"),
AnomalyType::OversizedResponse => write!(f, "oversized_response"),
AnomalyType::BandwidthSpike => write!(f, "bandwidth_spike"),
AnomalyType::ExfiltrationPattern => write!(f, "exfiltration_pattern"),
AnomalyType::UploadPattern => write!(f, "upload_pattern"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AnomalySeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Anomaly {
pub id: String,
pub detected_at: i64,
pub category: SignalCategory,
pub anomaly_type: AnomalyType,
pub severity: AnomalySeverity,
pub description: String,
pub signals: Vec<Signal>,
pub entities: Vec<String>,
pub metadata: AnomalyMetadata,
pub risk_applied: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnomalyMetadata {
pub previous_value: Option<String>,
pub new_value: Option<String>,
pub ip_count: Option<usize>,
pub change_count: Option<usize>,
pub time_delta: Option<i64>,
pub threshold: Option<f64>,
pub actual: Option<f64>,
pub template: Option<String>,
pub source: Option<String>,
pub unique_ip_count: Option<usize>,
pub ips: Option<Vec<String>>,
pub time_delta_ms: Option<i64>,
pub time_delta_minutes: Option<f64>,
pub token_hash_prefix: Option<String>,
pub detection_method: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct AnomalyQueryOptions {
pub severity: Option<AnomalySeverity>,
pub anomaly_type: Option<AnomalyType>,
pub category: Option<SignalCategory>,
pub from: Option<i64>,
pub to: Option<i64>,
pub entity_id: Option<String>,
pub limit: Option<usize>,
pub include_resolved: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_type_category() {
assert_eq!(SignalType::Jwt.category(), SignalCategory::AuthToken);
assert_eq!(SignalType::Ja4.category(), SignalCategory::Network);
assert_eq!(SignalType::Timing.category(), SignalCategory::Behavioral);
assert_eq!(SignalType::HeaderOrder.category(), SignalCategory::Device);
}
#[test]
fn test_anomaly_type_display() {
assert_eq!(
AnomalyType::FingerprintChange.to_string(),
"fingerprint_change"
);
assert_eq!(
AnomalyType::Ja4RotationPattern.to_string(),
"ja4_rotation_pattern"
);
}
#[test]
fn test_severity_ordering() {
assert!(AnomalySeverity::Low < AnomalySeverity::Medium);
assert!(AnomalySeverity::Medium < AnomalySeverity::High);
assert!(AnomalySeverity::High < AnomalySeverity::Critical);
}
}