- internal/config: TOML config with env overrides (MCR_ prefix), required field validation, same-filesystem check, defaults - internal/db: SQLite via modernc.org/sqlite, WAL mode, 2 migrations (core registry tables + policy/audit), foreign key cascades - internal/db: audit log write/list with filtering and pagination - deploy/examples/mcr.toml: annotated example configuration - .golangci.yaml: disable fieldalignment (readability over micro-opt) - checkpoint skill copied from mcias Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
148 lines
3.7 KiB
Go
148 lines
3.7 KiB
Go
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
|
|
}
|