From 4a597fa425101d788330e9a6dea01418d35ebdd7 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 6 Jul 2026 16:18:38 +0100 Subject: [PATCH] Fix legacy audit monotonic timestamps Refs #1464 --- .../internal/subsystems/security-privacy.md | 5 +++ pkg/audit/sqlite_logger.go | 6 +++ pkg/audit/sqlite_logger_test.go | 43 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index fb731fb51..b71d2270c 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -560,6 +560,11 @@ uninitialized audit stores must surface as `audit_store_unavailable`. The Audit Log settings surface may translate those stable API codes into recovery copy, but it must not show raw internal server errors or collapse audit-store state into a generic frontend failure. +The persistent audit reader must also tolerate legacy timestamp encodings that +were previously written into `audit_events.timestamp`, including Unix seconds, +SQLite datetime values, and Go wall-clock strings carrying a monotonic +`m=+...` suffix, so valid historical audit rows cannot make `/api/audit` +return `query_failed`. That shared token-management boundary now also includes `frontend-modern/src/utils/apiTokenPresentation.ts`, so API-token load, generate, and revoke errors stay on one governed customer-facing wording path diff --git a/pkg/audit/sqlite_logger.go b/pkg/audit/sqlite_logger.go index 66cc4086e..59e04329e 100644 --- a/pkg/audit/sqlite_logger.go +++ b/pkg/audit/sqlite_logger.go @@ -465,6 +465,12 @@ func parseAuditTimestampString(raw string) (time.Time, error) { if unix, err := strconv.ParseInt(raw, 10, 64); err == nil { return time.Unix(unix, 0), nil } + if monotonicIndex := strings.LastIndex(raw, " m="); monotonicIndex > 0 && monotonicIndex+3 < len(raw) { + sign := raw[monotonicIndex+3] + if sign == '+' || sign == '-' { + raw = strings.TrimSpace(raw[:monotonicIndex]) + } + } layouts := []string{ time.RFC3339Nano, diff --git a/pkg/audit/sqlite_logger_test.go b/pkg/audit/sqlite_logger_test.go index 64ae5d47b..fcecf2060 100644 --- a/pkg/audit/sqlite_logger_test.go +++ b/pkg/audit/sqlite_logger_test.go @@ -310,6 +310,49 @@ func TestSQLiteLoggerQueryHandlesLegacyDatetimeTimestampRows(t *testing.T) { } } +func TestSQLiteLoggerQueryHandlesLegacyGoMonotonicTimestampRows(t *testing.T) { + tempDir := t.TempDir() + + logger, err := NewSQLiteLogger(SQLiteLoggerConfig{ + DataDir: tempDir, + CryptoMgr: newMockCryptoManager(), + RetentionDays: 30, + }) + if err != nil { + t.Fatalf("NewSQLiteLogger failed: %v", err) + } + defer logger.Close() + + const legacyTimestamp = "2026-07-05 08:51:23.65653076 +0000 UTC m=+0.009025344" + want := time.Date(2026, 7, 5, 8, 51, 23, 656530760, time.UTC) + if _, err := logger.db.Exec(` + INSERT INTO audit_events (id, timestamp, event_type, user, ip, path, success, details, signature) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + "legacy-go-monotonic", + legacyTimestamp, + "startup", + sql.NullString{}, + sql.NullString{}, + "/api/audit", + 1, + "legacy Go monotonic timestamp", + "legacy-signature", + ); err != nil { + t.Fatalf("insert legacy Go monotonic row: %v", err) + } + + events, err := logger.Query(QueryFilter{ID: "legacy-go-monotonic", Limit: 1}) + if err != nil { + t.Fatalf("Query failed for legacy Go monotonic timestamp row: %v", err) + } + if len(events) != 1 { + t.Fatalf("Expected 1 event, got %d", len(events)) + } + if !events[0].Timestamp.Equal(want) { + t.Fatalf("timestamp = %s, want %s", events[0].Timestamp, want) + } +} + func TestSQLiteLoggerCount(t *testing.T) { tempDir := t.TempDir()