Files
sencho/docs/features/vulnerability-scanning.mdx
T
Anso b1decbb32a docs: v1 docs refresh (batch 7) (#1627)
* docs(introduction): refresh screenshots and correct stale nav/tab coverage

Replace all 5 screenshots with fresh 1920x1080 production captures.
Document the shipped Take down stack action, the Compose Labels stack
tab, and the Docker Labels Fleet tab, none of which were mentioned.
Correct the Console nav item to note it is a limited-availability
surface rather than a plain role/tier-gated view.

* docs(quickstart): refresh screenshots and correct preflight, dashboard, and menu drift

Replace all three quickstart screenshots with fresh captures and correct
several claims that drifted from the current UI:

- Document the environment preflight's 7th check (Sencho compose
  location), previously missing entirely from both the text and the
  screenshot alt text.
- Add the Stack health table's Source and Port columns, and note that
  columns are sortable and default to load order.
- Note the masthead's running-container count, live CPU/memory readout,
  and alert count.
- Fix the profile menu list: replace the nonexistent "Feedback" entry
  with "Open New Issue", drop "Appearance" (it lives in Settings, not
  the profile menu), and add the conditional "Billing" entry.
- Note that the sidebar groups stacks by Docker Compose label once
  labels are assigned, with pinned stacks first.

* docs(configuration): document missing env vars and fix JWT secret wording

Add SENCHO_UPLOAD_DIR, TRIVY_CACHE_DIR, SENCHO_MESH_RECONCILE_INTERVAL_MS,
SENCHO_MESH_PROXY_TUNNEL_IDLE_MS, SENCHO_COMPOSE_COMMAND_TIMEOUT_MS, and
SSO_OIDC_CUSTOM_ENABLED, all real environment variables that were missing
from the reference tables. Clarify that the JWT signing secret has no
environment variable at all, generated and stored in the database only,
rather than implying an unused JWT_SECRET var exists.

* docs(stack-management): refresh against current live app and source

Rewrites the Stack Management page against the production node and
frontend source: adds the Compose Labels anatomy tab (new since the
last refresh), the Mute action on the stack header and sidebar
context menu, corrects the update-available banner wording and the
sidebar context menu's lifecycle ordering, notes the Doctor tab's
severity dot and the scan-status banner, and replaces all 14
screenshots with fresh production captures.

* docs(editor): refresh against current live app and source

Replace all 8 screenshots with fresh 1920x1080 production captures (mobile
shot at phone viewport). Document the self-stack protection dialog that
blocks deploy/delete actions on the stack running the current Sencho
instance, the multi-container summary strip and Compact/Detailed density
toggle, the mutually exclusive Expand containers / Expand logs controls,
the Files tab full-screen toggle, and the post-deploy scan-status banner
on the Anatomy panel, none of which were previously covered.

* docs(editor): recapture mobile compose screenshot without redaction

The previous mobile screenshot used the plex stack, whose compose volume
mount includes a real host path that had to be blacked out and overlaid
with placeholder text, leaving a visible seam. Recapture against the
dozzle stack instead, whose only volume is the Docker socket, so the
screenshot needs no editing.

* docs(stack-file-explorer): refresh page against current app and code

Renames "Files & Volumes tab" references to the current "Files" tab
label, documents the full-screen toggle and word-wrap control, expands
the non-browsable-volume reasons to match the current containment
logic (single-file binds, the full protected host-path list, and the
Docker-unreachable named-volume case), documents the 100-item bulk
selection cap and the 5000-file/1 GiB archive limits, corrects the
non-existent DISK_FULL error code, notes that override compose files
are unprotected, and scopes the atomic-write claim to fs-backed roots.
Replaces all 9 screenshots with fresh production captures taken in the
Files tab's full-screen mode, so no other stack UI (health metrics,
logs) appears in the background.

* docs(resources): refresh against current live app and source

* docs(dashboard): refresh against current live app and source

* docs(app-store): refresh against current live app and source

Replaces all 6 screenshots with fresh production captures and corrects
the Environment Variables and About-panel metadata claims to match what
the bundled LinuxServer.io registry actually returns today.

* fix(docs): recapture app-store advanced-tab screenshot without scroll cut-off

The prior capture was taken mid-scroll to reveal the Security checkbox,
leaving the template logo and About text cropped awkwardly at the top.

* docs(global-search): refresh against current live app and source

* docs(deep-links): refresh against current live app and source

- Add App Store, Logs, Update, Console, and Audit to the URL table (previously undocumented)
- Document hub-only URL redirect behavior and cross-link to Multi-Node Fleet
- Note Fleet tab URL segments do not always match their on-screen label (Status/configuration, Map/dependencies)
- Document the no-env-files edge case (Env tab absent, falls back to compose)
- Clarify Settings section-list state is phone-only; desktop always normalizes to /settings/appearance
- Note which top-level views share identical URLs between desktop and phone
- Remove greenfield-violating temporal phrasing ("now has", "as today")
- Replace em dashes in touched bullet list

* docs(appearance): refresh against current live app and source

* docs(stack-activity): refresh against current live app and source

Add the missing "Stack taken down" event category, cross-link the
Suppressed badge to notification mute rules, list the new Compose
Labels tab in the Anatomy panel strip, and replace both screenshots
with current production captures.

* docs(dossier): fix generated-facts wording, add rollback readiness section

Corrects three Dossier tab Generated Facts rows against current frontend
behavior: the Ports count is not filtered to host-published mappings, the
Network row's "bridge" label is fixed text rather than a derived driver,
and missing env var names appear only in the Markdown export, not the
live tab. Adds a Rollback Readiness subsection under Connected Features
(previously only an orphaned Limitations bullet), links to Compose
Networking and Storage Portability, and clarifies that a missing export
section can mean either an older node build or a failed on-demand fetch,
not only a version gap. Recaptures all three screenshots against
production.

* docs(stack-drift): refresh against current live app and source

Fixes the stack detail tab bar description (Compose Labels tab was
missing, Files/Edit compose were wrongly listed as tabs), documents
that re-check only requires read access to the stack (so viewer and
auditor roles can trigger a ledger write), and cross-links the network
findings to the Networking tab's runtime drift section and the Fleet
Overview's Drift filter. Replaces all four screenshots with fresh
1920x1080 production captures.

* docs(compose-doctor): refresh against current live app and source

Corrects the rule count from 31 to 32 and documents the previously
undocumented self-managed-stack guard rule and enforcement, the
per-browser dismiss control for the summary card and Doctor tab dot,
the acknowledged summary state, the full Health-Gated Updates verdict
mapping, and the temporary exposure intent option. Replaces all four
screenshots with production captures.

* docs(compose-networking): refresh against current live app and source

Adds cross-links to the new node-wide Networking operator page, documents
the "view node networking" link and the unset/inherit exposure-intent pill
labels, corrects the stale Resources Hub network-tab reference, and
replaces all screenshots against the production node.

* fix(docs): unbreak stack-activity page render

The screenshot alt text used backslash-escaped quotes, which is
invalid MDX/HTML attribute syntax. Mintlify failed to parse the file
and silently dropped it from routing, so the page 404'd despite being
listed in docs.json navigation.

* docs(environment-guardrails): refresh against current live app and source

Fix the ENV FILES section description: it lists env_file: entries and
project files with an existence problem, not every declared env file.
A cleanly-resolving project file is already named in the Project
Environment File panel above it. Replace all three screenshots with
current production captures.

* docs(docker-label-audit): refresh against current live app and source

Add production screenshots (page had none), a Capability gating
section, a Limitations section, and a Troubleshooting accordion.
Tighten the secret-redaction heuristic description and cross-link
Stack Labels, Compose Doctor, Environment Guardrails, and Node
Compatibility.

* docs(compose-storage): refresh against current live app and source

Captured fresh production screenshots (both prior images were stale)
and fixed a leftover pre-rename card title pointing at the Files tab.

* docs(stack-labels): refresh against current live app and source

Removed the stale trailing-dot sidebar claim (that rendering was
removed from the sidebar in a prior UI pass), documented the new
label-scoped notification muting available from the sidebar, stack
context menu, and Settings panel, corrected the Fleet Actions card
copy to match the redesigned cards, added the previously undocumented
bulk-assign size cap and the node-scoped label bulk-action API, and
replaced all 8 screenshots with fresh production captures.

* docs(sidebar): refresh against current live app and source

Removes the stale per-row label dot claim (removed from StackRow), corrects the update indicator and Updates chip color from orange to fuchsia, and documents Mute/Take down in the context menu, the label group mute kebab, Ctrl+A/Esc bulk-mode shortcuts, pin-eviction toast, and the offline-node skip behavior in cross-node search. Replaces all 8 screenshots with fresh production captures.

* docs(deploy-progress): refresh against current live app and source

Corrected the Scan entry point (node-wide Scan this node from Security
Overview, not a per-stack config scan), added the missing Take down
verb and entry point, broadened recovery actions to cover restart and
rollback failures, fixed the modal status text and raw-output color
claims to match the live UI, and replaced all screenshots with fresh
captures from a throwaway demo stack on the production node.

* docs(atomic-deployments): refresh against current live app and source

* docs(health-gated-updates): refresh against current live app and source

* docs(deploy-enforcement): refresh against current live app and source

Correct the tier note to every-tier (verified against the current
policy gate, which no longer has a paid-only blocking switch), add the
Security Overview deploy-enforcement summary card, tighten the honor-
suppressions default wording, cross-link the separate pre-deploy scan
advisory dialog to avoid confusion with the block dialog, and replace
all three screenshots against the current production UI.

* docs(blueprint-model): refresh against current live app and source

Replaced all 11 screenshots with fresh production captures. Corrected
the volume-destroying-drift Enforce-downgrade claim (appeared three
times): that code path has no call sites in the runtime reconciler, so
any compose edit on a stateful or state-unknown blueprint always
re-enters Awaiting confirmation regardless of drift mode. Corrected
the Create workflow (creation does not trigger an immediate
reconciliation tick) and the Delete workflow (now requires typing the
blueprint name to confirm).

* docs(scheduled-operations): refresh against current live app and source

* docs(blueprint-model): drop tier framing, treat as Community-native

Blueprints carry no tier gate, so "available on every tier" implied a
comparison that doesn't exist. Removed the tier framing from the intro
note, the Prerequisites table, and the Security section; the role
requirement (admin to write, every role to read) already says
everything that matters.

* docs(auto-update-policies): refresh against current live app and source

Documents that every update trigger on this page, Apply now and
scheduled Auto-update tasks alike, runs through the same atomic backup,
build-aware rebuild, and post-update health gate as a manual update
from the stack editor, and is subject to the same deploy enforcement
policy gate. Corrects the readiness-computation description to scope it
to registry-image services and cross-links to Health-Gated Updates,
Atomic Deployments, Deploy Enforcement, and Stack Management. Replaces
the readiness board screenshot with a fresh production capture and
removes three orphaned screenshots left over from the page's prior
CRUD-policy layout.

* docs(auto-heal-policies): refresh against current live app and source

Correct the admin-only prerequisite: viewing policies and history is
open to every signed-in role, only creating/toggling/deleting requires
admin. Rewrite the matching troubleshooting entry to match, note the
overlap behavior between an All-services and a named-service policy on
the same container, and replace all three screenshots with a real
production policy.

* docs(webhooks): refresh against current live app and source

Replaced all three screenshots with fresh production captures (single
webhook create/reveal flow, two-card configured list), verified every
claim against WebhookService/StackOpLockService/registry.ts, and added
the previously undocumented per-stack lock skip behavior for
non-Git-source-sync actions with a matching troubleshooting entry.

* docs(global-observability): refresh against current live app and source

* docs(audit-log): refresh against current live app and source

- Fix nav terminology: Audit is a top navigation tab, not a sidebar tab.
- Correct retention claim: Admiral shows full retained history (default
  90 days), not a fixed 14-day window; the 14-day clamp only applies to
  the Community API, which has no navigation entry point at all.
- Fix settings path: Settings - Operations - Data Retention (previously
  pointed at the now-split Developer Diagnostics section).
- Restore current Recovery Vault naming (was stale Sencho Cloud Backup).
- Expand the tracked-actions list: stack take-down, label management,
  MFA reset, and notification suppression rules were missing.
- Note that assigning the Auditor role itself requires Admiral.
- Replace all four screenshots with fresh production captures (Stream,
  Table, expanded row, Data Retention) reflecting the current five-row
  retention card.

* fix(audit-log): reframe data-retention screenshot to card content only

The prior crop included the Settings sub-navigation panel, which isn't
part of what the surrounding text describes. Retake tightly cropped to
match the page's other screenshots (card content only, no chrome).

* docs(multi-node): refresh against current live app and source

Rewrites the Pilot Agent enrollment flow (now a generated compose.yaml
plus docker compose up -d, not a single docker run command) and the
Settings scope table (current registry: Stacks, Container Alerts,
Docker & Storage, Data Retention, Mute Rules, Recovery, and the
Admiral Account / Recovery Vault renames). Adds a Sencho Mesh
cross-link matching the live add-node form copy. Replaces all eight
screenshots with sanitized production captures.

* docs(pilot-agent): refresh against current live app and source

* docs(readme): refresh GitHub README against current live app

Replace all 9 screenshots (stale top nav showing a removed Console
item and missing the new Networking page); document Take down,
Drift Detection, Environment and Secrets Guardrails, Storage
Portability, Docker Label Audit, Remote Updates, and the node-wide
Networking dashboard; fix a broken non-root-user anchor link; correct
the notification channels list (no email channel exists yet) and the
global search scope (pages, nodes, and stacks, not containers or
services); rename "scan policy packs" to the current "scan policies"
terminology; broaden the App Store bullet to cover custom registries.

* docs(readme): reposition intro copy and refresh badge row

Lead with DevOps/platform/sysadmin framing instead of homelab-first,
drop pre-1.0 "production" language, state plainly that Sencho
provides a UI instead of promotional "does the work you do" phrasing,
and call out that multi-node was part of the architecture from the
start. Swap the Discussions badge for CodeQL, last-commit, open
issues, and live website/docs status badges, and add a blog link.

* docs: retake Dashboard and Global Search screenshots for accuracy

Configuration Status now shows Recovery Vault instead of the stale
Cloud Backup label, and the search palette capture includes the
Networking page entry added after the prior screenshot batch.

* docs: refresh Fleet View page against current app

Fleet View has drifted since its last rewrite: the Docker Labels tab,
Export Dossier action, Networking filter/badge, node label pills and
latency in topology, the Policy sync status row, and the node card
Mute submenu were all shipped but undocumented. The Check Updates and
Add Node actions also moved into the Overview toolbar (renamed Node
Update / Manage Nodes) and no longer sit in the shared action row.

Replaces all five screenshots with fresh production captures and
corrects the masthead's motion description (a calm shimmer on Healthy,
a steady glow on Degraded/Critical, not a pulsing dot). Also fixes a
Fleet Sync limitation that claimed no first-party sync-status panel
exists, now that the Status tab surfaces one.

* docs: refresh Fleet Dossier page against current app

Adds the page's first screenshot, documents the network exposure summary now included per stack, documents the storage-summary omission versus the Stack Dossier export, and adds two troubleshooting entries.

* docs(fleet-federation): deep rewrite for the rollout-approval gate

Federation's cordon and pin controls no longer take effect by
themselves: the reconciler requires a confirmed rollout preview
(Apply now -> Confirm Apply) before it mutates the fleet, and pinning
or unpinning always clears a blueprint's approval. Rewrote the mental
model, step-by-step, lifecycle table, security section, limitations,
practical workflows, troubleshooting, and cross-links to reflect that
gate. Also corrected the audit-log claims: cordon/uncordon/pin are not
filterable by a node.cordon/blueprint.pin action taxonomy, only by the
free-text search box against the method, path, and summary.

Replaced 4 screenshots and added 3 new ones (production fleet plus an
isolated instance to capture the rollout preview dialog in both a
safe and a blocked state), and fixed one stale pin-workflow sentence
in blueprint-model.mdx that the same audit surfaced.

* docs(fleet-federation): drop tier-availability framing

The feature isn't tier-gated, so calling out "every tier" reads as an
unnecessary advertisement. Removed the tier note and the Community/
Admiral prerequisite row, and reworded two role-visibility sentences
that had conflated tier with role.

* docs(fleet-actions): deep rewrite for the v2 action-card redesign

Rewrites the page against the shipped Fleet Action Card redesign: a
single cyan rail with per-card action-class chips instead of the old
per-card rose/purple/amber rails, plus the live blast-radius preview,
dry-run, and confirmed-target binding mechanics that replaced the old
static warning banners. Documents two previously-undocumented
endpoints (match-preview, prune/estimate), the preview-confirm-execute
model shared by all three cards, per-node locking, timeouts, and the
version-gated confirmed-target contract for mixed-version fleets. All
five screenshots recaptured against the live production UI.

* docs(fleet-actions): fix nested quote in screenshot alt text

* docs(fleet-sync): refresh against current app and document proxy gap

Retake all screenshots against the live fleet (control authoring view,
a genuine replica showing the read-only banner, and replicated
suppression rows) and document a previously-unwritten behavior: scan
policies, CVE suppressions, and misconfig acknowledgements are fetched
localOnly and are not proxied through the node switcher like most
other Security page tabs, so viewing a remote's Fleet Sync state
requires signing into that instance directly.

* docs(fleet-backups): refresh against current app and note new integration points

Replaces all 6 screenshots with fresh 1920x1080 production captures and
corrects several drifted details: the Cloud Snapshots panel's pagination
and per-item delete action, the exact Recovery Vault storage-mode label,
and the real per-file size-cap behavior (an oversized compose.yaml skips
the whole stack; an oversized .env is dropped but the stack still
captures). Documents two entry points that were missing from the page:
the pre-update snapshot checkbox in Health-Gated Updates and the snapshot
coverage nudge on the Storage Portability tab. Expands the single warning
banner description in the detail view to cover all four banner types,
notes the Snapshots tab and Recovery Vault are hub-only, and adds a
Where Fleet Backups fits table cross-linking the six adjacent features.

* docs(remote-updates): refresh against current app and add hardened-channel notes

Replace all screenshots with fresh 1920x1080 production captures and fix the
Node Updates trigger label (Check Updates -> Node Update). Add the Changelog
tab, correct the reconnect overlay's stale 'Update timed out'/Try Reloading
copy to the current Taking longer than expected/Reload to check text, and
document the Admiral Hardened Build channel's carve-outs (pinned-but-not-blocked,
entitlement-gated local update path). Disambiguate this page from the
unrelated top-level Update (Health-Gated Updates) nav tab.

* docs(node-compatibility): refresh against current app and expand capability list

Replace all screenshots with fresh production captures (node switcher,
capability lock card, connection test panel) and swap the lock-card example
from a now-hidden Host Console to Audit Log, which is directly reachable.
Bring the capability table from 26 to 35 entries to match the current
registry, add a Mute Rules row, and split out fine-grained fallback
capabilities (update-guard, service-scoped-update, cross-node-rbac,
stack-down-remove-volumes) with their non-lock-card fallback behavior.
Fix the compose-networking row, which incorrectly claimed to also gate the
node-level Networking overview. Note that Pilot Agent nodes never advertise
host-console regardless of version.

* docs(security): refresh against current app and document scan-node launcher

Replaces all screenshots with fresh production captures, documents the
Scan this node launcher and the third Scanner setup toggle (pre-deploy
scan advisory), tightens the Compose risks example list and the Policies
block-condition description to match the live app, and adds an On a
phone section.

* docs(vulnerability-scanning): refresh for exploit intel and risk-based policies

Documents exploit intelligence (KEV/EPSS evidence tags), the redesigned
risk-based scan policies (severity/known-exploited/fixable block
conditions replacing the single max-severity field), and the new
Findings badge state. Replaces all screenshots with fresh production
captures and adds a Secrets tab example.

* docs: refresh CVE suppressions page

Retook all screenshots against production (prior ones predated the
triage-status/OpenVEX UI and the edit capability). Documented the
suppression edit flow, which the page previously described as
delete-and-recreate only. Corrected the remote-node banner copy on the
Suppressions tab and added a screenshot of it. Cross-linked the
Misconfig acknowledgements panel that now sits on the same tab.

* docs(two-factor-authentication): refresh screenshots and document rate-limit layers

Replace all 11 non-count-variant screenshots with fresh production captures.
Document the throttled sign-in state precisely (kicker/hero/caption change,
not just the input), that a replayed TOTP counts toward the lockout counter,
and the separate per-network-address sign-in rate limit that sits in front
of the per-account MFA lockout.

* docs(api-tokens): refresh screenshots and document service-scoped deploy actions

Deploy Only now authorizes eight lifecycle POSTs (was six): the per-service
update and restore endpoints were missing from the scope table. Universal
restrictions gained a row for image channel management, which was already
rejecting API tokens in code but undocumented. Replaced all five screenshots
with fresh captures showing the current three-scope create flow and a
populated list with one token of each scope.

* docs(upgrade): refresh against current app and remove legacy-version framing

Fixes migration-list inaccuracies (registry credentials were never
plaintext, unlike node API tokens), rephrases the SSH/TLS and JSON-config
migration bullets to drop version-numbered legacy framing, adds a GHCR
mirror note matching the quickstart pattern, and cross-links the Hardened
Build entitlement-gated update path so digest-pinned installs aren't sent
down the manual docker pull steps.

* docs: refresh Backup & Restore against current backend

Broadens the encryption.key warning to cover everything CryptoService
actually encrypts (node tokens, Git source and Fleet Secrets, SSO/MFA,
Recovery Vault, fleet snapshot contents), not just registry credentials.
Adds the built-in backupData CLI as a no-host-tooling alternative to the
sqlite3 .backup command, cross-linked to Emergency command-line recovery.
Corrects node-token migration guidance: tokens are signed with a secret
stored in sencho.db and remain valid after a host move, so no
regeneration is required.

* docs: refresh Recovery guide against current Settings page and CLI

Documents the Settings · Recovery page's full System health / Environment /
Safe actions / Command-line hub instead of only its environment-preflight
section, adds the missing "Sencho compose location" preflight check, adds the
SSO/OIDC/LDAP lockout scenario now that disableSso.js exists, and corrects the
rollback description (exact UI label, backup-required and image-layer caveats).

* docs(emergency-cli): sync in-app page description, add CLI operational details

Aligns the "in-app Recovery page" summary with the freshly refreshed Recovery
guide (System health / Environment / Safe actions / Command-line recovery, SSO
providers, one-click command download). Adds constraints verified against the
CLI source: password/username minimums, the valid SSO provider identifiers with
an example, and a note that diagnostics.js always reports Docker as unreachable
(it runs without a live Docker connection, unlike the in-app page).

* docs(troubleshooting): refresh against current backend and nav

Corrects several claims that had drifted from the implementation:
network name validation (underscore is not a valid leading character),
the remote-update delayed-failure window (3 minutes, not 90 seconds),
and an unsubstantiated version-compatibility check on the Admiral 403
entry. Removes two entries describing legacy pre-v0.39 node behavior
that no longer applies to any currently shipped build. Replaces stale
"Profile > Settings > X" navigation with the current Settings hub
paths, and "wifi icon" with the current Test Connection label. Adds
Pilot Agent awareness to the remote-offline entry with a cross-link to
its dedicated troubleshooting section, and trims the docker-run
converter entry to point at the fuller, already-current version on the
Stack Management page instead of duplicating it with a broken anchor.

* docs(verifying-images): document the :dev GHCR integration tag

Verified the existing cosign/SBOM/VEX/tag-policy content against
docker-publish.yml and docker-preview.yml; all accurate. Added the
previously undocumented :dev/:dev-<sha> integration tag published on
every push to main via docker-dev.yml.

* docs(trivy-setup): sync install guide with node-scoped scanner and current Trivy upstream docs

Documents that Trivy installs independently per node, adds the TRIVY_BIN
override and non-PATH mounting option, fixes the deprecated apt-key install
flow and RHEL repo gpgkey placement, notes the Exploit intelligence toggle
for air-gapped hosts, and replaces the stale Scanner setup screenshot.

* docs(two-factor-admin): document lockout recovery and self-reset gotcha

Verified the reset flow, CLI recovery, and SSO toggle claims against
current source and replaced the stale admin-reset modal screenshot.
Added the failed-attempt lockout behavior (undocumented despite being
promised in the page description) and a warning about resetting your
own 2FA from the Users list, which signs you out immediately unlike
the self-service disable flow.

* fix(docs): make Settings and Self-hosting discoverable in the sidebar

The Reference and Operations groups used root pages that were only
reachable by clicking the ambiguous group label, and that click also
triggered an unwanted double action (expand plus navigate). List both
pages as explicit sidebar entries and disable global drilldown so group
headers only expand or collapse.

* docs(settings): sync reference page with current Settings Hub

Renamed License to Admiral Account throughout (Hardened Build channel
switch, image channel display, DURATION pill), corrected the password
policy, documented the new Appearance navigation and log-chip-color
controls plus font options, noted Mesh data plane is not on every
installation, fixed the label cap (50, not 100), and replaced every
screenshot with a fresh capture from the production node.

* fix(docs): apply the sidebar toggle-only fix to Start here and Product guide

These two groups had the same click-to-navigate-and-expand pattern as
Reference and Operations. Flattening them is safe here too: the
Product guide root page has no directory listing depending on it, and
the Start here root page's own hand-authored Next steps CardGroup
already covers the same links the auto-generated listing duplicated.

* docs(licensing): refresh licensing page for Hardened Build and sales-led Admiral pricing

Admiral pricing moved from self-serve checkout to a contact-sales model, and
a new Hardened Build image-channel switcher shipped in the Admiral Account
settings page; neither was reflected in the docs. Also documents the
lifetime-license edge case (no Manage subscription button, no Billing row)
and refreshes all four screenshots against the current UI.

* docs(security): sync reference page with current API-token and encryption scope

Verified every claim against the live implementation: the API-token universal
restrictions list was missing MFA management, Recovery Vault, and image
channel management; the encryption-at-rest field list was missing Recovery
Vault credentials; added a note on the password strength indicator's
recommended-vs-enforced distinction. Refreshed the SSO settings, API tokens,
and audit log screenshots against the production node.

* docs(settings): fix password policy and session-invalidation claims

Cross-checked against auth.ts while verifying the same claims on the
security reference page: the enforced minimum is 8 characters (the
12+/mixed-case/number text is a frontend strength hint, not a validated
rule), and a password change invalidates every other session for the
account rather than leaving them valid.

* docs(contact): sync contact channels with sales-led pricing and current support gating

Replace the retired contact@sencho.io with hello@sencho.io (the address actually
used in the website footer and the pricing page's Get in touch CTA), narrow the
licensing@sencho.io scope to existing-license activation/billing/refunds now that
new Admiral conversations route through hello@sencho.io, drop the stale LICENSE-file
and in-app upgrade-prompt claims (the AGPLv3 relicense removed both), correct the
Support settings path and add the published response-time targets, and remove the
unsubstantiated bug bounty mention.
2026-07-21 09:13:12 -04:00

524 lines
45 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Vulnerability Scanning"
sidebarTitle: Vulnerability scans
description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, prioritize with known-exploited and EPSS evidence, compare scans over time, and gate deploys on policy violations."
---
Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer, enriched with known-exploited (KEV) and EPSS exploit-probability evidence so severity alone is not the only signal. The dedicated [Security page](/features/security) is the command center for risk review: an action-posture overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions with OpenVEX triage, exploit intelligence, risk-based deploy policies, SARIF export, single-scan SBOM export, and managed Trivy auto-update are all available on every tier.
<Frame>
<img src="/images/vulnerability-scanning/resources-badges.png" alt="Resources Hub Images table with severity badges (CRITICAL, HIGH, MEDIUM, Clean, and Findings) on managed image rows alongside the Scan history button" />
</Frame>
## Prerequisites
The Trivy CLI must be available on the machine running Sencho. Trivy is not bundled with the Sencho Docker image; see [Installing Trivy](/operations/trivy-setup) for mount and installation options. Sencho checks for Trivy on startup and hides scanning UI when the binary is not available.
<Note>
Trivy is installed independently on each Sencho instance. When you select a remote node, the **Security** page → **Scanner setup** tab shows only the scanner status for that node; install, update, or uninstall Trivy from there to manage the remote's binary. Scan policies, CVE suppressions, and misconfig acknowledgements are managed on the control instance and replicate fleet-wide.
</Note>
## What's included
Every vulnerability-scanning capability is available on every tier:
- Install / update / uninstall Trivy, with auto-update of the managed binary
- On-demand image scanning and full scans (vulnerabilities + secrets)
- Compose file misconfiguration scanning
- Severity badges in the Resources Hub and a scan results drawer with grouped tabs
- Exploit intelligence: CISA known-exploited (KEV) and FIRST EPSS evidence tags on every finding
- Post-deploy automated scanning and a pre-deploy scan advisory on manual deploys
- Scan history sheet and scan comparison
- CVE suppressions with OpenVEX triage decisions, OpenVEX export, and misconfig acknowledgements
- Scheduled fleet scans (all images on a node)
- Risk-based scan policies (severity threshold, known-exploited, fixable Critical/High) with `block_on_deploy` enforcement, including suppression-aware deploy blocking
- SBOM generation (SPDX, CycloneDX) and SARIF export for code-scanning integration
The [Security page](/features/security) reads these results into an **action posture** (Action needed, Monitoring, Secure, Unknown) that prioritizes fixable and known-exploited findings over raw severity counts. This page covers the scanning mechanics behind that posture: how scans run, what a finding's evidence tags mean, and how policies and suppressions are configured.
## On-demand scanning
Open the **Resources** tab and switch to the **Images** panel. When Trivy is available, every image row shows a shield icon alongside the inspect and delete actions. Click it and the menu offers two scan modes:
- **Scan (vulnerabilities)**: the fast path. Trivy reads package metadata only.
- **Full scan (vulnerabilities + secrets)**: also walks the image filesystem looking for hardcoded credentials, tokens, and keys. Slower because of the filesystem traversal.
A spinner replaces the shield while the scan runs. Most vulnerability scans finish in 10 to 60 seconds depending on image size and whether the Trivy database is cached. Full scans add the time needed to read the filesystem. When the scan completes, a severity badge appears in the row's Status column. Click the badge to open the results drawer.
### Scanning a whole node at once
The **Security** page → **Overview** tab has a **Scan this node** button for admins when the node's scanner is ready. It runs every scan type you select in one pass:
- **Image vulnerabilities** and **Image secrets** scan every image on the node.
- **Compose misconfigurations** scan every stack's compose file.
All three are selected by default; pick any combination, then **Start scan**. Progress streams live in the [deploy progress](/features/deploy-progress) modal as each image and stack is processed, and the Overview refreshes when the run finishes. The scan stays bound to the node you started it on, so switching the active node mid-scan does not redirect it. Results feed the same severity badges, history, and charts as a single-image scan, and image results reuse the 24-hour digest cache. A partial failure across images or stacks surfaces as a warning toast rather than reading as a clean run.
### Reading severity badges
The badge color reflects the highest severity found:
| Badge | Meaning |
|-------|---------|
| **CRITICAL** (red) | At least one vulnerability with a CVSS score of 9.0 or higher |
| **HIGH** (orange) | At least one high-severity vulnerability |
| **MEDIUM** (blue) | Only medium and lower vulnerabilities |
| **LOW** (muted) | Only low-severity vulnerabilities |
| **Clean** (green) | Scan completed with zero findings of any kind |
| **Findings** (muted) | No CVEs, but the scan found secrets or Compose misconfigurations |
Hovering a badge shows when the scan ran and, for a vulnerability badge, a per-severity breakdown (`3C 5H 2M 1L`); for a Findings badge it shows the secret and misconfig counts instead.
Scan results are cached by image digest (see [Tags vs digests](#tags-vs-digests)). If the same digest is scanned again within 24 hours, Sencho returns the cached result instantly instead of re-running Trivy. To force a fresh scan, open the drawer and click **Re-scan**.
### Evidence tags
Severity is one signal among several, not the only one. Every vulnerability finding in the scan drawer and comparison sheet carries independently-verifiable evidence tags so you can tell scary from exploitable at a glance:
| Tag | Meaning |
|-----|---------|
| **KEV** | The CVE is on the CISA Known Exploited Vulnerabilities catalog: it has a confirmed history of active exploitation. |
| **EPSS N%** | The FIRST EPSS score: the modeled probability this CVE will be exploited in the next 30 days. Tinted amber at 10% or higher. |
| **CVSS N** | The CVSS base score, always shown for context but never used as the sole basis for a block condition. |
| **Won't fix** | The vendor has marked this finding `will_not_fix` or `end_of_life`; no upstream patch is coming. |
A scan's header also shows a **Published service** tag when the image belongs to a stack with a port bound to a non-loopback address or attached to host networking, so you can weigh exposure alongside severity. This reflects the Compose configuration, not live container state, and it is an informational signal only, not a policy gate input.
These tags depend on **exploit intelligence** being enabled: a daily background fetch of the CISA KEV catalog and FIRST EPSS scores. Toggle it on the **Security** page → **Scanner setup** tab; it degrades gracefully when offline (existing tags keep showing, new CVEs simply lack them) and can be turned off entirely for air-gapped or firewalled hosts.
## The scan results drawer
The drawer opens as a right-side sheet with the breadcrumb `Security Scans <image-ref>`. The header surfaces the primary scan actions, the summary header reports severity counts and fixable findings, and the body groups findings across three tabs: **Vulnerabilities**, **Secrets**, and **Misconfigs**.
<Frame>
<img src="/images/vulnerability-scanning/scan-details-sheet.png" alt="Vulnerability scan drawer for fyb3roptik/threadfin:latest showing the Re-scan, Compare, CSV, and SARIF header actions, the severity summary (1 CRITICAL, 23 HIGH, 554 MEDIUM, 134 LOW), an SBOM download button, the severity filter pills, and a paged CVE table with EPSS and CVSS evidence tags under each package" />
</Frame>
### Header actions
- **Re-scan**: kick off a fresh scan, ignoring the digest cache.
- **Compare**: pick a baseline scan from the dropdown to diff against this one.
- **CSV**: export the full vulnerability list for offline review.
- **SARIF**: download the full scan (vulnerabilities, secrets, and misconfigs) as SARIF 2.1.0 for upload to GitHub code scanning or any SARIF-aware tool.
The summary header below the actions reports the per-severity counts, the total, how many findings have a fix available, when the scan ran, and what triggered it. An **SBOM** button below the summary downloads a Software Bill of Materials in SPDX JSON or CycloneDX format.
### Vulnerabilities tab
Severity filter pills narrow the table, and the paginated list shows every CVE found with these columns:
- **CVE ID**: links to [cve.org](https://www.cve.org) for `CVE-…` identifiers and to the [GitHub Advisory Database](https://github.com/advisories) for `GHSA-…` identifiers.
- **Package**: the affected package name and installed version, with the [evidence tags](#evidence-tags) (KEV, EPSS, CVSS, Won't fix) underneath when applicable.
- **Severity**: badge matching the row tint.
- **Fixed**: the version that contains the fix, with a green checkmark when an upgrade is available.
Critical and high rows carry a left accent rail so the entries that need attention catch the eye even before you read the severity column. Every finding loads (the table paginates, but nothing is capped), so severity filtering, inspection, and suppression reach the full result set even on scans with thousands of CVEs. If the scan was evaluated against a [scan policy](#scan-policies) and any of its block conditions match, a destructive **Policy violation** banner appears at the top of the drawer naming the policy and the reasons it fired (severity threshold, known-exploited, and/or fixable Critical/High).
### Secrets tab
Hardcoded credentials or keys detected in the image filesystem, with severity, rule id, title, and the file path including the line number range when available. Secret values are redacted: only the first eight characters of the match are stored, so exporting or comparing a scan cannot leak the underlying credential.
### Misconfigs tab
Misconfiguration findings with severity, check id, title, target file, and a suggested fix. Image scans typically have an empty Misconfigs tab; stack-config scans (see [Compose misconfiguration scanning](#compose-misconfiguration-scanning)) populate it.
## Post-deploy automated scanning
When Trivy is available, Sencho scans every deployed image in the background after a successful deploy. This applies to all deploy paths:
- Stack deploy and redeploy
- Stack update
- Template deploy from the App Store
- Git source apply
- Git source create
The deploy itself is never blocked by scanning. Scans run asynchronously and surface their results via the severity badges in the Resources Hub. If high or critical findings appear, an alert is dispatched through your configured [notification channels](/features/alerts-notifications).
### Opting out per deployment
The App Store deploy sheet's **Advanced** tab has a **Security** section with a **Scan images for vulnerabilities after deploy** checkbox, enabled by default. Uncheck it to skip the post-deploy scan for that single deployment. This does not disable scan policies; it simply opts that deployment out of post-deploy scanning.
<Frame>
<img src="/images/vulnerability-scanning/app-store-toggle.png" alt="App Store deploy sheet Advanced tab scrolled to the Security section, showing the checked 'Scan images for vulnerabilities after deploy' checkbox" />
</Frame>
## Pre-deploy scan advisory
The pre-deploy scan advisory is an opt-in review step for manual deploys. When it is on, deploying a stack from the editor (**Deploy** or **Save & deploy**) first opens a dialog listing each image in the stack with its latest scan severity, so you can review the security posture before the deploy runs.
The advisory is visibility only: it never blocks a deploy. Anyone who can deploy reviews the breakdown and chooses **Deploy** to continue or **Cancel** to stop. Blocking a deploy on a severity threshold is a separate capability covered in [Deploy Enforcement](/features/deploy-enforcement).
Enable it on the **Security** page → **Scanner setup** tab with the **Pre-deploy scan advisory** toggle (admin only). It is off by default.
How it behaves:
- The counts come from each image's most recent cached scan. An image that has not been scanned yet shows **not scanned**, and the dialog notes how long ago each scan ran.
- It reads cached results only and never starts a new scan, so it does not slow the deploy. If the scan data cannot be read, the deploy proceeds normally.
- It covers manual editor deploys (**Deploy** and **Save & deploy**). Automated paths (stack update, template deploy, Git source apply, scheduled runs) are already covered by [post-deploy automated scanning](#post-deploy-automated-scanning) and, where configured, deploy enforcement.
## Scheduled fleet scans
You can run recurring scans of every image on a node through the standard [Scheduled Operations](/features/scheduled-operations) system. Create a scheduled task with action **Scan** and a cron expression. The scheduler iterates every image on the target node with a short delay between scans and records the result in the task's run history.
Use scheduled scans to keep badge counts fresh even for images that are rarely redeployed. A nightly cron like `0 3 * * *` is a sensible default for most fleets.
### Completion notifications
When a scheduled scan finishes, Sencho dispatches an alert through your configured [notification channels](/features/alerts-notifications). The alert names the task and summarises scanned, cached, and failed image counts:
- **Info** when every image scanned successfully.
- **Warning** when at least one image failed to scan.
Failures are usually transient (registry timeouts, missing credentials) and never stop the rest of the run from completing. The task's run history holds the detailed output.
## Scan policies
Policies define **block conditions** that govern whether a stack can deploy: a policy flags an image when any of its enabled conditions match. A policy with **Block on deploy** enabled runs a pre-flight scan on every image in the stack before `docker compose up` executes; if any image matches, the deploy is rejected with a dialog listing the offending images and the reasons they matched. Policies with **Block on deploy** disabled still evaluate every post-deploy and scheduled scan and dispatch warning alerts when a condition matches.
See [Deploy Enforcement](/features/deploy-enforcement) for the full pre-flight flow, admin bypass path, and audit-log behavior.
<Frame>
<img src="/images/vulnerability-scanning/add-policy.png" alt="New policy dialog with a Name field, an optional glob-style Stack pattern field, Block conditions toggles for Severity threshold (off), Known-exploited KEV (on), and Fixable Critical/High (on), a Block on deploy toggle, and an Enabled toggle" />
</Frame>
### Creating a policy
Go to the **Security** page → **Policies** tab and click **Add policy**.
| Field | Description |
|-------|-------------|
| **Name** | A descriptive label (e.g. `prod-high-gate`). |
| **Stack pattern** | Optional glob against stack names (e.g. `prod-*`). Leave empty to match every stack. |
| **Block conditions** | At least one must be enabled to block on deploy: **Severity threshold** flags an image whose highest finding meets or exceeds a chosen severity; **Known-exploited (KEV)** flags an image carrying a CVE on the CISA known-exploited list; **Fixable Critical/High** flags an image with a Critical or High finding that has a fix available. CVSS is always captured for context but is never, on its own, a block condition. |
| **Block on deploy** | When enabled, deploys are rejected before `docker compose up` runs if any image matches an active condition. When disabled, the policy still evaluates post-deploy and scheduled scans and dispatches warning alerts on matches. |
| **Enabled** | Disabled policies are skipped during evaluation. |
New policies default to a **risk-first** configuration: Known-exploited and Fixable Critical/High are on, and the raw Severity threshold is off. A blocking policy with every condition off is rejected, since it would silently block nothing; enable at least one condition before turning on **Block on deploy**.
### Policy scoping
When multiple policies match a deploy, Sencho picks the most specific one:
1. Policies scoped to a specific node win over global policies.
2. Policies with a stack pattern win over wildcard policies.
3. Disabled policies are never applied.
Only one policy is evaluated per deploy. Use a single tight pattern rather than overlapping policies for clarity.
<Note>
Policies created on a control instance replicate to every remote in the fleet automatically. See [Fleet Sync](/features/fleet-sync) for the replication, push retry, and replica demote behavior.
</Note>
### Example policies
**Block known-exploited and fixable findings in production.** The default risk-first gate: it blocks the CVEs most likely to matter (actively exploited, or patchable right now) without hard-blocking on every Critical CVSS score, many of which have no working exploit.
- **Name:** `prod-risk-gate`
- **Stack pattern:** `prod-*`
- **Block conditions:** Known-exploited (KEV) on, Fixable Critical/High on, Severity threshold off
- **Block on deploy:** On
- **Enabled:** On
**Alert on criticals in staging, never block.** Lets engineers iterate without friction while still surfacing critical findings for triage.
- **Name:** `staging-critical-alert`
- **Stack pattern:** `staging-*`
- **Block conditions:** Severity threshold on at `CRITICAL`
- **Block on deploy:** Off
- **Enabled:** On
### Honoring suppressions in deploy blocks
By default a block-on-deploy policy evaluates the **raw** (non-suppressed) scan result, so a CVE you have accepted in [CVE Suppressions](/features/cve-suppressions) still counts toward the block. That keeps the gate strict: suppressions silence alerts and dim findings in reports, but on their own they do not open the deploy path.
To have an accepted CVE stop counting toward the gate, turn on **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab. With it on, the gate re-derives each image's block conditions from the findings that remain after suppressions are applied, so an image whose only blocking findings are all suppressed deploys without a manual bypass. When a deploy proceeds for this reason, Sencho records it in the audit log so the suppression-driven pass stays traceable.
The toggle governs the Sencho instance that runs the deploy and is off by default. Enable it on each instance whose deploys should honor suppressions.
<Note>
A finding whose exploitability can't be confirmed fails **closed** on the known-exploited and fixable conditions: if the scan detail can't be read in full (a truncated cache hit or a database error), the gate treats KEV and fixable conditions as still active rather than silently skipping them. The severity condition still gates from the scan's stored aggregate either way.
</Note>
### Creating a policy via the API
Policy CRUD endpoints are documented in the [Security API reference](/api-reference/security). A typical create call from CI looks like this:
```bash
curl -X POST https://your-sencho-instance:1852/api/security/policies \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-risk-gate",
"stack_pattern": "prod-*",
"max_severity": "HIGH",
"block_on_severity": 0,
"block_on_kev": 1,
"block_on_fixable": 1,
"block_on_deploy": 1,
"enabled": 1
}'
```
`max_severity` is only enforced when `block_on_severity` is `1`; it still must be one of `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`.
## Secret detection
Full scans ask Trivy to walk the image filesystem for patterns that look like hardcoded credentials, API tokens, cloud access keys, or private keys. Detection rules cover the common providers (AWS, GCP, GitHub, Slack, Stripe) plus generic high-entropy strings.
Trigger a full scan from the shield-icon menu on any image row with **Full scan (vulnerabilities + secrets)**. Findings appear on the **Secrets** tab of the drawer:
<Frame>
<img src="/images/vulnerability-scanning/secrets-tab.png" alt="Secrets tab of a scan drawer listing five findings: three CRITICAL Google GCP Service-account matches and two MEDIUM JWT token matches, each with a redacted excerpt and a target file path" />
</Frame>
| Column | Description |
|--------|-------------|
| **Severity** | Trivy-assigned severity for the rule that matched. |
| **Rule** | The detection rule identifier (e.g. `aws-access-key-id`). |
| **Title** | A short description of what was detected. The second line shows a redacted excerpt of the match. |
| **Target** | The file path inside the image filesystem, including the line number range when available. |
Only the first eight characters of any matched secret are persisted. The full value is never written to the database, so exporting a scan or comparing two scans cannot leak the underlying credential.
Full scans take longer than vulnerability-only scans because Trivy reads every file in the image. If runtime is a concern, schedule full scans overnight and keep deploy-time scans on the default vulnerability-only setting.
## Compose misconfiguration scanning
Beyond package CVEs, Sencho can run `trivy config` against a stack's Compose file to flag insecure defaults before you deploy. Typical checks cover privileged containers, missing resource limits, host networking, mounted Docker sockets, and overly broad capabilities.
<Note>
This is a **security** audit of the Compose file, using Trivy's hardening rules. It is distinct from [Compose Doctor](/features/compose-doctor), which runs **deploy-readiness** checks (port conflicts, missing bind paths, unset variables, no healthcheck) before a deploy. Use Compose Doctor to confirm a stack will deploy cleanly; use Compose risk scanning to audit its security posture.
</Note>
From any stack page, open the **More actions** overflow menu next to the Update button and select **Scan config**. Sencho runs the scanner against the stack's working directory and opens the scan drawer on the **Misconfigs** tab.
<Frame>
<img src="/images/vulnerability-scanning/scan-config-button.png" alt="Plex stack page with the More actions overflow menu open, showing Rollback, Scan config with a shield icon, Mute, and Delete" />
</Frame>
The Misconfigs tab columns:
| Column | Description |
|--------|-------------|
| **Severity** | Rule severity (CRITICAL / HIGH / MEDIUM / LOW). |
| **Check** | Rule id or AVD identifier for the violated check. |
| **Title** | Short summary, linked to the upstream advisory when available. The second line shows Trivy's detailed message. |
| **Target** | The file that triggered the finding. |
| **Fix** | The recommended resolution. |
Config scans are stored in the same history as image scans with an `image_ref` of `stack:<name>`, so they appear on the Scan history sheet and can be exported as CSV.
## Misconfig acknowledgements
Some misconfigurations are intentional. A reverse proxy legitimately needs root to bind privileged ports; a network monitor might require host networking; an `--privileged` mount of the Docker socket might be exactly what your janitor service expects. Sencho lets admins acknowledge a rule so it stops triggering alerts on that stack without lowering the policy bar for every other stack.
Acknowledgements are applied at read time and never modify stored findings. Deleting an acknowledgement immediately resurfaces the underlying finding wherever it appears.
### Acknowledging from a scan result
1. Open a stack-config scan that contains the finding.
2. Click the shield-check icon at the right edge of the misconfig row. The dialog opens with the rule id prefilled and the stack pattern set to the current stack name, so a single click acknowledges *only this stack*. That is the narrowest possible scope by default.
3. Add a reason explaining why the misconfiguration is accepted. The reason is stored locally and replicates fleet-wide; it never appears in audit-log summaries to avoid leaking incident-tracker IDs or vendor secrets.
4. Optionally set an expiry in days. After expiry the acknowledgement stops applying and the finding resurfaces.
Acknowledged rows render dimmed with a strikethrough title; hovering surfaces the recorded reason.
### Managing acknowledgements
The **Security** page → **Suppressions** tab has a Misconfig Acknowledgements panel listing every acknowledgement on this control: rule id, optional stack pattern (glob), creator, expiry date, and a delete button. The same `replicated` badge that appears on CVE suppressions appears here for rows pushed from the control to a replica.
Replicas show the panel read-only; write operations return 403 with a "managed by control" message so configuration drift cannot accumulate on the leaf nodes.
### Scope and matching
An acknowledgement matches a misconfig finding when:
- `rule_id` equals the finding's `rule_id` (exact match), and
- `stack_pattern` is null **or** matches the scan's stack name via the same glob syntax used elsewhere (e.g. `traefik`, `web-*`), and
- the acknowledgement has not expired.
When more than one acknowledgement could match, Sencho picks the most specific: a stack-pinned ack beats a fleet-wide ack for the same rule.
### SARIF emission
Acknowledged misconfigs are emitted in the SARIF export with a `suppressions` entry of kind `external` and status `accepted`, mirroring CVE suppressions. Code-scanning dashboards that respect SARIF suppressions dismiss them with the recorded justification.
## SBOM generation
A Software Bill of Materials (SBOM) is a machine-readable inventory of every package in a container image. SBOMs satisfy security frameworks (SLSA, Executive Order 14028, EU Cyber Resilience Act) and support offline supply-chain analysis.
From the scan results drawer, click **SBOM** below the summary and choose a format:
| Format | Use case |
|--------|----------|
| **SPDX JSON** | Widely supported, best for tooling integration. |
| **CycloneDX** | Richer dependency metadata, better for supply-chain analysis. |
The download starts immediately and uses the image's digest (when available) in the filename.
## SARIF export
SARIF (Static Analysis Results Interchange Format) is the standard format supported by GitHub code scanning, Microsoft Defender for Cloud, and most security dashboards. Sencho generates SARIF 2.1.0 documents from the stored scan results, so the download matches what you see in the drawer (same findings, same suppression state) without re-running Trivy.
From the scan drawer header, click **SARIF** to download the report. The file is named after the image reference with a `.sarif.json` extension.
What the export contains:
- **Vulnerabilities**: one SARIF result per CVE, with `security-severity` scored 9.8 (CRITICAL), 7.5 (HIGH), 5.0 (MEDIUM), 2.5 (LOW), or 0.0 (UNKNOWN). The affected package appears as a logical location (`<pkg>@<version>`).
- **Secrets**: rule ids are namespaced as `SECRET:<rule>`. Results point at the file and line number where the match was found.
- **Misconfigs**: rule ids are namespaced as `MISCONFIG:<rule>`. Results point at the Compose file that triggered the check.
- **Suppressions**: CVEs you have suppressed in Sencho are emitted with a SARIF `suppressions` entry of kind `external` and status `accepted`, so code-scanning dashboards dismiss them with the justification you recorded.
- **Acknowledged misconfigs**: misconfigs you have acknowledged in Sencho are emitted with the same `suppressions` shape so dashboards apply the same dismissal logic.
Each finding type is capped at 5000 rows to bound memory and serialisation time on pathological scans. When any type trips the cap, the SARIF run carries `properties.truncated = true`, `properties.row_limit`, and a `properties.totals` object with the original counts so downstream tooling can flag the export as partial.
Typical upload flow for GitHub code scanning:
```yaml
- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sencho-scan.sarif.json
```
## Tags vs digests
A **tag** is a mutable name for an image (`nginx:1.25`, `app:latest`). Compose files and day-to-day updates usually pin by tag because the name is easy to read and bump.
A **digest** is the immutable content hash of that image (`sha256:…`, often written as `image@sha256:…`). The registry can move a tag to a new digest without changing the tag name, so two pulls of `app:latest` can be different builds.
Sencho keys security work on digest when Docker provides one:
- The 24-hour scan cache
- History primary labels and per-identity retention
- Apples-to-apples scan comparison
When no digest is stored (for example a compose config scan), History falls back to the image reference.
Tags still drive operations: compose pins, Resources row labels, semver auto-update, and the admin **Registry tags** browser (remote tag names only; see [Private registries](/features/private-registries#browse-tags-from-resources)).
When the tag name is unchanged but the remote content changed, auto-update surfaces **Rebuild available** / **Digest rebuild**, and a new scan gets its own History row under the new digest. Comparing across digests is allowed but flagged. If compose declares a tag while the running container is digest-pinned, [Stack drift](/features/stack-drift#image-comparison) treats them as different references on purpose.
## Scan history
Every scan Sencho runs is stored with its full vulnerability detail. History retention keeps a configurable number of scans per image digest (or per image reference when no digest is stored; default 50). Configure the cap under **Settings → Operations → Data Retention**. The history powers two things: digest caching (skip re-scanning a digest already scanned within 24 hours) and trend insight (compare a new scan to its predecessor to see what changed).
Open the **Security** page and select the **History** tab to browse completed scans. The table prefers digest identity when available (short digest with the image reference as a subtitle), lets you search by image reference or digest, and lets you tick two scans to compare. The **Scan history** button at the top of the Resources Hub is a shortcut to the same place.
<Frame>
<img src="/images/vulnerability-scanning/scan-history-sheet.png" alt="Security History tab listing completed scans with digest as the primary label, image reference as the subtitle, Trigger, Severity, Findings, and Fixable columns, a search box, and a Compare (0/2) button" />
</Frame>
## Comparing scans
Compare any two completed scans for an image to see what changed.
**From the History tab**: tick two scans (one baseline, one newer) and click **Compare**. Selecting a third scan replaces the oldest selection.
**From an open scan**: click **Compare** in the drawer header, then pick a baseline scan from the dropdown. Only completed scans for the same image appear.
The comparison sheet shows:
- A **delta ribbon** summarizing the net change per severity (CRITICAL, HIGH, MEDIUM, LOW). Net-positive deltas on CRITICAL render in destructive red so a regression on the worst tier is immediately visible.
- Filter pills to switch between **Added** (new findings since the baseline), **Removed** (resolved findings), and **Unchanged** (findings present in both).
- A sorted table of CVEs with severity, affected package, and direct links to the upstream advisory. CVE-prefixed identifiers resolve to [cve.org](https://www.cve.org); GHSA identifiers resolve to the [GitHub Advisory Database](https://github.com/advisories). Critical and high rows carry the same left-rail tint as the scan drawer for visual continuity.
Comparisons are scoped to a single node; scans taken on different nodes cannot be compared against each other.
Cross-image comparisons (picking scans from two different digests, or different references when digests are absent) are allowed but flagged with a warning, since package-level changes may reflect image differences rather than CVE drift. In that mode, the **Unchanged** pill is relabeled to **Shared** to reflect that same CVE + package matches across different images are not necessarily the same finding.
Up to 1000 findings per scan are loaded for comparison. When a scan exceeds this limit, the sheet shows a truncation banner indicating the comparison may be incomplete.
<Frame>
<img src="/images/vulnerability-scanning/scan-compare-sheet.png" alt="Compare scans sheet diffing two plex scans, with baseline and current rows, a cross-identity warning banner, a delta ribbon (HIGH +5, MEDIUM +32, LOW -16), Added/Removed/Shared filter pills, and a diff table of added CVEs" />
</Frame>
## How it works
1. On startup, Sencho looks for the `trivy` binary on `PATH` (and the path configured by `TRIVY_BIN` if set) and caches its availability.
2. When a scan is triggered, Sencho resolves the image digest via Docker and checks the 24-hour cache. If a completed scan exists for that digest, it is returned instantly.
3. Otherwise, Sencho spawns `trivy image --format json --quiet <image-ref>` and parses the JSON output into the vulnerability database.
4. Private registry credentials are forwarded automatically by writing a temporary `DOCKER_CONFIG` for the Trivy subprocess, then deleting it when the scan completes.
5. Scan status, counts per severity, and the full vulnerability list are persisted for display in the drawer and for policy evaluation.
## Troubleshooting
<AccordionGroup>
<Accordion title="Scan button is not visible">
Sencho hides scanning UI when the Trivy binary is not detected. Check the **Security** page → **Scanner setup** tab for the scanner status, then follow [Installing Trivy](/operations/trivy-setup) if it is missing.
</Accordion>
<Accordion title="Scans time out">
The default scan timeout is 5 minutes. Very large images (2 GB or more) over a slow connection may exceed this; pre-pulling the image to the host speeds up the scan significantly because Trivy then works against the local image store.
</Accordion>
<Accordion title="Private registry images fail to scan">
Sencho forwards the same registry credentials configured under **Settings → Registries** to Trivy. If a pull works in Sencho but a scan fails, make sure the image has been pulled at least once so Trivy can work against the cached local image.
</Accordion>
<Accordion title="Badge is out of date after an image update">
Post-deploy scanning only runs on deploy actions. For long-running images that aren't redeployed, schedule a recurring scan or click the shield icon in the Resources Hub to re-scan on demand.
</Accordion>
<Accordion title="A scan shows in progress for a long time">
Scans have a 5-minute internal timeout. The scheduler sweeps every tick and marks any scan stuck `in_progress` for more than 15 minutes as failed, so the UI always recovers on its own. Wait for the sweep, then click the shield icon again to trigger a fresh scan.
</Accordion>
<Accordion title="Trivy is not detected after installing it">
Sencho re-checks for the Trivy binary every ten minutes from the scheduler. Install Trivy on the host, wait for the next window, and the scanning UI lights up automatically.
</Accordion>
<Accordion title="Collecting diagnostic logs for support">
Enable **Developer Mode** under **Settings → Operations → Developer Diagnostics** and trigger the failing scan again. The backend logs verbose `[Trivy:diag]` entries covering detection timing, cache hits, Trivy invocation, and parse statistics. Attach these lines when filing a support issue. Turn Developer Mode off once you have captured the output.
</Accordion>
<Accordion title="Post-deploy scan failure notifications">
When a post-deploy scan fails for a specific image (for example because Trivy could not resolve a private registry pull), Sencho dispatches a warning-level alert through your configured notification channels. The deploy itself is never blocked by a scan failure.
</Accordion>
<Accordion title="A deploy was blocked by a policy I did not expect">
The block dialog names the policy that fired and lists every image that matched, along with which conditions matched (severity threshold, known-exploited, and/or fixable Critical/High). Open the **Security** page → **Policies** tab and review the matching policy: check the stack pattern glob and which block conditions are enabled. Remember that new policies default to blocking on known-exploited and fixable findings even when the severity threshold is off, so a CVSS-low finding can still trigger a block if it is on the KEV list. If the policy should not apply, tighten the pattern (for example `staging-*` instead of `*`), disable the condition that is too aggressive, or turn **Block on deploy** off to keep the evaluation in alert-only mode. Admins can bypass a single deploy with the **Deploy anyway** button; every bypass is recorded in the [Audit Log](/features/audit-log) with the actor, policy, and violation list.
</Accordion>
<Accordion title="Known-exploited and EPSS tags are missing from findings">
These evidence tags depend on **exploit intelligence**, a daily background fetch of the CISA KEV catalog and FIRST EPSS scores. Confirm the toggle is on under the **Security** page → **Scanner setup** tab. If it is on but tags are still missing, the host may be offline or firewalled: the feature degrades gracefully rather than failing the scan, so existing tags keep showing but new CVEs will not get fresh KEV/EPSS data until connectivity is restored. Air-gapped hosts can leave the toggle off intentionally; severity, CVSS, and fix-availability data are unaffected either way.
</Accordion>
<Accordion title="Trivy is not installed and a deploy with a block policy went through">
Sencho fails open when Trivy is not installed on the target node, so operators are never locked out by tooling state. A warning alert is dispatched through your configured notification channels with the message `Pre-deploy scan for "<stack>" skipped: Trivy not installed on this node`. Install Trivy from the **Security** page → **Scanner setup** tab to enforce the policy; see [Installing Trivy](/operations/trivy-setup) for options.
</Accordion>
<Accordion title="Compare button is disabled in Scan history">
The Compare primary action enables only after exactly two scans are ticked. Selecting zero, one, or three scans leaves it disabled. If you have only one scan for an image, trigger a second scan from the Resources Hub (or wait for a scheduled scan), then return to History and tick both.
</Accordion>
<Accordion title="Comparison shows unexpected results">
Two common causes:
- **Cross-identity comparison.** When the Baseline and Current rows point at different digests (or different image references when digests are absent), a warning banner appears and the "Unchanged" pill is labeled "Shared". Items in that bucket match on CVE + package name but may not be the same finding across two distinct images. Stick to scans of the same digest for apples-to-apples drift analysis.
- **Truncated scans.** When either scan has more than 1000 stored findings, the sheet shows a truncation banner. Items past the 1000-row cap do not contribute to the Added / Removed / Unchanged buckets and totals may be misleading. Re-run the scan with a tighter image or scope the investigation to the most severe findings to avoid truncation.
</Accordion>
<Accordion title="An older scan is missing from Scan history">
History uses server-driven pagination and keeps a configurable number of scans per image digest (or per image reference when no digest is stored). If you know the scan exists but cannot see it, search by image reference or digest, or page forward with the arrows. Raise **Scan history per digest** under **Settings → Operations → Data Retention** if you need a longer per-identity window.
</Accordion>
<Accordion title="Scan policies are missing on one of my nodes">
Scan policies are managed from the control Sencho instance and replicate to every remote. On a replica, the **Security** page → **Policies** tab shows a banner explaining that rules are managed upstream. See [Fleet Sync](/features/fleet-sync) for how replication works and how to investigate push failures.
</Accordion>
<Accordion title="I suppressed a CVE but the scan badge count is unchanged">
Badge counts always reflect raw findings so alerting stays accurate. Policy evaluation also uses raw findings by default; if you want suppressed CVEs to stop counting toward block-on-deploy policies, enable **Honor suppressions in deploy blocks** on the **Security** page → **Policies** tab. Open the scan drawer to confirm the row is dimmed with a shield-off icon. See [CVE Suppressions](/features/cve-suppressions) for how the filter is applied across the drawer, compare sheet, and other read surfaces.
</Accordion>
<Accordion title="The Secrets tab is empty on an image I expect to contain credentials">
Secret detection matches against Trivy's built-in rule set, which focuses on well-known provider patterns. Plain text passwords, custom token formats, or values that do not match any published rule will not appear. Make sure you picked **Full scan (vulnerabilities + secrets)** from the shield-icon menu; a plain vulnerability scan does not walk the filesystem.
</Accordion>
<Accordion title="Compose misconfiguration scan returns 404">
The scanner needs a Compose file in the stack directory. If the stack was created outside Sencho and the working directory does not contain a file named `compose.yml`, `compose.yaml`, `docker-compose.yml`, or `docker-compose.yaml`, the scan returns 404. Name the file accordingly or keep the stack under Sencho's managed compose directory.
</Accordion>
<Accordion title="SARIF download returns 409 Scan not complete">
SARIF export requires a completed scan. If a scan failed, timed out, or is still running, the button downloads nothing and the server returns a 409. Trigger a fresh scan from the Resources Hub or the stack page, wait for the drawer to populate, then export again.
</Accordion>
<Accordion title="SARIF download is missing findings I see in the drawer">
Each finding type (vulnerabilities, secrets, misconfigs) is capped at 5000 rows in the SARIF export. When any type trips the cap, the SARIF run includes `properties.truncated: true` along with the original counts, and the server logs a warning. The drawer paginates beyond 5000 so it shows everything, but the export is bounded for memory safety. If you need every row, narrow the scope (per-stack scan, per-image scan) before exporting.
</Accordion>
<Accordion title="Acknowledge button is missing on a misconfig finding">
The button is admin-only and hidden on replica nodes. Replicas read acknowledgements from the control via fleet sync; mutations happen on the control. If you are an admin on the control and still do not see the button, the row is likely already acknowledged. Look for the dimmed strikethrough rendering and hover for the recorded reason.
</Accordion>
<Accordion title="Findings reappeared after deleting a CVE suppression or misconfig acknowledgement">
Suppressions and acknowledgements are applied at read time and never modify stored finding rows. Removing one immediately resurfaces the underlying finding everywhere it appears (drawer, compare sheet, badge counts, SARIF export). The behavior is intentional: deleting an acknowledgement is meant to revert the operator decision, not to mask history.
</Accordion>
<Accordion title="Outbound traffic to ghcr.io and aquasecurity from the scanner host">
Trivy fetches its CVE and secret-rule database from public registries on first scan and refreshes periodically; that egress is required for vulnerability scanning to work. Sencho does not emit telemetry of its own. If your environment forbids egress, see Trivy's [air-gapped scanning guide](https://trivy.dev/latest/docs/advanced/air-gap/) for how to pre-seed the database and run scans with `--offline-scan`.
</Accordion>
<Accordion title="Compose stack scan returns 409 Already scanning this stack">
Sencho deduplicates concurrent scans of the same stack so two simultaneous calls cannot double-process the result. Wait for the in-flight scan to finish (its row appears in Scan history with status `in_progress` and flips to `completed` or `failed` shortly after) and trigger again.
</Accordion>
</AccordionGroup>