Skip to content

Audit System ​

The HTML library ships a pluggable audit pipeline that records security events and processing anomalies: behavior is configured by AuditConfig, events are recorded as AuditEntry, and output is delivered to one or more destinations through a pluggable Sink.

AuditConfig ​

go
type AuditConfig struct {
    Enabled            bool       `json:"enabled"`              // Enable audit
    LogBlockedTags     bool       `json:"log_blocked_tags"`     // Log blocked tags
    LogBlockedAttrs    bool       `json:"log_blocked_attrs"`    // Log blocked attributes
    LogBlockedURLs     bool       `json:"log_blocked_urls"`     // Log blocked URLs
    LogInputViolations bool       `json:"log_input_violations"` // Log input violations
    LogDepthViolations bool       `json:"log_depth_violations"` // Log depth violations
    LogTimeouts        bool       `json:"log_timeouts"`         // Log timeouts
    LogEncodingIssues  bool       `json:"log_encoding_issues"`  // Log encoding issues
    LogPathTraversal   bool       `json:"log_path_traversal"`   // Log path traversal attempts
    Sink               AuditSink  `json:"-"`                    // Audit output destination (excluded from JSON serialization)
    IncludeRawValues   bool       `json:"include_raw_values"`   // Include raw values
    MaxRawValueLength  int        `json:"max_raw_value_length"` // Max raw value length
    MaxEntries         int        `json:"max_entries"`          // In-memory audit entry cap
}

Fields ​

FieldTypeDescription
AuditConfig.EnabledboolWhether audit is enabled
AuditConfig.LogBlockedTagsboolLog removed dangerous tags
AuditConfig.LogBlockedAttrsboolLog removed dangerous attributes
AuditConfig.LogBlockedURLsboolLog blocked dangerous URLs
AuditConfig.LogInputViolationsboolLog input-size and validation violations
AuditConfig.LogDepthViolationsboolLog nesting-depth violations
AuditConfig.LogTimeoutsboolLog processing timeout events
AuditConfig.LogEncodingIssuesboolLog encoding-detection issues
AuditConfig.LogPathTraversalboolLog path traversal attempts
AuditConfig.SinkAuditSinkAudit output destination; when audit is enabled and this is nil, LoggerAuditSink is used by default
AuditConfig.IncludeRawValuesboolInclude raw attribute/URL values in logs (may contain malicious content; enable with care)
AuditConfig.MaxRawValueLengthintMaximum recorded length of raw values; truncated beyond this
AuditConfig.MaxEntriesintCap on entries retained in memory by GetAuditLog (0 uses the default 10000)

Preset Audit Configs ​

DefaultAuditConfig ​

DefaultAuditConfig returns the default audit configuration (disabled by default, all log flags true).

go
func DefaultAuditConfig() AuditConfig
FieldDefault
Enabledfalse
LogBlockedTagstrue
LogBlockedAttrstrue
LogBlockedURLstrue
LogInputViolationstrue
LogDepthViolationstrue
LogTimeoutstrue
LogEncodingIssuestrue
LogPathTraversaltrue
IncludeRawValuesfalse
MaxRawValueLength200
MaxEntries10000 (DefaultMaxAuditEntries)

HighSecurityAuditConfig ​

HighSecurityAuditConfig returns a high-security audit configuration with all logging and raw value recording enabled.

go
func HighSecurityAuditConfig() AuditConfig
FieldDefault
Enabledtrue
LogBlockedTagstrue
LogBlockedAttrstrue
LogBlockedURLstrue
LogInputViolationstrue
LogDepthViolationstrue
LogTimeoutstrue
LogEncodingIssuestrue
LogPathTraversaltrue
IncludeRawValuestrue
MaxRawValueLength500
MaxEntries10000 (DefaultMaxAuditEntries)

MaxEntries behavior

MaxEntries caps how many audit entries GetAuditLog retains in memory: setting it to 0 uses the default DefaultMaxAuditEntries (10000); negative values and values above 100000 (maxConfigAuditEntries) are rejected by Config.Validate; when the cap is reached, the oldest half of the retained entries are dropped, always keeping the most recent events. For retention unbounded by the in-memory cap, feed an AuditSink.

Type Definitions ​

go
type AuditEventType string  // Audit event type
type AuditLevel string      // Audit severity level

AuditEventType identifies the category of an audit event (values in the constants table below) and AuditLevel identifies its severity; both are string type aliases.

Audit Event Types ​

ConstantValueDescription
AuditEventBlockedTag"blocked_tag"Blocked tag
AuditEventBlockedAttr"blocked_attr"Blocked attribute
AuditEventBlockedURL"blocked_url"Blocked URL
AuditEventInputViolation"input_violation"Input violation
AuditEventDepthViolation"depth_violation"Depth violation
AuditEventTimeout"timeout"Processing timeout
AuditEventEncodingIssue"encoding_issue"Encoding issue
AuditEventPathTraversal"path_traversal"Path traversal attempt

Audit Levels ​

ConstantValueDescription
AuditLevelInfo"info"Information level
AuditLevelWarning"warning"Warning level
AuditLevelCritical"critical"Critical level

AuditEntry ​

Audit log entry.

go
type AuditEntry struct {
    Timestamp time.Time      `json:"timestamp"`           // Event time
    EventType AuditEventType `json:"event_type"`          // Event type
    Level     AuditLevel     `json:"level"`               // Audit level
    Message   string         `json:"message"`             // Event description
    Tag       string         `json:"tag,omitempty"`       // Related tag
    Attribute string         `json:"attribute,omitempty"` // Related attribute
    URL       string         `json:"url,omitempty"`       // Related URL
    InputSize int            `json:"input_size,omitempty"` // Input size
    MaxSize   int            `json:"max_size,omitempty"`  // Size limit
    Depth     int            `json:"depth,omitempty"`     // DOM depth
    MaxDepth  int            `json:"max_depth,omitempty"` // Depth limit
    Path      string         `json:"path,omitempty"`      // File path
    RawValue  string         `json:"raw_value,omitempty"` // Raw value
    Metadata  map[string]any `json:"metadata,omitempty"`  // Additional metadata
}

Fields ​

FieldTypeDescription
AuditEntry.Timestamptime.TimeWhen the event was recorded (UTC)
AuditEntry.EventTypeAuditEventTypeEvent category (e.g. blocked_tag, timeout)
AuditEntry.LevelAuditLevelSeverity level (info / warning / critical)
AuditEntry.MessagestringHuman-readable event description
AuditEntry.TagstringHTML tag name that triggered the event (when applicable)
AuditEntry.AttributestringAttribute name that triggered the event (when applicable)
AuditEntry.URLstringResource URL that triggered the event (when applicable)
AuditEntry.InputSizeintActual input size for input-violation events
AuditEntry.MaxSizeintConfigured size limit for input-violation events
AuditEntry.DepthintActual nesting depth for depth-violation events
AuditEntry.MaxDepthintConfigured depth limit for depth-violation events
AuditEntry.PathstringFile path for path-traversal events (sanitized to the filename)
AuditEntry.RawValuestringRaw attribute/URL value that triggered the event (populated only when IncludeRawValues=true)
AuditEntry.Metadatamap[string]anyAdditional event-specific key-value pairs

Field population conventions

Timestamp, EventType, Level, and Message are always populated on every entry; Tag, Attribute, and URL are filled only for the corresponding events (blocked tag/attribute/URL); InputSize/MaxSize and Depth/MaxDepth are filled with input-violation and depth-violation events respectively; RawValue is retained only when IncludeRawValues=true; Metadata carries event-specific extra key-value pairs.

AuditSink Interface ​

All Sink types implement this interface.

go
type AuditSink interface {
    Write(entry AuditEntry)
    Close() error
}

Implementations only need to provide two methods: AuditSink.Write writes a single AuditEntry (by convention non-blocking and concurrency-safe), and AuditSink.Close releases resources.

Sink Types ​

LoggerAuditSink ​

LoggerAuditSink outputs via the standard library log.Logger with an [AUDIT] prefix. Instances created by NewLoggerAuditSink default to standard error (os.Stderr); to redirect to a file, network connection, etc., use NewLoggerAuditSinkWithWriter(w) to specify any io.Writer — the constructor NewLoggerAuditSinkWithWriter falls back to standard error when w is nil.

go
func NewLoggerAuditSink() *LoggerAuditSink
func NewLoggerAuditSinkWithWriter(w io.Writer) *LoggerAuditSink
MethodDescription
LoggerAuditSink.WriteSerializes the entry to JSON and writes it to the logger ([AUDIT] prefix)
LoggerAuditSink.CloseNo-op; always returns nil

ChannelAuditSink ​

ChannelAuditSink sends to a buffered channel, suitable for async processing or integration with external logging systems. Create it with NewChannelAuditSink(bufferSize); consumers receive entries from the read-only channel returned by Channel. The constructor NewChannelAuditSink clamps a negative bufferSize to 0 (i.e., an unbuffered channel). ChannelAuditSink.Write is non-blocking: when the buffer is full, the entry is dropped and the drop count is incremented (no blocking, no error); the caller can query the number of dropped entries via DroppedCount and trigger alerts or scale-up in monitoring.

go
func NewChannelAuditSink(bufferSize int) *ChannelAuditSink
MethodSignatureDescription
ChannelAuditSink.Channelfunc (s *ChannelAuditSink) Channel() <-chan AuditEntryReturns the read-only receive channel
ChannelAuditSink.DroppedCountfunc (s *ChannelAuditSink) DroppedCount() int64Returns the number of entries dropped due to a full buffer
ChannelAuditSink.Writefunc (s *ChannelAuditSink) Write(entry AuditEntry)Non-blocking write; drops and counts when the buffer is full
ChannelAuditSink.Closefunc (s *ChannelAuditSink) Close() errorCloses the channel (idempotent: safe to call multiple times; only the first call actually closes it)
go
sink := html.NewChannelAuditSink(100)
go func() {
    for entry := range sink.Channel() {
        log.Println(entry.Message)
    }
}()

WriterAuditSink ​

WriterAuditSink writes in JSON Lines format to an io.Writer; create it with NewWriterAuditSink(w). The constructor NewWriterAuditSink returns a Sink instance bound to that writer.

go
func NewWriterAuditSink(w io.Writer) *WriterAuditSink
MethodDescription
WriterAuditSink.WriteSerializes the entry to JSON and writes it to the underlying writer
WriterAuditSink.CloseNo-op; always returns nil

MultiSink ​

MultiSink fans out to multiple Sinks; create it with NewMultiSink(sinks...). The constructor NewMultiSink returns an instance aggregating all child Sinks. MultiSink.Close uses Go 1.20+'s errors.Join to aggregate close errors from all child Sinks (rather than keeping only the last one) — for an audit subsystem, discarding an earlier Sink's delivery failure would mask real problems.

go
func NewMultiSink(sinks ...AuditSink) *MultiSink
MethodDescription
MultiSink.WriteWrites the entry to all child Sinks (skipping nil)
MultiSink.CloseCloses all child Sinks, aggregating errors with errors.Join

FilteredSink ​

FilteredSink filters entries with a predicate function; create it with NewFilteredSink(sink, filter). The constructor NewFilteredSink returns a filtering instance wrapping the underlying Sink.

go
func NewFilteredSink(sink AuditSink, filter func(AuditEntry) bool) *FilteredSink
MethodDescription
FilteredSink.WriteWrites to the underlying Sink when the predicate returns true (or when the predicate is nil)
FilteredSink.CloseCloses the underlying Sink

LevelFilteredSink ​

LevelFilteredSink filters by minimum level; create it with NewLevelFilteredSink(sink, minLevel). The constructor NewLevelFilteredSink returns an instance filtering at the minLevel threshold.

go
func NewLevelFilteredSink(sink AuditSink, minLevel AuditLevel) *LevelFilteredSink
MethodDescription
LevelFilteredSink.WriteWrites to the underlying Sink when the entry level is at or above minLevel
LevelFilteredSink.CloseCloses the underlying Sink

Example ​

go
// Create a multi-layer audit pipeline
writerSink := html.NewWriterAuditSink(auditFile)
levelSink := html.NewLevelFilteredSink(writerSink, html.AuditLevelWarning)
multiSink := html.NewMultiSink(levelSink, html.NewLoggerAuditSink())

cfg := html.DefaultConfig()
cfg.Audit = html.DefaultAuditConfig()
cfg.Audit.Enabled = true
cfg.Audit.Sink = multiSink

p, _ := html.New(cfg)
defer p.Close()

// Get audit log after processing
entries := p.GetAuditLog()
for _, e := range entries {
    fmt.Printf("[%s] %s: %s\n", e.Level, e.EventType, e.Message)
}