audit

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: MIT Imports: 15 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

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 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

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 NewUUIDv7

func NewUUIDv7() (string, error)

NewUUIDv7 returns a UUID v7 string (time-ordered) using crypto/rand. Same generator that Append uses internally for unset Record.ID;

func ParseTrailer

func ParseTrailer(s string) (int64, string, bool)

ParseTrailer extracts the unix timestamp and short ID from a coily:// trailer value. Returns (ts, short, true) on a well-formed input. Accepts

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 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 cli-guard/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. Used as
	// the stable identifier in commit trailers (`coily://<ts>/<short>`),
	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"`
	// CommitScope binds this row to a commit-trailer query. Resolved from
	// --commit-scope (default "auto" = cwd's git toplevel). Empty means the
	CommitScope string `json:"commit_scope,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"`
	// 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"`
	// AuditParent is the audit-row ID of an enclosing invocation when this
	// invocation was spawned by another coily process across a host boundary
	AuditParent string `json:"audit_parent,omitempty"`
	// RemoteArgv carries the post-`--` argv slice for ssh-passthrough rows:
	// the remote coily sub-command and its arguments. Set by the consumer
	RemoteArgv []string `json:"remote_argv,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 ReadAll

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

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

func (Record) ShortID

func (r Record) ShortID() string

ShortID returns the 8-char base32 prefix of the raw UUID bytes. Used in the trailer suffix. Returns empty if ID is unset or unparseable.

func (Record) Trailer

func (r Record) Trailer() string

Trailer returns the canonical Audit-log trailer value for this record: `coily://<unix-ts>/<short-id>`. Empty if ID is unset. This is the form

func (Record) TrailerLine

func (r Record) TrailerLine() string

TrailerLine returns the full Audit-log trailer value rendered for a commit: `coily://<unix-ts>/<short-id> - <argv summary>`. The argv summary

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. cli-guard supplies the mechanism; the consumer (today: coily) supplies the patterns.

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
	// 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/coilysiren/cli-guard/audit"
)

func main() {
	path := filepath.Join(os.TempDir(), "cli-guard-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, CommitScope, 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/coilysiren/cli-guard/audit"
)

func main() {
	w := audit.NewWriter(filepath.Join(os.TempDir(), "cli-guard-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

Jump to

Keyboard shortcuts

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