hub R-435: a window explains at most its hub-set cap; unreadable windows fall back to the half-rule (security review)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -304,8 +304,8 @@ func (oc *OffsiteChecker) pinnedTier(customerID string) bool {
|
||||
// window rule (a pinned tier), so the message can say „outside any clean-up window".
|
||||
//
|
||||
// R-435 (D7, `09` §3 decision 191): on a PINNED tier any fall the hub's own windows do not explain
|
||||
// alarms — even one snapshot. A window that cannot say what it removed (timeout, still open) explains
|
||||
// anything: no alarm, one INFO line. Non-pinned tiers (the household's NAS, which still prunes by
|
||||
// alarms — even one snapshot. Each window explains at most its hub-set max_remove (a timeout or still-open
|
||||
// window explains exactly that cap); when the windows cannot be read, the half-rule decides. Non-pinned tiers (the household's NAS, which still prunes by
|
||||
// itself) keep the half-rule below. Pinned by r435_pinned_drop_test.go.
|
||||
//
|
||||
// LIMIT: the count is the box's own (R-895) and it is a NET count — a night's new snapshots between
|
||||
@@ -319,15 +319,16 @@ func (oc *OffsiteChecker) snapshotDropped(customerID string, off *offsiteReport,
|
||||
if drop > 0 && oc.pinnedTier(customerID) {
|
||||
from := oc.lastCountAt[customerID].Add(-windowSlack)
|
||||
removed, unknown := oc.store.RemovedByWindowsBetween(customerID, from, reportAt)
|
||||
if unknown {
|
||||
oc.logger.Printf("[INFO] Offsite snapshot count for %s fell %d -> %d with a clean-up window that cannot say what it removed (timeout or still open) — treated as explained", customerID, prev, off.SnapshotCount)
|
||||
if !unknown {
|
||||
if drop > removed {
|
||||
return true, prev, off.SnapshotCount, true
|
||||
}
|
||||
oc.logger.Printf("[DEBUG] Offsite snapshot count for %s fell %d -> %d, explained by clean-up windows (up to %d)", customerID, prev, off.SnapshotCount, removed)
|
||||
return false, prev, off.SnapshotCount, true
|
||||
}
|
||||
if drop > removed {
|
||||
return true, prev, off.SnapshotCount, true
|
||||
}
|
||||
oc.logger.Printf("[DEBUG] Offsite snapshot count for %s fell %d -> %d, explained by clean-up windows (%d removed)", customerID, prev, off.SnapshotCount, removed)
|
||||
return false, prev, off.SnapshotCount, true
|
||||
// Fail closed (security review 2026-10-08): the windows could not be read, so the half-rule
|
||||
// decides — never „explained".
|
||||
oc.logger.Printf("[INFO] Offsite snapshot count for %s fell %d -> %d and the clean-up windows could not be read — the half-rule decides", customerID, prev, off.SnapshotCount)
|
||||
}
|
||||
if drop < snapshotDropFloor {
|
||||
return false, prev, off.SnapshotCount, false
|
||||
|
||||
@@ -94,6 +94,7 @@ func TestR435_WindowExplainsOnlyPart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -107,6 +108,15 @@ func TestR435_TimeoutWindowIsUnknownNoAlarm(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user