audit

package
v1.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package audit — redact.go implements sensitive field redaction for audit records.

Redaction is applied before every audit record is written to disk (in writeAuditWithAC in internal/check/handler.go). The default patterns cover the three most common credential leak vectors observed in agent tool outputs (T-04-05-02):

  1. Authorization: Bearer <token> — HTTP auth headers
  2. eyJ... — JWT tokens embedded in any string
  3. sk-proj-/sk-ant-/AKIA/ghp_/glpat- prefixes — common API key namespaces

All patterns are non-backtracking character classes (no nested quantifiers) to prevent catastrophic backtracking (T-04-05-07). Each pattern is pre-compiled once via defaultRedactPatterns().

applyRedaction is a pure function — it returns a new string, never modifies the input. RedactRecord returns a copy of the AuditRecord with sensitive fields replaced.

Package audit provides the Phase 1 NDJSON audit log: a Bumblebee-compatible append-only writer that records one record per policy decision with owner-only file permissions.

Every decision the hook handler makes — including fail-closed decisions — must be written here. The schema is Bumblebee-compatible (AUDT-01 minimum, CTLG-07 signedness provenance); full audit sinks (syslog, OTLP, query, export, rotation) are Phase 6 and deliberately out of scope.

Index

Constants

This section is empty.

Variables

View Source
var ErrSyslogNotSupported error // nil on linux/darwin

ErrSyslogNotSupported is returned on platforms that do not support syslog. It is defined here (linux/darwin build) as a nil sentinel; the windows stub defines the real error value. Both files share the same exported symbol name so callers can use errors.Is regardless of OS.

Note: the actual non-nil ErrSyslogNotSupported lives in syslog_stub.go (windows build). On linux/darwin we do not need it, but callers in sink.go reference it unconditionally, so we provide a nil var here for completeness. The errors.Is(err, ErrSyslogNotSupported) check in sink.go is safe: when NewSyslogSink succeeds on linux/darwin the var is nil and the check is never reached on the success path.

Functions

func DefaultRedactPatterns

func DefaultRedactPatterns() []redactPattern

DefaultRedactPatterns returns the default set of sensitive-field redaction patterns. Each pattern uses non-backtracking character classes (no nested quantifiers) to prevent catastrophic backtracking (T-04-05-07).

Patterns:

  1. Bearer tokens: Authorization: Bearer <token> → Authorization: Bearer [REDACTED]
  2. JWT tokens: eyJ<header>.<payload>.<sig> → [JWT_REDACTED]
  3. Common API key prefixes: sk-proj/sk-ant/AKIA/ghp_/glpat- → prefix[REDACTED]

WR-05: patterns are compiled once via sync.Once and reused on subsequent calls. This eliminates per-call regexp compilation overhead in the audit hot path (check handler + gateway handler both call this per request).

This function is exported so callers (check.writeAuditWithAC, gateway.writeAudit) can apply redaction at the single chokepoint before writing to disk.

func Export

func Export(ctx context.Context, r io.Reader, opts ExportOpts, out io.Writer) error

Export reads NDJSON records from r, applies the filters embedded in opts, and writes matching records to out in the requested format. Supported formats:

  • "ndjson" — raw NDJSON lines (delegates to Query)
  • "csv" — RFC 4180 CSV with a fixed header row
  • "otlp" — OpenTelemetry Logs OTLP/JSON envelope

Unknown formats return an error immediately without reading from r.

func HasSensitiveData

func HasSensitiveData(s string) bool

HasSensitiveData reports whether s matches any default redaction pattern. It is used in tests to verify that redaction is applied correctly.

func Query

func Query(ctx context.Context, r io.Reader, opts QueryOpts, out io.Writer) error

Query streams NDJSON lines from r, applies the filters in opts, and writes matching raw lines to out. It never re-marshals records — the raw line bytes are forwarded verbatim so downstream tooling can re-parse without loss.

Malformed lines are silently skipped; a summary count is printed after the loop when any lines were skipped. Query returns nil unless a context cancellation or write error occurs.

func RedactPatternsWith

func RedactPatternsWith(custom []string) ([]redactPattern, error)

RedactPatternsWith compiles the caller-supplied custom regex pattern strings and APPENDS them to DefaultRedactPatterns(), returning the combined set.

This is the chokepoint that makes config.GetRedactPatterns() (the `redact_patterns` config key) actually take effect. Without it, custom patterns were plumbed through config but silently inert — every redaction site used the hardcoded defaults (the LOW-severity "silently inert" finding). Callers that have config-supplied patterns should compile them through this helper and pass the result to RedactRecord:

patterns, err := audit.RedactPatternsWith(cfg.GetRedactPatterns())
if err != nil { /* surface the config error; fall back to defaults */ }
rec = audit.RedactRecord(rec, patterns)

Security invariant: custom patterns are ALWAYS appended to — never replace — the defaults. A defender (or an attacker who can influence config) therefore cannot weaken the built-in Bearer/JWT/API-key redaction by supplying a narrower or empty pattern list; they can only add additional redaction.

Each custom pattern is anchored with a custom replacement of "[REDACTED]". A pattern that fails to compile aborts with a wrapped error (the first bad pattern is reported); the caller decides whether to fall back to the defaults. The returned defaults slice is the shared sync.Once-compiled value; the combined slice is freshly allocated and safe for the caller to retain.

When custom is empty, RedactPatternsWith returns DefaultRedactPatterns() verbatim (no allocation) with a nil error.

func RedactString

func RedactString(s string) string

RedactString is a convenience helper for single-string redaction using the default patterns. It is used in tests and for ad-hoc redaction outside of audit records.

func RedactStringSlice

func RedactStringSlice(ss []string, patterns []redactPattern) []string

RedactStringSlice applies redaction to each element of ss and returns a new slice. Elements that do not match any pattern are returned unchanged.

func Rotate

func Rotate(auditPath string, maxBytes int64, retentionDays int) error

Rotate rotates the audit log at auditPath when its size exceeds maxBytes. Archives are named beekeeper.ndjson.1, beekeeper.ndjson.2, … up to .999. Archives older than retentionDays are deleted before the shift.

Rotate is NOT concurrent-safe on its own — the Writer mutex (added in Plan 03) serialises it. Rotate itself has no mutex.

func ValidateRemoteSinkEndpoint

func ValidateRemoteSinkEndpoint(endpoint string, requireHTTPS bool) error

Finding #12 (MEDIUM): the remote audit sink constructors POST the full record JSON to any URL with no validation. A misconfigured (or attacker-influenced) endpoint can turn the audit pipeline into an SSRF / credential-exfil channel — e.g. an http:// endpoint sends records in cleartext, and a link-local target like http://169.254.169.254/ reaches a cloud instance-metadata service.

ValidateRemoteSinkEndpoint fails CLOSED at construction so a misconfigured sink is rejected before any record leaves the host, rather than silently exfiltrating. It is a pure string check — it performs NO DNS resolution at construction (a hostname that only resolves to a private range at runtime is out of scope by design; we reject obvious SSRF targets by literal host).

Rejected:

  • non-https:// schemes (the remote sinks must use TLS in transit)
  • empty / whitespace-only hosts
  • loopback literals: localhost, 127.0.0.0/8, ::1
  • link-local literals: 169.254.0.0/16 (esp. the 169.254.169.254 metadata IP)
  • RFC 1918 private literals: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

requireHTTPS lets a future caller relax the TLS requirement; the OTLP and HTTPS audit sinks always pass true.

Types

type AuditRecord

type AuditRecord struct {
	RecordType     string              `json:"record_type"` // "policy_decision"
	RecordID       string              `json:"record_id"`
	Timestamp      string              `json:"timestamp"`    // RFC3339
	ScannerName    string              `json:"scanner_name"` // always "beekeeper"
	AgentName      string              `json:"agent_name"`
	ToolName       string              `json:"tool_name"`
	Decision       string              `json:"decision"` // allow|warn|block|alert (alert = Sentry detection-only, set by the corpus emitter in Phase 23)
	Reason         string              `json:"reason"`
	RuleIDs        []string            `json:"rule_ids"`
	CatalogMatches []CatalogProvenance `json:"catalog_matches"`
	Endpoint       string              `json:"endpoint"` // "check" in Phase 1
	// Phase 2 additions (CTLG-09):
	CorroborationCount int      `json:"corroboration_count"`
	SourcesAgreed      []string `json:"sources_agreed"`
	SourcesDissented   []string `json:"sources_dissented"`
	Quarantine         bool     `json:"quarantine,omitempty"`
	// Phase 4 additions (INTG-07): multi-agent lineage for forensic audit trail.
	AgentID       string   `json:"agent_id,omitempty"`
	ParentAgentID string   `json:"parent_agent_id,omitempty"`
	AgentDepth    int      `json:"agent_depth,omitempty"`
	AgentLineage  []string `json:"agent_lineage,omitempty"`
	// Phase 5 additions: sentry_alert record type (SLNX-08)
	SentryRuleID        string   `json:"sentry_rule_id,omitempty"`
	SentryRuleName      string   `json:"sentry_rule_name,omitempty"`
	SentrySeverity      string   `json:"sentry_severity,omitempty"`
	SentryBaselineMode  bool     `json:"sentry_baseline_mode,omitempty"`
	SentryProcessPID    uint32   `json:"sentry_process_pid,omitempty"`
	SentryProcessExe    string   `json:"sentry_process_exe,omitempty"`
	SentryParentChain   []string `json:"sentry_parent_chain,omitempty"`
	SentryFilesAccessed []string `json:"sentry_files_accessed,omitempty"`
	SentryNetworkDests  []string `json:"sentry_network_dests,omitempty"`
	SentryCorrelatedExt string   `json:"sentry_correlated_ext,omitempty"`
	SentryQuarantineRec bool     `json:"sentry_quarantine_recommended,omitempty"`
	// Phase 6 additions (LLMF-02, LLMF-03, LLMF-04)
	LLMFScanned    bool    `json:"llmf_scanned,omitempty"`
	LLMFScanKind   string  `json:"llmf_scan_kind,omitempty"` // prompt|code|alignment
	LLMFResult     string  `json:"llmf_result,omitempty"`    // clean|injection|unsafe|hijacked
	LLMFConfidence float64 `json:"llmf_confidence,omitempty"`
	LLMFLatencyMS  int64   `json:"llmf_latency_ms,omitempty"`
	// OriginalCommand / ReasonCode are general-purpose provenance fields. They
	// were introduced for the package-manager nudge (removed in v1.1.0) but are
	// ALSO used by the config_change record path (the dashboard settings panel and
	// `beekeeper config set` record the changed key + a human-readable summary
	// here), so they remain live — do NOT treat them as nudge-only.
	OriginalCommand string `json:"original_command,omitempty"`
	// Deprecated: nudge removed in v1.1.0; field retained for corpus schema compatibility, no longer populated.
	RewrittenCommand string `json:"rewritten_command,omitempty"`
	// ReasonCode carries a structured reason/key code. Still populated by the
	// config_change path (the changed config key).
	ReasonCode string `json:"reason_code,omitempty"`
	// Deprecated: nudge removed in v1.1.0; field retained for corpus schema compatibility, no longer populated.
	PMState string `json:"pm_state,omitempty"`
	// Deprecated: nudge removed in v1.1.0; field retained for corpus schema compatibility, no longer populated.
	NudgeAction string `json:"nudge_action,omitempty"`

	// SourceSurface is the branch key identifying which Beekeeper surface produced
	// this record. Valid values: hook|mcp_gateway|shim|file_watcher|sentry|scan.
	// Populated by the corpus emitter (Phase 23) from context, or by the Sentry
	// surface directly. (SCHEMA-01)
	SourceSurface string `json:"source_surface,omitempty"`

	// ClusterID binds correlated non-agent events (e.g. a Sentry correlation
	// window). Agent-mediated surfaces (hook/mcp_gateway/shim) adjudicate per
	// event; non-agent surfaces (file_watcher/sentry/scan) adjudicate per cluster.
	// Sentry surface may set this directly; the corpus emitter reads it. (SCHEMA-02)
	ClusterID string `json:"cluster_id,omitempty"`

	// RulesetVersion is the catalog snapshot version at decision time. Populated
	// by the policy loader. Recorded so schema/rule evolution is detectable. (SCHEMA-05)
	RulesetVersion string `json:"ruleset_version,omitempty"`

	// PostureOverrideAction names the graduated override the operator chose:
	// "allow_once" | "allow_always" | "enforce_block" | "enforce_warn". This is the
	// distinct-record discriminator the verifier and the audit viewer key on.
	PostureOverrideAction string `json:"posture_override_action,omitempty"`
	// PostureRule is the posture rule the override scopes to: "" (all rules, for an
	// all-rules allow), "release-age", "lifecycle", or "git-remote".
	PostureRule string `json:"posture_rule,omitempty"`
	// PostureEcosystem is the optional ecosystem the allow override is scoped to.
	PostureEcosystem string `json:"posture_ecosystem,omitempty"`
	// PosturePackage is the package an allow override exempts (empty for an enforce
	// override, which scopes to a rule, not a package).
	PosturePackage string `json:"posture_package,omitempty"`
}

AuditRecord is one NDJSON line in the audit log: a single policy decision with its provenance. ScannerName is always the literal "beekeeper" and Endpoint is "check" in Phase 1 (the only decision surface). RecordID and Timestamp are caller-supplied so that FromDecision stays a pure mapping and is trivially testable; the hook handler supplies real values at runtime.

Phase 2 additions (CTLG-09): CorroborationCount, SourcesAgreed, SourcesDissented, and Quarantine carry the full corroboration provenance so operators know exactly which sources agreed/dissented on every decision.

func FromDecision

func FromDecision(tc policy.ToolCall, d policy.Decision, recordID, timestamp string, ac policy.AgentContext) AuditRecord

FromDecision maps a policy Decision plus the originating tool call and caller-supplied metadata into an AuditRecord. It performs no I/O and reads no wall clock — recordID and timestamp are passed in verbatim — so it remains a pure, side-effect-free mapping that the hook handler and tests both rely on.

Phase 2 (CTLG-09): the catalog_matches slice is always present (non-nil), even when empty, so non-catalog decisions serialize as `"catalog_matches":[]` rather than `"catalog_matches":null`. The new corroboration fields are mapped directly from the Decision.

Phase 4 (INTG-07): ac carries agent lineage fields. Zero-value AgentContext{} produces no agent fields in the JSON output (all fields are omitempty).

func RedactRecord

func RedactRecord(rec AuditRecord, patterns []redactPattern) AuditRecord

RedactRecord returns a copy of rec with sensitive string values replaced by redaction placeholders. The following fields are redacted:

  • Reason: may contain credential snippets from policy engine messages
  • OriginalCommand: the verbatim agent-supplied Bash command (Phase 8 nudge, WR-01) — may carry a token/secret embedded in an install argument such as `npm install --registry=https://x:Bearer ...@host/`.
  • RewrittenCommand: the nudge-rewritten command (Phase 8) — derived from the original and may inherit the same secrets.
  • PMState: the flattened §9 PM-state string (Phase 8). It is structured detection metadata today, but it is redacted defensively so that no attacker-influenced data path bypasses redaction (WR-01).
  • SentryProcessExe: the path of the monitored process. May embed credential paths or tokens in argv-derived exe strings on some platforms (TM-D-03).
  • SentryCorrelatedExt: the extension ID correlated with a sentry alert. May contain publisher-supplied strings that embed tokens (TM-D-03).
  • SentryFilesAccessed: slice of file paths observed by the Sentry — may contain credential paths or secrets embedded in paths (TM-D-03).
  • SentryNetworkDests: slice of network destinations — may contain bearer tokens embedded in URL query strings or authority components (TM-D-03).
  • CatalogProvenance: struct slice — Reason and EntryID fields within each match may carry attacker-controlled strings from the catalog (TM-D-03).

Non-sensitive structural fields (RecordType, Decision, Timestamp, RuleIDs, boolean flags, numeric counters) are NOT redacted.

ToolInput is a map[string]any in policy.ToolCall (not in AuditRecord directly); string values would be redacted at the ToolCall layer if exposed here.

RedactRecord always returns a new AuditRecord — it never mutates the receiver.

func RedactRecordWithDefaults

func RedactRecordWithDefaults(rec AuditRecord) AuditRecord

RedactRecordWithDefaults returns a copy of rec with all default sensitive patterns applied. This is the corpus store's cross-package redaction entrypoint (Phase 23 prerequisite — research Finding 7 / T-22-02).

The unexported redactPattern type used by RedactRecord and DefaultRedactPatterns cannot cross package boundaries, so internal/corpus cannot call RedactRecord(rec, DefaultRedactPatterns()) directly. This wrapper exposes a cross-package-safe signature that takes only AuditRecord and returns AuditRecord, with no unexported types in the signature.

RedactRecordWithDefaults never mutates the input rec — it delegates to RedactRecord which always returns a new AuditRecord (copy semantics).

type CatalogProvenance

type CatalogProvenance struct {
	CatalogSource string `json:"catalog_source"`
	EntryID       string `json:"entry_id"`
	Ecosystem     string `json:"ecosystem"`
	Package       string `json:"package"`
	Version       string `json:"version"`
	Severity      string `json:"severity"`
	Signed        bool   `json:"signed"`
	// Phase 2 additions (CTLG-09):
	Corroborated   bool   `json:"corroborated"`
	Dissented      bool   `json:"dissented"`
	CatalogVersion string `json:"catalog_version"`
}

CatalogProvenance is the audit-record view of a single catalog hit. It mirrors policy.CatalogMatch field-for-field, including the Signed flag (CTLG-07), so the audit log records exactly which catalog source, entry, and signedness drove a decision.

Phase 2 additions (CTLG-09): Corroborated, Dissented, and CatalogVersion carry per-match provenance so each source's role in the corroboration decision is recorded in the forensic trail.

type ExportOpts

type ExportOpts struct {
	Format string // "ndjson" | "csv" | "otlp"
	QueryOpts
}

ExportOpts controls format and record filtering for Export.

type HTTPSink

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

HTTPSink delivers each AuditRecord as an individual HTTPS POST to the configured endpoint. The body is a single NDJSON line. Errors are treated as fire-and-forget: they are logged to stderr but never returned to the caller so a remote endpoint outage does not affect the local file audit trail.

func NewHTTPSink

func NewHTTPSink(endpoint string) *HTTPSink

NewHTTPSink returns an HTTPSink that POSTs each record to endpoint.

func (*HTTPSink) Close

func (s *HTTPSink) Close() error

Close is a no-op for HTTPSink; each record is sent immediately.

func (*HTTPSink) Write

func (s *HTTPSink) Write(rec AuditRecord) error

Write serialises rec as NDJSON and POSTs it to the endpoint.

type MultiSink

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

MultiSink fans out every Write and Close call to all registered sinks. Errors from individual sinks are not short-circuited: every sink always receives the call. The last non-nil error is returned.

func NewMultiSinkFromSinks

func NewMultiSinkFromSinks(sinks []Sink) *MultiSink

NewMultiSinkFromSinks returns a MultiSink that owns sinks.

func (*MultiSink) Close

func (m *MultiSink) Close() error

Close closes every sink; returns the last non-nil error.

func (*MultiSink) Write

func (m *MultiSink) Write(rec AuditRecord) error

Write delivers rec to every sink; returns the last non-nil error.

type OTLPSink

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

OTLPSink batches AuditRecords and flushes them as OTLP LogsData JSON to the configured endpoint via HTTP POST. Batches are flushed when they reach 100 records or when Close is called. Flush errors are logged to stderr and treated as fire-and-forget — they are never returned to the caller — so a remote collector outage does not affect the local file audit trail.

func NewOTLPSink

func NewOTLPSink(endpoint string) *OTLPSink

NewOTLPSink returns an OTLPSink that POSTs to endpoint.

func (*OTLPSink) Close

func (s *OTLPSink) Close() error

Close flushes any remaining records.

func (*OTLPSink) Write

func (s *OTLPSink) Write(rec AuditRecord) error

Write appends rec to the current batch. If the batch reaches 100 records it is flushed immediately (fire-and-forget on flush error).

type QueryOpts

type QueryOpts struct {
	Since    time.Time // zero value = no lower bound
	Agent    string    // empty = no filter
	Tool     string    // empty = no filter
	Decision string    // empty = no filter (allow|warn|block)
	Limit    int       // 0 = no limit
}

QueryOpts controls the streaming filter applied by Query.

type Sink

type Sink interface {
	Write(rec AuditRecord) error
	Close() error
}

Sink is the common interface for audit output targets. Every implementation must be safe for concurrent use (the Writer mutex guards calls from the file sink; remote sinks must manage their own concurrency).

func NewMultiSink

func NewMultiSink(auditPath string, cfg config.AuditConfig) (Sink, error)

NewMultiSink constructs the full sink graph for auditPath and the provided config.AuditConfig. A file-backed WriterSink is always created as the first sink. Additional sinks (syslog, otlp, https) are appended based on cfg.Sinks.

If NewSyslogSink returns ErrSyslogNotSupported the syslog sink is skipped with a warning printed to stderr; any other syslog error is returned to the caller. Remote sinks (OTLP, HTTPS) print a machine-readable warning so operators know audit data is leaving the local host.

func NewMultiSinkWithCorpus

func NewMultiSinkWithCorpus(auditPath string, auditCfg config.AuditConfig, corpusSink Sink) (Sink, error)

NewMultiSinkWithCorpus is identical to NewMultiSink but appends an additional caller-supplied corpus sink to the fan-out graph.

Design (OQ-1 resolution, Option C — least invasive):

  • NewMultiSink is UNCHANGED. Existing call sites are unaffected.
  • The corpus sink is passed as an audit.Sink INTERFACE (not as a concrete *corpus.StoreSink) to avoid an audit→corpus import cycle. The caller (cmd/beekeeper) constructs the corpus.StoreSink and passes it in as audit.Sink.
  • Fan-out semantics match MultiSink: every Write delivers to all sinks; errors from individual sinks are accumulated and the last non-nil error is returned. A corpus sink error does NOT prevent the file sink from receiving the record.

corpusSink may be nil (e.g. when cfg.Corpus.Enabled is false); in that case NewMultiSinkWithCorpus behaves identically to NewMultiSink.

type SyslogSink

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

SyslogSink delivers AuditRecords to a remote or local syslog daemon using RFC 5424 framing. The priority is LOG_LOCAL0|LOG_INFO (facility 16, severity 6).

func NewSyslogSink

func NewSyslogSink(address string) (*SyslogSink, error)

NewSyslogSink dials the syslog daemon at address. Address format: "udp:host:port", "tcp:host:port", or "host:port" (UDP default).

func (*SyslogSink) Close

func (s *SyslogSink) Close() error

Close closes the underlying syslog connection.

func (*SyslogSink) Write

func (s *SyslogSink) Write(rec AuditRecord) error

Write sends rec as an RFC 5424 syslog message. The MSG part is the full JSON representation of the record so no information is lost.

type Writer

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

Writer is an append-only NDJSON sink for AuditRecords. It enforces owner-only file permissions (0600 on Unix, owner-only DACL on Windows) on open and re-applies them after every write so that a recreated or externally-modified file is never left world-readable (Pitfall 5).

Phase 6 additions (AUDT-03/04): Writer now carries a sync.Mutex (safe for concurrent hook-handler calls), an optional maxBytes rotation threshold, and a list of additional remote sinks (syslog, OTLP, HTTPS) that receive a fan-out copy of each record after the local file write succeeds.

func NewWriter

func NewWriter(path string) (*Writer, error)

NewWriter opens (creating if needed) the audit log at path for appending and enforces owner-only permissions immediately. The parent directory is created with 0700 if absent. The file is opened with O_APPEND|O_CREATE|O_WRONLY: O_APPEND avoids truncating or recreating an existing log (Pitfall 5), so prior decision records are never lost.

NewWriter is equivalent to NewWriterWithOptions(path, 0, nil).

func NewWriterWithOptions

func NewWriterWithOptions(path string, maxBytes int64, sinks []Sink) (*Writer, error)

NewWriterWithOptions is like NewWriter but also configures log rotation and additional remote sinks. maxBytes controls when Rotate is called (0 = never). sinks receives a fan-out copy of each record after the file write; errors from remote sinks are logged to stderr and do not affect the caller's error return.

func (*Writer) Close

func (w *Writer) Close() error

Close closes the underlying audit-log file and all additional sinks.

func (*Writer) Write

func (w *Writer) Write(rec AuditRecord) error

Write marshals rec to a single NDJSON line and appends it to the audit log, then re-applies owner-only permissions. Permissions are re-enforced on every write so that if the file was recreated or its DACL reset between writes it is re-locked before the next record is observed (Pitfall 5).

Phase 6: Write acquires the Writer mutex before any I/O, making it safe for concurrent callers. After the file write, if maxBytes > 0 a rotation check is performed (errors are logged to stderr but do not fail the write). Finally, the record is fanned out to any additional sinks (remote sinks log errors to stderr internally and never surface them here).

Write returns any file-write or permissions error verbatim and never downgrades a decision on failure — the hook handler owns the fail-closed semantics for audit-write errors.

type WriterSink

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

WriterSink wraps an existing *Writer so it satisfies the Sink interface.

func NewWriterSink

func NewWriterSink(w *Writer) *WriterSink

NewWriterSink returns a Sink that delegates to w.

func (*WriterSink) Close

func (s *WriterSink) Close() error

Close closes the underlying Writer.

func (*WriterSink) Write

func (s *WriterSink) Write(rec AuditRecord) error

Write delegates to the underlying Writer.

Jump to

Keyboard shortcuts

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