use serde::{Deserialize, Serialize};
use crate::base::{category_for_class, compute_type_uid, ClassUid};
use crate::objects::actor::Actor;
use crate::objects::file::OcsfFile;
use crate::objects::metadata::Metadata;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum FileActivityType {
Create = 1,
Read = 2,
Update = 3,
Delete = 4,
Open = 14,
Other = 99,
}
impl FileActivityType {
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileActivity {
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 file: OcsfFile,
#[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 FileActivity {
#[must_use]
pub fn new(
activity: FileActivityType,
time: i64,
severity_id: u8,
status_id: u8,
metadata: Metadata,
file: OcsfFile,
) -> Self {
let class_uid = ClassUid::FileActivity;
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(file_activity_name(activity).to_string()),
time,
severity_id,
severity: None,
status_id,
status: None,
message: None,
metadata,
file,
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
}
}
fn file_activity_name(activity: FileActivityType) -> &'static str {
match activity {
FileActivityType::Create => "Create",
FileActivityType::Read => "Read",
FileActivityType::Update => "Update",
FileActivityType::Delete => "Delete",
FileActivityType::Open => "Open",
FileActivityType::Other => "Other",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_file() -> OcsfFile {
OcsfFile {
path: Some("/etc/shadow".to_string()),
name: Some("shadow".to_string()),
uid: None,
type_id: None,
size: None,
hashes: None,
}
}
#[test]
fn class_uid_is_1001() {
let e = FileActivity::new(
FileActivityType::Read,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_file(),
);
assert_eq!(e.class_uid, 1001);
}
#[test]
fn category_uid_is_1() {
let e = FileActivity::new(
FileActivityType::Read,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_file(),
);
assert_eq!(e.category_uid, 1);
}
#[test]
fn type_uid_read() {
let e = FileActivity::new(
FileActivityType::Read,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_file(),
);
assert_eq!(e.type_uid, 100102);
}
#[test]
fn type_uid_update() {
let e = FileActivity::new(
FileActivityType::Update,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_file(),
);
assert_eq!(e.type_uid, 100103);
}
#[test]
fn type_uid_open() {
let e = FileActivity::new(
FileActivityType::Open,
0,
0,
0,
Metadata::clawdstrike("0.1.3"),
sample_file(),
);
assert_eq!(e.type_uid, 100114);
}
#[test]
fn serialization_roundtrip() {
let e = FileActivity::new(
FileActivityType::Update,
1_709_366_400_000,
4,
2,
Metadata::clawdstrike("0.1.3"),
sample_file(),
)
.with_message("File write blocked");
let json = serde_json::to_string(&e).unwrap();
let e2: FileActivity = serde_json::from_str(&json).unwrap();
assert_eq!(e.type_uid, e2.type_uid);
assert_eq!(e.file.path, e2.file.path);
}
}