Secure the PipelineSCAStep 10 of 31

One Scanner for Dependencies, Secrets and Config (Trivy fs)

difficulty intermediatehands-on 20 min hands-on

32% complete

prereqs · owasp-dependency-check

concepts · SCA · transitive dependencies · unified scanning · severity triage

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

bash
brew install trivy
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range

trivy fs . --scanners vuln,secret,misconfig
text
app/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:

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

path-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, not package.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:

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

Root 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 fs is 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:

yaml
# .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: 1

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

yaml
# .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-14

expired_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-regexp and body-parser were 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: true and expired_at: are what keep a gate alive past the first fortnight.