-
Notifications
You must be signed in to change notification settings - Fork 50
Add Audit Logging for Security Events #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| package logger | ||
|
|
||
| import ( | ||
| "context" | ||
| "net" | ||
| "net/http" | ||
| "os" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog" | ||
| ) | ||
|
|
||
| type AuditEvent struct { | ||
| EventID string `json:"event_id"` | ||
| Timestamp time.Time `json:"timestamp"` | ||
| EventType string `json:"event_type"` | ||
| Actor string `json:"actor,omitempty"` | ||
| SourceIP string `json:"source_ip,omitempty"` | ||
| Details map[string]interface{} `json:"details,omitempty"` | ||
| } | ||
|
|
||
| var ( | ||
| auditLogger *zerolog.Logger | ||
| auditLoggerOnce sync.Once | ||
| ) | ||
|
|
||
| // getAuditLogger lazily initializes the audit logger. | ||
| // Configuration is controlled via environment variable AUDIT_LOG_PATH. | ||
| func getAuditLogger() *zerolog.Logger { | ||
| auditLoggerOnce.Do(func() { | ||
| path := os.Getenv("AUDIT_LOG_PATH") | ||
| var w *os.File | ||
| if path == "" { | ||
| w = os.Stdout | ||
| } else { | ||
| f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) | ||
| if err != nil { | ||
| return | ||
| } | ||
| w = f | ||
| } | ||
|
|
||
| l := zerolog.New(w).With().Timestamp().Logger() | ||
| auditLogger = &l | ||
| }) | ||
|
|
||
| return auditLogger | ||
|
Comment on lines
+31
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent init failure permanently disables audit logging. If the file open fails, the logger stays nil and 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| func LogEvent(_ context.Context, eventType, actor, sourceIP string, details map[string]interface{}) { | ||
| l := getAuditLogger() | ||
| if l == nil { | ||
| return | ||
| } | ||
|
|
||
| event := AuditEvent{ | ||
| EventID: uuid.NewString(), | ||
| Timestamp: time.Now().UTC(), | ||
| EventType: eventType, | ||
| Actor: actor, | ||
| SourceIP: sourceIP, | ||
| Details: details, | ||
| } | ||
|
|
||
| e := l.Info(). | ||
| Str("event_id", event.EventID). | ||
| Time("timestamp", event.Timestamp). | ||
| Str("event_type", event.EventType) | ||
|
|
||
| if event.Actor != "" { | ||
| e = e.Str("actor", event.Actor) | ||
| } | ||
| if event.SourceIP != "" { | ||
| e = e.Str("source_ip", event.SourceIP) | ||
| } | ||
| if len(event.Details) > 0 { | ||
| e = e.Fields(event.Details) | ||
| } | ||
|
|
||
| e.Msg("") | ||
| } | ||
|
|
||
| // ClientIP extracts the best-effort client IP for audit logging. | ||
| func ClientIP(r *http.Request) string { | ||
| if r == nil { | ||
| return "" | ||
| } | ||
|
|
||
| // Prefer X-Forwarded-For if present | ||
| if xff := r.Header.Get("X-Forwarded-For"); xff != "" { | ||
| return xff | ||
| } | ||
|
|
||
| host, _, err := net.SplitHostPort(r.RemoteAddr) | ||
| if err != nil { | ||
| return r.RemoteAddr | ||
| } | ||
| return host | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package logger | ||
|
|
||
| import ( | ||
| "io" | ||
| "os" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/rs/zerolog" | ||
| ) | ||
|
|
||
| // AuditEvent represents a security-relevant audit log entry. | ||
| type AuditEvent struct { | ||
| EventID string `json:"event_id"` | ||
| Timestamp time.Time `json:"timestamp"` | ||
| EventType string `json:"event_type"` | ||
| Actor string `json:"actor,omitempty"` | ||
| SourceIP string `json:"source_ip,omitempty"` | ||
| Details map[string]interface{} `json:"details,omitempty"` | ||
| } | ||
|
|
||
| var ( | ||
| auditLogger *zerolog.Logger | ||
| auditLoggerOnce sync.Once | ||
| ) | ||
|
Comment on lines
+23
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove package-level audit logger globals (lint violation). Strict golangci-lint disallows package-level globals. Please refactor to an injected/instance-based logger. As per coding guidelines: no global variables (gochecknoglobals). 🤖 Prompt for AI Agents |
||
|
|
||
| // InitAuditLogger configures the global audit logger. | ||
| // If writer is nil, audit logging is disabled. | ||
| func InitAuditLogger(writer io.Writer) { | ||
| if writer == nil { | ||
| return | ||
| } | ||
|
|
||
| auditLoggerOnce.Do(func() { | ||
| l := zerolog.New(writer).With().Timestamp().Logger() | ||
| auditLogger = &l | ||
| }) | ||
| } | ||
|
|
||
| // LogAuditEvent writes a structured audit event if the audit logger is configured. | ||
| func LogAuditEvent(eventType, actor, sourceIP string, details map[string]interface{}) { | ||
| if auditLogger == nil { | ||
| return | ||
| } | ||
|
|
||
| event := AuditEvent{ | ||
| EventID: uuid.NewString(), | ||
| Timestamp: time.Now().UTC(), | ||
| EventType: eventType, | ||
| Actor: actor, | ||
| SourceIP: sourceIP, | ||
| Details: details, | ||
| } | ||
|
|
||
| e := auditLogger.Info(). | ||
| Str("event_id", event.EventID). | ||
| Time("timestamp", event.Timestamp). | ||
| Str("event_type", event.EventType) | ||
|
|
||
| if event.Actor != "" { | ||
| e = e.Str("actor", event.Actor) | ||
| } | ||
| if event.SourceIP != "" { | ||
| e = e.Str("source_ip", event.SourceIP) | ||
| } | ||
| if len(event.Details) > 0 { | ||
| e = e.Fields(event.Details) | ||
| } | ||
|
|
||
| e.Msg("") | ||
| } | ||
|
|
||
| // NewFileAuditWriter returns a basic file writer for audit logs. | ||
| // Rotation is expected to be handled by external logrotate where used. | ||
| func NewFileAuditWriter(path string) (io.Writer, error) { | ||
| if path == "" { | ||
| return nil, nil | ||
| } | ||
| f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return f, nil | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ type AppConfig struct { | |
| TLS TLSConfig `json:"tls,omitempty"` | ||
| SPIFFE SPIFFEConfig `json:"spiffe,omitempty"` | ||
| EncryptConfig bool `json:"encrypt_config,omitempty"` | ||
| AuditLogPath string `json:"audit_log_path,omitempty"` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for AuditLogPath references across the codebase
echo "=== Searching for AuditLogPath references ==="
rg -n "AuditLogPath|audit_log_path" -S
echo -e "\n=== Checking config.example.json ==="
fd -a 'config.example.json' -x cat {}
echo -e "\n=== Checking pkg/config/validate.go ==="
fd -a 'validate.go' pkg -x cat {}Repository: container-registry/harbor-satellite Length of output: 5460 Add AuditLogPath to validation and config example. The 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| type StateConfig struct { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace package-level audit logger globals to satisfy lint rules.
Package-level state violates the “no globals” lint rule and will fail strict golangci-lint. Please refactor to an instance-based logger (e.g., attach an AuditLogger to Server or pass it explicitly).
As per coding guidelines: no global variables (gochecknoglobals).
🤖 Prompt for AI Agents