Help us shape the future of Go! Take the 2026 Go Developer Survey

audit

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package audit writes the append-only event log.

Append-only is enforced at the database level: the application role holds INSERT on audit_events and neither UPDATE nor DELETE (R-027). Every authorization denial is audited, not only successes, and the event is written before the privileged action, not after — an exec session that fails to open is still recorded as attempted (R-228).

Index

Constants

View Source
const CursorNow = "now"

CursorNow is the wire value meaning "after the newest settled event".

View Source
const DefaultArchiveInterval = 24 * time.Hour

DefaultArchiveInterval is how often retention runs. Months turn over once a month; a daily pass finds each one within a day and costs one small query on the days there is nothing to do.

View Source
const DefaultReadEvery = time.Hour

DefaultReadEvery is how often one reader's stream reads are recorded.

View Source
const DefaultRetentionMonths = 3

DefaultRetentionMonths is retention when policy does not say (R-348).

View Source
const MaxStreamWait = 60 * time.Second

MaxStreamWait bounds a long poll.

View Source
const MinRetentionMonths = 3

MinRetentionMonths is the floor (R-348): no month leaves the live log until it ended at least this long ago, whatever policy says. audit_drop_month holds the same number in the database, so lowering this alone changes nothing.

Variables

View Source
var Actions = []string{}/* 154 elements not displayed */

Actions is every action Pando writes to the audit log, sorted: the names an audit search may filter on (R-345).

From the code rather than from the log (O-54). Reading them from the log is a DISTINCT over every row, which costs what the log has grown to on each question; the names are fixed by the code that writes them, so the code is where they are listed. TestEveryActionWrittenIsCatalogued fails when a literal action written anywhere in the tree is missing here.

Functions

func Changes

func Changes(before, after any) map[string]any

Changes describes what changed between two versions of a document, for an audit event's detail (R-390): "changed" lists the paths of the fields that differ, and "before" and "after" hold their values where a value is a scalar. A list or an object that changed is named and not copied: the path is what an investigation searches for, and copying whole structures would make one policy edit the largest row in the log.

Both sides are read through their JSON encoding, so a secret.Value — which encodes as "[redacted]" — is the same on both sides and its change is never recorded as a value (R-194). A field holding a secret's value changes as a path alone at most.

Nil when nothing changed or either side cannot be encoded.

func ManifestName

func ManifestName(objectName string) string

ManifestName is the manifest's object name beside an archive.

func PageSize

func PageSize(requested int) int

PageSize is how many records a Query asking for `requested` will return.

Exported because the API layer has to know it to decide whether a page was full, and a function rather than the constant it used to export because knowing the default is not enough: a caller asking for 1000 got 500 records and a layer above comparing them against 1000, so the page never looked full and the cursor for the next page was never sent. Asking the same code that does the clamping is the only version of this that cannot drift.

It is also what keeps the allocation below bounded by a constant rather than by whatever number arrived in a query string.

func Str

func Str(p *string) string

Str is a nullable column's value, empty for null.

func StreamPageSize

func StreamPageSize(requested int) int

StreamPageSize is how many events a StreamQuery asking for `requested` scans.

func Verify

func Verify(ctx context.Context, st Store, rec ArchiveRecord) error

Verify reads an archive back and checks it against its record: the stored bytes' size and digest, that it decompresses, and that it holds RowCount events in id order from FirstID to LastID. Nothing is removed from the live log until this passes (R-347).

Types

type ArchiveMode

type ArchiveMode string

ArchiveMode is what happens to a month past retention.

const (
	// ArchiveKeep writes the archive under Pando's own directory. The default.
	ArchiveKeep ArchiveMode = "keep"
	// ArchiveExport writes it to a backup destination (R-217).
	ArchiveExport ArchiveMode = "export"
	// ArchiveOff archives nothing, so nothing leaves the live log: the log
	// grows as it did before retention existed.
	ArchiveOff ArchiveMode = "off"
)

func (ArchiveMode) Valid

func (m ArchiveMode) Valid() error

Valid refuses anything but the three modes. Empty is unset, which is keep.

type ArchiveRecord

type ArchiveRecord struct {
	ID         string    `json:"id"`
	Month      string    `json:"month"` // 2026-06
	AdapterRef string    `json:"adapter_ref,omitempty"`
	ObjectName string    `json:"object_name"`
	RowCount   int64     `json:"row_count"`
	FirstID    int64     `json:"first_id"`
	LastID     int64     `json:"last_id"`
	FirstAt    time.Time `json:"first_at"`
	LastAt     time.Time `json:"last_at"`
	SizeBytes  int64     `json:"size_bytes"`
	SHA256     string    `json:"sha256"`
	CreatedAt  time.Time `json:"created_at"`
}

ArchiveRecord is one archived month: where it is and what is in it.

The manifest R-347 asks for. A copy is written beside the archive, so an exported archive says what it holds without Pando's database.

type Archiver

type Archiver struct {
	Pool      *pgxpool.Pool
	Stores    Stores
	Retention func(ctx context.Context) (Retention, error)
	Clock     clock.Clock
	Logger    *zap.Logger
	Interval  time.Duration

	// Holds names the enabled audit sinks that have not been sent every event
	// in [lo, hi) (R-386). A month any of them holds is neither archived nor
	// removed this pass: archiving it now would record an archive for rows
	// still in the live log. Nil holds nothing.
	Holds func(ctx context.Context, lo, hi time.Time) ([]string, error)

	// Audit records that a month was held, once per month per process. The
	// archiver's own role cannot write the log, so this is the application's
	// writer. Nil records nothing.
	Audit func(ctx context.Context, e Event)
	// contains filtered or unexported fields
}

Archiver runs retention. Pool must be held as the archiver role (state.ArchiverRole); the application role cannot do any of this, by design.

func (*Archiver) Pass

func (a *Archiver) Pass(ctx context.Context) ([]ArchiveRecord, error)

Pass makes the partitions the next months need, then archives and removes every month past retention. It returns the archives that months left the live log under.

func (*Archiver) Run

func (a *Archiver) Run(ctx context.Context)

Run passes at startup and then every Interval until ctx ends.

type Archives

type Archives struct {
	Pool   *pgxpool.Pool
	Stores Stores
}

Archives lists and opens archives for the API (R-261). It reads with the application role, which may read the record and write none of it.

func (*Archives) List

func (a *Archives) List(ctx context.Context) ([]ArchiveRecord, error)

List is every archive, newest month first.

func (*Archives) Open

func (a *Archives) Open(ctx context.Context, archiveID string) (ArchiveRecord, io.ReadCloser, error)

Open returns an archive's record and its bytes, from wherever it was written. The caller closes the reader.

type Cursor

type Cursor struct {
	TxID uint64
	ID   int64
}

Cursor is a position in the stream: the last event read. The zero Cursor is before the first event in the live log.

func ParseCursor

func ParseCursor(s string) (Cursor, error)

ParseCursor reads a cursor as String writes it. Empty is the zero Cursor. "now" is not a cursor and is resolved by Reader.Now.

func (Cursor) IsZero

func (c Cursor) IsZero() bool

IsZero reports whether c is before the first event.

func (Cursor) String

func (c Cursor) String() string

type Encoder

type Encoder func(line json.RawMessage) (json.RawMessage, error)

Encoder turns a native line into another format, such as OCSF.

type Event

type Event struct {
	PrincipalKind PrincipalKind
	PrincipalID   string

	// OnBehalfOf is the owning user when a delegated token acted (R-229).
	// Authorization already resolved through the owner; the audit log records
	// both so "what did this person do" stays answerable.
	OnBehalfOf string

	Action     string
	AppID      string
	TargetKind string
	TargetID   string
	RequestID  string
	Detail     map[string]any

	// Outcome is success, denied or failed (R-379). Empty derives it from
	// the action (OutcomeOf), which is right for every event whose action
	// name already says how it ended. A finished deploy, whose one action
	// covers both, sets it.
	Outcome Outcome
}

Event is one audit record.

Nothing here is ever updated or deleted — the database enforces that, not this type (R-027). Detail must never contain a secret value; secret.Value makes that structural on the way in.

type Line

type Line struct {
	ID            int64          `json:"id"`
	OccurredAt    time.Time      `json:"occurred_at"`
	PrincipalKind string         `json:"principal_kind"`
	PrincipalID   *string        `json:"principal_id"`
	OnBehalfOf    *string        `json:"on_behalf_of"`
	Action        string         `json:"action"`
	AppID         *string        `json:"app_id"`
	TargetKind    *string        `json:"target_kind"`
	TargetID      *string        `json:"target_id"`
	RequestID     *string        `json:"request_id"`
	Detail        map[string]any `json:"detail"`
	SchemaVersion *int           `json:"schema_version"`
	Outcome       *string        `json:"outcome"`
	SourceIP      *string        `json:"source_ip"`
	PeerIP        *string        `json:"peer_ip"`
	UserAgent     *string        `json:"user_agent"`
	ActorName     *string        `json:"actor_name"`
	ActorEmail    *string        `json:"actor_email"`
}

Line is one native line (design 12 §6.1) decoded: the row as row_to_json writes it, every column present and null where unset. What the OCSF mapper and anything else re-encoding the stream reads.

func (Line) Version

func (l Line) Version() int

Version is the line's schema version: 1 for events written before R-379.

type Outcome

type Outcome string

Outcome is how the action an event records ended (R-379).

const (
	OutcomeSuccess Outcome = "success"
	OutcomeDenied  Outcome = "denied"
	OutcomeFailed  Outcome = "failed"
)

func OutcomeOf

func OutcomeOf(action string) Outcome

OutcomeOf is the outcome an action's name says (design 12 §3.1): .denied and .refused are denials, .failed and the few listed above are failures, and everything else succeeded. TestR379_EveryActionHasAnOutcome holds the catalog to it.

type PrincipalKind

type PrincipalKind string

PrincipalKind identifies who acted.

const (
	KindUser      PrincipalKind = "user"
	KindToken     PrincipalKind = "token"
	KindSystem    PrincipalKind = "system"
	KindAnonymous PrincipalKind = "anonymous"
)

type Query

type Query struct {
	// Action matches a prefix, so "app." finds every app event and
	// "session.denied" finds exactly one kind. A prefix rather than a substring
	// because actions are dotted namespaces — a substring match would make
	// "grant.delete" a result for a search for "delete".
	Action string

	// Actions are more prefixes, any of which matches, alongside Action:
	// "apps someone added or deleted" is app.create or app.delete, and filters
	// otherwise combine with AND.
	Actions []string

	AppID       string
	PrincipalID string

	// PrincipalKind narrows to one kind of actor: user, token, system or
	// anonymous. The only way to find anonymous events, which have no ID, and
	// every system process at once rather than one at a time.
	PrincipalKind string

	// TargetKind and TargetID narrow to what the action was done to — "every
	// change to this user", "everything done to any role". Exact matches.
	TargetKind string
	TargetID   string

	// Involving narrows to events where one ID is on either side: the actor
	// (or the person a token acted for) or the target. "Everything to do with
	// this account" — what it did and what was done to it — which the actor and
	// target filters cannot say, because filters combine with AND.
	Involving string

	// Since and Until bound when it happened: Since inclusive, Until
	// exclusive, so consecutive ranges neither overlap nor leave a gap. Zero
	// means unbounded.
	Since time.Time
	Until time.Time

	// Limit defaults to 100, capped at 500. PageSize is the clamp.
	Limit int

	// Cursor is the ID of the last record on the previous page. The log is
	// append-only with monotonic IDs, so "before this ID" is a stable page
	// boundary in a way an offset is not — with an offset, events arriving
	// between requests shift every later page.
	Cursor int64
}

Query narrows a read. Every field is optional; the zero value reads the whole log, newest first.

type ReadThrottle

type ReadThrottle struct {
	Every time.Duration
	// contains filtered or unexported fields
}

ReadThrottle decides when a read of the audit stream is itself audited (R-388): at most once per reader per Every, per replica. A collector long-polling every two seconds would otherwise put tens of thousands of events a day about itself into the log it is reading. Listing, exporting and downloading an archive are audited every time and do not use it.

func (*ReadThrottle) Due

func (t *ReadThrottle) Due(reader string, now time.Time) bool

Due reports whether reader's read at now should be audited, and records it if so.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader queries the audit log (R-227, R-229).

A separate type from Writer, because they are separate privileges: writing is something every code path does, and reading is gated by install.audit.read. Neither can update or delete — the database refuses both to the application role (R-027), so this type could not offer it even if someone added a method.

func NewReader

func NewReader(pool *pgxpool.Pool) *Reader

func (*Reader) Export

func (r *Reader) Export(ctx context.Context, q StreamQuery, enc Encoder, write func(line []byte) error) (int, error)

Export writes every settled event in [q.Since, q.Until), in commit order, one line each, through enc (nil for native). It pages from a cursor, so an export of a year is never held in memory.

func (*Reader) List

func (r *Reader) List(ctx context.Context, q Query) ([]Record, error)

List reads the log, newest first.

func (*Reader) Now

func (r *Reader) Now(ctx context.Context) (Cursor, error)

Now is the cursor after the newest settled event: what a reader that wants only what happens next starts from.

func (*Reader) Oldest

func (r *Reader) Oldest(ctx context.Context) (time.Time, bool, error)

Oldest is when the oldest event in the live log happened, and false when it is empty. An export reaching before it says so (R-387).

func (*Reader) ResolveCursor

func (r *Reader) ResolveCursor(ctx context.Context, s string) (Cursor, error)

ResolveCursor reads a cursor from the wire, "now" included.

func (*Reader) Stream

func (r *Reader) Stream(ctx context.Context, q StreamQuery) (StreamPage, error)

Stream reads one page past q.After.

func (*Reader) Wait

func (r *Reader) Wait(ctx context.Context, q StreamQuery, wait time.Duration, c clock.Clock) (StreamPage, error)

Wait reads like Stream, but when nothing settled lies past the cursor it looks again every second until something does or wait runs out, and then returns the empty page with the cursor unchanged.

type Record

type Record struct {
	ID            int64          `json:"id"`
	OccurredAt    time.Time      `json:"occurred_at"`
	PrincipalKind string         `json:"principal_kind"`
	PrincipalID   string         `json:"principal_id,omitempty"`
	OnBehalfOf    string         `json:"on_behalf_of,omitempty"`
	Action        string         `json:"action"`
	AppID         string         `json:"app_id,omitempty"`
	TargetKind    string         `json:"target_kind,omitempty"`
	TargetID      string         `json:"target_id,omitempty"`
	RequestID     string         `json:"request_id,omitempty"`
	Detail        map[string]any `json:"detail,omitempty"`

	// What R-379 adds. Empty on events written before it.
	Outcome       string `json:"outcome,omitempty"`
	SourceIP      string `json:"source_ip,omitempty"`
	PeerIP        string `json:"peer_ip,omitempty"`
	UserAgent     string `json:"user_agent,omitempty"`
	ActorName     string `json:"actor_name,omitempty"`
	ActorEmail    string `json:"actor_email,omitempty"`
	SchemaVersion int    `json:"schema_version"`
}

Record is one stored event, as read back.

type Retention

type Retention struct {
	// Months the live log keeps. Zero is DefaultRetentionMonths.
	Months int
	// Archive is the mode. Empty is ArchiveKeep.
	Archive ArchiveMode
	// Destination is the backup adapter an export goes to. Empty is the
	// default backup destination.
	Destination string
}

Retention is host policy's word on the audit log.

func (Retention) Mode

func (r Retention) Mode() ArchiveMode

Mode is the archive mode with the default applied.

func (Retention) MonthsKept

func (r Retention) MonthsKept() int

MonthsKept is retention with the default applied and the floor held.

type Store

type Store interface {
	Writer(ctx context.Context, name string) (io.WriteCloser, error)
	Reader(ctx context.Context, name string) (io.ReadCloser, error)
}

Store is somewhere an archive can be written and read back. A backup adapter is one (R-217); so is the directory Pando keeps archives in.

type Stores

type Stores struct {
	// Kept is Pando's own directory, for ArchiveKeep.
	Kept Store

	// Export resolves a backup adapter by reference, empty meaning the
	// default, and returns the reference it resolved to.
	Export func(ref string) (Store, string, error)
}

Stores resolves where an archive lives.

type StreamPage

type StreamPage struct {
	// Events are native lines (design 12 §6.1): each row as the archiver
	// writes it, oldest first.
	Events []json.RawMessage

	// IDs are the events' ids, in the same order.
	IDs []int64

	// Cursor is where the next read starts. It has moved past every event
	// scanned, including those a filter dropped.
	Cursor Cursor

	// CaughtUp says the read reached the horizon: nothing settled lies past
	// Cursor right now.
	CaughtUp bool
}

StreamPage is what one read returns.

type StreamQuery

type StreamQuery struct {
	After Cursor

	// Limit defaults to 500, capped at 1000 (StreamPageSize).
	Limit int

	// Actions and Exclude are prefixes. An event is read when it matches one
	// of Actions (or Actions is empty) and none of Exclude. The cursor moves
	// past the events a filter drops, so a filtered reader never reads them
	// again.
	Actions []string
	Exclude []string

	// Since and Until bound occurred_at, Since inclusive and Until exclusive,
	// for an export of a range. Zero is unbounded.
	Since time.Time
	Until time.Time
}

StreamQuery is one page of the stream.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer appends to the audit log.

func New

func New(pool *pgxpool.Pool) *Writer

func (*Writer) Write

func (w *Writer) Write(ctx context.Context, e Event) error

Write appends an event.

Callers write the event *before* the privileged action, not after (R-228): an exec session that fails to open is still recorded as attempted. Denials are audited as well as successes — a denial pattern is the signal that matters for detecting misuse, and it is the thing most commonly left out.

Directories

Path Synopsis
Package ocsf encodes native audit lines as OCSF events (design 12 §6.2, R-384).
Package ocsf encodes native audit lines as OCSF events (design 12 §6.2, R-384).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL