1use chrono::{DateTime, Utc};
7use sen_plugin_api::Capabilities;
8use serde::Serialize;
9use std::collections::VecDeque;
10use std::fmt;
11use std::fs::{File, OpenOptions};
12use std::io::{BufWriter, Write};
13use std::path::{Path, PathBuf};
14use std::sync::{Mutex, RwLock};
15use thiserror::Error;
16
17pub type Timestamp = String;
19
20fn now_iso8601() -> Timestamp {
22 let now: DateTime<Utc> = Utc::now();
23 now.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
24}
25
26#[derive(Debug, Clone, Serialize)]
28pub struct AuditEvent {
29 pub timestamp: Timestamp,
31 pub event_type: AuditEventType,
33 pub plugin: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub command: Option<String>,
38 pub details: AuditDetails,
40}
41
42impl AuditEvent {
43 pub fn new(
45 event_type: AuditEventType,
46 plugin: impl Into<String>,
47 details: AuditDetails,
48 ) -> Self {
49 Self {
50 timestamp: now_iso8601(),
51 event_type,
52 plugin: plugin.into(),
53 command: None,
54 details,
55 }
56 }
57
58 pub fn with_command(mut self, command: impl Into<String>) -> Self {
60 self.command = Some(command.into());
61 self
62 }
63}
64
65#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
67#[serde(rename_all = "snake_case")]
68pub enum AuditEventType {
69 PermissionRequested,
71 PermissionGranted,
73 PermissionDenied,
75 CapabilityUsed,
77 EscalationDetected,
79 PluginLoaded,
81 PluginUnloaded,
83}
84
85#[derive(Debug, Clone, Serialize)]
87#[serde(rename_all = "snake_case", tag = "type")]
88pub enum AuditDetails {
89 Permission {
91 #[serde(skip_serializing_if = "Option::is_none")]
93 trust_level: Option<TrustLevel>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 reason: Option<String>,
97 capabilities_hash: String,
99 },
100 FileAccess { path: PathBuf, mode: AccessMode },
102 EnvAccess { variable: String },
104 NetworkAccess { host: String, port: Option<u16> },
106 StdioAccess { stream: StdioStream },
108 Escalation { old_hash: String, new_hash: String },
110 Lifecycle {
112 #[serde(skip_serializing_if = "Option::is_none")]
113 path: Option<PathBuf>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 version: Option<String>,
116 },
117}
118
119#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
121#[serde(rename_all = "snake_case")]
122pub enum TrustLevel {
123 Once,
125 Session,
127 Permanent,
129}
130
131#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
133#[serde(rename_all = "snake_case")]
134pub enum AccessMode {
135 Read,
136 Write,
137 ReadWrite,
138}
139
140#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
142#[serde(rename_all = "snake_case")]
143pub enum StdioStream {
144 Stdin,
145 Stdout,
146 Stderr,
147}
148
149#[derive(Debug, Error)]
151pub enum AuditError {
152 #[error("Failed to write audit log: {0}")]
153 WriteError(#[from] std::io::Error),
154
155 #[error("Failed to serialize audit event: {0}")]
156 SerializationError(#[from] serde_json::Error),
157
158 #[error("Audit sink not available: {0}")]
159 Unavailable(String),
160}
161
162pub trait AuditSink: Send + Sync {
188 fn record(&self, event: AuditEvent) -> Result<(), AuditError>;
190
191 fn flush(&self) -> Result<(), AuditError>;
193
194 fn is_healthy(&self) -> bool {
196 true
197 }
198}
199
200pub struct FileAuditSink {
208 path: PathBuf,
209 writer: Mutex<BufWriter<File>>,
210}
211
212impl FileAuditSink {
213 pub fn new(path: impl AsRef<Path>) -> Result<Self, AuditError> {
215 let path = path.as_ref().to_path_buf();
216
217 if let Some(parent) = path.parent() {
219 std::fs::create_dir_all(parent)?;
220 }
221
222 let file = OpenOptions::new().create(true).append(true).open(&path)?;
223
224 Ok(Self {
225 path,
226 writer: Mutex::new(BufWriter::new(file)),
227 })
228 }
229
230 pub fn path(&self) -> &Path {
232 &self.path
233 }
234}
235
236impl AuditSink for FileAuditSink {
237 fn record(&self, event: AuditEvent) -> Result<(), AuditError> {
238 let json = serde_json::to_string(&event)?;
239 let mut writer = self.writer.lock().expect("FileAuditSink mutex poisoned");
240 writeln!(writer, "{}", json)?;
241 Ok(())
242 }
243
244 fn flush(&self) -> Result<(), AuditError> {
245 let mut writer = self.writer.lock().expect("FileAuditSink mutex poisoned");
246 writer.flush()?;
247 Ok(())
248 }
249
250 fn is_healthy(&self) -> bool {
251 self.path.parent().map(|p| p.exists()).unwrap_or(true)
252 }
253}
254
255impl fmt::Debug for FileAuditSink {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 f.debug_struct("FileAuditSink")
258 .field("path", &self.path)
259 .finish()
260 }
261}
262
263pub struct MemoryAuditSink {
267 events: RwLock<VecDeque<AuditEvent>>,
268 max_events: usize,
269}
270
271impl MemoryAuditSink {
272 pub fn new() -> Self {
274 Self::with_capacity(1000)
275 }
276
277 pub fn with_capacity(max_events: usize) -> Self {
279 Self {
280 events: RwLock::new(VecDeque::with_capacity(max_events.min(1000))),
281 max_events,
282 }
283 }
284
285 pub fn events(&self) -> Vec<AuditEvent> {
287 self.events
288 .read()
289 .expect("MemoryAuditSink RwLock poisoned")
290 .iter()
291 .cloned()
292 .collect()
293 }
294
295 pub fn count(&self) -> usize {
297 self.events
298 .read()
299 .expect("MemoryAuditSink RwLock poisoned")
300 .len()
301 }
302
303 pub fn clear(&self) {
305 self.events
306 .write()
307 .expect("MemoryAuditSink RwLock poisoned")
308 .clear();
309 }
310
311 pub fn find_by_type(&self, event_type: AuditEventType) -> Vec<AuditEvent> {
313 self.events
314 .read()
315 .expect("MemoryAuditSink RwLock poisoned")
316 .iter()
317 .filter(|e| e.event_type == event_type)
318 .cloned()
319 .collect()
320 }
321
322 pub fn find_by_plugin(&self, plugin: &str) -> Vec<AuditEvent> {
324 self.events
325 .read()
326 .expect("MemoryAuditSink RwLock poisoned")
327 .iter()
328 .filter(|e| e.plugin == plugin)
329 .cloned()
330 .collect()
331 }
332}
333
334impl Default for MemoryAuditSink {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340impl AuditSink for MemoryAuditSink {
341 fn record(&self, event: AuditEvent) -> Result<(), AuditError> {
342 let mut events = self
343 .events
344 .write()
345 .expect("MemoryAuditSink RwLock poisoned");
346 if events.len() >= self.max_events {
347 events.pop_front(); }
349 events.push_back(event);
350 Ok(())
351 }
352
353 fn flush(&self) -> Result<(), AuditError> {
354 Ok(())
355 }
356}
357
358impl fmt::Debug for MemoryAuditSink {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 f.debug_struct("MemoryAuditSink")
361 .field("count", &self.count())
362 .field("max_events", &self.max_events)
363 .finish()
364 }
365}
366
367#[derive(Debug, Default)]
369pub struct NullAuditSink;
370
371impl NullAuditSink {
372 pub fn new() -> Self {
373 Self
374 }
375}
376
377impl AuditSink for NullAuditSink {
378 fn record(&self, _event: AuditEvent) -> Result<(), AuditError> {
379 Ok(())
380 }
381
382 fn flush(&self) -> Result<(), AuditError> {
383 Ok(())
384 }
385}
386
387pub struct CompositeAuditSink {
389 sinks: Vec<Box<dyn AuditSink>>,
390}
391
392impl CompositeAuditSink {
393 pub fn new() -> Self {
394 Self { sinks: Vec::new() }
395 }
396
397 pub fn with_sink(mut self, sink: impl AuditSink + 'static) -> Self {
398 self.sinks.push(Box::new(sink));
399 self
400 }
401}
402
403impl Default for CompositeAuditSink {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409impl AuditSink for CompositeAuditSink {
410 fn record(&self, event: AuditEvent) -> Result<(), AuditError> {
411 for sink in &self.sinks {
412 sink.record(event.clone())?;
413 }
414 Ok(())
415 }
416
417 fn flush(&self) -> Result<(), AuditError> {
418 for sink in &self.sinks {
419 sink.flush()?;
420 }
421 Ok(())
422 }
423
424 fn is_healthy(&self) -> bool {
425 self.sinks.iter().all(|s| s.is_healthy())
426 }
427}
428
429impl fmt::Debug for CompositeAuditSink {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.debug_struct("CompositeAuditSink")
432 .field("sink_count", &self.sinks.len())
433 .finish()
434 }
435}
436
437pub fn permission_requested(plugin: &str, capabilities: &Capabilities) -> AuditEvent {
443 AuditEvent::new(
444 AuditEventType::PermissionRequested,
445 plugin,
446 AuditDetails::Permission {
447 trust_level: None,
448 reason: None,
449 capabilities_hash: capabilities.compute_hash(),
450 },
451 )
452}
453
454pub fn permission_granted(
456 plugin: &str,
457 capabilities: &Capabilities,
458 trust_level: TrustLevel,
459) -> AuditEvent {
460 AuditEvent::new(
461 AuditEventType::PermissionGranted,
462 plugin,
463 AuditDetails::Permission {
464 trust_level: Some(trust_level),
465 reason: None,
466 capabilities_hash: capabilities.compute_hash(),
467 },
468 )
469}
470
471pub fn permission_denied(plugin: &str, capabilities: &Capabilities, reason: &str) -> AuditEvent {
473 AuditEvent::new(
474 AuditEventType::PermissionDenied,
475 plugin,
476 AuditDetails::Permission {
477 trust_level: None,
478 reason: Some(reason.to_string()),
479 capabilities_hash: capabilities.compute_hash(),
480 },
481 )
482}
483
484pub fn escalation_detected(
486 plugin: &str,
487 old_caps: &Capabilities,
488 new_caps: &Capabilities,
489) -> AuditEvent {
490 AuditEvent::new(
491 AuditEventType::EscalationDetected,
492 plugin,
493 AuditDetails::Escalation {
494 old_hash: old_caps.compute_hash(),
495 new_hash: new_caps.compute_hash(),
496 },
497 )
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use sen_plugin_api::PathPattern;
504
505 #[test]
506 fn test_memory_sink() {
507 let sink = MemoryAuditSink::new();
508 let caps = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
509
510 let event = permission_requested("test-plugin", &caps);
511 sink.record(event).unwrap();
512
513 assert_eq!(sink.count(), 1);
514 let events = sink.find_by_type(AuditEventType::PermissionRequested);
515 assert_eq!(events.len(), 1);
516 assert_eq!(events[0].plugin, "test-plugin");
517 }
518
519 #[test]
520 fn test_memory_sink_eviction() {
521 let sink = MemoryAuditSink::with_capacity(2);
522 let caps = Capabilities::none();
523
524 for i in 0..3 {
525 let event = permission_requested(&format!("plugin-{}", i), &caps);
526 sink.record(event).unwrap();
527 }
528
529 assert_eq!(sink.count(), 2);
530 let events = sink.events();
531 assert_eq!(events[0].plugin, "plugin-1");
532 assert_eq!(events[1].plugin, "plugin-2");
533 }
534
535 #[test]
536 fn test_null_sink() {
537 let sink = NullAuditSink::new();
538 let caps = Capabilities::none();
539
540 let event = permission_requested("test", &caps);
541 assert!(sink.record(event).is_ok());
542 assert!(sink.flush().is_ok());
543 }
544
545 #[test]
546 fn test_composite_sink() {
547 let memory1 = MemoryAuditSink::new();
548 let memory2 = MemoryAuditSink::new();
549 let caps = Capabilities::none();
550
551 let event = permission_requested("test", &caps);
554 memory1.record(event.clone()).unwrap();
555 memory2.record(event).unwrap();
556
557 assert_eq!(memory1.count(), 1);
558 assert_eq!(memory2.count(), 1);
559 }
560
561 #[test]
562 fn test_event_serialization() {
563 let caps = Capabilities::default().with_fs_read(vec![PathPattern::new("./data")]);
564 let event =
565 permission_granted("test", &caps, TrustLevel::Permanent).with_command("data:export");
566
567 let json = serde_json::to_string(&event).unwrap();
568 assert!(json.contains("permission_granted"));
569 assert!(json.contains("test"));
570 assert!(json.contains("permanent"));
571 }
572
573 #[test]
574 fn test_file_sink() {
575 let dir = tempfile::tempdir().unwrap();
576 let path = dir.path().join("audit.jsonl");
577
578 let sink = FileAuditSink::new(&path).unwrap();
579 let caps = Capabilities::none();
580
581 let event = permission_requested("test", &caps);
582 sink.record(event).unwrap();
583 sink.flush().unwrap();
584
585 let content = std::fs::read_to_string(&path).unwrap();
586 assert!(content.contains("permission_requested"));
587 }
588}