package audit import ( "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" "log" "time" "github.com/google/uuid" ) type Event struct { ID string Timestamp time.Time UserID string Action string ResourceType string ResourceID string Success bool Details map[string]any // Enhanced fields Actor string // Username or user identifier Resource string // Full resource identifier (e.g., "pool:my-pool") PayloadHash string // SHA256 hash of request payload Result string // Success/failure message or status ClientIP string // Client IP address } type AuditLogger interface { Record(ctx context.Context, e Event) error } type SQLAuditLogger struct { DB *sql.DB } func NewSQLAuditLogger(db *sql.DB) *SQLAuditLogger { return &SQLAuditLogger{DB: db} } func (l *SQLAuditLogger) Record(ctx context.Context, e Event) error { if e.ID == "" { e.ID = uuid.New().String() } if e.Timestamp.IsZero() { e.Timestamp = time.Now() } // Set actor from UserID if not provided if e.Actor == "" { e.Actor = e.UserID } // Build resource string from ResourceType and ResourceID if e.Resource == "" { if e.ResourceID != "" { e.Resource = e.ResourceType + ":" + e.ResourceID } else { e.Resource = e.ResourceType } } // Set result from Success if not provided if e.Result == "" { if e.Success { e.Result = "success" } else { e.Result = "failure" } } detailsJSON, _ := json.Marshal(e.Details) // Try to insert with all columns, fallback to basic columns if enhanced columns don't exist _, err := l.DB.ExecContext(ctx, `INSERT INTO audit_events (id, ts, user_id, action, resource_type, resource_id, success, details, actor, resource, payload_hash, result, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, e.ID, e.Timestamp, e.UserID, e.Action, e.ResourceType, e.ResourceID, boolToInt(e.Success), string(detailsJSON), e.Actor, e.Resource, e.PayloadHash, e.Result, e.ClientIP) if err != nil { // Fallback to basic insert if enhanced columns don't exist yet _, err2 := l.DB.ExecContext(ctx, `INSERT INTO audit_events (id, ts, user_id, action, resource_type, resource_id, success, details) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, e.ID, e.Timestamp, e.UserID, e.Action, e.ResourceType, e.ResourceID, boolToInt(e.Success), string(detailsJSON)) if err2 != nil { log.Printf("audit record failed: %v (fallback also failed: %v)", err, err2) return err2 } log.Printf("audit record inserted with fallback (enhanced columns may not exist): %v", err) } return nil } // HashPayload computes SHA256 hash of a payload (JSON string or bytes) func HashPayload(payload interface{}) string { var data []byte switch v := payload.(type) { case []byte: data = v case string: data = []byte(v) default: jsonData, _ := json.Marshal(payload) data = jsonData } hash := sha256.Sum256(data) return hex.EncodeToString(hash[:]) } func boolToInt(b bool) int { if b { return 1 } return 0 }