---
title: "Container Image Vulnerability Scanning in CI/CD with Trivy"
description: "Learn how to automate container image vulnerability scanning in your CI/CD pipeline using Trivy. This guide covers setup, integration strategies, policy enforcement, and remediation workflows to secure your container supply chain."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/devtips/post/container-image-vulnerability-scanning-trivy
---

# Container Image Vulnerability Scanning in CI/CD with Trivy

## Why Container Security Matters

### The Vulnerability Problem

**Container images are a critical attack surface in modern deployments.**

Every time you build a container image, it includes the base OS layer, runtime libraries, dependencies, and your application code. Each layer can contain known vulnerabilities. Without scanning, vulnerable images reach production and expose your infrastructure to attacks.

### Real Statistics

- 80% of container images in production contain at least one known vulnerability
- Supply chain attacks targeting container registries are increasing
- Unpatched container vulnerabilities lead to data breaches and service disruptions

## The Security Challenge

### Why Manual Scanning Isn't Enough

Manual security reviews don't scale. You can't realistically inspect every dependency in every container layer. Without automation, vulnerable images slip through, and by the time they're discovered in production, the damage is already done.

### Common Vulnerabilities in Containers

- **Outdated base images** with unpatched OS vulnerabilities
- **Vulnerable dependencies** from npm, pip, pip, Maven packages
- **Exposed secrets** accidentally included in image layers
- **Misconfigurations** creating insecure defaults
- **Malware** hidden in supply chain attacks

## The Solution: Trivy

### What is Trivy?

Trivy is a simple, fast, and comprehensive container vulnerability scanner created by Aqua Security. It scans container images, filesystems, and configuration files for vulnerabilities, misconfigurations, and secrets.

### Why Choose Trivy?

- **Speed**: Scans images in seconds, not minutes
- **Accuracy**: Supports multiple vulnerability databases (NVD, GitHub Security, Aqua, Alpine)
- **Comprehensive**: Detects OS vulnerabilities, application dependencies, and misconfigurations
- **Zero setup**: Works out of the box without complex configuration
- **CI/CD ready**: Integrates easily into GitHub Actions, GitLab CI, Jenkins
- **Open-source**: Free, transparent, and community-driven

## Installation and Setup

### Install Trivy

```bash
# macOS
brew install trivy

# Linux (Ubuntu/Debian)
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | tee -a /etc/apt/sources.list.d/trivy.list
apt-get update
apt-get install trivy

# Docker
docker pull aquasec/trivy
```

### Basic Image Scanning

```bash
# Scan a local image
trivy image my-app:latest

# Scan from registry
trivy image nginx:latest

# Scan with detailed output
trivy image --severity HIGH,CRITICAL my-app:latest
```

## Setting Severity Thresholds

### Scan with Specific Severity Levels

```bash
# Only show critical and high severity issues
trivy image --severity CRITICAL,HIGH my-app:latest

# Exit with error code if vulnerabilities found
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest
```

### Output Formats

```bash
# JSON output for parsing
trivy image --format json my-app:latest

# SARIF format for GitHub integration
trivy image --format sarif my-app:latest

# Table format (default)
trivy image --format table my-app:latest
```

## CI/CD Integration

### GitHub Actions Workflow

```yaml title=".github/workflows/container-scan.yml"
name: Container Vulnerability Scan

on:
  push:
    branches: [main]
    paths:
      - 'Dockerfile'
      - 'src/**'
  pull_request:
    branches: [main]

jobs:
  trivy-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          file: ./Dockerfile
          push: false
          load: true
          tags: my-app:${{ github.sha }}

      - name: Run Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: my-app:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'

      - name: Upload Trivy results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Fail if critical vulnerabilities found
        run: |
          trivy image --severity CRITICAL my-app:${{ github.sha }} --exit-code 1
```

### GitLab CI Integration

```yaml title=".gitlab-ci.yml"
stages:
  - build
  - scan

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  allow_failure: false
```

## Policy Enforcement

### Create a Trivy Policy

```yaml title="trivy-policy.yaml"
# Define what constitutes a vulnerability violation
severity: HIGH,CRITICAL

# Ignore specific CVEs for known/accepted risks
ignorefile: .trivyignore

# Policy for failing builds
exit-code: 1

# Require sign-off for medium severity
medium-requires-approval: true
```

### Ignoring False Positives

```bash title=".trivyignore"
# Format: CVE-XXXX-XXXXX [optional: expiration date]

# Known false positive or acceptable risk (expires 2026-12-31)
CVE-2024-1234 2026-12-31

# Permanently ignore (use with caution)
CVE-2024-5678
```

## Advanced Scanning Strategies

### Scan Filesystems

```bash
# Scan local directory
trivy fs .

# Scan with detailed output
trivy fs --severity HIGH,CRITICAL --format json . > fs-scan.json
```

### Scan Configuration Files

```bash
# Detect misconfigurations in Dockerfile
trivy config Dockerfile

# Scan Kubernetes manifests
trivy config k8s-manifests/
```

### Generate Software Bill of Materials (SBOM)

```bash
# Generate SBOM in CycloneDX format
trivy image --format cyclonedx my-app:latest > sbom.xml

# Generate SBOM in SPDX format
trivy image --format spdx my-app:latest > sbom.spdx
```

## Remediation Workflow

### When Vulnerabilities Are Found

1. **Minimal Approach**: Update base image

```dockerfile
# Before
FROM ubuntu:20.04

# After
FROM ubuntu:22.04
```

2. **Direct Approach**: Update vulnerable dependency

```dockerfile
FROM node:18-alpine

# Install with security patches
RUN npm install --no-save my-package@latest
```

3. **Complete Approach**: Rebuild entire image

```bash
docker build --no-cache -t my-app:latest .
```

## Real-World Example: Multi-Stage Scanning

```yaml title=".github/workflows/production-scan.yml"
name: Production Container Security

on:
  schedule:
    # Run daily scans
    - cron: '0 2 * * *'
  workflow_dispatch:

jobs:
  scan-all-images:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        image:
          - my-app:latest
          - api-gateway:latest
          - worker-service:latest

    steps:
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ matrix.image }}
          format: 'json'
          output: 'trivy-${{ matrix.image }}.json'
          severity: 'CRITICAL,HIGH,MEDIUM'

      - name: Archive results
        uses: actions/upload-artifact@v3
        with:
          name: trivy-reports
          path: trivy-*.json

      - name: Notify security team
        if: failure()
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"Critical vulnerabilities found in ${{ matrix.image }}"}' \
            ${{ secrets.SLACK_WEBHOOK_URL }}
```

## Monitoring and Reporting

### Store Results Over Time

```bash
# Generate timestamped reports
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
trivy image --format json my-app:latest > reports/scan-$TIMESTAMP.json
```

### Track Vulnerability Trends

```bash
#!/bin/bash
# Count vulnerabilities by severity
trivy image --format json my-app:latest | \
  jq '[.Results[]?.Vulnerabilities[]?.Severity] | group_by(.) | map({severity: .[0], count: length})'
```

## Best Practices

### 1. Scan Early and Often

- Scan during development (local images)
- Scan in CI/CD pipeline (before merge)
- Scan in registry (continuous monitoring)
- Scan in production (runtime detection)

### 2. Use Minimal Base Images

```dockerfile
# Reduce attack surface
FROM alpine:3.18 as base
FROM gcr.io/distroless/base-debian11
```

### 3. Regular Dependency Updates

```bash
# Update dependencies regularly
npm audit fix --force
python -m pip install --upgrade pip
```

### 4. Maintain SBOMs

Generate and store SBOMs for supply chain transparency:

```bash
trivy image --format cyclonedx my-app:latest > sbom.json
git add sbom.json
git commit -m "Update SBOM for security tracking"
```

## Integration with Artifact Registry

### Push Only Scanned Images

```bash
# Only push if scan passes
trivy image --severity CRITICAL,HIGH --exit-code 1 my-app:latest && \
  docker push my-registry/my-app:latest
```

## Conclusion

**Container vulnerability scanning is non-negotiable in modern DevSecOps.**

Trivy makes it simple to detect and prevent vulnerable images from reaching production. Integrate it into your CI/CD pipeline, set enforcement policies, and maintain a culture of continuous security improvement.

## Resources

- [Trivy Official Documentation](https://aquasecurity.github.io/trivy)
- [GitHub Container Scanning Action](https://github.com/aquasecurity/trivy-action)
- [CVE Database References](https://nvd.nist.gov)
- [Distroless Images](https://github.com/GoogleContainerTools/distroless)
