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.
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.
- Pick a project you know well and list, from memory, the ten libraries you think it depends on.
- Open its dependency file (
package.json,requirements.txt,pom.xml,go.mod) and count the real number, including the ones you did not choose. - Compare the two numbers.
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.
- Think of the last time a widely reported vulnerability made the news.
- Ask yourself how you would find out, in under an hour, whether any of your services contained the affected library.
- Write down the steps you would take.
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.
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.
- For each of these, name the target subcommand: a Dockerfile you wrote, a
requirements.txtin a folder, a colleague's GitHub project, a cluster you have access to. - For each, say which scanner (vulnerabilities, misconfiguration, secrets, licences) you would ask for first.
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:
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:
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
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:
[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
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:
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:
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:
trivy --version
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:
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.
- Install Trivy using the method that matches your machine.
- Run
trivy --versionand confirm it prints a version at or above 0.74.0 (or a version you have deliberately chosen). - Run
trivy image --download-db-only, then runtrivy --versionagain.
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:
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:
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:
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:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
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.
- Run
trivy image python:3.4-alpineand let it finish. - Scroll to the top of the output and find the first section heading, which names the target.
- Run
trivy image --scanners vuln python:3.4-alpineand compare the two runs for speed and content.
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.
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
UNKNOWNthroughCRITICAL. 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.
fixedmeans a patched version exists and upgrading resolves it.affectedmeans you are vulnerable and no fix has been published yet.will_not_fixmeans 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:
- Start with
CRITICALandHIGHfindings that have a fixed version. These are real and have a cheap remedy: update the package or the base image. - 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.9topython:3.9-slimare one decision, not twelve chores. - 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.
- 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.
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.
- Scan
python:3.4-alpineand pick one finding that has a fixed version. - Write down its library, installed version and fixed version.
- Now scan
alpine:3.20and compare the number of findings.
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:
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:
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:
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:
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.
--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.
- Run
trivy image alpine:3.15and note the total. - Run it with
--severity HIGH,CRITICAL, then with--ignore-unfixedadded. - Write the three totals next to each other.
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.
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:
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:
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:
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.
- Run
trivy fs .in a project of yours that has a lock file. - Find a vulnerable library and use the dependency tree flag to see how it got in:
trivy fs --dependency-tree . - If none are found, try an older open-source project with
trivy repo.
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:
mkdir secret-demo && cd secret-demo
echo 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE' > settings.env
trivy fs --scanners secret .
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.
--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.
- Create the
settings.envfile above and scan it with--scanners secret. - Add a second line containing the text
hello worldand scan again. - Delete the file and the folder when you are done.
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:
trivy config ./infra
Here is a small Dockerfile with a flaw, and what Trivy says about it:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
COPY . /app
CMD ["/app/run.sh"]
trivy config .
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.
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.
- Create the small Dockerfile above in an empty folder and run
trivy config .. - Add a line
USER nobodybeforeCMDand scan again. - Run it once more with
--include-non-failures.
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:
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.
- Run
trivy fs --scanners license .on a project that has a lock file. - Find the most restrictive category in the output.
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:
trivy image --format json --output result.json alpine:3.15
The formats a beginner should know:
tableis the default, for reading in a terminal.jsonis the complete report, with every field. It is the right choice for scripts and the source of truth you can convert from later.sarifis 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,spdxandspdx-jsonproduce an SBOM rather than a vulnerability report. The next section covers these.templaterenders through a Go template, for custom layouts such as an HTML report; Trivy ships templates for HTML, JUnit and other targets.githubproduces a dependency snapshot for GitHub's dependency graph, andcosign-vulnproduces 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:
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:
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.
- Save a JSON report with
--format json --output result.json. - Convert it with
trivy convert --format sarif --output result.sarif result.json. - Open both files and compare their first twenty lines.
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.
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:
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:
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.
- Generate a CycloneDX SBOM for
alpine:3.15and open it. - Count the components, then run
trivy sbom sbom.cdx.jsonon it. - Compare the findings with those from a direct
trivy image alpine:3.15scan.
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:
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:
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.
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:
trivy image --exit-on-eol 1 myapp:1.0
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):
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.
- Run
trivy image --severity CRITICAL python:3.4-alpine; echo "exit: $?". - Run it again with
--exit-code 1added. - Run
trivy image --exit-code 1 --severity CRITICAL alpine:3.20; echo "exit: $?"on a recent image.
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:
# 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:
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.
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:
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.
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
- Pick one finding from a scan of
python:3.4-alpineand put its ID in a.trivyignorefile with an expiry date far in the future. - Re-run the scan and confirm it disappears and the total drops by one.
- Change the expiry to a date in the past and run it again.
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 ametadata.jsonsaying 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:
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.
- Find your cache directory and list it:
ls ~/.cache/trivy(orls ~/Library/Caches/trivyon macOS). - Open
db/metadata.jsonand read the build time. - Run a scan with
--skip-db-updateand compare its start-up with a normal one.
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:
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:
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.
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.
- Create the
trivy.yamlabove in a project, removing theexit-codeline. - Run
trivy fs .and confirm the severity filter applied without any flags. - Run it again with
--severity LOWon the command line.
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.
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:
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:
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.
--skip-images and a namespace filter, before pointing it at anything shared.
- Start a local cluster (kind or minikube) and deploy any sample application.
- Run
trivy k8s --report summary --skip-images. - Run it again with
--report all --scanners misconfigand read two findings.
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.
trivy image --timeout 15m myimage, or set TRIVY_TIMEOUT=15m.
--image-src remote to go straight to the registry.
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.
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.
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.
- Run
trivy image this-image-does-not-exist:nopeand read the last lines. - Run it again with
--debugand find which image sources Trivy tried. - Run
trivy image --timeout 1s python:3.4-alpineto provoke a timeout.
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
trivybinary 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-actionGitHub 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-trivywere 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:
- Pin exact versions. Install
v0.74.0, never "latest". Pass the version to the install script. Useaquasec/trivy:0.74.0, notaquasec/trivy:latest. - Pin by digest where you can. A digest (
aquasec/trivy@sha256:...) identifies exact content, and cannot be silently replaced. - 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.
- Verify what you download against the published checksums and signatures.
- Limit what a scan job can see. A scanner rarely needs your deployment credentials; give the job only what it needs.
- Do not run an installer from a moving reference without a pinned version argument.
- Find every place in your projects that installs or runs Trivy (Dockerfiles, workflows, scripts).
- For each, write down how the version is chosen: exact, range, tag, or "latest".
- Change one of them to an exact version, and one action reference to a full commit SHA.
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:
flask==2.0.1
requests==2.25.0
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)
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"]
severity:
- HIGH
- CRITICAL
scan:
scanners:
- vuln
- secret
- misconfig
vulnerability:
ignore-unfixed: true
Now run the stages in order.
# 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:
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.
- Build the project exactly as shown and run every stage.
- Upgrade the pinned libraries and add the non-root user until stage five exits with zero.
- Turn the five stages into a CI workflow that pins its tool versions.
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
- Trivy documentation
- Installation
- trivy image command reference
- Configuration file reference
- Troubleshooting
- Standalone mode
- Kubernetes scanning
- Filtering results
- Misconfiguration scanning
- Secret scanning
- SBOM
- VEX
- Trivy releases
- v0.72.0 highlights
- v0.73.0 highlights
- v0.74.0 highlights
- Security incident notice, 2026-03-19
- trivy-action