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

audit

package
v0.253.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package audit writes one JSONL record per CLI invocation to an append-only log outside the working tree. Per SECURITY.md, the

Span projection of an audit record. umbra ships no OTel SDK, the way it ships no store SDK: see docs/audit-spans.md.

Index

Examples

Constants

View Source
const (
	EgressAllow = "allow"
	EgressDeny  = "deny"
)

Egress decision values.

View Source
const (
	DecisionAccept = "accept"
	DecisionReject = "reject"
)

Decision values for Record.Decision.

View Source
const (
	DataSecurityLow    = "low"
	DataSecurityMedium = "medium"
	DataSecurityHigh   = "high"
	DataSecurityMax    = "max"
)

Tier values for ProfileDecision.Coordinate.DataSecurity. Mirrored here so the redactor does not import the profile package and can

View Source
const (
	AttrVerb        = "umbra.verb"
	AttrDecision    = "umbra.decision"
	AttrOutcome     = "umbra.outcome"
	AttrExitCode    = "umbra.exit_code"
	AttrKind        = "umbra.kind"
	AttrRefused     = "umbra.refused"
	AttrSessionID   = "umbra.session_id"
	AttrRepoRoot    = "umbra.repo_root"
	AttrPolicySkip  = "umbra.policy_skipped"
	AttrCacheStatus = "umbra.cache"
	AttrEgressHosts = "umbra.egress_host_count"
)

Span attribute keys, namespaced so a collector can select umbra's rows without matching on a verb name. Stable: a consumer's dashboard queries them.

View Source
const (
	OutcomeOK       = "ok"
	OutcomeRefused  = "refused"
	OutcomeFailed   = "failed"
	OutcomeInternal = "internal"
)

Outcome classes, the distinction the exit-code taxonomy already draws and a log line does not: a refusal is not a failure.

View Source
const MaxStderrTailBytes = 2048

MaxStderrTailBytes caps Record.StderrTail so the audit row stays small even when a wrapped tool spews. 2 KiB is enough to carry the last few

View Source
const RedactedValue = "[REDACTED]"

RedactedValue is the literal replacement token used everywhere redaction applies. Exported so consumers can grep for it in tests.

View Source
const SessionIDEnv = "CLAUDE_SESSION_ID"

SessionIDEnv is the environment variable consulted as the fallback source for Record.SessionID when no context value is present. Parent processes

View Source
const UnknownVersion = "(devel)"

UnknownVersion is the Record.Version of a row written by a binary that carries no module version, such as one built from a checkout.

Variables

View Source
var ErrPathUnset = errors.New("audit: log path not configured")

ErrPathUnset is returned when Append is called on a Writer with empty Path.

Functions

func AtOrAfter added in v0.250.0

func AtOrAfter(release string) func(Record) bool

AtOrAfter selects rows from a build at or after release, a tag like "v0.30.0". A non-semver Version ("dev", "(devel)", empty) cannot be placed, so never matches.

func BuildVersion added in v0.250.0

func BuildVersion() string

BuildVersion reports the module version linked into this binary, or UnknownVersion when the toolchain recorded none.

func ByVersion added in v0.250.0

func ByVersion(version string) func(Record) bool

ByVersion selects the rows stamped with exactly version.

func OutcomeFor

func OutcomeFor(code int) string

OutcomeFor classifies an exit code into the four span outcomes. This is the whole point of the projection: refused and failed are different populations.

func RedactArgv

func RedactArgv(argv []string, tier string, p RedactPolicy) []string

RedactArgv applies the data-security tier to argv. Returns the argv to persist. At "low" the argv is returned unchanged. At

func RedactIdentifiersInString

func RedactIdentifiersInString(s string, tier string, p RedactPolicy) string

RedactIdentifiersInString replaces every IdentifierPatterns match with [REDACTED]. Runs at "medium" and stricter. Empty input or

func SessionIDFromContext

func SessionIDFromContext(ctx context.Context) string

SessionIDFromContext returns the session id attached to ctx via WithSessionID, or empty string if none is set. Nil-safe.

func WithSessionID

func WithSessionID(ctx context.Context, id string) context.Context

WithSessionID returns a copy of ctx carrying id as the audit session id. Empty id is a no-op (returns ctx unchanged) so callers can safely pass

Types

type CIContext

type CIContext struct {
	Provider    string `json:"provider"`
	ServerURL   string `json:"server_url,omitempty"`
	Repository  string `json:"repository"`
	EventName   string `json:"event_name"`
	EventRef    string `json:"event_ref"`
	PullRequest string `json:"pull_request,omitempty"`
	BaseRef     string `json:"base_ref,omitempty"`
	HeadRef     string `json:"head_ref,omitempty"`
	HeadSHA     string `json:"head_sha"`
	Workflow    string `json:"workflow,omitempty"`
	Job         string `json:"job,omitempty"`
	Actor       string `json:"actor,omitempty"`
	RunID       string `json:"run_id"`
	RunNumber   string `json:"run_number,omitempty"`
	RunAttempt  string `json:"run_attempt,omitempty"`
}

CIContext is provider-neutral CI attribution supplied by consumers. Audit preserves it but does not establish trust or validate identifiers.

type Coordinate

type Coordinate struct {
	DataSecurity    string `json:"data_security,omitempty"`
	BlastRadius     string `json:"blast_radius,omitempty"`
	NetworkEgress   string `json:"network_egress,omitempty"`
	FilesystemReach string `json:"filesystem_reach,omitempty"`
}

Coordinate mirrors umbra/profile.Coordinate as a JSON-stable snapshot. Duplicated here so audit.Record (the wire format) does not

type EgressRow

type EgressRow struct {
	Host       string `json:"host"`
	Decision   string `json:"decision"`
	BytesUp    int64  `json:"bytes_up"`
	BytesDown  int64  `json:"bytes_down"`
	DurationMS int64  `json:"duration_ms"`
}

EgressRow is one (parent-invocation, host) pair from the egress proxy. Decision is "allow" or "deny"; deny rows are produced when the host fails

func RedactEgressRows

func RedactEgressRows(rows []EgressRow, tier string) []EgressRow

RedactEgressRows applies the data_security tier to egress rows. At "high" Host is stripped to the eTLD+1 best-effort (strip leading

type ProfileDecision

type ProfileDecision struct {
	Allowed    bool       `json:"allowed"`
	Profile    string     `json:"profile,omitempty"`
	Source     string     `json:"source,omitempty"`
	Coordinate Coordinate `json:"coordinate"`
	Reason     string     `json:"reason,omitempty"`
}

ProfileDecision is the structured outcome of a per-session lockdown-profile evaluation. Allowed=false plus a non-nil verb.Spec

type Record

type Record struct {
	// ID is a UUID v7 (time-ordered) populated on Append if unset. The
	// stable identifier for an audit row, used to join cross-host records.
	ID        string   `json:"id,omitempty"`
	Timestamp int64    `json:"ts"`
	Version   string   `json:"version,omitempty"`
	Decision  string   `json:"decision"`
	Verb      string   `json:"verb"`
	Argv      []string `json:"argv"`
	ExitCode  int      `json:"exit_code"`
	Error     string   `json:"error,omitempty"`
	// StderrTail is a bounded last-N-bytes capture of the wrapped tool's
	// stderr, populated by pass-through verbs on non-zero exit so the audit
	StderrTail string `json:"stderr_tail,omitempty"`
	DurationMS int64  `json:"duration_ms,omitempty"`
	// RepoRoot is git rev-parse --show-toplevel of cwd at invocation time,
	// or empty if cwd was not inside a git repo. Forensic only: tells the
	RepoRoot string `json:"repo_root,omitempty"`
	// CWDSubprocess is os.Getwd() captured by buildBaseRecord at the
	// moment the subprocess saw the world. Differs from CWDAtInvocation
	CWDSubprocess string `json:"cwd_subprocess,omitempty"`
	// CWDAtInvocation is the consumer-resolved operator cwd, populated
	// from verb.Spec.ResolveInvokeCWD when set. Empty when the consumer
	CWDAtInvocation string `json:"cwd_at_invocation,omitempty"`
	// SessionID joins this row to the Claude Code (or other agent harness)
	// session that produced the invocation. Resolution order at write time:
	SessionID string `json:"session_id,omitempty"`
	// AuditOverride is set true when a repo verb ran with
	// --audit-override-dirty: the clean+synced gate refused but the
	AuditOverride bool `json:"audit_override,omitempty"`
	// WorkingTreeStatus is the truncated `git status --porcelain` output
	// captured when a repo verb ran with --audit-override-dirty. Empty for
	WorkingTreeStatus string `json:"working_tree_status,omitempty"`
	// CI carries consumer-validated continuous-integration attribution.
	// The audit package preserves this evidence but does not establish trust.
	CI *CIContext `json:"ci,omitempty"`
	// Egress carries one row per host contacted by the wrapped subprocess
	// when the verb runs through the per-invocation HTTP CONNECT proxy
	Egress []EgressRow `json:"egress,omitempty"`
	// ProfileDecision captures the per-session lockdown-profile evaluation
	// for this verb, when a consumer has wired verb.Spec.OnEvaluate. Absent
	ProfileDecision *ProfileDecision `json:"profile_decision,omitempty"`
	// PolicySkipped is true when the shell-metacharacter validator was
	// bypassed for this invocation. Set by consumers whose verb wiring
	PolicySkipped bool `json:"policy_skipped,omitempty"`
	// Cache is the TTL-cache disposition of a cache-eligible action, e.g. "hit"
	// when a collect served from cache. Empty when no cache was consulted.
	Cache string `json:"cache,omitempty"`
}

Record is one line in the audit log. Timestamp is unix seconds (int64), JSON-encoded as a number. Easier to sort and diff than RFC3339 strings.

func Filter added in v0.250.0

func Filter(records []Record, keep func(Record) bool) []Record

Filter returns the records keep accepts, in order. Rows from before Record.Version existed have an empty Version and match no version selector.

func ReadAll

func ReadAll(r io.Reader) ([]Record, error)

ReadAll decodes every record from r. Useful for tests and for `audit tail`-style verbs.

func (Record) SpanOf

func (r Record) SpanOf() Span

SpanOf projects a Record onto a Span. It reads the record as written, after redaction, so a sink can never see a value the JSONL would not carry.

type RedactPolicy

type RedactPolicy struct {
	// SecretFlagPatterns is a list of flag-name prefixes (with leading
	// dashes). Matching is "argv token starts with this prefix" so
	SecretFlagPatterns []string

	// IdentifierPatterns is a list of compiled regexes. Any match in
	// Error/StderrTail/Reason fields gets replaced with [REDACTED].
	IdentifierPatterns []*regexp.Regexp
}

RedactPolicy carries the patterns the consumer wants applied. umbra supplies the mechanism; the consumer supplies the patterns.

type Sink

type Sink interface {
	Emit(Span)
}

Sink receives every appended record, for a consumer exporting spans. It runs after the JSONL write, so the durable log never depends on it.

type SinkFunc

type SinkFunc func(Span)

SinkFunc adapts a func to Sink.

func (SinkFunc) Emit

func (f SinkFunc) Emit(s Span)

Emit calls f.

type Span

type Span struct {
	Name       string
	StartUnix  int64
	DurationMS int64
	Error      bool
	Attributes map[string]any
}

Span is one audit record projected onto a tracing span: a name, whether it represents an error, and flat attributes. No SDK type appears here.

type Writer

type Writer struct {
	// Path is the JSONL file. Must be set.
	Path string
	// Now is used for timestamps. Tests override. Defaults to time.Now.
	Now func() time.Time
	// MaxSizeMB is the rotation trigger. Zero uses lumberjack's default (100).
	MaxSizeMB int
	// MaxBackups caps the number of rotated files retained. Zero keeps all.
	MaxBackups int
	// MaxAgeDays prunes rotated files older than this. Zero disables.
	MaxAgeDays int
	// Compress gzips rotated files.
	Compress bool

	// Version stamps rows whose Record.Version is unset, else BuildVersion.
	// Generated binaries set it to their -ldflags version.
	Version string

	// Sinks receive each appended record as a Span, after the JSONL write.
	// A panicking or slow sink is the consumer's problem, never the log's.
	Sinks []Sink
	// contains filtered or unexported fields
}

Writer appends records to a JSONL file. The zero value is unusable. Use NewWriter or set Path explicitly.

func NewWriter

func NewWriter(path string) *Writer

NewWriter returns a Writer with Now set to time.Now. Rotation fields default to zero (lumberjack defaults apply) and can be set by the caller.

Example

The most basic shape: open a writer, append one record, close. The writer creates and rotates the JSONL file under the hood.

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/coilyco/umbra/pkg/audit"
)

func main() {
	path := filepath.Join(os.TempDir(), "umbra-example.jsonl")
	w := audit.NewWriter(path)
	defer func() { _ = w.Close() }()

	if err := w.Preflight(); err != nil {
		fmt.Println("preflight:", err)
		return
	}

	rec := audit.Record{Verb: "hello", Argv: []string{"hello", "world"}}
	if err := w.Append(rec); err != nil {
		fmt.Println("append:", err)
		return
	}
	fmt.Println("ok")
}
Output:
ok

func (*Writer) Append

func (w *Writer) Append(r Record) error

Append writes one record as a JSON line. Timestamp is populated from the Writer if unset on the Record (zero).

func (*Writer) Close

func (w *Writer) Close() error

Close releases the underlying log file. Safe to call multiple times and on a Writer that was never used. Call at process exit if you want to be

func (*Writer) Preflight

func (w *Writer) Preflight() error

Preflight ensures the audit directory exists with 0700 perms and that the target path is writable. Call at startup so a broken config blows up

func (*Writer) SetRedactPolicy

func (w *Writer) SetRedactPolicy(p RedactPolicy)

SetRedactPolicy installs the consumer-supplied pattern list. Safe to call once at Runner construction. Calls during a hot pipeline

func (*Writer) Wrap

func (w *Writer) Wrap(ctx context.Context, base Record, fn func() error) error

Wrap records an invocation by running fn and logging the result. base supplies caller-set fields (Verb, Argv, RepoRoot, Version);

Example

Wrap runs a function and records one audit row per invocation, regardless of whether the function returns an error.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"

	"github.com/coilyco/umbra/pkg/audit"
)

func main() {
	w := audit.NewWriter(filepath.Join(os.TempDir(), "umbra-wrap-example.jsonl"))
	defer func() { _ = w.Close() }()
	_ = w.Preflight()

	err := w.Wrap(context.Background(), audit.Record{Verb: "demo"}, func() error {
		fmt.Println("doing work")
		return nil
	})
	fmt.Println("err:", err)
}
Output:
doing work
err: <nil>

func (*Writer) WrapHook

func (w *Writer) WrapHook(ctx context.Context, base Record, fn func() error, onComplete func(*Record)) error

WrapHook is Wrap with an optional onComplete callback. The hook runs after fn returns and before the record is appended; it gets a pointer

Directories

Path Synopsis
Package otelsink emits umbra audit records as OpenTelemetry spans.
Package otelsink emits umbra audit records as OpenTelemetry spans.

Jump to

Keyboard shortcuts

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