events.rs
141 lignes · 5116 octets
//! Stable on-the-wire format for guard events plus the [`GuardEventSink`] //! trait that backends (tracing, file, fan-out) implement. use async_trait::async_trait; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Discriminator describing what an observer reports. /// /// Each variant corresponds to one of the five detectors. Variants are /// serialised in `snake_case` to remain stable in JSON logs consumed /// by external SIEMs. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum GuardEventKind { /// A request's HTTP signature failed cryptographic verification. SignatureFailure, /// Replay window violated: stale `Date` header or duplicate nonce. ReplayAttempt, /// Outbound fetcher attempted to reach a private/loopback/link-local /// IP (SSRF guard hit). FetchSsrf, /// Inbox payload failed size, schema, or type validation. InboxAbuse, /// AP-native auth challenge initiated but never consumed; high /// volume from one domain signals scripted abuse. AuthSpam, /// A domain or actor was banned (auto or manual). Reported by the /// `BanManager` itself, not by detectors. Banned, /// A domain or actor ban was lifted (TTL expiry or admin action). Unbanned, } /// Severity hint attached to each event. Mirrors the levels accepted /// by `fediverse_guard_events.severity`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Severity { /// Informational (e.g. ban lifted by TTL). Info, /// Low — anomaly worth recording but not actionable alone. Low, /// Medium — repeated occurrences should trigger a review. Medium, /// High — likely abuse; detector seuils probably reached soon. High, /// Critical — confirmed abuse; auto-ban just triggered. Critical, } /// One observable guard event. /// /// Events are produced by the federation crate via the /// `FediverseEventObserver` trait and consumed by the guard's detector /// chain and sinks. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GuardEvent { /// Surrogate UUID for the event row. pub id: Uuid, /// What kind of anomaly is being reported. pub kind: GuardEventKind, /// Severity hint. pub severity: Severity, /// Domain the event is attributed to (lowercased, no scheme). pub domain: String, /// Optional ActivityPub actor URI (e.g. /// `https://a.tld/users/alice`) when the event narrows to one actor. pub actor_uri: Option<String>, /// FK to `remote_identities.id` when the actor has been resolved. pub remote_identity_id: Option<Uuid>, /// Optional HTTP status returned by the offending request, when /// applicable. pub http_status: Option<i32>, /// Detector-specific metadata (e.g. `{"skew_secs": 3601}`). /// Always a JSON object; never `null`. pub details: serde_json::Value, /// Server-side timestamp. pub created_at: DateTime<Utc>, } impl GuardEvent { /// Convenience constructor with sensible defaults for fields /// rarely populated by detectors. #[must_use] pub fn new(kind: GuardEventKind, domain: impl Into<String>, severity: Severity) -> Self { Self { id: Uuid::new_v4(), kind, severity, domain: domain.into(), actor_uri: None, remote_identity_id: None, http_status: None, details: serde_json::Value::Object(serde_json::Map::new()), created_at: Utc::now(), } } } /// Pluggable sink for [`GuardEvent`]. /// /// Implementations must be non-blocking: a slow sink should buffer /// internally rather than back-pressure the federation hot path. #[async_trait] pub trait GuardEventSink: Send + Sync { /// Emit one event. Errors are swallowed by the caller — sinks /// must not propagate failures into the request path. async fn emit(&self, event: &GuardEvent); } #[cfg(test)] mod tests { use super::*; #[test] fn guard_event_new_fills_defaults() { let evt = GuardEvent::new(GuardEventKind::SignatureFailure, "evil.tld", Severity::High); assert_eq!(evt.kind, GuardEventKind::SignatureFailure); assert_eq!(evt.domain, "evil.tld"); assert!(evt.actor_uri.is_none()); assert!(evt.http_status.is_none()); assert!(evt.details.is_object()); } #[test] fn guard_event_round_trips_through_json() { let evt = GuardEvent::new(GuardEventKind::ReplayAttempt, "a.tld", Severity::Medium); let json = serde_json::to_string(&evt).unwrap(); let back: GuardEvent = serde_json::from_str(&json).unwrap(); assert_eq!(back.kind, GuardEventKind::ReplayAttempt); assert_eq!(back.domain, "a.tld"); } #[test] fn guard_event_kind_serialises_snake_case() { let s = serde_json::to_string(&GuardEventKind::SignatureFailure).unwrap(); assert_eq!(s, "\"signature_failure\""); } }
GitRust