| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package log |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | func TestNew_RedactsSecretKeys(t *testing.T) { |
| 12 | t.Parallel() |
| 13 | var buf bytes.Buffer |
| 14 | logger := New(Options{Level: "debug", Format: "json", Writer: &buf}) |
| 15 | |
| 16 | logger.Info( |
| 17 | "login attempt", |
| 18 | "username", "alice", |
| 19 | "password", "hunter2", |
| 20 | "otp_secret", "JBSWY3DPEHPK3PXP", |
| 21 | "authorization", "Bearer eyJhbGc", |
| 22 | ) |
| 23 | |
| 24 | out := buf.String() |
| 25 | for _, leak := range []string{"hunter2", "JBSWY3DPEHPK3PXP", "Bearer eyJhbGc"} { |
| 26 | if strings.Contains(out, leak) { |
| 27 | t.Errorf("redaction missed %q\nlog: %s", leak, out) |
| 28 | } |
| 29 | } |
| 30 | if !strings.Contains(out, "alice") { |
| 31 | t.Errorf("non-secret value lost: %s", out) |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | func TestNew_RedactsSecretValuesByMarker(t *testing.T) { |
| 36 | t.Parallel() |
| 37 | var buf bytes.Buffer |
| 38 | logger := New(Options{Level: "info", Format: "json", Writer: &buf}) |
| 39 | |
| 40 | logger.Info("token used", "trace_note", "saw header: Bearer eyJfoo") |
| 41 | logger.Info("pat seen", "request_path", "/api/repos?token=shithub_pat_abc123") |
| 42 | logger.Info("totp uri", "url", "otpauth://totp/...") |
| 43 | |
| 44 | out := buf.String() |
| 45 | for _, leak := range []string{"eyJfoo", "shithub_pat_abc123", "otpauth://"} { |
| 46 | if strings.Contains(out, leak) { |
| 47 | t.Errorf("value-marker redaction missed %q\nlog: %s", leak, out) |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func TestNew_KeepsOrdinaryValues(t *testing.T) { |
| 53 | t.Parallel() |
| 54 | var buf bytes.Buffer |
| 55 | logger := New(Options{Level: "info", Format: "json", Writer: &buf}) |
| 56 | logger.Info("repo created", "owner", "alice", "name", "my-project") |
| 57 | out := buf.String() |
| 58 | if !strings.Contains(out, "alice") || !strings.Contains(out, "my-project") { |
| 59 | t.Errorf("non-secret fields dropped: %s", out) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestNew_StripsURLCredentials(t *testing.T) { |
| 64 | t.Parallel() |
| 65 | var buf bytes.Buffer |
| 66 | logger := New(Options{Level: "info", Format: "json", Writer: &buf}) |
| 67 | |
| 68 | // Non-secret-keyed value containing user:pass@host — the per-value |
| 69 | // regex strips just the userinfo, keeping host + path readable. |
| 70 | logger.Info("db", "uri", "postgres://shithub:hunter2@127.0.0.1:5432/shithub?sslmode=disable") |
| 71 | // PAT-bearing URL routes through the value-marker scrub (shithub_pat_), |
| 72 | // which is more aggressive — the whole value collapses to ***. |
| 73 | logger.Info("git remote", "remote_uri", "https://alice:shithub_pat_abcdefghijklmnopqrstuvwxyz0123456789@host.example/owner/repo.git") |
| 74 | |
| 75 | out := buf.String() |
| 76 | for _, leak := range []string{"hunter2", "shithub_pat_abc", "alice:shithub_pat_"} { |
| 77 | if strings.Contains(out, leak) { |
| 78 | t.Errorf("credential leaked: %q in %s", leak, out) |
| 79 | } |
| 80 | } |
| 81 | // Generic case keeps the host so logs stay useful. |
| 82 | if !strings.Contains(out, "127.0.0.1") { |
| 83 | t.Errorf("host stripped from generic URL: %s", out) |
| 84 | } |
| 85 | } |
| 86 |