From 44847836a2021b6988079010bb16e63facfd512e Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 19:41:33 +0000 Subject: [PATCH 1/2] test(audit): add failing test for audit log limit cap + offset pagination (PILOT-305) Verify that limit > 100 is capped, and that offset correctly skips entries for cursor-based pagination. Expected to fail until the handler is updated. --- zz_handlers_more_test.go | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/zz_handlers_more_test.go b/zz_handlers_more_test.go index f50f2d2..5520e25 100644 --- a/zz_handlers_more_test.go +++ b/zz_handlers_more_test.go @@ -336,6 +336,52 @@ func TestServer_HandleGetAuditLog_FiltersByNetwork(t *testing.T) { } } +func TestServer_HandleGetAuditLog_LimitCappedAt100(t *testing.T) { + t.Parallel() + s := newTestServer(t, "admin") + for i := 0; i < 200; i++ { + s.appendAudit("evt", 0, 0) + } + resp, err := s.handleGetAuditLog(map[string]interface{}{ + "admin_token": "admin", + "limit": float64(999), + }) + if err != nil { + t.Fatal(err) + } + entries := resp["entries"].([]map[string]interface{}) + if len(entries) > 100 { + t.Fatalf("limit capped: got %d entries, want <= 100", len(entries)) + } +} + +func TestServer_HandleGetAuditLog_OffsetsPastFirstPage(t *testing.T) { + t.Parallel() + s := newTestServer(t, "admin") + s.appendAudit("oldest", 0, 0) + s.appendAudit("middle", 0, 0) + s.appendAudit("newest", 0, 0) + resp, err := s.handleGetAuditLog(map[string]interface{}{ + "admin_token": "admin", + "limit": float64(2), + "offset": float64(1), + }) + if err != nil { + t.Fatal(err) + } + entries := resp["entries"].([]map[string]interface{}) + if len(entries) != 2 { + t.Fatalf("offset+limit: got %d entries, want 2", len(entries)) + } + // With offset=1, newest is skipped; should get middle, then oldest + if entries[0]["action"] != "middle" { + t.Fatalf("first after offset: %v, want middle", entries[0]["action"]) + } + if entries[1]["action"] != "oldest" { + t.Fatalf("second after offset: %v, want oldest", entries[1]["action"]) + } +} + // --- handleBeaconRegister ----------------------------------------------- func TestServer_HandleBeaconRegister_RequiresAdmin(t *testing.T) { From c314d0f6c9debfdf7903f6b3e5fc4f51259bcd2b Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Sat, 30 May 2026 19:42:41 +0000 Subject: [PATCH 2/2] fix(audit): cap audit log limit at 100, add offset-based pagination (PILOT-305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit log handler accepted limit values up to 1000 (full ring buffer) with no pagination support, allowing a single admin call to dump the entire audit trail. This makes bulk exfiltration trivial. Changes: - Cap max limit at 100 (down from 1000) — one call can no longer pull the entire ring buffer - Add optional "offset" parameter for cursor-based pagination — an admin can still page through the full log but must make N deliberate calls Combined with PILOT-304 (tamper-evident audit log), this raises the bar for covert audit scraping. Closes PILOT-305 --- server_handlers.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server_handlers.go b/server_handlers.go index 2bd5eb2..0ae0b1c 100644 --- a/server_handlers.go +++ b/server_handlers.go @@ -38,22 +38,31 @@ func (s *Server) handleGetAuditLog(msg map[string]interface{}) (map[string]inter filterNetID := jsonUint16(msg, "network_id") limit := 100 - if l, ok := msg["limit"].(float64); ok && l > 0 && l <= 1000 { + if l, ok := msg["limit"].(float64); ok && l > 0 && l <= 100 { limit = int(l) } + offset := 0 + if o, ok := msg["offset"].(float64); ok && o >= 0 { + offset = int(o) + } s.auditMu.Lock() all := make([]AuditEntry, len(s.auditLog)) copy(all, s.auditLog) s.auditMu.Unlock() - // Filter and reverse (newest first) + // Filter and reverse (newest first). Skip offset-matched entries before collecting. var entries []map[string]interface{} + skipped := 0 for i := len(all) - 1; i >= 0 && len(entries) < limit; i-- { e := all[i] if filterNetID != 0 && e.NetworkID != filterNetID { continue } + if skipped < offset { + skipped++ + continue + } m := map[string]interface{}{ "timestamp": e.Timestamp, "action": e.Action,