Files
sencho/docs/features/cve-suppressions.mdx
T
Anso fcd44f5693 fix(security): tie fixable CVE posture to image-update evidence (#1815)
* fix(security): tie fixable CVE posture to image-update evidence

Stop treating Trivy fixed_version alone as an Update affected images CTA. Reuse persisted ImageUpdateService status so Security only offers Review update when an applicable image update is confirmed, and otherwise surfaces waiting or uncertain remediation with truthful affordances.

* fix(security): move image-update recheck helper out of OverviewTab

Satisfy react-refresh/only-export-components so Frontend lint passes.

* fix(security): preserve posture reason image targets in Images drill-down

Carry affected image refs on overview reasons so public exposure and related CTAs open a clearable targeted Images list instead of an unfiltered hunt.

* fix(security): attach Networking exposure intent to posture targets

Preserve stack/service context and intentional classification on network-exposed Security reasons without suppressing risk or claiming Internet reachability.

* fix(security): persist Images exposure intent and triage scope

Standing image summaries carry Networking intent context with cap-safe aggregates, Anatomy Networking links, and scan-sheet triage that defaults to the current image.

* fix(security): clear CI lint errors for exposure helpers

* fix(security): stop intentional exposure from forcing Action needed

Separate exposure fact, intent correctness, and vulnerability drivers so
package fixed_version cannot recreate a permanent public_exposure blocker.

* fix(security): define Monitoring residual-risk narrative

* fix(security): define Secure via residual Crit/High triage

Replace triage-blind raw Crit/High Secure gating with residual material
risk so accepted and ignored stay Monitoring, while not_affected, false
positive, and fixed can clear residual without claiming no detections.

* fix(security): exclude rollback-hold images from Security scans

Hold-only sencho-rb tags are recovery state; keep them out of Trivy node scans, Security inventory, and Overview posture while dual-tagged images remain under their registry tag.

* fix(security): keep authoritative no-update rows after preview

Opening a stack page must not delete ok+false stack_update_status evidence; Security treats a missing row as uncertain and would flip waiting-upstream to unknown.
2026-08-12 15:02:14 -04:00

148 lines
13 KiB
Plaintext

---
title: "CVE Suppressions"
description: "Accept known-benign CVEs once and have them dimmed across the fleet at read time, without ever modifying stored scan results."
---
Not every CVE that Trivy reports needs a response. Some are false positives on your base image, some have been accepted after security review, and some are waiting on an upstream patch. CVE suppressions let you annotate those findings once so they stop competing for attention in every scan, comparison, and alert.
<Note>
CVE suppressions are available on every tier. The **admin** role is required to add, edit, or remove them. Suppressions created on the control Sencho instance replicate to every remote 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, export, or comparison, Sencho checks each finding against the active suppression list:
- Suppressed findings remain in the database and in the stored severity counts. Nothing is deleted.
- In the scan drawer and the comparison sheet, suppressed rows render dimmed and carry a small shield-off icon next to the CVE ID.
- Hovering the package column on a suppressed row surfaces the recorded reason so reviewers can see why it was accepted.
Badge counts in the Resources Hub continue to reflect the raw findings. Suppressions are a visual filter, not an accounting change. Remove a suppression and the underlying finding resurfaces on the next read, without rescanning.
## Creating a suppression
Open the **Security** page → **Suppressions** tab, then click **Add suppression**. The same tab also holds a **Misconfig acknowledgements** panel for Compose-config findings; that is a related but separate feature, covered in [Misconfig acknowledgements](/features/vulnerability-scanning#misconfig-acknowledgements).
<Frame>
<img src="/images/cve-suppressions/settings-panel.png" alt="Security page Suppressions tab with the CVE Suppressions panel listing two rules: one CVE tagged Accepted risk with a package image-pattern badge and a reason, and one GHSA tagged Not affected with an image-pattern badge, a reason, and a Justification line. Each row has edit and delete icons, with the Misconfig acknowledgements panel visible below." />
</Frame>
The dialog has the following fields:
| Field | Description |
|-------|-------------|
| **CVE or advisory ID** | Required. Accepts both `CVE-YYYY-NNNN` and `GHSA-xxxx-xxxx-xxxx`. |
| **Triage decision** | How the finding was triaged: accepted risk (default), not affected, false positive, affected, fixed, ignored, or needs review. Not affected, false positive, and fixed clear residual Crit/High so Secure can become reachable. Accepted and ignored residual risk stay on Monitoring and do not turn Secure green. Needs review and affected stay actionable. |
| **OpenVEX justification** | Required when the triage decision is not affected or false positive. Explains why the vulnerable code is not exploitable, for example vulnerable code not present, vulnerable code not in the execute path, component not present, or inline mitigations already exist. Carried into the OpenVEX export for that decision. |
| **Package (optional)** | Leave blank to suppress every occurrence of this CVE across every package, or pin a specific package name (e.g. `openssl`) to narrow the scope. |
| **Image pattern (optional)** | Glob applied to image references (`*` matches any sequence, case-sensitive). For example, `lscr.io/linuxserver/*` matches every LinuxServer image, and `*alpine*` matches anything containing `alpine`. Leave blank to apply fleet-wide. |
| **Reason** | Required. A short note explaining why the CVE is accepted. Surfaced on every suppressed row and on the hover title in scan results. Do not paste credentials, tokens, or vendor secrets, since reasons replicate fleet-wide. |
| **Expires in (days, optional)** | Number of days after which the suppression stops applying. Useful for "patched in the next release" entries. Leave blank for an indefinite suppression. |
<Frame>
<img src="/images/cve-suppressions/create-dialog.png" alt="New suppression dialog with kicker SUPPRESSIONS · NEW, title New suppression, and the form filled in for a GHSA scoped to a LinuxServer image, with the Triage decision set to Not affected and the resulting OpenVEX justification field set to Vulnerable code not present" />
</Frame>
### Suppressing directly from a scan result
The panel's empty state hints at the faster path: from any vulnerability scan, click **Triage finding**
on a finding's row. The dialog opens pre-filled with the CVE ID, the package name, and the **Image
pattern** set to the current image reference so the decision stays scoped to that image by default.
You set the Triage decision (accepted risk by default), add a Reason, and an optional Expiry. You can
clear or broaden the image pattern if you intend a wider match. Choosing not affected or false
positive also requires an OpenVEX justification. This is the recommended workflow for everyday
triage. To start from a blank image pattern for fleet-wide matching, create the rule from the
**Security** page → **Suppressions** tab instead.
### How specificity is resolved
When more than one suppression matches the same finding, the most specific one wins. Specificity is scored as:
1. Both **Package** and **Image pattern** set: score 3 (most specific).
2. **Package** only: score 2.
3. **Image pattern** only: score 1.
4. Neither (the broadest fleet-wide rule): score 0.
The winning rule's Reason field is the one displayed on the row.
## Viewing suppressed findings
Open any scan drawer and look at the Vulnerabilities table. Suppressed rows are dimmed and carry a shield-off icon next to the CVE ID:
<Frame>
<img src="/images/cve-suppressions/suppressed-row.png" alt="Vulnerabilities tab of a Sencho image scan showing one dimmed suppressed CVE row with a small shield icon next to the CVE ID, contrasted against the full-brightness unsuppressed rows above and below it" />
</Frame>
The same dim-and-icon treatment carries through to the **Compare** sheet, so a CVE you've already accepted does not look like a new regression when comparing against an older baseline. Hovering the package column of a suppressed row reveals the recorded Reason without expanding the row.
## Fleet-wide replication
Suppressions are managed on the **control** Sencho instance and replicate automatically to every remote you've registered:
- Creating, editing, or removing a suppression on the control pushes the full list to every remote.
- A Sencho instance that has received at least one push from a control is a **replica**. On a replica, the **Security** page → **Suppressions** tab shows the suppression list read-only, and replicated rows carry a small `replicated` badge so they are easy to tell apart from any locally-created entries.
- Suppressions and misconfig acknowledgements are always managed on the instance you are signed into directly, never proxied through the node switcher. If you're signed into one Sencho instance and pick a **remote node** from the node switcher, the Suppressions tab hides both panels and shows a "Managed on the local instance" notice instead, telling you to switch to that node's own URL to view or edit its rules.
<Frame>
<img src="/images/cve-suppressions/remote-node-banner.png" alt="Security page Suppressions tab with a remote node selected from the node switcher, showing a Managed on the local instance notice instead of the suppression panels" />
</Frame>
The full replication, retry, and reanchor flow (including the API call to re-bind a replica to a new control) is documented in [Fleet Sync](/features/fleet-sync).
## Suppressions and deploy blocking
By default, suppressions do not affect [block-on-deploy policies](/features/vulnerability-scanning#honoring-suppressions-in-deploy-blocks). A policy evaluates the raw scan result, so a CVE you have suppressed still blocks a deploy that violates the threshold. Suppressions silence the noise; the gate stays strict.
To have an accepted CVE stop counting toward the gate, an admin can enable **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab. With it on, a block-on-deploy policy re-derives each image's severity from the findings that remain after suppressions are applied, so a deploy whose only blocking findings are all suppressed proceeds without a manual bypass. Sencho records each such suppression-driven pass in the audit log.
The toggle is off by default and is set per Sencho instance, because the gate runs on whichever instance performs the deploy.
## Editing a suppression
Click the pencil icon on any row you created (not visible on `replicated` rows viewed from a replica) to open the same form pre-filled. The CVE or advisory ID and Package are identity fields and stay fixed; the Image pattern, Triage decision, OpenVEX justification, Reason, and Expiry are all editable.
<Frame>
<img src="/images/cve-suppressions/edit-dialog.png" alt="Edit suppression dialog with the CVE or advisory ID and Package fields disabled and greyed out, and the Image pattern field focused, showing the Save changes button" />
</Frame>
To change the CVE, advisory ID, or package a rule targets, remove the existing rule and create a fresh one instead; those two fields cannot be edited in place.
## Removing a suppression
Click the trash icon on any row in the panel. A confirmation dialog ("Remove suppression", kicker `SUPPRESSIONS · REMOVE · IRREVERSIBLE`) warns that future scan results will surface the CVE again wherever it applies.
## Export awareness
Suppressed findings carry through to the [SARIF export](/features/vulnerability-scanning#sarif-export) with a SARIF `suppressions` entry of `kind: external` and `status: accepted`. Code-scanning dashboards that respect SARIF suppressions dismiss those findings with the Reason you recorded.
You can also export the full set of triage decisions as an **OpenVEX** document from the Suppressions panel. Each decision becomes a VEX statement (`not_affected`, `fixed`, `affected`, or `under_investigation`) with its justification, for use with VEX-aware scanners and supply-chain tooling.
## Troubleshooting
<AccordionGroup>
<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. Suppressions apply when results are read for display (the scan drawer, the Compare sheet, the SARIF export), not when they are stored. Open the scan drawer to confirm the row is dimmed with a shield-off icon.
</Accordion>
<Accordion title="A suppressed CVE still blocks my deploy">
Block-on-deploy policies evaluate the raw scan result by default, so a suppressed CVE still counts toward the block. If you want accepted CVEs to stop counting, enable **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab on the instance that runs the deploy. See [Suppressions and deploy blocking](#suppressions-and-deploy-blocking) for the full behavior.
</Accordion>
<Accordion title="A suppression I added on the control is not visible on a replica">
Replication runs on every write. If the push failed (network blip, replica restart), the control retries every 5 minutes for 24 hours and the replica picks up the latest state on the next successful push. See [Fleet Sync](/features/fleet-sync) for how to investigate persistent push failures.
</Accordion>
<Accordion title="I see suppressions on a replica but cannot edit them">
Replicas are read-only for security rules. Sign in to the control instance to add, edit, or delete suppressions; changes sync automatically. Replicated rows show a `replicated` badge and no edit or trash icon.
</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 (case-sensitive) 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 panel shows an `expired` badge on that row; remove it and create a fresh rule.
</Accordion>
<Accordion title="A replica refuses pushes from my control with 'control identity mismatch'">
Each replica anchors to the first control fingerprint it receives. If a different control later tries to push (for example, after rebuilding the control from a snapshot or migrating it to a new server), the replica rejects the push until it is reanchored. The reanchor procedure (admin API call on the replica) is documented in [Fleet Sync](/features/fleet-sync#control-anchor); the next push from any control after a reanchor becomes the new anchor.
</Accordion>
<Accordion title="My suppression list is large and the fleet sync warns about truncation">
The fleet sync wire protocol caps a single push at 5,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 5,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>
</AccordionGroup>