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}
55
56pub 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 let json_line = crate::leakdetect::redact_secrets(&json_line);
87
88 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 pub fn read(&self, session_id: Option<&str>) -> Result<Vec<AuditEntry>> {
145 self.read_filtered(session_id, None)
146 }
147
148 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 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 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}