package db import ( "encoding/json" "fmt" "strings" ) // AuditEvent represents a row in the audit_log table. type AuditEvent struct { ID int64 `json:"id"` EventTime string `json:"event_time"` EventType string `json:"event_type"` ActorID string `json:"actor_id,omitempty"` Repository string `json:"repository,omitempty"` Digest string `json:"digest,omitempty"` IPAddress string `json:"ip_address,omitempty"` Details map[string]string `json:"details,omitempty"` } // WriteAuditEvent inserts a new audit log entry. func (d *DB) WriteAuditEvent(eventType, actorID, repository, digest, ip string, details map[string]string) error { var detailsJSON *string if len(details) > 0 { b, err := json.Marshal(details) if err != nil { return fmt.Errorf("db: marshal audit details: %w", err) } s := string(b) detailsJSON = &s } _, err := d.Exec( `INSERT INTO audit_log (event_type, actor_id, repository, digest, ip_address, details) VALUES (?, ?, ?, ?, ?, ?)`, eventType, nullIfEmpty(actorID), nullIfEmpty(repository), nullIfEmpty(digest), nullIfEmpty(ip), detailsJSON, ) if err != nil { return fmt.Errorf("db: write audit event: %w", err) } return nil } // AuditFilter specifies criteria for listing audit events. type AuditFilter struct { EventType string ActorID string Repository string Since string // RFC 3339 Until string // RFC 3339 Limit int Offset int } // ListAuditEvents returns audit events matching the filter, ordered by // event_time descending (most recent first). func (d *DB) ListAuditEvents(f AuditFilter) ([]AuditEvent, error) { var clauses []string var args []any if f.EventType != "" { clauses = append(clauses, "event_type = ?") args = append(args, f.EventType) } if f.ActorID != "" { clauses = append(clauses, "actor_id = ?") args = append(args, f.ActorID) } if f.Repository != "" { clauses = append(clauses, "repository = ?") args = append(args, f.Repository) } if f.Since != "" { clauses = append(clauses, "event_time >= ?") args = append(args, f.Since) } if f.Until != "" { clauses = append(clauses, "event_time <= ?") args = append(args, f.Until) } query := "SELECT id, event_time, event_type, actor_id, repository, digest, ip_address, details FROM audit_log" if len(clauses) > 0 { query += " WHERE " + strings.Join(clauses, " AND ") } query += " ORDER BY event_time DESC" limit := f.Limit if limit <= 0 { limit = 50 } query += fmt.Sprintf(" LIMIT %d", limit) if f.Offset > 0 { query += fmt.Sprintf(" OFFSET %d", f.Offset) } rows, err := d.Query(query, args...) if err != nil { return nil, fmt.Errorf("db: list audit events: %w", err) } defer func() { _ = rows.Close() }() var events []AuditEvent for rows.Next() { var e AuditEvent var actorID, repository, digest, ip, detailsStr *string if err := rows.Scan(&e.ID, &e.EventTime, &e.EventType, &actorID, &repository, &digest, &ip, &detailsStr); err != nil { return nil, fmt.Errorf("db: scan audit event: %w", err) } if actorID != nil { e.ActorID = *actorID } if repository != nil { e.Repository = *repository } if digest != nil { e.Digest = *digest } if ip != nil { e.IPAddress = *ip } if detailsStr != nil { if err := json.Unmarshal([]byte(*detailsStr), &e.Details); err != nil { return nil, fmt.Errorf("db: unmarshal audit details: %w", err) } } events = append(events, e) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("db: iterate audit events: %w", err) } return events, nil } func nullIfEmpty(s string) *string { if s == "" { return nil } return &s }