تخطَّ إلى المحتوى
العودة إلى أدلة الدارسين
TrivyDevOpsSecurity & secrets3 مستويات115 قسمًايغطّي Trivy 0.74دليل بالإنجليزية

The Complete Trivy Guide

Scan images, code, IaC and Kubernetes for vulnerabilities and misconfigurations with Trivy. Taught at three levels — Beginner, Mid-level and Senior — each with an in-depth guide, interview prep, and practical tips.

التوثيق الرسمي مسودّة بالذكاء الاصطناعي · مراجعة المجتمع جاريةساعدنا في مراجعته
23sections
58examples

This is part one of three. It covers everything you need to do real work with Trivy, not a teaser. By the end you can scan a container image, a project folder, a git repository, your infrastructure files and a Kubernetes cluster; you can read the report without panic; you can make a pipeline fail when something serious turns up; you can produce a software bill of materials; and you can suppress a finding the honest way, with a reason and an expiry date. Mid-level and Senior take the same topics further; nothing here is thrown away.

Each section ends with a Try it task. Do them as you go. They take a few minutes each, and scanners only start to make sense once you have watched one find something in an image you built yourself.

This guide is written against Trivy 0.74.0, released on 14 August 2026. Trivy is still a 0.x tool, which means minor releases can change flags and output, so every command here was checked against that version, and the habit of pinning an exact version (explained in the section on Trivy's own supply chain) matters more than it does for most tools.

What Trivy is, and the problem it solves

Every application you ship is mostly code you did not write. A typical container image contains an operating system's worth of packages (a C library, a TLS library, a shell, a package manager), plus a language runtime, plus dozens or hundreds of third-party libraries pulled in by your dependency file, and only then your own few thousand lines. Any of those pieces can contain a security flaw that somebody has already found, reported and given a public identifier. Those identifiers are called CVEs (Common Vulnerabilities and Exposures), and they look like CVE-2024-12345: the year it was published and a serial number.

The problem is not that flaws exist. The problem is that nobody can hold the list of everything they depend on, and the list of every known flaw, in their head at once. New CVEs are published every day. An image that was clean on Monday can have a critical finding on Friday without a single byte of it changing, because the world learned something new about a library inside it.

Trivy is a scanner that does that matching for you. You point it at something (an image, a folder, a repository, a cluster), it works out what software is inside, compares that inventory with a database of known vulnerabilities, and prints what matches. It is made by Aqua Security, is open source under the Apache 2.0 licence, is written in Go and ships as a single binary with no server to run. That last detail is why it became popular: you can try it in thirty seconds and you can also put it in a pipeline in five minutes.

TARGETimage, folder, repo, cluster
→
INVENTORYwhat packages are inside
→
DATABASEknown vulnerabilities
→
REPORTfindings, by severity

The diagram is the whole idea, and the rest of this guide is detail on each arrow. Notice that Trivy does not run your software. It reads files. That is why a scan takes seconds rather than minutes, and it is also the source of its limits, which we return to at the end of the guide.

Trivy does more than look for CVEs. It has four kinds of scanner, and a beginner should know all four exist even if you only use one at first:

  • Vulnerability scanning finds known flaws in operating-system packages and in language libraries (npm, pip, Maven, Go modules and many more).
  • Misconfiguration scanning reads infrastructure-as-code (Dockerfiles, Kubernetes manifests, Terraform, CloudFormation, Helm charts) and flags risky settings, such as a container that runs as root or a storage bucket open to the public.
  • Secret scanning looks for credentials that were committed by accident: cloud access keys, tokens, private keys.
  • License scanning reports the licences of the packages you depend on, which matters to a company's lawyers more than to its security team, but is the same inventory put to a different use.

What people use it for in practice:

🐳

Gate an image build

A pipeline scans the image it just built and fails if a critical flaw with a fix available is inside.

📁

Check a project

Scan a repository's lock files before you merge, to catch a vulnerable dependency at the pull request.

🏗️

Review infrastructure

Find the public bucket or the root container before it reaches a real cluster.

📋

Produce an inventory

Generate a software bill of materials that auditors, customers and other tools can read.

You need little to follow along: a terminal, Docker (or any container runtime) for the image examples, and a small project of your own. If your team is in the Gulf or Egypt and your employer has data-residency rules, one fact is worth holding on to early: a default scan sends nothing about your code or images anywhere. Trivy downloads a public vulnerability database and compares locally. Later sections cover the few network calls it does make, so you can answer a security reviewer precisely.

Try it
  1. Pick a project you know well and list, from memory, the ten libraries you think it depends on.
  2. Open its dependency file (package.json, requirements.txt, pom.xml, go.mod) and count the real number, including the ones you did not choose.
  3. Compare the two numbers.
the real list is several times longer than the remembered one, and that is before counting the operating system underneath. The gap between those numbers is the gap a scanner exists to close.

What came before, and why scanners became routine

For a long time, vulnerability management was a human process. A security team subscribed to mailing lists, somebody kept a spreadsheet of "what version of what is running where", and on a good day a patch cycle happened quarterly. That worked when a server was a long-lived machine that an administrator logged into, and the list of installed software was a thing you could ask the machine for.

Containers and package managers broke that model in two ways. First, the number of things to track exploded: a single service could have a thousand transitive dependencies, and a company could have hundreds of services. Second, artifacts became immutable and short-lived, so "log in and check" stopped being possible. The answer the industry settled on was to inspect the artifact itself, automatically, at build time and again later.

The first wave of tools did one thing each. Some scanned operating-system packages in images, some scanned language dependencies, some linted Terraform, some hunted for passwords in git history, and a team that wanted all four ran four tools with four output formats and four ways of failing a build. Trivy's design choice was to put all of those behind one command line, one report structure and one set of output formats. You can disagree about whether the all-in-one approach is the best at each individual task (more specialised tools exist, and a later section names them), but for someone starting out the single interface is a real advantage: learning trivy image, trivy fs and trivy config teaches you one tool, not three.

A second idea became standard at the same time, and you will meet it throughout this guide: the software bill of materials, or SBOM. It is simply the ingredient list of an artifact, in a standard file format. Once you have the ingredient list, matching it against the vulnerability database is a separate, repeatable step that you can run again tomorrow without touching the original image. Trivy can both produce these lists and scan them.

Scanning is not fixing A scanner tells you what is known and where. It does not patch anything, and it cannot tell you whether the vulnerable code path is actually reachable in your application. Treat a report as a prioritised to-do list that needs judgement, not as a verdict.
Try it
  1. Think of the last time a widely reported vulnerability made the news.
  2. Ask yourself how you would find out, in under an hour, whether any of your services contained the affected library.
  3. Write down the steps you would take.
a manual, slow, error-prone list. Keep it: by the end of this guide you will be able to replace it with one command.

The mental model: targets, scanners, data and results

Trivy has a small vocabulary. Learn these nouns and every command in the documentation reads naturally.

A target is what you point Trivy at, and it is chosen by the subcommand you type. The ones a beginner needs:

Subcommand Target Typical question it answers
trivy image A container image, from your local runtime, a registry, or a tar file "Is this image safe to ship?"
trivy filesystem (or fs) A local folder, such as a project "Do my dependencies or files have problems?"
trivy repository (or repo) A local path or a remote git URL, which Trivy clones first "What is in that open-source project?"
trivy config A folder of infrastructure files, for misconfiguration only "Is my Terraform or Kubernetes YAML risky?"
trivy kubernetes (or k8s) A live cluster, using your kubeconfig "What is wrong with what is running?"
trivy sbom An existing SBOM file "Given this ingredient list, what is vulnerable?"
trivy rootfs An unpacked root filesystem, such as a host or a mounted image "What is on this machine's disk?"

A scanner is the kind of finding you are asking for, chosen with the --scanners flag: vuln, misconfig, secret and license. Different subcommands have different defaults. For an image or a folder the default is vuln,secret: vulnerabilities and secrets on, misconfiguration and licences off until you ask. trivy config runs only the misconfiguration scanner.

Data is what Trivy compares against. The main piece is the Trivy DB, an aggregate of vulnerability information from the National Vulnerability Database, from the security advisories each Linux distribution publishes, from the GitHub Advisory Database and other sources. It is rebuilt roughly every six hours and distributed as an OCI artifact, the same packaging format container images use, so it is fetched from a registry. A second database, the Java DB, helps identify Java archives that lack metadata, and is only downloaded when Trivy finds JAR files. A third piece, the checks bundle, holds the rules for misconfiguration scanning, written in a policy language called Rego.

The cache is a folder on your disk where all of this lives. On Linux it is ~/.cache/trivy, on macOS ~/Library/Caches/trivy, and you can move it with --cache-dir. The first scan downloads the database into it (a few hundred megabytes), and later scans reuse it, refreshing when the database is stale.

A result is the output. Internally, a scan produces a report, which contains one result for each target inside the artifact and each class of finding. An image with Debian packages and a Python application has a result for the OS packages and another for the Python libraries. Each result lists vulnerabilities with a severity: UNKNOWN, LOW, MEDIUM, HIGH or CRITICAL. Finally a reporter renders the report as a table, JSON, SARIF, an SBOM or a template, depending on --format.

Two more words appear in the output and deserve an early definition. A vulnerability's status says where it stands: fixed means a patched version exists, affected means it is vulnerable and no fix has been published, will_not_fix means the maintainer has decided not to patch, fix_deferred and end_of_life mean what they say. The installed version is what your artifact contains, and the fixed version is the first release that resolves the flaw. Those two columns are the ones you will read most.

Every flag has three spellings Any command-line flag can also be set as an environment variable named TRIVY_ plus the flag in upper snake case (TRIVY_SEVERITY=HIGH,CRITICAL), or as a key in a config file. The command line wins over the environment, which wins over the file. This is what lets one pipeline setting apply to a hundred scan commands.
Try it
  1. For each of these, name the target subcommand: a Dockerfile you wrote, a requirements.txt in a folder, a colleague's GitHub project, a cluster you have access to.
  2. For each, say which scanner (vulnerabilities, misconfiguration, secrets, licences) you would ask for first.
image or config for the Dockerfile depending on whether you want the built result or the recipe, fs for the folder, repo for the project, k8s for the cluster. Being able to make this choice quickly is most of the skill.

Installing Trivy and checking your setup

Trivy is one static binary, so installation is mostly a question of which delivery mechanism suits your machine. The project officially supports container images, release binaries, an install script and several package managers. Pick one.

On macOS, Homebrew is the easiest:

BASH
brew install trivy

On Debian and Ubuntu, use the project's APT repository. Pay attention to the last line of the repository definition, because it changed recently and old tutorials are wrong:

BASH
sudo apt-get install wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install trivy
Use the generic APT distribution since v0.72.0 Older instructions put your release codename (jammy, noble, or $(lsb_release -sc)) in that line. Since version 0.72.0, new releases are published only to the generic distribution, so a machine still using a codename keeps working but silently stays frozen at v0.71.2 and never sees a newer Trivy or its fixes. If apt upgrade never moves Trivy, check this line first.

On Fedora, RHEL and CentOS, create /etc/yum.repos.d/trivy.repo with the repository definition, then install:

/etc/yum.repos.d/trivy.repo
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
BASH
sudo yum -y update && sudo yum -y install trivy

On Windows, the officially supported route is manual: download the windows-64bit.zip from the GitHub releases page, unzip it, and put trivy.exe on your PATH. If you use Docker Desktop, the most reliable setup is WSL2 with the Linux instructions above, so Trivy and the Docker socket live in the same Linux environment.

For a CI machine or a quick server install, there is an install script. Always pass a version, never run it unpinned:

BASH
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin v0.74.0

Finally, you can skip installation and run Trivy as a container. You give it the Docker socket so it can see your local images, and a cache volume so it does not re-download the database every run:

BASH
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/Library/Caches:/root/.cache/ \
  aquasec/trivy:0.74.0 image python:3.4-alpine

Two details. Since version 0.72.0 the project no longer publishes architecture-specific tags such as 0.71.0-amd64; use the plain multi-architecture tag shown above, or a digest. And mounting the Docker socket gives the container very broad power over your host, which is acceptable on your own laptop and something to avoid in shared CI (the Senior guide covers safer arrangements).

Now verify the installation:

BASH
trivy --version
TEXT
Version: 0.74.0

Once the database has been downloaded, the same command also prints the database and checks-bundle metadata. To warm the cache without scanning anything, which is useful before a demo or on a slow connection:

BASH
trivy image --download-db-only

This fetches the vulnerability database into your cache directory. If it fails, the error message will mention a registry such as ghcr.io or mirror.gcr.io: Trivy fetches its database from there, so a corporate firewall or proxy that blocks those hosts is the usual culprit. The errors section below has the fixes.

Do not install version 0.69.4 In March 2026 a malicious build of Trivy was published as v0.69.4 and was available for about three hours. If you installed or ran exactly that version, treat it as compromised. Safe versions are v0.69.3 and earlier, and v0.70 and later. The full story and what it teaches is in its own section near the end.
Try it
  1. Install Trivy using the method that matches your machine.
  2. Run trivy --version and confirm it prints a version at or above 0.74.0 (or a version you have deliberately chosen).
  3. Run trivy image --download-db-only, then run trivy --version again.
the second version output now includes database information, which shows the vulnerability database is in your cache and you are ready to scan.

Your first scan: a container image

The most common use of Trivy is scanning an image, so start there. Choose an image that is old enough to have findings, which makes the output worth reading:

BASH
trivy image python:3.4-alpine

The first time you run this, Trivy prints progress lines about downloading the vulnerability database, and on later runs it skips that. Then it inspects the image: it unpacks the layers, reads the operating system's release file to learn that this is Alpine Linux and which version, reads the package manager's database to list installed packages, and looks for language dependency files. A couple of seconds later you get a report.

Where does Trivy find the image? By default it looks in this order: your local Docker daemon, then containerd, then Podman, then the remote registry. If you have the image locally, it reads it from the daemon without pulling. If not, it pulls the layers it needs from the registry (it does not need a running container and it does not start one). You can force a source with --image-src:

BASH
trivy image --image-src remote python:3.4-alpine

That matters in CI environments where there is no Docker daemon at all: remote goes straight to the registry. A third way is to scan an image that was saved to a tar file, which is also how you scan in places with no registry access:

BASH
docker save -o myapp.tar myapp:1.0
trivy image --input myapp.tar

Some notes about naming. An image reference with no registry prefix, like python:3.4-alpine, means Docker Hub. A locally built image such as myapp:1.0 is found in the daemon. A fully qualified name like ghcr.io/example/app:2.3 goes to that registry and will need credentials if it is private; Trivy reuses the credentials in your Docker configuration file, so if docker pull works for you, trivy image generally does too.

Try a real example by building something tiny and scanning it, so you have a thing you own:

Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
BASH
docker build -t myapp:1.0 .
trivy image myapp:1.0

If you followed the Docker guide you will recognise each step (/student-guides/docker covers writing a Dockerfile in depth). The point here is that Trivy sees two different layers of software inside your image: the Debian packages that came with the base image, and the Python packages your requirements.txt installed. Your own code, app.py, is not scanned for vulnerabilities, because Trivy matches packages and not logic, although it will look at it for secrets.

Try it
  1. Run trivy image python:3.4-alpine and let it finish.
  2. Scroll to the top of the output and find the first section heading, which names the target.
  3. Run trivy image --scanners vuln python:3.4-alpine and compare the two runs for speed and content.
the second run is a little quicker because it skips secret scanning, and the vulnerability results are identical. You have just met your first scanner switch.

Reading the report

The default output is a table, and reading it well is the skill that separates "I ran a scanner" from "I can act on it". The table below is typical in shape. The exact packages, identifiers and counts will differ from what you see, because the database changes every few hours.

TEXT
python:3.4-alpine (alpine 3.9.2)
================================
Total: 12 (HIGH: 10, CRITICAL: 2)

┌────────────┬────────────────┬──────────┬──────────┬───────────────────┬───────────────┬───────────────────────────────┐
│  Library   │ Vulnerability  │ Severity │  Status  │ Installed Version │ Fixed Version │             Title             │
├────────────┼────────────────┼──────────┼──────────┼───────────────────┼───────────────┼───────────────────────────────┤
│ libcrypto1 │ CVE-2019-1543  │ HIGH     │ fixed    │ 1.1.1a-r1         │ 1.1.1b-r1     │ openssl: ChaCha20-Poly1305    │
│            │                │          │          │                   │               │ with long nonces              │
└────────────┴────────────────┴──────────┴──────────┴───────────────────┴───────────────┴───────────────────────────────┘

Read it from the top.

The header line names the target and what Trivy decided it is: python:3.4-alpine (alpine 3.9.2) tells you Trivy identified the operating system as Alpine 3.9.2. This matters more than it looks. Vulnerability data for operating-system packages is specific to each distribution and release, and if Trivy cannot identify the operating system (an image built FROM scratch, or a stripped-down base) it cannot match OS packages at all, and you will see no OS results. An empty report can mean "clean" or it can mean "not understood", so always check that the header shows what you expect.

The summary line, Total: 12 (HIGH: 10, CRITICAL: 2), is per section, not per image. An image with both OS packages and Python libraries has one section and one summary line for each. The summary counts only what is displayed after filters, so if you filtered by severity, the total reflects that.

In the table itself, each row is one finding:

  • Library is the package that contains the flaw. For OS packages this is the name the package manager knows (libcrypto1.1, openssl, zlib); for language dependencies it is the library (requests, lodash).
  • Vulnerability is the identifier. Most are CVEs, and some are advisory IDs from a language ecosystem. In modern terminals these are clickable links to a page with the details.
  • Severity is Trivy's assessment, from UNKNOWN through CRITICAL. It comes from the most appropriate source for that package (your distribution's own rating is preferred when it has one), so the same CVE can legitimately show different severities in a Debian image and an Alpine image.
  • Status is the most useful column for deciding what to do. fixed means a patched version exists and upgrading resolves it. affected means you are vulnerable and no fix has been published yet. will_not_fix means the vendor has decided not to patch, which can be reasonable when the affected feature is not compiled in.
  • Installed Version and Fixed Version tell you the gap. If the fixed version is blank, there is nothing to upgrade to.
  • Title is a one-line summary of the flaw.

Findings are not all equally actionable, and learning to sort them is most of the job. A rough triage order that works in practice:

  1. Start with CRITICAL and HIGH findings that have a fixed version. These are real and have a cheap remedy: update the package or the base image.
  2. Next, see whether the vulnerable package is in the image because of your choices or because of your base image. A dozen findings that all disappear when you move from python:3.9 to python:3.9-slim are one decision, not twelve chores.
  3. Findings with no fix are real but not something you can act on today. Note them, and see whether the vulnerable feature is one your software uses.
  4. Low and unknown severities are usually last.

A few more controls make the table easier to read. --table-mode lets you choose which parts of the table to show (for example only the summary), and --dependency-tree adds a view of how a vulnerable library got into your project, which is invaluable for dependencies you never chose directly: a flaw in a library three levels down is fixed by upgrading the thing at the top, and the tree tells you which thing that is.

BASH
trivy image --dependency-tree myapp:1.0

For a language-library result you may see a dependency-tree section showing that, for example, your direct dependency web-framework pulls in http-helper, which pulls in the vulnerable parser. That is the path you need to upgrade along.

A clean report is not a safe image No findings means "nothing known, in what Trivy could identify". It does not cover flaws that have not been published yet, libraries copied into your source tree rather than installed by a package manager, or a binary Trivy did not recognise. Check that each expected section appears, and that the header names the right operating system.
Try it
  1. Scan python:3.4-alpine and pick one finding that has a fixed version.
  2. Write down its library, installed version and fixed version.
  3. Now scan alpine:3.20 and compare the number of findings.
the old image has many findings with known fixes; the recent one has far fewer. The lesson is not that old is bad, but that the gap between installed and fixed version is how an image ages.

Filtering: severity, unfixed, and status

A raw report on an old image can have hundreds of rows, which is useless for a human and dangerous for a pipeline, because people learn to ignore alarms that always ring. Filtering is how you turn a report into a decision. There are three filters to know on day one.

Filter by severity with --severity, giving a comma-separated list:

BASH
trivy image --severity HIGH,CRITICAL alpine:3.15

Only the listed severities are shown, and the summary counts follow. HIGH,CRITICAL is the conventional threshold for a gate, because MEDIUM and below produce a large volume of findings that rarely justify blocking a release.

Filter out findings with no fix using --ignore-unfixed:

BASH
trivy image --ignore-unfixed alpine:3.15

This hides anything whose status means no patched version exists yet. The reasoning is practical: if you cannot do anything about a finding today, failing a build for it only teaches the team to bypass the check. Use it together with a severity filter for a gate that only reports what is both serious and fixable:

BASH
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.0

Filter by status gives finer control than the shortcut, through --ignore-status, listing the statuses to hide:

BASH
trivy image --ignore-status fixed,will_not_fix,end_of_life myapp:1.0

That is rarely what a beginner wants, but it shows the model: --ignore-unfixed is a convenience for a particular combination of statuses.

There are other filters that are worth knowing exist. --pkg-types os,library restricts the report to operating-system packages or to language libraries (both by default). --include-dev-deps includes development-only dependencies such as test frameworks, which are left out by default because they are not shipped. --skip-dirs and --skip-files exclude paths you know are noise, such as a vendored test fixture folder.

Keep one idea in mind about all filters: they change what you see and what a pipeline fails on, not what is true. A filtered report is a view. Use filters to express a policy your team agreed, write that policy down, and avoid filtering until the output looks clean, because a report made to look clean is the most dangerous kind.

A sensible first gate --severity HIGH,CRITICAL --ignore-unfixed is a good opening policy: it shows only what is serious and actionable. You can tighten it later, and you will know exactly what you relaxed it from.
Try it
  1. Run trivy image alpine:3.15 and note the total.
  2. Run it with --severity HIGH,CRITICAL, then with --ignore-unfixed added.
  3. Write the three totals next to each other.
the numbers shrink at each step. The smallest number is what a pipeline would act on; the largest is what exists.

Scanning a project: filesystem and repository

Images are what you ship, but you can catch problems earlier, in the folder where the code lives. trivy fs points at a directory and looks for the files package managers leave behind: package-lock.json, yarn.lock, requirements.txt and poetry.lock, go.mod, pom.xml, Gemfile.lock, Cargo.lock and many more. It reads those lock files as the inventory and matches them against the database.

BASH
cd my-project
trivy fs .

Because a lock file records the exact resolved versions, it is the best input for a scan. A requirements.txt with loose version ranges tells Trivy less than a lock file that pins each version. If your project has only loose ranges, Trivy can still report but the results reflect what the file says, not necessarily what will be installed. The habit this teaches is one worth adopting for its own sake: commit your lock files.

A filesystem scan runs the same default scanners (vuln,secret). To add misconfiguration checks for any infrastructure files sitting in the project:

BASH
trivy fs --scanners vuln,secret,misconfig .

To scan a project you do not have on disk, use trivy repo with a git URL. Trivy clones it to a temporary folder and scans the clone:

BASH
trivy repo https://github.com/user/repo

You can pick a branch, tag or commit with --branch, --tag or --commit. This is handy for vetting a dependency before you adopt it, or for auditing a repository without checking it out yourself. One caution: a repository scan looks at the working tree at that commit, not the full history. A password that was committed and later deleted is invisible to Trivy, and lives on in history. Tools that specialise in history (the Mid guide names some) are the right complement.

If a scan complains about disk space with a message mentioning a path like /tmp/fanal-..., the temporary folder is full (Trivy unpacks artifacts there). Point TMPDIR at a larger disk:

BASH
TMPDIR=/mnt/big-disk trivy repo https://github.com/user/repo

A few useful flags for real projects: --skip-dirs node_modules if you only want your lock file scanned rather than an installed dependency tree, and --skip-files to exclude particular files. Everything else you learned for images (severity, unfixed, formats, exit codes) applies identically to fs and repo, which is the payoff of the single interface.

Try it
  1. Run trivy fs . in a project of yours that has a lock file.
  2. Find a vulnerable library and use the dependency tree flag to see how it got in: trivy fs --dependency-tree .
  3. If none are found, try an older open-source project with trivy repo.
the report names the lock file as its target, lists libraries with installed and fixed versions, and the tree reveals whether you chose the library or inherited it.

Finding secrets before they leak

Of everything Trivy does, the secret scanner has the best ratio of effort to value. People commit credentials constantly: a cloud key pasted into a config file to "just test it", a token in a script, a private key in a forgotten folder. Once pushed, a credential should be treated as public, and automated bots harvest public repositories for them within minutes.

The secret scanner is on by default for image, filesystem and repository scans, so you may already have seen it fire. It works by matching a large set of built-in rules, each a pattern plus keywords that make a match plausible (an AWS access key has a recognisable shape and prefix, a GitHub token has its own), and it reports the file, the line, the rule and a redacted excerpt.

Try it on a file you create deliberately. The example key below is the well-known dummy value from the cloud provider's own documentation, not a live credential:

BASH
mkdir secret-demo && cd secret-demo
echo 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE' > settings.env
trivy fs --scanners secret .
TEXT
settings.env (secrets)
======================
Total: 1 (HIGH: 1)

HIGH: AWS (aws-access-key-id)
═══════════════════════════════
AWS Access Key ID
───────────────────────────────
 settings.env:1 (offset: 18 bytes)
───────────────────────────────
   1 [ AWS_ACCESS_KEY_ID=***************** ]
───────────────────────────────

Notice that Trivy redacts the matched value in its output, so the report itself does not become a second place a secret leaks. That said, treat reports as sensitive anyway, since file names and surrounding context can reveal a good deal.

The scanner also looks inside images, including their layers and build history, and it reads compiled Python .pyc files. A secret baked into one layer and deleted in a later one is still in the image, because layers are additive, and Trivy will find it. If you followed the Docker guide you know why.

If a secret is found, the response is the same whichever tool found it: rotate it first, then remove it. Deleting the line does not un-leak it. Rotate the credential at its source (revoke the key, issue a new one), then fix the code to read it from an environment variable or a secrets manager. The Vault guide at /student-guides/vault covers a place to keep them properly.

Sometimes a match is a false positive, for example an example key in documentation, or a test fixture. You can tune the scanner with a configuration file named trivy-secret.yaml in the working directory (or pointed to with --secret-config), where you can add your own rules, allow-list paths and patterns, and disable built-in rules you do not need. Leaving only the built-in rules you care about also makes scans faster on big repositories, because each rule is work Trivy has to do.

Turning the scanner off is a trade-off --scanners vuln skips secret scanning and speeds up large scans, which is a reasonable choice in an image scan where secrets were already checked at the source repository. Make it a deliberate decision rather than a default habit.
Try it
  1. Create the settings.env file above and scan it with --scanners secret.
  2. Add a second line containing the text hello world and scan again.
  3. Delete the file and the folder when you are done.
only the key line is flagged. Ordinary text is ignored, which shows the scanner is pattern-based, not a generic "looks suspicious" detector.

Checking infrastructure files: misconfiguration scanning

A vulnerability is a flaw someone else wrote into a library. A misconfiguration is a risky setting that you wrote yourself, and it is usually more dangerous because nobody else will patch it for you. Examples: a Dockerfile with no USER line (so the process runs as root), a Kubernetes pod that allows privilege escalation, a Terraform storage bucket that permits public reads, a security group open to the whole internet.

Trivy handles these with the misconfiguration scanner. It understands Dockerfiles, Kubernetes manifests, Terraform (including plan files), CloudFormation, Azure ARM templates and Helm charts. The simplest entry point is trivy config, which runs only this scanner against a folder:

BASH
trivy config ./infra

Here is a small Dockerfile with a flaw, and what Trivy says about it:

Dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
COPY . /app
CMD ["/app/run.sh"]
BASH
trivy config .
TEXT
Dockerfile (dockerfile)
=======================
Tests: 27 (SUCCESSES: 25, FAILURES: 2)
Failures: 2 (HIGH: 1, LOW: 1)

AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user as argument

Two things to learn from that output. First, the check ID, AVD-DS-0002: AVD stands for Aqua Vulnerability Database, DS is for Dockerfile, and the number is the rule. You will see older IDs in the style DS002 or KSV001 (Kubernetes) in some material; they refer to the same rules. Cloud checks look like AWS-0010. Every ID is searchable and has a documentation page explaining why the setting is risky and how to fix it.

Second, each result shows a pass/fail count: Tests: 27 (SUCCESSES: 25, FAILURES: 2). By default only failures are listed. Pass --include-non-failures if you want to see what passed as well, which is useful the first time you run it against a real project to see the coverage.

The checks themselves are Rego policies, in a bundle Trivy downloads and keeps in its cache (and embeds a copy of as a fallback). You do not need to write Rego as a beginner, but you should know it exists, because you can write your own checks for your company's rules later, and it explains why trivy config can check a Terraform plan or a Helm chart in the same way: it converts each format into a common structure and runs the same policies on it.

To run misconfiguration checks as part of a wider scan, add the scanner explicitly: trivy fs --scanners vuln,secret,misconfig . or trivy image --scanners vuln,misconfig myapp:1.0. For an image, misconfiguration checks look at the Dockerfile-equivalent information Trivy can reconstruct from the image configuration.

If a file is not being checked, the usual cause is that Trivy did not recognise it as infrastructure code. Use trivy config (which is explicit about what it is doing) or check that the file's extension and structure match what the scanner expects.

Scan before you apply Running trivy config on Terraform or Kubernetes YAML in a pull request is cheaper than finding the same problem in a running environment. See the Terraform guide (/student-guides/terraform) and Kubernetes guide (/student-guides/kubernetes) for the files this is checking.
Try it
  1. Create the small Dockerfile above in an empty folder and run trivy config ..
  2. Add a line USER nobody before CMD and scan again.
  3. Run it once more with --include-non-failures.
the high-severity finding about the user disappears after you add the line, and the last run shows the long list of checks that passed, which gives a feel for how many rules exist.

Licenses, in brief

The same inventory that finds vulnerabilities can report what licence each package is distributed under. This rarely concerns a student, but it concerns companies: using a library under a licence that forces you to publish your own code can be a legal problem, and procurement teams in regulated sectors ask for this list.

The licence scanner is off by default. Turn it on with --scanners license:

BASH
trivy fs --scanners license .
trivy image --scanners license --license-full myapp:1.0

By default, Trivy only reports licences from package metadata, and --license-full makes it also examine licence files in the tree, which is slower and more thorough. It classifies each licence into categories (forbidden, restricted, reciprocal, notice, permissive, unencumbered, unknown), so that you can see at a glance that one package is under a strong copyleft licence while most are permissive. The lists behind those categories are configurable, because what a company considers forbidden is a policy decision, not a fact of nature. Recent releases have also improved Java licence detection, reading it from embedded project metadata and mapping licence URLs to standard identifiers.

For now, remember that the scanner exists and how to switch it on; the Mid-level guide covers turning categories into policy.

Try it
  1. Run trivy fs --scanners license . on a project that has a lock file.
  2. Find the most restrictive category in the output.
a list grouped by category, where most packages are permissive and a few may need a second look. You now have the raw material for a licence review.

Output formats: table, JSON, SARIF and more

A table is for humans. Anything that consumes the result automatically (a dashboard, a code-scanning tab, a ticket-creating script) needs a structured format. You select it with --format, and write to a file with --output:

BASH
trivy image --format json --output result.json alpine:3.15

The formats a beginner should know:

  • table is the default, for reading in a terminal.
  • json is the complete report, with every field. It is the right choice for scripts and the source of truth you can convert from later.
  • sarif is a standard format for static-analysis results, and is what GitHub's code-scanning feature ingests, so findings appear in a repository's Security tab.
  • cyclonedx, spdx and spdx-json produce an SBOM rather than a vulnerability report. The next section covers these.
  • template renders through a Go template, for custom layouts such as an HTML report; Trivy ships templates for HTML, JUnit and other targets.
  • github produces a dependency snapshot for GitHub's dependency graph, and cosign-vuln produces a predicate for attaching a scan result to an image with the cosign signing tool.

A very useful trick is that you do not need to re-scan to get a different format. If you saved a JSON result, trivy convert renders it again:

BASH
trivy convert --format sarif --output result.sarif result.json

Use jq, a command-line JSON tool, to answer quick questions from a JSON report, for example to list every critical CVE:

BASH
trivy image --format json --output result.json myapp:1.0
jq -r '.Results[].Vulnerabilities[]? | select(.Severity=="CRITICAL") | .VulnerabilityID' result.json

Take a moment to look at the structure of result.json once. A top-level Results array has one entry per target and class, and each has a Vulnerabilities array whose entries carry fields like VulnerabilityID, PkgName, InstalledVersion, FixedVersion and Severity. The question marks in the jq expression handle results that have no vulnerabilities. Knowing this shape lets you build anything on top of Trivy.

Reports are sensitive data A report lists exactly which software you run and where it is weak. Do not paste full reports in public issues or chat, and be careful attaching them to public build artifacts.
Try it
  1. Save a JSON report with --format json --output result.json.
  2. Convert it with trivy convert --format sarif --output result.sarif result.json.
  3. Open both files and compare their first twenty lines.
two structured documents describing the same findings in different shapes, produced by one scan. That is why the JSON file is worth keeping.

Software bills of materials

An SBOM is an inventory: every package inside an artifact, with versions and identifiers, in a standard machine-readable file. The two common standards are CycloneDX and SPDX, and Trivy writes both.

BASH
trivy image --format cyclonedx --output sbom.cdx.json alpine:3.15
trivy image --format spdx-json --output sbom.spdx.json alpine:3.15
trivy fs --format cyclonedx --output sbom.json .

Why bother? Two reasons that matter to beginners. First, customers and regulators increasingly ask for one. Second, it separates two jobs that a plain scan does together: working out what is inside, and deciding what is vulnerable. The inventory of a released image never changes, but the vulnerability database does, every few hours. If you save the SBOM when you build, you can rescan it next week without pulling the image again:

BASH
trivy sbom ./sbom.spdx.json

That is how teams answer "are we affected by the new vulnerability?" quickly: they rescan a store of SBOMs rather than every image.

One detail surprises people. A CycloneDX file produced the ordinary way lists components only, with no vulnerabilities in it. If you want vulnerability data embedded alongside the inventory, say so:

BASH
trivy image --format cyclonedx --scanners vuln --output sbom-with-vulns.json alpine:3.15

Trivy will also notice SBOM files that already exist inside an image or virtual machine, with extensions such as .cdx.json and .spdx.json, and use them.

Try it
  1. Generate a CycloneDX SBOM for alpine:3.15 and open it.
  2. Count the components, then run trivy sbom sbom.cdx.json on it.
  3. Compare the findings with those from a direct trivy image alpine:3.15 scan.
the results agree closely, because the ingredient list is the same. The difference is that the second scan never touched the image.

Failing a build: exit codes

So far Trivy has only talked. To make it block something, you use its exit code. A program's exit code is the number it hands back to the shell when it finishes: zero means success, anything else means failure, and every CI system treats a non-zero step as a failed step.

Here is the detail that surprises nearly everyone: by default Trivy exits with zero even when it finds critical vulnerabilities. Finding problems is a successful scan, not a failed one. A pipeline that runs trivy image myapp:1.0 and nothing else will stay green forever, which gives a false sense of security that is worse than no scan at all.

To turn findings into a failure, pass --exit-code:

BASH
trivy image --exit-code 1 --severity CRITICAL myapp:1.0
echo $?

If any critical finding remains after filters, Trivy exits with 1 and echo $? prints 1. If none remain, it prints 0. The filters decide what counts: the severity list above means only critical findings trigger the failure, although everything you did not filter is still printed. A gate that matches the sensible policy from earlier looks like this:

BASH
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed myapp:1.0

A common pattern is two scans in one job: a first, informational one that prints everything and always succeeds, and a second, strict one that fails the build, so developers see the full picture but only serious, fixable findings block them.

BASH
trivy image --severity LOW,MEDIUM,HIGH,CRITICAL myapp:1.0
trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed myapp:1.0

There is a second, separate switch, --exit-on-eol, which fails the scan if the image's operating system has reached end of life. An unsupported base image stops receiving security fixes altogether, so this catches a risk that no individual CVE row shows:

BASH
trivy image --exit-on-eol 1 myapp:1.0
Do not confuse "found something" with "the tool broke" A non-zero exit can mean findings matched your policy, or that Trivy hit an error (a timeout, a failed database download). Read the log before assuming which. In pipelines, print the report before the gate so the reason is always visible.

In a GitHub Actions workflow, the same idea is expressed through an action's inputs. Pin the action to a full commit hash, not to a tag or branch (the next section explains why):

.github/workflows/scan.yml
name: 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 }} .
      - name: Scan image
        # Replace <full-commit-sha> with the verified commit of a safe release
        uses: aquasecurity/trivy-action@<full-commit-sha>
        with:
          image-ref: myapp:${{ github.sha }}
          severity: HIGH,CRITICAL
          ignore-unfixed: true
          exit-code: "1"

The action's exit-code input defaults to 0, which is the same trap in another costume. The GitHub Actions guide (/student-guides/github-actions) covers workflows in depth, and the GitLab CI guide (/student-guides/gitlab-ci) shows the same gate in a different system.

Try it
  1. Run trivy image --severity CRITICAL python:3.4-alpine; echo "exit: $?".
  2. Run it again with --exit-code 1 added.
  3. Run trivy image --exit-code 1 --severity CRITICAL alpine:3.20; echo "exit: $?" on a recent image.
the first prints findings and exit 0, the second exit 1, and the recent image most likely exit 0. The exit code, not the printed table, is what a pipeline acts on.

Ignoring findings honestly

Sooner or later a finding appears that you cannot or should not fix right now: the vulnerable function is not reachable in your code, the fix needs a major upgrade you have scheduled for next month, or it is a false positive. You need a way to say "I have seen this; do not fail the build for it" that is recorded, reviewable and temporary. Trivy provides it through an ignore file.

The simplest form is a plain text file named .trivyignore in the directory you run Trivy from. Each line is an identifier; exp: adds an expiry date:

.trivyignore
# Not reachable: we do not use the affected parser. Review with the Q1 upgrade.
CVE-2019-14697 exp:2026-12-31

# Dockerfile check: this build image needs root to install packages.
AVD-DS-0002

When the date passes, the ignore stops applying and the finding returns. Expiry is the single most valuable habit in suppression, because it converts "we decided once" into "we decide again on a schedule". An ignore without an expiry date becomes permanent by neglect.

For anything beyond a personal project, use the richer YAML format, which records a reason and can limit an ignore to particular paths or packages. You enable it with --ignorefile:

.trivyignore.yaml
vulnerabilities:
  - id: CVE-2023-29491
    statement: Not reachable, terminal library unused. Reviewed by platform team.
    expired_at: 2026-12-31
  - id: CVE-2023-3817
    purls: ["pkg:deb/debian/libssl1.1"]
    statement: Fix scheduled with base image update.

misconfigurations:
  - id: AVD-DS-0002
    paths: ["docs/Dockerfile"]
    statement: Documentation example, never built.

secrets:
  - id: aws-access-key-id
    paths: ["tests/fixtures/fake-credentials.txt"]
    statement: Dummy key used in unit tests.
BASH
trivy image --ignorefile ./.trivyignore.yaml myapp:1.0

Notice how much more a statement and a path tell the reviewer than a bare ID. Six months later, AVD-DS-0002 on its own is a mystery, and with a path and a sentence it is a decision.

Suppressed findings are hidden, not deleted. To see what you are currently ignoring, which is a good periodic audit:

BASH
trivy image --show-suppressed --ignorefile ./.trivyignore.yaml myapp:1.0

There is a more formal tool for the same problem, used when a vendor or maintainer wants to publish "this vulnerability does not affect product X, and here is why": VEX, short for Vulnerability Exploitability eXchange. A VEX statement says a CVE is not_affected, affected, fixed or under_investigation for a product, and Trivy can read such statements (from a file, a repository or an attached attestation) to suppress findings. You will meet it at the Mid level; for a beginner, .trivyignore.yaml with a statement and an expiry date is the right tool.

Treat the ignore file as code Commit it, and require a review for changes. A pull request that adds an ignore line is a security decision, and the diff is the audit trail.

An honest ignore

  • Names the exact ID, and the path or package where it applies
  • Has a sentence saying why
  • Has an expiry date
  • Was reviewed by someone else

A silent ignore

  • Bare ID with no explanation
  • No expiry, so it lasts forever
  • Added to "make the pipeline green"
  • Nobody remembers who added it
Try it
  1. Pick one finding from a scan of python:3.4-alpine and put its ID in a .trivyignore file with an expiry date far in the future.
  2. Re-run the scan and confirm it disappears and the total drops by one.
  3. Change the expiry to a date in the past and run it again.
with a future date the finding is hidden; with a past date it comes back. You have seen, first hand, that an expiry makes a suppression temporary by design.

The cache and the database

Trivy feels fast because it does its heavy lifting once and remembers it. Understanding where that memory lives solves most "why is it slow" and "why did it do that" questions.

Everything is stored in the cache directory (~/.cache/trivy on Linux, ~/Library/Caches/trivy on macOS). Inside you will find:

  • db/ holds the vulnerability database (trivy.db) and a metadata.json saying when it was built.
  • java-db/ holds the Java database, present only after a scan that found JAR files.
  • policy/ holds the downloaded misconfiguration checks bundle.
  • fanal/ holds the scan cache: results of analysing image layers, keyed by layer hash, so scanning an image whose layers you have seen before is nearly instant.

On each run, Trivy checks whether its local database is recent enough, and downloads a fresh one if not. The database is rebuilt about every six hours, so a scan on Monday morning and one on Monday evening can legitimately disagree. That is a feature, not a flaw: it is why an image that passed yesterday can fail today without any change to the image.

You control this behaviour with a few flags worth knowing:

BASH
trivy image --download-db-only              # fetch the DB and stop
trivy image --skip-db-update myapp:1.0      # use the cached DB, do not check for a newer one
trivy image --cache-dir /tmp/trivy-cache myapp:1.0
trivy clean --all                           # delete the caches and start fresh

--skip-db-update is what you use when you are offline or when a pipeline pre-seeds the cache, but it only works if a database is already present. The older spelling --skip-update was removed, so if a tutorial uses it, the tutorial is out of date.

Two warnings. First, do not run two Trivy processes against the same cache directory at the same time: the database file is locked, and the second process fails with a message that the database "may be in use by another process". Give each its own --cache-dir. Second, there is a flag called --offline-scan, and its name misleads: it only stops Trivy making some remote lookups (such as to Maven Central); it does not stop the database download. For a genuinely offline machine you need the database already in place and --skip-db-update. The senior guide covers running fully air-gapped.

If the cache ever seems corrupt, or after a major upgrade produces a confusing database-schema error, trivy clean --all is the reset button.

Try it
  1. Find your cache directory and list it: ls ~/.cache/trivy (or ls ~/Library/Caches/trivy on macOS).
  2. Open db/metadata.json and read the build time.
  3. Run a scan with --skip-db-update and compare its start-up with a normal one.
the metadata shows the database is hours old, and the skip-update scan starts without any download step. You now know where Trivy's knowledge lives and how old it is.

Configuration: flags, environment variables and a config file

Typing the same six flags on every command is error-prone, and it makes it too easy for a pipeline and a laptop to disagree. Trivy lets you set any option in three ways, and they stack.

The command line is for one-off use. Environment variables are named TRIVY_ plus the flag in upper snake case, which suits CI, where you set them once for the whole job:

BASH
export TRIVY_SEVERITY=HIGH,CRITICAL
export TRIVY_IGNORE_UNFIXED=true
export TRIVY_TIMEOUT=15m
trivy image myapp:1.0

A config file named trivy.yaml in the current directory is read automatically, and is the right place for team policy that you want in version control. Every flag has a matching key, nested by topic:

trivy.yaml
severity:
  - HIGH
  - CRITICAL
scan:
  scanners:
    - vuln
    - secret
    - misconfig
  skip-dirs:
    - node_modules
    - vendor
vulnerability:
  ignore-unfixed: true
timeout: 10m
exit-code: 1

With that file in place, a bare trivy fs . applies the whole policy. Precedence decides conflicts: a flag on the command line beats an environment variable, which beats the file, which beats the built-in default. To generate a file listing every option with its default, run trivy image --generate-default-config, then delete everything you do not need; a short file is easier to review. Recent versions also publish a JSON Schema for the file, so an editor can validate your keys as you type.

A few defaults worth memorising, because they explain behaviour that otherwise looks arbitrary: the scan timeout is five minutes, the default scanners are vuln,secret, and the config file is looked up as trivy.yaml and the ignore file as .trivyignore. The timeout matters for large images: a five-minute limit that is fine for a slim service can be too short for a fat machine-learning image, in which case you will see a context deadline exceeded error and raise it with --timeout.

There is also a choice between two detection modes. The default, precise, reports what Trivy is confident about and produces fewer false positives. comprehensive adds less certain matches and produces more findings. Stay with the default until you have a reason, and if a colleague's scan shows different results from yours, compare this setting first, along with the database date.

Keep one config per repository Commit a small trivy.yaml next to your code. Developers and CI then run the same policy without either having to remember flags, and policy changes arrive through code review.
Try it
  1. Create the trivy.yaml above in a project, removing the exit-code line.
  2. Run trivy fs . and confirm the severity filter applied without any flags.
  3. Run it again with --severity LOW on the command line.
the file applies by itself, and the command-line flag overrides it, which demonstrates the precedence order in practice.

Scanning a Kubernetes cluster

Kubernetes adds a fourth target: the running cluster. trivy kubernetes (or trivy k8s) connects using your kubeconfig, the same credentials kubectl uses, and scans what it finds. The Kubernetes guide (/student-guides/kubernetes) explains the objects involved, so read it first if Pods and Deployments are new.

BASH
trivy k8s --report summary

The command scans three layers: the cluster infrastructure (the API server, the kubelet and add-ons), the cluster configuration (roles and bindings, which decide who can do what) and the application workloads, including the images they run. By default it prints a summary, and --report all prints every finding.

A few flags you will reach for:

BASH
trivy k8s --report all --severity CRITICAL --scanners vuln,misconfig,secret
trivy k8s --include-namespaces prod,staging --exclude-kinds node
trivy k8s --skip-images
trivy k8s my-context --kubeconfig ~/.kube/config2

--include-namespaces narrows the scan, --skip-images avoids downloading and scanning every workload image (much faster, and you get configuration findings only), and naming a context chooses which cluster to scan if your kubeconfig holds several. Note that the old trivy k8s cluster and trivy k8s all subcommands no longer exist; the current form is trivy k8s [flags] [CONTEXT].

Your account needs permission to list many object kinds, and for node-level checks Trivy creates a short-lived job in a temporary namespace named trivy-temp. On a shared or production cluster, confirm with whoever owns it before you run a first scan, and start with --disable-node-collector if you want to avoid creating anything.

It can also measure the cluster against a named compliance benchmark:

BASH
trivy k8s --compliance=k8s-cis-1.23 --report all

Built-in specifications include k8s-nsa-1.0, k8s-cis-1.23, eks-cis-1.4 and the Pod Security Standards profiles. This is one way to answer "how do we measure up against the CIS benchmark" with a command instead of a spreadsheet. Continuous in-cluster scanning is done by a separate component, the Trivy Operator, which installs with Helm; the Mid-level guide covers it, and the Helm guide at /student-guides/helm is the place to learn the packaging.

Scanning is not free on a busy cluster A full scan pulls many images and makes many API calls. Do your first run against a development cluster, with --skip-images and a namespace filter, before pointing it at anything shared.
Try it
  1. Start a local cluster (kind or minikube) and deploy any sample application.
  2. Run trivy k8s --report summary --skip-images.
  3. Run it again with --report all --scanners misconfig and read two findings.
a summary by namespace and kind, then detail about settings such as containers running as root. Each one is a change you could make in a manifest.

Common errors and how to read them

Most Trivy errors fall into a handful of causes: the network, the cache, the image name, or a resource limit. The messages below are the real ones, with the cause and the fix.

analyze error: timeout: context deadline exceeded The scan exceeded the five-minute default, typically on a large image, many Java archives, or a slow network. Raise it: trivy image --timeout 15m myimage, or set TRIVY_TIMEOUT=15m.
unable to initialize a scanner: unable to initialize an image scanner Trivy could not get the image. Check the spelling of the name (no registry prefix means Docker Hub), whether Docker is running, whether a private registry needs you to log in, and whether a proxy is involved. If there is no Docker daemon, add --image-src remote to go straight to the registry.
x509: certificate signed by unknown authority A corporate proxy that inspects TLS traffic replaces certificates with its own, and Trivy does not trust that authority. Point it at the company certificate with SSL_CERT_FILE=/path/ca.pem. Disabling verification with TRIVY_INSECURE=true works but removes the protection TLS provides, so treat it as a last resort.
FATAL failed to download vulnerability DB Trivy fetches its database from mirror.gcr.io and ghcr.io. A firewall, a proxy, or registry rate limits block it. Allow those hosts or set a proxy, and on locked-down networks ask about mirroring the database internally.
DENIED: denied, when pulling from ghcr.io Stale or expired credentials for ghcr.io are being sent with the database request. Run docker logout ghcr.io, or unset GITHUB_TOKEN if that variable is set in your shell.

A few more, in brief:

Message or symptom Cause Fix
API rate limit exceeded Unauthenticated GitHub API calls Set GITHUB_TOKEN to a token with no special scopes
cache may be in use by another process Two Trivy processes share one cache Give each its own --cache-dir
no space left on device under /tmp/fanal-... The temporary folder is full Set TMPDIR to a bigger disk, or use --skip-dirs
No findings on a Kubernetes or Terraform file The misconfiguration scanner was not on Use trivy config, or add --scanners misconfig
apt never upgrades Trivy past 0.71.2 The repository line uses a codename Change it to generic
Pulling aquasec/trivy:0.71.0-amd64 fails Architecture tags stopped in v0.72 Use the plain tag or a digest
Homebrew: keychain credentials do not have sufficient scope A stale GitHub token in the macOS keychain Erase it with git credential-osxkeychain erase

For anything else, two flags give you more information. --debug prints what Trivy is doing at each step, including which source it chose for an image and which database it loaded. --trace-http prints the HTTP traffic, which is excellent for proxy problems but may print tokens and other sensitive data, so never paste its output into a public place. And trivy clean --all resets the cache when its state is the problem.

A final habit: read an error from the end of the output upward. The last line is usually the cause and the lines above are the path Trivy took to get there.

Try it
  1. Run trivy image this-image-does-not-exist:nope and read the last lines.
  2. Run it again with --debug and find which image sources Trivy tried.
  3. Run trivy image --timeout 1s python:3.4-alpine to provoke a timeout.
a scanner-initialisation failure that names the image, a debug log that shows it trying your local daemon and then the registry, and a deadline-exceeded error. Having seen the failures on purpose, they will be familiar when they happen for real.

Trivy's own supply chain: the March 2026 incident

A security scanner is itself software you download and run, often with access to your pipeline's secrets, and in March 2026 that stopped being a theoretical concern. Read this section carefully, because it shapes how you should install and use Trivy.

In March 2026 a threat actor used a compromised credential to publish malicious artifacts under Trivy's name. The official incident notice (GitHub discussion 10425, advisory GHSA-69fq-xp46-6x23) describes these pieces:

  • The trivy binary v0.69.4 was malicious. It was available for about three hours on 19 March 2026. Versions v0.69.3 and earlier, and v0.70 and later, are the safe ones.
  • Most tags of the aquasecurity/trivy-action GitHub Action before 0.35.0 were compromised for about twelve hours. Version 0.35.0 and later are safe.
  • All releases that then existed of aquasecurity/setup-trivy were compromised, for about four hours. Version v0.2.6 and later are safe.
  • Distribution channels affected included Docker Hub, GHCR, ECR Public, the deb and rpm repositories and the install-script host.

The official guidance is blunt: if an affected version ran in a pipeline, treat every secret that pipeline could see as compromised and rotate it immediately. It also lists a network address and a look-alike domain (spelled aquasecurtiy, with the typo) to block and to search logs for; take the exact indicators from the official notice rather than from this guide.

What matters for you is the lesson, which applies to every tool you use and not only this one. The attack worked because people referred to software by names that can move: a tag like latest, a branch like main, an action pinned as @v1. A name that can be re-pointed can be re-pointed by an attacker. The defences are simple habits, and you can adopt all of them on day one:

  1. Pin exact versions. Install v0.74.0, never "latest". Pass the version to the install script. Use aquasec/trivy:0.74.0, not aquasec/trivy:latest.
  2. Pin by digest where you can. A digest (aquasec/trivy@sha256:...) identifies exact content, and cannot be silently replaced.
  3. Pin GitHub Actions by full commit SHA. A commit hash is immutable; a tag is not. Let a tool such as Renovate propose updates as reviewable pull requests.
  4. Verify what you download against the published checksums and signatures.
  5. Limit what a scan job can see. A scanner rarely needs your deployment credentials; give the job only what it needs.
  6. Do not run an installer from a moving reference without a pinned version argument.
This is not a reason to avoid Trivy Being honest about an incident is a point in favour of a project, and the same incident befell other widely used tools in the same period. It is a reason to adopt the pinning habits above, which reduce the impact of the next one, whichever tool it affects.
Try it
  1. Find every place in your projects that installs or runs Trivy (Dockerfiles, workflows, scripts).
  2. For each, write down how the version is chosen: exact, range, tag, or "latest".
  3. Change one of them to an exact version, and one action reference to a full commit SHA.
a short table of places where your setup trusts a moving name. Fixing even one makes you safer than most setups.

Putting it all together

Here is one small end-to-end project that uses everything so far: a tiny Python service, scanned as files, as infrastructure and as an image, with a policy file, an ignore file, a saved report, an SBOM and a gate. Build it once and this page becomes reference material.

Create a folder with a few files:

requirements.txt
flask==2.0.1
requests==2.25.0
app.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return "hello"


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)
Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
trivy.yaml
severity:
  - HIGH
  - CRITICAL
scan:
  scanners:
    - vuln
    - secret
    - misconfig
vulnerability:
  ignore-unfixed: true

Now run the stages in order.

BASH
# 1. Scan the project's dependencies, secrets and config before building
trivy fs .

# 2. Check only the infrastructure files, including passes
trivy config --include-non-failures .

# 3. Build and scan the image
docker build -t demo:1.0 .
trivy image demo:1.0

# 4. Save the full report and an SBOM as build artifacts
trivy image --format json --output report.json demo:1.0
trivy image --format cyclonedx --output sbom.cdx.json demo:1.0

# 5. Apply the gate that a pipeline would use
trivy image --exit-code 1 demo:1.0
echo "gate exit code: $?"

Expect the project scan to flag the old pinned versions of flask and requests (they are deliberately old), and the config scan to flag the missing USER instruction. Fix them properly: raise the versions in requirements.txt to ones the report lists as fixed, add a non-root user to the Dockerfile, and rebuild:

Dockerfile
FROM python:3.9-slim
RUN useradd --create-home appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER appuser
EXPOSE 8080
CMD ["python", "app.py"]

Scan again and confirm that the gate's exit code is now zero. If something remains that you cannot yet fix, add it to a .trivyignore.yaml with a statement and an expiry date, rather than loosening the gate. Finally, write a short README section for whoever inherits the project: which scan runs where, what fails the build, and where the ignore file lives.

Try it
  1. Build the project exactly as shown and run every stage.
  2. Upgrade the pinned libraries and add the non-root user until stage five exits with zero.
  3. Turn the five stages into a CI workflow that pins its tool versions.
a project that scans clean at the level of your policy, with a report, an SBOM and an exit code a pipeline can trust. The README section is what makes it useful to somebody other than you.

What you can now do, and what comes next

You can explain what a vulnerability scanner does and does not know, choose the right Trivy target for an artifact, read a report including status and fixed versions, filter it into a policy, find committed secrets, catch risky infrastructure settings, produce and rescan an SBOM, make a pipeline fail on purpose, suppress a finding with a reason and an expiry, configure Trivy once for a whole repository, scan a cluster carefully, and install the tool defensively after the March 2026 incident. That is a working practitioner's toolkit.

Can you…
Say what Trivy scans, in one sentence? Images, folders, repos, IaC and clusters, for vulnerabilities, misconfigurations, secrets and licences
Explain why a clean report is not proof of safety? It covers known flaws in recognised packages only
Name the default scanners? vuln,secret
Explain why a build stays green despite findings? The default exit code is zero; use --exit-code 1
Hide unfixable findings? --ignore-unfixed
Suppress a finding properly? .trivyignore.yaml with a statement and an expiry
Rescan without the image? Save an SBOM and run trivy sbom
Explain why an image that passed yesterday fails today? The database is rebuilt about every six hours
Say what the March 2026 incident teaches? Pin versions, digests and commit SHAs

Mid-level takes every one of those topics further: how the scan works under the hood, the exact rules for caching and the database, CI patterns with SARIF and code scanning, VEX in practice, custom secret and misconfiguration rules, scanning private registries, Trivy Operator for continuous in-cluster scanning, and rescanning SBOMs on a schedule.

Senior covers running Trivy as a platform for a team: client and server mode with a shared cache, mirroring the database for air-gapped environments, securing the scanner's own supply chain, governance for ignores and exceptions, cost and scaling, upgrades across a 0.x release line, and where Trivy stops and another tool should take over.

Natural neighbours in this catalogue: the Docker guide (/student-guides/docker) for the images you are scanning, the Kubernetes guide (/student-guides/kubernetes) for the clusters, the Terraform guide (/student-guides/terraform) for the infrastructure you check, the Helm guide (/student-guides/helm) for packaging the operator, and the Argo CD guide (/student-guides/argo-cd) for delivering it.

Sources