crablock-core 0.1.2

Core library for crablock - encryption, package format, and common utilities
Documentation
use chrono::Utc;
use serde::Serialize;
use std::collections::HashMap;
use tracing::{error, info, warn};
use uuid::Uuid;

use crate::crypto::EncryptionAlgorithm;
use crate::manifest::Manifest;

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LogLevel {
    Info,
    Warn,
    Error,
    Debug,
}

#[derive(Debug, Clone, Serialize)]
pub struct AuditEvent {
    pub timestamp: String,
    pub event_type: String,
    pub package_id: Option<Uuid>,
    pub details: HashMap<String, serde_json::Value>,
}

impl AuditEvent {
    pub fn new(event_type: impl Into<String>) -> Self {
        Self {
            timestamp: Utc::now().to_rfc3339(),
            event_type: event_type.into(),
            package_id: None,
            details: HashMap::new(),
        }
    }

    pub fn with_package_id(mut self, id: Uuid) -> Self {
        self.package_id = Some(id);
        self
    }

    pub fn with_detail(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
        let key = key.into();
        let value = serde_json::to_value(value).unwrap_or_default();
        self.details.insert(key, value);
        self
    }

    pub fn log(&self) {
        // We always serialize the same event shape.
        // Only the log level changes based on the event type.
        let json = serde_json::to_string(self).unwrap_or_default();
        match self.event_type.as_str() {
            "error" | "decryption_failed" | "verification_failed" | "execution_failed" => {
                error!(target: "crablock_audit", "{}", json);
            }
            "warn" | "cleanup_failed" => {
                warn!(target: "crablock_audit", "{}", json);
            }
            _ => {
                info!(target: "crablock_audit", "{}", json);
            }
        }
    }
}

pub fn log_package_load(manifest: &Manifest, key_source: &str, decrypt_mode: &str) {
    AuditEvent::new("package_loaded")
        .with_package_id(manifest.package_id)
        .with_detail("artifact_name", &manifest.artifact_name)
        .with_detail(
            "encryption_algorithm",
            manifest.encryption_algorithm.as_str(),
        )
        .with_detail("key_source_type", key_source)
        .with_detail("decrypt_mode", decrypt_mode)
        .with_detail("version", &manifest.version)
        .log();
}

pub fn log_verification_result(package_id: Uuid, success: bool, details: &str) {
    let event_type = if success {
        "verification_success"
    } else {
        "verification_failed"
    };
    AuditEvent::new(event_type)
        .with_package_id(package_id)
        .with_detail("success", success)
        .with_detail("details", details)
        .log();
}

pub fn log_execution_start(package_id: Uuid, entrypoint: &str, pid: u32) {
    AuditEvent::new("execution_start")
        .with_package_id(package_id)
        .with_detail("entrypoint", entrypoint)
        .with_detail("pid", pid)
        .log();
}

pub fn log_execution_stop(package_id: Uuid, exit_code: i32) {
    AuditEvent::new("execution_stop")
        .with_package_id(package_id)
        .with_detail("exit_code", exit_code)
        .log();
}

pub fn log_decryption(package_id: Uuid, algorithm: EncryptionAlgorithm, mode: &str) {
    AuditEvent::new("decryption")
        .with_package_id(package_id)
        .with_detail("algorithm", algorithm.as_str())
        .with_detail("mode", mode)
        .log();
}

pub fn log_cleanup(package_id: Uuid, path: &str, success: bool) {
    let event = if success {
        "cleanup_success"
    } else {
        "cleanup_failed"
    };
    AuditEvent::new(event)
        .with_package_id(package_id)
        .with_detail("path", path)
        .with_detail("success", success)
        .log();
}

pub fn init_logging(json_format: bool) {
    use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

    // Default to `info` so the CLI is useful even when RUST_LOG is not set.
    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

    if json_format {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(fmt::layer().json())
            .init();
    } else {
        tracing_subscriber::registry()
            .with(env_filter)
            .with(fmt::layer())
            .init();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_audit_event_creation() {
        let event = AuditEvent::new("test_event")
            .with_package_id(Uuid::new_v4())
            .with_detail("key", "value");

        assert_eq!(event.event_type, "test_event");
        assert!(event.package_id.is_some());
        assert!(event.details.contains_key("key"));
    }
}