use serde::{Deserialize, Serialize};
use crate::base::{category_for_class, compute_type_uid, ClassUid};
use crate::objects::actor::Actor;
use crate::objects::metadata::Metadata;
use crate::objects::process::OcsfProcess;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ProcessActivityType {
Launch = 1,
Terminate = 2,
Open = 3,
Inject = 4,
SetUserId = 5,
Other = 99,
}
impl ProcessActivityType {
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProcessActivity {
pub class_uid: u16,
pub category_uid: u8,
pub type_uid: u32,
pub activity_id: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub activity_name: Option<String>,
pub time: i64,
pub severity_id: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<String>,
pub status_id: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
pub metadata: Metadata,
pub process: OcsfProcess,
#[serde(skip_serializing_if = "Option::is_none")]
pub actor: Option<Actor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unmapped: Option<serde_json::Value>,
}
impl ProcessActivity {
#[must_use]
pub fn new(
activity: ProcessActivityType,
time: i64,
severity_id: u8,
status_id: u8,
metadata: Metadata,
process: OcsfProcess,
) -> Self {
let class_uid = ClassUid::ProcessActivity;
let activity_id = activity.as_u8();
Self {
class_uid: class_uid.as_u16(),
category_uid: category_for_class(class_uid).as_u8(),
type_uid: compute_type_uid(class_uid.as_u16(), activity_id),
activity_id,
activity_name: Some(process_activity_name(activity).to_string()),
time,
severity_id,
severity: None,
status_id,
status: None,
message: None,
metadata,
process,
actor: None,
unmapped: None,
}
}
#[must_use]
pub fn with_message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
#[must_use]
pub fn with_actor(mut self, actor: Actor) -> Self {
self.actor = Some(actor);
self
}
#[must_use]
pub fn with_unmapped(mut self, unmapped: serde_json::Value) -> Self {
self.unmapped = Some(unmapped);
self
}
}
fn process_activity_name(activity: ProcessActivityType) -> &'static str {
match activity {
ProcessActivityType::Launch => "Launch",
ProcessActivityType::Terminate => "Terminate",
ProcessActivityType::Open => "Open",
ProcessActivityType::Inject => "Inject",
ProcessActivityType::SetUserId => "Set User ID",
ProcessActivityType::Other => "Other",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_process() -> OcsfProcess {
OcsfProcess {
pid: Some(1234),
name: Some("curl".to_string()),
cmd_line: Some("curl https://example.com".to_string()),
file: None,
user: None,
parent_process: None,
cwd: None,
}
}
#[test]
fn class_uid_is_1007() {
let e = ProcessActivity::new(
ProcessActivityType::Launch,
1_709_366_400_000,
1,
1,
Metadata::clawdstrike("0.1.3"),
sample_process(),
);
assert_eq!(e.class_uid, 1007);
}
#[test]
fn category_uid_is_1() {
let e = ProcessActivity::new(
ProcessActivityType::Launch,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_process(),
);
assert_eq!(e.category_uid, 1);
}
#[test]
fn type_uid_launch() {
let e = ProcessActivity::new(
ProcessActivityType::Launch,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_process(),
);
assert_eq!(e.type_uid, 100701);
}
#[test]
fn type_uid_terminate() {
let e = ProcessActivity::new(
ProcessActivityType::Terminate,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_process(),
);
assert_eq!(e.type_uid, 100702);
}
#[test]
fn serialization_roundtrip() {
let e = ProcessActivity::new(
ProcessActivityType::Launch,
1_709_366_400_000,
1,
1,
Metadata::clawdstrike("0.1.3"),
sample_process(),
)
.with_message("curl launched");
let json = serde_json::to_string(&e).unwrap();
let e2: ProcessActivity = serde_json::from_str(&json).unwrap();
assert_eq!(e.type_uid, e2.type_uid);
assert_eq!(e.process.pid, e2.process.pid);
}
}