audit

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Anchor

func Anchor(ctx context.Context, pool *pgxpool.Pool) error

Anchor records the current chain head (event count + head hash) so a future Verify can detect tail truncation or a re-seal. Call it after a successful Verify. It is a no-op on an empty log.

func VerifyAnchorSignature

func VerifyAnchorSignature(rec AnchorRecord, keys map[string]*rsa.PublicKey) error

VerifyAnchorSignature checks a record's signature against the public key named by its kid. Fails closed on an unknown kid.

Types

type AnchorRecord

type AnchorRecord struct {
	Count     int64  `json:"count"`      // events at the time of anchoring
	HeadHash  string `json:"head_hash"`  // hash of the head event
	HeadSeq   int64  `json:"head_seq"`   // seq of the head event
	CreatedAt int64  `json:"created_at"` // unix seconds when anchored
	KID       string `json:"kid"`        // signing key id (look up in the JWKS)
	Signature string `json:"signature"`  // base64 RS256 over canonicalAnchor(...)
}

AnchorRecord is a self-describing, signed checkpoint of the audit chain. Ship it to an append-only / off-box store; `legant audit anchor --check` validates the live database against it. The signature covers the canonical tuple below, so a shipped copy cannot be forged without the signing key.

func AnchorSigned

func AnchorSigned(ctx context.Context, pool *pgxpool.Pool, signer AnchorSigner) (AnchorRecord, error)

AnchorSigned verifies the chain, signs a checkpoint with the active key, stores it, and returns the signed record. Refuses to anchor a broken or empty chain.

type AnchorSigner

type AnchorSigner interface {
	ActiveKID() string
	ActiveSigner() *rsa.PrivateKey
}

AnchorSigner provides the active signing key — the SAME key published in the issuer's JWKS — so a signed anchor shares the existing trust root (the keystore satisfies this, as it does for the revocation feed).

type Event

type Event struct {
	ID           int64           `json:"id"`
	Seq          *int64          `json:"seq,omitempty"`
	ActorType    string          `json:"actor_type"`
	ActorID      string          `json:"actor_id,omitempty"`
	Action       string          `json:"action"`
	ResourceType string          `json:"resource_type,omitempty"`
	ResourceID   string          `json:"resource_id,omitempty"`
	OnBehalfOf   string          `json:"on_behalf_of_sub,omitempty"`
	ActorChain   []string        `json:"actor_chain,omitempty"`
	DelegationID *string         `json:"delegation_id,omitempty"`
	GrantJTI     string          `json:"grant_jti,omitempty"`
	OrgID        *string         `json:"org_id,omitempty"`
	Metadata     json.RawMessage `json:"metadata,omitempty"`
	CreatedAt    time.Time       `json:"created_at"`
}

Event is a read-model row of the audit trail, including the full delegation provenance (on_behalf_of_sub + actor_chain) of who acted for whom.

func Query

func Query(ctx context.Context, pool *pgxpool.Pool, f Filter) ([]Event, int64, error)

Query returns audit events matching the filter (newest first) and the total count of matches (before pagination). All filter values are bound as parameters — never interpolated — so the query is injection-safe.

type Filter

type Filter struct {
	ActorType    string
	ActorID      string
	Action       string
	OnBehalfOf   string
	DelegationID string
	GrantJTI     string
	Since        *time.Time
	Until        *time.Time
	Limit        int
	Offset       int
}

Filter narrows an audit query. Empty/zero fields are not applied.

type Handler

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

Handler serves the read side of the tamper-evident audit trail.

func NewHandler

func NewHandler(pool *pgxpool.Pool) *Handler

func (*Handler) List

func (h *Handler) List(w http.ResponseWriter, r *http.Request)

List returns audit events matching the query filters, newest first, paginated.

GET /audit?actor_type=&actor_id=&action=&on_behalf_of_sub=&delegation_id=
          &grant_jti=&since=&until=&limit=&offset=

func (*Handler) Routes

func (h *Handler) Routes() chi.Router

Routes mounts the audit read endpoints (intended behind admin authorization).

func (*Handler) VerifyChain

func (h *Handler) VerifyChain(w http.ResponseWriter, r *http.Request)

VerifyChain reports whether the audit hash chain is intact.

GET /audit/verify

type VerifyResult

type VerifyResult struct {
	Events    int64  // total events scanned
	OK        bool   // true if the whole chain verifies (and matches the last anchor)
	HeadHash  string // hash of the most recent event
	HeadSeq   int64  // seq of the most recent event
	BreakID   int64  // id of the first broken event (0 when OK or for a count regression)
	BreakKind string // "content" | "link" | "truncation" (rows removed vs the last anchor) | "prefix" (anchored prefix changed)
}

VerifyResult is the outcome of an audit hash-chain verification.

func CheckAgainstAnchor

func CheckAgainstAnchor(ctx context.Context, pool *pgxpool.Pool, ext AnchorRecord, keys map[string]*rsa.PublicKey) (VerifyResult, error)

CheckAgainstAnchor is the off-box tamper-evidence check: it verifies the live chain internally, validates the external anchor's signature, and then proves the live chain still matches that trusted anchor — so it detects truncation or a rewritten prefix even if the database's own audit_anchors table was forged.

func Verify

func Verify(ctx context.Context, pool *pgxpool.Pool) (VerifyResult, error)

Verify recomputes the audit hash chain in seq order and reports the first break. It reuses the in-database audit_row_hash function — the same one the insert trigger uses — so the verifier and the writer can never disagree.

Per-row checks:

  • content: stored hash == audit_row_hash(stored prev_hash, …fields). A mismatch means a field was edited in place.
  • link: stored prev_hash == the previous row's stored hash. A mismatch means a row was inserted, deleted, or reordered mid-chain.

Tail truncation and a full re-seal do not break the chain internally, so Verify additionally compares against the most recent anchor (see Anchor): a smaller event count is reported as "truncation", and a changed anchored prefix as "prefix".

Jump to

Keyboard shortcuts

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