audit

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package audit is the durable record of everything the server did: an append-and-prune store of projected events, with the in-memory and SQL backends behind one contract.

Why

The threat model's repudiation control states that every state change emits an event and that subscribers persist them. Phase 9 made the first half true with event/sink, which decides what an event may say. This package is the second half: the place those records go so a question can be asked afterwards. An slog sink is attributable but only as durable as the log stream someone remembered to ship; proving who erased a device three weeks ago needs a table.

The interface is deliberately append-and-prune. There is no update and no delete by id, because a trail whose rows can be edited answers no question worth asking. The only way a record leaves is age, so retention is a stated policy rather than a way to lose one inconvenient row.

What is stored is the projection from event/sink, never event.Data. The payload of a TokenUpdate carries UnlockToken, the secret that clears a device passcode, so a trail that persisted raw payloads would be a long-lived copy of every device's secrets. Fields holds what the projection allowed and nothing else.

This package owns the contract and the domain type. Persistence is audit/inmem and audit/sqlstore, which keeps its own migration set, and audit/audittest is the suite both must pass.

References

  • Decision record 0038: docs/research/decisions/0038-persisted-audit-trail.md
  • Decision record 0037: docs/research/decisions/0037-event-sinks-and-redaction.md
  • Plan of record: docs/research/implementation_plan.md (phase 9)
  • Threat model: docs/security/threat-model.md (repudiation)
  • Apple documents no audit surface; what is recorded is the protocol vocabulary cited by service and ddm.

Index

Constants

View Source
const DefaultPageSize = 100

DefaultPageSize applies when Page.Limit is not positive.

View Source
const MaxPageSize = 1000

MaxPageSize bounds one page. A caller asking for more gets this many, so a cursor is always cheap to serve.

Variables

View Source
var (
	// ErrNotFound is a record that does not exist.
	ErrNotFound = errors.New("audit: not found")
	// ErrInvalid is a malformed argument: a bad cursor, an empty record, a
	// nil database.
	ErrInvalid = errors.New("audit: invalid argument")
)

Sentinel errors. They mirror the shapes storage and adminauth use so a handler can map them to a status without knowing which store answered.

Functions

This section is empty.

Types

type Page

type Page struct {
	Cursor string
	Limit  int
}

Page requests one page of records. An empty Cursor starts from the newest; Limit <= 0 uses DefaultPageSize.

func (Page) Size

func (p Page) Size() int

Size returns the page size to use: the requested limit, defaulted and bounded, so every backend agrees without repeating the rule.

type Query

type Query struct {
	// Type restricts to one event type.
	Type string
	// Actor restricts to one actor, which is how "what did this admin do"
	// and "what did break-glass do" are asked.
	Actor string
	// Enrollment restricts to one enrollment id.
	Enrollment string
	// Since and Until bound the record time. Zero means unbounded.
	Since, Until time.Time
}

Query filters a listing. The zero Query matches everything.

func (Query) Matches

func (q Query) Matches(rec Record) bool

Matches reports whether rec satisfies q. The in-memory store filters with it; the SQL stores build the equivalent WHERE clause, and the contract suite is what keeps the two agreeing.

type Record

type Record struct {
	// ID orders the trail and is the pagination cursor. It is assigned by
	// the store, ascending, so the newest record has the highest id.
	ID int64
	// At is when the event happened, from the publisher's clock.
	At time.Time
	// Type is the event type, for example "command-queued".
	Type string
	// Actor is who caused it: "device", "admin", a principal name, or
	// "break-glass".
	Actor string
	// Enrollment is the enrollment the event concerned, zero for events with
	// no enrollment such as an admin action.
	Enrollment mdm.EnrollmentID
	// Fields is the projected payload, stored as JSON.
	Fields map[string]any
}

Record is one persisted event. It is the projection from event/sink after it has been reduced to what may leave the process, never the raw payload: a TokenUpdate's payload carries the device unlock token, so the trail stores what the projection allowed and nothing else.

type Result

type Result[T any] struct {
	Items      []T
	NextCursor string
}

Result is one page of records with the cursor for the next page ("" at the end).

type Store

type Store interface {
	// Append writes one record and returns it with its assigned ID.
	// ErrInvalid for a record with no type.
	Append(ctx context.Context, rec Record) (Record, error)
	// List pages the trail newest first, filtered by q. An unparsable
	// cursor is ErrInvalid.
	List(ctx context.Context, q Query, p Page) (Result[Record], error)
	// Get returns one record. ErrNotFound when it does not exist.
	Get(ctx context.Context, id int64) (Record, error)
	// Prune removes records older than before and returns how many went.
	// Retention is the only way a record leaves the trail.
	Prune(ctx context.Context, before time.Time) (int, error)
}

Store persists the audit trail.

It is deliberately append-and-prune: there is no update and no delete by id. A trail whose rows can be edited answers no question worth asking, and the only removal is by age, so retention is a policy rather than a way to lose one inconvenient record.

Directories

Path Synopsis
Package audittest is the contract every audit backend must satisfy, and a store that fails on demand.
Package audittest is the contract every audit backend must satisfy, and a store that fails on demand.
Package inmem is the in-memory audit trail: the backend every unit test uses and the one a deployment without a database falls back to.
Package inmem is the in-memory audit trail: the backend every unit test uses and the one a deployment without a database falls back to.

Jump to

Keyboard shortcuts

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