use crate::event::{EventOutcome, SecurityEvent};
use crate::kind::EventKind;
use security_core::severity::SecuritySeverity;
use std::collections::HashMap;
use std::sync::Mutex;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DetectionPoint {
InputOutOfRange,
AuthzDenied,
RepeatedDeserializerFailure,
CrossTenantProbe,
BruteForceAttempt,
}
pub struct DetectionEngine {
threshold: u32,
window_seconds: u64,
counters: Mutex<HashMap<String, (u32, std::time::Instant)>>,
}
impl DetectionEngine {
#[must_use]
pub fn new(threshold: u32, window_seconds: u64) -> Self {
Self {
threshold,
window_seconds,
counters: Mutex::new(HashMap::new()),
}
}
#[must_use]
pub fn record_authz_denied(&self, actor: &str) -> Option<SecurityEvent> {
let mut map = self.counters.lock().expect("mutex poisoned");
let now = std::time::Instant::now();
let entry = map.entry(actor.to_string()).or_insert((0, now));
if entry.1.elapsed().as_secs() > self.window_seconds {
*entry = (0, now);
}
entry.0 += 1;
if entry.0 > self.threshold {
let mut event = SecurityEvent::new(
EventKind::AuthzDeny,
SecuritySeverity::Critical,
EventOutcome::Blocked,
);
event.actor = Some(actor.to_string());
event.reason_code = Some("brute_force_detected");
Some(event)
} else {
None
}
}
#[must_use]
pub fn record_cross_tenant_probe(
&self,
actor: &str,
actor_tenant: &str,
resource_tenant: &str,
) -> SecurityEvent {
let mut event = SecurityEvent::new(
EventKind::CrossTenantAttempt,
SecuritySeverity::Critical,
EventOutcome::Blocked,
);
event.actor = Some(actor.to_string());
event.reason_code = Some("cross_tenant_probe");
event.labels.insert(
"actor_tenant".to_string(),
crate::event::EventValue::Classified {
value: actor_tenant.to_string(),
classification: security_core::classification::DataClassification::Internal,
},
);
event.labels.insert(
"resource_tenant".to_string(),
crate::event::EventValue::Classified {
value: resource_tenant.to_string(),
classification: security_core::classification::DataClassification::Internal,
},
);
event
}
}