feat(triage): cluster-scoped postpone/suppress for attack paths #47
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Motstandskraft/spam!47
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/attack-path-suppression"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements #22 — operational triage decisions (postpone / suppress) on an asset's attack path, scoped to a set of clusters.
Design
cve+secrettoday; archived/abandoned deps slot in later without touching the suppress plumbing). CVEs are grouped into 3 likelihood bands —immediate(KEV),likely(EPSS ≥ 0.5 or CRITICAL on an internet-exposed digest),possible(EPSS ≥ 0.1) — so raw EPSS jitter never churns the identity.(asset, cluster); hides the path for everyone who can read that cluster; actor + reason recorded and shown to all cluster viewers. A user can never suppress a cluster they can't access (hard 403), and may target all or a subset of their accessible clusters.Changes
api/internal/attackpathpackage (model, contributor registry, band logic, CVE + secret contributors).POST/DELETE /api/triage/attack-path/suppress[/{id}]+GET .../suppressions, ACL-enforced and audited (triage.suppress).20260709—attack_path_suppressionspartial unique index (soft-revoked history preserved).N/Mbadge. Bounded to assets that actually carry a suppression.AttackPathSuppressDialog.svelte(kind toggle, duration, required comment, cluster multiselect), inline history with revoke, header badges, Suppressed section.Coexists with
component_vexandsecret_dismissals; does not replace them. First cut covers image (digest) attack paths; repos/clusters-as-assets are a follow-up (model is generic onasset_type).Verification
go build ./...,go vet ./...clean;go test ./internal/attackpath/...(bands, escalation both directions, postpone window).npm run check— changed files error-free.Closes #22
🤖 Generated with Claude Code
The history {#each} was guarded by `(suppressionHistory.get(id) as SuppressionRow[] | undefined)?.length`, but the map value is `SuppressionRow[] | 'loading' | null`. During the fetch the value is the string 'loading', whose `.length` is 7 (truthy), so the guard passed and {#each hist as h (h.id)} iterated the string's characters — 7 items each with `h.id === undefined` → each_key_duplicate on first expand. This is the real cause (the earlier id-dedupe only touched the resolved array, never the 'loading' sentinel). Guard with Array.isArray so only a real array ever renders, which also removes the misleading cast. Repos hit this reliably; a previously-loaded image skips the 'loading' state, which is why images appeared to work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Building on the resurfaced-explanation lines: make the escalation data structured (EscalationChange{kind, id, detail}) instead of plain strings, so the frontend can map the exact canonical ids that changed. Contributor.Explain now returns []EscalationChange — cve_new / cve_climb carry the canonical id, secret_new carries just the detail line. UI: the expanded attack-path CVE list shows a small pill after the fix column — "new" (warning) for a CVE newly on the path, "escalated" (accent) for one that rose a band — for exactly the CVEs that made the suppression resurface. The resurfaced summary lines still render (change.detail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The new/escalated pills said what changed but not what it means. Wrap each in the existing Tooltip component (matching the EPSS-badge tooltip style): a titled explanation of new vs escalated — new = wasn't on the likely-exploited path at decision time; escalated = already on the path but climbed a band (new KEV, or EPSS crossing a threshold) — plus the exact change line underneath. escalatedCves now carries {label, detail, climb} per canonical id so the pill can render the term, tooltip copy, and specific detail without re-deriving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>A custom postpone date comes from <input type="date"> as "YYYY-MM-DD". new Date("YYYY-MM-DD") parses that as UTC midnight, so for any viewer behind UTC the stored expiry rendered a day early (toLocaleDateString shifts the UTC instant back into local time), and picking today produced an instant already in the past — leaving the Postpone button silently disabled via the canSubmit future-date check. Parse the y/m/d parts as a local date and snooze to end-of-day, so the chosen day is honored inclusively regardless of the viewer's timezone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.