forked from baron/baron-sso
241 lines
7.1 KiB
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
|
|
}
|