# Trivy Cheat Sheet — Image and Filesystem Scanning

> **Tool:** Trivy
> **Category:** Security & Compliance
> **Verified against:** Trivy 0.68.2, flags verified via `trivy --help` / `trivy image --help` / `trivy fs --help`
> run locally, 2026-08-29
> **Official docs:** https://trivy.dev/latest/

## What it is and where it fits

Trivy (by Aqua Security) is the most widely used open-source container scanner — genuinely broad in scope:
container images, filesystems, git repositories, IaC/config misconfigurations, and SBOMs, all from one static
binary with no daemon required. It overlaps deliberately with several other tools in this series: it does what
Grype does (image/fs vulnerability scanning), what tfsec/Checkov do (IaC misconfiguration scanning, and as of
2024 it literally absorbed tfsec's check library), and what Syft does (SBOM generation) — which is exactly why
many teams standardize on Trivy alone rather than running four separate scanners. This page covers
vulnerability scanning of images/filesystems/repos; the companion page covers config/IaC scanning and SBOM
handling.

## How Trivy's image scan actually works

```mermaid
flowchart TD
    A[trivy image myapp:latest] --> B{Image source}
    B -->|local daemon| C[docker/containerd/podman]
    B -->|--input file.tar| D[Local tarball, no daemon needed]
    B -->|registry: prefix| E[Pull directly from registry]
    C & D & E --> F[Extract package inventory: OS packages + language deps]
    F --> G[Match against local vulnerability DB]
    G --> H[Report: table / json / sarif / cyclonedx...]

    classDef source fill:#2563eb,color:#fff,stroke:#1e40af
    classDef result fill:#16a34a,color:#fff,stroke:#15803d
    class C,D,E source
    class H result
```

The vulnerability DB is downloaded and cached locally on first run (`~/.cache/trivy`) and refreshed
automatically on subsequent scans unless `--skip-db-update` is set — this is why the very first `trivy image`
run on a fresh machine is noticeably slower than every run after it.

## Installation

```bash
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# add -- <version> (e.g. v0.68.2) to pin instead of installing latest
brew install trivy                            # macOS/Linux Homebrew
apt-get install trivy                         # if you've added Aqua's official apt repo (see docs)

trivy --version
trivy image --download-db-only                # pre-warm the vulnerability DB (large first download)
```

Trivy shipped two supply-chain incidents in 2026 (a compromised release, and a compromised GitHub Action) —
always pin an install script to a specific `--version`/tag in CI rather than trusting "latest" unattended, and
watch Aqua's security advisories.

## Scanning a container image

```bash
trivy image python:3.4-alpine
trivy image --severity CRITICAL,HIGH myapp:latest
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:latest    # non-zero exit for CI gating (0 by default!)
trivy image --ignore-unfixed myapp:latest                          # hide vulns with no available fix yet
trivy image --input myapp.tar                                       # scan a `docker save` tarball, no daemon needed
trivy image --format json --output result.json myapp:latest
trivy image --format cyclonedx --output result.cdx myapp:latest    # SBOM-shaped output, straight from an image scan
```

`--exit-code` defaults to 0 — Trivy prints findings but won't fail a pipeline unless you set it explicitly.
This is the single most common "why didn't CI catch this" gotcha with Trivy, and it's shared by Grype too.

Sample `trivy image` table output (real shape — the exact CVE list depends entirely on the image scanned and
the DB's state on the day you scan; this is illustrative, not a literal fixed capture):

```
python:3.4-alpine (alpine 3.9.4)
==================================
Total: 42 (UNKNOWN: 0, LOW: 3, MEDIUM: 18, HIGH: 15, CRITICAL: 6)

┌─────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────┐
│   Library   │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │           Title           │
├─────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────┤
│ musl        │ CVE-2020-28928 │ MEDIUM   │ 1.1.20-r4          │ 1.1.20-r5     │ musl libc through 1.2.1  │
│ openssl     │ CVE-2021-3711  │ CRITICAL │ 1.1.1b-r1          │ 1.1.1k-r0     │ OpenSSL: SM2 decryption  │
└─────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────┘
```

## Scanning a filesystem or repository

```bash
trivy fs .                                    # local project — language-specific lockfiles + installed OS packages
trivy fs ./Pipfile.lock                       # a single manifest file
trivy repository https://github.com/org/repo   # clone + scan a remote git repo directly
trivy repository /path/to/local/repo
```

`trivy fs` is the tool for "what's vulnerable in my dependencies before I've even built an image" — the same
job `snyk test` or `npm audit` do, but offline and covering many ecosystems from one binary (npm, pip, Go
modules, Maven, Cargo, and more, auto-detected from lockfiles present).

## Filtering and scan scope

```bash
trivy image --scanners vuln myapp:latest                    # vuln only (default is vuln+secret)
trivy image --scanners vuln,secret,misconfig,license myapp:latest
trivy image --pkg-types os myapp:latest                     # OS packages only, skip language deps
trivy image --vuln-severity-source ghsa myapp:latest         # pick which advisory DB drives severity when sources disagree
trivy image --platform linux/arm64 myapp:latest              # scan a specific platform from a multi-arch image
```

## Real-world scenario: baseline a legacy image without drowning in noise

A container that's been in production for years typically has dozens of pre-existing HIGH/CRITICAL findings
with no available fix. Gate new builds on newly introduced, actually-fixable issues instead of the entire
backlog:

```bash
trivy image --exit-code 1 --severity CRITICAL,HIGH --ignore-unfixed myapp:latest
```

`--ignore-unfixed` alone doesn't solve everything — it filters out vulnerabilities with genuinely no available
patch yet, but a large image will still show real, fixable HIGH/CRITICAL findings on day one. Pair this with a
`.trivyignore` (see the companion page) for specific accepted-risk exceptions the team has actually reviewed.

## Real-world scenario: GitHub Actions CI recipe

```yaml
# .github/workflows/trivy.yml
name: Trivy Image Scan
on: [pull_request]
jobs:
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: myapp:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
          ignore-unfixed: true
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif
```

`if: always()` on the upload step matters — without it, a failed scan (exit code 1, which is the whole point
of gating) skips the SARIF upload too, so the findings never actually reach GitHub's code-scanning UI for
review.

## Shell completion

```bash
trivy completion bash | sudo tee /etc/bash_completion.d/trivy
# or, per-shell session: source <(trivy completion bash)
```

## Common pitfalls

- **Forgetting `--exit-code 1`** — like Grype, Trivy's default exit code ignores findings entirely.
- **Treating the first scan's slowness as a hang** — the first run on a machine downloads the full
  vulnerability DB; it's not stuck, just cold.
- **Not pinning the install script version** — see the supply-chain-incident note under Installation.
- **Scanning `latest` and assuming the results are stable** — a mutable tag can point to a different image
  tomorrow; pin to a digest (`myapp@sha256:...`) in any report you intend to compare over time.

## When to reach for something else

If the team is already standardized on Grype+Syft (Anchore's pairing) for image scanning and SBOM generation
specifically, there's little reason to add Trivy purely for image scanning — the two do materially the same
job for that one use case. Trivy's advantage is breadth (one tool instead of three) once IaC and SBOM scanning
are in scope too — see the companion page.
