package admin import ( "context" "encoding/json" "errors" "net/netip" "regexp" "time" "tug/internal/platform/auditcatalog" "tug/internal/storage" ) const ( AuditPerPage = 50 foldWindow = 2 * RecentEntries ) var ( reAuditID = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) reAuditSession = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`) ) type AuditCursor struct { At time.Time ID int64 } type AuditQuery struct { Actor string Action string Subject string Address netip.Addr From *time.Time To *time.Time Older *AuditCursor Newer *AuditCursor } type AuditRow struct { ID int64 At time.Time Actor string Action string Subject string Address string Detail string } type AuditPage struct { Rows []AuditRow Newer bool Older bool Retention time.Duration } func (s *Service) Audit(ctx context.Context, q AuditQuery) (AuditPage, error) { out := AuditPage{Retention: s.retention} actor, ok, err := s.auditActor(ctx, q.Actor) if err != nil || !ok { return out, err } subject, err := s.auditSubject(ctx, q.Subject) if err != nil { return out, err } filter := storage.AuditPageFilter{Actor: actor, Action: q.Action, Subject: subject, Address: q.Address, After: q.From, Before: q.To, Limit: AuditPerPage} if q.Older != nil { filter.Older = &storage.AuditCursor{OccurredAt: q.Older.At, ID: q.Older.ID} } if q.Newer != nil { filter.Newer = &storage.AuditCursor{OccurredAt: q.Newer.At, ID: q.Newer.ID} } page, err := s.store.ListAuditPage(ctx, filter) if err != nil { return out, failed("read the audit page", err) } names := s.resolver(ctx) for _, e := range page.Events { actor, err := names.actor(e.Actor) if err != nil { return out, err } subject, err := names.subjectOf(e) if err != nil { return out, err } row := AuditRow{ID: e.ID, At: e.OccurredAt, Actor: actor, Action: e.Action, Subject: subject, Detail: e.Detail} if e.Address.IsValid() { row.Address = e.Address.String() } out.Rows = append(out.Rows, row) } out.Older, out.Newer = page.Older != nil, page.Newer != nil return out, nil } func (s *Service) auditActor(ctx context.Context, name string) (string, bool, error) { if name == "" || name == "system" || reAuditID.MatchString(name) { return name, true, nil } u, err := s.store.UserByUsername(ctx, name) switch { case errors.Is(err, storage.ErrNotFound): return "", false, nil case err != nil: return "", false, failed("resolve the audit actor", err) } return u.ID, true, nil } func (s *Service) auditSubject(ctx context.Context, name string) (string, error) { if name == "" || reAuditID.MatchString(name) || reAuditSession.MatchString(name) { return name, nil } u, err := s.store.UserByUsername(ctx, name) switch { case err == nil: return u.ID, nil case !errors.Is(err, storage.ErrNotFound): return "", failed("resolve the audit subject", err) } g, err := s.store.GroupByName(ctx, name) switch { case err == nil: return g.ID, nil case !errors.Is(err, storage.ErrNotFound): return "", failed("resolve the audit subject", err) } return name, nil } func detailString(raw, key string) string { var fields map[string]any if json.Unmarshal([]byte(raw), &fields) != nil { return "" } v, _ := fields[key].(string) return v } func (r *resolver) subjectOf(e storage.AuditEvent) (string, error) { entry, ok := auditcatalog.Lookup(e.Action) if !ok { return r.subject(e.Subject) } for _, kind := range entry.Subjects { switch kind { case auditcatalog.SubjectSession: return detailString(e.Detail, "host"), nil case auditcatalog.SubjectUser: if name, found, err := r.username(e.Subject); err != nil || found { return name, err } case auditcatalog.SubjectGroup: if name, found, err := r.groupName(e.Subject); err != nil || found { return name, err } } } return e.Subject, nil } type foldKey struct { subject string at int64 } func foldSessions(events []storage.AuditEvent) ([]storage.AuditEvent, map[int64]bool) { opens := map[foldKey]int64{} for _, e := range events { if e.Action == "session.open" && detailString(e.Detail, "via") == "oidc" { opens[foldKey{e.Subject, e.OccurredAt.UnixMicro()}] = e.ID } } folded := map[int64]bool{} fresh := map[int64]bool{} for _, e := range events { if e.Action != "session.signin" { continue } if id, ok := opens[foldKey{e.Subject, e.OccurredAt.UnixMicro()}]; ok && !folded[id] { folded[id] = true fresh[e.ID] = true } } out := make([]storage.AuditEvent, 0, len(events)) for _, e := range events { if !folded[e.ID] { out = append(out, e) } } return out, fresh } func (r *resolver) recent(events []storage.AuditEvent) ([]AuditEntry, error) { events, fresh := foldSessions(events) entries, err := r.entries(events[:min(len(events), RecentEntries)]) if err != nil { return nil, err } for i := range entries { entries[i].NewSession = fresh[events[i].ID] } return entries, nil }