Files
mcias/internal/audit/detail_test.go
Kyle Isom 3b17f7f70b Fix SEC-11: use json.Marshal for audit details
- Add internal/audit package with JSON() and JSONWithRoles() helpers
  that use json.Marshal instead of fmt.Sprintf with %q
- Replace all fmt.Sprintf audit detail construction in:
  - internal/server/server.go (10 occurrences)
  - internal/ui/handlers_auth.go (4 occurrences)
  - internal/grpcserver/auth.go (4 occurrences)
- Add tests for the helpers including edge-case Unicode,
  null bytes, special characters, and odd argument counts
- Fix broken {"roles":%v} formatting that produced invalid JSON

Security: Audit log detail strings are now constructed via
json.Marshal, which correctly handles all Unicode edge cases
(U+2028, U+2029, null bytes, etc.) that fmt.Sprintf with %q
may mishandle. This prevents potential log injection or parsing
issues in audit event consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:46:00 -07:00

164 lines
4.1 KiB
Go

package audit
import (
"encoding/json"
"testing"
)
func TestJSON(t *testing.T) {
tests := []struct {
name string
pairs []string
verify func(t *testing.T, result string)
}{
{
name: "single pair",
pairs: []string{"username", "alice"},
verify: func(t *testing.T, result string) {
var m map[string]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if m["username"] != "alice" {
t.Fatalf("expected alice, got %s", m["username"])
}
},
},
{
name: "multiple pairs",
pairs: []string{"jti", "abc-123", "reason", "logout"},
verify: func(t *testing.T, result string) {
var m map[string]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if m["jti"] != "abc-123" {
t.Fatalf("expected abc-123, got %s", m["jti"])
}
if m["reason"] != "logout" {
t.Fatalf("expected logout, got %s", m["reason"])
}
},
},
{
name: "special characters in values",
pairs: []string{"username", "user\"with\\quotes"},
verify: func(t *testing.T, result string) {
var m map[string]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON for special chars: %v", err)
}
if m["username"] != "user\"with\\quotes" {
t.Fatalf("unexpected value: %s", m["username"])
}
},
},
{
name: "unicode edge cases",
pairs: []string{"username", "user\u2028line\u2029sep"},
verify: func(t *testing.T, result string) {
var m map[string]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON for unicode: %v", err)
}
if m["username"] != "user\u2028line\u2029sep" {
t.Fatalf("unexpected value: %s", m["username"])
}
},
},
{
name: "null bytes in value",
pairs: []string{"data", "before\x00after"},
verify: func(t *testing.T, result string) {
var m map[string]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON for null bytes: %v", err)
}
if m["data"] != "before\x00after" {
t.Fatalf("unexpected value: %q", m["data"])
}
},
},
{
name: "odd number of args returns empty object",
pairs: []string{"key"},
verify: func(t *testing.T, result string) {
if result != "{}" {
t.Fatalf("expected {}, got %s", result)
}
},
},
{
name: "no args returns empty object",
pairs: nil,
verify: func(t *testing.T, result string) {
if result != "{}" {
t.Fatalf("expected {}, got %s", result)
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := JSON(tc.pairs...)
tc.verify(t, result)
})
}
}
func TestJSONWithRoles(t *testing.T) {
tests := []struct {
name string
roles []string
verify func(t *testing.T, result string)
}{
{
name: "multiple roles",
roles: []string{"admin", "editor"},
verify: func(t *testing.T, result string) {
var m map[string][]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if len(m["roles"]) != 2 || m["roles"][0] != "admin" || m["roles"][1] != "editor" {
t.Fatalf("unexpected roles: %v", m["roles"])
}
},
},
{
name: "empty roles",
roles: []string{},
verify: func(t *testing.T, result string) {
var m map[string][]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if len(m["roles"]) != 0 {
t.Fatalf("expected empty roles, got %v", m["roles"])
}
},
},
{
name: "roles with special characters",
roles: []string{"role\"special"},
verify: func(t *testing.T, result string) {
var m map[string][]string
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if m["roles"][0] != "role\"special" {
t.Fatalf("unexpected role: %s", m["roles"][0])
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := JSONWithRoles(tc.roles)
tc.verify(t, result)
})
}
}