Aggregate and deduplicate vulnerability scan reports from Grype and Trivy
Project description
CVE Report Aggregation and Deduplication Tool
A Python package for aggregating and deduplicating Grype and Trivy vulnerability scan reports, extracted from Zarf packages. Optionally enrich CVE data using OpenAI GPT models to provide actionable mitigation summaries in the context of UDS Core security controls.
[!IMPORTANT] Will implement customizable prompts and support for additional AI providers in a future release.
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_DBis 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
export OPENAI_API_KEY=sk-...
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: Optional OpenAI integration for automated vulnerability mitigation analysis
- 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
bothoption - 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
- Configuration
- Pipeline Architecture
- Usage Examples
- CVE Enrichment
- Output Formats
- Performance
- Development
- Contributing
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)
Online Scans (Remote Packages)
For scanning remote packages from an OCI registry, use the single archive mount workflow:
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
| Environment Variable | Required | Description |
|---|---|---|
REGISTRY_URL |
Yes | OCI registry URL (e.g., registry.defenseunicorns.com) |
UDS_USERNAME |
Yes | Registry username |
UDS_PASSWORD |
Yes | Registry password |
SKIP_UPDATE_VULNDB_DB |
No | Set to "true" to skip vulnerability database updates (faster execution) |
Output: All artifacts are bundled into ./archive/artifacts.tar.gz:
- SBOM files (extracted via
zarf package inspect sbom) - Aggregated vulnerability reports (JSON)
- CSV exports
- Executive summary
See Docker Security & Supply Chain for signature verification and SBOM attestations.
From PyPI
# Install globally
pip install cve-report-aggregator
# Or install with uv (recommended)
uv tool install cve-report-aggregator
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]"
Prerequisites
For Docker users: No prerequisites needed - all tools are included in the image.
For local installation: You will need uv and at least one scanner:
- 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
Configuration
CVE Report Aggregator supports flexible configuration through multiple sources with the following precedence (highest to lowest):
- CLI Arguments - Command-line flags and options
- YAML Configuration File -
.cve-aggregator.yamlor.cve-aggregator.yml - Environment Variables - Prefixed with
CVE_AGGREGATOR_ - 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 OpenAI | false |
|
--openai-api-key |
OpenAI API key (defaults to OPENAI_API_KEY env var) |
None | |
--openai-model |
OpenAI model to use for enrichment | gpt-5-nano |
|
--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 (OpenAI)
enrich:
enabled: true
provider: openai # only openai is supported currently
model: gpt-5 # OpenAI model (gpt-5-nano, gpt-4o, etc.)
# apiKey: YOUR_OPENAI_API_KEY_HERE # or set via OPENAI_API_KEY environment variable
reasoningEffort: medium # Level of reasoning effort: minimal, low, medium, high
severities: # Severity levels to enrich
- Critical
- High
verbosity: medium # Verbosity level: low, medium, high
seed: 42 # Optional: Seed for reproducibility
metadata: # Optional: Metadata tags for OpenAI requests
project: cve-report-aggregator
organization: defenseunicorns
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: truein 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:
- Local packages in
./packages/(if exist) - Remote package downloads (if
downloadRemotePackages: trueand no local packages) - 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 OPENAI_API_KEY, which has 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 OPENAI_API_KEY=sk-... # OpenAI API key (no prefix)
export CVE_AGGREGATOR_ENRICH_CVES=true
export CVE_AGGREGATOR_OPENAI_MODEL=gpt-5-nano
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 OPENAI_API_KEY=sk-...
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-15seconds for 14 packages- A 10-14x speedup compared to sequential downloads (which can take
~150sfor 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?}
OPENAI[OpenAI Analysis<br/>UDS Security Context]
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| OPENAI
ENRICH_CHECK -->|No| JSON1
OPENAI --> 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 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,OPENAI,MITIGATION enrichStyle
class JSON1,CSV1,JSON2,CSV2,SUMMARY outputStyle
Processing Pipeline Overview
The complete processing pipeline consists of seven stages:
-
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)
-
SBOM Collection:
- Gathers all extracted SBOM files
- Auto-detects SBOM format (checks for
artifacts+descriptorfields)
-
Scanner Selection:
- Grype (default): Direct SBOM scanning
- Trivy: CycloneDX conversion then scanning
- Both: Runs Grype first, then Trivy (with CycloneDX conversion), and combines results
-
Scanner Processing:
- Grype Path:
grype sbom:<file> -o json→ Grype reports - Trivy Path:
syft convert <file> -o cyclonedx-json→trivy sbom <file> -f json→ Trivy reports - Both Path: Runs both scanners sequentially and merges results with source tracking
- Grype Path:
-
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
-
CVE Enrichment (optional, requires
--enrich-cvesflag):- Filter by severity (default: Critical + High)
- Analyze with OpenAI using UDS Core security context
- Add single-sentence mitigation summaries
-
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/
- Per-package JSON reports:
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:
- Navigate to the repository's Actions tab
- Select the Docker Build workflow run
- 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 OpenAI GPT models 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.
Note: Batch API enrichment typically completes within minutes to hours (up to 24-hour maximum). The CLI will poll for completion automatically and display progress updates.
Quick Start
# Set API key
export OPENAI_API_KEY=sk-...
# Enable enrichment (enriches Critical and High severity CVEs by default)
cve-report-aggregator --enrich-cves
# 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 usagemedium(default): Balanced analysis with good quality and reasonable token usagehigh: Most thorough analysis with higher quality but increased token usage
When to adjust:
- Use
minimalfor quick overviews or large CVE sets - Use
lowfor large CVE sets where speed and cost are priorities - Use
medium(default) for most production use cases - Use
highfor critical vulnerabilities requiring detailed analysis
Note: The reasoning_effort parameter is only supported by GPT-5 models (gpt-5-nano, 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": "gpt-5-nano",
"analysis_timestamp": "2025-01-20T12:34:56.789Z"
}
},
"summary": {
"enrichment": {
"enabled": true,
"total_cves": 150,
"enriched_cves": 45,
"model": "gpt-5-nano",
"severity_filter": ["Critical", "High"]
}
}
}
Docker Credentials Management
The Docker container supports two methods for providing registry credentials:
- Build-Time Secrets
- 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 OPENAI_API_KEY="$OPENAI_API_KEY" \
cve-report-aggregator:latest --help
How Credentials Are Handled
The entrypoint.sh script checks for Docker authentication on startup:
-
Docker config.json (Build-Time): Checks if
$DOCKER_CONFIG/config.jsonexists- If found: Skips all credential checks and login - uses existing Docker auth
- Location:
/home/cve-aggregator/.docker/config.json
-
Environment Variables (if config.json not found): Requires all three variables:
REGISTRY_URL- Registry URL (e.g.,registry.defenseunicorns.com)UDS_USERNAME- Registry usernameUDS_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_sourcesfield - 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 identifierSeverity: Severity level (Critical, High, Medium, Low, etc.)Count: Number of occurrences across all scanned imagesCVSS: 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 OpenAI enrichment (if enabled)Mitigation: Mitigation summary from OpenAI 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
│ ├── main.py # CLI entry point
│ ├── models.py # Type definitions
│ ├── utils.py # Utility functions
│ ├── severity.py # CVSS and severity logic
│ ├── scanner.py # Scanner integrations
│ ├── aggregator.py # Deduplication engine
│ └── report.py # Report generation
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures
│ ├── test_severity.py # Severity tests
│ └── test_aggregator.py # Aggregation tests
├── pyproject.toml # Project configuration
├── README.md # This file
└── LICENSE # MIT License
Example Workflows
Grype Workflow (Default)
# Scan multiple container images with Grype
grype registry.io/app/service1:v1.0 -o json > reports/service1.json
grype registry.io/app/service2:v1.0 -o json > reports/service2.json
grype registry.io/app/service3:v1.0 -o json > reports/service3.json
# Aggregate all reports (output saved to $HOME/output with timestamp)
cve-report-aggregator --log-level DEBUG
# Query results with jq (use the timestamped file)
REPORT=$(ls -t $HOME/output/unified-*.json | head -1)
jq '.summary' "$REPORT"
jq '.vulnerabilities[] | select(.vulnerability.severity == "Critical")' "$REPORT"
SBOM Workflow
# Generate SBOMs with Syft (or use Zarf-generated SBOMs)
syft registry.io/app/service1:v1.0 -o json > sboms/service1.json
syft registry.io/app/service2:v1.0 -o json > sboms/service2.json
# Script automatically detects and scans SBOMs with Grype
cve-report-aggregator -i ./sboms --log-level DEBUG
# Results include all vulnerabilities found (use timestamped file)
REPORT=$(ls -t $HOME/output/unified-*.json | head -1)
jq '.summary.by_severity' "$REPORT"
Trivy Workflow
# Start with Grype reports (script will convert to CycloneDX)
grype registry.io/app/service1:v1.0 -o json > reports/service1.json
grype registry.io/app/service2:v1.0 -o json > reports/service2.json
# Aggregate and scan with Trivy (auto-converts to CycloneDX)
cve-report-aggregator --scanner trivy --log-level DEBUG
# Or scan SBOMs directly with Trivy
cve-report-aggregator -i ./sboms --scanner trivy --log-level DEBUG
# View most recent output
REPORT=$(ls -t $HOME/output/unified-*.json | head -1)
jq '.summary' "$REPORT"
Both Scanners Workflow
# Generate SBOMs with Syft
syft registry.io/app/service1:v1.0 -o json > sboms/service1.json
syft registry.io/app/service2:v1.0 -o json > sboms/service2.json
# Run both scanners and combine results
cve-report-aggregator -i ./sboms --scanner both --log-level DEBUG
# View combined results with scanner source tracking
REPORT=$(ls -t $HOME/output/unified-*.json | head -1)
# See overall summary
jq '.summary' "$REPORT"
# See per-scanner severity breakdown
jq '.summary.vulnerabilities_by_severity_by_scanner' "$REPORT"
# Find CVEs detected by both scanners
jq '.vulnerabilities[] | select(.vulnerability.scanner_sources | length == 2)' "$REPORT"
# Find CVEs only detected by Grype
jq '.vulnerabilities[] | select(.vulnerability.scanner_sources == ["grype"])' "$REPORT"
# Find CVEs only detected by Trivy
jq '.vulnerabilities[] | select(.vulnerability.scanner_sources == ["trivy"])' "$REPORT"
License
MIT License - See LICENSE file for details
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Changelog
See CHANGELOG.md for version history and changes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cve_report_aggregator-0.19.3.tar.gz.
File metadata
- Download URL: cve_report_aggregator-0.19.3.tar.gz
- Upload date:
- Size: 395.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
536cfcabb06b644d7c2c33fe3302d748d17bfcc7103100a7460c6c0251dc8b24
|
|
| MD5 |
827733267c69433c0a73b3fc1d013162
|
|
| BLAKE2b-256 |
cc26d9d3ee5cb5effcf66341349fa8511314d7380ea7867d692a6cd40cbc5d70
|
File details
Details for the file cve_report_aggregator-0.19.3-py3-none-any.whl.
File metadata
- Download URL: cve_report_aggregator-0.19.3-py3-none-any.whl
- Upload date:
- Size: 139.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35ea5c5f2d9d3d7c9d017501f534906bae2aa90991c6b00313d7bc6f6ee4baf3
|
|
| MD5 |
d0e752d68ee89095206b037300db3769
|
|
| BLAKE2b-256 |
fc7ce056053af10b036835a5cc5075e6e166d0dd0f03c1c72c3b99ee9a9d548f
|