fix(security): mask alert secrets in the TUI detail panel and table
The alert detail panel dumped a.Settings raw — SMTP passwords, bot tokens, API keys on screen and into any recording or screen share. The table view leaked the PagerDuty routing key, Pushover user key, and full discord/slack/webhook URLs (the URL path is the credential). The redaction allowlist moves from internal/server to models.RedactAlertSettings so the backup export and the TUI render through one policy. Panel keys are sorted so rows stop reshuffling every tick; webhook URLs show scheme+host only; keys show first4…last4.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
)
|
||||
|
||||
func TestAlertDetailPanel_MasksSecretsStableOrder(t *testing.T) {
|
||||
m := newTestModel(&tuiMockStore{})
|
||||
m.termWidth, m.termHeight = 120, 40
|
||||
m.alerts = []models.AlertConfig{{
|
||||
ID: 1, Name: "ops", Type: "email",
|
||||
Settings: map[string]string{
|
||||
"host": "smtp.example.com",
|
||||
"port": "587",
|
||||
"user": "oncall@example.com",
|
||||
"pass": "hunter2-secret",
|
||||
"to": "team@example.com",
|
||||
},
|
||||
}}
|
||||
m.cursor = 0
|
||||
|
||||
out := m.viewAlertDetailPanel()
|
||||
if strings.Contains(out, "hunter2-secret") {
|
||||
t.Error("SMTP password rendered in alert detail panel")
|
||||
}
|
||||
if strings.Contains(out, "oncall@example.com") {
|
||||
t.Error("SMTP user (not on the allowlist) rendered in alert detail panel")
|
||||
}
|
||||
if !strings.Contains(out, "smtp.example.com") {
|
||||
t.Error("allowlisted setting (host) missing from panel")
|
||||
}
|
||||
|
||||
// Map iteration must not reshuffle rows between renders.
|
||||
for i := 0; i < 5; i++ {
|
||||
if m.viewAlertDetailPanel() != out {
|
||||
t.Fatal("panel output unstable across renders — settings keys not sorted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
||||
m := newTestModel(&tuiMockStore{})
|
||||
|
||||
webhook := m.fmtAlertConfig(struct {
|
||||
Type string
|
||||
Settings map[string]string
|
||||
}{"discord", map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}})
|
||||
if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") {
|
||||
t.Errorf("webhook URL path (the credential) rendered in table: %q", webhook)
|
||||
}
|
||||
if !strings.Contains(webhook, "discord.com") {
|
||||
t.Errorf("webhook host missing from table config: %q", webhook)
|
||||
}
|
||||
|
||||
pd := m.fmtAlertConfig(struct {
|
||||
Type string
|
||||
Settings map[string]string
|
||||
}{"pagerduty", map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}})
|
||||
if strings.Contains(pd, "R0123456789ABCDEFGHIJ") {
|
||||
t.Errorf("pagerduty routing key rendered raw in table: %q", pd)
|
||||
}
|
||||
if !strings.Contains(pd, "R012") || !strings.Contains(pd, "GHIJ") {
|
||||
t.Errorf("masked routing key should keep identifying ends: %q", pd)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user