clawdstrike_ocsf/classes/
network_activity.rs1use 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::network_endpoint::{ConnectionInfo, NetworkEndpoint};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[repr(u8)]
15pub enum NetworkActivityType {
16 Open = 1,
18 Close = 2,
20 Reset = 3,
22 Fail = 4,
24 Refuse = 5,
26 Traffic = 6,
28 Other = 99,
30}
31
32impl NetworkActivityType {
33 #[must_use]
35 pub const fn as_u8(self) -> u8 {
36 self as u8
37 }
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct NetworkActivity {
44 pub class_uid: u16,
47 pub category_uid: u8,
49 pub type_uid: u32,
51 pub activity_id: u8,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub activity_name: Option<String>,
56 pub time: i64,
58 pub severity_id: u8,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub severity: Option<String>,
63 pub status_id: u8,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub status: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub message: Option<String>,
71 pub metadata: Metadata,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub src_endpoint: Option<NetworkEndpoint>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub dst_endpoint: Option<NetworkEndpoint>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub connection_info: Option<ConnectionInfo>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub actor: Option<Actor>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub action_id: Option<u8>,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub disposition_id: Option<u8>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub unmapped: Option<serde_json::Value>,
96}
97
98impl NetworkActivity {
99 #[must_use]
101 pub fn new(
102 activity: NetworkActivityType,
103 time: i64,
104 severity_id: u8,
105 status_id: u8,
106 metadata: Metadata,
107 ) -> Self {
108 let class_uid = ClassUid::NetworkActivity;
109 let activity_id = activity.as_u8();
110 Self {
111 class_uid: class_uid.as_u16(),
112 category_uid: category_for_class(class_uid).as_u8(),
113 type_uid: compute_type_uid(class_uid.as_u16(), activity_id),
114 activity_id,
115 activity_name: Some(network_activity_name(activity).to_string()),
116 time,
117 severity_id,
118 severity: None,
119 status_id,
120 status: None,
121 message: None,
122 metadata,
123 src_endpoint: None,
124 dst_endpoint: None,
125 connection_info: None,
126 actor: None,
127 action_id: None,
128 disposition_id: None,
129 unmapped: None,
130 }
131 }
132
133 #[must_use]
135 pub fn with_src_endpoint(mut self, ep: NetworkEndpoint) -> Self {
136 self.src_endpoint = Some(ep);
137 self
138 }
139
140 #[must_use]
142 pub fn with_dst_endpoint(mut self, ep: NetworkEndpoint) -> Self {
143 self.dst_endpoint = Some(ep);
144 self
145 }
146
147 #[must_use]
149 pub fn with_connection_info(mut self, ci: ConnectionInfo) -> Self {
150 self.connection_info = Some(ci);
151 self
152 }
153
154 #[must_use]
156 pub fn with_message(mut self, msg: impl Into<String>) -> Self {
157 self.message = Some(msg.into());
158 self
159 }
160
161 #[must_use]
163 pub fn with_actor(mut self, actor: Actor) -> Self {
164 self.actor = Some(actor);
165 self
166 }
167
168 #[must_use]
170 pub fn with_action_id(mut self, action_id: u8) -> Self {
171 self.action_id = Some(action_id);
172 self
173 }
174
175 #[must_use]
177 pub fn with_disposition_id(mut self, disposition_id: u8) -> Self {
178 self.disposition_id = Some(disposition_id);
179 self
180 }
181}
182
183fn network_activity_name(activity: NetworkActivityType) -> &'static str {
184 match activity {
185 NetworkActivityType::Open => "Open",
186 NetworkActivityType::Close => "Close",
187 NetworkActivityType::Reset => "Reset",
188 NetworkActivityType::Fail => "Fail",
189 NetworkActivityType::Refuse => "Refuse",
190 NetworkActivityType::Traffic => "Traffic",
191 NetworkActivityType::Other => "Other",
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn class_uid_is_4001() {
201 let e = NetworkActivity::new(
202 NetworkActivityType::Traffic,
203 0,
204 0,
205 0,
206 Metadata::clawdstrike("0.1.3"),
207 );
208 assert_eq!(e.class_uid, 4001);
209 }
210
211 #[test]
212 fn category_uid_is_4() {
213 let e = NetworkActivity::new(
214 NetworkActivityType::Traffic,
215 0,
216 0,
217 0,
218 Metadata::clawdstrike("0.1.3"),
219 );
220 assert_eq!(e.category_uid, 4);
221 }
222
223 #[test]
224 fn type_uid_traffic() {
225 let e = NetworkActivity::new(
226 NetworkActivityType::Traffic,
227 0,
228 0,
229 0,
230 Metadata::clawdstrike("0.1.3"),
231 );
232 assert_eq!(e.type_uid, 400106);
233 }
234
235 #[test]
236 fn type_uid_refuse() {
237 let e = NetworkActivity::new(
238 NetworkActivityType::Refuse,
239 0,
240 0,
241 0,
242 Metadata::clawdstrike("0.1.3"),
243 );
244 assert_eq!(e.type_uid, 400105);
245 }
246
247 #[test]
248 fn type_uid_fail() {
249 let e = NetworkActivity::new(
250 NetworkActivityType::Fail,
251 0,
252 0,
253 0,
254 Metadata::clawdstrike("0.1.3"),
255 );
256 assert_eq!(e.type_uid, 400104);
257 }
258
259 #[test]
260 fn serialization_roundtrip() {
261 let e = NetworkActivity::new(
262 NetworkActivityType::Traffic,
263 1_709_366_400_000,
264 1,
265 1,
266 Metadata::clawdstrike("0.1.3"),
267 )
268 .with_src_endpoint(NetworkEndpoint {
269 ip: Some("10.0.0.1".to_string()),
270 port: Some(8080),
271 domain: None,
272 hostname: None,
273 subnet_uid: None,
274 })
275 .with_dst_endpoint(NetworkEndpoint {
276 ip: Some("93.184.216.34".to_string()),
277 port: Some(443),
278 domain: Some("example.com".to_string()),
279 hostname: None,
280 subnet_uid: None,
281 })
282 .with_message("Egress traffic observed");
283
284 let json = serde_json::to_string(&e).unwrap();
285 let e2: NetworkActivity = serde_json::from_str(&json).unwrap();
286 assert_eq!(e.type_uid, e2.type_uid);
287 assert_eq!(
288 e.dst_endpoint.as_ref().and_then(|ep| ep.domain.as_deref()),
289 Some("example.com")
290 );
291 }
292}