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 #[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: String,
29 ttl_seconds: u64,
30 budget: Option<f64>,
31 allowed_actions: Vec<String>,
32 command: Vec<String>,
33 #[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 ResourceCreated {
58 service: String,
59 resource_type: String,
60 identifier: String,
61 },
62}
63
64pub struct AuditLog {
66 path: PathBuf,
67}
68
69impl AuditLog {
70 pub fn new() -> Result<Self> {
71 let dir = dirs::data_local_dir()
72 .unwrap_or_else(|| PathBuf::from("."))
73 .join("audex")
74 .join("audit");
75 std::fs::create_dir_all(&dir)?;
76 let path = dir.join("audit.jsonl");
77 Ok(Self { path })
78 }
79
80 pub fn with_path(path: PathBuf) -> Result<Self> {
83 if let Some(parent) = path.parent() {
84 std::fs::create_dir_all(parent)?;
85 }
86 Ok(Self { path })
87 }
88
89 pub fn log(&self, session_id: &str, event: AuditEvent) -> Result<()> {
90 self.log_with_provider(session_id, "aws", event)
91 }
92
93 pub fn log_with_provider(
94 &self,
95 session_id: &str,
96 provider: &str,
97 event: AuditEvent,
98 ) -> Result<()> {
99 let entry = AuditEntry {
100 timestamp: Utc::now(),
101 session_id: session_id.to_string(),
102 provider: provider.to_string(),
103 event,
104 };
105 let json_line = serde_json::to_string(&entry)?;
106
107 let json_line = crate::leakdetect::redact_secrets(&json_line);
109
110 use fs4::fs_std::FileExt;
116 use std::io::Write;
117 let mut file = std::fs::OpenOptions::new()
118 .create(true)
119 .append(true)
120 .read(true)
121 .open(&self.path)?;
122 file.lock_exclusive()?;
123
124 let previous_hash = crate::integrity::last_chain_hash(&self.path);
126 let signed_line = crate::integrity::sign_line(&json_line, &previous_hash);
127
128 let mut output = signed_line;
129 output.push('\n');
130
131 let write_result = file.write_all(output.as_bytes());
132 let _ = FileExt::unlock(&file);
134 write_result?;
135
136 Ok(())
137 }
138
139 pub fn log_session_created(&self, session: &Session) -> Result<()> {
140 let allowed_actions: Vec<String> = match session.provider {
141 CloudProvider::Gcp => session
142 .policy
143 .actions
144 .iter()
145 .map(|a| a.to_gcp_permission())
146 .collect(),
147 CloudProvider::Azure => session
148 .policy
149 .actions
150 .iter()
151 .map(|a| a.to_azure_permission())
152 .collect(),
153 CloudProvider::Aws => session
154 .policy
155 .actions
156 .iter()
157 .map(|a| a.to_iam_action())
158 .collect(),
159 };
160
161 self.log_with_provider(
162 &session.id,
163 &session.provider.to_string(),
164 AuditEvent::SessionCreated {
165 role_arn: session.role_arn.clone(),
166 ttl_seconds: session.ttl_seconds,
167 budget: session.budget,
168 allowed_actions,
169 command: session.command.clone(),
170 agent_id: session.agent_id.clone(),
171 },
172 )
173 }
174
175 pub fn log_session_ended(&self, session: &Session, exit_code: Option<i32>) -> Result<()> {
176 let duration = (Utc::now() - session.created_at).num_seconds();
177 self.log_with_provider(
178 &session.id,
179 &session.provider.to_string(),
180 AuditEvent::SessionEnded {
181 status: session.status.to_string(),
182 duration_seconds: duration,
183 exit_code,
184 },
185 )
186 }
187
188 pub fn read(&self, session_id: Option<&str>) -> Result<Vec<AuditEntry>> {
190 self.read_filtered(session_id, None)
191 }
192
193 pub fn read_filtered(
195 &self,
196 session_id: Option<&str>,
197 provider: Option<&str>,
198 ) -> Result<Vec<AuditEntry>> {
199 if !self.path.exists() {
200 return Ok(Vec::new());
201 }
202 let content = std::fs::read_to_string(&self.path)?;
203 let entries: Vec<AuditEntry> = content
204 .lines()
205 .filter(|l| !l.trim().is_empty())
206 .filter_map(|l| {
207 let (json_content, _) = crate::integrity::parse_line(l);
209 serde_json::from_str(json_content).ok()
210 })
211 .filter(|e: &AuditEntry| session_id.is_none_or(|id| e.session_id == id))
212 .filter(|e: &AuditEntry| provider.is_none_or(|p| e.provider == p))
213 .collect();
214 Ok(entries)
215 }
216
217 pub fn verify(&self) -> Result<crate::integrity::VerificationResult> {
219 crate::integrity::verify_audit_log(&self.path)
220 }
221
222 pub fn path(&self) -> &PathBuf {
223 &self.path
224 }
225}