Container Catalog
Per-distro serverless catalog API for Hummingbird container images.
Features
- Image Directory - Browse all container images with metadata
- Tag Browser - View tags, digests, architectures per image
- Specifications - Per-architecture OCI config details (env, cmd, user, labels)
- SBOM - Per-architecture package lists from SPDX attestations
- Vulnerabilities - CVE scanning results via Grype
- CVE Metrics - CloudWatch metrics for CVE exposure duration (see Error Budgets)
- Provenance - Source traceability from SLSA attestations
- Release History - Timeline of past builds with drill-down
- OpenAPI Spec - Machine-readable API documentation at /v1/openapi.json
- Swagger UI - Interactive API explorer at /v1/docs/
Architecture
Rust serverless stack deployed as two isolated per-distro stacks:
- API Lambda - DynamoDB pass-through (~10ms response)
- Sync Lambda - Incremental DynamoDB sync from SNS Release events (~22 registry calls per release)
- Metrics Lambda (
metrics-lambda) - DynamoDB Stream-triggered CVE exposure metrics and structured logs to CloudWatch - DynamoDB - Pre-computed JSON items (single-table PK/SK design, Streams: KEYS_ONLY)
- CloudFront - CDN with per-endpoint cache TTLs
- CloudWatch - CVE exposure metrics and structured logs
- catalog sync - Full DynamoDB population from GitLab + Quay.io OCI v2 registry
- catalog scan - CVE scanning via Grype from DynamoDB-stored SBOMs
(no registry access), with
first_seentracking - Catalog SPA - Lit 3 web app served from S3 via CloudFront
Prerequisites
- Rust 1.75+ (for building backend)
- Node.js 22+ (for building frontend)
- AWS credentials (for DynamoDB access and deployment)
- SAM CLI (for deployment)
API Endpoints
| Endpoint | Description |
|---|---|
GET /v1/images |
Image directory |
GET /v1/stats |
Catalog statistics |
GET /v1/images/{name} |
Image overview (README) |
GET /v1/images/{name}/tags |
Tags for an image |
GET /v1/images/{name}/specifications/{canonical} |
Per-arch OCI config |
GET /v1/images/{name}/sbom/{canonical} |
Package list |
GET /v1/images/{name}/provenance/{canonical} |
Source provenance |
GET /v1/images/{name}/history/{stream}/{variant} |
Release timeline |
GET /v1/images/{name}/history/{digest}/specifications |
Historical per-arch specs |
GET /v1/images/{name}/history/{digest}/sbom |
Historical SBOM |
GET /v1/images/{name}/vulnerabilities/{canonical} |
Vulnerability scan |
GET /v1/images/{name}/releases/vulnerabilities/{digest} |
Vulnerability scan (hash) |
GET /v1/openapi.json |
OpenAPI 3.1 specification |
GET /v1/docs/ |
Interactive Swagger UI |
Timestamp Fields
The oldest_created field on ImageSummary, Tag, and HistorySummary is
the earliest OCI created timestamp across all architectures in the release.
All architectures were built at or after this date, making it useful for
conservative staleness detection.
The specifications endpoint returns per-architecture data keyed by architecture
name. Each architecture’s created field is the direct OCI config root
timestamp for that specific architecture.
Usage
All tools are built as a single catalog binary with subcommands (api,
sync, sync-lambda, scan, metrics, metrics-lambda). The binary is
built in the Rust container and
CLI subcommands are run in the gitlab-ci container (which provides grype and
other tools). Only make and podman are required.
Sync
# Dry run (print items to stdout)
make container-catalog/sync ARGS="--distro rawhide --dry-run"
# Populate DynamoDB
make container-catalog/sync ARGS="--distro rawhide --table-name <table>"
Sync Lambda
The sync-lambda subcommand runs as an AWS Lambda function triggered by SNS
Release events from kubernetes-event-forwarder.
It incrementally syncs a single image release to DynamoDB (~22 registry API
calls per release vs ~104 for a full sync).
The Lambda:
- Decodes gzip+base64 SNS messages
- Filters for Succeeded releases targeting the configured Quay.io namespace
- Fetches OCI manifest data for the new digest
- Writes per-digest items (DETAILS, SBOM, RELEASE_DETAILS, RELEASE_SBOM)
- Merges into aggregate items (TAGS, HISTORY, OVERVIEW, DIRECTORY)
- Fetches README from GitLab for OVERVIEW content (uses
README.redhat.mdfor hummingbird,README.mdfor rawhide)
Registry fetch errors (manifest, SBOM, attestation) propagate as hard failures so the Lambda retries automatically (up to 2 retries with backoff) before sending to the DLQ. GitLab README failures are non-fatal – the existing README is preserved if the fetch fails.
| Environment Variable | Description |
|---|---|
TABLE_NAME |
DynamoDB table name |
DISTRO |
rawhide or hummingbird |
SENTRY_DSN |
Optional Sentry DSN for error tracking |
Scan
The scan subcommand reads image listings, tags, and SBOMs from DynamoDB
(no registry access needed) and runs Grype against each image’s stored SBOM
packages. Results include a first_seen timestamp per CVE, tracked at the
group+variant+stream level and carried across releases for SLI computation.
# Dry run (print items to stdout)
make container-catalog/scan ARGS="--distro hummingbird --table-name <table> --dry-run"
# Scan and write to DynamoDB (purge stale vuln data first, implies --scope=all)
make container-catalog/scan ARGS="--distro hummingbird --table-name <table> --purge"
# Scan only non-superseded (current) tags — used by CronJob
make container-catalog/scan ARGS="--distro hummingbird --table-name <table> --scope non-superseded"
# Scan all releases including historic (tagless) releases
make container-catalog/scan ARGS="--distro hummingbird --table-name <table> --scope all"
# Scan a single image
make container-catalog/scan ARGS="--distro hummingbird --table-name <table> --image caddy --dry-run"
Kubernetes CronJob
An hourly CronJob runs catalog scan --scope non-superseded to keep
vulnerability data up to date for all current image tags. The CronJob
infrastructure is defined in rprm-infrastructure under
kubernetes/container-catalog-scan-service/.
Metrics
The metrics subcommand performs a one-shot read of all non-superseded
vulnerability data from DynamoDB and outputs structured CVE exposure logs.
With --dry-run, it skips the CloudWatch push (useful for local inspection).
# Dry run (print structured logs to stdout, no CloudWatch push)
make container-catalog/metrics ARGS="--distro hummingbird --table-name <table> --dry-run"
# Push metrics to CloudWatch and print structured logs
make container-catalog/metrics ARGS="--distro hummingbird --table-name <table>"
Metrics Lambda
The metrics-lambda subcommand runs as a DynamoDB Stream-triggered Lambda that
emits CVE exposure duration metrics to CloudWatch. These metrics feed the SLO
dashboard and alarm defined in the
error-budgets stack.
How It Works
The Lambda is triggered by DynamoDB Stream events filtered on VULNERABILITIES#
and TAGS changes, with a 60-second batching window and reserved concurrency
of 1 (single instance). It maintains an in-memory active CVE table across warm
invocations:
- Cold start: Reads
CATALOG/DIRECTORY, allTAGS, and all non-supersededVULNERABILITIES#items from DynamoDB to build the full table (~2-3s at 10000 tags) - Warm invocations: Incrementally updates the table from stream event keys (~50-100 GetItem calls per batch)
- After each invocation: Recomputes and emits all metrics and structured logs
CloudWatch Metrics
| Metric | Type | Dimensions |
|---|---|---|
CveExposureDuration |
Distribution | [Distro, Fixable], [Distro, Sev, Fix] |
ActiveCveCount |
Count | [Distro, Severity, Fixable] |
CveExposureDuration values are in hours, computed as now - first_seen for
each active CVE on each non-superseded canonical tag. The Fixable dimension
is true when a fix version is known upstream, false otherwise.
Structured Logs
Each invocation emits one JSON log line per active CVE to stdout (captured by
CloudWatch Logs). Each line includes fixable (boolean) and fixed_in
(version string or null). To query fixable CVEs only:
filter message = "active_cve" and fixable = true
| fields cve, severity, exposure_hours, fixed_in, repository, stream, variant, component
| sort exposure_hours desc
| Environment Variable | Description |
|---|---|
TABLE_NAME |
DynamoDB table name |
DISTRO |
rawhide or hummingbird |
CLOUDWATCH_NAMESPACE |
CloudWatch namespace for metrics |
SENTRY_DSN |
Optional Sentry DSN |
Deployment
make container-catalog/build
make container-catalog/deploy
Configuration
catalog sync
| Argument | Description |
|---|---|
--distro |
rawhide or hummingbird |
--table-name |
DynamoDB table name |
--purge |
Delete all items before writing |
--cache-dir |
Cache directory (auto-detected) |
--image |
Sync only a specific repo |
--legacy-discovery |
Use GitLab-based repo discovery |
catalog scan
| Argument | Description |
|---|---|
--distro |
rawhide or hummingbird |
--table-name |
DynamoDB table name (required) |
--scope |
non-superseded, tags (default), or all |
--dry-run |
Print items without writing |
--purge |
Purge vuln data before writing (implies --scope all) |
--cache-dir |
Cache directory (auto-detected) |
--parallel |
Number of concurrent scans (default: 4) |
--image |
Scan only a specific image |
--tag |
Scan only a specific tag (requires --image) |
--upstream-cpe |
Map RPMs to upstream CPEs via CPE dictionary (default) |
SAM Parameters
| Parameter | Description |
|---|---|
Distro |
rawhide or hummingbird |
CacheEnabled |
Enable CloudFront caching |
CatalogDomainName |
Catalog web UI domain |
ApiDomainName |
API domain |
HostedZoneId |
Route53 hosted zone |
CorsOrigin |
Override CORS origin (* to disable) |
SnsTopicArn |
SNS topic ARN for Release events (enables sync Lambda) |
Frontend
The catalog web UI is a Lit 3 SPA (Web Components) with Tailwind CSS, built
per-distro with Vite. Source is in container-catalog/frontend/.
Only make and podman are required (no local Node.js needed).
Defaults from .envrc.defaults are applied automatically.
# Install dependencies
make container-catalog/frontend/setup
# Development server at http://localhost:5173
make container-catalog/frontend/dev
# Production build
make container-catalog/frontend/build
Host variants (*-host) run without podman (for CI or local Node.js).
Frontend Build Variables
| Variable | Description |
|---|---|
VITE_API_URL |
API base URL for the distro |
VITE_DISTRO |
rawhide or hummingbird |
VITE_DISTRO_LABEL |
Display label for current distro |
VITE_OTHER_CATALOG_URL |
URL of the other distro’s catalog (optional, hides link if unset) |
VITE_OTHER_DISTRO_LABEL |
Display label for other distro (optional) |
VITE_VULNERABILITIES_ENABLED |
Show vulnerabilities tab |
Development
# Backend
cargo test # Run tests
cargo clippy --all-targets # Lint
cargo fmt # Format
# Frontend (host variants, requires local Node.js)
cd container-catalog/frontend
npm run typecheck # Type check
npm run build # Production build
License
Apache-2.0