Skip to main content

clawdstrike_ocsf/classes/
process_activity.rs

1//! OCSF Process Activity (class_uid = 1007, category_uid = 1 System Activity).
2//!
3//! Activity IDs: 1=Launch, 2=Terminate, 3=Open, 4=Inject, 5=SetUserId.
4
5use serde::{Deserialize, Serialize};
6
7use crate::base::{category_for_class, compute_type_uid, ClassUid};
8use crate::objects::actor::Actor;
9use crate::objects::metadata::Metadata;
10use crate::objects::process::OcsfProcess;
11
12/// OCSF activity IDs for Process Activity.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[repr(u8)]
15pub enum ProcessActivityType {
16    /// Process launched / created.
17    Launch = 1,
18    /// Process terminated / exited.
19    Terminate = 2,
20    /// Process opened (e.g., ptrace attach).
21    Open = 3,
22    /// Code injected into process.
23    Inject = 4,
24    /// UID changed (setuid).
25    SetUserId = 5,
26    /// Other (vendor-specific).
27    Other = 99,
28}
29
30impl ProcessActivityType {
31    /// Returns the integer representation.
32    #[must_use]
33    pub const fn as_u8(self) -> u8 {
34        self as u8
35    }
36}
37
38/// OCSF Process Activity event (class_uid = 1007).
39#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct ProcessActivity {
42    // ── OCSF base fields ──
43    /// Always 1007.
44    pub class_uid: u16,
45    /// Always 1 (System Activity).
46    pub category_uid: u8,
47    /// `class_uid * 100 + activity_id`.
48    pub type_uid: u32,
49    /// Activity ID.
50    pub activity_id: u8,
51    /// Human-readable activity name.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub activity_name: Option<String>,
54    /// Event time as epoch milliseconds.
55    pub time: i64,
56    /// Severity ID (0-6, 99).
57    pub severity_id: u8,
58    /// Human-readable severity label.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub severity: Option<String>,
61    /// Status ID (0=Unknown, 1=Success, 2=Failure).
62    pub status_id: u8,
63    /// Human-readable status label.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub status: Option<String>,
66    /// Human-readable event message.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub message: Option<String>,
69    /// Metadata (required).
70    pub metadata: Metadata,
71
72    // ── Process Activity-specific fields ──
73    /// The process (required).
74    pub process: OcsfProcess,
75    /// Actor who initiated the process activity.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub actor: Option<Actor>,
78    /// Vendor-specific unmapped data.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub unmapped: Option<serde_json::Value>,
81}
82
83impl ProcessActivity {
84    /// Create a new Process Activity event with required fields.
85    #[must_use]
86    pub fn new(
87        activity: ProcessActivityType,
88        time: i64,
89        severity_id: u8,
90        status_id: u8,
91        metadata: Metadata,
92        process: OcsfProcess,
93    ) -> Self {
94        let class_uid = ClassUid::ProcessActivity;
95        let activity_id = activity.as_u8();
96        Self {
97            class_uid: class_uid.as_u16(),
98            category_uid: category_for_class(class_uid).as_u8(),
99            type_uid: compute_type_uid(class_uid.as_u16(), activity_id),
100            activity_id,
101            activity_name: Some(process_activity_name(activity).to_string()),
102            time,
103            severity_id,
104            severity: None,
105            status_id,
106            status: None,
107            message: None,
108            metadata,
109            process,
110            actor: None,
111            unmapped: None,
112        }
113    }
114
115    /// Set the event message.
116    #[must_use]
117    pub fn with_message(mut self, msg: impl Into<String>) -> Self {
118        self.message = Some(msg.into());
119        self
120    }
121
122    /// Set the actor.
123    #[must_use]
124    pub fn with_actor(mut self, actor: Actor) -> Self {
125        self.actor = Some(actor);
126        self
127    }
128
129    /// Set unmapped vendor data.
130    #[must_use]
131    pub fn with_unmapped(mut self, unmapped: serde_json::Value) -> Self {
132        self.unmapped = Some(unmapped);
133        self
134    }
135}
136
137fn process_activity_name(activity: ProcessActivityType) -> &'static str {
138    match activity {
139        ProcessActivityType::Launch => "Launch",
140        ProcessActivityType::Terminate => "Terminate",
141        ProcessActivityType::Open => "Open",
142        ProcessActivityType::Inject => "Inject",
143        ProcessActivityType::SetUserId => "Set User ID",
144        ProcessActivityType::Other => "Other",
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn sample_process() -> OcsfProcess {
153        OcsfProcess {
154            pid: Some(1234),
155            name: Some("curl".to_string()),
156            cmd_line: Some("curl https://example.com".to_string()),
157            file: None,
158            user: None,
159            parent_process: None,
160            cwd: None,
161        }
162    }
163
164    #[test]
165    fn class_uid_is_1007() {
166        let e = ProcessActivity::new(
167            ProcessActivityType::Launch,
168            1_709_366_400_000,
169            1,
170            1,
171            Metadata::clawdstrike("0.1.3"),
172            sample_process(),
173        );
174        assert_eq!(e.class_uid, 1007);
175    }
176
177    #[test]
178    fn category_uid_is_1() {
179        let e = ProcessActivity::new(
180            ProcessActivityType::Launch,
181            0,
182            0,
183            0,
184            Metadata::clawdstrike("0.1.3"),
185            sample_process(),
186        );
187        assert_eq!(e.category_uid, 1);
188    }
189
190    #[test]
191    fn type_uid_launch() {
192        let e = ProcessActivity::new(
193            ProcessActivityType::Launch,
194            0,
195            0,
196            0,
197            Metadata::clawdstrike("0.1.3"),
198            sample_process(),
199        );
200        assert_eq!(e.type_uid, 100701);
201    }
202
203    #[test]
204    fn type_uid_terminate() {
205        let e = ProcessActivity::new(
206            ProcessActivityType::Terminate,
207            0,
208            0,
209            0,
210            Metadata::clawdstrike("0.1.3"),
211            sample_process(),
212        );
213        assert_eq!(e.type_uid, 100702);
214    }
215
216    #[test]
217    fn serialization_roundtrip() {
218        let e = ProcessActivity::new(
219            ProcessActivityType::Launch,
220            1_709_366_400_000,
221            1,
222            1,
223            Metadata::clawdstrike("0.1.3"),
224            sample_process(),
225        )
226        .with_message("curl launched");
227
228        let json = serde_json::to_string(&e).unwrap();
229        let e2: ProcessActivity = serde_json::from_str(&json).unwrap();
230        assert_eq!(e.type_uid, e2.type_uid);
231        assert_eq!(e.process.pid, e2.process.pid);
232    }
233}