Skip to main content

sigil_protocol/
audit.rs

1//! Audit — tamper-evident logging of all security events.
2//!
3//! SIGIL defines the event schema (concrete struct) and the logger trait.
4//! The schema is part of the protocol; the logging backend is implementation-specific.
5
6use serde::{Deserialize, Serialize};
7
8/// Types of security events defined by the SIGIL protocol.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "snake_case")]
11pub enum AuditEventType {
12    /// An agent tool/command was executed.
13    CommandExecution,
14    /// A file was accessed.
15    FileAccess,
16    /// Configuration was changed.
17    ConfigChange,
18    /// Authentication succeeded.
19    AuthSuccess,
20    /// Authentication failed.
21    AuthFailure,
22    /// A security policy was violated.
23    PolicyViolation,
24    /// A general security event.
25    SecurityEvent,
26    /// Sensitive content was intercepted by the scanner.
27    SigilInterception,
28    /// An MCP tool call was gated.
29    McpToolGated,
30    /// An agent-to-agent delegation boundary was crossed.
31    DelegationCrossing,
32}
33
34/// Actor — who performed the action.
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct Actor {
37    /// Channel the action originated from (e.g., "cli", "web", "mcp").
38    pub channel: Option<String>,
39    /// User identifier.
40    pub user_id: Option<String>,
41    /// Human-readable username.
42    pub username: Option<String>,
43}
44
45/// Action — what was done.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct Action {
48    /// Description of the action (e.g., command string, tool name).
49    pub description: String,
50    /// Risk level assessment.
51    pub risk_level: String,
52    /// Whether the action was approved (by user or policy).
53    pub approved: bool,
54    /// Whether the action was allowed (by security policy).
55    pub allowed: bool,
56}
57
58/// Result of an action execution.
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct ExecutionResult {
61    /// Whether the action succeeded.
62    pub success: bool,
63    /// Exit code (for command executions).
64    pub exit_code: Option<i32>,
65    /// Duration in milliseconds.
66    pub duration_ms: u64,
67    /// Error message if failed.
68    pub error: Option<String>,
69}
70
71/// A complete SIGIL audit event.
72///
73/// This is the protocol's standard event format. All SIGIL-compliant
74/// systems must produce events conforming to this schema.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AuditEvent {
77    /// Unique event identifier.
78    pub id: String,
79    /// ISO 8601 timestamp.
80    pub timestamp: String,
81    /// Event type.
82    pub event_type: AuditEventType,
83    /// Who performed the action.
84    pub actor: Actor,
85    /// What was done.
86    pub action: Action,
87    /// Execution result.
88    pub result: ExecutionResult,
89    /// Optional HMAC signature for tamper evidence.
90    pub signature: Option<String>,
91}
92
93impl AuditEvent {
94    /// Create a new audit event with a unique ID and current timestamp.
95    pub fn new(event_type: AuditEventType) -> Self {
96        Self {
97            id: uuid::Uuid::new_v4().to_string(),
98            timestamp: chrono::Utc::now().to_rfc3339(),
99            event_type,
100            actor: Actor::default(),
101            action: Action::default(),
102            result: ExecutionResult::default(),
103            signature: None,
104        }
105    }
106
107    /// Set the actor.
108    pub fn with_actor(
109        mut self,
110        channel: String,
111        user_id: Option<String>,
112        username: Option<String>,
113    ) -> Self {
114        self.actor = Actor {
115            channel: Some(channel),
116            user_id,
117            username,
118        };
119        self
120    }
121
122    /// Set the action.
123    pub fn with_action(
124        mut self,
125        description: String,
126        risk_level: String,
127        approved: bool,
128        allowed: bool,
129    ) -> Self {
130        self.action = Action {
131            description,
132            risk_level,
133            approved,
134            allowed,
135        };
136        self
137    }
138
139    /// Set the execution result.
140    pub fn with_result(
141        mut self,
142        success: bool,
143        exit_code: Option<i32>,
144        duration_ms: u64,
145        error: Option<String>,
146    ) -> Self {
147        self.result = ExecutionResult {
148            success,
149            exit_code,
150            duration_ms,
151            error,
152        };
153        self
154    }
155}
156
157/// Trait for audit logging backends.
158///
159/// Implementations choose their storage (file, database, remote, etc.)
160/// and may add features like HMAC signing, rotation, or streaming.
161pub trait AuditLogger: Send + Sync {
162    /// Log a SIGIL audit event.
163    fn log(&self, event: &AuditEvent) -> anyhow::Result<()>;
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn audit_event_creates_unique_ids() {
172        let e1 = AuditEvent::new(AuditEventType::CommandExecution);
173        let e2 = AuditEvent::new(AuditEventType::CommandExecution);
174        assert_ne!(e1.id, e2.id);
175    }
176
177    #[test]
178    fn audit_event_serializes_to_json() {
179        let event = AuditEvent::new(AuditEventType::SigilInterception)
180            .with_actor("cli".into(), Some("u1".into()), Some("alice".into()))
181            .with_action("Redacted IBAN".into(), "high".into(), true, true);
182
183        let json = serde_json::to_string(&event).unwrap();
184        assert!(json.contains("sigil_interception"));
185        assert!(json.contains("alice"));
186    }
187
188    #[test]
189    fn audit_event_type_variants_exhaustive() {
190        // Ensure the protocol defines all expected event types
191        let types = vec![
192            AuditEventType::CommandExecution,
193            AuditEventType::FileAccess,
194            AuditEventType::ConfigChange,
195            AuditEventType::AuthSuccess,
196            AuditEventType::AuthFailure,
197            AuditEventType::PolicyViolation,
198            AuditEventType::SecurityEvent,
199            AuditEventType::SigilInterception,
200            AuditEventType::McpToolGated,
201            AuditEventType::DelegationCrossing,
202        ];
203        assert_eq!(types.len(), 10);
204    }
205}