Trivy : CVEs Scanner | Docs | Trivy image

Trivy scans OCI-container images, OS folders, Kubernetes clusters, Git repos, virtual machines, and more. And can create SBOM and CVE-vulnerabilities audits of them.

Install

mkdir -p trivy
cd trivy

# Download/Extract the binary
ver='0.49.1'
tarball="trivy_${ver}_Linux-64bit.tar.gz"
url=https://github.com/aquasecurity/trivy/releases/download/v${ver}/$tarball
curl -LO $url && tar -xvaf $tarball

# Install it
sudo cp trivy /usr/local/bin/

Useage

List of Commands

# Update Trivy's CVEs database
trivy image --download-db-only 

# Remove cached databases
trivy image --reset

# Scan container (ctnr) image and log results
trivy image -o log $image

# Scan ctnr image for CVEs, create its SPDX-compliant SBOM, out to JSON
sbom=sbom.spdx
trivy image --scanners vuln --format spdx-json -o $sbom.json $image
# Scan/Audit SBOM file for vulnerabilities (CVEs) of declared severities
trivy sbom --severity CRITICAL,HIGH --format json -o $sbom.audit.json $sbom.json

# Scan K8s cluster (experimental)
trivy k8s --report summary $cluster_name

# Scan host filesystem (FS)
trivy fs --scanners vuln,secret,misconfig $target_path

# Scan a binary file on host filesystem (FS)
trivy fs --scanners vuln,secret,misconfig $(which $bin)

# Scan a remote repo
trivy repo https://github.com/aquasecurity/trivy-ci-test

# Scan a virtual machine image
trivy vm --scanners vuln $adisk.vmdk

# Scan AWS machine image
trivy vm ami:$id

# Scan AWS EBS snapshot
trivy vm ebs:$id

Trivy Format Options

Regarding SBOMs and CVE audits of OCI-compliant (AKA container) images, not all SBOM specifications include CVE information. Regardless, "trivy sbom ...FILE" of the SBOM file generated by "trivy image ... IMAGE" provides the detailed vulnerabilities audit.

-f, --format string   format(table,json,template,sarif,cyclonedx,spdx,spdx-json,github,cosign-vuln) (default table)
  1. SPDX (Software Package Data Exchange) :
    SPDX is an open standard for sharing information about software package licenses, copyrights and such OSS tracking. It is widely used for creating and sharing SBOMs. Originally developed by the Linux Foundation.
    • trivy image --format spdx-json ...
  2. CycloneDX :
    CycloneDX is another SBOM standard focused on security and software component and supply chain audit. Created by the OWASP Foundation.

    • trivy image --format cyclonedx ...

      ...
      "description": "The grafana plugin SDK bundles build metadata into ... including said credentials.",
      "recommendation": "Upgrade github.com/grafana/grafana-plugin-sdk-go to version 0.250.0",
      "advisories": [
          ...
          {
              "url": "https://grafana.com/security/security-advisories/cve-2024-8986"
          },
          ...
          {
              "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-8986"
          },
          ...
      ],
      ...
      
  3. SARIF (Static Analysis Results Interchange Format) :
    SARIF is a standard format developed by OASIS for reporting the results of static code analysis tools. It focuses on security vulnerabilities and code quality issues rather than an SBOM. SARIF does not include an SBOM. It is primarily for representing the results of vulnerability scanning in a way that can be consumed by other security tools.

  4. Cosign-vuln :
    Cosign is part of the Sigstore project and is used for signing container images and verifying their integrity; attestations. The "cosign-vuln" format is used to integrate vulnerability scanning results with Cosign's signing and verification workflow. Cosign-vuln format does not include an SBOM. It's primarily focused on vulnerability reporting and container image signing.

Other SBOM standards:

SBOMs : Trivy v. Syft

Syft does not include vulnerabilities. It's purely SBOM. Syft appears to capture more components, yet less dependencies than Trivy:

# Install Syft
☩ curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
# Syft scan
☩ syft scan $image -o cyclonedx-json=$sbom.syft.cdx.json

# @ Syft 
☩ cat $sbom.syft.cdx.json |jq .components |wc -l
28132
☩ cat $sbom.syft.cdx.json |jq .dependencies |wc -l
641

# @ Trivy
☩ cat $sbom.cdx.json |jq .components |wc -l
9143
☩ cat $sbom.cdx.json |jq .dependencies |wc -l
2505

SBOM / Audit : SPDX v. CycloneDX

The output of a Trivy scan of a container image, "trivy image ...", by either of these two DX formats includes the image's SBOM.

The output of a Trivy scan of an SBOM (file), "trivy sbom ...", also includes the image's SBOM.

Regardless of format, all SBOMs contain much the same information. However, their specified keys differ. For example, CycloneDX key vulnerabilities[].advisories[].url is SPDX key packages[].externalRefs[].referenceLocator, yet Trivy (default, non-standard) SBOM key Results[].Vulnerabilities[].PrimaryURL.

Naming Convention

For FILEs of a container-image scan :

# trivy image ... -o FILE ... IMAGE 
[$registry[_$port].][.$repo.]${app}_${tag}.sbom.$spec.$ext`

For FILEs of a CVEs audit of an SBOM file :

# trivy sbom ... -o FILE ... SBOM_FILE
[$registry[_$port].][.$repo.]${app}_${tag}.sbom.$spec.audit.$ext`

 

Not all SBOM standards include vulnerability audit. Syft for example. Use "trivy sbom ..." on that or any other SBOM file to obtain detailed vulnerability audit based on its SBOM. Output of Trivy's sbom command includes the input SBOM by default.

# Analyze non-standard (trivy) SBOM file : FAILing
trivy sbom $severity $sbom.trivy.json -o $sbom.trivy.audit.log
trivy sbom $severity $sbom.trivy.json --format json -o $sbom.trivy.audit.json

# Analyze SBOM of SPDX file 
trivy sbom $severity $sbom.spdx.json -o $sbom.spdx.audit.log
trivy sbom $severity $sbom.spdx.json --format json -o $sbom.spdx.audit.json

# Analyze SBOM of CycloneDX file 
trivy sbom $severity $sbom.cdx.json -o $sbom.cdx.audit.log 
trivy sbom $severity $sbom.cdx.json --format json -o $sbom.cdx.audit.json

Refactor CycloneDX SBOM/Audit

Filter out all keys not required for remediation AKA mitigation :

image=docker.io/grafana/grafana:11.2.0
#image=python:3.12.6
sbom=${image////.};sbom=${sbom//:/_}
severity='--severity CRITICAL,HIGH'

## Create SBOM 
sbom=$sbom.cdx.json
trivy image --scanners vuln $severity $image --format cyclonedx -o $sbom

## Filter SBOM 
# out=$sbom.cdx.filtered
# cat $sbom.cdx.json |jq -Mr '{
#     About: "Filtered CycloneDX-compliant SBOM created by Trivy scan of a container image.",
#     Image: .metadata.component.name,
#     Scan: "'"trivy image --scanners vuln $severity $image --format cyclonedx -o $sbom"'",
#     Source: "'$sbom'",
#     Date: "'$(date -u --iso-8601=seconds)'",
#     CVEs: [
#         .vulnerabilities[]? | {
#             "ID": .id,
#             "Severity": (.ratings | map({"source":.source.sbom,"severity":.severity}) // []),
#             "Description": .description,
#             "Recommendation": (.recommendation // "See Advisories."),
#             "Advisories": (.advisories // []) | map(.url)
#         }
#     ]
# }' |tee $out.json

## Audit SBOM and filter the audit.
audit=$sbom.cdx.audit.json
out=$sbom.cdx.audit.filtered
# trivy sbom $severity $sbom --format json -o $audit
trivy sbom $severity --format json $sbom.cdx.json |jq -Mr '{
    Image: .ArtifactName,
    CVEs: .Results | .[]?
} | {
    About: "Filtered CVEs audit of a CycloneDX-compliant SBOM file.",
    Image: "'$image'", 
    SBOM: "'$sbom.cdx.json'",
    Audit: "trivy sbom '"--scanners vuln $severity --format json <SBOM>"'",
    Date: "'$(date -u --iso-8601=seconds)'",
    CVEs: {
        Vulnerabilities: [
            .CVEs.Vulnerabilities[]? | select(. != null and .VulnerabilityID != null) | {
                ID: .VulnerabilityID,
                Severity: .Severity,
                Title: .Title,
                Description: .Description,
                PkgName: .PkgName,
                PkgVersionInstalled: .InstalledVersion,
                PkgVersionFixed: (.FixedVersion // "N.A"),
                Status: .Status,
                PrimaryURL: .PrimaryURL,
                References: .References
            }
        ] 
    }
} | select(.CVEs.Vulnerabilities | length > 0)' \
    |tee $out.json \
    |yq -P -o=yaml eval \
    |tee $out.yaml \
    >/dev/null

rm $sbom.cdx.json

Containerized Trivy

K8s : trivy-operator

The Trivy Operator automatically discovers and scans all images running in a K8s cluster, including images of application pods and system pods. Scan reports are summarized and saved as VulnerabilityReport (CRD) resources, which are owned by a Kubernetes controller.

Install by Helm

ver='0.24.1' # trivy v0.22.0
helm repo add trivy-operator https://aquasecurity.github.io/helm-charts/
helm upgrade --install trivy trivy-operator/trivy-operator --version $ver

  Scan reports saved to CRD: kind: VulnerabilityReport

k get VulnerabilityReport 

vr=statefulset-my-vault-vault

k -n vault get VulnerabilityReport $vr -o json \
    |jq -Mr '.report.vulnerabilities | .[]? |select(.severity == "CRITICAL" or .severity == "HIGH")'
    |jq . --slurp
[
  {
    "fixedVersion": "23.0.15, 26.1.5, 27.1.1, 25.0.6",
    "installedVersion": "v25.0.5+incompatible",
    "lastModifiedDate": "2024-07-30T20:15:04Z",
    "links": [],
    "primaryLink": "https://avd.aquasec.com/nvd/cve-2024-41110",
    "publishedDate": "2024-07-24T17:15:11Z",
    "resource": "github.com/docker/docker",
    "score": 9.9,
    "severity": "CRITICAL",
    "target": "",
    "title": "moby: Authz zero length regression",
    "vulnerabilityID": "CVE-2024-41110"
  },
  {
    ...
    "severity": "HIGH",
    "target": "",
    "title": "encoding/gob: golang: Calling Decoder.Decode ... can cause a panic due to stack exhaustion",
    "vulnerabilityID": "CVE-2024-34156"
  }
]

Docker


trivy=aquasec/trivy:0.52.2
subject=docker.io/grafana/grafana:11.2.0
sbom=${subject////.}
sbom=${sbom//:/_}.sbom.cdx.json

docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /tmp/trivy:/root/.cache/ \
    -v $(pwd):/tmp \
    $trivy image -q \
        --scanners vuln \
        --format cyclonedx \
        -o /tmp/$sbom\
        $subject

Advanced Docker Configuration : Rootless

Trivy running as a container (above) has a bind mount to Docker's socket (/var/run/docker.sock) to provide the required communication to Docker API for pulling images, examining layers and such; however, that's a big security issue because the container must then run as root. A more secure arrangement (if by TLS) is to connect via TCP using Trivy flag --docker-host. Note we're configuring for Trivy to run as non-root user, which is a security boost regardless.

To implement this, configure the Docker daemon to listen on the host's common interface (eth0) rather than to whatever file descriptor (fd) is passed by systemd.

1. Get host address

Want Docker API available via host interface.

$ ip -4 -brief addr 
lo               UNKNOWN        127.0.0.1/8 10.255.255.254/32
eth0             UP             172.27.240.169/20
docker0          DOWN           172.17.0.1/16

2. Reconfigure Docker server (daemon)

ifc=172.27.240.169
cat <<-EOH |sudo tee /etc/docker/daemon.json
{
    "hosts": [
        "tcp://$ifc:2375",
        "unix:///var/run/docker.sock"
    ]
}
EOH

Verify the configuation file

# Validate the configuration 
sudo systemctl stop docker.service
sudo dockerd --config-file /etc/docker/daemon.json

3. Reconfigure docker.service

Remove the -H fd:// flag, which allows for file descriptor (fd) passing and is used in the systemd environment to tell Docker to listen on a socket activated by systemd. We rather declare its listening socket in the Docker daemon's config /etc/docker/daemon.json.

Use a drop-in file, leaving the default unit file unaltered. That is the advised method for managing systemd unit-file configurations.

# Override the default unit file by adding a drop-in file
sudo mkdir -p /etc/systemd/system/docker.service.d

cat <<-EOH |sudo tee /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
EOH

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl status docker

4. Declare TRIVY_CACHE_DIR

Trivy's default cache directory is /root/.cache, which requires root user. Yet this may be set to that accessible by non-root user.

Now we're ready.

Run containerized Trivy as non-root (UID:GID)
ifc=172.27.240.169
ctnr_cache=/tmp/trivy_cache
ctnr_work=/mnt/trivy_work
trivy=aquasec/trivy:0.52.2
subject=docker.io/grafana/grafana:11.2.0
sbom=${subject////.}
sbom=${sbom//:/_}.sbom.cdx.json

docker run --rm \
    -e TRIVY_CACHE_DIR=$ctnr_cache \
    -v /tmp/trivy:$ctnr_cache \
    -v $(pwd):$ctnr_work \
    --user 1000:1000 \
    $trivy image \
        --scanners vuln \
        --docker-host tcp://$ifc:2375 \
        --format cyclonedx \
        -o $ctnr_work/$sbom \
        $subject

Secure by TLS

@ /etc/docker/daemon.json

ifc=172.27.240.169
cat <<-EOH |sudo tee /etc/docker/daemon.json
{
    "hosts": [
        "tcp://$ifc:2375",
        "unix:///var/run/docker.sock"
    ],
    "tls": true,
    "tlsverify": true,
    "tlscacert": "/etc/docker/certs.d/ca.pem",
    "tlscert": "/etc/docker/certs.d/server-cert.pem",
    "tlskey": "/etc/docker/certs.d/server-key.pem"
}
EOH

docker run --rm \
    -v /path/to/certs:/etc/docker/certs.d \
    -e DOCKER_HOST=tcp://$ifc:2376 \
    -e DOCKER_TLS_VERIFY=1 \
    -e DOCKER_CERT_PATH=/etc/docker/certs.d \
    ...