Skip to main content

clawdstrike_ocsf/classes/
network_activity.rs

1//! OCSF Network Activity (class_uid = 4001, category_uid = 4 Network Activity).
2//!
3//! Activity IDs: 1=Open, 2=Close, 3=Reset, 4=Fail, 5=Refuse, 6=Traffic.
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::network_endpoint::{ConnectionInfo, NetworkEndpoint};
11
12/// OCSF activity IDs for Network Activity.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[repr(u8)]
15pub enum NetworkActivityType {
16    /// Connection opened.
17    Open = 1,
18    /// Connection closed.
19    Close = 2,
20    /// Connection reset.
21    Reset = 3,
22    /// Connection failed.
23    Fail = 4,
24    /// Connection refused.
25    Refuse = 5,
26    /// Generic traffic observed.
27    Traffic = 6,
28    /// Other (vendor-specific).
29    Other = 99,
30}
31
32impl NetworkActivityType {
33    /// Returns the integer representation.
34    #[must_use]
35    pub const fn as_u8(self) -> u8 {
36        self as u8
37    }
38}
39
40/// OCSF Network Activity event (class_uid = 4001).
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct NetworkActivity {
44    // ── OCSF base fields ──
45    /// Always 4001.
46    pub class_uid: u16,
47    /// Always 4 (Network Activity).
48    pub category_uid: u8,
49    /// `class_uid * 100 + activity_id`.
50    pub type_uid: u32,
51    /// Activity ID.
52    pub activity_id: u8,
53    /// Human-readable activity name.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub activity_name: Option<String>,
56    /// Event time as epoch milliseconds.
57    pub time: i64,
58    /// Severity ID (0-6, 99).
59    pub severity_id: u8,
60    /// Human-readable severity label.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub severity: Option<String>,
63    /// Status ID (0=Unknown, 1=Success, 2=Failure).
64    pub status_id: u8,
65    /// Human-readable status label.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub status: Option<String>,
68    /// Human-readable event message.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub message: Option<String>,
71    /// Metadata (required).
72    pub metadata: Metadata,
73
74    // ── Network Activity-specific fields ──
75    /// Source endpoint.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub src_endpoint: Option<NetworkEndpoint>,
78    /// Destination endpoint.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub dst_endpoint: Option<NetworkEndpoint>,
81    /// Connection info.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub connection_info: Option<ConnectionInfo>,
84    /// Actor who initiated the network activity.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub actor: Option<Actor>,
87    /// Action ID (1=Allowed, 2=Denied).
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub action_id: Option<u8>,
90    /// Disposition ID.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub disposition_id: Option<u8>,
93    /// Vendor-specific unmapped data.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub unmapped: Option<serde_json::Value>,
96}
97
98impl NetworkActivity {
99    /// Create a new Network Activity event with required fields.
100    #[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    /// Set the source endpoint.
134    #[must_use]
135    pub fn with_src_endpoint(mut self, ep: NetworkEndpoint) -> Self {
136        self.src_endpoint = Some(ep);
137        self
138    }
139
140    /// Set the destination endpoint.
141    #[must_use]
142    pub fn with_dst_endpoint(mut self, ep: NetworkEndpoint) -> Self {
143        self.dst_endpoint = Some(ep);
144        self
145    }
146
147    /// Set connection info.
148    #[must_use]
149    pub fn with_connection_info(mut self, ci: ConnectionInfo) -> Self {
150        self.connection_info = Some(ci);
151        self
152    }
153
154    /// Set the event message.
155    #[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    /// Set the actor.
162    #[must_use]
163    pub fn with_actor(mut self, actor: Actor) -> Self {
164        self.actor = Some(actor);
165        self
166    }
167
168    /// Set action ID.
169    #[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    /// Set disposition ID.
176    #[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}