devsecops

Scanning IaC and Images with Trivy

Scan container images, IaC, and SBOMs for vulnerabilities and misconfigurations in CI/CD with Trivy.

ci-cdcontainer-securitydevsecopsiac-securitymisconfigurationsbomtrivyvulnerability-scanning
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Trivy (by Aqua Security) is a comprehensive, open-source security scanner that finds vulnerabilities (CVEs), misconfigurations (IaC), secrets, software licenses, and software supply-chain weaknesses across a wide range of targets: container images, filesystems, Git repositories, virtual machine images, Kubernetes clusters, and SBOM documents. It is widely adopted as a "shift-left" gate in CI/CD pipelines because it is fast, runs as a single static binary, requires no agent, and supports machine-readable output formats (JSON, SARIF, CycloneDX, SPDX) for integration with code-scanning dashboards.

Trivy bundles four primary scanners that can be toggled with --scanners:

  • vuln — OS package and language-dependency vulnerability detection (CVE matching against the Trivy vulnerability DB).
  • misconfig — Infrastructure-as-Code and configuration misconfiguration detection (Terraform, CloudFormation, Kubernetes manifests, Dockerfile, Helm) using built-in and custom Rego policies.
  • secret — Hard-coded secret/credential detection (API keys, tokens, private keys).
  • license — Software license identification and policy enforcement.

This skill covers building a Trivy-based scanning workflow that gates a CI/CD pipeline: scanning images before push, scanning IaC before apply, generating and re-scanning SBOMs, and failing builds on policy violations. Detecting these weaknesses defends against the MITRE ATT&CK technique T1525 (Implant Internal Image), where adversaries plant malicious or vulnerable images in a registry to be deployed across the environment.

When to Use

  • When integrating vulnerability and misconfiguration scanning into a CI/CD pipeline as a quality/security gate before images are pushed or infrastructure is applied.
  • When auditing container images in a registry for known CVEs prior to deployment.
  • When validating Terraform, CloudFormation, Kubernetes, Dockerfile, or Helm IaC for security misconfigurations.
  • When generating an SBOM (CycloneDX/SPDX) for supply-chain transparency and later re-scanning that SBOM for newly disclosed CVEs.
  • When scanning a running Kubernetes cluster for vulnerable workloads and misconfigured RBAC/resources.
  • When enforcing license compliance policy on dependencies.

Prerequisites

  • A Linux/macOS/Windows host or CI runner with network access to download the Trivy vulnerability database.
  • Docker (optional) if scanning local images by name or using the containerized Trivy.
  • Install Trivy (Aqua Security official methods):
# Debian/Ubuntu (APT repository)
sudo apt-get install -y 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 /etc/apt/sources.list.d/trivy.list
sudo apt-get update
sudo apt-get install -y trivy
 
# RHEL/CentOS (YUM repository), macOS (Homebrew), and install script
brew install trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
 
# Containerized usage (no install)
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image python:3.10-alpine
 
# Verify
trivy --version
  • For air-gapped or rate-limited environments, pre-download the DB with trivy image --download-db-only and the Java/policy bundles as needed.

Objectives

  • Scan a container image for OS and language vulnerabilities and fail the build above a severity threshold.
  • Scan IaC (Terraform/Kubernetes/Dockerfile) for misconfigurations.
  • Detect hard-coded secrets in a repository or filesystem.
  • Generate a CycloneDX or SPDX SBOM and re-scan it for vulnerabilities.
  • Emit SARIF for GitHub code scanning and JSON for programmatic gating.
  • Wire Trivy into a CI/CD pipeline with --exit-code to enforce policy.

MITRE ATT&CK Mapping

Technique ID Name Tactic Relevance
T1525 Implant Internal Image Persistence Trivy detects vulnerable or malicious images/layers and embedded secrets before they are implanted in a registry and propagated to running workloads.

Workflow

1. Scan a container image for vulnerabilities

Run a vulnerability-only scan of a registry image, restricting to high/critical findings and ignoring CVEs with no available fix:

trivy image \
  --scanners vuln \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --format table \
  python:3.10-alpine

Scan an image saved as a tarball (useful when the image is built but not yet pushed):

docker save myorg/app:1.4.0 -o app.tar
trivy image --input app.tar --severity CRITICAL --format json --output app-vulns.json

2. Enable multiple scanners on an image

Run vulnerability, misconfiguration, secret, and license scanners together:

trivy image \
  --scanners vuln,misconfig,secret,license \
  --severity MEDIUM,HIGH,CRITICAL \
  myorg/app:1.4.0

Scan the image's embedded config (Dockerfile-equivalent build history and history secrets):

trivy image --image-config-scanners misconfig,secret myorg/app:1.4.0

3. Scan Infrastructure-as-Code (misconfiguration)

Scan a directory of Terraform / Kubernetes / Dockerfile / Helm / CloudFormation for misconfigurations using the config target:

# Scan a Terraform / IaC directory
trivy config \
  --severity HIGH,CRITICAL \
  --format table \
  ./infra
 
# Scan with custom Rego policies and a specific policy namespace
trivy config \
  --config-policy ./policies \
  --policy-namespaces user \
  ./infra

Alternatively use the fs (filesystem) target with the misconfig scanner explicitly:

trivy fs --scanners misconfig,secret --severity HIGH,CRITICAL ./infra

4. Scan a repository and detect secrets

Scan a local working tree (or remote repo) for vulnerabilities in lockfiles and hard-coded secrets:

# Local filesystem (dependencies + secrets)
trivy fs --scanners vuln,secret --severity HIGH,CRITICAL .
 
# Remote Git repository
trivy repository --scanners vuln,secret https://github.com/myorg/myrepo

5. Generate and re-scan an SBOM

Produce a CycloneDX SBOM from an image, then scan the SBOM itself for vulnerabilities (so a stored SBOM can be re-evaluated as new CVEs are disclosed):

# Generate CycloneDX SBOM
trivy image --format cyclonedx --output sbom.cdx.json myorg/app:1.4.0
 
# Generate SPDX SBOM
trivy image --format spdx-json --output sbom.spdx.json myorg/app:1.4.0
 
# Re-scan the SBOM for vulnerabilities later
trivy sbom --severity HIGH,CRITICAL sbom.cdx.json

6. Emit SARIF for code-scanning dashboards

Produce SARIF for GitHub Advanced Security / code scanning ingestion:

trivy image \
  --format sarif \
  --output trivy-results.sarif \
  --severity HIGH,CRITICAL \
  myorg/app:1.4.0

7. Gate the CI/CD pipeline with exit codes

Use --exit-code 1 so the pipeline step fails when findings at or above the chosen severity are present. Separate the "report everything" run (exit 0) from the "enforce" run (exit 1):

# 1) Informational report (never fails the build)
trivy image --severity LOW,MEDIUM,HIGH,CRITICAL --exit-code 0 --format table myorg/app:1.4.0
 
# 2) Enforcement gate (fails build on HIGH/CRITICAL with a fix available)
trivy image \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --exit-code 1 \
  --format json --output gate.json \
  myorg/app:1.4.0

Example GitHub Actions step using the official action:

- name: Run Trivy image scan (gate)
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myorg/app:1.4.0'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'HIGH,CRITICAL'
    ignore-unfixed: true
    exit-code: '1'

8. Manage false positives and the DB

Suppress accepted-risk findings with a .trivyignore file and keep the DB current:

# .trivyignore — one CVE/AVD/secret rule ID per line
echo "CVE-2023-12345" >> .trivyignore
echo "AVD-AWS-0089"   >> .trivyignore
 
# Refresh DBs explicitly (useful for caching layers in CI)
trivy image --download-db-only
trivy image --download-java-db-only
 
# Scan a Kubernetes cluster (summary report)
trivy k8s --report summary --severity HIGH,CRITICAL cluster

Tools and Resources

Tool / Resource Purpose Link
Trivy Core scanner CLI https://github.com/aquasecurity/trivy
Trivy Documentation Official docs (targets, scanners, flags) https://trivy.dev/latest/docs/
trivy-action GitHub Actions integration https://github.com/aquasecurity/trivy-action
Trivy Operator In-cluster Kubernetes continuous scanning https://github.com/aquasecurity/trivy-operator
Trivy vulnerability DB OSS vulnerability data source https://github.com/aquasecurity/trivy-db
CycloneDX SBOM standard emitted by Trivy https://cyclonedx.org/

Validation Criteria

  • Trivy installed and trivy --version reports a valid version.
  • Container image scanned for vulnerabilities with severity filtering applied.
  • Image scanned with multiple scanners (vuln, misconfig, secret, license).
  • IaC directory scanned with trivy config and misconfigurations reviewed.
  • Secret scanning run against the repository.
  • CycloneDX/SPDX SBOM generated and successfully re-scanned with trivy sbom.
  • SARIF output produced for the code-scanning dashboard.
  • CI/CD gate fails the build on HIGH/CRITICAL findings via --exit-code 1.
  • .trivyignore configured for accepted-risk findings with documented justification.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md2.9 KB

Trivy — Command and Flag Reference

Scan Targets (subcommands)

Command Target Example
trivy image Container image (registry/tar) trivy image alpine:3.19
trivy fs Local filesystem / project dir trivy fs ./
trivy repository (repo) Git repository (local or remote URL) trivy repo https://github.com/org/repo
trivy config IaC / config misconfiguration trivy config ./infra
trivy sbom Existing SBOM (CycloneDX/SPDX) trivy sbom sbom.cdx.json
trivy kubernetes (k8s) Live Kubernetes cluster trivy k8s --report summary cluster
trivy vm VM image (AMI/EBS/VMDK) trivy vm ami:ami-0123
trivy rootfs Extracted root filesystem trivy rootfs /mnt/rootfs

Key Flags

Flag Description
--scanners vuln,misconfig,secret,license Select which scanners to run
--severity LOW,MEDIUM,HIGH,CRITICAL Filter results by severity
--exit-code <n> Exit code when matching results are found (gating)
--ignore-unfixed Suppress vulnerabilities with no fixed version
--format table|json|sarif|cyclonedx|spdx-json Output format
--output <file> Write report to file
--input <file> Scan an image tar instead of a registry ref
--vuln-type os,library Limit vulnerability detection scope
--image-config-scanners misconfig,secret Scan image build config/history
--config-policy <dir> Custom Rego misconfig policy directory
--policy-namespaces <ns> Rego policy namespaces to evaluate
--download-db-only Pre-download vulnerability DB (caching/air-gap)
--download-java-db-only Pre-download Java index DB
--skip-dirs / --skip-files Exclude paths from scan
--ignorefile <path> Path to .trivyignore (default .trivyignore)

Output Formats

Format Use
table Human-readable console (default)
json Programmatic gating / parsing
sarif GitHub code scanning / IDE ingestion
cyclonedx CycloneDX SBOM
spdx-json SPDX SBOM (JSON)
github GitHub dependency snapshot

.trivyignore Format

# One ID per line; supports CVE, AVD (misconfig), and secret rule IDs
CVE-2023-12345
AVD-AWS-0089
generic-api-key

GitHub Actions (trivy-action)

- uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'image'      # image | fs | config | repo | sbom
    image-ref: 'myorg/app:1.4.0'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'HIGH,CRITICAL'
    ignore-unfixed: true
    exit-code: '1'

External References

standards.md1.4 KB

Standards and References — Scanning IaC and Images with Trivy

NIST CSF 2.0

ID Name Rationale
ID.RA-01 Asset vulnerabilities are identified and recorded Trivy enumerates CVEs, misconfigurations, and embedded secrets across images, IaC, and SBOMs, recording them for risk assessment and remediation tracking.

MITRE ATT&CK

Technique ID Name Tactic Rationale
T1525 Implant Internal Image Persistence Scanning images and SBOMs before they reach a registry detects vulnerable, secret-laden, or malicious layers an adversary could implant to persist across deployments.

Supporting Frameworks and Standards

  • CIS Benchmarks — Trivy misconfig checks align with CIS Docker/Kubernetes hardening guidance.
  • CycloneDX / SPDX — SBOM output formats supported for supply-chain transparency.
  • SARIF — Static Analysis Results Interchange Format for code-scanning dashboards.
  • OWASP Top 10 (A06: Vulnerable and Outdated Components) — Trivy directly addresses outdated/vulnerable dependency detection.

Official Resources

Scripts 1

agent.py6.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Trivy scanning helper.

Wraps the Trivy CLI to scan container images, filesystems/IaC, and SBOMs,
parse the JSON results, summarise findings by severity, and exit non-zero
when findings at or above a chosen threshold are present (CI/CD gating).

Requires the `trivy` binary on PATH. See https://github.com/aquasecurity/trivy
"""

import argparse
import json
import shutil
import subprocess
import sys
from collections import Counter
from datetime import datetime, timezone

SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]


def ensure_trivy() -> str:
    """Return the trivy executable path or exit with guidance."""
    path = shutil.which("trivy")
    if not path:
        print("[!] trivy not found on PATH. Install: "
              "https://trivy.dev/latest/getting-started/installation/",
              file=sys.stderr)
        sys.exit(2)
    return path


def build_command(trivy: str, args: argparse.Namespace) -> list:
    """Construct the trivy command line for the requested target."""
    cmd = [trivy, args.target]
    cmd += ["--scanners", args.scanners]
    if args.severity:
        cmd += ["--severity", args.severity]
    if args.ignore_unfixed:
        cmd.append("--ignore-unfixed")
    # Always request JSON so this helper can parse + gate deterministically.
    cmd += ["--format", "json"]
    # exit-code 0 here; gating is enforced in Python after parsing.
    cmd += ["--exit-code", "0"]
    cmd.append(args.subject)
    return cmd


def run_scan(cmd: list, timeout: int) -> dict:
    """Execute trivy and return the parsed JSON report."""
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        print(f"[!] Trivy scan timed out after {timeout}s", file=sys.stderr)
        sys.exit(3)
    if proc.returncode not in (0,):
        # Trivy writes scan results to stdout; real errors go to stderr.
        if not proc.stdout.strip():
            print(f"[!] Trivy failed (rc={proc.returncode}): {proc.stderr.strip()}",
                  file=sys.stderr)
            sys.exit(proc.returncode)
    try:
        return json.loads(proc.stdout)
    except json.JSONDecodeError:
        print("[!] Could not parse Trivy JSON output.", file=sys.stderr)
        print(proc.stderr.strip(), file=sys.stderr)
        sys.exit(4)


def summarise(report: dict) -> tuple:
    """Return (severity Counter, list of finding dicts) from a Trivy report."""
    counts = Counter()
    findings = []
    for result in report.get("Results", []) or []:
        target = result.get("Target", "")
        for vuln in result.get("Vulnerabilities", []) or []:
            sev = (vuln.get("Severity") or "UNKNOWN").upper()
            counts[sev] += 1
            findings.append({
                "type": "vuln",
                "target": target,
                "id": vuln.get("VulnerabilityID"),
                "pkg": vuln.get("PkgName"),
                "installed": vuln.get("InstalledVersion"),
                "fixed": vuln.get("FixedVersion"),
                "severity": sev,
            })
        for mis in result.get("Misconfigurations", []) or []:
            sev = (mis.get("Severity") or "UNKNOWN").upper()
            counts[sev] += 1
            findings.append({
                "type": "misconfig",
                "target": target,
                "id": mis.get("ID"),
                "title": mis.get("Title"),
                "severity": sev,
            })
        for sec in result.get("Secrets", []) or []:
            sev = (sec.get("Severity") or "UNKNOWN").upper()
            counts[sev] += 1
            findings.append({
                "type": "secret",
                "target": target,
                "id": sec.get("RuleID"),
                "title": sec.get("Title"),
                "severity": sev,
            })
    return counts, findings


def gate(counts: Counter, threshold: str) -> bool:
    """Return True if any finding is at or above threshold severity."""
    if threshold not in SEVERITY_ORDER:
        return False
    idx = SEVERITY_ORDER.index(threshold)
    blocking = SEVERITY_ORDER[idx:]
    return any(counts.get(s, 0) > 0 for s in blocking)


def main() -> None:
    parser = argparse.ArgumentParser(description="Trivy scan + CI/CD gate helper")
    parser.add_argument("subject", help="Image ref, directory, repo URL, or SBOM file")
    parser.add_argument("--target", default="image",
                        choices=["image", "fs", "config", "repo", "sbom"],
                        help="Trivy scan target subcommand")
    parser.add_argument("--scanners", default="vuln",
                        help="Comma list: vuln,misconfig,secret,license")
    parser.add_argument("--severity", default="HIGH,CRITICAL",
                        help="Severities to include")
    parser.add_argument("--ignore-unfixed", action="store_true",
                        help="Skip vulns with no fix available")
    parser.add_argument("--gate", default="", choices=["", *SEVERITY_ORDER],
                        help="Exit non-zero if findings >= this severity")
    parser.add_argument("--timeout", type=int, default=600, help="Scan timeout (s)")
    parser.add_argument("--output", help="Write JSON summary to file")
    args = parser.parse_args()

    trivy = ensure_trivy()
    cmd = build_command(trivy, args)
    print(f"[*] {datetime.now(timezone.utc).isoformat()} running: {' '.join(cmd)}")
    report = run_scan(cmd, args.timeout)
    counts, findings = summarise(report)

    print("\n=== TRIVY FINDINGS BY SEVERITY ===")
    for sev in reversed(SEVERITY_ORDER):
        print(f"  {sev:<9}: {counts.get(sev, 0)}")
    print(f"  TOTAL    : {sum(counts.values())}")

    if args.output:
        with open(args.output, "w", encoding="utf-8") as fh:
            json.dump({"counts": dict(counts), "findings": findings}, fh, indent=2)
        print(f"[+] Summary written to {args.output}")

    if args.gate and gate(counts, args.gate):
        print(f"[!] GATE FAILED: findings at or above {args.gate}", file=sys.stderr)
        sys.exit(1)
    print("[+] Scan complete.")


if __name__ == "__main__":
    main()
Keep exploring