clawdstrike_ocsf/classes/
file_activity.rs1use serde::{Deserialize, Serialize};
6
7use crate::base::{category_for_class, compute_type_uid, ClassUid};
8use crate::objects::actor::Actor;
9use crate::objects::file::OcsfFile;
10use crate::objects::metadata::Metadata;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[repr(u8)]
15pub enum FileActivityType {
16 Create = 1,
18 Read = 2,
20 Update = 3,
22 Delete = 4,
24 Open = 14,
26 Other = 99,
28}
29
30impl FileActivityType {
31 #[must_use]
33 pub const fn as_u8(self) -> u8 {
34 self as u8
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct FileActivity {
42 pub class_uid: u16,
45 pub category_uid: u8,
47 pub type_uid: u32,
49 pub activity_id: u8,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub activity_name: Option<String>,
54 pub time: i64,
56 pub severity_id: u8,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub severity: Option<String>,
61 pub status_id: u8,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub status: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub message: Option<String>,
69 pub metadata: Metadata,
71
72 pub file: OcsfFile,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub actor: Option<Actor>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub unmapped: Option<serde_json::Value>,
81}
82
83impl FileActivity {
84 #[must_use]
86 pub fn new(
87 activity: FileActivityType,
88 time: i64,
89 severity_id: u8,
90 status_id: u8,
91 metadata: Metadata,
92 file: OcsfFile,
93 ) -> Self {
94 let class_uid = ClassUid::FileActivity;
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(file_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 file,
110 actor: None,
111 unmapped: None,
112 }
113 }
114
115 #[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 #[must_use]
124 pub fn with_actor(mut self, actor: Actor) -> Self {
125 self.actor = Some(actor);
126 self
127 }
128}
129
130fn file_activity_name(activity: FileActivityType) -> &'static str {
131 match activity {
132 FileActivityType::Create => "Create",
133 FileActivityType::Read => "Read",
134 FileActivityType::Update => "Update",
135 FileActivityType::Delete => "Delete",
136 FileActivityType::Open => "Open",
137 FileActivityType::Other => "Other",
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn sample_file() -> OcsfFile {
146 OcsfFile {
147 path: Some("/etc/shadow".to_string()),
148 name: Some("shadow".to_string()),
149 uid: None,
150 type_id: None,
151 size: None,
152 hashes: None,
153 }
154 }
155
156 #[test]
157 fn class_uid_is_1001() {
158 let e = FileActivity::new(
159 FileActivityType::Read,
160 0,
161 0,
162 0,
163 Metadata::clawdstrike("0.1.3"),
164 sample_file(),
165 );
166 assert_eq!(e.class_uid, 1001);
167 }
168
169 #[test]
170 fn category_uid_is_1() {
171 let e = FileActivity::new(
172 FileActivityType::Read,
173 0,
174 0,
175 0,
176 Metadata::clawdstrike("0.1.3"),
177 sample_file(),
178 );
179 assert_eq!(e.category_uid, 1);
180 }
181
182 #[test]
183 fn type_uid_read() {
184 let e = FileActivity::new(
185 FileActivityType::Read,
186 0,
187 0,
188 0,
189 Metadata::clawdstrike("0.1.3"),
190 sample_file(),
191 );
192 assert_eq!(e.type_uid, 100102);
193 }
194
195 #[test]
196 fn type_uid_update() {
197 let e = FileActivity::new(
198 FileActivityType::Update,
199 0,
200 0,
201 0,
202 Metadata::clawdstrike("0.1.3"),
203 sample_file(),
204 );
205 assert_eq!(e.type_uid, 100103);
206 }
207
208 #[test]
209 fn type_uid_open() {
210 let e = FileActivity::new(
211 FileActivityType::Open,
212 0,
213 0,
214 0,
215 Metadata::clawdstrike("0.1.3"),
216 sample_file(),
217 );
218 assert_eq!(e.type_uid, 100114);
220 }
221
222 #[test]
223 fn serialization_roundtrip() {
224 let e = FileActivity::new(
225 FileActivityType::Update,
226 1_709_366_400_000,
227 4,
228 2,
229 Metadata::clawdstrike("0.1.3"),
230 sample_file(),
231 )
232 .with_message("File write blocked");
233
234 let json = serde_json::to_string(&e).unwrap();
235 let e2: FileActivity = serde_json::from_str(&json).unwrap();
236 assert_eq!(e.type_uid, e2.type_uid);
237 assert_eq!(e.file.path, e2.file.path);
238 }
239}