@@ -14,19 +14,30 @@ use std::time::{SystemTime, UNIX_EPOCH};
1414/// creating a tamper-evident chain from the genesis entry.
1515pub struct AuditLogger {
1616 entries : Mutex < Vec < AuditEntry > > ,
17+ max_entries : Option < usize > ,
1718}
1819
1920impl AuditLogger {
20- /// Create an empty audit logger.
21+ /// Create an empty audit logger with no entry limit .
2122 pub fn new ( ) -> Self {
2223 Self {
2324 entries : Mutex :: new ( Vec :: new ( ) ) ,
25+ max_entries : None ,
26+ }
27+ }
28+
29+ /// Create an audit logger that retains at most `max` entries,
30+ /// evicting the oldest when the limit is exceeded.
31+ pub fn with_max_entries ( max : usize ) -> Self {
32+ Self {
33+ entries : Mutex :: new ( Vec :: new ( ) ) ,
34+ max_entries : Some ( max) ,
2435 }
2536 }
2637
2738 /// Append a new entry to the audit chain and return it.
2839 pub fn log ( & self , agent_id : & str , action : & str , decision : & str ) -> AuditEntry {
29- let mut entries = self . entries . lock ( ) . unwrap ( ) ;
40+ let mut entries = self . entries . lock ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) ;
3041 let seq = entries. len ( ) as u64 ;
3142 let prev_hash = entries
3243 . last ( )
@@ -51,6 +62,15 @@ impl AuditLogger {
5162 } ;
5263
5364 entries. push ( entry. clone ( ) ) ;
65+
66+ // Evict oldest entries when the retention limit is exceeded.
67+ if let Some ( max) = self . max_entries {
68+ if entries. len ( ) > max {
69+ let overflow = entries. len ( ) - max;
70+ entries. drain ( ..overflow) ;
71+ }
72+ }
73+
5474 entry
5575 }
5676
@@ -59,7 +79,7 @@ impl AuditLogger {
5979 /// Returns `true` if every entry's hash is correct and linked to the
6080 /// previous entry's hash.
6181 pub fn verify ( & self ) -> bool {
62- let entries = self . entries . lock ( ) . unwrap ( ) ;
82+ let entries = self . entries . lock ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) ;
6383 for ( i, entry) in entries. iter ( ) . enumerate ( ) {
6484 let expected_prev = if i == 0 {
6585 String :: new ( )
@@ -87,14 +107,23 @@ impl AuditLogger {
87107
88108 /// Return all audit entries.
89109 pub fn entries ( & self ) -> Vec < AuditEntry > {
90- self . entries . lock ( ) . unwrap ( ) . clone ( )
110+ self . entries
111+ . lock ( )
112+ . unwrap_or_else ( |e| e. into_inner ( ) )
113+ . clone ( )
114+ }
115+
116+ /// Serialise all audit entries to a JSON string.
117+ pub fn export_json ( & self ) -> String {
118+ let entries = self . entries . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
119+ serde_json:: to_string ( & * entries) . unwrap_or_else ( |_| "[]" . to_string ( ) )
91120 }
92121
93122 /// Return entries matching the given filter.
94123 pub fn get_entries ( & self , filter : & AuditFilter ) -> Vec < AuditEntry > {
95124 self . entries
96125 . lock ( )
97- . unwrap ( )
126+ . unwrap_or_else ( |e| e . into_inner ( ) )
98127 . iter ( )
99128 . filter ( |e| {
100129 if let Some ( ref id) = filter. agent_id {
@@ -240,4 +269,54 @@ mod tests {
240269 "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
241270 ) ;
242271 }
272+
273+ #[ test]
274+ fn test_export_json ( ) {
275+ let logger = AuditLogger :: new ( ) ;
276+ logger. log ( "agent-1" , "data.read" , "allow" ) ;
277+ logger. log ( "agent-2" , "shell:rm" , "deny" ) ;
278+ let json = logger. export_json ( ) ;
279+ let parsed: Vec < AuditEntry > = serde_json:: from_str ( & json) . unwrap ( ) ;
280+ assert_eq ! ( parsed. len( ) , 2 ) ;
281+ assert_eq ! ( parsed[ 0 ] . agent_id, "agent-1" ) ;
282+ assert_eq ! ( parsed[ 1 ] . agent_id, "agent-2" ) ;
283+ }
284+
285+ #[ test]
286+ fn test_export_json_empty ( ) {
287+ let logger = AuditLogger :: new ( ) ;
288+ let json = logger. export_json ( ) ;
289+ assert_eq ! ( json, "[]" ) ;
290+ }
291+
292+ #[ test]
293+ fn test_max_entries_eviction ( ) {
294+ let logger = AuditLogger :: with_max_entries ( 3 ) ;
295+ for i in 0 ..5 {
296+ logger. log ( "agent" , & format ! ( "action-{}" , i) , "allow" ) ;
297+ }
298+ let entries = logger. entries ( ) ;
299+ assert_eq ! ( entries. len( ) , 3 ) ;
300+ // Oldest entries (action-0, action-1) should have been evicted
301+ assert_eq ! ( entries[ 0 ] . action, "action-2" ) ;
302+ assert_eq ! ( entries[ 1 ] . action, "action-3" ) ;
303+ assert_eq ! ( entries[ 2 ] . action, "action-4" ) ;
304+ }
305+
306+ #[ test]
307+ fn test_max_entries_not_exceeded ( ) {
308+ let logger = AuditLogger :: with_max_entries ( 10 ) ;
309+ logger. log ( "a" , "x" , "allow" ) ;
310+ logger. log ( "b" , "y" , "deny" ) ;
311+ assert_eq ! ( logger. entries( ) . len( ) , 2 ) ;
312+ }
313+
314+ #[ test]
315+ fn test_no_limit_grows_unbounded ( ) {
316+ let logger = AuditLogger :: new ( ) ;
317+ for i in 0 ..100 {
318+ logger. log ( "a" , & format ! ( "act-{}" , i) , "allow" ) ;
319+ }
320+ assert_eq ! ( logger. entries( ) . len( ) , 100 ) ;
321+ }
243322}
0 commit comments