About standalone, Kubernetes-native Software Bill of Materials (SBOM) visualization and governance platform
See the code
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
| Tool | Minimum Version |
|---|---|
| Docker + Docker Compose | v2.20+ |
| Go | 1.26+ (only for local dev) — backend/go.mod pins go 1.26.8 |
| Node.js | 22+ (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:
localhost:9000 (TCP) / localhost:8123 (HTTP)localhost:8080sboms/ for new files)localhost:8090Open http://localhost:8090 in your browser.
.env)Copy .env.example to .env and adjust:
cp .env.example .env
| Variable | Default | Description |
|---|---|---|
SBOM_SOURCE_DIR | ./sboms | Path to your SBOM files (can point to an external repo checkout) |
SBOM_LIMIT | 0 | Max SBOMs to enqueue per watcher run. 0 = unlimited. Use 50–200 for local dev. |
WORKER_REPLICAS | 1 | Number of parallel parsing worker containers |
WORKER_BATCH_SIZE | 50 | Jobs claimed per polling cycle per worker |
SKIP_OSV | false | Skip OSV vulnerability API calls. Set true for fast initial bulk load (licenses only), then re-run with false. |
SKIP_GITHUB_RESOLVE | false | Skip 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_RESOLVE | false | Skip npm registry license resolution for pkg:npm/* packages with NOASSERTION/empty licenses. |
SKIP_NUGET_RESOLVE | false | Skip 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_ENABLED | false | Enable 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_MB | 50 | Max 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.json | Path 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_ENDPOINT | s3.amazonaws.com | S3 endpoint URL. |
S3_REGION | us-east-1 | AWS 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
Two JSON config files control license governance. Edit them and restart the affected services.
| File | Mounted in | Purpose |
|---|---|---|
sboms/license-policy.json | API Gateway, Workers | Defines which SPDX IDs are permissive vs. copyleft. Anything not listed = unknown. |
sboms/license-exceptions.json | API Gateway, Workers | Empty by default. Explicit organization-approved blanket or package/license/project exceptions. Structure and configuration. |
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.
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.
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:
ListObjects from each bucket (paginated, no full listing in memory)*.spdx.json / *_spdx.json → SBOM, *.openvex.json / *.vex.json → VEXs3://bucket/key URIsSBOM_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.
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.
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.
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
└─────────────────────────┘
The Parsing Worker processes each SBOM in a carefully ordered pipeline:
spdxVersion field)@context: https://spdx.org/rdf/3.x/..., parsed via protobom)predicate field, common with Syft/BuildKit)bomFormat: "CycloneDX", versions 1.0–1.7)USE_PROTOBOM=true to delegate all parsing to protobom for maximum format coverageNOASSERTION/empty licenses, query the GitHub API using three resolution strategies:
github.com/{owner}/{repo} extraction from PURLsgolang.org/x/* → golang/*, gopkg.in/*, go.uber.org/*, k8s.io/*, dario.cat/mergo, etc.)/repos/{owner}/{repo}/license endpointopencontainers/go-digest, shopspring/decimal)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.
| Command | Description |
|---|---|
| Docker Compose | |
make dev | Start full stack via Docker Compose |
make dev-down | Stop all containers |
make dev-restart | Restart with new .env values (keeps data) |
make dev-logs | Follow all container logs |
make dev-reset | Destroy data volumes and restart fresh |
make dev-status | Show container status and ingestion progress |
make re-ingest | Re-trigger the Ingestion Watcher (scans for new files) |
make re-scan | Wipe all data and re-process everything (e.g. after enabling OSV) |
make cve-refresh | Check all known PURLs for new CVEs (without re-scanning SBOMs) |
make migrate | Run all pending database migrations |
| Kind (Local Kubernetes) | |
make kind-up | Create Kind cluster and deploy BOMHort via Helm |
make kind-down | Destroy the Kind cluster (deletes everything) |
make kind-stop | Stop the Kind cluster without losing data (preserves volumes) |
make kind-start | Resume a stopped Kind cluster (all pods & data intact) |
make kind-status | Show Kind cluster and pod status |
make kind-build | Build all container images and load them into Kind |
make kind-deploy | Build images, Helm upgrade, and restart pods |
make kind-reingest | Re-ingest all SBOMs (truncate data, re-queue, no re-download) |
| ClickHouse | |
make ch-only | Start only ClickHouse (for local dev) |
make ch-migrate | Run SQL migrations against ClickHouse |
make ch-shell | Open ClickHouse CLI |
| Local Dev | |
make api | Run API Gateway locally |
make ingest | Run Ingestion Watcher locally |
make worker | Run Parsing Worker locally |
make ui-dev | Start Angular dev server with API proxy |
make backend-build | Build all Go binaries |
make backend-test | Run all Go tests |
make backend-vet | Run go vet + go fmt |
make ui-build | Build Angular for production |
| Images | |
make images | Build all 5 container images locally (TAG=dev) |
make images-push | Build and push all images to GHCR |
| Method | Endpoint | Description |
|---|---|---|
| GET | /healthz | Health check |
| GET | /api/v1/stats/dashboard | Dashboard stats (VEX effective/suppressed counts) |
| GET | /api/v1/stats/dependencies?limit=N | Top 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}/detail | SBOM detail with severity breakdown |
| GET | /api/v1/sboms/{id}/vulnerabilities | Vulnerabilities for a specific SBOM |
| GET | /api/v1/sboms/{id}/licenses | License breakdown for a specific SBOM |
| GET | /api/v1/sboms/{id}/dependencies | Dependency tree |
| GET | /api/v1/vulnerabilities?page=&page_size= | Paginated vulnerabilities (every finding, VEX status attached) |
| GET | /api/v1/vulnerabilities/{id}/affected-projects | All projects affected by a CVE |
| GET | /api/v1/licenses/compliance | Global license compliance overview |
| GET | /api/v1/projects/license-compliance | Projects with license violations (filtered by exceptions) |
| GET | /api/v1/license-exceptions | Active license exceptions (read-only, from config file) |
| GET | /api/v1/license-policy | Active license classification policy (permissive/copyleft lists) |
| GET | /api/v1/vex/statements?page=&page_size= | Paginated VEX statements |
| GET | /api/v1/packages/archived | Packages 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.
.spdx.json files in the sboms/ directory (or set SBOM_SOURCE_DIR in .env).openvex.json or .vex.json files in the same directoryThe Ingestion Watcher deduplicates by SHA256 hash — it will skip files that have already been processed.
# 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
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:
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.
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
| Layer | Technology |
|---|---|
| Backend | Go 1.26, net/http (stdlib) |
| Database | ClickHouse (MergeTree family) |
| Frontend | Angular 19, CDK Virtual Scrolling |
| Vuln Scanning | OSV.dev API |
| VEX | OpenVEX Spec v0.2.0 |
| Deployment | Helm 3, Docker Compose |
The seebom-labs organization hosts additional BOMHort subprojects:
See all repositories: https://github.com/seebom-labs
We welcome contributions! See the Contributing Guide for how to get started.
Go
70.6%
TypeScript
25.1%
Makefile
1.7%
Python
1.6%
About standalone, Kubernetes-native Software Bill of Materials (SBOM) visualization and governance platform
See the code
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
| Tool | Minimum Version |
|---|---|
| Docker + Docker Compose | v2.20+ |
| Go | 1.26+ (only for local dev) — backend/go.mod pins go 1.26.8 |
| Node.js | 22+ (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:
localhost:9000 (TCP) / localhost:8123 (HTTP)localhost:8080sboms/ for new files)localhost:8090Open http://localhost:8090 in your browser.
.env)Copy .env.example to .env and adjust:
cp .env.example .env
| Variable | Default | Description |
|---|---|---|
SBOM_SOURCE_DIR | ./sboms | Path to your SBOM files (can point to an external repo checkout) |
SBOM_LIMIT | 0 | Max SBOMs to enqueue per watcher run. 0 = unlimited. Use 50–200 for local dev. |
WORKER_REPLICAS | 1 | Number of parallel parsing worker containers |
WORKER_BATCH_SIZE | 50 | Jobs claimed per polling cycle per worker |
SKIP_OSV | false | Skip OSV vulnerability API calls. Set true for fast initial bulk load (licenses only), then re-run with false. |
SKIP_GITHUB_RESOLVE | false | Skip 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_RESOLVE | false | Skip npm registry license resolution for pkg:npm/* packages with NOASSERTION/empty licenses. |
SKIP_NUGET_RESOLVE | false | Skip 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_ENABLED | false | Enable 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_MB | 50 | Max 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.json | Path 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_ENDPOINT | s3.amazonaws.com | S3 endpoint URL. |
S3_REGION | us-east-1 | AWS 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
Two JSON config files control license governance. Edit them and restart the affected services.
| File | Mounted in | Purpose |
|---|---|---|
sboms/license-policy.json | API Gateway, Workers | Defines which SPDX IDs are permissive vs. copyleft. Anything not listed = unknown. |
sboms/license-exceptions.json | API Gateway, Workers | Empty by default. Explicit organization-approved blanket or package/license/project exceptions. Structure and configuration. |
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.
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.
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:
ListObjects from each bucket (paginated, no full listing in memory)*.spdx.json / *_spdx.json → SBOM, *.openvex.json / *.vex.json → VEXs3://bucket/key URIsSBOM_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.
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.
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.
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
└─────────────────────────┘
The Parsing Worker processes each SBOM in a carefully ordered pipeline:
spdxVersion field)@context: https://spdx.org/rdf/3.x/..., parsed via protobom)predicate field, common with Syft/BuildKit)bomFormat: "CycloneDX", versions 1.0–1.7)USE_PROTOBOM=true to delegate all parsing to protobom for maximum format coverageNOASSERTION/empty licenses, query the GitHub API using three resolution strategies:
github.com/{owner}/{repo} extraction from PURLsgolang.org/x/* → golang/*, gopkg.in/*, go.uber.org/*, k8s.io/*, dario.cat/mergo, etc.)/repos/{owner}/{repo}/license endpointopencontainers/go-digest, shopspring/decimal)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.
| Command | Description |
|---|---|
| Docker Compose | |
make dev | Start full stack via Docker Compose |
make dev-down | Stop all containers |
make dev-restart | Restart with new .env values (keeps data) |
make dev-logs | Follow all container logs |
make dev-reset | Destroy data volumes and restart fresh |
make dev-status | Show container status and ingestion progress |
make re-ingest | Re-trigger the Ingestion Watcher (scans for new files) |
make re-scan | Wipe all data and re-process everything (e.g. after enabling OSV) |
make cve-refresh | Check all known PURLs for new CVEs (without re-scanning SBOMs) |
make migrate | Run all pending database migrations |
| Kind (Local Kubernetes) | |
make kind-up | Create Kind cluster and deploy BOMHort via Helm |
make kind-down | Destroy the Kind cluster (deletes everything) |
make kind-stop | Stop the Kind cluster without losing data (preserves volumes) |
make kind-start | Resume a stopped Kind cluster (all pods & data intact) |
make kind-status | Show Kind cluster and pod status |
make kind-build | Build all container images and load them into Kind |
make kind-deploy | Build images, Helm upgrade, and restart pods |
make kind-reingest | Re-ingest all SBOMs (truncate data, re-queue, no re-download) |
| ClickHouse | |
make ch-only | Start only ClickHouse (for local dev) |
make ch-migrate | Run SQL migrations against ClickHouse |
make ch-shell | Open ClickHouse CLI |
| Local Dev | |
make api | Run API Gateway locally |
make ingest | Run Ingestion Watcher locally |
make worker | Run Parsing Worker locally |
make ui-dev | Start Angular dev server with API proxy |
make backend-build | Build all Go binaries |
make backend-test | Run all Go tests |
make backend-vet | Run go vet + go fmt |
make ui-build | Build Angular for production |
| Images | |
make images | Build all 5 container images locally (TAG=dev) |
make images-push | Build and push all images to GHCR |
| Method | Endpoint | Description |
|---|---|---|
| GET | /healthz | Health check |
| GET | /api/v1/stats/dashboard | Dashboard stats (VEX effective/suppressed counts) |
| GET | /api/v1/stats/dependencies?limit=N | Top 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}/detail | SBOM detail with severity breakdown |
| GET | /api/v1/sboms/{id}/vulnerabilities | Vulnerabilities for a specific SBOM |
| GET | /api/v1/sboms/{id}/licenses | License breakdown for a specific SBOM |
| GET | /api/v1/sboms/{id}/dependencies | Dependency tree |
| GET | /api/v1/vulnerabilities?page=&page_size= | Paginated vulnerabilities (every finding, VEX status attached) |
| GET | /api/v1/vulnerabilities/{id}/affected-projects | All projects affected by a CVE |
| GET | /api/v1/licenses/compliance | Global license compliance overview |
| GET | /api/v1/projects/license-compliance | Projects with license violations (filtered by exceptions) |
| GET | /api/v1/license-exceptions | Active license exceptions (read-only, from config file) |
| GET | /api/v1/license-policy | Active license classification policy (permissive/copyleft lists) |
| GET | /api/v1/vex/statements?page=&page_size= | Paginated VEX statements |
| GET | /api/v1/packages/archived | Packages 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.
.spdx.json files in the sboms/ directory (or set SBOM_SOURCE_DIR in .env).openvex.json or .vex.json files in the same directoryThe Ingestion Watcher deduplicates by SHA256 hash — it will skip files that have already been processed.
# 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
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:
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.
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
| Layer | Technology |
|---|---|
| Backend | Go 1.26, net/http (stdlib) |
| Database | ClickHouse (MergeTree family) |
| Frontend | Angular 19, CDK Virtual Scrolling |
| Vuln Scanning | OSV.dev API |
| VEX | OpenVEX Spec v0.2.0 |
| Deployment | Helm 3, Docker Compose |
The seebom-labs organization hosts additional BOMHort subprojects:
See all repositories: https://github.com/seebom-labs
We welcome contributions! See the Contributing Guide for how to get started.
Go
70.6%
TypeScript
25.1%
Makefile
1.7%
Python
1.6%