1212
1313use chrono:: { DateTime , Utc } ;
1414use serde:: { Deserialize , Serialize } ;
15+ use std:: borrow:: Cow ;
1516use std:: cmp:: Ordering ;
1617use std:: fmt;
1718use uuid:: Uuid ;
1819
20+ use crate :: redaction:: { redact_details, redact_optional_string, redact_string} ;
21+
1922/// Default retention period for audit logs in days.
2023pub const DEFAULT_AUDIT_RETENTION_DAYS : i64 = 90 ;
2124
@@ -112,7 +115,11 @@ pub enum AuditEventType {
112115 RepoCreated ,
113116 RepoDeleted ,
114117 MirrorCreated ,
118+ MirrorDeleted ,
115119 MirrorSynced ,
120+ WebhookCreated ,
121+ WebhookUpdated ,
122+ WebhookDeleted ,
116123 WebhookReceived ,
117124
118125 // SCIM events
@@ -274,7 +281,11 @@ impl fmt::Display for AuditEventType {
274281 AuditEventType :: RepoCreated => "repo_created" ,
275282 AuditEventType :: RepoDeleted => "repo_deleted" ,
276283 AuditEventType :: MirrorCreated => "mirror_created" ,
284+ AuditEventType :: MirrorDeleted => "mirror_deleted" ,
277285 AuditEventType :: MirrorSynced => "mirror_synced" ,
286+ AuditEventType :: WebhookCreated => "webhook_created" ,
287+ AuditEventType :: WebhookUpdated => "webhook_updated" ,
288+ AuditEventType :: WebhookDeleted => "webhook_deleted" ,
278289 AuditEventType :: WebhookReceived => "webhook_received" ,
279290
280291 // User management events
@@ -401,6 +412,8 @@ impl AuditEventType {
401412 | AuditEventType :: RepoCreated
402413 | AuditEventType :: MirrorCreated
403414 | AuditEventType :: MirrorSynced
415+ | AuditEventType :: WebhookCreated
416+ | AuditEventType :: WebhookUpdated
404417 | AuditEventType :: WebhookReceived
405418 | AuditEventType :: ScimUserCreated
406419 | AuditEventType :: ScimUserUpdated
@@ -469,6 +482,8 @@ impl AuditEventType {
469482 | AuditEventType :: UserRestored
470483 | AuditEventType :: WeaverDeleted
471484 | AuditEventType :: RepoDeleted
485+ | AuditEventType :: MirrorDeleted
486+ | AuditEventType :: WebhookDeleted
472487 | AuditEventType :: ScimUserDeleted
473488 | AuditEventType :: ScimUserDeprovisioned
474489 | AuditEventType :: ScimGroupDeleted
@@ -767,7 +782,28 @@ impl AuditLogBuilder {
767782 }
768783
769784 /// Build the audit log entry.
770- pub fn build ( self ) -> AuditLogEntry {
785+ ///
786+ /// Automatically redacts secrets from sensitive fields using `loom-redact` patterns.
787+ /// The following fields are redacted:
788+ /// - `details`: All string values in the JSON structure
789+ /// - `resource_id`: May contain identifiers that could be secrets
790+ /// - `action`: Custom action descriptions may contain secrets
791+ /// - `user_agent`: Unlikely but could contain leaked tokens
792+ pub fn build ( mut self ) -> AuditLogEntry {
793+ // Redact secrets from fields that may contain sensitive data
794+ let details = redact_details ( & self . details ) ;
795+ redact_optional_string ( & mut self . resource_id ) ;
796+ redact_optional_string ( & mut self . user_agent ) ;
797+
798+ // Redact action if custom, otherwise use event type display
799+ let action = match self . action {
800+ Some ( ref a) => match redact_string ( a) {
801+ Cow :: Borrowed ( s) => s. to_string ( ) ,
802+ Cow :: Owned ( s) => s,
803+ } ,
804+ None => self . event_type . to_string ( ) ,
805+ } ;
806+
771807 AuditLogEntry {
772808 id : Uuid :: new_v4 ( ) ,
773809 timestamp : Utc :: now ( ) ,
@@ -779,10 +815,10 @@ impl AuditLogBuilder {
779815 impersonating_user_id : self . impersonating_user_id ,
780816 resource_type : self . resource_type ,
781817 resource_id : self . resource_id ,
782- action : self . action . unwrap_or_else ( || self . event_type . to_string ( ) ) ,
818+ action,
783819 ip_address : self . ip_address ,
784820 user_agent : self . user_agent ,
785- details : self . details ,
821+ details,
786822 trace_id : self . trace_id ,
787823 span_id : self . span_id ,
788824 request_id : self . request_id ,
@@ -848,7 +884,7 @@ mod tests {
848884 assert_eq ! ( event, AuditEventType :: AccessDenied ) ;
849885 }
850886
851- const ALL_EVENT_TYPES : [ AuditEventType ; 85 ] = [
887+ const ALL_EVENT_TYPES : [ AuditEventType ; 89 ] = [
852888 AuditEventType :: Login ,
853889 AuditEventType :: Logout ,
854890 AuditEventType :: LoginFailed ,
@@ -896,7 +932,11 @@ mod tests {
896932 AuditEventType :: RepoCreated ,
897933 AuditEventType :: RepoDeleted ,
898934 AuditEventType :: MirrorCreated ,
935+ AuditEventType :: MirrorDeleted ,
899936 AuditEventType :: MirrorSynced ,
937+ AuditEventType :: WebhookCreated ,
938+ AuditEventType :: WebhookUpdated ,
939+ AuditEventType :: WebhookDeleted ,
900940 AuditEventType :: WebhookReceived ,
901941 // Feature flag events
902942 AuditEventType :: FlagCreated ,
@@ -1488,6 +1528,89 @@ mod tests {
14881528 assert_eq ! ( entry. actor_user_id, Some ( target_user) ) ;
14891529 assert_eq ! ( entry. impersonating_user_id, Some ( admin_user) ) ;
14901530 }
1531+
1532+ fn github_pat ( ) -> String {
1533+ format ! ( "ghp_{}" , "A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8" )
1534+ }
1535+
1536+ #[ test]
1537+ fn redacts_secrets_in_details ( ) {
1538+ let entry = AuditLogBuilder :: new ( AuditEventType :: Login )
1539+ . details ( json ! ( {
1540+ "token" : format!( "Bearer {}" , github_pat( ) ) ,
1541+ "user" : "test@example.com"
1542+ } ) )
1543+ . build ( ) ;
1544+
1545+ let token_val = entry. details [ "token" ] . as_str ( ) . unwrap ( ) ;
1546+ assert ! (
1547+ token_val. contains( "[REDACTED:" ) ,
1548+ "Expected redaction in details: {}" ,
1549+ token_val
1550+ ) ;
1551+ assert ! ( !token_val. contains( & github_pat( ) ) ) ;
1552+ assert_eq ! ( entry. details[ "user" ] , "test@example.com" ) ;
1553+ }
1554+
1555+ #[ test]
1556+ fn redacts_secrets_in_action ( ) {
1557+ let entry = AuditLogBuilder :: new ( AuditEventType :: Login )
1558+ . action ( format ! ( "User logged in with token {}" , github_pat( ) ) )
1559+ . build ( ) ;
1560+
1561+ assert ! (
1562+ entry. action. contains( "[REDACTED:" ) ,
1563+ "Expected redaction in action: {}" ,
1564+ entry. action
1565+ ) ;
1566+ assert ! ( !entry. action. contains( & github_pat( ) ) ) ;
1567+ }
1568+
1569+ #[ test]
1570+ fn redacts_secrets_in_resource_id ( ) {
1571+ let entry = AuditLogBuilder :: new ( AuditEventType :: ApiKeyUsed )
1572+ . resource ( "api_key" , github_pat ( ) )
1573+ . build ( ) ;
1574+
1575+ let resource_id = entry. resource_id . as_ref ( ) . unwrap ( ) ;
1576+ assert ! (
1577+ resource_id. contains( "[REDACTED:" ) ,
1578+ "Expected redaction in resource_id: {}" ,
1579+ resource_id
1580+ ) ;
1581+ assert ! ( !resource_id. contains( & github_pat( ) ) ) ;
1582+ }
1583+
1584+ #[ test]
1585+ fn redacts_secrets_in_user_agent ( ) {
1586+ let entry = AuditLogBuilder :: new ( AuditEventType :: Login )
1587+ . user_agent ( format ! ( "CustomClient/1.0 token={}" , github_pat( ) ) )
1588+ . build ( ) ;
1589+
1590+ let user_agent = entry. user_agent . as_ref ( ) . unwrap ( ) ;
1591+ assert ! (
1592+ user_agent. contains( "[REDACTED:" ) ,
1593+ "Expected redaction in user_agent: {}" ,
1594+ user_agent
1595+ ) ;
1596+ assert ! ( !user_agent. contains( & github_pat( ) ) ) ;
1597+ }
1598+
1599+ #[ test]
1600+ fn preserves_non_secret_values ( ) {
1601+ let entry = AuditLogBuilder :: new ( AuditEventType :: Login )
1602+ . action ( "User logged in successfully" )
1603+ . resource ( "session" , "sess-12345" )
1604+ . user_agent ( "Mozilla/5.0" )
1605+ . details ( json ! ( { "ip" : "192.168.1.1" , "method" : "password" } ) )
1606+ . build ( ) ;
1607+
1608+ assert_eq ! ( entry. action, "User logged in successfully" ) ;
1609+ assert_eq ! ( entry. resource_id, Some ( "sess-12345" . to_string( ) ) ) ;
1610+ assert_eq ! ( entry. user_agent, Some ( "Mozilla/5.0" . to_string( ) ) ) ;
1611+ assert_eq ! ( entry. details[ "ip" ] , "192.168.1.1" ) ;
1612+ assert_eq ! ( entry. details[ "method" ] , "password" ) ;
1613+ }
14911614 }
14921615
14931616 mod constants {
0 commit comments