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) } }