1+ package audit
2+
3+ import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "sync"
11+ "time"
12+
13+ "github.com/hyperledger/firefly-common/pkg/fftypes"
14+ "github.com/hyperledger/firefly-common/pkg/log"
15+ )
16+
17+ const (
18+ maxLogSize = 100 * 1024 * 1024 // 100MB
19+ maxLogAge = 30 * 24 * time .Hour // 30 days
20+ maxLogBackups = 10
21+ )
22+
23+ // AuditEvent represents a security audit event
24+ type AuditEvent struct {
25+ Timestamp time.Time `json:"timestamp"`
26+ EventType string `json:"eventType"`
27+ UserID string `json:"userId"`
28+ Action string `json:"action"`
29+ Resource string `json:"resource"`
30+ IPAddress string `json:"ipAddress"`
31+ UserAgent string `json:"userAgent"`
32+ Details map [string ]interface {} `json:"details"`
33+ Status string `json:"status"`
34+ Error string `json:"error,omitempty"`
35+ }
36+
37+ // AuditLogger handles security audit logging
38+ type AuditLogger struct {
39+ mu sync.RWMutex
40+ logFile * os.File
41+ basePath string
42+ size int64
43+ }
44+
45+ // NewAuditLogger creates a new audit logger
46+ func NewAuditLogger (basePath string ) (* AuditLogger , error ) {
47+ if err := os .MkdirAll (basePath , 0700 ); err != nil {
48+ return nil , fmt .Errorf ("failed to create audit log directory: %v" , err )
49+ }
50+
51+ // Create a new log file with timestamp
52+ timestamp := time .Now ().Format ("2006-01-02" )
53+ logPath := filepath .Join (basePath , fmt .Sprintf ("audit-%s.log" , timestamp ))
54+ logFile , err := os .OpenFile (logPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0600 )
55+ if err != nil {
56+ return nil , fmt .Errorf ("failed to create audit log file: %v" , err )
57+ }
58+
59+ // Get initial file size
60+ info , err := logFile .Stat ()
61+ if err != nil {
62+ logFile .Close ()
63+ return nil , fmt .Errorf ("failed to get log file info: %v" , err )
64+ }
65+
66+ return & AuditLogger {
67+ logFile : logFile ,
68+ basePath : basePath ,
69+ size : info .Size (),
70+ }, nil
71+ }
72+
73+ // LogEvent logs a security audit event
74+ func (al * AuditLogger ) LogEvent (ctx context.Context , event * AuditEvent ) error {
75+ al .mu .Lock ()
76+ defer al .mu .Unlock ()
77+
78+ // Ensure timestamp is set
79+ if event .Timestamp .IsZero () {
80+ event .Timestamp = time .Now ()
81+ }
82+
83+ // Marshal event to JSON
84+ data , err := json .Marshal (event )
85+ if err != nil {
86+ return fmt .Errorf ("failed to marshal audit event: %v" , err )
87+ }
88+
89+ // Add newline for log file
90+ data = append (data , '\n' )
91+
92+ // Check if we need to rotate the log
93+ if al .size + int64 (len (data )) > maxLogSize {
94+ if err := al .rotateLog (); err != nil {
95+ return fmt .Errorf ("failed to rotate log: %v" , err )
96+ }
97+ }
98+
99+ // Write to log file
100+ n , err := al .logFile .Write (data )
101+ if err != nil {
102+ return fmt .Errorf ("failed to write audit event: %v" , err )
103+ }
104+
105+ // Update size
106+ al .size += int64 (n )
107+
108+ // Also log to standard logger for monitoring
109+ log .L (ctx ).Infof ("AUDIT: %s - %s - %s - %s - %s" ,
110+ event .EventType ,
111+ event .UserID ,
112+ event .Action ,
113+ event .Resource ,
114+ event .Status )
115+
116+ return nil
117+ }
118+
119+ // Close closes the audit logger
120+ func (al * AuditLogger ) Close () error {
121+ al .mu .Lock ()
122+ defer al .mu .Unlock ()
123+
124+ if al .logFile != nil {
125+ return al .logFile .Close ()
126+ }
127+ return nil
128+ }
129+
130+ // RotateLog rotates the audit log file
131+ func (al * AuditLogger ) rotateLog () error {
132+ // Close current file
133+ if al .logFile != nil {
134+ al .logFile .Close ()
135+ }
136+
137+ // Create new file with current timestamp
138+ timestamp := time .Now ().Format ("2006-01-02" )
139+ logPath := filepath .Join (al .basePath , fmt .Sprintf ("audit-%s.log" , timestamp ))
140+ logFile , err := os .OpenFile (logPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0600 )
141+ if err != nil {
142+ return fmt .Errorf ("failed to create new audit log file: %v" , err )
143+ }
144+
145+ al .logFile = logFile
146+ al .size = 0
147+
148+ // Clean up old log files
149+ if err := al .cleanupOldLogs (); err != nil {
150+ return fmt .Errorf ("failed to cleanup old logs: %v" , err )
151+ }
152+
153+ return nil
154+ }
155+
156+ // cleanupOldLogs removes old log files
157+ func (al * AuditLogger ) cleanupOldLogs () error {
158+ entries , err := os .ReadDir (al .basePath )
159+ if err != nil {
160+ return err
161+ }
162+
163+ now := time .Now ()
164+ for _ , entry := range entries {
165+ if entry .IsDir () {
166+ continue
167+ }
168+
169+ // Check if file is an audit log
170+ if ! strings .HasPrefix (entry .Name (), "audit-" ) || ! strings .HasSuffix (entry .Name (), ".log" ) {
171+ continue
172+ }
173+
174+ // Parse timestamp from filename
175+ timestamp , err := time .Parse ("2006-01-02" , strings .TrimSuffix (strings .TrimPrefix (entry .Name (), "audit-" ), ".log" ))
176+ if err != nil {
177+ continue
178+ }
179+
180+ // Check if file is too old
181+ if now .Sub (timestamp ) > maxLogAge {
182+ path := filepath .Join (al .basePath , entry .Name ())
183+ if err := os .Remove (path ); err != nil {
184+ return fmt .Errorf ("failed to remove old log file %s: %v" , path , err )
185+ }
186+ }
187+ }
188+
189+ return nil
190+ }
0 commit comments