mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 10:49:35 +00:00
f794702171
* feat(security): reframe masthead as action posture, not worst-CVE severity Derive the Security masthead from an action posture (Action needed / Monitoring / Secure / Unknown) instead of raw scanner severity, and label the raw Critical/High counts as scanner detections. "Secure" now means nothing is actionable right now, never a claim that no vulnerabilities exist; Unknown covers a missing scanner or a node with no completed scan. Phase-1 bootstrap: "actionable" is approximated from the overview facts that already exist (fixable findings, secrets, misconfigs); a later phase moves the bucketing to the backend. * feat(security): derive overview action posture from triaged facts Add deriveSecurityPosture as the single bucketing function and extend /security/overview with posture facts (fixableCriticalHigh, dangerousCompose, accepted, rawCritical/rawHigh, plus knownExploited/publiclyExposed placeholders that later phases populate) and the derived posture verb. Suppression- and acknowledgement-aware counts come from one bounded read-time pass over the latest-scan Critical/High findings, grouped per image so the existing read-time filters apply unchanged. The pass is capped and flags posturePartial, so a large node degrades gracefully instead of scanning every detail row. The masthead now prefers the backend posture and keeps the local bootstrap only as a fallback for older remote nodes reached through the proxy. * feat(security): capture Trivy finding enrichment (status, CVSS, vendor, purl, layer) parseTrivyOutput now keeps the per-finding fields Trivy already returns and we previously discarded: Status (fixed / will_not_fix / end_of_life / ...), CVSS (score + vector, preferring the NVD source then falling back), vendor severity, package URL, package path, and layer digest. Persisted on vulnerability_details via additive nullable columns (guarded ALTER), bound null when absent, and carried through the cached-scan reconstruction path. These fields separate scary from exploitable and feed the action posture and the per-finding evidence tags. Field paths verified against Trivy's documented image-scan JSON; covered by parse and insert/read round-trip tests. * feat(security): add CVE exploit-intel service (CISA KEV + FIRST EPSS) Add CveIntelService, a daily background cache of CISA KEV membership and FIRST EPSS scores stored in a new cve_intel table and joined to findings at read time by CVE id (never frozen onto scan rows, so a CVE entering KEV later lights up on scans already stored). EPSS is fetched only for CVE ids present in stored findings, batched; both feeds are best-effort and keep the last cache on failure, so the Security page degrades gracefully offline. Wired into startup/shutdown like the other background services. The overview now counts known-exploited Critical/High findings, and KEV membership escalates posture to Action needed even when no fix is available. A per-instance "Exploit intelligence" toggle on the scanner setup surface lets air-gapped or firewalled hosts disable the outbound fetch; the daily tick keeps running but skips the fetch body when it is off. * feat(security): show per-finding evidence tags (KEV, EPSS, vendor status, CVSS) The vulnerabilities endpoint joins read-time exploit intel (KEV membership and EPSS score) onto each finding by CVE id, and the scan sheet renders evidence tags beside each CVE: known-exploited, EPSS probability, vendor will-not-fix / end-of-life, and the CVSS score. Severity becomes one signal among several so an operator can tell scary from exploitable, with no invented composite score. * feat(security): evolve CVE suppressions into triage decisions Layer a triage status and optional OpenVEX justification onto CVE suppressions. Statuses: needs review / affected / not affected / accepted risk / fixed / false positive / ignored. Dismissing states (not affected, accepted, fixed, false positive, ignored) stop a finding from driving the action posture; needs review and affected stay actionable and are surfaced as counts. Existing rows default to "accepted" (the prior suppress behavior), so nothing changes for them. The overview now reports needsReview / notAffected / accepted as distinct facts derived from the triage status. The decision replicates across the fleet (snapshot + replicated-insert carry status + justification) so a replica's posture matches the control node. The inline suppress dialog gains a triage decision selector; the read-time filter surfaces the status and justification on every finding. * feat(security): export fleet triage decisions as OpenVEX (Admiral) Add an OpenVEX exporter that turns the instance's CVE triage decisions into a standard VEX document (not_affected / fixed / affected / under_investigation, with justifications), and a GET /security/vex/export endpoint to download it. Authoring fleet VEX is a governance capability, so it is gated to Admiral (paid) plus admin, mirroring the SARIF export gate; the Suppressions panel shows an Export VEX action only on Admiral. * docs(security): document action posture, evidence tags, exploit intel, and triage Update the Security page and CVE suppressions docs for the action-posture masthead (scanner detections vs product posture), per-finding evidence tags (KEV / EPSS / CVSS / vendor status), the exploit-intelligence toggle (CISA KEV + FIRST EPSS) on scanner setup, triage decisions layered on suppressions, and OpenVEX export of fleet triage decisions. * test(security): match intel hosts exactly in CveIntelService test Route the fetch stub and its call assertions by exact hostname (www.cisa.gov / api.first.org) instead of a domain substring check. Resolves the js/incomplete-url-substring-sanitization code-scanning alerts on the test's URL routing; behavior is unchanged.
128 lines
11 KiB
Plaintext
128 lines
11 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**.
|
|
|
|
<Frame>
|
|
<img src="/images/cve-suppressions/settings-panel.png" alt="Security page Suppressions tab with the CVE Suppressions panel listing two accepted CVEs, each showing the CVE ID, an outlined package badge, a clamped reason line, the author and expiry metadata, and a trash icon for removal" />
|
|
</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, fixed, ignored, or needs review, with an optional OpenVEX justification. A decided state (accepted, not affected, false positive, fixed, ignored) stops the finding from driving the action posture; "needs review" keeps it counted but still actionable. |
|
|
| **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 all five fields populated with example values for a Git CVE scoped to LinuxServer images" />
|
|
</Frame>
|
|
|
|
### Suppressing directly from a scan result
|
|
|
|
The panel's empty state hints at the faster path: from any vulnerability scan, click the small shield icon at the right edge of a finding's row. The dialog opens pre-filled with the CVE ID and the package name from that row (both read-only in this flow), leaving you to add a Reason, an optional Image pattern, and an optional Expiry. This is the recommended workflow for everyday triage, because it keeps the scope as narrow as the originating finding. To broaden the scope (for example, to suppress across every package), 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 two dimmed suppressed rows for Docker CVEs at the top with the shield-off icon, and one un-suppressed GHSA row below at full brightness with an inline Suppress button on the right" />
|
|
</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.
|
|
- When you're signed into a control and have a **remote node selected** from the node switcher, the CVE Suppressions panel itself is hidden and a "Scanner is per-node" banner explains that scanning runs on the remote while rules live on the control.
|
|
|
|
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.
|
|
|
|
## 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.
|
|
|
|
To change a suppression's scope (for example, to narrow an image pattern or extend the expiry), remove the existing rule and create a fresh one with the updated fields.
|
|
|
|
## 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.
|
|
|
|
Admiral instances 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 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>
|