1
0
forked from baron/baron-sso
Files
baron-sso/backend/internal/logger/audit_logger.go

241 lines
7.1 KiB
Go

package logger
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// AuditLogEntry holds common audit logging fields.
type AuditLogEntry struct {
RequestID string
Stage string
Operation string // e.g., "SendPasswordReset", "Verify"
Method string
Path string
Status int
LatencyMs time.Duration
IP string
UserAgent string
Origin string
Referer string
Query map[string]string
Headers map[string]string // 핵심 헤더(민감 키는 마스킹됨)
LoginIDs map[string]string // loginId and loginId_normalized
Token string // For reset tokens, magic link tokens
ProviderError string
ProviderStatus int // Provider HTTP status
ProviderBody string // Provider response body (full raw)
RefreshToken string
SessionJwt string
AccessJwt string
UserLoginId string
UserID string
Email string
Phone string
SetCookieName string
SetCookieValue string
SetCookieAttrs map[string]string
RedirectTo string
HasCookieDSRF bool
ParsedCookieDSRF string
// ... potentially more fields specific to different stages
}
// NewAuditLogEntry creates a new AuditLogEntry with a generated RequestID and initial common fields.
func NewAuditLogEntry(c *fiber.Ctx, stage string) *AuditLogEntry {
reqID := uuid.New().String()
// Extract query parameters
queryParams := make(map[string]string)
c.Context().QueryArgs().VisitAll(func(key, value []byte) {
k := string(key)
queryParams[k] = maskSensitiveByKey(k, string(value))
})
// Extract relevant headers
headers := make(map[string]string)
headers["Host"] = c.Get("Host")
headers["User-Agent"] = c.Get("User-Agent")
headers["Origin"] = c.Get("Origin")
headers["Referer"] = c.Get("Referer")
return &AuditLogEntry{
RequestID: reqID,
Stage: stage,
Method: c.Method(),
Path: c.Path(),
IP: c.IP(),
UserAgent: c.Get("User-Agent"),
Origin: c.Get("Origin"),
Referer: c.Get("Referer"),
Query: queryParams,
Headers: headers,
LoginIDs: make(map[string]string),
SetCookieAttrs: make(map[string]string),
}
}
// Log emits an audit log entry using slog.
// It includes common fields and allows for additional custom fields.
func (ale *AuditLogEntry) Log(level slog.Level, msg string, args ...any) {
attrs := []slog.Attr{
slog.String("req_id", ale.RequestID),
slog.String("stage", ale.Stage),
}
if ale.Operation != "" {
attrs = append(attrs, slog.String("op", ale.Operation))
}
if ale.Method != "" {
attrs = append(attrs, slog.String("method", ale.Method))
}
if ale.Path != "" {
attrs = append(attrs, slog.String("path", ale.Path))
}
if ale.Status != 0 {
attrs = append(attrs, slog.Int("status", ale.Status))
}
if ale.LatencyMs != 0 {
attrs = append(attrs, slog.Duration("latency_ms", ale.LatencyMs))
}
if ale.IP != "" {
attrs = append(attrs, slog.String("ip", ale.IP))
}
if ale.UserAgent != "" {
attrs = append(attrs, slog.String("user_agent", ale.UserAgent))
}
if ale.Origin != "" {
attrs = append(attrs, slog.String("origin", ale.Origin))
}
if ale.Referer != "" {
attrs = append(attrs, slog.String("referer", ale.Referer))
}
if len(ale.Query) > 0 {
queryGroupArgs := make([]any, 0, len(ale.Query))
for k, v := range ale.Query {
queryGroupArgs = append(queryGroupArgs, slog.String(k, maskSensitiveByKey(k, v)))
}
attrs = append(attrs, slog.Group("query", queryGroupArgs...))
}
if len(ale.Headers) > 0 {
headersGroupArgs := make([]any, 0, len(ale.Headers))
for k, v := range ale.Headers {
headersGroupArgs = append(headersGroupArgs, slog.String(k, maskSensitiveByKey(k, v)))
}
attrs = append(attrs, slog.Group("headers", headersGroupArgs...))
}
if len(ale.LoginIDs) > 0 {
loginIDGroupArgs := make([]any, 0, len(ale.LoginIDs))
for k, v := range ale.LoginIDs {
loginIDGroupArgs = append(loginIDGroupArgs, slog.String(k, v))
}
attrs = append(attrs, slog.Group("login_ids", loginIDGroupArgs...))
}
if ale.Token != "" {
attrs = append(attrs, slog.Bool("has_token", true))
}
if ale.ProviderError != "" {
attrs = append(attrs, slog.String("provider_error", ale.ProviderError))
}
if ale.ProviderStatus != 0 {
attrs = append(attrs, slog.Int("provider_http_status", ale.ProviderStatus))
}
if ale.ProviderBody != "" {
attrs = append(attrs, slog.String("provider_response_body", ale.ProviderBody))
}
if ale.RefreshToken != "" {
attrs = append(attrs, slog.Bool("has_refresh_token", true))
}
if ale.SessionJwt != "" {
attrs = append(attrs, slog.Bool("has_session_jwt", true))
}
if ale.AccessJwt != "" {
attrs = append(attrs, slog.Bool("has_access_jwt", true))
}
if ale.UserLoginId != "" {
attrs = append(attrs, slog.String("user_login_id", ale.UserLoginId))
}
if ale.UserID != "" {
attrs = append(attrs, slog.String("user_id", ale.UserID))
}
if ale.Email != "" {
attrs = append(attrs, slog.String("email", ale.Email))
}
if ale.Phone != "" {
attrs = append(attrs, slog.String("phone", ale.Phone))
}
if ale.SetCookieName != "" {
attrs = append(attrs, slog.String("set_cookie_name", ale.SetCookieName))
if ale.SetCookieValue != "" {
attrs = append(attrs, slog.Bool("has_set_cookie_value", true))
}
if len(ale.SetCookieAttrs) > 0 {
cookieAttrsGroupArgs := make([]any, 0, len(ale.SetCookieAttrs))
for k, v := range ale.SetCookieAttrs {
cookieAttrsGroupArgs = append(cookieAttrsGroupArgs, slog.String(k, v))
}
attrs = append(attrs, slog.Group("set_cookie_attrs", cookieAttrsGroupArgs...))
}
}
if ale.RedirectTo != "" {
attrs = append(attrs, slog.String("redirect_to", ale.RedirectTo))
}
if ale.HasCookieDSRF {
attrs = append(attrs, slog.Bool("has_cookie_DSRF", ale.HasCookieDSRF))
}
if ale.ParsedCookieDSRF != "" {
attrs = append(attrs, slog.Bool("has_parsed_cookie_DSRF", true))
}
// Convert variadic args to slog.Attr before appending
for i := 0; i < len(args); i += 2 {
if i+1 < len(args) {
attrs = append(attrs, slog.Any(fmt.Sprintf("%v", args[i]), args[i+1]))
} else {
// Handle odd number of arguments - log the last one with a generic key
attrs = append(attrs, slog.Any(fmt.Sprintf("extra_arg_%d", i), args[i]))
}
}
slog.Default().LogAttrs(context.Background(), level, msg, attrs...)
}
var sensitiveAuditKeys = map[string]struct{}{
"password": {},
"currentpassword": {},
"newpassword": {},
"oldpassword": {},
"token": {},
"accesstoken": {},
"refreshtoken": {},
"authorization": {},
"cookie": {},
"setcookie": {},
"verificationcode": {},
"code": {},
"loginchallenge": {},
"loginverifier": {},
"sessionjwt": {},
"accessjwt": {},
"refreshjwt": {},
}
func maskSensitiveByKey(key, value string) string {
if value == "" {
return value
}
k := strings.ToLower(key)
k = strings.ReplaceAll(k, "-", "")
k = strings.ReplaceAll(k, "_", "")
if _, ok := sensitiveAuditKeys[k]; ok {
return "*****"
}
return value
}