audit

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Apr 13, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

Audit Middleware

The audit middleware provides tamper-evident security audit logging for the mono-framework. It captures framework events with optional cryptographic hash chaining to detect log tampering.

Features

  • Event Logging: Automatically logs module lifecycle and service registration events
  • Hash Chaining: Optional SHA-256 hash chain for tamper detection
  • User Context: Track which user triggered each event
  • Custom Audit Trail: Channel service for modules to log custom audit entries
  • Observer Pattern: Passes events through unchanged (non-intrusive)
  • JSON Output: Structured JSON format for easy parsing and analysis

Quick Start

package main

import (
    "os"

    "github.com/go-monolith/mono/middleware/audit"
    "github.com/go-monolith/mono"
)

func main() {
    // Create audit log file with restricted permissions
    auditFile, _ := os.OpenFile("audit.log",
        os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)

    // Create audit middleware with hash chaining enabled
    auditModule, _ := audit.New(
        audit.WithOutput(auditFile),
        audit.WithHashChaining(""),  // Start new chain
    )

    // Create and configure framework
    framework, _ := mono.NewMonoApplication()
    framework.Register(auditModule)  // Register early to capture events
    framework.Start(ctx)
}

Configuration Options

Output Writer
// Write to file
auditFile, _ := os.OpenFile("audit.log",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
audit.New(audit.WithOutput(auditFile))

// Write to stdout
audit.New(audit.WithOutput(os.Stdout))
Hash Chaining

Hash chaining creates a cryptographic chain where each entry contains:

  • prev_hash: SHA-256 hash of the previous entry
  • entry_hash: SHA-256 hash of the current entry

This allows detection of any tampering or deletion of log entries.

// Start a new hash chain
audit.New(
    audit.WithOutput(auditFile),
    audit.WithHashChaining(""),
)

// Continue an existing chain (after restart)
lastHash := getLastHashFromPreviousSession()
audit.New(
    audit.WithOutput(auditFile),
    audit.WithHashChaining(lastHash),
)
User Context

Track which user triggered each event:

audit.New(
    audit.WithOutput(auditFile),
    audit.WithUserContext(func(ctx context.Context) string {
        if userID, ok := ctx.Value(userIDKey).(string); ok {
            return userID
        }
        return "system"
    }),
)

Captured Events

The audit middleware automatically captures these framework events:

Event Type Description Details Captured
module.started Module started successfully duration_ms
module.stopped Module stopped error (if any)
service.registered Service registered service_type
configuration.updated Configuration changed option_name, old_value, new_value
custom.audit_trail Custom entry from module User-defined

Note: module.registered is NOT captured because module registration occurs before the middleware chain is built (during framework.Register(), before framework.Start()).

Log Output Format

Each log entry is a JSON object on a single line:

{"timestamp":"2024-01-15T10:30:00Z","event_type":"module.started","module_name":"inventory","details":{"duration_ms":5},"user_context":"","prev_hash":"abc123...","entry_hash":"def456..."}
{"timestamp":"2024-01-15T10:30:00Z","event_type":"service.registered","service_name":"check-stock","module_name":"inventory","details":{"service_type":"request_reply"},"user_context":"","prev_hash":"def456...","entry_hash":"ghi789..."}
Entry Fields
Field Type Description
timestamp string UTC timestamp in RFC3339 format
event_type string Type of security event
module_name string Module name (if applicable)
service_name string Service name (if applicable)
details object Event-specific structured data
user_context string User/request context
prev_hash string SHA-256 hash of previous entry
entry_hash string SHA-256 hash of this entry

Custom Audit Trail Service

Modules can log custom audit entries via the built-in channel service:

// In your module, get the audit adapter
func (m *MyModule) SetDependencyServiceContainer(dep string, container types.ServiceContainer) {
    if dep == "audit" {
        m.auditAdapter = audit.NewAdapter(container)
    }
}

// Log custom audit entries
func (m *MyModule) handleSensitiveOperation(ctx context.Context) error {
    // Perform operation...

    // Log to audit trail
    err := m.auditAdapter.SaveEntry(ctx, audit.Entry{
        EventType:   "custom.user_access",
        ModuleName:  m.Name(),
        Details: map[string]any{
            "action":      "data_export",
            "record_count": 1000,
        },
    })

    return err
}

Verifying the Hash Chain

Use the VerifyChain function to detect tampering:

import "github.com/go-monolith/mono/middleware/audit"

// Read audit log entries
entries := readEntriesFromFile("audit.log")

// Verify the chain
valid, err := audit.VerifyChain(entries)
if err != nil {
    log.Printf("Chain verification error: %v", err)
}
if !valid {
    log.Printf("WARNING: Audit log may have been tampered with!")
}

Sensitive Value Redaction

Configuration change events automatically redact sensitive values:

// If a configuration option contains sensitive keywords like "password",
// "secret", "token", "key", the value is redacted:

// Original: {"option_name": "nats_password", "old_value": "secret123", "new_value": "newsecret"}
// Logged:   {"option_name": "nats_password", "old_value": "[REDACTED]", "new_value": "[REDACTED]"}

Example Output

{"timestamp":"2024-01-15T10:30:00Z","event_type":"module.started","module_name":"audit","details":{"duration_ms":1},"prev_hash":"","entry_hash":"a1b2c3..."}
{"timestamp":"2024-01-15T10:30:00Z","event_type":"module.started","module_name":"inventory","details":{"duration_ms":5},"prev_hash":"a1b2c3...","entry_hash":"d4e5f6..."}
{"timestamp":"2024-01-15T10:30:00Z","event_type":"service.registered","service_name":"check-stock","module_name":"inventory","details":{"service_type":"request_reply"},"prev_hash":"d4e5f6...","entry_hash":"g7h8i9..."}
{"timestamp":"2024-01-15T10:30:01Z","event_type":"module.stopped","module_name":"inventory","details":{},"prev_hash":"g7h8i9...","entry_hash":"j0k1l2..."}

Security Best Practices

  1. File Permissions: Use restrictive permissions (0600) for audit log files
  2. Enable Hash Chaining: Always enable hash chaining in production
  3. Persist Last Hash: Store the last hash externally to detect file truncation
  4. Regular Verification: Periodically verify the hash chain integrity
  5. Backup Audit Logs: Store audit logs on separate, secure storage
  6. Monitor for Gaps: Alert on missing or out-of-sequence entries

Thread Safety

The audit module is thread-safe:

  • All writes are protected by mutex
  • Hash chain updates are atomic
  • Channel service uses buffered channels with proper shutdown coordination

Performance Considerations

  • Hash computation: ~1μs per entry (SHA-256)
  • JSON encoding: ~500ns per entry
  • File write: varies by storage
  • Total overhead: typically < 5μs per event

The audit module is an observer - it does not modify events, so it adds minimal latency to the request path.

API Reference

Functions
// New creates a new audit middleware module
func New(opts ...Option) (*AuditModule, error)

// NewAdapter creates a client adapter for the audit trail service
func NewAdapter(container types.ServiceContainer) *Adapter

// VerifyChain verifies the integrity of an audit log hash chain
func VerifyChain(entries []Entry) (bool, error)
Options
// WithOutput sets the output writer for audit logs
func WithOutput(w io.Writer) Option

// WithHashChaining enables hash chaining with optional initial hash
func WithHashChaining(lastSavedHash string) Option

// WithUserContext sets a function to extract user context
func WithUserContext(fn func(context.Context) string) Option
Event Types
const (
    EventModuleStarted       = "module.started"
    EventModuleStopped       = "module.stopped"
    EventServiceRegistered   = "service.registered"
    EventConfigurationUpdate = "configuration.updated"
    EventCustomAuditTrail    = "custom.audit_trail"
)

Analyzing Audit Logs

# View all module lifecycle events
cat audit.log | jq 'select(.event_type | startswith("module."))'

# Find configuration changes
cat audit.log | jq 'select(.event_type == "configuration.updated")'

# Extract all service registrations
cat audit.log | jq 'select(.event_type == "service.registered") | {service: .service_name, module: .module_name, type: .details.service_type}'

# Verify hash chain with jq (simple check)
cat audit.log | jq -s 'reduce .[] as $item ([]; . + [$item.prev_hash, $item.entry_hash]) | unique | length'

See Also

License

See the main framework LICENSE file.

Documentation

Overview

Package audit provides a middleware module for tamper-evident audit logging.

The audit module implements types.MiddlewareModule to intercept framework events and log them with cryptographic hash chaining for tamper detection.

Example usage:

auditFile, _ := os.OpenFile("audit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
auditModule, _ := audit.New(
    audit.WithOutput(auditFile),
    audit.WithHashChaining(""),  // Start new chain
)

framework, _ := mono.NewMonoApplication()
framework.Register(auditModule)  // Register as first module
framework.Start(ctx)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ComputeEntryHash

func ComputeEntryHash(entry Entry) string

ComputeEntryHash computes the SHA-256 hash of an audit entry. The hash is computed from a deterministic representation of the entry fields. The EntryHash field itself is excluded from the computation.

func VerifyChain

func VerifyChain(entries []Entry) error

VerifyChain verifies the integrity of an audit log chain.

Returns an error if:

  • The chain is broken (prev_hash doesn't match previous entry_hash)
  • Any entry_hash is invalid (doesn't match computed hash)

Example:

entries, _ := parseAuditLogFile("audit.log")
if err := audit.VerifyChain(entries); err != nil {
    log.Fatalf("Audit log tampering detected: %v", err)
}

Types

type AuditAdapterPort

type AuditAdapterPort interface {
	// SaveAuditTrail saves a list of audit entries synchronously.
	// Each entry is sent as a separate message to the channel and waits for confirmation.
	// The Timestamp field of each entry will be set by the audit module.
	SaveAuditTrail(ctx context.Context, entries []Entry) (int, error)

	// AsyncSaveAuditTrail saves a list of audit entries asynchronously (fire-and-forget).
	// Each entry is sent as a separate message to the channel.
	// It does not wait for confirmation and returns immediately after sending all entries.
	// The Timestamp field of each entry will be set by the audit module.
	AsyncSaveAuditTrail(ctx context.Context, entries []Entry) error
}

AuditAdapterPort provides a type-safe interface for saving custom audit entries.

Note: The adapter is safe for concurrent use. Multiple goroutines can call SaveAuditTrail and AsyncSaveAuditTrail simultaneously.

func NewAuditAdapter

func NewAuditAdapter(container types.ServiceContainer, consumerModuleName string) (AuditAdapterPort, error)

NewAuditAdapter creates a new audit adapter from a service container.

The adapter wraps the "audit-trail" channel service and provides type-safe methods for saving audit entries. The consumerModuleName parameter identifies the module consuming the audit service and ensures it receives a dedicated response channel.

Returns an error if the container is nil or the channel service is not found.

Example:

func (m *MyModule) SetDependencyServiceContainer(dep string, container types.ServiceContainer) {
    if dep == "audit" {
        adapter, err := audit.NewAuditAdapter(container, m.Name())
        if err != nil {
            // Handle error
        }
        m.auditAdapter = adapter
    }
}

type AuditModule

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

AuditModule implements types.MiddlewareModule to provide tamper-evident audit logging.

The module:

  • Logs all module lifecycle events (registered, started, stopped)
  • Logs all service registration events
  • Logs configuration change events
  • Uses SHA-256 hash chaining for tamper detection
  • Writes JSON-formatted entries to configured output
  • Provides a channel service for custom audit trail entries

The module is an observer - it doesn't modify events, just logs them.

func New

func New(opts ...Option) (*AuditModule, error)

New creates a new audit module with the given options.

The module requires at least WithOutput to be specified. Hash chaining must be explicitly enabled using WithHashChaining.

Example:

auditModule, err := audit.New(
    audit.WithOutput(auditFile),
    audit.WithHashChaining(""),  // Start new chain, or pass lastHash to resume
    audit.WithUserContext(extractUserFromContext),
)

func (*AuditModule) Name

func (m *AuditModule) Name() string

Name returns the module name.

func (*AuditModule) OnConfigurationChange

func (m *AuditModule) OnConfigurationChange(ctx context.Context, event types.ConfigurationEvent) types.ConfigurationEvent

OnConfigurationChange intercepts configuration change events and logs them. The event is passed through unchanged (observer pattern).

func (*AuditModule) OnEventConsumerRegistration

func (m *AuditModule) OnEventConsumerRegistration(ctx context.Context, entry types.EventConsumerEntry) types.EventConsumerEntry

OnEventConsumerRegistration logs event consumer registration (observer pattern). The entry is passed through unchanged.

func (*AuditModule) OnEventStreamConsumerRegistration

func (m *AuditModule) OnEventStreamConsumerRegistration(ctx context.Context, entry types.EventStreamConsumerEntry) types.EventStreamConsumerEntry

OnEventStreamConsumerRegistration logs event stream consumer registration (observer pattern). The entry is passed through unchanged.

func (*AuditModule) OnModuleLifecycle

OnModuleLifecycle intercepts module lifecycle events and logs them. The event is passed through unchanged (observer pattern).

Note: ModuleRegisteredEvent cannot be captured because it occurs before the middleware chain is built (during framework.Register(), which happens before framework.Start()). Only ModuleStartedEvent and ModuleStoppedEvent are captured.

func (*AuditModule) OnOutgoingMessage

OnOutgoingMessage passes through outgoing messages without modification. Audit module does not intercept outgoing messages.

func (*AuditModule) OnServiceRegistration

func (m *AuditModule) OnServiceRegistration(ctx context.Context, reg types.ServiceRegistration) types.ServiceRegistration

OnServiceRegistration intercepts service registration events and logs them. The registration is passed through unchanged (observer pattern).

func (*AuditModule) RegisterServices

func (m *AuditModule) RegisterServices(container types.ServiceContainer) error

RegisterServices registers the channel-based audit-trail service.

Other modules can use this service to save custom audit entries via the adapter. The service name is "audit-trail".

func (*AuditModule) Start

func (m *AuditModule) Start(_ context.Context) error

Start initializes the audit module and starts the channel handler goroutine.

func (*AuditModule) Stop

func (m *AuditModule) Stop(_ context.Context) error

Stop flushes and closes the audit log. If the writer implements io.Closer, it will be closed. Stop is idempotent - multiple calls will have no effect.

type Entry

type Entry struct {
	// Timestamp is the UTC time when the event occurred.
	Timestamp time.Time `json:"timestamp"`

	// EventType identifies the type of security event.
	EventType EventType `json:"event_type"`

	// ModuleName is the name of the module (if applicable).
	ModuleName string `json:"module_name,omitempty"`

	// ServiceName is the name of the service (if applicable).
	ServiceName string `json:"service_name,omitempty"`

	// Details contains event-specific structured data.
	Details map[string]any `json:"details,omitempty"`

	// UserContext contains user/request context information.
	UserContext string `json:"user_context,omitempty"`

	// PrevHash is the SHA-256 hash of the previous entry (empty for first entry).
	PrevHash string `json:"prev_hash"`

	// EntryHash is the SHA-256 hash of this entry.
	EntryHash string `json:"entry_hash"`
}

Entry represents a single audit log entry with tamper-evident hash chaining.

Each entry contains:

  • Timestamp: UTC timestamp in RFC3339 format
  • EventType: Type of security event
  • ModuleName: Name of the module (if applicable)
  • ServiceName: Name of the service (if applicable)
  • Details: Event-specific structured data
  • UserContext: User/request context information
  • PrevHash: SHA-256 hash of previous entry (empty for first entry)
  • EntryHash: SHA-256 hash of current entry

Hash chaining ensures that any modification to the audit log can be detected by verifying the chain using VerifyChain().

type EventType

type EventType string

EventType represents the type of security event being logged.

const (
	// EventModuleStarted indicates a module was started.
	EventModuleStarted EventType = "module.started"

	// EventModuleStopped indicates a module was stopped.
	EventModuleStopped EventType = "module.stopped"

	// EventConfigurationUpdate indicates a configuration change.
	EventConfigurationUpdate EventType = "configuration.updated"

	// EventServiceRegistered indicates a service was registered.
	EventServiceRegistered EventType = "service.registered"

	// EventCustomAuditTrail indicates a custom audit trail entry from another module.
	EventCustomAuditTrail EventType = "custom.audit_trail"
)

type HashChain

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

HashChain manages SHA-256 hash chaining for tamper-evident audit logging.

Each audit entry contains:

  • prev_hash: SHA-256 hash of previous entry
  • entry_hash: SHA-256 hash of current entry (includes prev_hash)

This creates a chain where any modification to past entries can be detected by verifying the chain using VerifyChain().

func NewHashChain

func NewHashChain(lastSavedHash string) *HashChain

NewHashChain creates a new hash chain with an optional initial hash.

The lastSavedHash parameter allows resuming an existing chain:

  • Empty string: starts a new chain
  • Non-empty string: continues from this hash (used as prev_hash for first entry)

func (*HashChain) AddEntry

func (h *HashChain) AddEntry(entry Entry) Entry

AddEntry adds an audit entry to the chain and returns the entry with hashes populated. This method is thread-safe.

type Option

type Option func(*options) error

Option configures an audit module.

func WithHashChaining

func WithHashChaining(lastSavedHash string) Option

WithHashChaining enables hash chaining for tamper-evidence with an optional initial hash to continue an existing chain.

When enabled, each audit entry contains:

  • prev_hash: SHA-256 hash of previous entry
  • entry_hash: SHA-256 hash of current entry

This creates a tamper-evident chain that can be verified using audit.VerifyChain().

The lastSavedHash parameter allows resuming an existing audit chain:

  • Empty string: starts a new chain (first entry has empty prev_hash)
  • Non-empty string: continues from this hash (first entry uses it as prev_hash)

Example:

audit.New(audit.WithHashChaining(""))           // Start new chain
audit.New(audit.WithHashChaining(lastHash))    // Continue existing chain

func WithOutput

func WithOutput(w io.Writer) Option

WithOutput sets the output writer for audit logs.

The writer should support concurrent writes (e.g., *os.File) as the audit module writes from multiple goroutines.

Example:

auditFile, _ := os.OpenFile("audit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
audit.New(audit.WithOutput(auditFile))

func WithUserContext

func WithUserContext(fn func(context.Context) string) Option

WithUserContext sets a function to extract user context from context.Context.

The function is called for each audit event to populate the UserContext field. This is useful for tracking which user triggered each event.

Example:

audit.New(audit.WithUserContext(func(ctx context.Context) string {
    if user, ok := ctx.Value(userKey).(string); ok {
        return user
    }
    return "system"
}))

type SaveEntryResponse

type SaveEntryResponse struct {
	Success bool   `json:"success"`
	Error   string `json:"error,omitempty"`
}

SaveEntryResponse represents the response after saving a single audit entry.

Jump to

Keyboard shortcuts

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