feat(14-01): sunscreen redacting slog handler installed by every generated main
- Wrap redacts sensitive keys at any depth and scrubs Bearer, sk- and x-api-key shapes - InstallDefault is the first statement of the generated run; hello main regenerated - surf test pins that recovered panics echo no credential - sunscreen README, root modules row and the logging docs page
This commit is contained in:
144
modules/sunscreen/sunscreen_test.go
Normal file
144
modules/sunscreen/sunscreen_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package sunscreen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
skKey = "sk-abcdefghijklmnopqrstuvwxyz123456"
|
||||
antKey = "sk-ant-api03-abcdefghijklmnopqrstuvwxyz"
|
||||
bearer = "eyJhbGciOiJIUzI1NiJ9.payload.sig"
|
||||
apiKey = "live-api-key-value"
|
||||
passwd = "hunter2-secret-pass"
|
||||
nested = "nested-token-value"
|
||||
valuerV = "valuer-secret-value"
|
||||
)
|
||||
|
||||
type secretValuer struct{}
|
||||
|
||||
func (secretValuer) LogValue() slog.Value {
|
||||
return slog.GroupValue(slog.String("Token", valuerV), slog.String("name", "alice"))
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Endpoint string
|
||||
Header string
|
||||
}
|
||||
|
||||
func newTestLogger(buf *bytes.Buffer) *slog.Logger {
|
||||
return slog.New(Wrap(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
}
|
||||
|
||||
func TestRedactHandler(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := newTestLogger(&buf)
|
||||
|
||||
logger.With("API_KEY", apiKey, slog.Group("ctx", slog.String("password", passwd))).
|
||||
WithGroup("req").
|
||||
Info("calling vendor with Bearer "+bearer,
|
||||
"Authorization", "Bearer "+bearer,
|
||||
"user", 7,
|
||||
slog.Group("deep", slog.Group("deeper", slog.String("Secret", nested), slog.String("note", "key "+skKey))),
|
||||
"valuer", secretValuer{},
|
||||
"err", errors.New("upstream said: x-api-key: "+apiKey+", retry"),
|
||||
"headers", http.Header{"Authorization": {"Bearer " + bearer}, "Accept": {"application/json"}},
|
||||
"payload", map[string]any{"OpenAI_API_Key": skKey, "inner": map[string]string{"webhook_secret": nested, "ok": "fine"}},
|
||||
"cfg", config{Endpoint: "https://api.example.com", Header: "x-api-key: " + apiKey},
|
||||
"raw", []byte("token sk-"+strings.Repeat("Z", 24)),
|
||||
"anthropic", antKey,
|
||||
)
|
||||
out := buf.String()
|
||||
for _, secret := range []string{skKey, antKey, bearer, apiKey, passwd, nested, valuerV, strings.Repeat("Z", 24)} {
|
||||
if strings.Contains(out, secret) {
|
||||
t.Fatalf("log leaks %q:\n%s", secret, out)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{
|
||||
"API_KEY=[REDACTED]",
|
||||
"ctx.password=[REDACTED]",
|
||||
"req.Authorization=[REDACTED]",
|
||||
"req.user=7",
|
||||
"req.deep.deeper.Secret=[REDACTED]",
|
||||
`req.deep.deeper.note="key sk-[REDACTED]"`,
|
||||
"req.valuer.Token=[REDACTED]",
|
||||
"req.valuer.name=alice",
|
||||
`x-api-key: [REDACTED]`,
|
||||
"Bearer [REDACTED]",
|
||||
"ok:fine",
|
||||
"https://api.example.com",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("log missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled follows the wrapped handler; wrapping twice is a no-op.
|
||||
quiet := Wrap(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||
if quiet.Enabled(t.Context(), slog.LevelInfo) || !quiet.Enabled(t.Context(), slog.LevelError) {
|
||||
t.Fatal("Enabled must follow the wrapped handler")
|
||||
}
|
||||
if Wrap(quiet) != quiet {
|
||||
t.Fatal("Wrap of a sunscreen handler must return it unchanged")
|
||||
}
|
||||
|
||||
keys := RedactedKeys()
|
||||
keys[0] = "mutated"
|
||||
if RedactedKeys()[0] != "api_key" || len(keys) != 12 {
|
||||
t.Fatalf("RedactedKeys must return a copy of the 12 keys: %v", RedactedKeys())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
logger.Info("plain", "nil", nil, "count", 3, "ok", true)
|
||||
if !strings.Contains(buf.String(), "nil=<nil>") || !strings.Contains(buf.String(), "count=3") {
|
||||
t.Fatalf("non-secret values changed: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrub(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"Authorization: Bearer abc.DEF-123_+/=", "Authorization: Bearer [REDACTED]"},
|
||||
{"bearer lower.case.token", "Bearer [REDACTED]"},
|
||||
{"key=" + skKey + " end", "key=sk-[REDACTED] end"},
|
||||
{antKey, "sk-[REDACTED]"},
|
||||
{"sk-short", "sk-short"},
|
||||
{"X-API-KEY: abc123, next", "x-api-key: [REDACTED], next"},
|
||||
{"x-api-key:abc", "x-api-key: [REDACTED]"},
|
||||
{"nothing secret here: 42 albums", "nothing secret here: 42 albums"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := Scrub(c.in); got != c.want {
|
||||
t.Errorf("Scrub(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
if Scrub(Scrub("Bearer abc")) != "Bearer [REDACTED]" {
|
||||
t.Fatal("Scrub must be idempotent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallDefault(t *testing.T) {
|
||||
prev := slog.Default()
|
||||
prevFlags, prevWriter := log.Flags(), log.Writer()
|
||||
t.Cleanup(func() {
|
||||
slog.SetDefault(prev)
|
||||
log.SetFlags(prevFlags)
|
||||
log.SetOutput(prevWriter)
|
||||
})
|
||||
var buf bytes.Buffer
|
||||
InstallDefault(&buf)
|
||||
slog.Info("login", "password", passwd)
|
||||
log.Printf("legacy log with Bearer %s", bearer)
|
||||
out := buf.String()
|
||||
if strings.Contains(out, passwd) || strings.Contains(out, bearer) {
|
||||
t.Fatalf("default logger leaks:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "password=[REDACTED]") || !strings.Contains(out, "Bearer [REDACTED]") {
|
||||
t.Fatalf("default logger output:\n%s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user