Files
sencho/docs/features/cve-suppressions.mdx
T
Anso 4b1de35dda Audit-hardening pass for fleet-replicated CVE suppressions (#976)
* perf(security): bucket CVE suppressions by id at read time

applySuppressions now builds a Map<cve_id, suppression[]> once before the
per-finding loop, dropping per-finding work to O(matching-cve-suppressions)
rather than O(all-suppressions). At a fleet-wide cap of 10000 rows against
a multi-thousand-finding scan, the prior linear-per-finding shape drifted
into tens of millions of comparisons per render.

Public API of findSuppression and applySuppressions is unchanged.
Specificity scoring, expiry handling, and image-glob matching are
preserved bit-for-bit. Adds a regression guard that pins a 10000x2000
workload under 1.5s and asserts at least one match was actually returned.

* feat(security): record audit-log entries on control-side CVE suppression CRUD

The replica receive path already wrote an audit-log entry on apply; the
control-side POST/PUT/DELETE handlers did not. Operators reading the
audit panel saw mirrored security-rule changes from the replica view but
could not see who originated them on the control. Symmetric logging
closes that gap.

The summary records the CVE id and pinned scope (pkg, image) but never
the suppression's reason text. Reasons are free-form admin input that
replicate fleet-wide and may carry incident-tracker IDs or vendor
context the operator did not intend to broadcast.

The summary is also sanitised before emission so an operator-supplied
package name or image pattern carrying a smuggled newline plus a forged
"cve_suppression.delete:" prefix cannot inject a fake row into the audit
panel. Control characters become "?" and the field is capped to its
validator length.

Adds a Control-side audit log block to suppression-routes.test.ts that
asserts the privacy contract on each verb (scope present, reason absent),
plus a log-injection guard test, plus a regression that GET still works
when the local instance is a replica.

* test(security): cover cve_suppressions fleet-sync receive path

The existing fleet-sync route tests covered the protocol mechanics
(auth, anchor, stale push, reanchor, demote) for cve_suppressions only
with empty-rows payloads. The suppression-specific concerns were
unverified end-to-end:

  - actual rows replace prior replicated rows on a fresh push
  - the receive path writes an audit-log entry naming the source
    fingerprint and row count
  - a malformed suppression row is rejected at the validator before any
    DB write

Adds a focused block exercising those three properties against the real
Express app, real SQLite, and the real apply transaction.

* docs(features): refresh CVE suppressions troubleshooting and field guidance

Converts the troubleshooting section to Mintlify Accordion blocks so
each entry is foldable and the page stays scannable. Adds entries for:

  - control-identity mismatch on a replica (anchor-aware reanchor flow)
  - mirrored rules persisting after a control demote
  - the 10000-row truncation cap on a fleet sync push

Adds a privacy note to the Reason field guidance: do not paste
credentials, tokens, or vendor secrets there, since the field replicates
fleet-wide and surfaces on every node's suppressions panel.
2026-05-07 16:18:19 -04:00

110 lines
7.9 KiB
Plaintext

---
title: "CVE Suppressions"
description: "Accept known-benign vulnerabilities fleet-wide so your scan results stay focused on findings that actually need action."
---
Not every CVE that Trivy reports requires a response. Some are false positives on your base image, some have been accepted by your security review, and some are waiting on an upstream patch. CVE suppressions let you annotate these findings once so they stop competing for attention in every scan, comparison, and alert.
<Note>
CVE suppressions are available on every tier. Suppressions written on a control node replicate to its replicas at fleet scope.
</Note>
## What suppressions do
A suppression is a rule that says "this CVE is acknowledged." When a scan's findings are read back for display or comparison, Sencho checks each finding against the active suppression list:
- Suppressed findings remain in the database and in the scan totals. Nothing is deleted.
- In the scan drawer and the comparison sheet, suppressed rows are dimmed and marked with a shield-off icon.
- The reason you recorded is visible in the row so reviewers understand why it was accepted.
Counts on badges and summary ribbons continue to reflect the raw findings. Suppressions are a visual filter, not an accounting trick. If you suppress a CVE and then remove the suppression, the finding resurfaces on the next read, without rescanning.
## Creating a suppression
Go to **Settings → Security** and scroll to **CVE Suppressions**, then click **Add Suppression**.
<Frame>
<img src="/images/cve-suppressions/settings-panel.png" alt="Settings Security section showing the CVE Suppressions panel with a list of accepted CVEs" />
</Frame>
| Field | Description |
|-------|-------------|
| **CVE ID** | The identifier of the finding. Accepts both `CVE-YYYY-NNNN` and GitHub advisory IDs like `GHSA-xxxx-xxxx-xxxx`. |
| **Package** | Optional. Leave empty to suppress every occurrence of this CVE regardless of package, or set it to a specific package name (e.g. `openssl`) to narrow the scope. |
| **Image pattern** | Optional glob against image references (e.g. `registry.example.com/app*`). Leave empty to apply fleet-wide. |
| **Reason** | Required. A short note explaining why this CVE is accepted. Surfaced on every suppressed row. Do not paste credentials, tokens, or vendor secrets here — reasons replicate fleet-wide and surface on every node's suppressions panel. |
| **Expires in** | Optional. Number of days after which the suppression stops applying. Useful for "patched in the next release" acknowledgements. Leave empty for an indefinite suppression. |
<Frame>
<img src="/images/cve-suppressions/create-dialog.png" alt="Dialog for adding a new CVE suppression with fields for CVE ID, package, image pattern, reason, and expiry" />
</Frame>
### How specificity is resolved
When multiple suppressions match the same finding, the most specific one wins:
1. A suppression that pins both a package name and an image pattern is the most specific.
2. A suppression with only a package name beats a wildcard pattern.
3. A suppression with only an image pattern beats a fully-wildcard rule.
The reason field of the winning suppression is the one displayed on the row.
## Viewing suppressed findings
Open any scan drawer and scroll to the vulnerability table. Suppressed rows look like this:
<Frame>
<img src="/images/cve-suppressions/suppressed-row.png" alt="Vulnerability scan drawer with a suppressed row dimmed and labeled with a shield-off icon" />
</Frame>
Suppressed rows also carry through to the **Compare scans** view. Both the added and removed columns show the suppression state, so a finding that you've already accepted will not look like a new regression when comparing an older baseline.
## Fleet-wide replication
Suppressions are managed on the **control** Sencho instance and replicate automatically to every remote Sencho you've registered. There is nothing extra to configure:
- Creating or editing a suppression on the control pushes the full list to every remote.
- Remote instances show the suppression list in a read-only state. The **Add Suppression** and **Delete** buttons are hidden, and a banner explains that rules are managed upstream.
- Incoming scan results on the control and every remote apply the same suppression set.
If a push to a remote fails (for example because the remote is temporarily offline), Sencho records the failure and retries on the next fleet sync tick. See [Fleet Sync](/features/fleet-sync) for the details of how replication works and how to inspect push status.
## Removing a suppression
Click the trash icon on any row in the suppressions panel. A confirmation dialog calls out that removing the rule will cause matching findings to reappear in scan results.
To change a suppression's scope (for example, to narrow an image pattern or extend an expiry), delete the existing rule and create a new one with the updated fields.
## Troubleshooting
<Accordion title="I suppressed a CVE but the count on the badge is unchanged">
Badge counts reflect the raw findings so they remain meaningful for alerting and policy evaluation. The filter is applied in the scan drawer, the comparison sheet, and every read surface, but the stored totals do not change. Open the scan drawer to confirm the row is dimmed with a shield-off icon.
</Accordion>
<Accordion title="A suppression I added on the control is not visible on a remote">
Replication runs on every write. If the push failed (network blip, remote restart), check the fleet sync status on the control under **Fleet → Sync status**. The remote picks up the latest state on the next successful push.
</Accordion>
<Accordion title="I see suppressions on a remote but cannot edit them">
Remote Sencho instances are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions. Changes sync automatically.
</Accordion>
<Accordion title="A suppression does not match a finding I expect it to">
Two common causes:
- **Image pattern mismatch.** The pattern uses glob syntax where `*` matches any sequence. `nginx*` matches `nginx:1.25` but not `docker.io/library/nginx:1.25`; use `*nginx*` for a broader match.
- **Expired rule.** If **Expires in** was set, the suppression stops applying after the deadline. The row shows an "expired" badge; edit it or create a fresh rule.
</Accordion>
<Accordion title="A remote refuses pushes from my control with a 'control identity mismatch' error">
Each replica anchors to the first control fingerprint it sees. If the same replica is later pushed from a different control (for example, after rebuilding the control instance from a snapshot or migrating to a new server), the replica rejects the new control until it is reanchored. On the replica, open **Fleet → Sync status** and use **Reanchor** to clear the cached fingerprint so the next push from the new control establishes a fresh anchor. See [Fleet Sync](/features/fleet-sync) for the full reanchor flow.
</Accordion>
<Accordion title="I demoted the control and the old replica still shows the mirrored suppressions">
Demoting a control to a standalone instance only affects that one node. Replicas that were following it keep the last set of replicated rows until either a new control pushes to them or you reanchor the replica. From the replica's **Fleet → Sync status**, use **Reanchor** to drop the mirrored rules; the suppressions panel returns to a clean local-only state.
</Accordion>
<Accordion title="My suppression list is enormous and the fleet sync warns about truncation">
The fleet sync wire protocol caps a single push at 10,000 rows so a misconfigured control cannot wedge a replica with an unbounded payload. If your local list exceeds that cap, the warning appears in the control logs and only the first 10,000 rows are pushed. Trim expired or unused entries from the suppressions panel, or split scopes across distinct rules so each remains meaningful, before relying on fleet replication for the full set.
</Accordion>