deb-bootc-compose/internal/security/audit.go
2025-08-18 23:32:51 -07:00

342 lines
9.6 KiB
Go

package security
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
// AuditEvent represents an audit event
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
EventType string `json:"event_type"`
Username string `json:"username"`
Provider string `json:"provider"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Resource string `json:"resource,omitempty"`
Action string `json:"action,omitempty"`
Result string `json:"result,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
SessionID string `json:"session_id,omitempty"`
RequestID string `json:"request_id,omitempty"`
Error string `json:"error,omitempty"`
}
// AuditLogger manages audit logging
type AuditLogger struct {
config *AuditConfig
logger *logrus.Logger
writer *lumberjack.Logger
}
// NewAuditLogger creates a new audit logger
func NewAuditLogger(config *AuditConfig) (*AuditLogger, error) {
// Create log directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(config.LogFile), 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
// Configure log rotation
writer := &lumberjack.Logger{
Filename: config.LogFile,
MaxSize: config.MaxSize, // megabytes
MaxBackups: config.MaxBackups,
MaxAge: config.MaxAge, // days
Compress: true,
}
// Create logger
logger := logrus.New()
logger.SetOutput(writer)
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: time.RFC3339,
})
// Set log level
level, err := logrus.ParseLevel(config.LogLevel)
if err != nil {
level = logrus.InfoLevel
}
logger.SetLevel(level)
audit := &AuditLogger{
config: config,
logger: logger,
writer: writer,
}
return audit, nil
}
// LogAuthEvent logs an authentication event
func (al *AuditLogger) LogAuthEvent(eventType, username, provider string, metadata map[string]interface{}) {
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Provider: provider,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
Metadata: metadata,
}
al.logEvent(event)
}
// LogAccessEvent logs an access control event
func (al *AuditLogger) LogAccessEvent(eventType, username, resource, action, result string, metadata map[string]interface{}) {
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Resource: resource,
Action: action,
Result: result,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
al.logEvent(event)
}
// LogSecurityEvent logs a security-related event
func (al *AuditLogger) LogSecurityEvent(eventType, username, description string, metadata map[string]interface{}) {
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
if metadata == nil {
metadata = make(map[string]interface{})
}
metadata["description"] = description
al.logEvent(event)
}
// LogErrorEvent logs an error event
func (al *AuditLogger) LogErrorEvent(eventType, username, errorMsg string, metadata map[string]interface{}) {
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Error: errorMsg,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
al.logEvent(event)
}
// LogComposeEvent logs a compose-related event
func (al *AuditLogger) LogComposeEvent(eventType, username, composeID string, metadata map[string]interface{}) {
if metadata == nil {
metadata = make(map[string]interface{})
}
metadata["compose_id"] = composeID
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
al.logEvent(event)
}
// LogVariantEvent logs a variant-related event
func (al *AuditLogger) LogVariantEvent(eventType, username, variantName string, metadata map[string]interface{}) {
if metadata == nil {
metadata = make(map[string]interface{})
}
metadata["variant_name"] = variantName
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
al.logEvent(event)
}
// LogPhaseEvent logs a phase execution event
func (al *AuditLogger) LogPhaseEvent(eventType, username, phaseName, composeID string, metadata map[string]interface{}) {
if metadata == nil {
metadata = make(map[string]interface{})
}
metadata["phase_name"] = phaseName
metadata["compose_id"] = composeID
event := &AuditEvent{
Timestamp: time.Now(),
EventType: eventType,
Username: username,
Metadata: metadata,
IPAddress: al.getClientIP(),
UserAgent: al.getUserAgent(),
}
al.logEvent(event)
}
// logEvent logs an audit event
func (al *AuditLogger) logEvent(event *AuditEvent) {
// Convert to JSON for structured logging
eventJSON, err := json.Marshal(event)
if err != nil {
al.logger.Errorf("Failed to marshal audit event: %v", err)
return
}
// Log based on event type
switch event.EventType {
case "authentication_success", "token_validation":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"provider": event.Provider,
"ip_address": event.IPAddress,
}).Info("Authentication event")
case "authentication_failure", "token_invalid":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"provider": event.Provider,
"ip_address": event.IPAddress,
}).Warn("Authentication failure")
case "authorization_check":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"resource": event.Resource,
"action": event.Action,
"result": event.Result,
"ip_address": event.IPAddress,
}).Info("Authorization check")
case "access_denied":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"resource": event.Resource,
"action": event.Action,
"ip_address": event.IPAddress,
}).Warn("Access denied")
case "security_violation", "suspicious_activity":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"ip_address": event.IPAddress,
"metadata": event.Metadata,
}).Error("Security violation detected")
case "compose_started", "compose_completed", "compose_failed":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"compose_id": event.Metadata["compose_id"],
"ip_address": event.IPAddress,
}).Info("Compose event")
case "phase_started", "phase_completed", "phase_failed":
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"phase_name": event.Metadata["phase_name"],
"compose_id": event.Metadata["compose_id"],
"ip_address": event.IPAddress,
}).Info("Phase event")
default:
al.logger.WithFields(logrus.Fields{
"event_type": event.EventType,
"username": event.Username,
"ip_address": event.IPAddress,
"metadata": event.Metadata,
}).Info("Audit event")
}
// Also log the full event as JSON for external processing
al.logger.WithField("audit_event", string(eventJSON)).Debug("Full audit event")
}
// getClientIP gets the client IP address (placeholder implementation)
func (al *AuditLogger) getClientIP() string {
// In a real implementation, this would extract the client IP from the request context
// For now, return a placeholder
return "127.0.0.1"
}
// getUserAgent gets the user agent (placeholder implementation)
func (al *AuditLogger) getUserAgent() string {
// In a real implementation, this would extract the user agent from the request context
// For now, return a placeholder
return "deb-bootc-compose/1.0"
}
// Close closes the audit logger
func (al *AuditLogger) Close() error {
if al.writer != nil {
return al.writer.Close()
}
return nil
}
// Rotate rotates the log file
func (al *AuditLogger) Rotate() error {
if al.writer != nil {
return al.writer.Rotate()
}
return nil
}
// GetLogStats returns log statistics
func (al *AuditLogger) GetLogStats() map[string]interface{} {
stats := make(map[string]interface{})
if al.writer != nil {
stats["current_size"] = al.writer.Size()
stats["max_size"] = al.writer.MaxSize
stats["max_backups"] = al.writer.MaxBackups
stats["max_age"] = al.writer.MaxAge
}
return stats
}
// SearchEvents searches audit events (placeholder implementation)
func (al *AuditLogger) SearchEvents(query map[string]interface{}) ([]*AuditEvent, error) {
// In a real implementation, this would search through the audit log
// For now, return an empty result
return []*AuditEvent{}, nil
}
// ExportEvents exports audit events to various formats
func (al *AuditLogger) ExportEvents(format string, startTime, endTime time.Time) ([]byte, error) {
// In a real implementation, this would export events from the specified time range
// For now, return an empty result
return []byte{}, nil
}
// CleanupOldEvents cleans up old audit events
func (al *AuditLogger) CleanupOldEvents(before time.Time) error {
// In a real implementation, this would clean up events older than the specified time
// For now, do nothing
return nil
}