devsecops

Scanning Containers with Trivy in CI/CD

This skill covers integrating Aqua Security's Trivy scanner into CI/CD pipelines for comprehensive container image vulnerability detection. It addresses scanning Docker images for OS package and application dependency CVEs, detecting misconfigurations in Dockerfiles, scanning filesystem and git repositories, and establishing severity-based quality gates that block deployment of vulnerable images.

cicdcontainer-securitydevsecopssecure-sdlctrivyvulnerability-scanning
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When building Docker container images in CI/CD and needing automated vulnerability scanning before registry push
  • When establishing quality gates that prevent images with critical or high CVEs from reaching production
  • When compliance requirements mandate vulnerability scanning of all container images before deployment
  • When scanning IaC files (Dockerfiles, Kubernetes manifests) alongside container image scanning
  • When needing a single tool to scan OS packages, language-specific dependencies, and misconfigurations

Do not use for runtime container security monitoring (use Falco), for scanning running containers in production (use runtime agents), or when only scanning application source code without containerization (use SAST tools).

Prerequisites

  • Trivy CLI installed (v0.50+) or access to aquasecurity/trivy-action GitHub Action
  • Docker daemon available in CI/CD for building and scanning images
  • Container registry credentials for pulling base images and pushing scanned images
  • Trivy vulnerability database accessible (downloaded automatically or cached)

Workflow

Step 1: Configure Trivy Scanning in GitHub Actions

Set up a GitHub Actions workflow that builds a Docker image and scans it with Trivy before pushing to a container registry.

# .github/workflows/container-security.yml
name: Container Security Scan
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
    paths:
      - 'Dockerfile'
      - 'docker-compose*.yml'
      - 'src/**'
      - 'requirements*.txt'
      - 'package*.json'
 
jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      contents: read
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Build Docker image
        run: docker build -t app:${{ github.sha }} .
 
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: 'app:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
          ignore-unfixed: true
 
      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
          category: 'trivy-container'
 
      - name: Run Trivy misconfiguration scanner
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: 'config'
          scan-ref: '.'
          format: 'table'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

Step 2: Scan Dockerfiles for Misconfigurations

Trivy detects common Dockerfile security issues such as running as root, using latest tags, and exposing unnecessary ports.

# Scan Dockerfile for misconfigurations
trivy config --severity HIGH,CRITICAL ./Dockerfile
 
# Scan with custom policy directory
trivy config --policy ./security-policies --severity MEDIUM,HIGH,CRITICAL .
 
# Example secure Dockerfile practices Trivy checks for:
# - USER instruction present (not running as root)
# - HEALTHCHECK instruction defined
# - Base image uses specific tag, not :latest
# - No secrets in ENV or ARG instructions
# - COPY preferred over ADD

Step 3: Integrate with GitLab CI/CD

# .gitlab-ci.yml
stages:
  - build
  - scan
  - push
 
variables:
  TRIVY_CACHE_DIR: .trivycache/
 
build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker save $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -o image.tar
  artifacts:
    paths:
      - image.tar
 
trivy-scan:
  stage: scan
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  cache:
    paths:
      - .trivycache/
  script:
    - trivy image
        --input image.tar
        --exit-code 1
        --severity CRITICAL,HIGH
        --ignore-unfixed
        --format json
        --output trivy-report.json
    - trivy image
        --input image.tar
        --severity CRITICAL,HIGH,MEDIUM
        --format table
  artifacts:
    reports:
      container_scanning: trivy-report.json
    paths:
      - trivy-report.json
  allow_failure: false
 
push:
  stage: push
  needs: [trivy-scan]
  script:
    - docker load -i image.tar
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Step 4: Configure Trivy Ignore and Exception Handling

Manage false positives and accepted risks through Trivy's ignore file and VEX statements.

# .trivyignore.yaml
vulnerabilities:
  - id: CVE-2023-44487    # HTTP/2 rapid reset - mitigated at load balancer
    statement: "Mitigated by WAF rate limiting at ingress layer"
    expires: 2026-06-01
 
  - id: CVE-2024-21626    # runc container escape - patched in base image update
    statement: "Tracked in JIRA-SEC-1234, base image update scheduled"
    expires: 2026-03-15
 
misconfigurations:
  - id: DS002             # User not set - required for init containers
    paths:
      - "docker/init-container/Dockerfile"
    statement: "Init container requires root for volume permission setup"

Step 5: Implement Database Caching and Offline Scanning

Cache the Trivy vulnerability database in CI/CD to reduce scan times and enable air-gapped environments.

# GitHub Actions with database caching
- name: Cache Trivy DB
  uses: actions/cache@v4
  with:
    path: /tmp/trivy-db
    key: trivy-db-${{ hashFiles('.github/workflows/container-security.yml') }}
    restore-keys: trivy-db-
 
- name: Run Trivy with cached DB
  uses: aquasecurity/trivy-action@0.28.0
  with:
    image-ref: 'app:${{ github.sha }}'
    cache-dir: /tmp/trivy-db
    format: 'json'
    output: 'trivy-results.json'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'
# Air-gapped: Download DB manually and mount
trivy image --download-db-only --cache-dir /path/to/cache
# Transfer cache to air-gapped system
trivy image --skip-db-update --cache-dir /path/to/cache myimage:tag

Step 6: Generate SBOM and Scan for License Compliance

Use Trivy to generate Software Bill of Materials alongside vulnerability scanning.

# Generate SBOM in CycloneDX format
trivy image --format cyclonedx --output sbom.cdx.json app:latest
 
# Generate SBOM in SPDX format
trivy image --format spdx-json --output sbom.spdx.json app:latest
 
# Scan SBOM for vulnerabilities (decouple generation from scanning)
trivy sbom sbom.cdx.json --severity CRITICAL,HIGH
 
# Scan with license detection
trivy image --scanners vuln,license --severity HIGH,CRITICAL app:latest

Key Concepts

Term Definition
CVE Common Vulnerabilities and Exposures — standardized identifiers for publicly known security vulnerabilities
Vulnerability DB Trivy's regularly updated database aggregating CVE data from NVD, vendor advisories, and language-specific sources
Misconfiguration Security-relevant configuration issue in Dockerfiles, Kubernetes manifests, or IaC templates
SBOM Software Bill of Materials — complete inventory of all components and dependencies in a container image
Ignore Unfixed Flag to skip CVEs without available patches, reducing noise from vulnerabilities with no actionable fix
VEX Vulnerability Exploitability eXchange — machine-readable statements about whether a vulnerability is exploitable in context
Exit Code Non-zero return code from Trivy when findings exceed the severity threshold, used to fail CI/CD pipelines

Tools & Systems

  • Trivy: Open-source vulnerability scanner by Aqua Security supporting images, filesystems, repos, and IaC
  • trivy-action: Official GitHub Action for running Trivy scans in GitHub Actions workflows
  • Trivy Operator: Kubernetes operator that continuously scans cluster workloads with Trivy
  • Grype: Alternative image scanner by Anchore for comparison and validation of scan results
  • Harbor: Container registry with built-in Trivy integration for automatic image scanning on push

Common Scenarios

Scenario: Multi-Stage Build with Separate Scan and Push

Context: A team builds multi-stage Docker images and needs to scan the final production image before pushing to ECR, while also scanning the build stage for supply chain risks.

Approach:

  1. Build the Docker image with --target production for the final stage
  2. Run Trivy with --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed to block on exploitable issues
  3. Generate an SBOM in CycloneDX format and store as a build artifact
  4. Upload SARIF results to GitHub Security tab for visibility
  5. Only push to ECR if the Trivy scan exits with code 0
  6. Tag the pushed image with the scan timestamp and Trivy DB version for audit traceability

Pitfalls: Scanning only the final stage misses vulnerable packages that were present in build stages and may have influenced the build. Run trivy fs on the build context separately. Caching the Trivy DB too aggressively (weekly) means newly published CVEs take days to appear in scans.

Output Format

Trivy Container Scan Report
=============================
Image: app:a1b2c3d4
Base Image: python:3.12-slim-bookworm
Scan Date: 2026-02-23
DB Version: 2026-02-23T00:15:00Z
 
VULNERABILITY SUMMARY:
  Total: 47
  Critical: 2
  High: 5
  Medium: 18
  Low: 22
  Unfixed: 8 (excluded from gate)
 
CRITICAL FINDINGS:
  CVE-2025-12345  libssl3    3.0.11-1  3.0.13-1  OpenSSL buffer overflow
  CVE-2025-67890  curl       7.88.1-10 7.88.1-12 curl HSTS bypass
 
HIGH FINDINGS:
  CVE-2025-11111  zlib1g     1.2.13    1.2.13.1  zlib heap buffer overflow
  CVE-2025-22222  python3.12 3.12.1    3.12.3    CPython path traversal
  CVE-2025-33333  requests   2.31.0    2.32.0    requests SSRF in redirects
 
MISCONFIGURATION:
  DS002  [HIGH]   Dockerfile: USER instruction not set (running as root)
  DS026  [MEDIUM] Dockerfile: No HEALTHCHECK defined
 
QUALITY GATE: FAILED (2 Critical, 5 High findings)
Source materials

References and resources

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

References 3

api-reference.md1.9 KB

API Reference: Scanning Containers with Trivy in CI/CD

Trivy CLI Commands

Command Description
trivy image <ref> Scan container image for vulnerabilities
trivy config <path> Scan Dockerfiles/IaC for misconfigurations
trivy fs <path> Scan filesystem for vulnerabilities
trivy sbom <file> Scan existing SBOM for vulnerabilities
trivy image --format sarif SARIF output for GitHub Security
trivy image --format cyclonedx CycloneDX SBOM generation
trivy image --exit-code 1 Non-zero exit on findings

Scan Options

Flag Description
--severity CRITICAL,HIGH Filter by severity level
--ignore-unfixed Skip CVEs without patches
--scanners vuln,misconfig,secret Select scanner types
--format json/sarif/cyclonedx Output format
--exit-code 1 Fail pipeline on findings
--skip-db-update Use cached vulnerability DB
--cache-dir <path> Set database cache directory

CI/CD Integration

Platform Method
GitHub Actions aquasecurity/trivy-action@v0.28.0
GitLab CI aquasec/trivy:latest Docker image
Jenkins Trivy CLI in pipeline script
Azure DevOps Trivy CLI task

Quality Gate Severities

Level CVSS Range Default Gate Action
CRITICAL 9.0 - 10.0 Block deployment
HIGH 7.0 - 8.9 Block deployment
MEDIUM 4.0 - 6.9 Warn
LOW 0.1 - 3.9 Allow

Python Libraries

Library Version Purpose
subprocess stdlib Execute trivy CLI
json stdlib Parse scan results
pathlib stdlib Output file management

References

standards.md2.7 KB

Standards Reference: Container Scanning with Trivy

NIST SP 800-190 - Application Container Security Guide

Image Vulnerabilities (Section 3.1)

  • Scan all container images for known vulnerabilities before deployment
  • Establish organizational policies for maximum acceptable vulnerability severity
  • Monitor images in registries for newly discovered vulnerabilities
  • Maintain an up-to-date vulnerability database for scanning tools

Image Configuration Defects (Section 3.2)

  • Verify images follow CIS Docker Benchmark configuration guidelines
  • Ensure images run as non-root users unless operationally required
  • Remove unnecessary packages, shells, and utilities from production images

CIS Docker Benchmark v1.6.0

Image Level Controls

  • 4.1: Ensure a user for the container has been created (maps to Trivy DS002)
  • 4.2: Ensure containers use trusted base images
  • 4.3: Ensure unnecessary packages are not installed in the container
  • 4.6: Ensure HEALTHCHECK instructions have been added (maps to Trivy DS026)
  • 4.7: Ensure update instructions are not used alone in the Dockerfile
  • 4.9: Ensure COPY is used instead of ADD in Dockerfiles (maps to Trivy DS005)

OWASP Docker Security Cheat Sheet

Vulnerability Management

  • Scan images in the CI/CD pipeline before pushing to registries
  • Use --ignore-unfixed to focus on actionable vulnerabilities
  • Implement SBOM generation for full component visibility
  • Re-scan images on a schedule to catch newly published CVEs

Dockerfile Security

  • Use minimal base images (distroless, Alpine, slim variants)
  • Pin base image versions with digest for reproducibility
  • Run containers as non-root with explicit USER instruction
  • Use multi-stage builds to exclude build tools from production images

NIST SSDF (SP 800-218)

PW.4: Reuse Existing, Well-Secured Software

  • PW.4.1: Use automated tools to check for known vulnerabilities in dependencies
  • Map to Trivy's OS package and language dependency scanning capabilities

PS.1: Protect All Forms of Code

  • PS.1.1: Store all forms of code in a code repository protected by access controls
  • Container images in registries should be scanned and signed before use

SLSA Framework Alignment

Source Level

  • Trivy filesystem scanning validates source dependencies before build
  • Git repository scanning detects secrets and vulnerable dependencies in source

Build Level

  • Trivy image scanning validates the build output for vulnerabilities
  • SBOM generation creates a verifiable bill of materials for the built artifact

Deployment Level

  • Admission controllers can verify Trivy scan results before pod scheduling
  • Harbor registry integration enforces scan-before-pull policies
workflows.md4.2 KB

Workflow Reference: Container Scanning with Trivy in CI/CD

Container Security Scanning Pipeline

Source Code Push


┌──────────────────┐
│ Build Docker      │
│ Image             │
└──────┬───────────┘

       ├──────────────────────┐
       ▼                      ▼
┌──────────────┐    ┌──────────────┐
│ Trivy Image  │    │ Trivy Config │
│ Vuln Scan    │    │ Misconfig    │
└──────┬───────┘    └──────┬───────┘
       │                    │
       ▼                    ▼
┌──────────────┐    ┌──────────────┐
│ SARIF/JSON   │    │ Table/JSON   │
│ Output       │    │ Output       │
└──────┬───────┘    └──────┬───────┘
       │                    │
       └──────────┬─────────┘

       ┌──────────────────┐
       │ Quality Gate     │
       │ Evaluation       │
       └──────┬───────────┘

    ┌─────────┴──────────┐
    ▼                    ▼
 PASS: Push to         FAIL: Block
 Registry + Tag        + Alert Team


┌──────────────┐
│ Generate     │
│ SBOM + Sign  │
└──────────────┘

Trivy Scan Types Reference

Image Scanning

# Full scan (OS + language packages)
trivy image --severity CRITICAL,HIGH --exit-code 1 myimage:tag
 
# OS packages only
trivy image --vuln-type os myimage:tag
 
# Language-specific packages only
trivy image --vuln-type library myimage:tag
 
# From Docker archive
trivy image --input image.tar

Filesystem Scanning

# Scan project directory for vulnerable dependencies
trivy fs --severity HIGH,CRITICAL /path/to/project
 
# Scan specific lockfile
trivy fs --severity HIGH,CRITICAL requirements.txt

Repository Scanning

# Scan remote git repository
trivy repo https://github.com/org/repo
 
# Scan specific branch
trivy repo --branch develop https://github.com/org/repo

Configuration Scanning

# Scan Dockerfile and Kubernetes manifests
trivy config .
 
# Scan Terraform files
trivy config --tf-vars terraform.tfvars ./terraform/
 
# Scan Helm charts
trivy config ./charts/myapp/

Output Format Options

Format Use Case Flag
table Human-readable terminal output --format table
json Programmatic processing and storage --format json
sarif GitHub Security tab upload --format sarif
cyclonedx SBOM generation (CycloneDX) --format cyclonedx
spdx-json SBOM generation (SPDX) --format spdx-json
template Custom report format --format template --template @template.tpl
cosign-vuln Cosign attestation format --format cosign-vuln

Severity Threshold Matrix

Environment Block On Ignore Unfixed Rationale
Development CRITICAL Yes Fast feedback, focus on worst issues
Staging CRITICAL, HIGH Yes Catch more issues before production
Production CRITICAL, HIGH No Full visibility even for unfixed CVEs
Compliance ALL No Complete audit trail required

Database Management

Database Update Strategy

# Download DB only (for caching)
trivy image --download-db-only --cache-dir /shared/trivy-cache
 
# Skip DB update (use cached)
trivy image --skip-db-update --cache-dir /shared/trivy-cache myimage:tag
 
# Java DB for JAR scanning
trivy image --download-java-db-only --cache-dir /shared/trivy-cache

Cache Locations

  • Default: ~/.cache/trivy/
  • CI override: TRIVY_CACHE_DIR=/tmp/trivy-cache
  • GitHub Actions: Use actions/cache with key based on date

Scripts 2

agent.py6.1 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for scanning containers with Trivy in CI/CD pipelines.

Runs Trivy vulnerability and misconfiguration scans against
container images and Dockerfiles, enforces severity-based
quality gates, and generates CI/CD-compatible reports.
"""

import json
import subprocess
import sys
from pathlib import Path
from datetime import datetime


class TrivyCICDAgent:
    """Integrates Trivy scanning into CI/CD workflows."""

    def __init__(self, output_dir="./trivy_cicd"):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.scan_results = []

    def _run_trivy(self, subcmd, target, extra_args=None):
        """Execute trivy CLI and return parsed results."""
        cmd = ["trivy", subcmd, "--format", "json", "--quiet"]
        if extra_args:
            cmd.extend(extra_args)
        cmd.append(target)
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
            if result.stdout.strip():
                return json.loads(result.stdout)
            return {"error": result.stderr.strip() or "No output"}
        except FileNotFoundError:
            return {"error": "trivy not found. Install from https://trivy.dev"}
        except subprocess.TimeoutExpired:
            return {"error": "Trivy scan timed out after 600 seconds"}
        except json.JSONDecodeError:
            return {"error": "Failed to parse trivy JSON output"}

    def scan_image(self, image_ref, severity="CRITICAL,HIGH", ignore_unfixed=True):
        """Scan a container image for vulnerabilities."""
        args = ["--severity", severity]
        if ignore_unfixed:
            args.append("--ignore-unfixed")
        raw = self._run_trivy("image", image_ref, args)
        if "error" in raw:
            return raw

        vulns = []
        for result in raw.get("Results", []):
            for v in result.get("Vulnerabilities", []):
                vulns.append({
                    "id": v.get("VulnerabilityID", ""),
                    "severity": v.get("Severity", "UNKNOWN"),
                    "package": v.get("PkgName", ""),
                    "installed": v.get("InstalledVersion", ""),
                    "fixed": v.get("FixedVersion", ""),
                    "title": v.get("Title", ""),
                })

        summary = {}
        for v in vulns:
            summary[v["severity"]] = summary.get(v["severity"], 0) + 1

        scan = {
            "image": image_ref,
            "scan_type": "vulnerability",
            "scan_date": datetime.utcnow().isoformat(),
            "total": len(vulns),
            "summary": summary,
            "vulnerabilities": vulns,
        }
        self.scan_results.append(scan)
        return scan

    def scan_config(self, path, severity="CRITICAL,HIGH"):
        """Scan Dockerfiles or IaC for misconfigurations."""
        args = ["--severity", severity]
        raw = self._run_trivy("config", path, args)
        if "error" in raw:
            return raw

        misconfigs = []
        for result in raw.get("Results", []):
            for mc in result.get("Misconfigurations", []):
                misconfigs.append({
                    "id": mc.get("ID", ""),
                    "severity": mc.get("Severity", "UNKNOWN"),
                    "title": mc.get("Title", ""),
                    "message": mc.get("Message", ""),
                    "resolution": mc.get("Resolution", ""),
                })

        scan = {
            "path": path,
            "scan_type": "misconfiguration",
            "scan_date": datetime.utcnow().isoformat(),
            "total": len(misconfigs),
            "misconfigurations": misconfigs,
        }
        self.scan_results.append(scan)
        return scan

    def generate_sbom(self, image_ref, sbom_format="cyclonedx"):
        """Generate an SBOM for a container image."""
        out_file = self.output_dir / f"sbom.{sbom_format}.json"
        cmd = ["trivy", "image", "--format", sbom_format,
               "--output", str(out_file), image_ref]
        try:
            subprocess.run(cmd, capture_output=True, text=True, timeout=300)
            return {"sbom_path": str(out_file)}
        except (FileNotFoundError, subprocess.TimeoutExpired) as e:
            return {"error": str(e)}

    def enforce_quality_gate(self, severity_threshold="HIGH"):
        """Check if any scan result exceeds the severity threshold."""
        sev_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
        threshold_idx = sev_order.index(severity_threshold) if severity_threshold in sev_order else 0
        blocking_sevs = sev_order[:threshold_idx + 1]

        for scan in self.scan_results:
            summary = scan.get("summary", {})
            for sev in blocking_sevs:
                if summary.get(sev, 0) > 0:
                    return {"gate": "FAILED", "reason": f"{summary[sev]} {sev} findings in {scan.get('image', scan.get('path', ''))}"}
        return {"gate": "PASSED"}

    def generate_report(self):
        gate = self.enforce_quality_gate()
        report = {
            "report_date": datetime.utcnow().isoformat(),
            "scans": self.scan_results,
            "quality_gate": gate,
        }
        out = self.output_dir / "trivy_cicd_report.json"
        with open(out, "w") as f:
            json.dump(report, f, indent=2)
        print(json.dumps(report, indent=2))
        return report


def main():
    if len(sys.argv) < 2:
        print("Usage: agent.py <image_ref> [--config <path>] [--severity CRITICAL,HIGH]")
        sys.exit(1)

    agent = TrivyCICDAgent()
    image = sys.argv[1]
    severity = "CRITICAL,HIGH"
    if "--severity" in sys.argv:
        idx = sys.argv.index("--severity")
        if idx + 1 < len(sys.argv):
            severity = sys.argv[idx + 1]

    agent.scan_image(image, severity=severity)

    if "--config" in sys.argv:
        idx = sys.argv.index("--config")
        if idx + 1 < len(sys.argv):
            agent.scan_config(sys.argv[idx + 1], severity=severity)

    report = agent.generate_report()
    if report["quality_gate"]["gate"] == "FAILED":
        sys.exit(1)


if __name__ == "__main__":
    main()
process.py11.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Trivy Container Scanning Pipeline Script

Scans Docker images with Trivy, evaluates quality gates, generates reports,
and optionally uploads results. Supports both local scanning and CI/CD integration.

Usage:
    python process.py --image myapp:latest --severity-threshold high
    python process.py --image myapp:latest --output report.json --fail-on-findings
    python process.py --dockerfile ./Dockerfile --scan-config
"""

import argparse
import json
import os
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional


SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4}


@dataclass
class Vulnerability:
    vuln_id: str
    pkg_name: str
    installed_version: str
    fixed_version: str
    severity: str
    title: str
    description: str
    cvss_score: float = 0.0
    references: list = field(default_factory=list)


@dataclass
class Misconfiguration:
    misconfig_id: str
    title: str
    severity: str
    message: str
    resolution: str
    file_path: str = ""


@dataclass
class ScanResult:
    image: str
    vulnerabilities: list = field(default_factory=list)
    misconfigurations: list = field(default_factory=list)
    scan_duration: float = 0.0
    db_version: str = ""
    trivy_version: str = ""
    error: str = ""


def run_trivy_scan(image: str, severity: str = "CRITICAL,HIGH,MEDIUM,LOW",
                   ignore_unfixed: bool = True, scan_type: str = "image",
                   cache_dir: Optional[str] = None, skip_db_update: bool = False) -> dict:
    """Execute Trivy scan and return JSON results."""
    cmd = ["trivy", scan_type]

    if scan_type == "image":
        cmd.extend([
            "--format", "json",
            "--severity", severity,
        ])
        if ignore_unfixed:
            cmd.append("--ignore-unfixed")
        if cache_dir:
            cmd.extend(["--cache-dir", cache_dir])
        if skip_db_update:
            cmd.append("--skip-db-update")
        cmd.append(image)
    elif scan_type == "config":
        cmd.extend([
            "--format", "json",
            "--severity", severity,
            image
        ])

    try:
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=600
        )

        if proc.stdout:
            return json.loads(proc.stdout)
        else:
            return {"error": proc.stderr[:500]}

    except subprocess.TimeoutExpired:
        return {"error": "Trivy scan timed out after 600 seconds"}
    except FileNotFoundError:
        return {"error": "trivy binary not found. Install from https://trivy.dev"}
    except json.JSONDecodeError:
        return {"error": "Failed to parse Trivy JSON output"}


def parse_vulnerabilities(trivy_json: dict) -> list:
    """Extract vulnerability findings from Trivy JSON output."""
    vulns = []
    for result in trivy_json.get("Results", []):
        for vuln in result.get("Vulnerabilities", []):
            vulns.append(Vulnerability(
                vuln_id=vuln.get("VulnerabilityID", ""),
                pkg_name=vuln.get("PkgName", ""),
                installed_version=vuln.get("InstalledVersion", ""),
                fixed_version=vuln.get("FixedVersion", ""),
                severity=vuln.get("Severity", "UNKNOWN"),
                title=vuln.get("Title", ""),
                description=vuln.get("Description", "")[:300],
                cvss_score=vuln.get("CVSS", {}).get("nvd", {}).get("V3Score", 0.0),
                references=vuln.get("References", [])[:3]
            ))
    return vulns


def parse_misconfigurations(trivy_json: dict) -> list:
    """Extract misconfiguration findings from Trivy JSON output."""
    misconfigs = []
    for result in trivy_json.get("Results", []):
        for mc in result.get("Misconfigurations", []):
            if mc.get("Status") == "FAIL":
                misconfigs.append(Misconfiguration(
                    misconfig_id=mc.get("ID", ""),
                    title=mc.get("Title", ""),
                    severity=mc.get("Severity", "UNKNOWN"),
                    message=mc.get("Message", ""),
                    resolution=mc.get("Resolution", ""),
                    file_path=result.get("Target", "")
                ))
    return misconfigs


def generate_sarif(scan_result: ScanResult) -> dict:
    """Generate SARIF format output for GitHub Security tab integration."""
    rules = []
    results = []
    rule_ids_seen = set()

    for vuln in scan_result.vulnerabilities:
        if vuln.vuln_id not in rule_ids_seen:
            rule_ids_seen.add(vuln.vuln_id)
            severity_map = {"CRITICAL": "9.5", "HIGH": "7.5", "MEDIUM": "5.0", "LOW": "2.5"}
            rules.append({
                "id": vuln.vuln_id,
                "shortDescription": {"text": vuln.title or vuln.vuln_id},
                "fullDescription": {"text": vuln.description[:500]},
                "properties": {
                    "security-severity": str(vuln.cvss_score or severity_map.get(vuln.severity, "5.0")),
                    "tags": ["security", "vulnerability", vuln.severity.lower()]
                }
            })

        level_map = {"CRITICAL": "error", "HIGH": "error", "MEDIUM": "warning", "LOW": "note"}
        results.append({
            "ruleId": vuln.vuln_id,
            "level": level_map.get(vuln.severity, "warning"),
            "message": {
                "text": f"{vuln.pkg_name} {vuln.installed_version} -> {vuln.fixed_version}: {vuln.title}"
            },
            "locations": [{
                "physicalLocation": {
                    "artifactLocation": {"uri": "Dockerfile"},
                    "region": {"startLine": 1}
                }
            }]
        })

    return {
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "Trivy",
                    "informationUri": "https://trivy.dev",
                    "rules": rules
                }
            },
            "results": results
        }]
    }


def evaluate_quality_gate(scan_result: ScanResult, threshold: str) -> dict:
    """Evaluate whether scan results pass the quality gate."""
    threshold_level = SEVERITY_ORDER.get(threshold.upper(), 1)

    blocking_vulns = [
        v for v in scan_result.vulnerabilities
        if SEVERITY_ORDER.get(v.severity, 4) <= threshold_level
    ]

    blocking_misconfigs = [
        m for m in scan_result.misconfigurations
        if SEVERITY_ORDER.get(m.severity, 4) <= threshold_level
    ]

    severity_counts = {}
    for v in scan_result.vulnerabilities:
        severity_counts[v.severity] = severity_counts.get(v.severity, 0) + 1

    return {
        "passed": len(blocking_vulns) == 0 and len(blocking_misconfigs) == 0,
        "threshold": threshold.upper(),
        "total_vulnerabilities": len(scan_result.vulnerabilities),
        "total_misconfigurations": len(scan_result.misconfigurations),
        "blocking_vulnerabilities": len(blocking_vulns),
        "blocking_misconfigurations": len(blocking_misconfigs),
        "severity_counts": severity_counts,
        "blocking_details": [
            {"id": v.vuln_id, "pkg": v.pkg_name, "severity": v.severity,
             "installed": v.installed_version, "fixed": v.fixed_version}
            for v in blocking_vulns[:20]
        ]
    }


def main():
    parser = argparse.ArgumentParser(description="Trivy Container Scanning Pipeline")
    parser.add_argument("--image", required=True, help="Docker image to scan (e.g., myapp:latest)")
    parser.add_argument("--output", default="trivy-report.json", help="Output report file path")
    parser.add_argument("--sarif-output", default=None, help="SARIF output file path")
    parser.add_argument("--severity-threshold", default="high",
                        choices=["critical", "high", "medium", "low"],
                        help="Minimum severity to block pipeline")
    parser.add_argument("--fail-on-findings", action="store_true",
                        help="Exit non-zero if quality gate fails")
    parser.add_argument("--ignore-unfixed", action="store_true", default=True,
                        help="Ignore vulnerabilities without fixes")
    parser.add_argument("--scan-config", action="store_true",
                        help="Also scan for misconfigurations")
    parser.add_argument("--dockerfile", default=None,
                        help="Path to Dockerfile for misconfiguration scanning")
    parser.add_argument("--cache-dir", default=None, help="Trivy cache directory")
    parser.add_argument("--skip-db-update", action="store_true", help="Skip DB update")
    args = parser.parse_args()

    scan_result = ScanResult(image=args.image)
    start_time = datetime.now(timezone.utc)

    print(f"[*] Scanning image: {args.image}")
    trivy_json = run_trivy_scan(
        args.image,
        ignore_unfixed=args.ignore_unfixed,
        cache_dir=args.cache_dir,
        skip_db_update=args.skip_db_update
    )

    if "error" in trivy_json:
        print(f"[ERROR] {trivy_json['error']}")
        sys.exit(2)

    scan_result.vulnerabilities = parse_vulnerabilities(trivy_json)
    scan_result.db_version = trivy_json.get("Metadata", {}).get("DB", {}).get("UpdatedAt", "unknown")

    if args.scan_config and args.dockerfile:
        print(f"[*] Scanning configuration: {args.dockerfile}")
        config_path = os.path.dirname(args.dockerfile) or "."
        config_json = run_trivy_scan(config_path, scan_type="config")
        if "error" not in config_json:
            scan_result.misconfigurations = parse_misconfigurations(config_json)

    scan_result.scan_duration = (datetime.now(timezone.utc) - start_time).total_seconds()

    quality_gate = evaluate_quality_gate(scan_result, args.severity_threshold)

    report = {
        "scan_metadata": {
            "image": args.image,
            "scan_date": datetime.now(timezone.utc).isoformat(),
            "duration_seconds": scan_result.scan_duration,
            "db_version": scan_result.db_version
        },
        "quality_gate": quality_gate,
        "vulnerabilities": [
            {
                "id": v.vuln_id, "package": v.pkg_name, "severity": v.severity,
                "installed": v.installed_version, "fixed": v.fixed_version,
                "title": v.title, "cvss": v.cvss_score
            }
            for v in sorted(scan_result.vulnerabilities,
                            key=lambda x: SEVERITY_ORDER.get(x.severity, 4))
        ],
        "misconfigurations": [
            {"id": m.misconfig_id, "title": m.title, "severity": m.severity,
             "message": m.message, "resolution": m.resolution}
            for m in scan_result.misconfigurations
        ]
    }

    with open(os.path.abspath(args.output), "w") as f:
        json.dump(report, f, indent=2)
    print(f"[*] Report: {os.path.abspath(args.output)}")

    if args.sarif_output:
        sarif = generate_sarif(scan_result)
        with open(os.path.abspath(args.sarif_output), "w") as f:
            json.dump(sarif, f, indent=2)
        print(f"[*] SARIF: {os.path.abspath(args.sarif_output)}")

    print(f"\n[*] Vulnerabilities: {len(scan_result.vulnerabilities)} "
          f"| Misconfigs: {len(scan_result.misconfigurations)}")

    if quality_gate["passed"]:
        print(f"[PASS] Quality gate passed.")
    else:
        print(f"[FAIL] Quality gate failed. {quality_gate['blocking_vulnerabilities']} blocking vulns.")
        for d in quality_gate["blocking_details"][:10]:
            print(f"  - [{d['severity']}] {d['id']} in {d['pkg']} ({d['installed']} -> {d['fixed']})")

    if args.fail_on_findings and not quality_gate["passed"]:
        sys.exit(1)


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 4.8 KB
Keep exploring