Handle legacy audit timestamp rows

This commit is contained in:
rcourtman 2026-07-05 23:18:37 +01:00
parent 6212eecf12
commit 0f1211d510
2 changed files with 107 additions and 3 deletions

View file

@ -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, &timestamp, &e.EventType, &user, &ip, &path, &success, &details, &signature)
err := rows.Scan(&e.ID, &timestampValue, &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

View file

@ -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()