From 0f1211d5108f0b819251fe8a1f330e935a0906e1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 5 Jul 2026 23:18:37 +0100 Subject: [PATCH] Handle legacy audit timestamp rows --- pkg/audit/sqlite_logger.go | 67 +++++++++++++++++++++++++++++++-- pkg/audit/sqlite_logger_test.go | 43 +++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/pkg/audit/sqlite_logger.go b/pkg/audit/sqlite_logger.go index 1cc55b4a5..66cc4086e 100644 --- a/pkg/audit/sqlite_logger.go +++ b/pkg/audit/sqlite_logger.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "sync" "time" @@ -406,16 +407,20 @@ func (l *SQLiteLogger) queryLocked(filter QueryFilter) ([]Event, error) { var events []Event for rows.Next() { var e Event - var timestamp int64 + var timestampValue any var success int var user, ip, path, details, signature sql.NullString - err := rows.Scan(&e.ID, ×tamp, &e.EventType, &user, &ip, &path, &success, &details, &signature) + err := rows.Scan(&e.ID, ×tampValue, &e.EventType, &user, &ip, &path, &success, &details, &signature) + if err != nil { + return nil, fmt.Errorf("failed to scan audit event: %w", err) + } + timestamp, err := parseAuditTimestamp(timestampValue) if err != nil { return nil, fmt.Errorf("failed to scan audit event: %w", err) } - e.Timestamp = time.Unix(timestamp, 0) + e.Timestamp = timestamp e.Success = success == 1 e.User = user.String e.IP = ip.String @@ -429,6 +434,62 @@ func (l *SQLiteLogger) queryLocked(filter QueryFilter) ([]Event, error) { return events, rows.Err() } +func parseAuditTimestamp(value any) (time.Time, error) { + switch v := value.(type) { + case time.Time: + return v, nil + case int64: + return time.Unix(v, 0), nil + case int: + return time.Unix(int64(v), 0), nil + case int32: + return time.Unix(int64(v), 0), nil + case float64: + return time.Unix(int64(v), 0), nil + case []byte: + return parseAuditTimestampString(string(v)) + case string: + return parseAuditTimestampString(v) + case nil: + return time.Time{}, errors.New("timestamp is null") + default: + return time.Time{}, fmt.Errorf("unsupported timestamp type %T", value) + } +} + +func parseAuditTimestampString(raw string) (time.Time, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return time.Time{}, errors.New("timestamp is empty") + } + if unix, err := strconv.ParseInt(raw, 10, 64); err == nil { + return time.Unix(unix, 0), nil + } + + layouts := []string{ + time.RFC3339Nano, + time.RFC3339, + "2006-01-02 15:04:05.999999999Z07:00", + "2006-01-02 15:04:05.999999Z07:00", + "2006-01-02 15:04:05Z07:00", + "2006-01-02 15:04:05.999999999-07:00", + "2006-01-02 15:04:05.999999-07:00", + "2006-01-02 15:04:05-07:00", + "2006-01-02 15:04:05.999999999 -0700 MST", + "2006-01-02 15:04:05.999999 -0700 MST", + "2006-01-02 15:04:05 -0700 MST", + "2006-01-02 15:04:05.999999999", + "2006-01-02 15:04:05.999999", + "2006-01-02 15:04:05", + } + for _, layout := range layouts { + if timestamp, err := time.Parse(layout, raw); err == nil { + return timestamp, nil + } + } + return time.Time{}, fmt.Errorf("unsupported timestamp value %q", raw) +} + // Count returns the number of events matching the filter. func (l *SQLiteLogger) Count(filter QueryFilter) (int, error) { var count int diff --git a/pkg/audit/sqlite_logger_test.go b/pkg/audit/sqlite_logger_test.go index 7f9a2225e..64ae5d47b 100644 --- a/pkg/audit/sqlite_logger_test.go +++ b/pkg/audit/sqlite_logger_test.go @@ -1,6 +1,7 @@ package audit import ( + "database/sql" "errors" "fmt" "testing" @@ -267,6 +268,48 @@ func TestSQLiteLoggerQuery(t *testing.T) { }) } +func TestSQLiteLoggerQueryHandlesLegacyDatetimeTimestampRows(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() + + legacyTime := time.Date(2026, 7, 5, 16, 4, 13, 0, time.UTC) + if _, err := logger.db.Exec(` + INSERT INTO audit_events (id, timestamp, event_type, user, ip, path, success, details, signature) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + "legacy-datetime", + legacyTime, + "startup", + sql.NullString{}, + sql.NullString{}, + "/api/audit", + 1, + "legacy Pro runtime timestamp", + "legacy-signature", + ); err != nil { + t.Fatalf("insert legacy datetime row: %v", err) + } + + events, err := logger.Query(QueryFilter{ID: "legacy-datetime", Limit: 1}) + if err != nil { + t.Fatalf("Query failed for legacy datetime timestamp row: %v", err) + } + if len(events) != 1 { + t.Fatalf("Expected 1 event, got %d", len(events)) + } + if !events[0].Timestamp.Equal(legacyTime) { + t.Fatalf("timestamp = %s, want %s", events[0].Timestamp, legacyTime) + } +} + func TestSQLiteLoggerCount(t *testing.T) { tempDir := t.TempDir()