1use crate::types::*;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum AuditAction {
19 Read,
21 Write,
23 Delete,
25 Invoke,
27}
28
29impl fmt::Display for AuditAction {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Read => write!(f, "read"),
33 Self::Write => write!(f, "write"),
34 Self::Delete => write!(f, "delete"),
35 Self::Invoke => write!(f, "invoke"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AuditEntry {
47 pub caller: String,
49 pub target: String,
51 pub action: AuditAction,
53 pub tool: String,
55 pub evidence_class: EvidenceClass,
57 pub timestamp: String,
59 pub duration_ms: u64,
61 pub success: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AuditLedger {
72 entries: Vec<AuditEntry>,
73 #[serde(skip)]
74 caller_index: HashMap<String, Vec<usize>>,
75 #[serde(skip)]
76 target_index: HashMap<String, Vec<usize>>,
77 #[serde(skip)]
78 tool_index: HashMap<String, Vec<usize>>,
79}
80
81impl Default for AuditLedger {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87impl AuditLedger {
88 pub fn new() -> Self {
90 Self {
91 entries: Vec::new(),
92 caller_index: HashMap::new(),
93 target_index: HashMap::new(),
94 tool_index: HashMap::new(),
95 }
96 }
97
98 pub fn rebuild_indexes(&mut self) {
102 self.caller_index.clear();
103 self.target_index.clear();
104 self.tool_index.clear();
105 for (i, entry) in self.entries.iter().enumerate() {
106 self.caller_index
107 .entry(entry.caller.clone())
108 .or_default()
109 .push(i);
110 self.target_index
111 .entry(entry.target.clone())
112 .or_default()
113 .push(i);
114 self.tool_index
115 .entry(entry.tool.clone())
116 .or_default()
117 .push(i);
118 }
119 }
120
121 #[allow(clippy::too_many_arguments)]
123 pub fn record(
124 &mut self,
125 caller: &str,
126 target: &str,
127 action: AuditAction,
128 tool: &str,
129 evidence_class: EvidenceClass,
130 duration_ms: u64,
131 success: bool,
132 ) -> &AuditEntry {
133 let idx = self.entries.len();
134 let entry = AuditEntry {
135 caller: caller.to_string(),
136 target: target.to_string(),
137 action,
138 tool: tool.to_string(),
139 evidence_class,
140 timestamp: chrono::Utc::now().to_rfc3339(),
141 duration_ms,
142 success,
143 };
144 self.entries.push(entry);
145
146 self.caller_index
147 .entry(caller.to_string())
148 .or_default()
149 .push(idx);
150 self.target_index
151 .entry(target.to_string())
152 .or_default()
153 .push(idx);
154 self.tool_index
155 .entry(tool.to_string())
156 .or_default()
157 .push(idx);
158
159 &self.entries[idx]
160 }
161
162 pub fn query_caller(&self, name: &str) -> Vec<&AuditEntry> {
164 self.caller_index
165 .get(name)
166 .map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
167 .unwrap_or_default()
168 }
169
170 pub fn query_target(&self, resource: &str) -> Vec<&AuditEntry> {
172 self.target_index
173 .get(resource)
174 .map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
175 .unwrap_or_default()
176 }
177
178 pub fn query_tool(&self, tool: &str) -> Vec<&AuditEntry> {
180 self.tool_index
181 .get(tool)
182 .map(|indices| indices.iter().map(|&i| &self.entries[i]).collect())
183 .unwrap_or_default()
184 }
185
186 pub fn hot_targets(&self, n: usize) -> Vec<(&str, usize)> {
188 let mut counts: Vec<(&str, usize)> = self
189 .target_index
190 .iter()
191 .map(|(k, v)| (k.as_str(), v.len()))
192 .collect();
193 counts.sort_by(|a, b| b.1.cmp(&a.1));
194 counts.truncate(n);
195 counts
196 }
197
198 pub fn active_callers(&self, n: usize) -> Vec<(&str, usize)> {
200 let mut counts: Vec<(&str, usize)> = self
201 .caller_index
202 .iter()
203 .map(|(k, v)| (k.as_str(), v.len()))
204 .collect();
205 counts.sort_by(|a, b| b.1.cmp(&a.1));
206 counts.truncate(n);
207 counts
208 }
209
210 pub fn cross_reference(&self, caller: &str, target: &str) -> Vec<&AuditEntry> {
212 let caller_indices = match self.caller_index.get(caller) {
213 Some(v) => v,
214 None => return Vec::new(),
215 };
216 let target_indices = match self.target_index.get(target) {
217 Some(v) => v,
218 None => return Vec::new(),
219 };
220
221 let mut result = Vec::new();
223 let (mut ci, mut ti) = (0, 0);
224 while ci < caller_indices.len() && ti < target_indices.len() {
225 match caller_indices[ci].cmp(&target_indices[ti]) {
226 std::cmp::Ordering::Equal => {
227 result.push(&self.entries[caller_indices[ci]]);
228 ci += 1;
229 ti += 1;
230 }
231 std::cmp::Ordering::Less => ci += 1,
232 std::cmp::Ordering::Greater => ti += 1,
233 }
234 }
235 result
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn record_and_query() {
245 let mut ledger = AuditLedger::new();
246 ledger.record(
247 "agent-a",
248 "file.rs",
249 AuditAction::Read,
250 "Read",
251 EvidenceClass::Direct,
252 42,
253 true,
254 );
255 ledger.record(
256 "agent-b",
257 "file.rs",
258 AuditAction::Write,
259 "Edit",
260 EvidenceClass::Inferred,
261 100,
262 true,
263 );
264 ledger.record(
265 "agent-a",
266 "config.toml",
267 AuditAction::Read,
268 "Read",
269 EvidenceClass::Direct,
270 10,
271 true,
272 );
273
274 assert_eq!(ledger.query_caller("agent-a").len(), 2);
275 assert_eq!(ledger.query_target("file.rs").len(), 2);
276 assert_eq!(ledger.query_tool("Read").len(), 2);
277 assert_eq!(ledger.cross_reference("agent-a", "file.rs").len(), 1);
278
279 let hot = ledger.hot_targets(1);
280 assert_eq!(hot[0].0, "file.rs");
281 assert_eq!(hot[0].1, 2);
282
283 let active = ledger.active_callers(1);
284 assert_eq!(active[0].0, "agent-a");
285 assert_eq!(active[0].1, 2);
286 }
287}