The previous lessons ran a different tool per concern: gitleaks for secrets, Semgrep for code, Checkov for Terraform, Hadolint for the Dockerfile. Four tools, four configs, four sets of output to reconcile.
trivy fs does dependencies, secrets and infrastructure config in one pass over the same directory. This lesson is about when that consolidation is the right call — and the specific things it will cost you.
Verified against Trivy 0.72.0, scanning the Range. Every number is copied from a real terminal.
Step 1 — One command, three scanners
brew install trivy
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range
trivy fs . --scanners vuln,secret,misconfigapp/package-lock.json (npm)
===========================
Total: 54 (UNKNOWN: 0, LOW: 7, MEDIUM: 23, HIGH: 21, CRITICAL: 3)
docker/Dockerfile (dockerfile)
==============================
Tests: 29 (SUCCESSES: 22, FAILURES: 7)
Failures: 7 (LOW: 1, MEDIUM: 1, HIGH: 2, CRITICAL: 3)
terraform/broken.tf (terraform)
===============================
Tests: 11 (SUCCESSES: 0, FAILURES: 11)
Failures: 11 (LOW: 2, MEDIUM: 1, HIGH: 7, CRITICAL: 1)
docker/Dockerfile (secrets)
CRITICAL: AWS (aws-access-key-id)
CRITICAL: AWS (aws-secret-access-key)One invocation, four result sets, no configuration.
Step 2 — 54 CVEs from seven declared dependencies
app/package.json declares seven packages. The scan reports 54 vulnerabilities across rather more than seven:
axios HIGH 11
lodash HIGH 4
lodash CRITICAL 1
path-to-regexp HIGH 3
body-parser HIGH 1
ejs CRITICAL 1
jsonwebtoken HIGH 1
minimist CRITICAL 1path-to-regexp and body-parser are not in package.json at all. They arrived as transitive dependencies of express@4.16.0 — dependencies of your dependencies, which is where most of the risk in a JavaScript project lives and none of the decisions were yours.
This is why the lockfile matters. Trivy scanned
package-lock.json, notpackage.json. The manifest lists intent — "some version of express 4.16" — while the lockfile records the exact resolved graph. Scan the manifest and you get a rough guess; scan the lockfile and you get what is actually installed. A repository without a committed lockfile cannot be scanned accurately, which is a supply-chain problem before it is a scanning problem.
ejs@2.5.7 carries CVE-2022-29078, a template-injection issue leading to remote code execution — worth noting because the Range renders search results through EJS, so it is reachable rather than theoretical.
Step 3 — The Dockerfile findings Hadolint missed
The Hadolint lesson got three findings on this file, all apt-get hygiene. Trivy gets seven on the same file:
DS-0002 (HIGH): Specify at least 1 USER command with a non-root user
DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile
DS-0029 (HIGH): '--no-install-recommends' flag is missed
DS-0031 (CRITICAL): Possible exposure of secret env "AWS_ACCESS_KEY_ID" in ENV
DS-0031 (CRITICAL): Possible exposure of secret env "AWS_SECRET_ACCESS_KEY" in ENV
DS-0031 (CRITICAL): Possible exposure of secret env "DB_PASSWORD" in ENV
DS-0013 (MEDIUM): RUN should not be used to change directoryRoot user and three baked-in credentials — the findings that actually matter, none of which the dedicated Dockerfile linter reported. Still no image build required.
Step 4 — Where the single scanner loses
Consolidation is not free, and the earlier lessons are the evidence:
| Concern | Trivy fs | Dedicated tool |
|---|---|---|
| npm CVEs | 54 | — |
| Secrets | 2 (AWS pair) | gitleaks: 2 · TruffleHog: 2 + git history, + live verification |
| Terraform | 11 | Checkov: 26, incl. wildcard IAM |
| Dockerfile | 7 | Hadolint: 3 |
| Application code | none | Semgrep: XSS, and a custom rule for the SQLi |
Three real gaps.
It has no idea your code is vulnerable. trivy fs does not do taint analysis. The SQL injection and the reflected XSS in app/server.js are invisible to it. SAST is a different category, not a missing rule.
It only sees the working tree. TruffleHog found the same credential in deleted commits and told us whether it still authenticates. Trivy scans files as they are on disk.
It found 11 Terraform issues where Checkov found 26, and the wildcard-admin IAM policy is in the difference.
The honest framing.
trivy fsis the best single command to put in a pipeline that currently has none — one binary, one line, immediate coverage of three concerns. It is not a replacement for the specialists in the areas where they are stronger. Start here; add Semgrep and Checkov when the noise from this one is under control.
Step 5 — Gate it without drowning
54 findings on day one blocks every pull request forever. Two mechanisms make it survivable:
# .github/workflows/scan.yml
name: scan
on: [pull_request]
permissions:
contents: read
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
scan-ref: .
scanners: vuln,secret,misconfig
severity: HIGH,CRITICAL
ignore-unfixed: true # only what you can actually act on
exit-code: 1severity: HIGH,CRITICAL drops the 30 low and medium findings out of the gate.
ignore-unfixed: true is the one that changes the mood of the backlog: it hides vulnerabilities with no released patch. A CVE you cannot fix is not a task, it is an anxiety — track it, but do not fail a build on it.
Both together take 54 findings to a handful that name a package and a version to upgrade to.
Step 6 — Ignore with an expiry
# .trivyignore.yaml
vulnerabilities:
- id: CVE-2022-29078
paths: ["app/package-lock.json"]
statement: "EJS RCE. Lab fixture, deliberately vulnerable. Reviewed 2026-08-14."
expired_at: 2026-11-14expired_at is the field worth adopting everywhere. An ignore without an expiry is permanent by default, and permanent ignores are how a scanner ends up reporting clean on a codebase nobody has looked at in two years. With one, the finding comes back and somebody re-decides.
What you learned
- One command covered dependencies, secrets and IaC — 54 + 2 + 18 findings with no configuration.
- Seven dependencies produced 54 CVEs. Most arrived transitively;
path-to-regexpandbody-parserwere never in your manifest. - Scan the lockfile, not the manifest. Without a committed lockfile you are scanning intent, not reality.
- Trivy found the root user and the ENV secrets that the dedicated Dockerfile linter missed.
- It cannot see into your application code, into git history, or as deeply into IAM policy as Checkov. Best first scanner; not the only one.
ignore-unfixed: trueandexpired_at:are what keep a gate alive past the first fortnight.