- Fix #61: handleRotateKey and handleDeleteUser now zeroize stored privBytes instead of calling Bytes() (which returns a copy). New state populates privBytes; old references nil'd for GC. - Add audit logging subsystem (internal/audit) with structured event recording for cryptographic operations. - Add audit log engine spec (engines/auditlog.md). - Add ValidateName checks across all engines for path traversal (#48). - Update AUDIT.md: all High findings resolved (0 open). - Add REMEDIATION.md with detailed remediation tracking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
2.9 KiB
Go
125 lines
2.9 KiB
Go
package audit
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"testing"
|
|
)
|
|
|
|
func TestNilLoggerIsSafe(t *testing.T) {
|
|
var l *Logger
|
|
// Must not panic.
|
|
l.Log(context.Background(), Event{
|
|
Caller: "test",
|
|
Operation: "issue",
|
|
Outcome: "success",
|
|
})
|
|
}
|
|
|
|
func TestLogWritesJSON(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
|
Level: slog.Level(-10), // accept all levels
|
|
})
|
|
l := New(h)
|
|
|
|
l.Log(context.Background(), Event{
|
|
Caller: "kyle",
|
|
Roles: []string{"admin"},
|
|
Operation: "issue",
|
|
Engine: "ca",
|
|
Mount: "pki",
|
|
Outcome: "success",
|
|
Detail: map[string]interface{}{"serial": "01:02:03"},
|
|
})
|
|
|
|
var entry map[string]interface{}
|
|
if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
|
|
t.Fatalf("invalid JSON: %v\nbody: %s", err, buf.String())
|
|
}
|
|
|
|
checks := map[string]string{
|
|
"caller": "kyle",
|
|
"operation": "issue",
|
|
"engine": "ca",
|
|
"mount": "pki",
|
|
"outcome": "success",
|
|
}
|
|
for k, want := range checks {
|
|
got, ok := entry[k].(string)
|
|
if !ok || got != want {
|
|
t.Errorf("field %q = %q, want %q", k, got, want)
|
|
}
|
|
}
|
|
|
|
detail, ok := entry["detail"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("detail is not a map: %T", entry["detail"])
|
|
}
|
|
if serial, _ := detail["serial"].(string); serial != "01:02:03" {
|
|
t.Errorf("detail.serial = %q, want %q", serial, "01:02:03")
|
|
}
|
|
}
|
|
|
|
func TestLogOmitsEmptyFields(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
|
Level: slog.Level(-10),
|
|
})
|
|
l := New(h)
|
|
|
|
l.Log(context.Background(), Event{
|
|
Caller: "kyle",
|
|
Operation: "unseal",
|
|
Outcome: "success",
|
|
})
|
|
|
|
var entry map[string]interface{}
|
|
if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
|
|
for _, key := range []string{"roles", "engine", "mount", "resource", "error", "detail"} {
|
|
if _, ok := entry[key]; ok {
|
|
t.Errorf("field %q should be omitted for empty value", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLogIncludesError(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
|
Level: slog.Level(-10),
|
|
})
|
|
l := New(h)
|
|
|
|
l.Log(context.Background(), Event{
|
|
Caller: "operator",
|
|
Operation: "unseal",
|
|
Outcome: "denied",
|
|
Error: "invalid password",
|
|
})
|
|
|
|
var entry map[string]interface{}
|
|
if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
|
|
t.Fatalf("invalid JSON: %v", err)
|
|
}
|
|
if got, _ := entry["error"].(string); got != "invalid password" {
|
|
t.Errorf("error = %q, want %q", got, "invalid password")
|
|
}
|
|
if got, _ := entry["outcome"].(string); got != "denied" {
|
|
t.Errorf("outcome = %q, want %q", got, "denied")
|
|
}
|
|
}
|
|
|
|
func TestNewWithNilHandlerReturnsNil(t *testing.T) {
|
|
l := New(nil)
|
|
if l != nil {
|
|
t.Errorf("New(nil) = %v, want nil", l)
|
|
}
|
|
// Must not panic.
|
|
l.Log(context.Background(), Event{Caller: "test", Operation: "test", Outcome: "success"})
|
|
}
|