feat(security): per-image scroll + retention cap in scan history (#1231)

* feat(security): per-image scroll + retention cap in scan history

Long scan histories for hot images used to monopolise the Scan history
sheet: a single image with dozens of scans pushed every other image off
screen, and the underlying vulnerability_scans table grew without
bound.

Each image group's table now renders inside its own ScrollArea capped
at max-h-64 (~6 rows visible) so a busy image scrolls independently
while the list of images stays navigable. A new global setting
scan_history_per_image_limit (default 50, min 5, max 1000) backs both
a window-function query that caps the response per image_ref and a
prune step that runs on the existing MonitorService cleanup tick. The
response now carries cappedImageRefs + perImageLimit so the UI can
render a "Capped at N · older scans pruned" hint on groups sitting at
the ceiling without a second settings round-trip.

Single-image deep-dive (imageRef query param) bypasses the cap so a
user clicking into one image can still see its full history. The
prune uses self-contained subqueries to avoid SQLITE_MAX_VARIABLE_NUMBER
issues on first-run installs with large backlogs, and explicitly
deletes child rows from vulnerability_details, secret_findings, and
misconfig_findings inside a transaction since FK cascade is not
enabled at the connection level.

Settings → Developer → Data retention gains a "Scan history per image"
field.

* fix(security): skip searchDraft debounce on mount to stop page-reset race

The searchDraft debounce useEffect fires once on initial mount with the
unchanged value and, 300ms later, unconditionally calls setPage(0).
When a user (or a test) paginates inside that 300ms window, the
pending debounce silently undoes the page advance.

CI surfaced this as a flaky 3rd fetch in the "advances offset when the
user pages forward" test once the per-image cap work added enough
state-update overhead to push the click past the 300ms threshold on
the slower Linux jsdom run.

Track searchDraft with a ref and exit the effect when the value has
not actually changed, so the debounce only runs in response to real
user typing.
This commit is contained in:
Anso
2026-05-25 23:44:31 -04:00
committed by GitHub
parent 80499ee18d
commit 42e8d3a78c
8 changed files with 352 additions and 85 deletions
+2
View File
@@ -23,6 +23,7 @@ const ALLOWED_SETTING_KEYS = new Set([
'log_retention_days',
'audit_retention_days',
'mesh_auto_recreate',
'scan_history_per_image_limit',
]);
// Bulk PATCH schema. All keys optional; present keys are fully validated.
@@ -39,6 +40,7 @@ const SettingsPatchSchema = z.object({
log_retention_days: z.coerce.number().int().min(1).max(365).transform(String),
audit_retention_days: z.coerce.number().int().min(1).max(365).transform(String),
mesh_auto_recreate: z.enum(['0', '1']),
scan_history_per_image_limit: z.coerce.number().int().min(5).max(1000).transform(String),
}).partial();
export const settingsRouter = Router();