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 ¶
- Constants
- Variables
- func NewUUIDv7() (string, error)
- func ParseTrailer(s string) (int64, string, bool)
- func RedactArgv(argv []string, tier string, p RedactPolicy) []string
- func RedactIdentifiersInString(s string, tier string, p RedactPolicy) string
- func SessionIDFromContext(ctx context.Context) string
- func WithSessionID(ctx context.Context, id string) context.Context
- type Coordinate
- type EgressRow
- type ProfileDecision
- type Record
- type RedactPolicy
- type Writer
- func (w *Writer) Append(r Record) error
- func (w *Writer) Close() error
- func (w *Writer) Preflight() error
- func (w *Writer) SetRedactPolicy(p RedactPolicy)
- func (w *Writer) Wrap(ctx context.Context, base Record, fn func() error) error
- func (w *Writer) WrapHook(ctx context.Context, base Record, fn func() error, onComplete func(*Record)) error
Examples ¶
Constants ¶
const ( EgressAllow = "allow" EgressDeny = "deny" )
Egress decision values.
const ( DecisionAccept = "accept" DecisionReject = "reject" )
Decision values for Record.Decision.
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
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
const RedactedValue = "[REDACTED]"
RedactedValue is the literal replacement token used everywhere redaction applies. Exported so consumers can grep for it in tests.
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 ¶
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 ¶
NewUUIDv7 returns a UUID v7 string (time-ordered) using crypto/rand. Same generator that Append uses internally for unset Record.ID;
func ParseTrailer ¶
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 ¶
SessionIDFromContext returns the session id attached to ctx via WithSessionID, or empty string if none is set. Nil-safe.
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 ¶
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 ¶
ReadAll decodes every record from r. Useful for tests and for `coily audit tail`-style verbs.
func (Record) ShortID ¶
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 ¶
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 ¶
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 ¶
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 ¶
Append writes one record as a JSON line. Timestamp is populated from the Writer if unset on the Record (zero).
func (*Writer) Close ¶
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 ¶
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 ¶
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>