120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
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
|
|
}
|