e1ff3210eb
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
191 lines
7.2 KiB
Go
191 lines
7.2 KiB
Go
package monitor
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
)
|
|
|
|
// R-435 (D7, `09` §3 decision 191). On a PINNED tier (the hub holds a confirmed append-only key) the
|
|
// only legitimate fall of the off-site snapshot count is a clean-up window the hub opened, so any fall
|
|
// the windows do not explain alarms — even one snapshot. Non-pinned tiers keep the half-rule.
|
|
//
|
|
// RED-PROOF (2026-10-08): on the pre-R-435 checker TestR435_PinnedUnexplainedFallAlarms fails —
|
|
// 69 → 60 is 9 < 34.5, so no event.
|
|
|
|
type r435Setup struct {
|
|
pinned, confirmed bool
|
|
window func(st *store.Store) // runs between the baseline and the next report
|
|
next string
|
|
}
|
|
|
|
func r435Run(t *testing.T, cust string, s r435Setup) []string {
|
|
t.Helper()
|
|
st := newDiskStore(t)
|
|
if s.pinned {
|
|
if err := st.RecordOffsiteKeyInstalled(cust, "fp-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.confirmed {
|
|
if ok, err := st.RecordOffsiteKeyConfirmed(cust, "fp-1"); err != nil || !ok {
|
|
t.Fatalf("confirm: %v %v", ok, err)
|
|
}
|
|
}
|
|
}
|
|
var msgs []string
|
|
saveOffsiteReport(t, st, cust, dropJSON(69, true, "", "ok"))
|
|
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, sev, msg, _, _ string) {
|
|
if et == "offsite_snapshots_dropped" {
|
|
if sev != "error" {
|
|
t.Errorf("severity %q, want error", sev)
|
|
}
|
|
msgs = append(msgs, msg)
|
|
}
|
|
}, quietLog())
|
|
if s.window != nil {
|
|
s.window(st)
|
|
}
|
|
saveOffsiteReport(t, st, cust, s.next)
|
|
oc.Check()
|
|
return msgs
|
|
}
|
|
|
|
func closedWindow(t *testing.T, cust string, before, after int) func(*store.Store) {
|
|
return func(st *store.Store) {
|
|
id, err := st.OpenOffsiteWindowRow(cust, time.Now().Add(30*time.Minute), before, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.CloseOffsiteWindowRow(id, after, "ok", "box"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestR435_PinnedUnexplainedFallAlarms(t *testing.T) {
|
|
msgs := r435Run(t, "p1", r435Setup{pinned: true, confirmed: true, next: dropJSON(60, true, "", "ok")})
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("pinned 69 -> 60 with no window: want exactly 1 offsite_snapshots_dropped, got %d", len(msgs))
|
|
}
|
|
if !strings.Contains(msgs[0], "outside any clean-up window") || !strings.Contains(msgs[0], "69") || !strings.Contains(msgs[0], "60") {
|
|
t.Fatalf("message must name both counts and say „outside any clean-up window\": %s", msgs[0])
|
|
}
|
|
}
|
|
|
|
func TestR435_PinnedOneSnapshotAlarms(t *testing.T) {
|
|
if msgs := r435Run(t, "p2", r435Setup{pinned: true, confirmed: true, next: dropJSON(68, true, "", "ok")}); len(msgs) != 1 {
|
|
t.Fatalf("pinned 69 -> 68: even one snapshot must alarm, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestR435_WindowExplainsFall(t *testing.T) {
|
|
if msgs := r435Run(t, "p3", r435Setup{pinned: true, confirmed: true,
|
|
window: closedWindow(t, "p3", 69, 60), next: dropJSON(60, true, "", "ok")}); len(msgs) != 0 {
|
|
t.Fatalf("a closed window that removed 9 explains 69 -> 60; got %d alarm(s): %v", len(msgs), msgs)
|
|
}
|
|
}
|
|
|
|
func TestR435_WindowExplainsOnlyPart(t *testing.T) {
|
|
if msgs := r435Run(t, "p4", r435Setup{pinned: true, confirmed: true,
|
|
window: closedWindow(t, "p4", 69, 64), next: dropJSON(60, true, "", "ok")}); len(msgs) != 1 {
|
|
t.Fatalf("a window that removed 5 does not explain 69 -> 60; got %d alarm(s)", len(msgs))
|
|
}
|
|
}
|
|
|
|
// A timeout-closed window explains up to its hub-set cap (10 here): 69 → 60 is explained.
|
|
func TestR435_TimeoutWindowIsUnknownNoAlarm(t *testing.T) {
|
|
timeout := func(st *store.Store) {
|
|
id, err := st.OpenOffsiteWindowRow("p5", time.Now().Add(30*time.Minute), 69, 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = st.CloseOffsiteWindowRow(id, -1, "", "timeout")
|
|
}
|
|
if msgs := r435Run(t, "p5", r435Setup{pinned: true, confirmed: true, window: timeout, next: dropJSON(60, true, "", "ok")}); len(msgs) != 0 {
|
|
t.Fatalf("a timeout-closed window cannot say what it removed → no alarm; got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
// Security review 2026-10-08: a box that claims its window removed everything explains only the cap the
|
|
// hub granted (10): 69 → 40 is 29 > 10 → one alarm. RED-PROOF: drop the cap in RemovedByWindowsBetween → 0 alarms.
|
|
func TestR435_LyingWindowExplainsOnlyItsCap(t *testing.T) {
|
|
if msgs := r435Run(t, "p9", r435Setup{pinned: true, confirmed: true,
|
|
window: closedWindow(t, "p9", 69, 0), next: dropJSON(40, true, "", "ok")}); len(msgs) != 1 {
|
|
t.Fatalf("a window capped at 10 cannot explain 69 -> 40; got %d alarm(s)", len(msgs))
|
|
}
|
|
}
|
|
|
|
// Security review 2026-10-08: one window explains ONE fall. A window that removed 9 explains 69 → 60; a
|
|
// second fall 60 → 55 in the next report (inside the 2-h slack, no new window) alarms.
|
|
// RED-PROOF: stop marking windows as spent (usedWindows) → the second fall is explained again → 0 alarms → FAILS.
|
|
func TestR435_OneWindowExplainsOneFall(t *testing.T) {
|
|
st := newDiskStore(t)
|
|
cust := "p10"
|
|
if err := st.RecordOffsiteKeyInstalled(cust, "fp-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ok, err := st.RecordOffsiteKeyConfirmed(cust, "fp-1"); err != nil || !ok {
|
|
t.Fatalf("confirm: %v %v", ok, err)
|
|
}
|
|
var msgs []string
|
|
saveOffsiteReport(t, st, cust, dropJSON(69, true, "", "ok"))
|
|
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, msg, _, _ string) {
|
|
if et == "offsite_snapshots_dropped" {
|
|
msgs = append(msgs, msg)
|
|
}
|
|
}, quietLog())
|
|
closedWindow(t, cust, 69, 60)(st)
|
|
saveOffsiteReport(t, st, cust, dropJSON(60, true, "", "ok"))
|
|
oc.Check()
|
|
if len(msgs) != 0 {
|
|
t.Fatalf("the window explains 69 -> 60; got %d alarm(s)", len(msgs))
|
|
}
|
|
saveOffsiteReport(t, st, cust, dropJSON(55, true, "", "ok"))
|
|
oc.Check()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("a spent window must not explain the next fall 60 -> 55; got %d alarm(s)", len(msgs))
|
|
}
|
|
}
|
|
|
|
// Controls: the half-rule still governs a non-pinned tier, and an installed-but-unconfirmed key.
|
|
func TestR435_NotPinnedKeepsHalfRule(t *testing.T) {
|
|
if msgs := r435Run(t, "n1", r435Setup{next: dropJSON(60, true, "", "ok")}); len(msgs) != 0 {
|
|
t.Fatalf("NAS tier 69 -> 60 must stay silent (half-rule); got %d", len(msgs))
|
|
}
|
|
if msgs := r435Run(t, "n2", r435Setup{pinned: true, next: dropJSON(60, true, "", "ok")}); len(msgs) != 0 {
|
|
t.Fatalf("an unconfirmed key is not pinned; 69 -> 60 must stay silent; got %d", len(msgs))
|
|
}
|
|
if msgs := r435Run(t, "n3", r435Setup{next: dropJSON(4, true, "", "ok")}); len(msgs) != 1 {
|
|
t.Fatalf("NAS tier 69 -> 4 still alarms by the half-rule; got %d", len(msgs))
|
|
} else if strings.Contains(msgs[0], "outside any clean-up window") {
|
|
t.Fatalf("the half-rule message must not claim a window rule: %s", msgs[0])
|
|
}
|
|
}
|
|
|
|
// An untrustworthy report on a pinned tier: no alarm, and the baseline does not move.
|
|
func TestR435_UntrustworthyReportNoAlarmBaselineKept(t *testing.T) {
|
|
st := newDiskStore(t)
|
|
_ = st.RecordOffsiteKeyInstalled("u1", "fp")
|
|
_, _ = st.RecordOffsiteKeyConfirmed("u1", "fp")
|
|
n := 0
|
|
saveOffsiteReport(t, st, "u1", dropJSON(69, true, "", "ok"))
|
|
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, _, _, _ string) {
|
|
if et == "offsite_snapshots_dropped" {
|
|
n++
|
|
}
|
|
}, quietLog())
|
|
saveOffsiteReport(t, st, "u1", dropJSON(0, false, "", "ok")) // stats unknown
|
|
oc.Check()
|
|
if n != 0 {
|
|
t.Fatalf("untrustworthy report must not alarm; got %d", n)
|
|
}
|
|
oc.mu.Lock()
|
|
base := oc.lastCounts["u1"]
|
|
oc.mu.Unlock()
|
|
if base != 69 {
|
|
t.Fatalf("baseline must stay 69 after an untrustworthy report, got %d", base)
|
|
}
|
|
}
|