use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RecordType {
SessionInit,
SyscallDenial,
FsMutation,
ResourceSample,
TreeExtinction,
SessionVerdict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TierClassification {
#[serde(rename = "TIER_1_LINUX")]
Tier1Linux,
#[serde(rename = "TIER_2_MACOS")]
Tier2Macos,
#[serde(rename = "TIER_3_WINDOWS")]
Tier3Windows,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SyscallActionTaken {
Blocked,
Audited,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FsMutationType {
Created,
Modified,
Deleted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionInitPayload {
pub platform: String,
pub kernel_release: String,
pub agent_name: String,
pub tier: TierClassification,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyscallDenialPayload {
pub syscall_name: String,
pub target_path: String,
pub lsm_backend: String,
pub action_taken: SyscallActionTaken,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FsMutationPayload {
pub relative_path: String,
pub mutation_type: FsMutationType,
pub sha256_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResourceSamplePayload {
pub cpu_percent: f64,
pub memory_rss_bytes: u64,
pub pid_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TreeExtinctionPayload {
pub tier: String,
pub survivors: u32,
pub signal_sent: i32,
pub elapsed_ms: u64,
pub clean: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionVerdictPayload {
pub verdict: String,
pub evidence_strength: String,
pub exit_code: i32,
pub root_dag_digest: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AuditPayload {
SessionInit(SessionInitPayload),
SyscallDenial(SyscallDenialPayload),
FsMutation(FsMutationPayload),
ResourceSample(ResourceSamplePayload),
TreeExtinction(TreeExtinctionPayload),
SessionVerdict(SessionVerdictPayload),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VettoAuditRecord {
pub timestamp_utc: String,
pub record_type: RecordType,
pub session_id: String,
pub contract_digest: String,
pub payload: AuditPayload,
}
impl VettoAuditRecord {
pub fn new(
record_type: RecordType,
session_id: impl Into<String>,
contract_digest: impl Into<String>,
payload: AuditPayload,
) -> Self {
Self {
timestamp_utc: chrono::Utc::now().to_rfc3339(),
record_type,
session_id: session_id.into(),
contract_digest: contract_digest.into(),
payload,
}
}
pub fn session_init(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
platform: impl Into<String>,
kernel_release: impl Into<String>,
agent_name: impl Into<String>,
tier: TierClassification,
) -> Self {
Self::new(
RecordType::SessionInit,
session_id,
contract_digest,
AuditPayload::SessionInit(SessionInitPayload {
platform: platform.into(),
kernel_release: kernel_release.into(),
agent_name: agent_name.into(),
tier,
}),
)
}
pub fn syscall_denial(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
syscall_name: impl Into<String>,
target_path: impl Into<String>,
lsm_backend: impl Into<String>,
action_taken: SyscallActionTaken,
) -> Self {
Self::new(
RecordType::SyscallDenial,
session_id,
contract_digest,
AuditPayload::SyscallDenial(SyscallDenialPayload {
syscall_name: syscall_name.into(),
target_path: target_path.into(),
lsm_backend: lsm_backend.into(),
action_taken,
}),
)
}
pub fn fs_mutation(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
relative_path: impl Into<String>,
mutation_type: FsMutationType,
sha256_digest: impl Into<String>,
) -> Self {
Self::new(
RecordType::FsMutation,
session_id,
contract_digest,
AuditPayload::FsMutation(FsMutationPayload {
relative_path: relative_path.into(),
mutation_type,
sha256_digest: sha256_digest.into(),
}),
)
}
pub fn resource_sample(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
cpu_percent: f64,
memory_rss_bytes: u64,
pid_count: u32,
) -> Self {
Self::new(
RecordType::ResourceSample,
session_id,
contract_digest,
AuditPayload::ResourceSample(ResourceSamplePayload {
cpu_percent,
memory_rss_bytes,
pid_count,
}),
)
}
pub fn tree_extinction(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
tier: impl Into<String>,
survivors: u32,
signal_sent: i32,
elapsed_ms: u64,
clean: bool,
) -> Self {
Self::new(
RecordType::TreeExtinction,
session_id,
contract_digest,
AuditPayload::TreeExtinction(TreeExtinctionPayload {
tier: tier.into(),
survivors,
signal_sent,
elapsed_ms,
clean,
}),
)
}
pub fn session_verdict(
session_id: impl Into<String>,
contract_digest: impl Into<String>,
verdict: &crate::audit::verdict::FinalVerdict,
root_dag_digest: impl Into<String>,
) -> Self {
Self::new(
RecordType::SessionVerdict,
session_id,
contract_digest,
AuditPayload::SessionVerdict(SessionVerdictPayload {
verdict: verdict.status.label().to_string(),
evidence_strength: verdict.strength.label().to_string(),
exit_code: verdict.exit_code,
root_dag_digest: root_dag_digest.into(),
}),
)
}
pub fn to_json_line(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_types_screaming_snake_case() {
assert_eq!(
serde_json::to_string(&RecordType::SessionInit).unwrap(),
"\"SESSION_INIT\""
);
assert_eq!(
serde_json::to_string(&RecordType::SyscallDenial).unwrap(),
"\"SYSCALL_DENIAL\""
);
assert_eq!(
serde_json::to_string(&RecordType::FsMutation).unwrap(),
"\"FS_MUTATION\""
);
assert_eq!(
serde_json::to_string(&RecordType::ResourceSample).unwrap(),
"\"RESOURCE_SAMPLE\""
);
assert_eq!(
serde_json::to_string(&RecordType::TreeExtinction).unwrap(),
"\"TREE_EXTINCTION\""
);
assert_eq!(
serde_json::to_string(&RecordType::SessionVerdict).unwrap(),
"\"SESSION_VERDICT\""
);
}
#[test]
fn test_tier_classification_screaming_snake_case() {
assert_eq!(
serde_json::to_string(&TierClassification::Tier1Linux).unwrap(),
"\"TIER_1_LINUX\""
);
assert_eq!(
serde_json::to_string(&TierClassification::Tier2Macos).unwrap(),
"\"TIER_2_MACOS\""
);
assert_eq!(
serde_json::to_string(&TierClassification::Tier3Windows).unwrap(),
"\"TIER_3_WINDOWS\""
);
}
#[test]
fn test_syscall_action_taken_screaming_snake_case() {
assert_eq!(
serde_json::to_string(&SyscallActionTaken::Blocked).unwrap(),
"\"BLOCKED\""
);
assert_eq!(
serde_json::to_string(&SyscallActionTaken::Audited).unwrap(),
"\"AUDITED\""
);
}
#[test]
fn test_fs_mutation_type_screaming_snake_case() {
assert_eq!(
serde_json::to_string(&FsMutationType::Created).unwrap(),
"\"CREATED\""
);
assert_eq!(
serde_json::to_string(&FsMutationType::Modified).unwrap(),
"\"MODIFIED\""
);
assert_eq!(
serde_json::to_string(&FsMutationType::Deleted).unwrap(),
"\"DELETED\""
);
}
}