---
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>
