Skip to main content

tryaudex_core/
audit.rs

1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::error::Result;
7use crate::session::{CloudProvider, Session};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct AuditEntry {
11    pub timestamp: DateTime<Utc>,
12    pub session_id: String,
13    /// Cloud provider for this entry (defaults to "aws" for backwards compat).
14    #[serde(default = "default_provider")]
15    pub provider: String,
16    pub event: AuditEvent,
17}
18
19fn default_provider() -> String {
20    "aws".to_string()
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum AuditEvent {
26    SessionCreated {
27        /// "Role ARN" for AWS, "Service Account" for GCP, "Subscription" for Azure
28        role_arn: String,
29        ttl_seconds: u64,
30        budget: Option<f64>,
31        allowed_actions: Vec<String>,
32        command: Vec<String>,
33        /// AI agent/model identity (from AUDEX_AGENT_ID env var).
34        #[serde(default, skip_serializing_if = "Option::is_none")]
35        agent_id: Option<String>,
36    },
37    CredentialsIssued {
38        access_key_id: String,
39        expires_at: DateTime<Utc>,
40    },
41    SessionEnded {
42        status: String,
43        duration_seconds: i64,
44        exit_code: Option<i32>,
45    },
46    BudgetWarning {
47        current_spend: f64,
48        limit: f64,
49    },
50    BudgetExceeded {
51        final_spend: f64,
52        limit: f64,
53    },
54}
55
56/// Append-only audit logger that writes to a local JSONL file.
57pub struct AuditLog {
58    path: PathBuf,
59}
60
61impl AuditLog {
62    pub fn new() -> Result<Self> {
63        let dir = dirs::data_local_dir()
64            .unwrap_or_else(|| PathBuf::from("."))
65            .join("audex")
66            .join("audit");
67        std::fs::create_dir_all(&dir)?;
68        let path = dir.join("audit.jsonl");
69        Ok(Self { path })
70    }
71
72    pub fn log(&self, session_id: &str, event: AuditEvent) -> Result<()> {
73        self.log_with_provider(session_id, "aws", event)
74    }
75
76    pub fn log_with_provider(&self, session_id: &str, provider: &str, event: AuditEvent) -> Result<()> {
77        let entry = AuditEntry {
78            timestamp: Utc::now(),
79            session_id: session_id.to_string(),
80            provider: provider.to_string(),
81            event,
82        };
83        let json_line = serde_json::to_string(&entry)?;
84
85        // Redact any credential material before persisting
86        let json_line = crate::leakdetect::redact_secrets(&json_line);
87
88        // Sign with chain HMAC for tamper detection
89        let previous_hash = crate::integrity::last_chain_hash(&self.path);
90        let signed_line = crate::integrity::sign_line(&json_line, &previous_hash);
91
92        let mut output = signed_line;
93        output.push('\n');
94
95        use std::io::Write;
96        let mut file = std::fs::OpenOptions::new()
97            .create(true)
98            .append(true)
99            .open(&self.path)?;
100        file.write_all(output.as_bytes())?;
101
102        Ok(())
103    }
104
105    pub fn log_session_created(&self, session: &Session) -> Result<()> {
106        let allowed_actions: Vec<String> = match session.provider {
107            CloudProvider::Gcp => session.policy.actions.iter().map(|a| a.to_gcp_permission()).collect(),
108            CloudProvider::Azure => session.policy.actions.iter().map(|a| a.to_azure_permission()).collect(),
109            CloudProvider::Aws => session.policy.actions.iter().map(|a| a.to_iam_action()).collect(),
110        };
111
112        self.log_with_provider(
113            &session.id,
114            &session.provider.to_string(),
115            AuditEvent::SessionCreated {
116                role_arn: session.role_arn.clone(),
117                ttl_seconds: session.ttl_seconds,
118                budget: session.budget,
119                allowed_actions,
120                command: session.command.clone(),
121                agent_id: session.agent_id.clone(),
122            },
123        )
124    }
125
126    pub fn log_session_ended(
127        &self,
128        session: &Session,
129        exit_code: Option<i32>,
130    ) -> Result<()> {
131        let duration = (Utc::now() - session.created_at).num_seconds();
132        self.log_with_provider(
133            &session.id,
134            &session.provider.to_string(),
135            AuditEvent::SessionEnded {
136                status: session.status.to_string(),
137                duration_seconds: duration,
138                exit_code,
139            },
140        )
141    }
142
143    /// Read all audit entries, optionally filtered by session ID.
144    pub fn read(&self, session_id: Option<&str>) -> Result<Vec<AuditEntry>> {
145        self.read_filtered(session_id, None)
146    }
147
148    /// Read audit entries with optional session ID and provider filters.
149    pub fn read_filtered(&self, session_id: Option<&str>, provider: Option<&str>) -> Result<Vec<AuditEntry>> {
150        if !self.path.exists() {
151            return Ok(Vec::new());
152        }
153        let content = std::fs::read_to_string(&self.path)?;
154        let entries: Vec<AuditEntry> = content
155            .lines()
156            .filter(|l| !l.trim().is_empty())
157            .filter_map(|l| {
158                // Strip HMAC signature if present before parsing JSON
159                let (json_content, _) = crate::integrity::parse_line(l);
160                serde_json::from_str(json_content).ok()
161            })
162            .filter(|e: &AuditEntry| {
163                session_id.is_none_or(|id| e.session_id == id)
164            })
165            .filter(|e: &AuditEntry| {
166                provider.is_none_or(|p| e.provider == p)
167            })
168            .collect();
169        Ok(entries)
170    }
171
172    /// Verify the integrity of the audit log using chain HMACs.
173    pub fn verify(&self) -> Result<crate::integrity::VerificationResult> {
174        crate::integrity::verify_audit_log(&self.path)
175    }
176
177    pub fn path(&self) -> &PathBuf {
178        &self.path
179    }
180}