Files
Pulse/pkg/audit/audit.go
rcourtman 2a8f55d719 feat(enterprise): add Advanced Reporting and Audit Webhooks integration
This commit adds enterprise-grade reporting and audit capabilities:

Reporting:
- Refactored metrics store from internal/ to pkg/ for enterprise access
- Added pkg/reporting with shared interfaces for report generation
- Created API endpoint: GET /api/admin/reports/generate
- New ReportingPanel.tsx for PDF/CSV report configuration

Audit Webhooks:
- Extended pkg/audit with webhook URL management interface
- Added API endpoint: GET/POST /api/admin/webhooks/audit
- New AuditWebhookPanel.tsx for webhook configuration
- Updated Settings.tsx with Reporting and Webhooks tabs

Server Hardening:
- Enterprise hooks now execute outside mutex with panic recovery
- Removed dbPath from metrics Stats API to prevent path disclosure
- Added storage metrics persistence to polling loop

Documentation:
- Updated README.md feature table
- Updated docs/API.md with new endpoints
- Updated docs/PULSE_PRO.md with feature descriptions
- Updated docs/WEBHOOKS.md with audit webhooks section
2026-01-09 21:31:49 +00:00

179 lines
4.9 KiB
Go

// Package audit provides audit logging functionality for Pulse.
//
// This package defines the AuditLogger interface which can be implemented
// by different backends. The OSS version uses ConsoleAuditLogger (logs to zerolog),
// while the enterprise version can provide a more sophisticated implementation
// with persistent storage, signing, and webhook delivery.
//
// This package is in pkg/ so it can be imported by external modules (pulse-enterprise).
package audit
import (
"sync"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
// Event represents a single audit log entry.
type Event struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event"` // "login", "logout", "config_change", etc.
User string `json:"user,omitempty"`
IP string `json:"ip"`
Path string `json:"path,omitempty"`
Success bool `json:"success"`
Details string `json:"details,omitempty"`
Signature string `json:"signature,omitempty"` // Empty for OSS, HMAC for enterprise
}
// QueryFilter defines filters for querying audit events.
type QueryFilter struct {
ID string
StartTime *time.Time
EndTime *time.Time
EventType string
User string
Success *bool
Limit int
Offset int
}
// Logger defines the interface for audit logging backends.
// The OSS version uses ConsoleLogger which outputs to zerolog.
// Enterprise implementations can provide persistent storage with signing.
type Logger interface {
// Log records an audit event
Log(event Event) error
// Query retrieves audit events matching the filter (optional, may return empty for console logger)
Query(filter QueryFilter) ([]Event, error)
// Count returns the number of audit events matching the filter
Count(filter QueryFilter) (int, error)
// Webhook Management (Optional, may return empty/not implemented for console logger)
GetWebhookURLs() []string
UpdateWebhookURLs(urls []string) error
// Close releases any resources held by the logger
Close() error
}
// Global logger instance with thread-safe access
var (
globalLogger Logger
loggerMu sync.RWMutex
loggerOnce sync.Once
)
// SetLogger sets the global audit logger.
// This should be called during application initialization.
// If called multiple times, subsequent calls replace the previous logger.
func SetLogger(l Logger) {
loggerMu.Lock()
defer loggerMu.Unlock()
globalLogger = l
}
// GetLogger returns the current global audit logger.
// If no logger has been set, it returns a ConsoleLogger.
func GetLogger() Logger {
loggerMu.RLock()
l := globalLogger
loggerMu.RUnlock()
if l != nil {
return l
}
// Initialize default console logger on first access
loggerOnce.Do(func() {
loggerMu.Lock()
defer loggerMu.Unlock()
if globalLogger == nil {
globalLogger = NewConsoleLogger()
}
})
loggerMu.RLock()
defer loggerMu.RUnlock()
return globalLogger
}
// Log is a convenience function that logs an event using the global logger.
func Log(eventType, user, ip, path string, success bool, details string) {
event := Event{
ID: uuid.NewString(),
Timestamp: time.Now(),
EventType: eventType,
User: user,
IP: ip,
Path: path,
Success: success,
Details: details,
}
if err := GetLogger().Log(event); err != nil {
log.Error().Err(err).Str("event", eventType).Msg("Failed to log audit event")
}
}
// ConsoleLogger implements Logger by writing to zerolog.
// This is the default implementation used by the OSS version.
type ConsoleLogger struct{}
// NewConsoleLogger creates a new console-based audit logger.
func NewConsoleLogger() *ConsoleLogger {
return &ConsoleLogger{}
}
// Log writes an audit event to zerolog.
func (c *ConsoleLogger) Log(event Event) error {
logEvent := log.With().
Str("audit_id", event.ID).
Str("event", event.EventType).
Str("user", event.User).
Str("ip", event.IP).
Str("path", event.Path).
Time("timestamp", event.Timestamp).
Str("details", event.Details).
Logger()
if event.Success {
logEvent.Info().Msg("Audit event")
} else {
logEvent.Warn().Msg("Audit event - FAILED")
}
return nil
}
// Query returns an empty slice for the console logger.
// Console logs are not queryable - use enterprise version for persistent storage.
func (c *ConsoleLogger) Query(filter QueryFilter) ([]Event, error) {
return []Event{}, nil
}
// Count returns zero for the console logger.
func (c *ConsoleLogger) Count(filter QueryFilter) (int, error) {
return 0, nil
}
// GetWebhookURLs returns an empty slice for the console logger.
func (c *ConsoleLogger) GetWebhookURLs() []string {
return []string{}
}
// UpdateWebhookURLs returns an error for the console logger.
func (c *ConsoleLogger) UpdateWebhookURLs(urls []string) error {
return nil // Or return an error saying it's not supported
}
// Close is a no-op for the console logger.
func (c *ConsoleLogger) Close() error {
return nil
}