- 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
145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|