use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum Event {
SessionStarted {
ts: DateTime<Utc>,
pid: u32,
tier: String,
net_mode: String,
profile: String,
},
FileObserved {
ts: DateTime<Utc>,
pid: u32,
comm: String,
path: String,
access: FileAccess,
},
ExecObserved {
ts: DateTime<Utc>,
pid: u32,
argv: Vec<String>,
},
BlockedAttempt {
ts: DateTime<Utc>,
pid: u32,
comm: String,
path: String,
source: String,
},
NetRequest {
ts: DateTime<Utc>,
host: String,
port: u16,
allowed: bool,
},
DnsResolved {
ts: DateTime<Utc>,
host: String,
ips: Vec<String>,
},
NetEgress {
ts: DateTime<Utc>,
host: String,
ip: String,
port: u16,
bytes_tx: u64,
bytes_rx: u64,
},
NetQuotaExceeded {
ts: DateTime<Utc>,
host: String,
limit_bytes: u64,
used_bytes: u64,
},
SecretMasked { ts: DateTime<Utc>, path: String },
Notice { ts: DateTime<Utc>, message: String },
SessionTimeout { ts: DateTime<Utc> },
SessionEnded {
ts: DateTime<Utc>,
exit_code: i32,
duration_secs: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FileAccess {
Read,
Write,
Unknown,
}
impl Event {
pub fn ts(&self) -> DateTime<Utc> {
match self {
Event::SessionStarted { ts, .. }
| Event::FileObserved { ts, .. }
| Event::ExecObserved { ts, .. }
| Event::BlockedAttempt { ts, .. }
| Event::NetRequest { ts, .. }
| Event::DnsResolved { ts, .. }
| Event::NetEgress { ts, .. }
| Event::NetQuotaExceeded { ts, .. }
| Event::SecretMasked { ts, .. }
| Event::Notice { ts, .. }
| Event::SessionTimeout { ts }
| Event::SessionEnded { ts, .. } => *ts,
}
}
pub fn kind(&self) -> &'static str {
match self {
Event::SessionStarted { .. } => "session_started",
Event::FileObserved { .. } => "file_observed",
Event::ExecObserved { .. } => "exec_observed",
Event::BlockedAttempt { .. } => "blocked_attempt",
Event::NetRequest { .. } => "net_request",
Event::DnsResolved { .. } => "dns_resolved",
Event::NetEgress { .. } => "net_egress",
Event::NetQuotaExceeded { .. } => "net_quota_exceeded",
Event::SecretMasked { .. } => "secret_masked",
Event::Notice { .. } => "notice",
Event::SessionTimeout { .. } => "session_timeout",
Event::SessionEnded { .. } => "session_ended",
}
}
pub fn path(&self) -> Option<&str> {
match self {
Event::FileObserved { path, .. }
| Event::BlockedAttempt { path, .. }
| Event::SecretMasked { path, .. } => Some(path),
_ => None,
}
}
pub fn network_target(&self) -> Option<(&str, u16, bool)> {
match self {
Event::NetRequest {
host,
port,
allowed,
..
} => Some((host, *port, *allowed)),
_ => None,
}
}
pub fn hint(&self) -> Option<String> {
match self {
Event::BlockedAttempt { path, .. } => Some(format!(
"to allow: add `read = \"{path}\"` (or net domain) to policy.toml"
)),
Event::NetRequest {
host,
allowed: false,
..
} => Some(format!(
"to allow: add `allow = [\"{host}\"]` (or net domain) to policy.toml"
)),
_ => None,
}
}
}
pub fn now() -> DateTime<Utc> {
Utc::now()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_hint_provides_remediation_for_blocked_attempts() {
let blocked_file = Event::BlockedAttempt {
ts: now(),
pid: 1234,
comm: "agent".into(),
path: "~/.aws/credentials".into(),
source: "landlock".into(),
};
assert_eq!(
blocked_file.hint().unwrap(),
"to allow: add `read = \"~/.aws/credentials\"` (or net domain) to policy.toml"
);
let blocked_net = Event::NetRequest {
ts: now(),
host: "api.custom.com".into(),
port: 443,
allowed: false,
};
assert_eq!(
blocked_net.hint().unwrap(),
"to allow: add `allow = [\"api.custom.com\"]` (or net domain) to policy.toml"
);
let allowed_net = Event::NetRequest {
ts: now(),
host: "api.custom.com".into(),
port: 443,
allowed: true,
};
assert!(allowed_net.hint().is_none());
}
}