use std::collections::BTreeSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use theway_contract::extension::{
ExtensionAuditEvent, ExtensionAuditOperation, ExtensionAuditOutcome, ExtensionPermission,
};
#[derive(Clone)]
pub struct ExtensionAuditLog {
path: PathBuf,
events: Arc<parking_lot::Mutex<Vec<ExtensionAuditEvent>>>,
writer: Arc<parking_lot::Mutex<()>>,
}
impl ExtensionAuditLog {
pub fn for_base(base: &Path) -> Self {
Self {
path: base.join("extensions").join("audit.jsonl"),
events: Arc::new(parking_lot::Mutex::new(Vec::new())),
writer: Arc::new(parking_lot::Mutex::new(())),
}
}
pub fn events(&self) -> Vec<ExtensionAuditEvent> {
self.events.lock().clone()
}
pub(super) fn record(
&self,
extension_id: impl Into<String>,
session_id: Option<String>,
operation: ExtensionAuditOperation,
outcome: ExtensionAuditOutcome,
capability: Option<ExtensionPermission>,
target: Option<&str>,
redacted_fields: impl IntoIterator<Item = String>,
) {
let target =
target.map(|value| crate::bug_report::redact(value).chars().take(160).collect());
let event = ExtensionAuditEvent {
timestamp: chrono::Utc::now().to_rfc3339(),
extension_id: extension_id.into(),
session_id,
operation,
outcome,
capability,
target,
redacted_fields: redacted_fields.into_iter().collect::<BTreeSet<_>>(),
};
self.events.lock().push(event.clone());
let _writer = self.writer.lock();
let Some(parent) = self.path.parent() else {
return;
};
if std::fs::create_dir_all(parent).is_err() {
return;
}
let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
else {
return;
};
if let Ok(serialized) = serde_json::to_string(&event) {
let _ = writeln!(file, "{serialized}");
}
}
}
#[cfg(test)]
mod coverage_gap {
use super::*;
#[test]
fn record_handles_missing_parent_and_unwritable_paths() {
let log = ExtensionAuditLog {
path: PathBuf::from("audit.jsonl"),
events: Arc::new(parking_lot::Mutex::new(Vec::new())),
writer: Arc::new(parking_lot::Mutex::new(())),
};
log.record(
"ext",
Some("sess".into()),
ExtensionAuditOperation::TrustChanged,
ExtensionAuditOutcome::Allowed,
None,
None,
std::iter::empty(),
);
let base = tempfile::tempdir().unwrap();
std::fs::write(base.path().join("extensions"), "not a directory").unwrap();
let log = ExtensionAuditLog::for_base(base.path());
log.record(
"ext",
Some("sess".into()),
ExtensionAuditOperation::TrustChanged,
ExtensionAuditOutcome::Allowed,
None,
None,
std::iter::empty(),
);
let base = tempfile::tempdir().unwrap();
std::fs::create_dir_all(base.path().join("extensions/audit.jsonl")).unwrap();
let log = ExtensionAuditLog::for_base(base.path());
log.record(
"ext",
Some("sess".into()),
ExtensionAuditOperation::TrustChanged,
ExtensionAuditOutcome::Allowed,
None,
None,
std::iter::empty(),
);
}
}