seebom-labs/BOMHort

About standalone, Kubernetes-native Software Bill of Materials (SBOM) visualization and governance platform

Go

53

283 commits

updated Sep 22, 2026

See the code

README

BOMHort

Kubernetes-native Software Bill of Materials (SBOM) Visualization & Governance Platform

CI OpenSSF Scorecard OpenSSF Best Practices

Ingest 1000+ SPDX and CycloneDX SBOMs, scan for vulnerabilities via OSV, enforce license compliance, and apply VEX statements — all visualized in a fast Angular dashboard backed by ClickHouse analytics.

BOMHort (formerly known as SeeBOM) is the same project with a new name. Read more: Why we renamed SeeBOM to BOMHort.

Getting Started · Architecture · Roadmap · Contributing · AI Policy

BOMHort Dashboard


Quick Start

Prerequisites

ToolMinimum Version
Docker + Docker Composev2.20+
Go1.26+ (only for local dev) — backend/go.mod pins go 1.26.8
Node.js22+ (only for local dev)
# 1. Clone the repo
git clone https://github.com/seebom-labs/BOMHort.git && cd BOMHort

# 2. Place your SBOM files in the sboms/ directory
#    Supports SPDX 2.x JSON, SPDX 3 JSON-LD, CycloneDX JSON, and in-toto attestation envelopes (auto-detected)
#    (examples included: sboms/_example.spdx.json, sboms/_example.cdx.json)

# 3. Start everything
make dev

# Or without make:
docker compose up --build -d

This starts:

  • ClickHouse on localhost:9000 (TCP) / localhost:8123 (HTTP)
  • API Gateway on localhost:8080
  • Ingestion Watcher (runs once, scans sboms/ for new files)
  • Parsing Worker (processes queued SBOM/VEX files)
  • Angular UI on localhost:8090

Open http://localhost:8090 in your browser.

Configuration (.env)

Copy .env.example to .env and adjust:

cp .env.example .env
VariableDefaultDescription
SBOM_SOURCE_DIR./sbomsPath to your SBOM files (can point to an external repo checkout)
SBOM_LIMIT0Max SBOMs to enqueue per watcher run. 0 = unlimited. Use 50200 for local dev.
WORKER_REPLICAS1Number of parallel parsing worker containers
WORKER_BATCH_SIZE50Jobs claimed per polling cycle per worker
SKIP_OSVfalseSkip OSV vulnerability API calls. Set true for fast initial bulk load (licenses only), then re-run with false.
SKIP_GITHUB_RESOLVEfalseSkip GitHub license resolution for packages with NOASSERTION/empty licenses.
GITHUB_TOKEN(empty)GitHub personal access token for license resolution. Increases rate limit from 60 to 5000 req/h. No scopes needed.
SKIP_NPM_RESOLVEfalseSkip npm registry license resolution for pkg:npm/* packages with NOASSERTION/empty licenses.
SKIP_NUGET_RESOLVEfalseSkip NuGet license resolution for pkg:nuget/* packages with NOASSERTION/empty licenses. Legacy packages without licenseExpression fall back to their GitHub repository license.
CLUSTER_NAME(empty)Cluster identifier for multi-cluster deployments. All ingested data is tagged with this value. Empty = single-instance mode.
NAMESPACE(empty)Default deployment-namespace label (#138) stamped onto all ingested data. Overridable per bucket and per upload (?namespace=).
PROJECT(empty)Default project label (#57) stamped onto all ingested data. Overridable per bucket and per upload (?project=).
INGEST_PATH_LAYOUT(empty)Opt-in: derive cluster/namespace/project from an SBOM's position in the source, e.g. cluster/namespace/project for keys like prod-eu/payments/payment-service/app.spdx.json. _ skips a level. A malformed layout fails at startup. Explicit config always outranks derivation.
TAGS(empty)Comma-separated free-form grouping labels (#357) stamped onto all ingested data, e.g. sandbox-applications,platform. Tags group projects — they do not replace them: a project keeps its identity and can carry several tags. Unlike CLUSTER_NAME/NAMESPACE/PROJECT, per-bucket and per-upload (?tags=) values are merged, not overridden. Normalised to lowercase, trimmed, deduplicated and sorted. Read back via GET /api/v1/tags; filter with GET /api/v1/projects?tag=<tag>.
AUTH_ENABLEDfalseEnable API authentication middleware. When false (default), all API endpoints are unauthenticated.
SERVICE_TOKEN(empty)Shared secret for upstream proxy/gateway integrations. Accepted via Authorization: Bearer <token> or X-Service-Token: <token>.
API_KEYS(empty)Comma-separated list of API keys for direct API consumers (CI/CD, scripts). Accepted via X-API-Key: <key>.
MAX_UPLOAD_SIZE_MB50Max request body size for POST /api/v1/sboms/upload. The endpoint requires AUTH_ENABLED=true.
CUSTOM_THEME(example file)Path to a custom CSS theme file for the UI. See "Custom Theme" section.
UI_CONFIG./ui/public/ui-config.jsonPath to a JSON file with UI text overrides (brand name, dashboard texts, disclaimer). See "Site Configuration" section.
S3_BUCKETS(empty)JSON array of S3 bucket configs (supports per-bucket cluster override and a skipScan flag for the push-upload target). See "S3 Ingestion" section.
S3_BUCKET(empty)Single S3 bucket name (simpler alternative to S3_BUCKETS).
S3_ENDPOINTs3.amazonaws.comS3 endpoint URL.
S3_REGIONus-east-1AWS region.
S3_ACCESS_KEY(empty)Shared S3 access key (applied to all buckets). Leave empty for public buckets.
S3_SECRET_KEY(empty)Shared S3 secret key.

After changing .env:

# Apply new values (keeps ClickHouse data):
docker compose up -d --force-recreate

# Or full reset (wipes ClickHouse data):
make dev-reset

Configuration Files

Two JSON config files control license governance. Edit them and restart the affected services.

FileMounted inPurpose
sboms/license-policy.jsonAPI Gateway, WorkersDefines which SPDX IDs are permissive vs. copyleft. Anything not listed = unknown.
sboms/license-exceptions.jsonAPI Gateway, WorkersEmpty by default. Explicit organization-approved blanket or package/license/project exceptions. Structure and configuration.

Custom Theme (CSS)

The entire UI color scheme is defined via CSS Custom Properties and can be overridden without rebuilding Angular.

Local (Docker Compose): Create a CSS file and set CUSTOM_THEME in .env:

# .env
CUSTOM_THEME=./my-theme.css
/* my-theme.css */
:root {
  --accent: #e94560;
  --nav-bg: #1a1a2e;
  --nav-brand: #e94560;
  --severity-critical: #ff4444;
  --license-permissive: #22c55e;
}
docker compose up -d --force-recreate ui

Kubernetes: Enable the theme ConfigMap in Helm values:

ui:
  customTheme:
    enabled: true

Then edit the ConfigMap:

kubectl create configmap bomhort-custom-theme \
  --from-file=custom-theme.css=./my-theme.css \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment bomhort-ui

See ui/src/assets/custom-theme.example.css for all available variables.

Site Configuration (Texts & Branding)

All UI text content (brand name, page title, dashboard description, disclaimer, etc.) can be customised without rebuilding Angular via a ui-config.json file.

Local (Docker Compose): Edit the default file directly or point to your own:

# Option 1: Edit the built-in default
vim ui/public/ui-config.json

# Option 2: Use a custom file via .env
UI_CONFIG=./my-ui-config.json
docker compose up -d --force-recreate ui

Example ui-config.json:

{
  "brandName": "My SBOM Platform",
  "pageTitle": "My SBOM Platform",
  "dashboard": {
    "title": "Overview",
    "subtitle": "Software Supply Chain Governance",
    "description": "<strong>Welcome</strong> to our internal SBOM governance platform.",
    "disclaimer": "Internal use only. Data sourced from OSV and GitHub."
  },
  "footer": {
    "enabled": true,
    "text": "© 2026 My Company"
  }
}

All fields are optional — any missing key falls back to the built-in BOMHort default. HTML is supported in description and disclaimer.

Kubernetes: Enable the site config in Helm values:

ui:
  siteConfig:
    enabled: true
    content:
      brandName: "My SBOM Platform"
      pageTitle: "My SBOM Platform"
      dashboard:
        title: "Overview"
        subtitle: "Software Supply Chain Governance"
        description: "<strong>Welcome</strong> to our SBOM platform."
        disclaimer: "Internal use only."

Changes take effect after a pod restart (kubectl rollout restart deployment bomhort-ui). No rebuild needed.

S3 Ingestion (Default)

Ingest SBOMs directly from S3-compatible buckets (AWS S3, MinIO, GCS). This is the default and recommended ingestion method — no volumes, PVCs, or git-sync needed.

Single bucket:

# .env
S3_BUCKET=my-org-sboms
S3_ENDPOINT=s3.amazonaws.com
S3_REGION=us-east-1

Multiple buckets (JSON array):

# .env
S3_BUCKETS='[{"name":"my-org-sboms","endpoint":"s3.amazonaws.com","region":"us-east-1"},{"name":"platform-sboms","region":"us-east-1"}]'

Private buckets with credentials:

# .env (shared credentials for all buckets)
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
S3_BUCKETS='[{"name":"my-private-bucket"}]'

# Or per-bucket credentials in JSON:
S3_BUCKETS='[{"name":"my-bucket","accessKey":"AKIA...","secretKey":"..."}]'

Multi-cluster: per-bucket cluster assignment:

# .env — each bucket maps to a different cluster
CLUSTER_NAME=default
S3_BUCKETS='[
  {"name":"prod-eu-sboms", "cluster":"prod-eu", "region":"eu-west-1"},
  {"name":"prod-us-sboms", "cluster":"prod-us", "region":"us-east-1"},
  {"name":"staging-sboms", "cluster":"staging"}
]'

Buckets without a cluster field inherit the global CLUSTER_NAME. If neither is set, data is untagged (single-instance mode).

Cluster is one of three ownership dimensions — namespace (#138) and project (#57) work the same way (NAMESPACE/PROJECT defaults, per-bucket fields, ?namespace=/?project= on upload), and INGEST_PATH_LAYOUT can derive all three from the ingestion path. See the deployment guide for details.

How it works:

  • The Ingestion Watcher streams ListObjects from each bucket (paginated, no full listing in memory)
  • Files are classified by extension: *.spdx.json / *_spdx.json → SBOM, *.openvex.json / *.vex.json → VEX
  • SHA256 hashes are computed by streaming the object (not loaded into memory)
  • Jobs are enqueued in batches of 500 for efficient ClickHouse inserts
  • The Parsing Worker fetches S3 objects on-demand via s3://bucket/key URIs
  • Local filesystem ingestion (from SBOM_SOURCE_DIR) still works alongside S3
# After editing config files:
docker compose up -d --force-recreate api-gateway parsing-worker

See docs/DEPLOYMENT_GUIDE.md for Kubernetes deployment instructions.

Option B: Local Kubernetes (Kind)

Deploy the full stack to a local Kind cluster, including ClickHouse Operator, SBOM ingestion, and the Angular UI:

# 1. Copy secrets template and fill in your values
cp examples/kind/secrets.env.example local/secrets.env
vi local/secrets.env

# 2. Deploy
make kind-up

# UI: http://localhost:8090   API: http://localhost:8080/healthz

See examples/ for Kind and production Kubernetes deployment configs.

Option C: Local Development (Hot Reload)

Use this when you want to iterate on code quickly:

# 1. Start only ClickHouse
make ch-only

# 2. Run the migrations (first time only)
make ch-migrate

# 3. In separate terminals:

# Terminal 1: API Gateway
make api

# Terminal 2: Run Ingestion Watcher (once)
make ingest

# Terminal 3: Start Parsing Worker
make worker

# Terminal 4: Angular dev server (hot reload, proxied to API)
make ui-dev

Open http://localhost:4200 — Angular proxies /api/* to localhost:8080.


Architecture

sboms/*.spdx.json + *.openvex.json
       │
       ▼
┌─────────────────────────┐
│   Ingestion Watcher     │  CronJob: scans files, deduplicates by SHA256,
│   (Go binary)           │  enqueues jobs into ClickHouse queue
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   Parsing Workers (N)   │  Stateless: claims jobs, parses SPDX/VEX
│   (Go binary)           │  (supports plain SPDX + in-toto attestation
│                         │  envelopes), resolves unknown licenses via
│                         │  GitHub API (50+ well-known Go module mappings),
│                         │  batch-INSERTs resolved data into ClickHouse,
│                         │  then queries OSV for vulns and checks license
│                         │  compliance
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   CVE Refresher         │  CronJob (daily): checks all PURLs for new CVEs
│   (Go binary)           │  without re-scanning all SBOMs
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   ClickHouse            │  11 tables: sboms, sbom_packages, vulnerabilities,
│                         │  license_compliance, vex_statements, ingestion_queue,
│                         │  dashboard_stats_mv, cve_refresh_log, github_license_cache,
│                         │  github_repo_metadata
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   API Gateway           │  25 REST endpoints, stateless
│   (Go binary)           │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   Angular UI            │  10 lazy-loaded pages, virtual scrolling,
│                         │  OnPush change detection, dark mode,
│                         │  CSS custom properties theming
└─────────────────────────┘

Parsing Pipeline

The Parsing Worker processes each SBOM in a carefully ordered pipeline:

  1. Parse — Auto-detect format and decode:
    • SPDX 2.x JSON (plain documents with spdxVersion field)
    • SPDX 3 JSON-LD (detected via @context: https://spdx.org/rdf/3.x/..., parsed via protobom)
    • In-toto attestation envelopes (SPDX wrapped in predicate field, common with Syft/BuildKit)
    • CycloneDX JSON (detected via bomFormat: "CycloneDX", versions 1.0–1.7)
    • Optional: Set USE_PROTOBOM=true to delegate all parsing to protobom for maximum format coverage
  2. Resolve Licenses — For packages with NOASSERTION/empty licenses, query the GitHub API using three resolution strategies:
    • Direct github.com/{owner}/{repo} extraction from PURLs
    • Well-known Go module mappings (50+ entries: golang.org/x/*golang/*, gopkg.in/*, go.uber.org/*, k8s.io/*, dario.cat/mergo, etc.)
    • Fallback to the dedicated GitHub /repos/{owner}/{repo}/license endpoint
    • Static overrides for repos where GitHub misdetects the license (e.g., opencontainers/go-digest, shopspring/decimal)
  3. Insert — Batch-INSERT SBOM metadata and packages (with resolved licenses) into ClickHouse
  4. Scan Vulnerabilities — OSV batch query for all PURLs
  5. Check License Compliance — Classify licenses against the policy and apply exceptions

See docs/ARCHITECTURE_PLAN.md for the full blueprint.
See docs/DEPLOYMENT_GUIDE.md for Kubernetes deployment.
See docs/RELEASE.md for building and publishing container images.
See docs/TESTING.md for writing and running tests.
See the API Reference for complete endpoint documentation.


Makefile Commands

CommandDescription
Docker Compose
make devStart full stack via Docker Compose
make dev-downStop all containers
make dev-restartRestart with new .env values (keeps data)
make dev-logsFollow all container logs
make dev-resetDestroy data volumes and restart fresh
make dev-statusShow container status and ingestion progress
make re-ingestRe-trigger the Ingestion Watcher (scans for new files)
make re-scanWipe all data and re-process everything (e.g. after enabling OSV)
make cve-refreshCheck all known PURLs for new CVEs (without re-scanning SBOMs)
make migrateRun all pending database migrations
Kind (Local Kubernetes)
make kind-upCreate Kind cluster and deploy BOMHort via Helm
make kind-downDestroy the Kind cluster (deletes everything)
make kind-stopStop the Kind cluster without losing data (preserves volumes)
make kind-startResume a stopped Kind cluster (all pods & data intact)
make kind-statusShow Kind cluster and pod status
make kind-buildBuild all container images and load them into Kind
make kind-deployBuild images, Helm upgrade, and restart pods
make kind-reingestRe-ingest all SBOMs (truncate data, re-queue, no re-download)
ClickHouse
make ch-onlyStart only ClickHouse (for local dev)
make ch-migrateRun SQL migrations against ClickHouse
make ch-shellOpen ClickHouse CLI
Local Dev
make apiRun API Gateway locally
make ingestRun Ingestion Watcher locally
make workerRun Parsing Worker locally
make ui-devStart Angular dev server with API proxy
make backend-buildBuild all Go binaries
make backend-testRun all Go tests
make backend-vetRun go vet + go fmt
make ui-buildBuild Angular for production
Images
make imagesBuild all 5 container images locally (TAG=dev)
make images-pushBuild and push all images to GHCR

API Endpoints

MethodEndpointDescription
GET/healthzHealth check
GET/api/v1/stats/dashboardDashboard stats (VEX effective/suppressed counts)
GET/api/v1/stats/dependencies?limit=NTop N dependencies across all projects
GET/api/v1/stats/version-skew?page=&page_size=&search=Packages with inconsistent versions across projects
GET/api/v1/sboms?page=&page_size=&search=Paginated SBOM list (searchable)
GET/api/v1/sboms/{id}/detailSBOM detail with severity breakdown
GET/api/v1/sboms/{id}/vulnerabilitiesVulnerabilities for a specific SBOM
GET/api/v1/sboms/{id}/licensesLicense breakdown for a specific SBOM
GET/api/v1/sboms/{id}/dependenciesDependency tree
GET/api/v1/vulnerabilities?page=&page_size=Paginated vulnerabilities (every finding, VEX status attached)
GET/api/v1/vulnerabilities/{id}/affected-projectsAll projects affected by a CVE
GET/api/v1/licenses/complianceGlobal license compliance overview
GET/api/v1/projects/license-complianceProjects with license violations (filtered by exceptions)
GET/api/v1/license-exceptionsActive license exceptions (read-only, from config file)
GET/api/v1/license-policyActive license classification policy (permissive/copyleft lists)
GET/api/v1/vex/statements?page=&page_size=Paginated VEX statements
GET/api/v1/packages/archivedPackages using archived GitHub repos (no longer maintained)
GET/api/v1/packages/search?q=&page=&page_size=Package name search across all SBOMs
GET/api/v1/search?q=&limit=Global faceted search (packages, projects, CVEs, licenses)
GET/api/v1/packages/detail?name=&page=&page_size=All projects using a specific package (paginated)

For complete API documentation with request/response examples, see the API Reference.


Adding Your SBOMs

  1. Place .spdx.json files in the sboms/ directory (or set SBOM_SOURCE_DIR in .env)
  2. Place .openvex.json or .vex.json files in the same directory
  3. Re-trigger ingestion (see below)
  4. The Parsing Worker will automatically process new files

The Ingestion Watcher deduplicates by SHA256 hash — it will skip files that have already been processed.

Re-triggering Ingestion

# Run the watcher again (scans for new files, exits when done):
docker compose up ingestion-watcher

# If you changed SBOM_LIMIT or SBOM_SOURCE_DIR, force-recreate:
docker compose up --force-recreate ingestion-watcher

# To re-ingest everything from scratch (wipes all data):
make dev-reset

License Policy

The bundled default policy is derived from the CNCF Allowed Third-Party License Policy, because it is a well-reviewed public baseline — not because BOMHort targets CNCF projects. Replace it wholesale via licensePolicy.custom:

  • Permissive (allowed): Apache-2.0, MIT, MIT-0, 0BSD, BSD-2-Clause, BSD-3-Clause, ISC, PSF-2.0, Python-2.0, PostgreSQL, UPL-1.0, X11, Zlib, OpenSSL, and a few more (18 total)
  • Copyleft (flagged): GPL, LGPL, AGPL, MPL-2.0, EPL, EUPL, CPAL, and others (21 total)
  • Unknown: Any license not in either list is flagged for review

License Exceptions

No exceptions are enabled or downloaded by default. Configure only approvals that apply to your organization using licenseExceptions.custom (a YAML object or JSON string), or --set-file licenseExceptions.custom=./my-exceptions.json with Helm. Docker Compose uses the initially empty sboms/license-exceptions.json.

The inactive example and migration guide describe the structure and matching rules. Package exceptions stay package-scoped; project restricts them to an exact SBOM document name. There is no CNCF-specific blanket promotion. An empty configuration is authoritative and does not fall back to old approvals in the SBOM directory.

Helm changes roll out both API and workers. Re-process existing SBOMs to update stored results after changing approvals; a watcher run alone skips unchanged files.

Customising the Policy

Override the default policy via Helm values:

licensePolicy:
  custom: |
    {
      "permissive": ["Apache-2.0", "MIT"],
      "copyleft": ["GPL-3.0-only", "AGPL-3.0-only"]
    }

Or edit the ConfigMap directly:

kubectl edit configmap bomhort-license-policy -n bomhort

Tech Stack

LayerTechnology
BackendGo 1.26, net/http (stdlib)
DatabaseClickHouse (MergeTree family)
FrontendAngular 19, CDK Virtual Scrolling
Vuln ScanningOSV.dev API
VEXOpenVEX Spec v0.2.0
DeploymentHelm 3, Docker Compose

Subprojects

The seebom-labs organization hosts additional BOMHort subprojects:

See all repositories: https://github.com/seebom-labs


Contributing

We welcome contributions! See the Contributing Guide for how to get started.


License

Apache License 2.0

Badges

OpenSSF Scorecard

sbom
sbom-quality
security
suite
supply-chain
supply-chain-security

Contributors

mfahlandt

146 commits

dependabot[bot]

82 commits

koksay

44 commits

jeefy

8 commits

seebom-labs/BOMHort

About standalone, Kubernetes-native Software Bill of Materials (SBOM) visualization and governance platform

Go

53

283 commits

updated Sep 22, 2026

See the code

README

BOMHort

Kubernetes-native Software Bill of Materials (SBOM) Visualization & Governance Platform

CI OpenSSF Scorecard OpenSSF Best Practices

Ingest 1000+ SPDX and CycloneDX SBOMs, scan for vulnerabilities via OSV, enforce license compliance, and apply VEX statements — all visualized in a fast Angular dashboard backed by ClickHouse analytics.

BOMHort (formerly known as SeeBOM) is the same project with a new name. Read more: Why we renamed SeeBOM to BOMHort.

Getting Started · Architecture · Roadmap · Contributing · AI Policy

BOMHort Dashboard


Quick Start

Prerequisites

ToolMinimum Version
Docker + Docker Composev2.20+
Go1.26+ (only for local dev) — backend/go.mod pins go 1.26.8
Node.js22+ (only for local dev)
# 1. Clone the repo
git clone https://github.com/seebom-labs/BOMHort.git && cd BOMHort

# 2. Place your SBOM files in the sboms/ directory
#    Supports SPDX 2.x JSON, SPDX 3 JSON-LD, CycloneDX JSON, and in-toto attestation envelopes (auto-detected)
#    (examples included: sboms/_example.spdx.json, sboms/_example.cdx.json)

# 3. Start everything
make dev

# Or without make:
docker compose up --build -d

This starts:

  • ClickHouse on localhost:9000 (TCP) / localhost:8123 (HTTP)
  • API Gateway on localhost:8080
  • Ingestion Watcher (runs once, scans sboms/ for new files)
  • Parsing Worker (processes queued SBOM/VEX files)
  • Angular UI on localhost:8090

Open http://localhost:8090 in your browser.

Configuration (.env)

Copy .env.example to .env and adjust:

cp .env.example .env
VariableDefaultDescription
SBOM_SOURCE_DIR./sbomsPath to your SBOM files (can point to an external repo checkout)
SBOM_LIMIT0Max SBOMs to enqueue per watcher run. 0 = unlimited. Use 50200 for local dev.
WORKER_REPLICAS1Number of parallel parsing worker containers
WORKER_BATCH_SIZE50Jobs claimed per polling cycle per worker
SKIP_OSVfalseSkip OSV vulnerability API calls. Set true for fast initial bulk load (licenses only), then re-run with false.
SKIP_GITHUB_RESOLVEfalseSkip GitHub license resolution for packages with NOASSERTION/empty licenses.
GITHUB_TOKEN(empty)GitHub personal access token for license resolution. Increases rate limit from 60 to 5000 req/h. No scopes needed.
SKIP_NPM_RESOLVEfalseSkip npm registry license resolution for pkg:npm/* packages with NOASSERTION/empty licenses.
SKIP_NUGET_RESOLVEfalseSkip NuGet license resolution for pkg:nuget/* packages with NOASSERTION/empty licenses. Legacy packages without licenseExpression fall back to their GitHub repository license.
CLUSTER_NAME(empty)Cluster identifier for multi-cluster deployments. All ingested data is tagged with this value. Empty = single-instance mode.
NAMESPACE(empty)Default deployment-namespace label (#138) stamped onto all ingested data. Overridable per bucket and per upload (?namespace=).
PROJECT(empty)Default project label (#57) stamped onto all ingested data. Overridable per bucket and per upload (?project=).
INGEST_PATH_LAYOUT(empty)Opt-in: derive cluster/namespace/project from an SBOM's position in the source, e.g. cluster/namespace/project for keys like prod-eu/payments/payment-service/app.spdx.json. _ skips a level. A malformed layout fails at startup. Explicit config always outranks derivation.
TAGS(empty)Comma-separated free-form grouping labels (#357) stamped onto all ingested data, e.g. sandbox-applications,platform. Tags group projects — they do not replace them: a project keeps its identity and can carry several tags. Unlike CLUSTER_NAME/NAMESPACE/PROJECT, per-bucket and per-upload (?tags=) values are merged, not overridden. Normalised to lowercase, trimmed, deduplicated and sorted. Read back via GET /api/v1/tags; filter with GET /api/v1/projects?tag=<tag>.
AUTH_ENABLEDfalseEnable API authentication middleware. When false (default), all API endpoints are unauthenticated.
SERVICE_TOKEN(empty)Shared secret for upstream proxy/gateway integrations. Accepted via Authorization: Bearer <token> or X-Service-Token: <token>.
API_KEYS(empty)Comma-separated list of API keys for direct API consumers (CI/CD, scripts). Accepted via X-API-Key: <key>.
MAX_UPLOAD_SIZE_MB50Max request body size for POST /api/v1/sboms/upload. The endpoint requires AUTH_ENABLED=true.
CUSTOM_THEME(example file)Path to a custom CSS theme file for the UI. See "Custom Theme" section.
UI_CONFIG./ui/public/ui-config.jsonPath to a JSON file with UI text overrides (brand name, dashboard texts, disclaimer). See "Site Configuration" section.
S3_BUCKETS(empty)JSON array of S3 bucket configs (supports per-bucket cluster override and a skipScan flag for the push-upload target). See "S3 Ingestion" section.
S3_BUCKET(empty)Single S3 bucket name (simpler alternative to S3_BUCKETS).
S3_ENDPOINTs3.amazonaws.comS3 endpoint URL.
S3_REGIONus-east-1AWS region.
S3_ACCESS_KEY(empty)Shared S3 access key (applied to all buckets). Leave empty for public buckets.
S3_SECRET_KEY(empty)Shared S3 secret key.

After changing .env:

# Apply new values (keeps ClickHouse data):
docker compose up -d --force-recreate

# Or full reset (wipes ClickHouse data):
make dev-reset

Configuration Files

Two JSON config files control license governance. Edit them and restart the affected services.

FileMounted inPurpose
sboms/license-policy.jsonAPI Gateway, WorkersDefines which SPDX IDs are permissive vs. copyleft. Anything not listed = unknown.
sboms/license-exceptions.jsonAPI Gateway, WorkersEmpty by default. Explicit organization-approved blanket or package/license/project exceptions. Structure and configuration.

Custom Theme (CSS)

The entire UI color scheme is defined via CSS Custom Properties and can be overridden without rebuilding Angular.

Local (Docker Compose): Create a CSS file and set CUSTOM_THEME in .env:

# .env
CUSTOM_THEME=./my-theme.css
/* my-theme.css */
:root {
  --accent: #e94560;
  --nav-bg: #1a1a2e;
  --nav-brand: #e94560;
  --severity-critical: #ff4444;
  --license-permissive: #22c55e;
}
docker compose up -d --force-recreate ui

Kubernetes: Enable the theme ConfigMap in Helm values:

ui:
  customTheme:
    enabled: true

Then edit the ConfigMap:

kubectl create configmap bomhort-custom-theme \
  --from-file=custom-theme.css=./my-theme.css \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment bomhort-ui

See ui/src/assets/custom-theme.example.css for all available variables.

Site Configuration (Texts & Branding)

All UI text content (brand name, page title, dashboard description, disclaimer, etc.) can be customised without rebuilding Angular via a ui-config.json file.

Local (Docker Compose): Edit the default file directly or point to your own:

# Option 1: Edit the built-in default
vim ui/public/ui-config.json

# Option 2: Use a custom file via .env
UI_CONFIG=./my-ui-config.json
docker compose up -d --force-recreate ui

Example ui-config.json:

{
  "brandName": "My SBOM Platform",
  "pageTitle": "My SBOM Platform",
  "dashboard": {
    "title": "Overview",
    "subtitle": "Software Supply Chain Governance",
    "description": "<strong>Welcome</strong> to our internal SBOM governance platform.",
    "disclaimer": "Internal use only. Data sourced from OSV and GitHub."
  },
  "footer": {
    "enabled": true,
    "text": "© 2026 My Company"
  }
}

All fields are optional — any missing key falls back to the built-in BOMHort default. HTML is supported in description and disclaimer.

Kubernetes: Enable the site config in Helm values:

ui:
  siteConfig:
    enabled: true
    content:
      brandName: "My SBOM Platform"
      pageTitle: "My SBOM Platform"
      dashboard:
        title: "Overview"
        subtitle: "Software Supply Chain Governance"
        description: "<strong>Welcome</strong> to our SBOM platform."
        disclaimer: "Internal use only."

Changes take effect after a pod restart (kubectl rollout restart deployment bomhort-ui). No rebuild needed.

S3 Ingestion (Default)

Ingest SBOMs directly from S3-compatible buckets (AWS S3, MinIO, GCS). This is the default and recommended ingestion method — no volumes, PVCs, or git-sync needed.

Single bucket:

# .env
S3_BUCKET=my-org-sboms
S3_ENDPOINT=s3.amazonaws.com
S3_REGION=us-east-1

Multiple buckets (JSON array):

# .env
S3_BUCKETS='[{"name":"my-org-sboms","endpoint":"s3.amazonaws.com","region":"us-east-1"},{"name":"platform-sboms","region":"us-east-1"}]'

Private buckets with credentials:

# .env (shared credentials for all buckets)
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=...
S3_BUCKETS='[{"name":"my-private-bucket"}]'

# Or per-bucket credentials in JSON:
S3_BUCKETS='[{"name":"my-bucket","accessKey":"AKIA...","secretKey":"..."}]'

Multi-cluster: per-bucket cluster assignment:

# .env — each bucket maps to a different cluster
CLUSTER_NAME=default
S3_BUCKETS='[
  {"name":"prod-eu-sboms", "cluster":"prod-eu", "region":"eu-west-1"},
  {"name":"prod-us-sboms", "cluster":"prod-us", "region":"us-east-1"},
  {"name":"staging-sboms", "cluster":"staging"}
]'

Buckets without a cluster field inherit the global CLUSTER_NAME. If neither is set, data is untagged (single-instance mode).

Cluster is one of three ownership dimensions — namespace (#138) and project (#57) work the same way (NAMESPACE/PROJECT defaults, per-bucket fields, ?namespace=/?project= on upload), and INGEST_PATH_LAYOUT can derive all three from the ingestion path. See the deployment guide for details.

How it works:

  • The Ingestion Watcher streams ListObjects from each bucket (paginated, no full listing in memory)
  • Files are classified by extension: *.spdx.json / *_spdx.json → SBOM, *.openvex.json / *.vex.json → VEX
  • SHA256 hashes are computed by streaming the object (not loaded into memory)
  • Jobs are enqueued in batches of 500 for efficient ClickHouse inserts
  • The Parsing Worker fetches S3 objects on-demand via s3://bucket/key URIs
  • Local filesystem ingestion (from SBOM_SOURCE_DIR) still works alongside S3
# After editing config files:
docker compose up -d --force-recreate api-gateway parsing-worker

See docs/DEPLOYMENT_GUIDE.md for Kubernetes deployment instructions.

Option B: Local Kubernetes (Kind)

Deploy the full stack to a local Kind cluster, including ClickHouse Operator, SBOM ingestion, and the Angular UI:

# 1. Copy secrets template and fill in your values
cp examples/kind/secrets.env.example local/secrets.env
vi local/secrets.env

# 2. Deploy
make kind-up

# UI: http://localhost:8090   API: http://localhost:8080/healthz

See examples/ for Kind and production Kubernetes deployment configs.

Option C: Local Development (Hot Reload)

Use this when you want to iterate on code quickly:

# 1. Start only ClickHouse
make ch-only

# 2. Run the migrations (first time only)
make ch-migrate

# 3. In separate terminals:

# Terminal 1: API Gateway
make api

# Terminal 2: Run Ingestion Watcher (once)
make ingest

# Terminal 3: Start Parsing Worker
make worker

# Terminal 4: Angular dev server (hot reload, proxied to API)
make ui-dev

Open http://localhost:4200 — Angular proxies /api/* to localhost:8080.


Architecture

sboms/*.spdx.json + *.openvex.json
       │
       ▼
┌─────────────────────────┐
│   Ingestion Watcher     │  CronJob: scans files, deduplicates by SHA256,
│   (Go binary)           │  enqueues jobs into ClickHouse queue
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   Parsing Workers (N)   │  Stateless: claims jobs, parses SPDX/VEX
│   (Go binary)           │  (supports plain SPDX + in-toto attestation
│                         │  envelopes), resolves unknown licenses via
│                         │  GitHub API (50+ well-known Go module mappings),
│                         │  batch-INSERTs resolved data into ClickHouse,
│                         │  then queries OSV for vulns and checks license
│                         │  compliance
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   CVE Refresher         │  CronJob (daily): checks all PURLs for new CVEs
│   (Go binary)           │  without re-scanning all SBOMs
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   ClickHouse            │  11 tables: sboms, sbom_packages, vulnerabilities,
│                         │  license_compliance, vex_statements, ingestion_queue,
│                         │  dashboard_stats_mv, cve_refresh_log, github_license_cache,
│                         │  github_repo_metadata
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   API Gateway           │  25 REST endpoints, stateless
│   (Go binary)           │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   Angular UI            │  10 lazy-loaded pages, virtual scrolling,
│                         │  OnPush change detection, dark mode,
│                         │  CSS custom properties theming
└─────────────────────────┘

Parsing Pipeline

The Parsing Worker processes each SBOM in a carefully ordered pipeline:

  1. Parse — Auto-detect format and decode:
    • SPDX 2.x JSON (plain documents with spdxVersion field)
    • SPDX 3 JSON-LD (detected via @context: https://spdx.org/rdf/3.x/..., parsed via protobom)
    • In-toto attestation envelopes (SPDX wrapped in predicate field, common with Syft/BuildKit)
    • CycloneDX JSON (detected via bomFormat: "CycloneDX", versions 1.0–1.7)
    • Optional: Set USE_PROTOBOM=true to delegate all parsing to protobom for maximum format coverage
  2. Resolve Licenses — For packages with NOASSERTION/empty licenses, query the GitHub API using three resolution strategies:
    • Direct github.com/{owner}/{repo} extraction from PURLs
    • Well-known Go module mappings (50+ entries: golang.org/x/*golang/*, gopkg.in/*, go.uber.org/*, k8s.io/*, dario.cat/mergo, etc.)
    • Fallback to the dedicated GitHub /repos/{owner}/{repo}/license endpoint
    • Static overrides for repos where GitHub misdetects the license (e.g., opencontainers/go-digest, shopspring/decimal)
  3. Insert — Batch-INSERT SBOM metadata and packages (with resolved licenses) into ClickHouse
  4. Scan Vulnerabilities — OSV batch query for all PURLs
  5. Check License Compliance — Classify licenses against the policy and apply exceptions

See docs/ARCHITECTURE_PLAN.md for the full blueprint.
See docs/DEPLOYMENT_GUIDE.md for Kubernetes deployment.
See docs/RELEASE.md for building and publishing container images.
See docs/TESTING.md for writing and running tests.
See the API Reference for complete endpoint documentation.


Makefile Commands

CommandDescription
Docker Compose
make devStart full stack via Docker Compose
make dev-downStop all containers
make dev-restartRestart with new .env values (keeps data)
make dev-logsFollow all container logs
make dev-resetDestroy data volumes and restart fresh
make dev-statusShow container status and ingestion progress
make re-ingestRe-trigger the Ingestion Watcher (scans for new files)
make re-scanWipe all data and re-process everything (e.g. after enabling OSV)
make cve-refreshCheck all known PURLs for new CVEs (without re-scanning SBOMs)
make migrateRun all pending database migrations
Kind (Local Kubernetes)
make kind-upCreate Kind cluster and deploy BOMHort via Helm
make kind-downDestroy the Kind cluster (deletes everything)
make kind-stopStop the Kind cluster without losing data (preserves volumes)
make kind-startResume a stopped Kind cluster (all pods & data intact)
make kind-statusShow Kind cluster and pod status
make kind-buildBuild all container images and load them into Kind
make kind-deployBuild images, Helm upgrade, and restart pods
make kind-reingestRe-ingest all SBOMs (truncate data, re-queue, no re-download)
ClickHouse
make ch-onlyStart only ClickHouse (for local dev)
make ch-migrateRun SQL migrations against ClickHouse
make ch-shellOpen ClickHouse CLI
Local Dev
make apiRun API Gateway locally
make ingestRun Ingestion Watcher locally
make workerRun Parsing Worker locally
make ui-devStart Angular dev server with API proxy
make backend-buildBuild all Go binaries
make backend-testRun all Go tests
make backend-vetRun go vet + go fmt
make ui-buildBuild Angular for production
Images
make imagesBuild all 5 container images locally (TAG=dev)
make images-pushBuild and push all images to GHCR

API Endpoints

MethodEndpointDescription
GET/healthzHealth check
GET/api/v1/stats/dashboardDashboard stats (VEX effective/suppressed counts)
GET/api/v1/stats/dependencies?limit=NTop N dependencies across all projects
GET/api/v1/stats/version-skew?page=&page_size=&search=Packages with inconsistent versions across projects
GET/api/v1/sboms?page=&page_size=&search=Paginated SBOM list (searchable)
GET/api/v1/sboms/{id}/detailSBOM detail with severity breakdown
GET/api/v1/sboms/{id}/vulnerabilitiesVulnerabilities for a specific SBOM
GET/api/v1/sboms/{id}/licensesLicense breakdown for a specific SBOM
GET/api/v1/sboms/{id}/dependenciesDependency tree
GET/api/v1/vulnerabilities?page=&page_size=Paginated vulnerabilities (every finding, VEX status attached)
GET/api/v1/vulnerabilities/{id}/affected-projectsAll projects affected by a CVE
GET/api/v1/licenses/complianceGlobal license compliance overview
GET/api/v1/projects/license-complianceProjects with license violations (filtered by exceptions)
GET/api/v1/license-exceptionsActive license exceptions (read-only, from config file)
GET/api/v1/license-policyActive license classification policy (permissive/copyleft lists)
GET/api/v1/vex/statements?page=&page_size=Paginated VEX statements
GET/api/v1/packages/archivedPackages using archived GitHub repos (no longer maintained)
GET/api/v1/packages/search?q=&page=&page_size=Package name search across all SBOMs
GET/api/v1/search?q=&limit=Global faceted search (packages, projects, CVEs, licenses)
GET/api/v1/packages/detail?name=&page=&page_size=All projects using a specific package (paginated)

For complete API documentation with request/response examples, see the API Reference.


Adding Your SBOMs

  1. Place .spdx.json files in the sboms/ directory (or set SBOM_SOURCE_DIR in .env)
  2. Place .openvex.json or .vex.json files in the same directory
  3. Re-trigger ingestion (see below)
  4. The Parsing Worker will automatically process new files

The Ingestion Watcher deduplicates by SHA256 hash — it will skip files that have already been processed.

Re-triggering Ingestion

# Run the watcher again (scans for new files, exits when done):
docker compose up ingestion-watcher

# If you changed SBOM_LIMIT or SBOM_SOURCE_DIR, force-recreate:
docker compose up --force-recreate ingestion-watcher

# To re-ingest everything from scratch (wipes all data):
make dev-reset

License Policy

The bundled default policy is derived from the CNCF Allowed Third-Party License Policy, because it is a well-reviewed public baseline — not because BOMHort targets CNCF projects. Replace it wholesale via licensePolicy.custom:

  • Permissive (allowed): Apache-2.0, MIT, MIT-0, 0BSD, BSD-2-Clause, BSD-3-Clause, ISC, PSF-2.0, Python-2.0, PostgreSQL, UPL-1.0, X11, Zlib, OpenSSL, and a few more (18 total)
  • Copyleft (flagged): GPL, LGPL, AGPL, MPL-2.0, EPL, EUPL, CPAL, and others (21 total)
  • Unknown: Any license not in either list is flagged for review

License Exceptions

No exceptions are enabled or downloaded by default. Configure only approvals that apply to your organization using licenseExceptions.custom (a YAML object or JSON string), or --set-file licenseExceptions.custom=./my-exceptions.json with Helm. Docker Compose uses the initially empty sboms/license-exceptions.json.

The inactive example and migration guide describe the structure and matching rules. Package exceptions stay package-scoped; project restricts them to an exact SBOM document name. There is no CNCF-specific blanket promotion. An empty configuration is authoritative and does not fall back to old approvals in the SBOM directory.

Helm changes roll out both API and workers. Re-process existing SBOMs to update stored results after changing approvals; a watcher run alone skips unchanged files.

Customising the Policy

Override the default policy via Helm values:

licensePolicy:
  custom: |
    {
      "permissive": ["Apache-2.0", "MIT"],
      "copyleft": ["GPL-3.0-only", "AGPL-3.0-only"]
    }

Or edit the ConfigMap directly:

kubectl edit configmap bomhort-license-policy -n bomhort

Tech Stack

LayerTechnology
BackendGo 1.26, net/http (stdlib)
DatabaseClickHouse (MergeTree family)
FrontendAngular 19, CDK Virtual Scrolling
Vuln ScanningOSV.dev API
VEXOpenVEX Spec v0.2.0
DeploymentHelm 3, Docker Compose

Subprojects

The seebom-labs organization hosts additional BOMHort subprojects:

See all repositories: https://github.com/seebom-labs


Contributing

We welcome contributions! See the Contributing Guide for how to get started.


License

Apache License 2.0

Badges

OpenSSF Scorecard

sbom
sbom-quality
security
suite
supply-chain
supply-chain-security

Contributors

mfahlandt

146 commits

dependabot[bot]

82 commits

koksay

44 commits

jeefy

8 commits

Languages

Go

70.6%

TypeScript

25.1%

Makefile

1.7%

Python

1.6%