audit

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Apr 10, 2026 License: Apache-2.0 Imports: 32 Imported by: 6

README

go-audit

go-audit

Structured, Schema-Enforced Audit Logging for Go Services

CI Go Reference Go Report Card License Status

🚀 Quick Start | ✨ Features | 📚 Examples | 📖 API Reference


🔍 Overview

go-audit is an audit logging library for Go. Audit logging is different from application logging — application logs record technical details for debugging (log/slog, zap), while audit logs record who did what, when, and to which resource for compliance, forensics, and accountability. If your application handles user data, authentication, or financial transactions, regulations like SOX, HIPAA, and GDPR require structured audit trails that application loggers don't enforce.

go-audit splits audit configuration into two layers:

  • Compile-time (taxonomy): Your event schema — which event types exist, which fields are required, what's optional — is defined in a YAML file and embedded into your binary with go:embed. A code generator (audit-gen) produces typed Go builders from this schema, so invalid event names and missing required fields are caught by the compiler, not at runtime. The taxonomy is your audit contract — it ships with the binary and cannot be changed without recompiling.

  • Runtime (outputs): Where events go — files, syslog, webhooks — is configured in a separate YAML file loaded at startup. Output destinations, routing filters, formatters, and sensitivity label exclusions can all change per environment without rebuilding.

The library validates events against the compiled taxonomy, delivers them asynchronously to multiple outputs simultaneously, and supports both JSON and CEF (Common Event Format) for SIEM integration.


✨ Key Features

Feature Description Docs
📋 Taxonomy Validation Define event schemas in YAML; every event validated at runtime Learn more
⚙️ Code Generation audit-gen generates typed builders; typos become compile errors Learn more
🛡️ CEF Format Common Event Format for SIEM platforms (Splunk, ArcSight, QRadar) Learn more
📄 JSON Format Line-delimited JSON with deterministic field order Learn more
📡 5 Output Types File (rotation), syslog (RFC 5424), webhook (NDJSON), Loki (stream labels), stdout — fan-out to all simultaneously Learn more
🔀 Event Routing Route events by category or severity to specific outputs Learn more
🔒 Sensitivity Labels Classify fields as PII/financial; strip per-output for compliance Learn more
Async Delivery Sub-microsecond enqueue; background drain goroutine Learn more
🌐 HTTP Middleware Automatically captures HTTP request fields for audit logging Learn more
📊 Metrics & Monitoring Track dropped events, delivery errors, and output health Learn more
📝 YAML Configuration Configure outputs in YAML with environment variable substitution Learn more
🔐 HMAC Integrity Per-output tamper detection with NIST-approved algorithms Learn more
🧪 Testing Support In-memory recorder with same validation as production Learn more

❓ Why Audit Logging?

Audit logging is not application logging. They serve fundamentally different purposes:

🔧 Application Logging 📋 Audit Logging
Purpose Debugging, troubleshooting, observability Compliance, forensics, accountability
Audience Developers, SREs Security teams, auditors, legal
Guarantees Best-effort — missing a log line is fine Schema-enforced — missing a field is a compliance gap
Retention Days to weeks Months to years (regulatory requirements)
Content Technical details (errors, stack traces) Who did what, when, to which resource, and why
Destinations Log aggregator (OpenSearch, Datadog, Loki) SIEM (Splunk, ArcSight, QRadar), compliance archives

If your application handles user data, financial transactions, authentication, or access control, regulations like SOX, HIPAA, GDPR, and PCI-DSS require audit trails. Application loggers (log/slog, zap, zerolog) do not enforce the structure, completeness, or delivery guarantees that compliance demands.


💡 Why go-audit?

No existing Go library provides schema-enforced audit logging with multi-output fan-out and SIEM-native format support:

  • 📋 Schema enforcement — every event validated against your taxonomy; missing required fields are rejected, not silently dropped
  • 🛡️ SIEM-native outputCEF format understood by Splunk, ArcSight, QRadar out of the box, alongside JSON for log aggregators
  • 📡 Multi-output fan-out — send events to files, syslog, webhooks, Loki, and stdout simultaneously, each with its own formatter and filters
  • 🔒 Sensitive field strippingclassify fields as PII or financial; strip them per-output for GDPR/PCI compliance
  • Non-blocking — sub-microsecond AuditEvent() calls; async delivery via a background drain goroutine with completeness monitoring
  • 🔌 No vendor lock-inpluggable metrics interface; no Prometheus, OpenTelemetry, or logging framework dependency in core

🚀 Quick Start

go-audit uses a YAML-first workflow: define your events in a taxonomy file, configure outputs in another, and generate type-safe Go code.

1️⃣ Define your taxonomy (taxonomy.yaml) - This is your source code.
version: 1

categories:
  write:
    severity: 3
    events:
      - user_create
  security:
    severity: 8
    events:
      - auth_failure

events:
  user_create:
    description: "A new user account was created"
    fields:
      outcome:  { required: true }
      actor_id: { required: true }

  auth_failure:
    description: "An authentication attempt failed"
    fields:
      outcome:  { required: true }
      actor_id: { required: true }

💡 Fields like target_id, source_ip, and reason are reserved standard fields — always available on every event without declaration. Use .SetTargetID(), .SetSourceIP(), etc. on any builder.

2️⃣ Configure outputs (outputs.yaml) - This is your config.
version: 1
app_name: my-service
host: "${HOSTNAME:-localhost}"

outputs:
  console:
    type: stdout

  siem_log:
    type: file
    file:
      path: "./audit-cef.log"
    formatter:
      type: cef
      vendor: "MyCompany"
      product: "MyApp"
      version: "1.0"
3️⃣ Generate type-safe code
go run github.com/axonops/go-audit/cmd/audit-gen \
  -input taxonomy.yaml \
  -output audit_generated.go \
  -package main

💡 go run fetches the tool automatically — no separate install needed.

4️⃣ Use the generated builders
// Required fields are constructor parameters — typos are compile errors
err := logger.AuditEvent(
    NewUserCreateEvent("alice", "success").
        SetTargetID("user-42"),
)

📚 For the complete runnable application (taxonomy loading, output configuration, logger creation), see examples/02-code-generation.

Output

📄 JSON (default formatter):

{"timestamp":"...","event_type":"user_create","severity":3,"app_name":"my-service","host":"prod-01","timezone":"UTC","pid":12345,"actor_id":"alice","outcome":"success","target_id":"user-42","event_category":"write"}

🛡️ CEF (SIEM formatter):

CEF:0|MyCompany|MyApp|1.0|user_create|A new user account was created|3|rt=... act=user_create deviceProcessName=my-service dvchost=prod-01 dvcpid=12345 suser=alice outcome=success cat=write

app_name, host, and pid are framework fields — set once in your outputs.yaml and automatically included in every event. The host, timezone, and pid values reflect your system — they will differ from the example above. app_name is set in outputs.yaml and stays constant across deployments. SIEMs use the CEF extension keys (deviceProcessName, dvchost, dvcpid) for automatic host-level correlation.


📦 Installation

Requires Go 1.26+.

go get github.com/axonops/go-audit             # core: logger, taxonomy, validation, formatters, stdout output
go get github.com/axonops/go-audit/file         # file output with rotation
go get github.com/axonops/go-audit/syslog       # RFC 5424 syslog (TCP/UDP/TLS/mTLS)
go get github.com/axonops/go-audit/webhook      # batched HTTP webhook with SSRF protection
go get github.com/axonops/go-audit/loki         # Grafana Loki with stream labels and gzip
go get github.com/axonops/go-audit/outputconfig # YAML-based output configuration

💡 The core module includes StdoutOutput (no additional dependency) and the audittest package for testing.


🏗️ Module Structure

Module Description
github.com/axonops/go-audit Core: Logger, taxonomy validation, JSON + CEF formatters, HTTP middleware, stdout output, fan-out, routing, audittest
github.com/axonops/go-audit/file File output with size-based rotation and gzip compression
github.com/axonops/go-audit/syslog RFC 5424 syslog output (TCP/UDP/TLS/mTLS)
github.com/axonops/go-audit/webhook Batched HTTP webhook with retry and SSRF protection
github.com/axonops/go-audit/loki Grafana Loki output with stream labels, gzip, multi-tenancy
github.com/axonops/go-audit/outputconfig YAML-based output configuration with env var substitution

Outputs are isolated in separate modules so the core library carries minimal third-party dependencies. Import only the outputs you use.


📚 Documentation

Resource Description
📖 Progressive Examples 18 examples from "hello world" to a complete CRUD API — every output type, TLS policy, routing, formatters, testing, and buffering
📘 API Reference pkg.go.dev documentation
🏗️ Architecture Pipeline design, module boundaries, thread safety
🤝 Contributing Development setup, PR process, code standards
📝 Changelog Release history and breaking changes
Error Reference Every error explained with recovery guidance
🔧 Troubleshooting Common problems and how to fix them
🔒 Security Policy Vulnerability reporting
Benchmarks Performance baseline and methodology

⚠️ Status

This library is pre-release (v0.x). The API may change between minor versions until v1.0.0. Pin your dependency version.


🙏 Acknowledgements

go-audit builds on excellent open-source projects. See ACKNOWLEDGEMENTS.md for full attribution and license details.


📄 License

Apache License 2.0 — Copyright 2026 AxonOps Limited.


Made with ❤️ by AxonOps

Documentation

Overview

Package audit provides a standalone, taxonomy-driven audit logging framework for Go applications.

The library validates every audit event against a consumer-defined taxonomy, delivers events asynchronously via a buffered channel, and fans out to multiple configurable outputs.

Multi-Module Structure

Output backends live in separate Go modules so consumers import only what they need:

  • github.com/axonops/go-audit — core (this package; depends on github.com/goccy/go-yaml for ParseTaxonomyYAML)
  • github.com/axonops/go-audit/file — file output with rotation
  • github.com/axonops/go-audit/syslog — RFC 5424 syslog (TCP/UDP/TLS)
  • github.com/axonops/go-audit/webhook — batched HTTP webhook
  • github.com/axonops/go-audit/outputconfig — YAML-based output configuration

StdoutOutput and the audittest package ship with core and require no additional import.

Stability

This package is pre-release (v0.x). The API is not yet stable; breaking changes may occur between minor versions until v1.0.0 is released.

Quick Start

Define a taxonomy describing your event types, create a logger with a stdout output, and emit an event:

taxonomy := audit.Taxonomy{
    Version: 1,
    Categories: map[string]*audit.CategoryDef{
        "write": {Events: []string{"user_create"}},
    },
    Events: map[string]*audit.EventDef{
        "user_create": {Required: []string{"outcome", "actor_id"}},
    },
}

stdout, err := audit.NewStdoutOutput(audit.StdoutConfig{})
if err != nil {
    log.Fatal(err)
}

logger, err := audit.NewLogger(
    audit.Config{Version: 1, Enabled: true},
    audit.WithTaxonomy(taxonomy),
    audit.WithOutputs(stdout),
)
if err != nil {
    log.Fatal(err)
}
defer func() {
    if err := logger.Close(); err != nil {
        log.Printf("audit close: %v", err)
    }
}()

// This prints a JSON line to stdout:
if err := logger.AuditEvent(audit.NewEvent("user_create", audit.Fields{
    "outcome":  "success",
    "actor_id": "alice",
})); err != nil {
    log.Printf("audit: %v", err)
}

Core API

Events

Outputs

  • Output — interface for audit event destinations (file, syslog, webhook, stdout)
  • StdoutOutput — writes events to stdout or any io.Writer; included in core
  • WithOutputs — registers unnamed outputs; WithNamedOutput for per-output routing
  • DeliveryReporter — optional interface for outputs that handle their own delivery metrics
  • MetadataWriter — optional interface for outputs that need structured per-event context (event type, severity, category, timestamp)
  • EventMetadata — per-event value type passed to [MetadataWriter.WriteWithMetadata]

Formatters

  • Formatter — interface for event serialisation
  • JSONFormatter — default; line-delimited JSON with deterministic field order
  • CEFFormatter — Common Event Format for SIEM integration (Splunk, ArcSight, QRadar)
  • FormatOptions — per-output context for sensitivity label exclusion

Taxonomy

Event Routing

  • EventRoute — per-output event filter (include/exclude categories, severity range)
  • ValidateEventRoute — validates route configuration against a taxonomy
  • MatchesRoute — checks whether an event matches a route filter

HTTP Middleware

  • Middleware — wraps an HTTP handler to capture request metadata for audit logging
  • Hints — per-request audit metadata populated by handlers via HintsFromContext
  • TransportMetadata — auto-captured HTTP fields (client IP, method, status code, duration)
  • EventBuilder — callback that transforms hints + transport into an audit event

Metrics

  • Metrics — optional instrumentation interface; track deliveries, drops, and errors

Code Generation Support

  • LabelInfo — sensitivity label descriptor; embedded in FieldInfo
  • FieldInfo — field descriptor with name, required flag, and labels; returned by generated builders
  • CategoryInfo — category descriptor with name and optional severity; returned by generated builders

Advanced

How Taxonomy Validation Works

The framework does not hardcode event types, field names, or categories. Consumers register their entire audit taxonomy at bootstrap via WithTaxonomy. The framework then validates every Logger.AuditEvent call against the registered definitions, catching missing required fields, unknown event types, and unrecognised field names at runtime.

Sensitivity Labels

Consumers MAY define sensitivity labels in SensitivityConfig to classify fields (e.g., "pii", "financial"). Labels are assigned to fields via three mechanisms: explicit per-event annotation in the YAML fields: map, global field name mapping in [SensitivityLabel.Fields], and regex patterns in [SensitivityLabel.Patterns]. Per-output field stripping is configured via WithNamedOutput using the excludeLabels parameter. Framework fields (timestamp, event_type, severity, duration_ms, event_category, app_name, host, timezone, pid) are never stripped.

Reserved Standard Fields

The library defines 31 well-known audit field names (actor_id, source_ip, reason, target_id, etc.) that are always accepted without taxonomy declaration. These reserved standard fields have generated setter methods on every builder and map to standard ArcSight CEF extension keys. See ReservedStandardFieldNames for the complete list.

Framework Fields

Every serialised event includes framework fields that identify the deployment: app_name, host, timezone (set via WithAppName, WithHost, WithTimezone or outputs YAML), and pid (auto-captured via os.Getpid). These fields cannot be stripped by sensitivity labels and are emitted in both JSON and CEF output.

Async Delivery

Events are enqueued to a buffered channel (configurable capacity, default 10,000) and drained by a single background goroutine. If the buffer is full, Logger.AuditEvent returns ErrBufferFull and the drop is recorded via the Metrics interface.

Graceful Shutdown

Logger.Close MUST be called when the logger is no longer needed. Failing to call Close leaks the drain goroutine and causes any buffered events to be lost. Close signals the drain goroutine to stop, waits up to [Config.DrainTimeout] for pending events to flush, then closes all outputs in sequence. Events still in the buffer when DrainTimeout expires are lost; a warning is emitted via log/slog. Close is idempotent via sync.Once.

Index

Examples

Constants

View Source
const (
	// ValidationStrict rejects unknown fields with an error; it is the
	// default when [Config.ValidationMode] is empty.
	ValidationStrict ValidationMode = "strict"

	// ValidationWarn logs a warning for unknown fields via [log/slog]
	// but accepts the event.
	ValidationWarn ValidationMode = "warn"

	// ValidationPermissive silently accepts unknown fields.
	ValidationPermissive ValidationMode = "permissive"

	// DefaultBufferSize is the default async channel capacity.
	DefaultBufferSize = 10_000

	// MaxBufferSize is the maximum allowed async channel capacity.
	// Values above this limit cause [NewLogger] to return an error
	// wrapping [ErrConfigInvalid].
	MaxBufferSize = 1_000_000

	// DefaultDrainTimeout is the default graceful shutdown deadline.
	DefaultDrainTimeout = 5 * time.Second

	// MaxDrainTimeout is the maximum allowed graceful shutdown deadline.
	// Values above this limit cause [NewLogger] to return an error
	// wrapping [ErrConfigInvalid]. Setting DrainTimeout too low on a
	// high-throughput system causes events to be lost at shutdown.
	MaxDrainTimeout = 60 * time.Second
)
View Source
const MaxTaxonomyInputSize = 1 << 20 // 1 MiB

MaxTaxonomyInputSize is the maximum YAML input size accepted by ParseTaxonomyYAML. Inputs exceeding this limit are rejected with ErrInvalidInput.

View Source
const MinSaltLength = 16

MinSaltLength is the minimum salt length in bytes for HMAC computation, per NIST SP 800-224 (minimum key length: 128 bits).

Variables

View Source
var (
	// ErrClosed is returned by [Logger.AuditEvent] when the logger has
	// been shut down via [Logger.Close]. Once returned, all subsequent
	// [Logger.AuditEvent] calls return ErrClosed immediately.
	ErrClosed = errors.New("audit: logger is closed")

	// ErrBufferFull is returned by [Logger.AuditEvent] when the async
	// buffer is at capacity and the event is dropped. Consumers SHOULD
	// treat this as a drop notification rather than a fatal error.
	// Increasing [Config.BufferSize] or reducing event emission rate
	// reduces the frequency of this error.
	ErrBufferFull = errors.New("audit: buffer full")

	// ErrDuplicateDestination is returned by [WithOutputs] and
	// [WithNamedOutput] when two outputs implement [DestinationKeyer]
	// and return the same key. This prevents accidental double-delivery
	// to the same file, syslog address, or webhook URL.
	ErrDuplicateDestination = errors.New("audit: duplicate destination")

	// ErrConfigInvalid is the sentinel error wrapped by all configuration
	// validation failures. Use [errors.Is] to test for it:
	//
	//	if errors.Is(err, audit.ErrConfigInvalid) { ... }
	ErrConfigInvalid = errors.New("audit: config validation failed")

	// ErrHandleNotFound is returned by [Logger.Handle], and wrapped in
	// the panic value of [Logger.MustHandle], when the requested event
	// type is not registered in the taxonomy.
	ErrHandleNotFound = errors.New("audit: event type not found")

	// ErrOutputClosed is returned by [Output.Write] when the output has
	// already been closed.
	ErrOutputClosed = errors.New("audit: output is closed")

	// ErrTaxonomyInvalid is the sentinel error wrapped by taxonomy
	// validation failures. Use [errors.Is] to test for it:
	//
	//	if errors.Is(err, audit.ErrTaxonomyInvalid) { ... }
	ErrTaxonomyInvalid = errors.New("audit: taxonomy validation failed")

	// ErrInvalidInput is returned by [ParseTaxonomyYAML] when the input
	// is structurally unsuitable — empty, larger than [MaxTaxonomyInputSize],
	// a multi-document YAML stream, or syntactically invalid. Taxonomy
	// content validation errors wrap [ErrTaxonomyInvalid] instead.
	ErrInvalidInput = errors.New("audit: invalid input")
)

Sentinel errors returned by the audit package. Use errors.Is to test for these in consumer code.

View Source
var ErrHijackNotSupported = errors.New("audit: underlying ResponseWriter does not support hijacking")

ErrHijackNotSupported is returned by the middleware's response writer Hijack method when the underlying http.ResponseWriter does not implement http.Hijacker.

Functions

func AppendPostFields added in v0.3.0

func AppendPostFields(data []byte, formatter Formatter, fields []PostField) []byte

AppendPostFields appends one or more post-serialisation fields to cached bytes. The formatter type determines the syntax: JSON: ,"key":"val" inserted before }\n CEF: key=val inserted before the newline.

func CheckSSRFAddress added in v0.3.0

func CheckSSRFAddress(address string, allowPrivate bool) error

CheckSSRFAddress validates that a resolved address is not blocked by SSRF policy. The address must be in host:port format.

func CheckSSRFIP added in v0.3.0

func CheckSSRFIP(ip net.IP, allowPrivate bool) error

CheckSSRFIP validates that an IP address is not blocked by SSRF policy. Exported for direct unit testing of IP classification.

func ComputeHMAC added in v0.3.0

func ComputeHMAC(payload, salt []byte, algorithm string) (string, error)

ComputeHMAC computes the HMAC for the given payload and returns the lowercase hex-encoded result. The algorithm must be one of the supported NIST-approved values (see SupportedHMACAlgorithms).

func DefaultCEFFieldMapping

func DefaultCEFFieldMapping() map[string]string

DefaultCEFFieldMapping returns a new map containing the built-in field mapping from audit field names to standard CEF extension keys. Each call returns a distinct map instance; callers may freely mutate the result. Consumers can use this as a base, add or override entries, and pass the result to [CEFFormatter.FieldMapping].

func IsReservedStandardField added in v0.3.0

func IsReservedStandardField(name string) bool

IsReservedStandardField reports whether name is a reserved standard field. Reserved standard fields are well-known audit field names (actor_id, source_ip, reason, etc.) that are always accepted without taxonomy declaration. Uses a precomputed map for O(1) lookup.

func MatchesRoute

func MatchesRoute(route *EventRoute, eventType, category string, severity int) bool

MatchesRoute reports whether an event should be delivered to an output with the given route. eventType is the event name, category is its taxonomy category, severity is the event's resolved severity (0-10). An empty route matches all events.

Severity filtering is an AND condition: the event must pass both the severity check and the category/event type check. Severity is checked first for performance — severity-only routes (the PagerDuty use case) short-circuit without entering the category/event type logic.

When pre-computed sets are available (route created via setRoute), category/event type lookups are O(1). Falls back to slices.Contains for routes constructed as direct struct literals.

func Middleware

func Middleware(logger *Logger, builder EventBuilder) func(http.Handler) http.Handler

Middleware returns HTTP middleware that captures transport metadata automatically and calls the EventBuilder after the handler returns. The builder transforms Hints (populated by the handler) and TransportMetadata into an audit event.

If logger is nil, the returned middleware is an identity function that passes requests through without auditing. This allows consumers to conditionally disable audit middleware without nil-checking at every call site.

Middleware panics if builder is nil. Passing a nil builder is a programming error: there is no recoverable behaviour when the event-building callback is absent. Pass a nil *Logger instead to disable auditing without removing the middleware.

Example
taxonomy := audit.Taxonomy{
	Version: 1,
	Categories: map[string]*audit.CategoryDef{
		"access": {Events: []string{"http_request"}},
	},
	Events: map[string]*audit.EventDef{
		"http_request": {
			Required: []string{"outcome"},
			Optional: []string{"status_code"},
		},
	},
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(taxonomy),
)
if err != nil {
	log.Fatal(err)
}
defer func() { _ = logger.Close() }()

// The EventBuilder transforms per-request hints into an audit event.
builder := func(hints *audit.Hints, transport *audit.TransportMetadata) (string, audit.Fields, bool) {
	return "http_request", audit.Fields{
		"outcome":     hints.Outcome,
		"method":      transport.Method,
		"path":        transport.Path,
		"status_code": transport.StatusCode,
	}, false
}

mw := audit.Middleware(logger, builder)
_ = mw // wrap your http.Handler with mw(handler)

fmt.Println("middleware created")
Output:
middleware created
Example (Router)
package main

import (
	"fmt"
)

func main() {
	// Middleware works with any router that supports
	// the func(http.Handler) http.Handler middleware pattern.
	//
	//   // net/http
	//   mux := http.NewServeMux()
	//   mux.Handle("/", handler)
	//   http.ListenAndServe(":8080", mw(mux))
	//
	//   // chi
	//   r := chi.NewRouter()
	//   r.Use(mw)
	//
	//   // gorilla/mux
	//   r := mux.NewRouter()
	//   r.Use(mw)

	fmt.Println("works with any router")
}
Output:
works with any router
Example (Skip)
taxonomy := audit.Taxonomy{
	Version: 1,
	Categories: map[string]*audit.CategoryDef{
		"access": {Events: []string{"http_request"}},
	},
	Events: map[string]*audit.EventDef{
		"http_request": {
			Required: []string{"outcome"},
			Optional: []string{},
		},
	},
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(taxonomy),
)
if err != nil {
	log.Fatal(err)
}
defer func() { _ = logger.Close() }()

// Skip health-check endpoints to reduce noise.
builder := func(hints *audit.Hints, transport *audit.TransportMetadata) (string, audit.Fields, bool) {
	if transport.Path == "/healthz" || transport.Path == "/readyz" {
		return "", nil, true // skip
	}
	return "http_request", audit.Fields{
		"outcome": hints.Outcome,
		"path":    transport.Path,
	}, false
}

mw := audit.Middleware(logger, builder)
_ = mw

fmt.Println("skip middleware created")
Output:
skip middleware created

func MigrateTaxonomy added in v0.3.0

func MigrateTaxonomy(t *Taxonomy) error

MigrateTaxonomy applies backwards-compatible migrations to older taxonomy versions. It returns an error wrapping ErrTaxonomyInvalid if the version is unsupported.

This function is called automatically by WithTaxonomy; it is exported so that ParseTaxonomyYAML and other callers can apply migration before calling ValidateTaxonomy. For v0.1.0 only version 1 exists, so this is scaffolding.

func NewSSRFDialControl added in v0.3.0

func NewSSRFDialControl(opts ...SSRFOption) func(string, string, syscall.RawConn) error

NewSSRFDialControl returns a net.Dialer Control function that checks every resolved IP address before a connection is established. Use it with net/http.Transport:

transport := &http.Transport{
    DialContext: (&net.Dialer{
        Control: audit.NewSSRFDialControl(),
    }).DialContext,
}

The Control function blocks connections to:

  • Loopback addresses (127.0.0.0/8, ::1)
  • Link-local addresses (169.254.0.0/16, fe80::/10)
  • Cloud metadata endpoints (169.254.169.254)
  • RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • IPv6 unique local addresses (fc00::/7)
  • Multicast addresses (224.0.0.0/4, ff00::/8)
  • Unspecified addresses (0.0.0.0, ::)

Use AllowPrivateRanges to permit private and loopback addresses.

func RegisterOutputFactory added in v0.3.0

func RegisterOutputFactory(typeName string, factory OutputFactory)

RegisterOutputFactory registers a factory for the given output type name (e.g. "file", "syslog", "webhook"). It is intended to be called from init() functions in output modules.

Registering the same name twice overwrites the previous factory. This allows consumers to replace init()-registered default factories with metrics-aware factories before calling the config loader.

RegisterOutputFactory panics if typeName is empty or factory is nil. These are programming errors that should be caught at startup.

func RegisteredOutputTypes added in v0.3.0

func RegisteredOutputTypes() []string

RegisteredOutputTypes returns a sorted list of all registered output type names. Useful for error messages suggesting available types.

func ReservedStandardFieldNames added in v0.3.0

func ReservedStandardFieldNames() []string

ReservedStandardFieldNames returns the well-known audit field names that are always available on any event without explicit taxonomy declaration. These fields are automatically accepted by the unknown-field check and have standard CEF extension key mappings. The returned slice is a fresh copy; callers may modify it safely.

func SupportedHMACAlgorithms added in v0.3.0

func SupportedHMACAlgorithms() []string

SupportedHMACAlgorithms returns the list of supported HMAC algorithm names for use in documentation and error messages.

func ValidateEventRoute

func ValidateEventRoute(route *EventRoute, taxonomy *Taxonomy) error

ValidateEventRoute checks that the route is well-formed: include and exclude fields are not mixed, severity fields are in range 0-10 and min does not exceed max, and all referenced categories and event types exist in the taxonomy.

func ValidateHMACConfig added in v0.3.0

func ValidateHMACConfig(cfg *HMACConfig) error

ValidateHMACConfig checks that an HMACConfig is valid. Returns an error wrapping ErrConfigInvalid if the config is enabled but has missing or invalid fields. Salt values are never included in error messages.

func ValidateTaxonomy added in v0.3.0

func ValidateTaxonomy(t Taxonomy) error

ValidateTaxonomy checks the taxonomy for internal consistency. It verifies version bounds, category-event references, severity ranges, field overlaps, reserved field names, and sensitivity label validity. Returns an error wrapping ErrTaxonomyInvalid containing all problems found, with deterministic output. Callers MUST use errors.Is to test for ErrTaxonomyInvalid.

func VerifyHMAC added in v0.3.0

func VerifyHMAC(payload []byte, hmacValue string, salt []byte, algorithm string) (bool, error)

VerifyHMAC verifies that the HMAC value matches the payload. The hmacValue is expected to be lowercase hex-encoded (as produced by ComputeHMAC). Returns (true, nil) if valid, (false, nil) if invalid, or (false, err) for parameter errors.

func WriteJSONString added in v0.3.0

func WriteJSONString(buf *bytes.Buffer, s string)

WriteJSONString writes the JSON-encoded form of s directly to buf, producing byte-for-byte identical output to encoding/json.Marshal for string values. This includes HTML-safe escaping of <, >, and &, and JavaScript-safe escaping of U+2028/U+2029 line/paragraph separators. Invalid UTF-8 is replaced with \ufffd.

Writing directly to the buffer eliminates the per-call allocation that json.Marshal incurs for its return value.

WriteJSONString is exported for use by output modules (e.g. loki) that construct JSON payloads and need allocation-free string escaping.

Types

type CEFFormatter

type CEFFormatter struct {
	// SeverityFunc maps event types to CEF severity (0-10). If nil,
	// taxonomy-defined severity is used via [EventDef.ResolvedSeverity].
	// Values are clamped to 0-10. Set SeverityFunc only to override
	// the taxonomy.
	SeverityFunc func(eventType string) int

	// DescriptionFunc maps event types to human-readable CEF
	// descriptions. If nil, [EventDef.Description] is used when
	// non-empty, falling back to the event type name.
	DescriptionFunc func(eventType string) string

	// FieldMapping maps audit field names to CEF extension keys. If nil,
	// [DefaultCEFFieldMapping] is used. If non-nil, entries are merged
	// with [DefaultCEFFieldMapping]: consumer entries override matching
	// defaults, and defaults not present in FieldMapping remain active.
	// To suppress all defaults, call [DefaultCEFFieldMapping], delete
	// unwanted entries, and pass the result. Unmapped fields use their
	// original audit field name as the extension key.
	FieldMapping map[string]string

	// Vendor is the CEF header vendor field (e.g. "AxonOps"). If empty,
	// the vendor position in the header is blank but the pipe
	// delimiters are preserved. SHOULD be non-empty for
	// standard-compliant CEF output.
	Vendor string

	// Product is the CEF header product field (e.g. "SchemaRegistry").
	// If empty, the product position is blank. SHOULD be non-empty.
	Product string

	// Version is the CEF header product version field (e.g. "1.0").
	// If empty, the version position is blank. SHOULD be non-empty.
	Version string

	// OmitEmpty controls whether zero-value fields are omitted from
	// extensions.
	OmitEmpty bool
	// contains filtered or unexported fields
}

CEFFormatter serialises audit events in Common Event Format (CEF).

The output format is:

CEF:0|{Vendor}|{Product}|{Version}|{eventType}|{description}|{severity}|{extensions}

Header fields use pipe (|) as a delimiter. Extension values use key=value pairs separated by spaces.

Escaping

Header fields escape backslash and pipe: \ -> \\, | -> \|. Newlines and carriage returns in headers are replaced with spaces. Extension values escape backslash, equals, newline, and CR: \ -> \\, = -> \=, newline -> \n (literal), CR -> \r (literal). All remaining C0 control characters (0x00-0x1F) are stripped.

Severity

Severity is determined by [CEFFormatter.SeverityFunc] if set. If nil, the taxonomy-defined severity is used via EventDef.ResolvedSeverity: event Severity (if non-nil) → first category Severity in alphabetical order (if non-nil) → 5. Values are clamped to the valid CEF range 0-10.

func (*CEFFormatter) Format

func (cf *CEFFormatter) Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)

Format serialises a single audit event as a CEF line using a single buffer for both header and extensions.

func (*CEFFormatter) SetFrameworkFields added in v0.3.0

func (cf *CEFFormatter) SetFrameworkFields(appName, host, timezone string, pid int)

SetFrameworkFields stores logger-wide framework metadata for emission in every CEF event. Called once at construction time.

type CategoryDef added in v0.3.0

type CategoryDef struct {
	// Severity is the default CEF severity (0-10) for all events in
	// this category. Nil means not set — events inherit the global
	// default (5). A non-nil pointer to 0 means explicitly severity 0.
	Severity *int

	// Events lists the event type names belonging to this category.
	Events []string
}

CategoryDef defines a taxonomy category with its member events and optional default severity.

type CategoryInfo added in v0.3.0

type CategoryInfo struct {
	Severity *int   // category-level severity, nil if not set
	Name     string // category name, e.g., "write"
}

CategoryInfo describes a category an event type belongs to.

type Config

type Config struct {
	// ValidationMode controls how unknown fields are handled.
	// One of [ValidationStrict], [ValidationWarn], or
	// [ValidationPermissive]. Empty defaults to [ValidationStrict].
	ValidationMode ValidationMode

	// DrainTimeout is the maximum time [Logger.Close] waits for
	// pending events to flush. Zero means [DefaultDrainTimeout] (5s).
	// Values above [MaxDrainTimeout] (60s) cause [NewLogger] to
	// return an error. Setting this too low on a high-throughput
	// system will cause events to be lost at shutdown.
	DrainTimeout time.Duration

	// Version is the config schema version. MUST be > 0; the zero
	// value causes [NewLogger] to return an error wrapping
	// [ErrConfigInvalid]. Set to 1 for all current consumers; this
	// field enables forward-compatible migrations when the config
	// schema changes in future library versions.
	Version int

	// BufferSize is the async channel capacity. Zero means
	// [DefaultBufferSize] (10,000). Values above [MaxBufferSize]
	// (1,000,000) cause [NewLogger] to return an error.
	BufferSize int

	// Enabled controls whether audit logging is active. When false
	// (the zero value), [NewLogger] returns a no-op logger that
	// discards all events.
	Enabled bool

	// OmitEmpty controls whether empty/nil/zero-value fields are
	// included in serialised output. When true, only non-zero fields
	// are serialised. When false (the zero value), all registered
	// fields are present. Consumers operating under compliance regimes
	// that require all registered fields SHOULD leave this as false.
	OmitEmpty bool
}

Config holds configuration for the audit Logger.

type DeliveryReporter added in v0.2.0

type DeliveryReporter interface {
	ReportsDelivery() bool
}

DeliveryReporter is an optional interface that Output implementations may satisfy to indicate they handle their own delivery metrics reporting. When satisfied and [DeliveryReporter.ReportsDelivery] returns true, the core logger skips its default per-event [Metrics.RecordEvent] calls for that output — the output is responsible for calling them after actual delivery.

type DestinationKeyer added in v0.3.0

type DestinationKeyer interface {
	DestinationKey() string
}

DestinationKeyer is an optional interface that Output implementations MAY satisfy to enable duplicate destination detection at construction time. When two outputs return the same key from DestinationKey, WithOutputs and WithNamedOutput return an error.

Returning an empty string from DestinationKey opts out of duplicate detection for that output.

Key format conventions by output type:

  • File: absolute filesystem path
  • Syslog: network address (host:port)
  • Webhook: full URL

Outputs that do not implement this interface (e.g. StdoutOutput) are silently skipped during destination dedup.

type Event added in v0.3.0

type Event interface {
	// EventType returns the event type name (e.g., "user_create").
	EventType() string
	// Fields returns the event's field key-value pairs.
	Fields() Fields
}

Event represents a typed audit event with its event type and fields. Implementations are generated by audit-gen for compile-time field safety. For dynamic use without code generation, see NewEvent.

The interface is intentionally minimal. Richer metadata (per-field descriptors, categories, description) lives on the generated builder structs directly, accessible via the concrete type.

func NewEvent added in v0.3.0

func NewEvent(eventType string, fields Fields) Event

NewEvent creates an untyped audit event. Prefer generated typed builders from audit-gen for compile-time field safety. This function exists for consumers who construct events dynamically or do not use code generation.

type EventBuilder

type EventBuilder func(hints *Hints, transport *TransportMetadata) (eventType string, fields Fields, skip bool)

EventBuilder is a callback that transforms per-request Hints and TransportMetadata into an audit event. The middleware calls it after the handler returns (or panics).

Return values:

  • eventType: the taxonomy event type name to pass to Logger.AuditEvent
  • fields: the audit event fields
  • skip: if true, no audit event is emitted for this request

type EventDef

type EventDef struct {
	// Categories lists the taxonomy categories this event belongs to
	// (e.g. ["write"], ["security", "access"]). Derived from the
	// [Taxonomy.Categories] map during parsing — not set by consumers.
	// Sorted alphabetically. May be empty for uncategorised events.
	Categories []string

	// Description is an optional human-readable explanation of what
	// this event type represents. It is informational metadata only
	// — it has no effect on validation, routing, or serialisation.
	// When present, [audit-gen] emits it as a Go comment above the
	// generated constant. Also used as the default CEF description.
	Description string

	// Severity is the event-level CEF severity (0-10). Nil means
	// inherit from the category. A non-nil pointer to 0 means
	// explicitly severity 0. Resolution: event → category → 5.
	Severity *int

	// Required lists field names that must be present in every
	// [Logger.AuditEvent] call for this event type. Missing required
	// fields always produce an error regardless of validation mode.
	Required []string

	// Optional lists field names that may be present. In strict
	// validation mode, any field not in Required or Optional
	// produces an error.
	Optional []string

	// FieldLabels maps field names to their resolved sensitivity labels,
	// represented as a set (map key = label name, value always struct{}).
	// Populated at taxonomy registration time from all three label
	// sources: explicit per-event annotation, global field name mapping,
	// and regex patterns. Nil when no sensitivity config is defined.
	// Read-only after construction — consumers MUST NOT modify this map.
	FieldLabels map[string]map[string]struct{}
	// contains filtered or unexported fields
}

EventDef defines a single audit event type in the taxonomy.

func (*EventDef) ResolvedSeverity added in v0.3.0

func (d *EventDef) ResolvedSeverity() int

ResolvedSeverity returns the effective severity for this event type. The value is precomputed during taxonomy registration and is always in the range 0-10. Resolution chain: event Severity (if non-nil) → first category Severity in alphabetical order (if non-nil) → 5. For events in multiple categories, set event-level Severity to avoid depending on alphabetical category ordering.

type EventMetadata added in v0.3.0

type EventMetadata struct {
	// EventType is the taxonomy event type name (e.g. "user_create").
	EventType string

	// Severity is the resolved severity (0-10) for this event.
	Severity int

	// Category is the delivery-specific category. Empty for
	// uncategorised events. When an event belongs to multiple
	// categories, each delivery pass has a different Category.
	Category string

	// Timestamp is the wall-clock time recorded at drain time.
	Timestamp time.Time
}

EventMetadata carries per-event context for outputs that need structured access to framework fields (e.g., for Loki labels or Elasticsearch index routing). The struct is constructed once per delivery pass in [deliverToOutputs] and passed by value to [MetadataWriter.WriteWithMetadata].

The struct is small (64 bytes on amd64), passed by value, and zero-allocation by design. All fields are read from existing local variables in the drain goroutine.

type EventRoute

type EventRoute struct {
	// IncludeCategories lists category names to allow. Events whose
	// category is in this list are delivered. Mutually exclusive with
	// ExcludeCategories and ExcludeEventTypes.
	IncludeCategories []string

	// IncludeEventTypes lists event type names to allow. Events whose
	// type is in this list are delivered regardless of category.
	// Mutually exclusive with ExcludeCategories and ExcludeEventTypes.
	IncludeEventTypes []string

	// ExcludeCategories lists category names to deny. Events whose
	// category is in this list are skipped. Mutually exclusive with
	// IncludeCategories and IncludeEventTypes.
	ExcludeCategories []string

	// ExcludeEventTypes lists event type names to deny. Events whose
	// type is in this list are skipped regardless of category.
	// Mutually exclusive with IncludeCategories and IncludeEventTypes.
	ExcludeEventTypes []string

	// MinSeverity sets a minimum severity threshold. Events with
	// severity below this value are not delivered. Nil means no
	// minimum filter. A non-nil pointer to 0 means "severity >= 0"
	// (effectively no filter). Severity filtering is an AND condition
	// with category/event type filtering.
	MinSeverity *int

	// MaxSeverity sets a maximum severity threshold. Events with
	// severity above this value are not delivered. Nil means no
	// maximum filter. Combined with MinSeverity to create a range.
	MaxSeverity *int
	// contains filtered or unexported fields
}

EventRoute restricts which events are delivered to a specific output. Routes operate in one of two mutually exclusive modes:

Include mode (allow-list): events are delivered only if their category is in [EventRoute.IncludeCategories] OR their event type is in [EventRoute.IncludeEventTypes]. The two fields form a union.

Exclude mode (deny-list): events are delivered unless their category is in [EventRoute.ExcludeCategories] OR their event type is in [EventRoute.ExcludeEventTypes]. The two fields form a union.

Setting both include and exclude fields on the same route is a bootstrap error. An empty route (all fields nil/empty) delivers all globally-enabled events.

Example (Exclude)
// Exclude mode: all events except reads are delivered.
route := audit.EventRoute{
	ExcludeCategories: []string{"read"},
}
fmt.Println("empty:", route.IsEmpty())
Output:
empty: false
Example (Include)
// Include mode: only security events are delivered to this output.
route := audit.EventRoute{
	IncludeCategories: []string{"security"},
}
fmt.Println("empty:", route.IsEmpty())
Output:
empty: false

func (*EventRoute) IsEmpty

func (r *EventRoute) IsEmpty() bool

IsEmpty reports whether all route fields are empty, meaning the output receives all globally-enabled events.

type EventType

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

EventType is a handle for a registered audit event type. It carries the event type name and a reference to the owning Logger, enabling audit calls without repeated string lookup. Obtain a handle via Logger.Handle or Logger.MustHandle.

func (*EventType) Audit

func (e *EventType) Audit(fields Fields) error

Audit emits an audit event using this handle's bound event type. Fields are wrapped in NewEvent internally.

func (*EventType) Name

func (e *EventType) Name() string

Name returns the event type name this handle represents.

type FieldInfo added in v0.3.0

type FieldInfo struct {
	Name     string      // field name, e.g., "email"
	Labels   []LabelInfo // sensitivity labels on this field
	Required bool        // true if the field must be present
}

FieldInfo describes a single field on an audit event type.

type Fields

type Fields = map[string]any

Fields is a typed alias for audit event field maps. Consumers pass field values as Fields to Logger.AuditEvent.

type FormatOptions added in v0.3.0

type FormatOptions struct {
	// ExcludedLabels is the set of sensitivity labels to exclude.
	// Set once at construction time; immutable after that.
	ExcludedLabels map[string]struct{}
	// FieldLabels maps field names to their resolved sensitivity labels.
	// Set by the library per-event from [EventDef.FieldLabels] before
	// calling Format. Implementations MUST NOT retain this pointer.
	FieldLabels map[string]map[string]struct{}
}

FormatOptions carries optional per-output context to the formatter. A nil *FormatOptions means no special handling — all fields are emitted. When non-nil, the formatter skips fields whose sensitivity labels overlap with ExcludedLabels.

The library sets FieldLabels per-event before calling [Formatter.Format]. Implementations MUST NOT retain the opts pointer or modify its fields beyond the duration of the Format call.

func (*FormatOptions) IsExcluded added in v0.3.0

func (o *FormatOptions) IsExcluded(fieldName string) bool

IsExcluded reports whether fieldName carries any label in the excluded set. Custom Formatter implementations should call this to honor sensitivity exclusions.

type Formatter

type Formatter interface {
	// Format serialises a single audit event into a wire-format byte
	// slice. Implementations MUST append a newline terminator; the
	// library passes the result directly to [Output.Write].
	//
	// ts is the wall-clock time recorded at drain time (not
	// submission). eventType is the registered event type name.
	// fields contains the caller-supplied key-value pairs. def is the
	// [EventDef] for eventType; it is never nil when called by the
	// library. opts carries per-output sensitivity exclusion context;
	// nil means no field exclusion. Use [FormatOptions.IsExcluded] to
	// check whether a field should be skipped. Implementations MUST
	// NOT retain the opts pointer beyond the Format call.
	//
	// A non-nil error causes the event to be dropped and
	// [Metrics.RecordSerializationError] to be called.
	Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)
}

Formatter serialises an audit event into a wire-format byte slice. Implementations MUST append a newline terminator. The library provides JSONFormatter and CEFFormatter.

Format is called from a single goroutine (the drain loop); implementations do not need to be safe for concurrent use.

type FrameworkFieldReceiver added in v0.3.0

type FrameworkFieldReceiver interface {
	SetFrameworkFields(appName, host, timezone string, pid int)
}

FrameworkFieldReceiver is an optional interface that Output implementations may satisfy to receive logger-wide framework fields (app_name, host, timezone, pid) at construction time. The library calls SetFrameworkFields once after all options are applied and before the first Write or WriteWithMetadata call.

This is the output-side analogue of FrameworkFieldSetter for formatters. Outputs that need framework fields for labelling or routing (e.g., Loki stream labels) implement this interface. Outputs that do not implement it are silently skipped.

type FrameworkFieldSetter added in v0.3.0

type FrameworkFieldSetter interface {
	SetFrameworkFields(appName, host, timezone string, pid int)
}

FrameworkFieldSetter is implemented by formatters that emit logger-wide framework metadata (app_name, host, timezone, pid) in serialised output. The library calls SetFrameworkFields once at construction time, after all options are applied and before the first Format call.

JSONFormatter and CEFFormatter implement this interface. Third-party formatters that do not implement it silently omit these fields.

type HMACConfig added in v0.3.0

type HMACConfig struct {
	// Enabled controls whether HMAC is computed for this output.
	// Default: false. Must be explicitly true.
	Enabled bool

	// SaltVersion is a user-defined identifier for the salt, included
	// in the output alongside the HMAC. Supports salt rotation —
	// consumers use this to look up the correct salt for verification.
	SaltVersion string

	// SaltValue is the raw salt bytes. MUST be at least MinSaltLength
	// (16 bytes / 128 bits). Never appears in logs or error messages.
	SaltValue []byte

	// Algorithm is the HMAC hash algorithm. Must be one of the
	// NIST-approved values: HMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512,
	// HMAC-SHA3-256, HMAC-SHA3-384, HMAC-SHA3-512.
	Algorithm string
}

HMACConfig holds per-output HMAC configuration. When Enabled is true, every event delivered to the output has an HMAC appended. The HMAC is computed over the final serialised payload (after field stripping and event_category append).

func (HMACConfig) GoString added in v0.3.0

func (c HMACConfig) GoString() string

GoString implements fmt.GoStringer to prevent salt leakage via %#v.

func (HMACConfig) String added in v0.3.0

func (c HMACConfig) String() string

String returns a safe representation that never includes the salt value.

type Hints

type Hints struct {
	// Extra holds arbitrary domain-specific fields. It is initialised
	// lazily by the handler. Keys and values are passed through to
	// the [EventBuilder] callback.
	Extra map[string]any

	// EventType is the audit event type name (e.g. "user_create").
	// If empty, the [EventBuilder] decides the event type.
	EventType string

	// Outcome is the high-level result: "success", "failure", "denied", etc.
	Outcome string

	// ActorID identifies the authenticated principal (user ID, service account, etc.).
	ActorID string

	// ActorType categorises the actor: "user", "service", "api_key", etc.
	ActorType string

	// AuthMethod describes how the actor authenticated: "bearer", "mtls", "session", etc.
	AuthMethod string

	// Role is the actor's role or permission level at the time of the request.
	Role string

	// TargetType categorises the resource being acted upon: "document", "user", etc.
	TargetType string

	// TargetID identifies the specific resource being acted upon.
	TargetID string

	// Reason is a human-readable justification for the action, if applicable.
	Reason string

	// Error holds an error message when the request fails.
	Error string
}

Hints carries mutable, per-request audit metadata through the request context. Handlers retrieve it with HintsFromContext and populate domain-specific fields (actor, target, outcome). The middleware reads these fields after the handler returns and passes them to the EventBuilder callback.

Each request receives its own *Hints allocation; there is no shared mutable state between concurrent requests.

func HintsFromContext added in v0.3.0

func HintsFromContext(ctx context.Context) *Hints

HintsFromContext retrieves the Hints from the request context. Returns nil if the request was not wrapped by Middleware.

Example
// Inside an HTTP handler wrapped by Middleware:
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	hints := audit.HintsFromContext(r.Context())
	if hints != nil {
		hints.ActorID = "user-42"
		hints.Outcome = "success"
		hints.TargetType = "document"
		hints.TargetID = "doc-99"
	}
	w.WriteHeader(http.StatusOK)
})

_ = handler // register with your router

fmt.Println("handler with hints")
Output:
handler with hints

type JSONFormatter

type JSONFormatter struct {
	// Timestamp controls the timestamp format. Empty defaults to
	// [TimestampRFC3339Nano].
	Timestamp TimestampFormat

	// OmitEmpty controls whether zero-value fields are omitted.
	OmitEmpty bool
	// contains filtered or unexported fields
}

JSONFormatter serialises audit events as line-delimited JSON.

Fields are emitted in deterministic order: framework fields first (timestamp, event_type, severity, duration_ms if present as time.Duration), then required fields (sorted), then optional fields (sorted), then any extra fields (sorted). Each event is terminated by a newline.

time.Duration values are converted to int64 milliseconds. Timestamps are rendered according to [JSONFormatter.Timestamp] (default TimestampRFC3339Nano).

func (*JSONFormatter) Format

func (jf *JSONFormatter) Format(ts time.Time, eventType string, fields Fields, def *EventDef, opts *FormatOptions) ([]byte, error)

Format serialises a single audit event as a JSON line.

func (*JSONFormatter) SetFrameworkFields added in v0.3.0

func (jf *JSONFormatter) SetFrameworkFields(appName, host, timezone string, pid int)

SetFrameworkFields stores logger-wide framework metadata for emission in every JSON event. Called once at construction time.

type LabelInfo added in v0.3.0

type LabelInfo struct {
	Name        string // label name, e.g., "pii"
	Description string // human-readable explanation
}

LabelInfo describes a sensitivity label defined in the taxonomy.

type Logger

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

Logger is the core audit logger. It validates events against a registered Taxonomy, filters by category and per-event overrides, and delivers events asynchronously to configured Output destinations.

The library uses log/slog for internal diagnostics (buffer drops, serialisation failures, output write errors). Consumers can configure the slog default handler to control this output.

A Logger is safe for concurrent use by multiple goroutines.

func NewLogger

func NewLogger(cfg Config, opts ...Option) (*Logger, error)

NewLogger creates a new audit Logger with the given configuration and options. A taxonomy MUST be provided via WithTaxonomy; NewLogger returns an error if none is supplied.

When [Config.Enabled] is false, NewLogger returns a valid no-op logger. All Logger.AuditEvent calls return nil immediately without validation or delivery.

NewLogger MUST NOT return a non-nil *Logger when cfg or the taxonomy is invalid. Config version migration runs before validation; a zero [Config.Version] returns an error wrapping ErrConfigInvalid.

Example
// Create a stdout output that writes to a buffer for this example.
var buf bytes.Buffer
stdout, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
	log.Fatal(err)
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version: 1,
		Categories: map[string]*audit.CategoryDef{
			"write": {Events: []string{"user_create"}},
		},
		Events: map[string]*audit.EventDef{
			"user_create": {Required: []string{"outcome", "actor_id"}},
		},
	}),
	audit.WithOutputs(stdout),
)
if err != nil {
	log.Fatal(err)
}

// Emit an event — it will be written to the buffer as a JSON line.
if err := logger.AuditEvent(audit.NewEvent("user_create", audit.Fields{
	"outcome":  "success",
	"actor_id": "alice",
})); err != nil {
	log.Fatal(err)
}

// Close drains the async buffer so all events are flushed.
if err := logger.Close(); err != nil {
	log.Fatal(err)
}

// The buffer now contains the JSON-serialised event.
fmt.Println("has event_type:", bytes.Contains(buf.Bytes(), []byte(`"event_type":"user_create"`)))
fmt.Println("has actor_id:", bytes.Contains(buf.Bytes(), []byte(`"actor_id":"alice"`)))
Output:
has event_type: true
has actor_id: true

func (*Logger) AuditEvent added in v0.3.0

func (l *Logger) AuditEvent(evt Event) error

AuditEvent validates and enqueues a typed audit event. Use generated event builders from audit-gen for compile-time field safety, or NewEvent for dynamic event construction.

AuditEvent returns ErrBufferFull if the async buffer is at capacity (the event is dropped), ErrClosed if the logger has been closed, or a descriptive error for validation failures. If the event's category is globally disabled (and no per-event override enables it), the event is silently discarded without error.

Example
var buf bytes.Buffer
stdout, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
	log.Fatal(err)
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version:    1,
		Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
		Events: map[string]*audit.EventDef{
			"doc_create": {Required: []string{"outcome"}},
		},
	}),
	audit.WithOutputs(stdout),
)
if err != nil {
	log.Fatal(err)
}

if err = logger.AuditEvent(audit.NewEvent("doc_create", audit.Fields{"outcome": "success"})); err != nil {
	fmt.Println("audit error:", err)
	return
}

if err = logger.Close(); err != nil {
	log.Fatal(err)
}

// The event is now in the buffer as a JSON line.
fmt.Println("has event_type:", bytes.Contains(buf.Bytes(), []byte(`"event_type":"doc_create"`)))
fmt.Println("has outcome:", bytes.Contains(buf.Bytes(), []byte(`"outcome":"success"`)))
Output:
has event_type: true
has outcome: true

func (*Logger) ClearOutputRoute

func (l *Logger) ClearOutputRoute(outputName string) error

ClearOutputRoute removes the per-output event route for the named output, causing it to receive all globally-enabled events.

ClearOutputRoute is safe for concurrent use with event delivery.

func (*Logger) Close

func (l *Logger) Close() error

Close shuts down the logger gracefully. Close MUST be called when the logger is no longer needed; failing to call Close leaks the drain goroutine and loses all buffered events.

Close signals the drain goroutine to stop, waits up to [Config.DrainTimeout] for pending events to flush, then closes all outputs in sequence.

Close is idempotent -- subsequent calls return nil (or the same error if an output failed to close on the first call).

Example
logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version:    1,
		Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
		Events: map[string]*audit.EventDef{
			"doc_create": {Required: []string{"outcome"}},
		},
	}),
)
if err != nil {
	log.Fatal(err)
}

// Best practice: defer Close immediately after creation.
defer func() {
	if err := logger.Close(); err != nil {
		log.Printf("audit close: %v", err)
	}
}()

fmt.Println("logger will be closed on function exit")
Output:
logger will be closed on function exit

func (*Logger) DisableCategory

func (l *Logger) DisableCategory(category string) error

DisableCategory disables all events in the named category. The category MUST exist in the registered taxonomy. Per-event overrides via Logger.EnableEvent take precedence over category state.

func (*Logger) DisableEvent

func (l *Logger) DisableEvent(eventType string) error

DisableEvent disables a specific event type regardless of its category's state. The event type MUST exist in the registered taxonomy. Per-event overrides take precedence over category state.

func (*Logger) EnableCategory

func (l *Logger) EnableCategory(category string) error

EnableCategory enables all events in the named category. The category MUST exist in the registered taxonomy. Per-event overrides via Logger.DisableEvent take precedence over category state.

Example
logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version: 1,
		Categories: map[string]*audit.CategoryDef{
			"read":  {Events: []string{"doc_read"}},
			"write": {Events: []string{"doc_create"}},
		},
		Events: map[string]*audit.EventDef{
			"doc_read":   {Required: []string{"outcome"}},
			"doc_create": {Required: []string{"outcome"}},
		},
	}),
)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if err := logger.Close(); err != nil {
		log.Printf("audit close: %v", err)
	}
}()

// "read" category is disabled by default. Enable it at runtime.
if err := logger.EnableCategory("read"); err != nil {
	fmt.Println("enable error:", err)
	return
}

fmt.Println("read category enabled")
Output:
read category enabled

func (*Logger) EnableEvent

func (l *Logger) EnableEvent(eventType string) error

EnableEvent enables a specific event type regardless of its category's state. The event type MUST exist in the registered taxonomy. Per-event overrides take precedence over category state.

func (*Logger) Handle

func (l *Logger) Handle(eventType string) (*EventType, error)

Handle returns an EventType handle for the named event type. The handle enables zero-allocation audit calls. Returns ErrHandleNotFound if the event type is not registered.

func (*Logger) MustHandle

func (l *Logger) MustHandle(eventType string) *EventType

MustHandle returns an EventType handle for the named event type. It panics with an error wrapping ErrHandleNotFound if the event type is not registered. Use Logger.Handle to receive the error instead of panicking.

Example
logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version:    1,
		Categories: map[string]*audit.CategoryDef{"write": {Events: []string{"doc_create"}}},
		Events: map[string]*audit.EventDef{
			"doc_create": {Required: []string{"outcome"}},
		},
	}),
)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if closeErr := logger.Close(); closeErr != nil {
		log.Printf("audit close: %v", closeErr)
	}
}()

// Get a handle for zero-allocation audit calls.
docCreate := logger.MustHandle("doc_create")

if err = docCreate.Audit(audit.Fields{"outcome": "success"}); err != nil {
	fmt.Println("audit error:", err)
	return
}

fmt.Println("handle name:", docCreate.Name())
Output:
handle name: doc_create

func (*Logger) OutputRoute added in v0.3.0

func (l *Logger) OutputRoute(outputName string) (EventRoute, error)

OutputRoute returns a copy of the current per-output event route for the named output. An unknown output name returns an error.

func (*Logger) SetOutputRoute

func (l *Logger) SetOutputRoute(outputName string, route *EventRoute) error

SetOutputRoute sets the per-output event route for the named output. The route is validated against the taxonomy; unknown categories or event types return an error. Mixed include/exclude routes return an error. An unknown output name returns an error.

SetOutputRoute is safe for concurrent use with event delivery.

Example
var buf bytes.Buffer
out, err := audit.NewStdoutOutput(audit.StdoutConfig{Writer: &buf})
if err != nil {
	log.Fatal(err)
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version: 1,
		Categories: map[string]*audit.CategoryDef{
			"write":    {Events: []string{"user_create"}},
			"security": {Events: []string{"auth_failure"}},
		},
		Events: map[string]*audit.EventDef{
			"user_create":  {Required: []string{"outcome"}},
			"auth_failure": {Required: []string{"outcome"}},
		},
	}),
	audit.WithNamedOutput(out, &audit.EventRoute{}, nil),
)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if closeErr := logger.Close(); closeErr != nil {
		log.Printf("audit close: %v", closeErr)
	}
}()

// Restrict output to security events only at runtime.
if err := logger.SetOutputRoute("stdout", &audit.EventRoute{
	IncludeCategories: []string{"security"},
}); err != nil {
	fmt.Println("route error:", err)
	return
}

fmt.Println("route set to security only")
Output:
route set to security only

type MetadataWriter added in v0.3.0

type MetadataWriter interface {
	WriteWithMetadata(data []byte, meta EventMetadata) error
}

MetadataWriter is an optional interface that Output implementations may satisfy to receive structured event metadata alongside pre-serialised bytes. When an output implements MetadataWriter, the library calls WriteWithMetadata instead of [Output.Write].

Implementations MUST NOT retain meta or take its address after returning. The library passes meta by value on the stack; retaining it forces heap allocation. The caller must not assume the value remains valid after return.

type Metrics

type Metrics interface {
	// RecordEvent records an event delivery attempt to the named output.
	// status is always one of the string literals "success" or "error";
	// implementers MAY assume no other value is passed.
	RecordEvent(output, status string)

	// RecordOutputError records a write error on the named output.
	RecordOutputError(output string)

	// RecordOutputFiltered records that a per-output event route filter
	// prevented an event from being delivered to the named output.
	// This is distinct from [Metrics.RecordFiltered], which records
	// global category/event filter drops before any output is reached.
	RecordOutputFiltered(output string)

	// RecordValidationError records that [Logger.AuditEvent] rejected an
	// event due to a validation failure: unknown event type, missing
	// required fields, or unknown fields in strict mode. The
	// eventType parameter is the event type string that was passed to
	// AuditEvent.
	RecordValidationError(eventType string)

	// RecordFiltered records that an event was silently discarded by
	// the global category/event filter. This is distinct from
	// [Metrics.RecordOutputFiltered] which tracks per-output route
	// filtering.
	RecordFiltered(eventType string)

	// RecordSerializationError records that the configured [Formatter]
	// returned an error (or panicked) when serialising an event. The
	// event is dropped when this occurs.
	RecordSerializationError(eventType string)

	// RecordBufferDrop records that an event was dropped because the
	// main async buffer was full.
	RecordBufferDrop()
}

Metrics is an optional instrumentation interface that consumers implement to collect audit pipeline telemetry. Pass an implementation via WithMetrics; pass nil to disable metrics collection.

The library never imports a concrete metrics library (Prometheus, OpenTelemetry, etc.). Consumers wire their own.

type Option

type Option func(*Logger) error

Option configures a Logger during construction via NewLogger.

func WithAppName added in v0.3.0

func WithAppName(name string) Option

WithAppName sets the application name emitted as a framework field in every serialised event. The value must be non-empty.

func WithFormatter

func WithFormatter(f Formatter) Option

WithFormatter sets the event serialisation formatter. If not provided, a JSONFormatter is created from the Config. Use this to configure a CEFFormatter or a custom Formatter implementation.

Example
cef := &audit.CEFFormatter{
	Vendor:  "MyCompany",
	Product: "MyApp",
	Version: "1.0",
	SeverityFunc: func(eventType string) int {
		if eventType == "auth_failure" {
			return 8
		}
		return 5
	},
}

logger, err := audit.NewLogger(
	audit.Config{Version: 1, Enabled: true},
	audit.WithTaxonomy(audit.Taxonomy{
		Version:    1,
		Categories: map[string]*audit.CategoryDef{"security": {Events: []string{"auth_failure"}}},
		Events: map[string]*audit.EventDef{
			"auth_failure": {Required: []string{"outcome"}},
		},
	}),
	audit.WithFormatter(cef),
)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if err := logger.Close(); err != nil {
		log.Printf("audit close: %v", err)
	}
}()

fmt.Println("CEF formatter configured")
Output:
CEF formatter configured

func WithHost added in v0.3.0

func WithHost(host string) Option

WithHost sets the hostname emitted as a framework field in every serialised event. The value must be non-empty and at most 255 bytes.

func WithMetrics

func WithMetrics(m Metrics) Option

WithMetrics sets the metrics recorder for the logger. If m is nil, or if WithMetrics is not called, metrics are silently discarded. Implementations MUST be safe for concurrent calls from the drain goroutine.

func WithNamedOutput

func WithNamedOutput(output Output, route *EventRoute, formatter Formatter, excludeLabels ...string) Option

WithNamedOutput adds a single named output with an optional EventRoute and per-output Formatter. The route restricts which events are delivered to this output. If formatter is nil, the logger's default formatter is used.

excludeLabels specifies sensitivity labels whose fields should be stripped from events before delivery to this output. When non-empty, the taxonomy MUST define a SensitivityConfig and every label in excludeLabels MUST be defined within it; NewLogger returns an error if either condition is violated. An empty slice means no field stripping — the output receives all fields. Framework fields (timestamp, event_type, severity, duration_ms) are never stripped.

WithNamedOutput MUST NOT be combined with WithOutputs; if WithOutputs was already applied, WithNamedOutput returns an error.

Output names MUST be unique across all outputs; duplicate names cause NewLogger to return an error. Duplicate destinations are also detected via DestinationKeyer. Routes are validated against the taxonomy after all options have been applied.

func WithOutputHMAC added in v0.3.0

func WithOutputHMAC(name string, cfg *HMACConfig) Option

WithOutputHMAC configures HMAC on a named output. Used by the outputconfig loader to apply HMAC settings after output registration. The config is validated — invalid configs (short salt, unknown algorithm) cause NewLogger to return an error.

func WithOutputs

func WithOutputs(outputs ...Output) Option

WithOutputs sets the output destinations for the logger. Events are fanned out to all provided outputs. Each output receives all globally-enabled events (no per-output filtering). Use WithNamedOutput to configure per-output event routes or formatters.

WithOutputs MUST NOT be combined with WithNamedOutput; mixing the two returns an error. Duplicate output destinations are also detected: if two outputs implement DestinationKeyer and return the same key, WithOutputs returns an error. If no outputs are configured, events are validated and filtered but silently discarded.

func WithStandardFieldDefaults added in v0.3.0

func WithStandardFieldDefaults(defaults map[string]string) Option

WithStandardFieldDefaults sets deployment-wide default values for reserved standard fields. Defaults are applied in Logger.AuditEvent before validation — a default satisfies required: true constraints. Per-event values always override defaults (key existence check, not zero value). This option may be called at most once; a second call returns an error.

func WithTaxonomy

func WithTaxonomy(t Taxonomy) Option

WithTaxonomy registers the event taxonomy for validation. This option is required; NewLogger returns an error if no taxonomy is provided. WithTaxonomy SHOULD be called exactly once per NewLogger call. Calling it more than once replaces the taxonomy and resets all runtime category and event overrides established by the previous call.

The taxonomy is validated and pre-computed at construction time.

func WithTimezone added in v0.3.0

func WithTimezone(tz string) Option

WithTimezone sets the timezone name emitted as a framework field in every serialised event. The value must be non-empty and at most 64 bytes. If not set, no timezone field is emitted.

type Output

type Output interface {
	// Write sends a single serialised audit event to the output.
	// data is a complete, newline-terminated byte slice. Write is
	// called from a single goroutine; concurrent calls from the
	// library will not occur. Implementers MAY assume single-caller
	// access.
	Write(data []byte) error

	// Close flushes any buffered data and releases resources. The
	// library guarantees Write will not be called after Close. Close
	// is called exactly once by [Logger.Close].
	Close() error

	// Name returns a human-readable identifier for the output,
	// used in log messages and metrics labels.
	Name() string
}

Output is the interface that audit event destinations MUST implement. All outputs receive pre-serialised bytes (JSON, CEF, or a custom format chosen via WithFormatter). The library will provide built-in implementations for stdout, file, syslog, and webhook.

func WrapOutput added in v0.3.0

func WrapOutput(inner Output, name string) Output

WrapOutput wraps an Output with a consumer-chosen name. The returned output delegates all methods to the inner output except [Output.Name], which returns the provided name.

The returned output always satisfies DestinationKeyer, DeliveryReporter, MetadataWriter, and FrameworkFieldReceiver regardless of the inner output. When the inner output does not implement these interfaces, the wrapper returns zero-value behaviour: empty string for DestinationKey, false for ReportsDelivery, delegation to Write for WriteWithMetadata, and no-op for SetFrameworkFields.

type OutputFactory added in v0.3.0

type OutputFactory func(name string, rawConfig []byte, coreMetrics Metrics) (Output, error)

OutputFactory creates a named Output from raw YAML configuration bytes and core pipeline metrics.

name is the consumer-chosen output name from the YAML config (e.g. "compliance_file"). The factory SHOULD use this to set the output's identity via WrapOutput or equivalent.

rawConfig is the YAML bytes of the type-specific configuration block (e.g. the content under the "file:" key). The factory MUST NOT retain rawConfig after returning.

coreMetrics is the logger-level Metrics recorder (may be nil). Forwarded to outputs that need it (e.g. webhook for delivery reporting). Per-output-type metrics (e.g. file rotation, syslog reconnection) are NOT passed through this signature — they are captured in the factory closure at registration time.

func LookupOutputFactory added in v0.3.0

func LookupOutputFactory(typeName string) OutputFactory

LookupOutputFactory returns the registered factory for the given type name, or nil if no factory has been registered for that type.

type PostField added in v0.3.0

type PostField struct {
	// JSONKey is the JSON object key used when appending to JSON output.
	JSONKey string
	// CEFKey is the extension key used when appending to CEF output.
	CEFKey string
	// Value is the string value to emit for this field. Values are
	// escaped automatically (JSON via [WriteJSONString], CEF via cefEscapeExtValue).
	Value string
}

PostField represents a field appended to serialised bytes after format caching. Used for delivery-specific context (category) and future features (e.g., HMAC checksum).

type SSRFOption added in v0.3.0

type SSRFOption func(*ssrfConfig)

SSRFOption configures SSRF protection behaviour for NewSSRFDialControl.

func AllowPrivateRanges added in v0.3.0

func AllowPrivateRanges() SSRFOption

AllowPrivateRanges permits connections to RFC 1918 private address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), IPv6 ULA (fc00::/7), and loopback (127.0.0.0/8, ::1). This is intended for private network deployments where output receivers run on internal infrastructure, and for testing with net/http/httptest which binds to 127.0.0.1.

Cloud metadata addresses (169.254.169.254) remain blocked even when private ranges are allowed.

type SensitivityConfig added in v0.3.0

type SensitivityConfig struct {
	// Labels maps label names (e.g., "pii", "financial") to their
	// definitions. Label names MUST be non-empty and match the
	// pattern `^[a-z][a-z0-9_]*$` for code generation safety.
	// [ValidateTaxonomy] rejects any name that does not conform.
	Labels map[string]*SensitivityLabel
}

SensitivityConfig holds all sensitivity label definitions for a taxonomy. It is optional; a nil SensitivityConfig means no sensitivity labels are defined and the feature is fully disabled with zero overhead.

type SensitivityLabel added in v0.3.0

type SensitivityLabel struct {
	// Description is an optional human-readable explanation of what
	// this label represents.
	Description string

	// Fields lists field names that are globally assigned this label
	// across all events. A field listed here receives this label in
	// every event where it appears, regardless of per-event annotation.
	Fields []string

	// Patterns lists regex patterns. Any field name matching a pattern
	// is assigned this label. Patterns are compiled once at parse time.
	Patterns []string
	// contains filtered or unexported fields
}

SensitivityLabel defines a single sensitivity label with optional global field mappings and regex patterns. Labels are defined in the taxonomy's sensitivity section and can be associated with fields via three mechanisms: explicit per-event annotation, global field name mapping, and regex patterns.

type StdoutConfig

type StdoutConfig struct {
	// Writer is the destination for audit events. When nil, [os.Stdout]
	// is used. This field exists primarily for testing. The writer does
	// not need to be safe for concurrent use; StdoutOutput serialises
	// writes internally.
	Writer io.Writer
}

StdoutConfig holds configuration for StdoutOutput.

type StdoutOutput

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

StdoutOutput writes serialised audit events to an io.Writer, defaulting to os.Stdout. It is intended for development and debugging; production deployments SHOULD use [FileOutput] or another persistent output.

StdoutOutput does NOT close the underlying writer on [Close] because the writer is typically os.Stdout, which must not be closed.

StdoutOutput is safe for concurrent use.

func NewStdoutOutput

func NewStdoutOutput(cfg StdoutConfig) (*StdoutOutput, error)

NewStdoutOutput creates a new StdoutOutput from the given config. If [StdoutConfig.Writer] is nil, os.Stdout is used.

Example
// Create a stdout output for development/debugging. When Writer is
// nil, os.Stdout is used. Here we use a bytes.Buffer for testing.
var buf bytes.Buffer
out, err := audit.NewStdoutOutput(audit.StdoutConfig{
	Writer: &buf,
})
if err != nil {
	log.Fatal(err)
}
defer func() { _ = out.Close() }()

fmt.Println("stdout output:", out.Name())
Output:
stdout output: stdout

func (*StdoutOutput) Close

func (s *StdoutOutput) Close() error

Close marks the output as closed. Subsequent calls to [Write] return ErrOutputClosed. Close does NOT close the underlying writer. Close is idempotent and safe for concurrent use with StdoutOutput.Write.

func (*StdoutOutput) Name

func (s *StdoutOutput) Name() string

Name returns the human-readable identifier for this output.

func (*StdoutOutput) Write

func (s *StdoutOutput) Write(data []byte) error

Write sends a serialised audit event to the underlying writer. Write returns ErrOutputClosed if the output has been closed. Write is safe for concurrent use.

type TLSPolicy

type TLSPolicy struct {
	// AllowTLS12 permits TLS 1.2 connections in addition to TLS 1.3.
	// When false (the default), MinVersion is set to TLS 1.3.
	AllowTLS12 bool

	// AllowWeakCiphers disables cipher suite filtering when AllowTLS12
	// is true. By default, only cipher suites from [tls.CipherSuites]
	// (the non-insecure list) are permitted. Setting this to true allows
	// Go's full default suite selection, which may include weaker ciphers.
	// Has no effect when AllowTLS12 is false, because TLS 1.3 cipher
	// suites are not configurable in Go.
	AllowWeakCiphers bool
}

TLSPolicy controls TLS version and cipher suite policy for output connections. The zero value enforces TLS 1.3 only with Go's default (secure) cipher suites.

func (*TLSPolicy) Apply

func (p *TLSPolicy) Apply(cfg *tls.Config) (result *tls.Config, warnings []string)

Apply sets TLS version and cipher suite policy on cfg. If cfg is nil, a fresh tls.Config is created. Apply does not modify RootCAs, Certificates, ServerName, or any other pre-existing field. A nil receiver is treated as the zero value (TLS 1.3 only).

The returned warnings slice contains human-readable messages for security-sensitive configurations (e.g. weak ciphers enabled).

type Taxonomy

type Taxonomy struct {
	// Categories maps category names to their definitions. An event
	// type may appear in multiple categories or in none (uncategorised
	// events are always globally enabled).
	Categories map[string]*CategoryDef

	// Events maps event type names to their definitions. Every event
	// type listed in Categories MUST have a corresponding entry here.
	// Pointers are used to avoid per-event heap escapes when passing
	// definitions through the drain path.
	Events map[string]*EventDef

	// Sensitivity defines the sensitivity label configuration. Nil
	// means no sensitivity labels are defined; the feature is fully
	// disabled with zero overhead.
	Sensitivity *SensitivityConfig

	// EmitEventCategory controls whether the delivery-specific category
	// name is appended as an `event_category` field in serialised output.
	// When set via [ParseTaxonomyYAML], defaults to true when absent
	// from YAML. The Go zero value is false — programmatic consumers
	// must set this explicitly. When false, the append is skipped
	// entirely with zero overhead.
	EmitEventCategory bool

	// Version is the taxonomy schema version. MUST be > 0. Currently
	// only version 1 is supported; higher values cause [WithTaxonomy]
	// to return an error wrapping [ErrTaxonomyInvalid].
	Version int
}

Taxonomy defines the complete set of audit event types, their categories, required and optional fields, and which categories are enabled by default. Consumers register a taxonomy at bootstrap via WithTaxonomy.

The framework does not hardcode any event types, field names, or categories. The only events the framework injects are "startup" and "shutdown" lifecycle events, which are added automatically if not already present.

func ParseTaxonomyYAML added in v0.3.0

func ParseTaxonomyYAML(data []byte) (Taxonomy, error)

ParseTaxonomyYAML parses a YAML document into a Taxonomy. The input MUST be a single YAML document containing a valid taxonomy definition. Unknown keys are rejected.

The returned Taxonomy is fully migrated, validated, and lifecycle-injected. Passing it to WithTaxonomy is safe; migration, injection, and validation run again inside WithTaxonomy but produce no additional errors for a well-formed taxonomy.

Input errors (empty, oversized, multi-document, invalid syntax) wrap ErrInvalidInput. Taxonomy validation errors wrap ErrTaxonomyInvalid.

ParseTaxonomyYAML accepts []byte only — no file paths, no readers. Use embed.FS or os.ReadFile in the caller to load from disk.

Example
// In production code, use //go:embed to load the YAML file.
data := []byte(`
version: 1
categories:
  write:
    - user_create
  security:
    - auth_failure
events:
  user_create:
    fields:
      outcome: {required: true}
      actor_id: {required: true}
  auth_failure:
    fields:
      outcome: {required: true}
`)

tax, err := audit.ParseTaxonomyYAML(data)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println("version:", tax.Version)
fmt.Println("events:", len(tax.Events))
Output:
version: 1
events: 2
Example (SensitivityLabels)

ExampleParseTaxonomyYAML_sensitivityLabels demonstrates defining sensitivity labels in a taxonomy and inspecting the resolved field labels after parsing.

data := []byte(`
version: 1
sensitivity:
  labels:
    pii:
      description: "Personally identifiable information"
      fields: [email]
      patterns: ["_email$"]
    financial:
      fields: [card_number]
categories:
  write:
    - user_create
events:
  user_create:
    fields:
      outcome: {required: true}
      email: {}
      card_number: {}
      contact_email: {}
`)

tax, err := audit.ParseTaxonomyYAML(data)
if err != nil {
	fmt.Println("error:", err)
	return
}

def := tax.Events["user_create"]
for _, field := range []string{"email", "card_number", "contact_email", "outcome"} {
	if labels, ok := def.FieldLabels[field]; ok {
		names := make([]string, 0, len(labels))
		for l := range labels {
			names = append(names, l)
		}
		fmt.Printf("%s: %v\n", field, names)
	} else {
		fmt.Printf("%s: no labels\n", field)
	}
}
Output:
email: [pii]
card_number: [financial]
contact_email: [pii]
outcome: no labels
Example (Validation)
// ParseTaxonomyYAML returns an error wrapping audit.ErrTaxonomyInvalid
// when the taxonomy is structurally inconsistent — here, a category
// references an event type that is not defined in the events map.
data := []byte(`
version: 1
categories:
  ops:
    - deploy
    - nonexistent_event
events:
  deploy:
    fields:
      outcome: {required: true}
`)

_, err := audit.ParseTaxonomyYAML(data)
if errors.Is(err, audit.ErrTaxonomyInvalid) {
	fmt.Println("taxonomy validation failed")
}
Output:
taxonomy validation failed

type TimestampFormat

type TimestampFormat string

TimestampFormat controls how timestamps are rendered in serialised output. Unrecognised values default to TimestampRFC3339Nano.

const (
	// TimestampRFC3339Nano renders timestamps as RFC 3339 with
	// nanosecond precision (e.g. "2006-01-02T15:04:05.999999999Z07:00").
	// This is the default.
	TimestampRFC3339Nano TimestampFormat = "rfc3339nano"

	// TimestampUnixMillis renders timestamps as Unix epoch
	// milliseconds (e.g. 1709222400000).
	TimestampUnixMillis TimestampFormat = "unix_ms"
)

type TransportMetadata

type TransportMetadata struct {
	// ClientIP is the client's IP address, extracted from the
	// rightmost X-Forwarded-For entry, X-Real-IP, or RemoteAddr.
	ClientIP string

	// TransportSecurity describes the TLS state: "none", "tls", or "mtls".
	TransportSecurity string

	// Method is the HTTP method (GET, POST, etc.).
	Method string

	// Path is the request URL path.
	Path string

	// UserAgent is the request's User-Agent header value.
	UserAgent string

	// RequestID is the request identifier, taken from the X-Request-Id
	// header or generated as a v4 UUID.
	RequestID string

	// Duration is the wall-clock time the handler took to execute.
	Duration time.Duration

	// StatusCode is the HTTP status code written by the handler.
	StatusCode int
}

TransportMetadata contains HTTP transport-level fields captured automatically by the middleware. These are read-only values passed to the EventBuilder callback; handlers do not need to set them.

type ValidationMode

type ValidationMode string

ValidationMode controls how Logger.AuditEvent handles unknown fields (fields not listed in the event's Required or Optional lists).

Directories

Path Synopsis
Package audittest provides test helpers for consumers of the go-audit library.
Package audittest provides test helpers for consumers of the go-audit library.
examples
01-basic command
Basic demonstrates the minimum viable audit event: create a logger with an inline taxonomy, emit one valid event, and show what happens when a required field is missing.
Basic demonstrates the minimum viable audit event: create a logger with an inline taxonomy, emit one valid event, and show what happens when a required field is missing.
02-code-generation command
Code-generation demonstrates the recommended go-audit workflow: define events in a YAML taxonomy, generate type-safe Go constants with audit-gen, and configure outputs in a separate YAML file.
Code-generation demonstrates the recommended go-audit workflow: define events in a YAML taxonomy, generate type-safe Go constants with audit-gen, and configure outputs in a separate YAML file.
03-standard-fields command
Example 03: Standard Fields & Framework Configuration
Example 03: Standard Fields & Framework Configuration
04-stdout-output command
Stdout-output demonstrates the simplest possible output: writing audit events to standard output.
Stdout-output demonstrates the simplest possible output: writing audit events to standard output.
05-file-output command
File-output demonstrates writing audit events to a log file with automatic rotation, size limits, and restricted file permissions.
File-output demonstrates writing audit events to a log file with automatic rotation, size limits, and restricted file permissions.
06-syslog-output command
Syslog-output demonstrates sending audit events to a syslog server using RFC 5424 format over TCP.
Syslog-output demonstrates sending audit events to a syslog server using RFC 5424 format over TCP.
07-webhook-output command
Webhook-output demonstrates sending audit events as NDJSON batches to an HTTP endpoint with retry, custom headers, and SSRF protection.
Webhook-output demonstrates sending audit events as NDJSON batches to an HTTP endpoint with retry, custom headers, and SSRF protection.
08-loki-output command
Example 08: Loki Output
Example 08: Loki Output
09-multi-output command
Multi-output demonstrates fan-out: a single Audit call delivers events to both stdout and a file simultaneously.
Multi-output demonstrates fan-out: a single Audit call delivers events to both stdout and a file simultaneously.
10-tls-policy command
TLS-policy demonstrates how to configure global and per-output TLS policy in go-audit.
TLS-policy demonstrates how to configure global and per-output TLS policy in go-audit.
11-event-routing command
Event-routing demonstrates per-output filtering: security events go to one file, write events go to another, and stdout gets everything.
Event-routing demonstrates per-output filtering: security events go to one file, write events go to another, and stdout gets everything.
12-sensitivity-labels command
Sensitivity-labels demonstrates per-output field stripping: the same audit event is delivered to three outputs with different field subsets based on sensitivity labels.
Sensitivity-labels demonstrates per-output field stripping: the same audit event is delivered to three outputs with different field subsets based on sensitivity labels.
13-hmac-integrity command
HMAC Integrity demonstrates per-output HMAC tamper detection.
HMAC Integrity demonstrates per-output HMAC tamper detection.
14-formatters command
Formatters demonstrates JSON vs CEF output side by side: the same events are written to two files with different formatters configured in outputs.yaml.
Formatters demonstrates JSON vs CEF output side by side: the same events are written to two files with different formatters configured in outputs.yaml.
15-middleware command
Middleware demonstrates automatic HTTP audit logging: the audit middleware captures transport metadata (method, path, status, duration), handlers populate domain hints (actor, outcome), and health checks are skipped.
Middleware demonstrates automatic HTTP audit logging: the audit middleware captures transport metadata (method, path, status, duration), handlers populate domain hints (actor, outcome), and health checks are skipped.
17-testing command
Testing demonstrates how to test code that uses go-audit.
Testing demonstrates how to test code that uses go-audit.
18-buffering command
Example 18: Buffering and Backpressure
Example 18: Buffering and Backpressure
file module
internal
testhelper
Package testhelper provides shared test utilities for go-audit sub-modules.
Package testhelper provides shared test utilities for go-audit sub-modules.
loki module
outputconfig module
secrets module
openbao module
vault module
syslog module
tests
bdd/steps
Package steps provides Godog step definitions for go-audit BDD tests.
Package steps provides Godog step definitions for go-audit BDD tests.
webhook module

Jump to

Keyboard shortcuts

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