mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 09:49:51 +00:00
Audit: correlation event_ids include stage and response code so the multi-stage lines of one request no longer collapse under dedup; match scans are bounded to a time window and a page instead of the object's full history. Web: dry-run results are read from dry_run_result (nested) so the verdict is rendered correctly and conflicts/errors/skipped are shown; the plans view no longer POSTs an unsolicited plan on page load; coverage and streams surface API errors instead of showing a misleading empty state and follow cursor pagination so they are not stuck on the oldest page; the diff path tokenizer handles backslash-escaped dotted keys. Deploy/CI: the chart no longer grants the query server a cluster-wide read ClusterRole, runs as non-root with a read-only root filesystem, adds liveness/readiness probes, wires the ConfigMap as env (STORE_PATH, LISTEN_ADDR), defaults the journal to a PVC instead of an ephemeral emptyDir, and adds imagePullSecrets; the replay ClusterRole drops the unused update verb; a Dockerfile builds a static distroless image; GitHub actions are pinned by commit SHA, jobs set least-privilege permissions, the Vercel deploy skips fork PRs, CI passes the Makefile test timeouts, and make lint runs a real web syntax check. Docs: event-schema/consistency no longer describe an ingest_sequence field, an observed-time-based event_id, or restart-from-checkpoint; the threat model documents the unauthenticated HTTP API surface and the chart's RBAC change; replay-safety matches the enforced dry-run gate.
91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
// Package audit correlates Kubernetes audit-log entries with events stored in
|
|
// the journal. Correlation is best-effort: an audit entry is only persisted as
|
|
// a TypeAuditCorrelation record when it matches an already-recorded watch
|
|
// event on (cluster, namespace, name, uid, resourceVersion).
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/krply/krply/internal/event"
|
|
"github.com/krply/krply/internal/storage"
|
|
)
|
|
|
|
// ErrNoMatch is returned when no stored event matches an audit entry.
|
|
var ErrNoMatch = errors.New("no matching journal event")
|
|
|
|
// AuditEvent is a normalized view of one Kubernetes audit-log entry.
|
|
// Resource is the plural resource name (e.g. "configmaps").
|
|
type AuditEvent struct {
|
|
ClusterID string
|
|
RequestID string
|
|
Verb string
|
|
Resource string
|
|
Namespace string
|
|
Name string
|
|
UID string
|
|
ResourceVersion string
|
|
User string
|
|
UserAgent string
|
|
SourceIPs []string
|
|
ResponseCode int
|
|
Stage string
|
|
Object json.RawMessage
|
|
ResponseObject json.RawMessage
|
|
Annotations map[string]string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// Correlator matches audit entries against stored events and writes
|
|
// TypeAuditCorrelation records into a Store.
|
|
type Correlator struct {
|
|
store storage.Store
|
|
}
|
|
|
|
// NewCorrelator returns a Correlator backed by the given Store.
|
|
func NewCorrelator(store storage.Store) *Correlator {
|
|
return &Correlator{store: store}
|
|
}
|
|
|
|
// Match searches the store for an EVENT record whose (ClusterID,
|
|
// Resource.Namespace, Resource.Name, Resource.UID) equal the audit event's and
|
|
// whose Resource.ResourceVersion equals evt.ResourceVersion. It returns the
|
|
// matched record's EventID, or ErrNoMatch when no event matches.
|
|
func (c *Correlator) Match(ctx context.Context, evt AuditEvent) (string, error) {
|
|
rec, err := c.matchRecord(ctx, evt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return rec.EventID, nil
|
|
}
|
|
|
|
func (c *Correlator) matchRecord(ctx context.Context, evt AuditEvent) (*event.Record, error) {
|
|
// The correlated journal event must be close in time to the audit line, so
|
|
// the scan is bounded to a window and a page instead of the object's whole
|
|
// history.
|
|
f := storage.EventFilter{
|
|
ClusterID: evt.ClusterID,
|
|
Namespace: evt.Namespace,
|
|
Name: evt.Name,
|
|
RecordType: event.TypeEvent,
|
|
Limit: 500,
|
|
}
|
|
if !evt.Timestamp.IsZero() {
|
|
f.Since = evt.Timestamp.Add(-1 * time.Minute)
|
|
f.Until = evt.Timestamp.Add(1 * time.Minute)
|
|
}
|
|
recs, err := c.store.Events(ctx, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range recs {
|
|
r := &recs[i]
|
|
if r.Resource.UID == evt.UID && r.Resource.ResourceVersion == evt.ResourceVersion {
|
|
return r, nil
|
|
}
|
|
}
|
|
return nil, ErrNoMatch
|
|
}
|