Risk Bundles (Air-Gap)

Audience: operators and release engineers running Stella Ops in offline or air-gapped environments.

Purpose: build and verify deterministic, signed risk-intelligence bundles without network connectivity.

Status: routes, configuration keys, file names and the CLI verify verb re-checked against src/ at commit 942a18727c (2026-09-26); the verification behaviour and troubleshooting answers were not re-verified.

Risk bundles package vulnerability-risk intelligence (CISA KEV, FIRST EPSS, OpenSSF OSV) into deterministic, signed archives. They can be verified against their manifest and signatures without network access.

Bundle Structure

A risk bundle is a gzip-compressed tar archive (risk-bundle.tar.gz) with the following structure:

risk-bundle.tar.gz
├── manifests/
│   └── provider-manifest.json      # Bundle metadata and provider entries
├── providers/
│   ├── cisa-kev/
│   │   └── snapshot                # CISA Known Exploited Vulnerabilities JSON
│   ├── epss/
│   │   └── snapshot                # FIRST EPSS scores CSV/JSON
│   └── osv/                        # (optional) OpenSSF OSV bulk JSON
│       └── snapshot
└── signatures/
    └── provider-manifest.dsse      # DSSE envelope for manifest

Provider Manifest

The provider-manifest.json contains bundle metadata and per-provider entries:

{
  "version": "1.0.0",
  "bundleId": "risk-bundle-20241211-120000",
  "createdAt": "2024-12-11T12:00:00Z",
  "inputsHash": "sha256:abc123...",
  "providers": [
    {
      "providerId": "cisa-kev",
      "digest": "sha256:def456...",
      "snapshotDate": "2024-12-11T00:00:00Z",
      "optional": false
    },
    {
      "providerId": "epss",
      "digest": "sha256:789abc...",
      "snapshotDate": "2024-12-11T00:00:00Z",
      "optional": true
    }
  ]
}
FieldDescription
versionManifest schema version (currently 1.0.0)
bundleIdUnique identifier for this bundle
createdAtISO-8601 UTC timestamp of bundle creation
inputsHashSHA-256 hash of concatenated provider digests (deterministic ordering)
providers[]Array of provider entries sorted by providerId

Provider Entry Fields

FieldDescription
providerIdProvider identifier (cisa-kev, nvd, osv, ghsa, epss)
digestSHA-256 hash of snapshot file (sha256:<hex>)
snapshotDateISO-8601 timestamp of provider data snapshot
optionalWhether the provider is required for bundle validity

Provider Catalog

ProviderSourceCoverageRefreshRequired
cisa-kevCISA Known Exploited VulnerabilitiesExploited CVEs with KEV flagDailyYes
epssFIRST EPSS scoresExploitation probability per CVEDailyNo
nvdNIST National Vulnerability DatabaseCVE recordsnot specifiedNo
ghsaGitHub Security AdvisoriesOSS advisoriesnot specifiedNo
osvOpenSSF OSVOSS advisories with affected rangesWeeklyNo (opt-in)

Building Risk Bundles

Risk bundles are built by the risk-bundle job on offlinekit-worker (src/OfflineKit/__Libraries/StellaOps.ExportCenter.RiskBundles, StellaOps.ExportCenter.WorkerRuntime). The loop is composed only when RiskBundles:Enabled is true (release bundle: OFFLINEKIT_RISK_BUNDLES_ENABLED, default false); enabled, it requires RiskBundles:Signing:Key and RiskBundles:Storage:RootPath at start and has no unsigned mode.

Jobs are submitted and tracked on offlinekit-web; the whole route group requires the scope export.operator:

RoutePurpose
GET /api/offlinekit/v1/risk-bundles/providersavailable providers
POST /api/offlinekit/v1/risk-bundles/jobssubmit a job (selectedProviders, includeOsv, providerOverrides, bundleFileName, allowMissingOptional, allowStaleOptional, metadata)
GET /api/offlinekit/v1/risk-bundles/jobs, GET …/jobs/{jobId}job list and status
POST /api/offlinekit/v1/risk-bundles/jobs/{jobId}/cancelcancel

Jobs carry a tenant-scoped idempotency key, so a replayed submission returns the existing job. Artefacts are written under RiskBundles:Storage:RootPath (release bundle: /var/lib/stella/exportcenter/risk-bundles).

Verifying Risk Bundles

Using the CLI

# Basic verification
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz

# With detached signature
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --signature-path ./bundle.sig

# Check Sigstore Rekor transparency log
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --check-rekor

# JSON output for automation
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --json

# Verbose output with warnings
stella risk bundle verify --bundle-path ./risk-bundle.tar.gz --verbose

CLI Options

OptionDescription
--bundle-path, -bPath to risk bundle file (required)
--signature-path, -sPath to detached signature file
--check-rekorVerify transparency log entry in Sigstore Rekor
--jsonOutput results as JSON
--tenantTenant context for verification
--verboseShow detailed output including warnings

Verification Steps

The verification process performs these checks:

  1. Archive integrity — bundle is a valid tar.gz archive.
  2. Structure validation — required files present (manifests/provider-manifest.json).
  3. Manifest parsing — valid JSON with required fields (bundleId, version, providers).
  4. Provider hash verification — each provider snapshot matches its declared digest.
  5. Mandatory provider check — cisa-kev must be present and valid.
  6. DSSE signature validation — manifest signature verified (if present).
  7. Detached signature — bundle archive signature verified (if provided).

Exit Codes

CodeMeaning
0Bundle is valid
1Bundle is invalid or verification failed
2Input error (missing file, bad arguments)

JSON Output Format

{
  "valid": true,
  "bundleId": "risk-bundle-20241211-120000",
  "version": "1.0.0",
  "providerCount": 2,
  "mandatoryProviderFound": true,
  "errorCount": 0,
  "warningCount": 1,
  "errors": [],
  "warnings": ["Optional provider not found: osv"]
}

Importing Risk Bundles

At this commit no command or service imports a risk bundle into a sealed estate: there is no import-risk-bundle verb and the predecessor Concelier service is retired. Verify a received bundle with stella risk bundle verify and treat it as reference data until an owning service declares an import contract. Offline content that a service activates travels as an OfflineKit carrier part (Importer).

Signing and Trust

DSSE Manifest Signature

The signatures/provider-manifest.dsse file contains a DSSE envelope over the manifest, signed with the key configured as RiskBundles:Signing:Key / RiskBundles:Signing:KeyId (release-bundle default key id risk-bundle-hmac):

{
  "payloadType": "application/stellaops.risk-bundle.provider-manifest+json",
  "payload": "<base64-encoded-manifest>",
  "signatures": [
    {
      "keyid": "risk-bundle-hmac",
      "sig": "<signature>"
    }
  ]
}

Offline Trust Roots

For air-gapped verification, include the public keys in the bundle:

signatures/
├── provider-manifest.dsse
└── pubkeys/
    └── <tenant>.pem

Sigstore/Rekor Integration

When --check-rekor is specified, verification queries the Sigstore Rekor transparency log to confirm the bundle was published to the public ledger. In sealed mode this step is skipped unless an offline Rekor checkpoint is supplied.

Determinism Checklist

Risk bundles are designed for reproducible builds:

Troubleshooting

Q: Hash mismatch after transfer.

A: Binary transfers can corrupt files. Use checksums:

# On source system
sha256sum risk-bundle.tar.gz > checksum.txt

# On target system
sha256sum -c checksum.txt

Q: “Optional provider not found” warning.

A: This is informational. Optional providers (EPSS, OSV) enhance risk analysis but aren’t required.