ironpost_core/
event.rs

1//! 이벤트 시스템 — 모듈 간 통신의 기본 단위
2//!
3//! 모든 모듈 간 통신은 이벤트 기반 메시지 패싱으로 수행됩니다.
4//! [`EventMetadata`]는 모든 이벤트에 공통으로 포함되는 메타데이터이며,
5//! [`Event`] trait은 모든 이벤트 타입이 구현해야 하는 인터페이스입니다.
6
7use std::fmt;
8use std::time::SystemTime;
9
10use bytes::Bytes;
11use serde::{Deserialize, Serialize};
12
13use crate::types::{Alert, LogEntry, PacketInfo, Severity};
14
15// --- 모듈명 상수 ---
16
17/// eBPF 엔진 모듈명
18pub const MODULE_EBPF: &str = "ebpf-engine";
19/// 로그 파이프라인 모듈명
20pub const MODULE_LOG_PIPELINE: &str = "log-pipeline";
21/// 컨테이너 가드 모듈명
22pub const MODULE_CONTAINER_GUARD: &str = "container-guard";
23/// SBOM 스캐너 모듈명
24pub const MODULE_SBOM_SCANNER: &str = "sbom-scanner";
25
26// --- 이벤트 타입 상수 ---
27
28/// 패킷 이벤트 타입
29pub const EVENT_TYPE_PACKET: &str = "packet";
30/// 로그 이벤트 타입
31pub const EVENT_TYPE_LOG: &str = "log";
32/// 알림 이벤트 타입
33pub const EVENT_TYPE_ALERT: &str = "alert";
34/// 액션 이벤트 타입
35pub const EVENT_TYPE_ACTION: &str = "action";
36/// 스캔 이벤트 타입
37pub const EVENT_TYPE_SCAN: &str = "scan";
38
39/// 이벤트 메타데이터 — 모든 이벤트에 공통으로 포함되는 추적 정보
40///
41/// 각 이벤트의 발생 시각, 생성 모듈, 분산 추적 ID를 담고 있어
42/// 이벤트 흐름을 추적하고 디버깅할 수 있습니다.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EventMetadata {
45    /// 이벤트 발생 시각
46    pub timestamp: SystemTime,
47    /// 이벤트를 생성한 모듈명 (예: "ebpf-engine", "log-pipeline")
48    pub source_module: String,
49    /// 분산 추적 ID — 같은 흐름의 이벤트를 연결합니다
50    pub trace_id: String,
51}
52
53impl EventMetadata {
54    /// 기존 trace_id를 사용하여 새 메타데이터를 생성합니다.
55    ///
56    /// 이벤트 체인에서 동일한 추적 ID를 유지할 때 사용합니다.
57    pub fn new(source_module: impl Into<String>, trace_id: impl Into<String>) -> Self {
58        Self {
59            timestamp: SystemTime::now(),
60            source_module: source_module.into(),
61            trace_id: trace_id.into(),
62        }
63    }
64
65    /// 새로운 UUID v4 trace_id를 생성하여 메타데이터를 만듭니다.
66    ///
67    /// 새로운 이벤트 체인의 시작점에서 사용합니다.
68    pub fn with_new_trace(source_module: impl Into<String>) -> Self {
69        Self {
70            timestamp: SystemTime::now(),
71            source_module: source_module.into(),
72            trace_id: uuid::Uuid::new_v4().to_string(),
73        }
74    }
75}
76
77impl fmt::Display for EventMetadata {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(
80            f,
81            "[{}] source={} trace={}",
82            unix_timestamp_str(self.timestamp),
83            self.source_module,
84            self.trace_id,
85        )
86    }
87}
88
89/// 모든 이벤트가 구현해야 하는 기본 trait
90///
91/// 각 모듈은 자체 이벤트 타입을 정의하고 이 trait을 구현합니다.
92/// `Send + Sync + 'static` 바운드로 `tokio::mpsc` 채널을 통한
93/// 안전한 전송을 보장합니다.
94pub trait Event: Send + Sync + 'static {
95    /// 이벤트 고유 ID (UUID v4)
96    fn event_id(&self) -> &str;
97
98    /// 이벤트 메타데이터 (timestamp, source_module, trace_id)
99    fn metadata(&self) -> &EventMetadata;
100
101    /// 이벤트 타입명 (로깅 및 라우팅에 사용)
102    fn event_type(&self) -> &str;
103}
104
105/// eBPF에서 탐지한 패킷 이벤트
106///
107/// eBPF XDP 프로그램에서 캡처한 네트워크 패킷 정보를 담습니다.
108/// 원시 패킷 데이터는 `bytes::Bytes`로 제로카피 슬라이싱이 가능합니다.
109#[derive(Debug, Clone)]
110pub struct PacketEvent {
111    /// 이벤트 고유 ID
112    pub id: String,
113    /// 이벤트 메타데이터
114    pub metadata: EventMetadata,
115    /// 패킷 정보 (IP, 포트, 프로토콜 등)
116    pub packet_info: PacketInfo,
117    /// 원시 패킷 데이터
118    pub raw_data: Bytes,
119}
120
121impl PacketEvent {
122    /// 새로운 trace를 시작하는 패킷 이벤트를 생성합니다.
123    pub fn new(packet_info: PacketInfo, raw_data: Bytes) -> Self {
124        Self {
125            id: uuid::Uuid::new_v4().to_string(),
126            metadata: EventMetadata::with_new_trace(MODULE_EBPF),
127            packet_info,
128            raw_data,
129        }
130    }
131
132    /// 기존 trace에 연결된 패킷 이벤트를 생성합니다.
133    pub fn with_trace(
134        packet_info: PacketInfo,
135        raw_data: Bytes,
136        trace_id: impl Into<String>,
137    ) -> Self {
138        Self {
139            id: uuid::Uuid::new_v4().to_string(),
140            metadata: EventMetadata::new(MODULE_EBPF, trace_id),
141            packet_info,
142            raw_data,
143        }
144    }
145}
146
147impl Event for PacketEvent {
148    fn event_id(&self) -> &str {
149        &self.id
150    }
151
152    fn metadata(&self) -> &EventMetadata {
153        &self.metadata
154    }
155
156    fn event_type(&self) -> &str {
157        EVENT_TYPE_PACKET
158    }
159}
160
161impl fmt::Display for PacketEvent {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        write!(
164            f,
165            "PacketEvent[{}] {}:{} -> {}:{} proto={} size={}",
166            &self.id[..8.min(self.id.len())],
167            self.packet_info.src_ip,
168            self.packet_info.src_port,
169            self.packet_info.dst_ip,
170            self.packet_info.dst_port,
171            self.packet_info.protocol,
172            self.packet_info.size,
173        )
174    }
175}
176
177/// 파싱된 로그 이벤트
178///
179/// 로그 파이프라인에서 원시 로그를 파싱한 결과를 담습니다.
180#[derive(Debug, Clone)]
181pub struct LogEvent {
182    /// 이벤트 고유 ID
183    pub id: String,
184    /// 이벤트 메타데이터
185    pub metadata: EventMetadata,
186    /// 파싱된 로그 엔트리
187    pub entry: LogEntry,
188}
189
190impl LogEvent {
191    /// 새로운 trace를 시작하는 로그 이벤트를 생성합니다.
192    pub fn new(entry: LogEntry) -> Self {
193        Self {
194            id: uuid::Uuid::new_v4().to_string(),
195            metadata: EventMetadata::with_new_trace(MODULE_LOG_PIPELINE),
196            entry,
197        }
198    }
199
200    /// 기존 trace에 연결된 로그 이벤트를 생성합니다.
201    pub fn with_trace(entry: LogEntry, trace_id: impl Into<String>) -> Self {
202        Self {
203            id: uuid::Uuid::new_v4().to_string(),
204            metadata: EventMetadata::new(MODULE_LOG_PIPELINE, trace_id),
205            entry,
206        }
207    }
208}
209
210impl Event for LogEvent {
211    fn event_id(&self) -> &str {
212        &self.id
213    }
214
215    fn metadata(&self) -> &EventMetadata {
216        &self.metadata
217    }
218
219    fn event_type(&self) -> &str {
220        EVENT_TYPE_LOG
221    }
222}
223
224impl fmt::Display for LogEvent {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        write!(
227            f,
228            "LogEvent[{}] source={} host={} severity={}",
229            &self.id[..8.min(self.id.len())],
230            self.entry.source,
231            self.entry.hostname,
232            self.entry.severity,
233        )
234    }
235}
236
237/// 룰 매칭으로 생성된 알림 이벤트
238///
239/// 탐지 규칙에 매칭되어 보안 알림이 발생했을 때 생성됩니다.
240#[derive(Debug, Clone)]
241pub struct AlertEvent {
242    /// 이벤트 고유 ID
243    pub id: String,
244    /// 이벤트 메타데이터
245    pub metadata: EventMetadata,
246    /// 알림 상세 정보
247    pub alert: Alert,
248    /// 알림 심각도
249    pub severity: Severity,
250}
251
252impl AlertEvent {
253    /// 새로운 trace를 시작하는 알림 이벤트를 생성합니다.
254    ///
255    /// 기본 source module은 `MODULE_LOG_PIPELINE`입니다.
256    pub fn new(alert: Alert, severity: Severity) -> Self {
257        Self {
258            id: uuid::Uuid::new_v4().to_string(),
259            metadata: EventMetadata::with_new_trace(MODULE_LOG_PIPELINE),
260            alert,
261            severity,
262        }
263    }
264
265    /// 지정된 source module로 새로운 trace를 시작하는 알림 이벤트를 생성합니다.
266    ///
267    /// # 사용 예시
268    ///
269    /// ```ignore
270    /// use ironpost_core::event::AlertEvent;
271    /// use ironpost_core::MODULE_EBPF_ENGINE;
272    ///
273    /// let alert_event = AlertEvent::with_source(alert, severity, MODULE_EBPF_ENGINE);
274    /// ```
275    pub fn with_source(alert: Alert, severity: Severity, source_module: &'static str) -> Self {
276        Self {
277            id: uuid::Uuid::new_v4().to_string(),
278            metadata: EventMetadata::with_new_trace(source_module),
279            alert,
280            severity,
281        }
282    }
283
284    /// 기존 trace에 연결된 알림 이벤트를 생성합니다.
285    ///
286    /// 기본 source module은 `MODULE_LOG_PIPELINE`입니다.
287    pub fn with_trace(alert: Alert, severity: Severity, trace_id: impl Into<String>) -> Self {
288        Self {
289            id: uuid::Uuid::new_v4().to_string(),
290            metadata: EventMetadata::new(MODULE_LOG_PIPELINE, trace_id),
291            alert,
292            severity,
293        }
294    }
295}
296
297impl Event for AlertEvent {
298    fn event_id(&self) -> &str {
299        &self.id
300    }
301
302    fn metadata(&self) -> &EventMetadata {
303        &self.metadata
304    }
305
306    fn event_type(&self) -> &str {
307        EVENT_TYPE_ALERT
308    }
309}
310
311impl fmt::Display for AlertEvent {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        write!(
314            f,
315            "AlertEvent[{}] rule={} severity={} title={}",
316            &self.id[..8.min(self.id.len())],
317            self.alert.rule_name,
318            self.severity,
319            self.alert.title,
320        )
321    }
322}
323
324/// 실행된 액션 이벤트 (컨테이너 격리 등)
325///
326/// 알림에 대한 대응 조치가 실행되었을 때 생성됩니다.
327#[derive(Debug, Clone)]
328pub struct ActionEvent {
329    /// 이벤트 고유 ID
330    pub id: String,
331    /// 이벤트 메타데이터
332    pub metadata: EventMetadata,
333    /// 액션 타입 (예: "container_isolate", "block_ip")
334    pub action_type: String,
335    /// 대상 (예: 컨테이너 ID, IP 주소)
336    pub target: String,
337    /// 성공 여부
338    pub success: bool,
339}
340
341impl ActionEvent {
342    /// 새로운 trace를 시작하는 액션 이벤트를 생성합니다.
343    pub fn new(action_type: impl Into<String>, target: impl Into<String>, success: bool) -> Self {
344        Self {
345            id: uuid::Uuid::new_v4().to_string(),
346            metadata: EventMetadata::with_new_trace(MODULE_CONTAINER_GUARD),
347            action_type: action_type.into(),
348            target: target.into(),
349            success,
350        }
351    }
352
353    /// 기존 trace에 연결된 액션 이벤트를 생성합니다.
354    pub fn with_trace(
355        action_type: impl Into<String>,
356        target: impl Into<String>,
357        success: bool,
358        trace_id: impl Into<String>,
359    ) -> Self {
360        Self {
361            id: uuid::Uuid::new_v4().to_string(),
362            metadata: EventMetadata::new(MODULE_CONTAINER_GUARD, trace_id),
363            action_type: action_type.into(),
364            target: target.into(),
365            success,
366        }
367    }
368}
369
370impl Event for ActionEvent {
371    fn event_id(&self) -> &str {
372        &self.id
373    }
374
375    fn metadata(&self) -> &EventMetadata {
376        &self.metadata
377    }
378
379    fn event_type(&self) -> &str {
380        EVENT_TYPE_ACTION
381    }
382}
383
384impl fmt::Display for ActionEvent {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        let status = if self.success { "OK" } else { "FAILED" };
387        write!(
388            f,
389            "ActionEvent[{}] type={} target={} status={}",
390            &self.id[..8.min(self.id.len())],
391            self.action_type,
392            self.target,
393            status,
394        )
395    }
396}
397
398/// SystemTime을 사람이 읽을 수 있는 형태로 변환합니다.
399fn unix_timestamp_str(time: SystemTime) -> String {
400    match time.duration_since(SystemTime::UNIX_EPOCH) {
401        Ok(duration) => {
402            let secs = duration.as_secs();
403            format!("{secs}")
404        }
405        Err(_) => "unknown".to_owned(),
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use std::net::IpAddr;
413
414    fn sample_packet_info() -> PacketInfo {
415        PacketInfo {
416            src_ip: "192.168.1.1".parse::<IpAddr>().unwrap(),
417            dst_ip: "10.0.0.1".parse::<IpAddr>().unwrap(),
418            src_port: 12345,
419            dst_port: 80,
420            protocol: 6,
421            size: 1500,
422            timestamp: SystemTime::now(),
423        }
424    }
425
426    fn sample_log_entry() -> LogEntry {
427        LogEntry {
428            source: "/var/log/syslog".to_owned(),
429            timestamp: SystemTime::now(),
430            hostname: "server-01".to_owned(),
431            process: "sshd".to_owned(),
432            message: "Failed password for root".to_owned(),
433            severity: Severity::High,
434            fields: vec![("pid".to_owned(), "1234".to_owned())],
435        }
436    }
437
438    fn sample_alert() -> Alert {
439        Alert {
440            id: "alert-001".to_owned(),
441            title: "Brute force detected".to_owned(),
442            description: "Multiple failed SSH login attempts".to_owned(),
443            severity: Severity::High,
444            rule_name: "ssh_brute_force".to_owned(),
445            source_ip: Some("192.168.1.100".parse().unwrap()),
446            target_ip: Some("10.0.0.1".parse().unwrap()),
447            created_at: SystemTime::now(),
448        }
449    }
450
451    #[test]
452    fn event_metadata_new_preserves_trace_id() {
453        let meta = EventMetadata::new("test-module", "trace-abc-123");
454        assert_eq!(meta.source_module, "test-module");
455        assert_eq!(meta.trace_id, "trace-abc-123");
456        assert!(meta.timestamp <= SystemTime::now());
457    }
458
459    #[test]
460    fn event_metadata_with_new_trace_generates_uuid() {
461        let meta = EventMetadata::with_new_trace("test-module");
462        assert_eq!(meta.source_module, "test-module");
463        assert!(!meta.trace_id.is_empty());
464        // UUID v4 형식 확인: 8-4-4-4-12
465        assert_eq!(meta.trace_id.len(), 36);
466        assert_eq!(meta.trace_id.chars().filter(|c| *c == '-').count(), 4);
467    }
468
469    #[test]
470    fn event_metadata_display() {
471        let meta = EventMetadata::new("ebpf-engine", "trace-xyz");
472        let display = meta.to_string();
473        assert!(display.contains("ebpf-engine"));
474        assert!(display.contains("trace-xyz"));
475    }
476
477    #[test]
478    fn packet_event_implements_event_trait() {
479        let event = PacketEvent::new(sample_packet_info(), Bytes::from_static(b"raw-data"));
480        assert_eq!(event.event_type(), "packet");
481        assert!(!event.event_id().is_empty());
482        assert_eq!(event.metadata().source_module, "ebpf-engine");
483    }
484
485    #[test]
486    fn packet_event_with_trace_preserves_trace_id() {
487        let event = PacketEvent::with_trace(
488            sample_packet_info(),
489            Bytes::from_static(b"data"),
490            "my-trace-id",
491        );
492        assert_eq!(event.metadata().trace_id, "my-trace-id");
493    }
494
495    #[test]
496    fn packet_event_display() {
497        let event = PacketEvent::new(sample_packet_info(), Bytes::from_static(b"data"));
498        let display = event.to_string();
499        assert!(display.contains("192.168.1.1"));
500        assert!(display.contains("10.0.0.1"));
501        assert!(display.contains("PacketEvent"));
502    }
503
504    #[test]
505    fn log_event_implements_event_trait() {
506        let event = LogEvent::new(sample_log_entry());
507        assert_eq!(event.event_type(), "log");
508        assert!(!event.event_id().is_empty());
509        assert_eq!(event.metadata().source_module, "log-pipeline");
510    }
511
512    #[test]
513    fn log_event_with_trace() {
514        let event = LogEvent::with_trace(sample_log_entry(), "existing-trace");
515        assert_eq!(event.metadata().trace_id, "existing-trace");
516    }
517
518    #[test]
519    fn alert_event_implements_event_trait() {
520        let event = AlertEvent::new(sample_alert(), Severity::High);
521        assert_eq!(event.event_type(), "alert");
522        assert_eq!(event.severity, Severity::High);
523        assert!(!event.event_id().is_empty());
524    }
525
526    #[test]
527    fn alert_event_display() {
528        let event = AlertEvent::new(sample_alert(), Severity::High);
529        let display = event.to_string();
530        assert!(display.contains("ssh_brute_force"));
531        assert!(display.contains("High"));
532    }
533
534    #[test]
535    fn action_event_implements_event_trait() {
536        let event = ActionEvent::new("container_isolate", "container-abc", true);
537        assert_eq!(event.event_type(), "action");
538        assert_eq!(event.action_type, "container_isolate");
539        assert_eq!(event.target, "container-abc");
540        assert!(event.success);
541    }
542
543    #[test]
544    fn action_event_with_trace() {
545        let event = ActionEvent::with_trace("block_ip", "192.168.1.100", false, "trace-from-alert");
546        assert_eq!(event.metadata().trace_id, "trace-from-alert");
547        assert!(!event.success);
548    }
549
550    #[test]
551    fn action_event_display_success() {
552        let event = ActionEvent::new("container_isolate", "abc", true);
553        assert!(event.to_string().contains("OK"));
554    }
555
556    #[test]
557    fn action_event_display_failure() {
558        let event = ActionEvent::new("container_isolate", "abc", false);
559        assert!(event.to_string().contains("FAILED"));
560    }
561
562    #[test]
563    fn events_are_send_sync() {
564        fn assert_send_sync<T: Send + Sync + 'static>() {}
565        assert_send_sync::<PacketEvent>();
566        assert_send_sync::<LogEvent>();
567        assert_send_sync::<AlertEvent>();
568        assert_send_sync::<ActionEvent>();
569    }
570}