Skip to main content

Aggregate and deduplicate vulnerability scan reports from Grype and Trivy

Project description

CVE Report Aggregation and Deduplication Tool

Python Version Python Version PyPI version PyPI downloads uv License CI codecov Latest Release Docker Code style: ruff

CVE Report Aggregator Logo

A Python package for aggregating and deduplicating Grype and Trivy vulnerability scan reports, extracted from Zarf packages. Optionally enrich CVE data using AI models via a pluggable provider architecture to provide actionable mitigation summaries in the context of UDS Core security controls.

[!NOTE] The enrichment module supports a pluggable provider architecture with four providers: OpenRouter (default, synchronous), Anthropic (Messages Batch API, 50% cost savings), OpenAI (Batch API, 50% cost savings), and Pipeline (fan-out/fan-in across multiple providers with optional reviewer stage).

Quick Start

Using Docker (Recommended)

# Pull the latest signed image
docker pull ghcr.io/mkm29/cve-report-aggregator:latest

# Process local reports with default settings
docker run --rm \
  -v $(pwd)/reports:/home/cve-aggregator/reports:ro \
  -v $(pwd)/output:/home/cve-aggregator/output \
  ghcr.io/mkm29/cve-report-aggregator:latest

# Output: $HOME/output/<package>-<version>.json

Online Scans (Remote Packages)

For scanning remote packages from a registry, use a single archive mount to retrieve all artifacts:

docker run --rm \
  -v ./.cve-aggregator.yaml:/home/cve-aggregator/.cve-aggregator.yaml \
  -v ./archive:/home/cve-aggregator/archive \
  -e REGISTRY_URL="$UDS_URL" \
  -e UDS_USERNAME="$UDS_USERNAME" \
  -e UDS_PASSWORD="$UDS_PASSWORD" \
  -e SKIP_UPDATE_VULNDB_DB="true" \
  ghcr.io/mkm29/cve-report-aggregator:latest

[!NOTE] SKIP_UPDATE_VULNDB_DB is optional. Set to "true" to skip updating the vulnerability database(s) (useful for air-gapped environments or faster execution).

Output: All artifacts are bundled into ./archive/artifacts.tar.gz, including:

  • SBOM files (from zarf package inspect sbom)
  • Aggregated vulnerability reports (JSON)
  • CSV exports
  • Executive summary

Using uv (Local Installation)

# Install
uv tool install cve-report-aggregator

# Process reports from ./reports/
cve-report-aggregator

# Output: $HOME/output/unified-YYYYMMDDhhmmss.json

Common Usage Patterns

# Use Trivy scanner instead of Grype
cve-report-aggregator --scanner trivy

# Run both scanners and combine results
cve-report-aggregator --scanner both

# Enable AI-powered CVE enrichment (set the API key for your provider)
export OPENROUTER_API_KEY=sk-or-...   # for openrouter (default)
# export ANTHROPIC_API_KEY=sk-ant-...  # for anthropic
# export OPENAI_API_KEY=sk-...         # for openai
cve-report-aggregator --enrich-cves

# Process with custom input directory and verbose logging
cve-report-aggregator -i /path/to/reports --log-level DEBUG

# Use highest severity across multiple scans
cve-report-aggregator --mode highest-score

Features

  • Self-Contained Docker Image: Includes all scanning tools (Grype, Syft, Trivy, UDS CLI) in a single hardened Alpine-based image
  • Supply Chain Security: SLSA Level 3 compliant with signed images, SBOMs, and provenance attestations
  • AI-Powered CVE Enrichment: Pluggable provider architecture supporting OpenRouter (default), Anthropic (Messages Batch API), OpenAI (Batch API), and Pipeline (multi-provider fan-out/fan-in with optional reviewer)
  • Production-Ready Package: Installable via pip/uv with proper dependency management
  • Rich Terminal Output: Beautiful, color-coded tables and progress indicators using the Rich library
  • Multi-Scanner Support: Works with both Grype and Trivy scanners, or run both simultaneously with the both option
  • Scanner Source Tracking: Each vulnerability includes metadata showing which scanner(s) detected it
  • SBOM Auto-Scan: Automatically detects and scans Syft SBOM files with Grype
  • Auto-Conversion: Automatically converts Grype reports to CycloneDX format for Trivy scanning
  • CVE Deduplication: Combines identical vulnerabilities across multiple scans
  • Automatic Null CVSS Filtering: Filters out invalid CVSS scores (null, N/A, or zero) from all vulnerability reports
  • CVSS 3.x-Based Severity Selection: Optional mode to select highest severity based on actual CVSS 3.x base scores
  • Occurrence Tracking: Counts how many times each CVE appears
  • Flexible CLI: Click-based interface with rich-click styling and sensible defaults
  • Security Hardened: Non-root user (UID 1001), minimal Alpine base, pinned dependencies, and vulnerability-scanned

Table of Contents

Installation

Docker (Recommended)

All scanning tools are included in the image -- no prerequisites needed.

docker pull ghcr.io/mkm29/cve-report-aggregator:latest

docker run --rm \
  -v $(pwd)/reports:/home/cve-aggregator/reports:ro \
  -v $(pwd)/output:/home/cve-aggregator/output \
  ghcr.io/mkm29/cve-report-aggregator:latest

See the Docker Guide for building from source, Docker Compose, image tags, attestation verification, and credentials management.

From PyPI

pip install cve-report-aggregator

# Or install with uv (recommended)
uv tool install cve-report-aggregator

From Source

git clone https://github.com/mkm29/cve-report-aggregator.git
cd cve-report-aggregator
pip install -e ".[dev]"

Prerequisites (Local Installation)

Docker users need no prerequisites. For local installation, you need uv and at least one scanner:

  • Grype -- default scanner
  • Trivy -- alternative scanner
  • Syft -- required for Trivy workflow (CycloneDX conversion)

Configuration

CVE Report Aggregator supports flexible configuration through multiple sources with the following precedence (highest to lowest):

  1. CLI Arguments -- command-line flags and options
  2. YAML Configuration File -- .cve-aggregator.yaml or .cve-aggregator.yml
  3. Environment Variables -- prefixed with CVE_AGGREGATOR_
  4. Default Values

CLI Options

Option Short Description Default
--input-dir -i Input directory containing scan reports or SBOMs ./reports
--scanner -s Scanner type to process (grype, trivy, or both) grype
--log-level -l Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) INFO
--mode -m Aggregation mode: highest-score, first-occurrence, grype-only, trivy-only highest-score
--archive-dir Directory for tarball archive output (bundles all artifacts into a single .tar.gz) None
--enrich-cves Enable CVE enrichment with AI provider (openrouter, anthropic, openai, pipeline) false
--openai-api-key OpenAI API key (defaults to OPENAI_API_KEY env var) None
--openai-model OpenAI model to use for enrichment x-ai/grok-code-fast-1
--openai-reasoning-effort Reasoning effort level (low, medium, high) medium
--max-cves-to-enrich Maximum number of CVEs to enrich None (all)
--enrich-severity-filter Severity levels to enrich (can be used multiple times) Critical, High
--help -h Show help message and exit N/A
--version Show version and exit N/A

YAML Configuration File

Create a .cve-aggregator.yaml or .cve-aggregator.yml file in your project directory:

# Scanner and processing settings
scanner: grype                          # Scanner type: grype, trivy, or both
mode: highest-score                     # Aggregation mode
log_level: INFO                         # Logging level

input_dir: ./reports                    # Input directory for reports

# Parallel processing
maxWorkers: 14                          # Concurrent download workers (auto-detect if omitted)

# Archive output (bundles all artifacts into a tarball)
# archiveDir: /path/to/archive          # When set, creates artifacts.tar.gz

# Remote package downloads
downloadRemotePackages: true            # Enable remote SBOM downloads
registry: registry.defenseunicorns.com
organization: sld-45
packages:
  - name: gitlab
    version: 18.4.2-uds.0-unicorn
    architecture: amd64
  - name: gitlab-runner
    version: 18.4.0-uds.0-unicorn
    architecture: amd64

# CVE Enrichment (Pluggable Provider Architecture)
enrich:
  enabled: true
  provider: openrouter  # Provider: openrouter | anthropic | openai | pipeline
  model: x-ai/grok-code-fast-1  # Model identifier (provider-specific)
  # API key is auto-detected per provider:
  #   openrouter  -> OPENROUTER_API_KEY
  #   anthropic   -> ANTHROPIC_API_KEY
  #   openai      -> OPENAI_API_KEY
  #   pipeline    -> per-stage provider key
  # apiKey: YOUR_API_KEY_HERE  # or set via the env var above
  reasoningEffort: medium  # Level of reasoning effort: minimal, low, medium, high
  pollInterval: 30  # Batch API poll interval in seconds (5-300, default 30)
  severities:  # Severity levels to enrich
    - Critical
    - High
  verbosity: medium  # Verbosity level: low, medium, high
  seed: 42  # Optional: Seed for reproducibility
  maxWorkers: 5  # Concurrent API requests (1-20)
  metadata:  # Optional: Metadata tags for requests
    project: cve-report-aggregator
    organization: defenseunicorns

  # Pipeline mode (multi-provider fan-out/fan-in)
  # pipelineStages:
  #   - provider: anthropic
  #     model: claude-sonnet-4-5-20241022
  #   - provider: openai
  #     model: gpt-4o
  # reviewer: true  # When true, last pipeline stage reviews all prior stages' outputs

Pipeline Enrichment Configuration

For multi-provider enrichment using the pipeline provider:

# Pipeline enrichment (multi-provider)
enrich:
  enabled: true
  provider: pipeline
  pipelineStages:
    - provider: anthropic
      model: claude-sonnet-4-5-20241022
    - provider: openai
      model: gpt-4o
  reviewer: true  # Last stage reviews all prior stages' outputs

The Pipeline constructor takes an enrichers list (one per stage). Each stage runs concurrently, and when reviewer is true the last stage acts as a reviewer that synthesizes outputs from all prior stages.

See .cve-aggregator.example.yaml for a complete example.

Package Sources: Local vs Remote

CVE Report Aggregator supports two package sources with intelligent fallback behavior:

1. Local Packages (Priority)

If a ./packages/ directory exists with Zarf package archives (.tar.zst files), the application will automatically use them:

# Directory structure
./packages/
├── zarf-package-gitlab-amd64-18.4.2-uds.0-unicorn.tar.zst
├── zarf-package-gitlab-runner-amd64-18.4.0-uds.0-unicorn.tar.zst
└── zarf-package-headlamp-amd64-0.35.0-uds.0-registry1.tar.zst

# Run with local packages (no configuration needed)
cve-report-aggregator

Benefits:

  • No configuration required - automatically detected
  • Package metadata extracted from archives using zarf package inspect
  • Faster than remote downloads (no network I/O)
  • Works in air-gapped environments

Note: Zarf init packages (zarf-init-*.tar.zst) are automatically excluded as they contain infrastructure components.

2. Remote Packages (Fallback)

Remote packages are downloaded only if no local packages are found in ./packages/:

# .cve-aggregator.yaml
downloadRemotePackages: true
registry: registry.defenseunicorns.com
organization: sld-45
packages:
  - name: gitlab
    version: 18.4.2-uds.0-unicorn
    architecture: amd64

Requirements:

  • downloadRemotePackages: true in configuration
  • Registry and organization configured
  • UDS Zarf CLI installed and authenticated
  • Package list with name, version, and architecture

3. Local-Only Mode

To disable remote downloads entirely (useful for air-gapped environments):

# .cve-aggregator.yaml
localOnly: true  # Skip remote downloads even if no local packages found

Behavior Summary

Scenario Local Packages? downloadRemotePackages Behavior
Local packages exist Any value Use local packages
No local packages + remote enabled true Download remote packages
No local packages + remote disabled false Process existing reports in ./reports/
Local-only mode enabled Any Any value Use local packages only, skip remote

Priority Order:

  1. Local packages in ./packages/ (if exist)
  2. Remote package downloads (if downloadRemotePackages: true and no local packages)
  3. Existing reports in ./reports/ (fallback)

Environment Variables

All configuration options can be set via environment variables with the CVE_AGGREGATOR_ prefix (with the exception of the provider API keys OPENROUTER_API_KEY, ANTHROPIC_API_KEY, and OPENAI_API_KEY, which have no prefix). For example:

# Scanner settings
export CVE_AGGREGATOR_SCANNER=grype
export CVE_AGGREGATOR_MODE=highest-score
export CVE_AGGREGATOR_LOG_LEVEL=DEBUG

# Input/output
export CVE_AGGREGATOR_INPUT_DIR=/path/to/reports
export CVE_AGGREGATOR_OUTPUT_FILE=/path/to/output.json

# Parallel processing
export CVE_AGGREGATOR_MAX_WORKERS=14

# Archive output
export CVE_AGGREGATOR_ARCHIVE_DIR=/path/to/archive

# Remote packages
export CVE_AGGREGATOR_DOWNLOAD_REMOTE_PACKAGES=true
export CVE_AGGREGATOR_REGISTRY=registry.example.com
export CVE_AGGREGATOR_ORGANIZATION=my-org

# CVE Enrichment
export OPENROUTER_API_KEY=sk-or-...                     # OpenRouter API key (no prefix)
export ANTHROPIC_API_KEY=sk-ant-...                     # Anthropic API key (no prefix)
export OPENAI_API_KEY=sk-...                            # OpenAI API key (no prefix)
export CVE_AGGREGATOR_ENRICH_CVES=true
export CVE_AGGREGATOR_OPENAI_MODEL=x-ai/grok-code-fast-1
export CVE_AGGREGATOR_OPENAI_REASONING_EFFORT=medium
export CVE_AGGREGATOR_MAX_CVES_TO_ENRICH=50

Configuration Examples

Basic Usage with Defaults

# Process reports from ./reports/ with default settings
cve-report-aggregator

# Output: $HOME/output/unified-YYYYMMDDhhmmss.json

Custom Scanner and Verbosity

# Use Trivy scanner with debug logging
cve-report-aggregator --scanner trivy --log-level DEBUG

# Run both scanners and combine results
cve-report-aggregator --scanner both --log-level DEBUG

CVE Enrichment

# Enable AI-powered enrichment for Critical and High CVEs
export OPENROUTER_API_KEY=sk-or-...  # or ANTHROPIC_API_KEY / OPENAI_API_KEY
cve-report-aggregator --enrich-cves

# Customize enrichment settings
cve-report-aggregator \
  --enrich-cves \
  --openai-model gpt-4o \
  --openai-reasoning-effort high \
  --max-cves-to-enrich 10 \
  --enrich-severity-filter Critical

Remote Package Downloads

# .cve-aggregator.yaml
downloadRemotePackages: true
registry: registry.defenseunicorns.com
organization: sld-45
maxWorkers: 14
packages:
  - name: gitlab
    version: 18.4.2-uds.0-unicorn
# Run with config file
cve-report-aggregator --config .cve-aggregator.yaml

Prerequisites

This depends on how you plan to use CVE Report Aggregator. The recommended method is via the Docker image, which includes all necessary tooling. In which case, the only prerequisite is having Docker installed.

For running from source, you will need uv. Additionally, you will need at least one of the following scanners installed, depending on your workflow: You can expect the following performance improvements when utilizing parallel downloads (ThreadPoolExecutor):

  • ~10-15 seconds for 14 packages
  • A 10-14x speedup compared to sequential downloads (which can take ~150s for 14 packages)

Auto-Detection: If maxWorkers is not specified, the optimal worker count is automatically detected using the formula: min(<number_of_packages>, cpu_cores * 2 - 2). Set to 1 to disable parallelization.

Thread Safety: All parallel operations use thread-safe data structures (Lock()) to ensure data integrity across concurrent workers.

Pipeline Architecture

CVE Report Aggregator uses a package-first download architecture where Zarf packages are downloaded in parallel, each yielding multiple SBOM files (one per container image). These SBOMs are then processed with the selected scanner (Grype or Trivy), followed by aggregation, deduplication, and optional CVE enrichment.

Part 1: Download & Scanning Pipeline

The first stage handles parallel package downloads and vulnerability scanning in a 3-row hybrid layout:

flowchart TB
    %% Row 1: Package Downloads (Force Horizontal Layout with invisible connections)
    subgraph downloads[" "]
        direction LR
        pkg1[gitlab]
        pkg2[gitlab-runner]
        pkg3[headlamp]
        pkg4[metrics-server]

        pkg1 ~~~ pkg2 ~~~ pkg3 ~~~ pkg4
    end

    %% Invisible connector for spacing
    downloads --> collector

    %% Row 2: Two-Column Processing
    subgraph processing[" "]
        direction LR

        %% Left Column
        subgraph collector["SBOM Collection"]
            direction TB
            extract[Extract SBOMs]:::sbomStyle
            detect{Detect Format<br/>}:::decisionStyle
            select[Scanner Selection]:::decisionStyle
            extract --> detect --> select
        end

        %% Right Column
        subgraph scanners["Scanner Execution"]
            direction TB

            subgraph grype["Grype Path"]
                direction LR
                g1[Direct Scan<br/>grype sbom:file]:::grypeStyle
                g2[Grype<br/>Reports]:::grypeStyle
                g1 --> g2
            end

            subgraph trivy["Trivy Path"]
                direction LR
                t1[Convert<br/>syft convert]:::trivyStyle
                t2[Scan<br/>trivy sbom]:::trivyStyle
                t3[Trivy<br/>Reports]:::trivyStyle
                t1 --> t2 --> t3
            end
        end

        select -.Grype.-> grype
        select -.Trivy.-> trivy
    end

    %% Row 3: Output
    subgraph output_row["Output"]
        output[Scan Reports Ready<br/>for Aggregation]:::transitionStyle
    end

    %% Connect to output
    g2 --> output
    t3 --> output

    %% Style Definitions
    classDef downloadStyle fill:#1976d2,stroke:#0d47a1,color:#fff,stroke-width:2px
    classDef sbomStyle fill:#7b1fa2,stroke:#4a148c,color:#fff,stroke-width:2px
    classDef decisionStyle fill:#ffa000,stroke:#ff6f00,color:#000,stroke-width:2px
    classDef grypeStyle fill:#d32f2f,stroke:#b71c1c,color:#fff,stroke-width:2px
    classDef trivyStyle fill:#f57c00,stroke:#e65100,color:#fff,stroke-width:2px
    classDef transitionStyle fill:#546e7a,stroke:#37474f,color:#fff,stroke-width:2px

    class pkg1,pkg2,pkg3,pkg4 downloadStyle

Part 2: Aggregation & Output Pipeline

The second stage handles deduplication, enrichment, and report generation:

flowchart TB
    INPUT1[Scan Reports<br/>From Scanner]

    subgraph agg["5. Aggregation & Deduplication"]
        direction TB
        MERGE[Merge Reports<br/>GHSA → CVE Conversion]
        DEDUP[Deduplicate by CVE ID]
        SEVERITY{Severity Mode?}
        FIRST[First Occurrence<br/>Use First Report]
        HIGHEST[Highest Score<br/>Compare CVSS 3.x]
    end

    subgraph enrich["6. CVE Enrichment<br/>(Optional)"]
        direction TB
        FILTER[Filter by Severity<br/>Default: Critical + High]
        ENRICH_CHECK{enrich-cves<br/>enabled?}
        FACTORY[EnricherFactory<br/>Provider Selection]
        PROVIDER[AI Provider<br/>OpenRouter/Anthropic/OpenAI/Pipeline/Mock]
        MITIGATION[Add Mitigation<br/>Summaries]
    end

    subgraph output["7. Report Generation"]
        direction TB
        JSON1[gitlab-VERSION.json]
        CSV1[gitlab-VERSION.csv]
        JSON2[gitlab-runner-VERSION.json]
        CSV2[gitlab-runner-VERSION.csv]
        SUMMARY[executive-summary.json]
    end

    INPUT1 --> MERGE
    MERGE --> DEDUP
    DEDUP --> SEVERITY

    SEVERITY -->|first-occurrence| FIRST
    SEVERITY -->|highest-score| HIGHEST

    FIRST --> FILTER
    HIGHEST --> FILTER

    FILTER --> ENRICH_CHECK
    ENRICH_CHECK -->|Yes| FACTORY
    ENRICH_CHECK -->|No| JSON1

    FACTORY --> PROVIDER
    PROVIDER --> MITIGATION
    MITIGATION --> JSON1

    JSON1 --> CSV1
    JSON1 --> JSON2
    JSON2 --> CSV2
    JSON2 --> SUMMARY

    classDef transitionStyle fill:#546e7a,stroke:#37474f,color:#fff,stroke-width:2px
    classDef aggStyle fill:#388e3c,stroke:#1b5e20,color:#fff,stroke-width:2px
    classDef decisionStyle fill:#ffa000,stroke:#ff6f00,color:#000,stroke-width:2px
    classDef enrichStyle fill:#5e35b1,stroke:#311b92,color:#fff,stroke-width:2px
    classDef factoryStyle fill:#7b1fa2,stroke:#4a148c,color:#fff,stroke-width:2px
    classDef outputStyle fill:#00796b,stroke:#004d40,color:#fff,stroke-width:2px

    class INPUT1 transitionStyle
    class MERGE,DEDUP,FIRST,HIGHEST aggStyle
    class SEVERITY,ENRICH_CHECK decisionStyle
    class FILTER,MITIGATION enrichStyle
    class FACTORY,PROVIDER factoryStyle
    class JSON1,CSV1,JSON2,CSV2,SUMMARY outputStyle

Processing Pipeline Overview

The complete processing pipeline consists of seven stages:

  1. Parallel Package Downloads (ThreadPoolExecutor):

    • Downloads Zarf packages concurrently using UDS CLI
    • Worker count: min(num_packages, cpu_count * 2 - 2)
    • Each package extracts multiple SBOM files (one per container image)
  2. SBOM Collection:

    • Gathers all extracted SBOM files
    • Auto-detects SBOM format (checks for artifacts + descriptor fields)
  3. Scanner Selection:

    • Grype (default): Direct SBOM scanning
    • Trivy: CycloneDX conversion then scanning
    • Both: Runs Grype first, then Trivy (with CycloneDX conversion), and combines results
  4. Scanner Processing:

    • Grype Path: grype sbom:<file> -o json → Grype reports
    • Trivy Path: syft convert <file> -o cyclonedx-jsontrivy sbom <file> -f json → Trivy reports
    • Both Path: Runs both scanners sequentially and merges results with source tracking
  5. Aggregation & Deduplication:

    • Merge all scan reports
    • Convert GHSA IDs to CVE IDs (preferred for standardization)
    • Deduplicate by CVE ID
    • Track scanner sources for each vulnerability
    • Severity Mode Selection:
      • first-occurrence: Use severity from first report (default)
      • highest-score: Compare CVSS 3.x scores, select highest
  6. CVE Enrichment (optional, requires --enrich-cves flag):

    • Filter by severity (default: Critical + High)
    • Use EnricherFactory to create provider based on configuration
    • Analyze with AI provider (OpenRouter, Anthropic, OpenAI, or Pipeline) using UDS Core security context
    • Anthropic and OpenAI batch providers offer 50% cost savings vs synchronous API calls
    • Pipeline mode enables fan-out/fan-in across multiple providers with optional reviewer stage
    • Add single-sentence mitigation summaries
  7. Report Generation:

    • Per-package JSON reports: <package>-<version>.json
    • Per-package CSV reports: <package>-<version>.csv
    • Executive summary: executive-summary-<timestamp>.json
    • All files saved to $HOME/output/

Prerequisites

Depending on scanner choice:

  • grype - For Grype scanning (default scanner)
  • trivy - For Trivy scanning
    • syft - For converting reports to CycloneDX format (Trivy workflow)
# Install Grype
brew install grype

# Install syft (for Trivy workflow)
brew install syft

# Install trivy
brew install aquasecurity/trivy/trivy

Installation

Using Docker (Recommended)

The easiest way to use CVE Report Aggregator is via the pre-built Docker image, which includes all necessary scanning tools (Grype, Syft, Trivy, UDS CLI):

# Pull the latest signed image from GitHub Container Registry
docker pull ghcr.io/mkm29/cve-report-aggregator:latest

# Or build locally
docker build -t cve-report-aggregator .

# Or use Docker Compose
docker compose run cve-aggregator --help

# Run with mounted volumes for reports and output
docker run --rm \
  -v $(pwd)/reports:/workspace/reports:ro \
  -v $(pwd)/output:/home/cve-aggregator/output \
  -v $(pwd)/packages:/home/cve-aggregator/packages \
  ghcr.io/mkm29/cve-report-aggregator:latest \
  --verbose

# Note: Output files are automatically saved to $HOME/output with package name and version:
# Format: <package_name>-<package_version>.json (e.g., core-logging-0.54.1-unicorn.json)

Image Security & Supply Chain

All container images are built securely using GitHub Actions with artifact attestations, achieving SLSA Level 3 compliance with the following features:

  • Build Provenance: GitHub Artifact Attestation (SLSA Level 3)
  • SBOM Included: CycloneDX attestation attached to every image
  • Attestations: Viewable in GitHub Actions UI and verifiable via CLI
  • Multi-Architecture: Supports both amd64 and arm64
  • Vulnerability Scanned: Regularly scanned with Grype and Trivy
Verify Attestations
# Verify build provenance and SBOM attestations
gh attestation verify oci://ghcr.io/mkm29/cve-report-aggregator:latest --owner mkm29

# View attestations in JSON format
gh attestation verify oci://ghcr.io/mkm29/cve-report-aggregator:latest --owner mkm29 --format json | jq .

# List all attestations for an image
gh attestation list oci://ghcr.io/mkm29/cve-report-aggregator:latest --owner mkm29
Download SBOM
# Download CycloneDX SBOM from attestation
gh attestation verify oci://ghcr.io/mkm29/cve-report-aggregator:latest \
  --owner mkm29 \
  --format json | \
  jq -r '.[] | select(.predicateType | contains("cyclonedx")) | .predicate' > sbom-cyclonedx.json
View in GitHub UI

Attestations are also viewable directly in the GitHub Actions UI:

  1. Navigate to the repository's Actions tab
  2. Select the Docker Build workflow run
  3. View the "Attestations" section in the workflow summary

Available Image Tags

Images are published to GitHub Container Registry with the following tags:

  • latest - Latest stable release (recommended for production)
  • v*.*.* - Specific version tags (e.g., v0.5.1, v0.5.2)
  • rc - Release candidate builds (for testing pre-release versions)
# Pull specific version
docker pull ghcr.io/mkm29/cve-report-aggregator:v0.5.1

# Pull latest stable
docker pull ghcr.io/mkm29/cve-report-aggregator:latest

# Pull release candidate (if available)
docker pull ghcr.io/mkm29/cve-report-aggregator:rc

All tags are signed and include full attestations (signature, SBOM, provenance).

CVE Enrichment

CVE Report Aggregator supports optional AI-powered enrichment using a pluggable provider architecture to automatically analyze vulnerabilities in the context of UDS Core security controls. This feature generates concise, actionable mitigation summaries that explain how defense-in-depth security measures help protect against specific CVEs.

Architecture

The enrichment module uses a factory pattern with protocol-based abstractions:

flowchart TB
    subgraph config["Configuration"]
        direction LR
        YAML[".cve-aggregator.yaml"]
        ENV["Environment Variables"]
        CLI["CLI Arguments"]
    end

    subgraph factory["EnricherFactory"]
        direction TB
        CREATE["create_enricher()"]
        REGISTRY["Provider Registry"]
    end

    subgraph providers["Providers"]
        direction LR
        OR["OpenRouterEnricher"]
        ANTH["AnthropicEnricher"]
        OAI["OpenAIEnricher"]
        PIPE["Pipeline"]
        MOCK["MockEnricher"]
    end

    subgraph protocol["Enricher Protocol"]
        direction TB
        MODEL["model: str"]
        ENRICH["enrich_report()"]
    end

    config --> CREATE
    CREATE --> REGISTRY
    REGISTRY --> OR
    REGISTRY --> ANTH
    REGISTRY --> OAI
    REGISTRY --> PIPE
    REGISTRY --> MOCK

    OR --> protocol
    ANTH --> protocol
    OAI --> protocol
    PIPE --> protocol
    MOCK --> protocol

    classDef configStyle fill:#1976d2,stroke:#0d47a1,color:#fff
    classDef factoryStyle fill:#7b1fa2,stroke:#4a148c,color:#fff
    classDef providerStyle fill:#388e3c,stroke:#1b5e20,color:#fff
    classDef protocolStyle fill:#f57c00,stroke:#e65100,color:#fff

    class YAML,ENV,CLI configStyle
    class CREATE,REGISTRY factoryStyle
    class OR,ANTH,OAI,PIPE,MOCK providerStyle
    class MODEL,ENRICH protocolStyle

Available Providers

Provider Description API Key Env Var Status
openrouter OpenRouter API with synchronous processing OPENROUTER_API_KEY Default
anthropic Anthropic Messages Batch API (50% cost savings vs synchronous) ANTHROPIC_API_KEY Available
openai OpenAI Batch API (50% cost savings vs synchronous) OPENAI_API_KEY Available
pipeline Fan-out/fan-in multi-provider enrichment with optional reviewer Per-stage provider key Available
mock Mock enricher for testing N/A Testing only

Note: Batch API enrichment (Anthropic and OpenAI) typically completes within minutes to hours (up to 24-hour maximum). The CLI will poll for completion automatically (configurable via pollInterval, default 30 seconds) and display progress updates.

Quick Start

# OpenRouter (default provider)
# OpenRouter (default provider)
export OPENROUTER_API_KEY=sk-or-...
cve-report-aggregator --enrich-cves

# Anthropic (Messages Batch API - 50% cost savings)
export ANTHROPIC_API_KEY=sk-ant-...
cve-report-aggregator --enrich-cves
# Set provider to anthropic in .cve-aggregator.yaml:
#   enrich:
#     provider: anthropic
#     model: claude-sonnet-4-5-20241022

# OpenAI (Batch API - 50% cost savings)
export OPENAI_API_KEY=sk-...
cve-report-aggregator --enrich-cves
# Set provider to openai in .cve-aggregator.yaml:
#   enrich:
#     provider: openai
#     model: gpt-4o

# Pipeline (multi-provider fan-out/fan-in)
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...
cve-report-aggregator --enrich-cves
# Set provider to pipeline in .cve-aggregator.yaml:
#   enrich:
#     provider: pipeline
#     pipelineStages:
#       - provider: anthropic
#         model: claude-sonnet-4-5-20241022
#       - provider: openai
#         model: gpt-4o
#     reviewer: true

# Customize enrichment with higher reasoning effort
cve-report-aggregator \
  --enrich-cves \
  --openai-model gpt-4o \
  --openai-reasoning-effort high \
  --max-cves-to-enrich 10 \
  --enrich-severity-filter Critical

Reasoning Effort

The openai_reasoning_effort parameter controls how deeply the AI model analyzes each CVE:

  • minimal: Basic analysis with minimal token usage
  • low: Faster, more concise analysis with lower token usage
  • medium (default): Balanced analysis with good quality and reasonable token usage
  • high: Most thorough analysis with higher quality but increased token usage

When to adjust:

  • Use minimal for quick overviews or large CVE sets
  • Use low for large CVE sets where speed and cost are priorities
  • Use medium (default) for most production use cases
  • Use high for critical vulnerabilities requiring detailed analysis

Note: The reasoning_effort parameter is only supported by GPT-5 models (x-ai/grok-code-fast-1, gpt-5-mini). The temperature parameter is fixed at 1.0 for GPT-5 models as required by OpenAI.

# Example: High-quality analysis for critical CVEs only
cve-report-aggregator \
  --enrich-cves \
  --openai-reasoning-effort high \
  --enrich-severity-filter Critical

Output Format

Enrichments are added to the unified report under the enrichments key:

{
  "enrichments": {
    "CVE-2024-12345": {
      "cve_id": "CVE-2024-12345",
      "mitigation_summary": "UDS helps to mitigate CVE-2024-12345 by enforcing non-root container execution through Pepr admission policies and blocking unauthorized external network access via default-deny NetworkPolicies.",
      "analysis_model": "x-ai/grok-code-fast-1",
      "analysis_timestamp": "2025-01-20T12:34:56.789Z"
    }
  },
  "summary": {
    "enrichment": {
      "enabled": true,
      "total_cves": 150,
      "enriched_cves": 45,
      "model": "x-ai/grok-code-fast-1",
      "severity_filter": ["Critical", "High"]
    }
  }
}

Docker Credentials Management

The Docker container supports two methods for providing registry credentials:

  1. Build-Time Secrets
  2. Environment Variables

Method 1: Build-Time Secrets

[!IMPORTANT] Since this package (image) currently has public access, this method is not used. Changing the visibility to private would be required to safely use this method, however, since the credentials are baked in, shared credentials would need to be used for all users. This method makes it difficult to granularly control access per user. Consider using environment variables for more flexible credential management.

Create a credentials file in JSON format with username, password, and registry fields:

cat > docker/config.json <<EOF
{
  "username": "myuser",
  "password": "mypassword",
  "registry": "ghcr.io"
}
EOF
chmod 600 docker/config.json

Important: Always encrypt the credentials file with SOPS before committing:

# Encrypt the credentials file
sops -e docker/config.json.dec > docker/config.json.enc

# Or encrypt in place
sops -e docker/config.json.dec > docker/config.json.enc

Build the image with the secret:

# If using encrypted file, decrypt first
sops -d docker/config.json.enc > docker/config.json.dec

# Build with the decrypted credentials
docker buildx build \
  --secret id=credentials,src=./docker/config.json.dec \
  -f docker/Dockerfile \
  -t cve-report-aggregator:latest .

# Remove decrypted file after build
rm docker/config.json.dec

Or build directly with unencrypted file (for local development):

docker buildx build \
  --secret id=credentials,src=./docker/config.json \
  -f docker/Dockerfile \
  -t local/cve-report-aggregator:latest .

The credentials will be stored in the image at $DOCKER_CONFIG/config.json (defaults to /home/cve-aggregator/.docker/config.json) in proper Docker authentication format with base64-encoded credentials.

Run the container (no runtime credentials needed - uses baked-in config.json):

docker run --rm cve-report-aggregator:latest --help

Important: This method bakes credentials into the image. Only use for private registries and never push images with credentials to public registries.

Method 2: Environment Variables

docker run -it --rm \
  -e REGISTRY_URL="$UDS_URL" \
  -e UDS_USERNAME="$UDS_USERNAME" \
  -e UDS_PASSWORD="$UDS_PASSWORD" \
  -e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
  cve-report-aggregator:latest --help

How Credentials Are Handled

The entrypoint.sh script checks for Docker authentication on startup:

  1. Docker config.json (Build-Time): Checks if $DOCKER_CONFIG/config.json exists

    • If found: Skips all credential checks and login - uses existing Docker auth
    • Location: /home/cve-aggregator/.docker/config.json
  2. Environment Variables (if config.json not found): Requires all three variables:

    • REGISTRY_URL - Registry URL (e.g., registry.defenseunicorns.com)
    • UDS_USERNAME - Registry username
    • UDS_PASSWORD - Registry password

If config.json doesn't exist and environment variables are not provided, the container exits with an error.

From Source

# Clone the repository
git clone https://github.com/mkm29/cve-report-aggregator.git
cd cve-report-aggregator

# Install in development mode
pip install -e .

# Or install with dev dependencies
pip install -e ".[dev]"

From PyPI

# Install globally
pip install cve-report-aggregator

# Or install with uv (recommended)
uv tool install cve-report-aggregator

Usage

Basic Usage (Default Locations)

Process reports from ./reports/ and automatically save timestamped output to $HOME/output/:

cve-report-aggregator
# Output:
#   $HOME/output/<package>/<package>-<version>.json
#   $HOME/output/<package>/<package>-<version>.csv

Use Trivy Scanner

Automatically convert reports to CycloneDX and scan with Trivy:

cve-report-aggregator --scanner trivy

Use Both Scanners

Run both Grype and Trivy scanners and combine the results:

cve-report-aggregator --scanner both

This will:

  • Run Grype scanner first (direct SBOM scanning)
  • Run Trivy scanner second (with CycloneDX conversion)
  • Combine results from both scanners
  • Track which scanner(s) detected each vulnerability via the scanner_sources field
  • Generate executive summary with per-scanner severity breakdown

Process SBOM Files

The script automatically detects and scans Syft SBOM files:

cve-report-aggregator -i /path/to/sboms -v

Custom Input Directory

# Specify custom input directory (output still goes to $HOME/output)
cve-report-aggregator -i /path/to/reports

Verbose Mode

Enable detailed processing output:

cve-report-aggregator -v

Combined Options

cve-report-aggregator -i ./scans --scanner trivy -v
# Output:
#   $HOME/output/<package>/<package>-<version>.json
#   $HOME/output/<package>/<package>-<version>.csv

Use Highest Severity Across Scanners

When scanning with multiple scanners (or multiple runs of the same scanner), automatically select the highest severity rating:

# Scan the same image with both Grype and Trivy, use highest severity
grype myapp:latest -o json > reports/grype-app.json
trivy image myapp:latest -f json -o reports/trivy-app.json
cve-report-aggregator -i reports/ --mode highest-score
# Output:
#   $HOME/output/<package>/<package>-<version>.json
#   $HOME/output/<package>/<package>-<version>.csv

This is particularly useful when:

  • Combining results from multiple scanners with different severity assessments
  • Ensuring conservative (worst-case) severity ratings for compliance
  • Aggregating multiple scans over time where severity data may have been updated

Note: All output files are automatically saved to $HOME/output/ in a <package> subdirectory with the package version in the format <package_name>-<package_version>.json.

For complete configuration options, see the Configuration section.

Output Formats

The tool generates reports in two formats for maximum flexibility:

1. JSON Format (Unified Report)

The unified report includes:

Metadata

  • Generation timestamp
  • Scanner type and version
  • Source report count and filenames
  • Package name and version

Summary

  • Total vulnerability occurrences
  • Unique vulnerability count
  • Severity breakdown (Critical, High, Medium, Low, Negligible, Unknown)
  • Per-image scan results
  • Scanner-specific severity breakdown (when using --scanner both):
    • vulnerabilities_by_severity_by_scanner: Shows vulnerability counts by severity for each scanner

    • Example:

      {
        "vulnerabilities_by_severity_by_scanner": {
          "grype": {
            "Critical": 10,
            "High": 25,
            "Medium": 50
          },
          "trivy": {
            "Critical": 12,
            "High": 28,
            "Medium": 55
          }
        }
      }
      

Vulnerabilities (Deduplicated)

For each unique CVE/GHSA:

  • Vulnerability ID

  • Occurrence count

  • Scanner sources (scanner_sources): Array showing which scanner(s) detected the vulnerability

    • Single scanner: ["grype"] or ["trivy"]

    • Both scanners: ["grype", "trivy"]

    • Example:

      {
        "vulnerability": {
          "id": "CVE-2024-12345",
          "scanner_sources": ["grype", "trivy"]
        }
      }
      
  • Severity and CVSS scores

  • Fix availability and versions

  • All affected sources (images and artifacts)

  • Detailed match information

2. CSV Format (Simplified Export)

A simplified CSV export is automatically generated alongside each unified JSON report for easy consumption in spreadsheet applications and reporting tools.

Filename Format: <package_name>-<timestamp>.csv

Columns:

  • CVE ID: Vulnerability identifier
  • Severity: Severity level (Critical, High, Medium, Low, etc.)
  • Count: Number of occurrences across all scanned images
  • CVSS: Highest CVSS 3.x score (or "N/A" if unavailable)
  • Scanner Sources: Comma-separated list of scanners that detected the vulnerability (e.g., "grype, trivy")
  • Impact: Impact analysis from AI enrichment (if enabled)
  • Mitigation: Mitigation summary from AI enrichment (if enabled)

Example:

"CVE-2023-4863","Critical","5","9.8","grype, trivy","Without UDS Core controls, this critical vulnerability...","UDS helps to mitigate CVE-2023-4863 by..."
"CVE-2023-4973","High","3","7.5","grype","This vulnerability could allow...","UDS helps to mitigate CVE-2023-4973 by..."

Features:

  • Sorted by severity (Critical > High > Medium > Low) and CVSS score
  • Includes scanner source tracking
  • Includes enrichment data when CVE enrichment is enabled
  • UTF-8 encoded with proper CSV escaping

Location: $HOME/output/<package_name>/<package_name>-<package_version>.csv

Development

Running Tests

# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov=cve_report_aggregator --cov-report=html

# Run specific test file
uv run pytest tests/test_severity.py

Code Quality

# Lint code
uv run ruff check src/ tests/

# Type checking with ty (https://github.com/astral-sh/ty)
uv run ty check

# Or run both
uv run lint

Building the Package

# Build distribution packages
python -m build

# Install locally
pip install dist/cve_report_aggregator-0.1.0-py3-none-any.whl

Project Structure

cve-report-aggregator/
├── src/
│   └── cve_report_aggregator/
│       ├── __init__.py           # Package exports and metadata       ├── cli.py                # CLI entry point with Click       ├── utils.py              # Utility functions       ├── core/
│          ├── config.py         # Configuration management          ├── constants.py      # Application constants          ├── models.py         # Pydantic models          └── orchestrator.py   # Main processing orchestrator       ├── enhance/
│          ├── __init__.py       # Public API exports          ├── base.py           # Enricher protocol and BaseEnricher ABC          ├── factory.py        # EnricherFactory for provider creation          ├── exceptions.py     # Module-specific exceptions          ├── models.py         # Enrichment data models          └── providers/
│              ├── openrouter.py # OpenRouter synchronous API implementation              ├── anthropic.py  # Anthropic Messages Batch API implementation              ├── openai.py     # OpenAI Batch API implementation              ├── pipeline.py   # Pipeline fan-out/fan-in multi-provider              └── mock.py       # Mock enricher for testing       ├── io/
│          ├── csv_export.py     # CSV report generation          ├── downloader.py     # Package downloading          ├── local_packages.py # Local package handling          └── report.py         # JSON report generation       └── processing/
│           ├── aggregator.py     # CVE deduplication engine           ├── scanner.py        # Scanner integrations           └── severity.py       # CVSS and severity logic
├── tests/
│   ├── unit/
│      ├── enhance/
│         ├── test_base.py      # BaseEnricher and protocol tests         ├── test_factory.py   # EnricherFactory tests         ├── test_exceptions.py# Exception tests
│         └── providers/
│             ├── test_openrouter.py
│             ├── test_anthropic.py
│             ├── test_openai.py
│             ├── test_pipeline.py
│             └── test_mock.py
│      └── ...                   # Other unit tests   └── integration/              # Integration tests
├── docs/                         # Detailed documentation   ├── architecture.md           # Pipeline diagrams and stage details   ├── configuration.md          # Full configuration reference   ├── docker.md                 # Docker guide (build, compose, credentials)   ├── enrichment.md             # CVE enrichment architecture and details   ├── output-formats.md         # JSON and CSV format specifications   └── usage.md                  # Usage examples and scanner workflows
├── pyproject.toml                # Project configuration
├── README.md                     # This file
└── LICENSE                       # MIT License

License

MIT License - See LICENSE file for details

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

Changelog

See CHANGELOG.md for version history and changes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cve_report_aggregator-0.19.10.tar.gz (468.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cve_report_aggregator-0.19.10-py3-none-any.whl (168.1 kB view details)

Uploaded Python 3

File details

Details for the file cve_report_aggregator-0.19.10.tar.gz.

File metadata

  • Download URL: cve_report_aggregator-0.19.10.tar.gz
  • Upload date:
  • Size: 468.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for cve_report_aggregator-0.19.10.tar.gz
Algorithm Hash digest
SHA256 fe2bbeecae668feb3d35eee1e45c127d6a6bd078ead7a1906e964904b1f80a05
MD5 2727715aeaf48a23ccf2a0e924f47f62
BLAKE2b-256 0bf4d7703b0e9e59f71f47edcf9e0d47f8a5eb714f929085afbe6ce1f1697281

See more details on using hashes here.

File details

Details for the file cve_report_aggregator-0.19.10-py3-none-any.whl.

File metadata

  • Download URL: cve_report_aggregator-0.19.10-py3-none-any.whl
  • Upload date:
  • Size: 168.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for cve_report_aggregator-0.19.10-py3-none-any.whl
Algorithm Hash digest
SHA256 551fa1bc9b4f6721a6d028075d55d8d4bdb85cc1a3ce382df771e7dac31d1961
MD5 83c7a241b83545d0d6f51aa679f09a23
BLAKE2b-256 5ee7005d2bc3a645eedb912b0b133c9eff979fb8ffb419bd386a8735ae71bdbd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page