Skip to main content

termivar_core/
events.rs

1//! Historical scanner lifecycle event records.
2//!
3//! This module is available only with the non-default `legacy-contracts`
4//! feature. The default reasoning runtime does not publish these events.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Event types in the scanning lifecycle.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
11pub enum EventType {
12    #[serde(rename = "scan_started")]
13    ScanStarted,
14    #[serde(rename = "scan_completed")]
15    ScanCompleted,
16    #[serde(rename = "scan_failed")]
17    ScanFailed,
18    #[serde(rename = "finding_found")]
19    FindingFound,
20    #[serde(rename = "finding_dismissed")]
21    FindingDismissed,
22    #[serde(rename = "proxy_request")]
23    ProxyRequest,
24    #[serde(rename = "proxy_response")]
25    ProxyResponse,
26    #[serde(rename = "worker_started")]
27    WorkerStarted,
28    #[serde(rename = "worker_finished")]
29    WorkerFinished,
30    #[serde(rename = "worker_failed")]
31    WorkerFailed,
32    #[serde(rename = "plugin_loaded")]
33    PluginLoaded,
34    #[serde(rename = "plugin_executed")]
35    PluginExecuted,
36    #[serde(rename = "phase_started")]
37    PhaseStarted,
38    #[serde(rename = "phase_completed")]
39    PhaseCompleted,
40    #[serde(rename = "phase_failed")]
41    PhaseFailed,
42    #[serde(rename = "alert_triggered")]
43    AlertTriggered,
44    #[serde(rename = "config_reloaded")]
45    ConfigReloaded,
46}
47
48impl EventType {
49    /// Returns the serialized name for the event type.
50    pub fn as_str(&self) -> &str {
51        match self {
52            EventType::ScanStarted => "scan_started",
53            EventType::ScanCompleted => "scan_completed",
54            EventType::ScanFailed => "scan_failed",
55            EventType::FindingFound => "finding_found",
56            EventType::FindingDismissed => "finding_dismissed",
57            EventType::ProxyRequest => "proxy_request",
58            EventType::ProxyResponse => "proxy_response",
59            EventType::WorkerStarted => "worker_started",
60            EventType::WorkerFinished => "worker_finished",
61            EventType::WorkerFailed => "worker_failed",
62            EventType::PluginLoaded => "plugin_loaded",
63            EventType::PluginExecuted => "plugin_executed",
64            EventType::PhaseStarted => "phase_started",
65            EventType::PhaseCompleted => "phase_completed",
66            EventType::PhaseFailed => "phase_failed",
67            EventType::AlertTriggered => "alert_triggered",
68            EventType::ConfigReloaded => "config_reloaded",
69        }
70    }
71}
72
73/// Event severity levels.
74#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
75pub enum EventSeverity {
76    #[serde(rename = "debug")]
77    Debug = 0,
78    #[serde(rename = "info")]
79    Info = 1,
80    #[serde(rename = "warning")]
81    Warning = 2,
82    #[serde(rename = "error")]
83    Error = 3,
84    #[serde(rename = "critical")]
85    Critical = 4,
86}
87
88/// Versioned event data used by the opt-in historical scanner event bus.
89///
90/// # Examples
91///
92/// ```
93/// use termivar_core::{Event, EventSeverity, EventType};
94///
95/// let event = Event::builder(EventType::PhaseStarted, "scanner")
96///     .correlation_id("scan-42")
97///     .severity(EventSeverity::Info)
98///     .data("phase", "recon")
99///     .build();
100///
101/// assert_eq!(event.event_type, EventType::PhaseStarted);
102/// assert_eq!(event.correlation_id, "scan-42");
103/// ```
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Event {
106    pub event_type: EventType,
107    pub version: u16,
108    pub timestamp_ms: u64,
109    pub correlation_id: String,
110    pub source: String,
111    pub data: HashMap<String, String>,
112    pub severity: EventSeverity,
113    pub event_id: String,
114}
115
116impl Event {
117    /// Creates an event with a generated timestamp, correlation ID, and event ID.
118    pub fn new(event_type: EventType, source: impl Into<String>) -> Self {
119        EventBuilder::new(event_type, source).build()
120    }
121
122    /// Creates a fluent event builder.
123    pub fn builder(event_type: EventType, source: impl Into<String>) -> EventBuilder {
124        EventBuilder::new(event_type, source)
125    }
126
127    /// Replaces the correlation ID.
128    pub fn with_correlation_id(mut self, scan_id: impl Into<String>) -> Self {
129        self.correlation_id = scan_id.into();
130        self
131    }
132
133    /// Adds a custom data field.
134    pub fn with_data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
135        self.data.insert(key.into(), value.into());
136        self
137    }
138
139    /// Replaces the event severity.
140    pub fn with_severity(mut self, severity: EventSeverity) -> Self {
141        self.severity = severity;
142        self
143    }
144
145    /// Replaces the event schema version.
146    pub fn with_version(mut self, version: u16) -> Self {
147        self.version = version;
148        self
149    }
150
151    /// Returns the timestamp in `seconds.milliseconds` form.
152    pub fn timestamp_str(&self) -> String {
153        let secs = self.timestamp_ms / 1000;
154        let millis = self.timestamp_ms % 1000;
155        format!("{secs}.{millis:03}")
156    }
157}
158
159/// Fluent builder for [`Event`].
160pub struct EventBuilder {
161    event_type: EventType,
162    version: u16,
163    timestamp_ms: u64,
164    correlation_id: String,
165    source: String,
166    data: HashMap<String, String>,
167    severity: EventSeverity,
168    event_id: String,
169}
170
171impl EventBuilder {
172    /// Creates a builder with generated identifiers and an info severity.
173    pub fn new(event_type: EventType, source: impl Into<String>) -> Self {
174        Self {
175            event_type,
176            version: 1,
177            timestamp_ms: now_ms(),
178            correlation_id: uuid::Uuid::new_v4().to_string(),
179            source: source.into(),
180            data: HashMap::new(),
181            severity: EventSeverity::Info,
182            event_id: uuid::Uuid::new_v4().to_string(),
183        }
184    }
185
186    pub fn correlation_id(mut self, scan_id: impl Into<String>) -> Self {
187        self.correlation_id = scan_id.into();
188        self
189    }
190
191    pub fn version(mut self, version: u16) -> Self {
192        self.version = version;
193        self
194    }
195
196    pub fn data(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
197        self.data.insert(key.into(), value.into());
198        self
199    }
200
201    pub fn severity(mut self, severity: EventSeverity) -> Self {
202        self.severity = severity;
203        self
204    }
205
206    pub fn timestamp_ms(mut self, timestamp_ms: u64) -> Self {
207        self.timestamp_ms = timestamp_ms;
208        self
209    }
210
211    pub fn build(self) -> Event {
212        Event {
213            event_type: self.event_type,
214            version: self.version,
215            timestamp_ms: self.timestamp_ms,
216            correlation_id: self.correlation_id,
217            source: self.source,
218            data: self.data,
219            severity: self.severity,
220            event_id: self.event_id,
221        }
222    }
223}
224
225fn now_ms() -> u64 {
226    std::time::SystemTime::now()
227        .duration_since(std::time::UNIX_EPOCH)
228        .unwrap_or_default()
229        .as_millis() as u64
230}
231
232#[cfg(test)]
233mod tests {
234    use super::{Event, EventType};
235
236    #[test]
237    fn timestamp_string_preserves_three_millisecond_digits() {
238        let event = Event::builder(EventType::ScanStarted, "test")
239            .timestamp_ms(1_007)
240            .build();
241
242        assert_eq!(event.timestamp_str(), "1.007");
243    }
244}