FAQ
Common questions about running BOMHort — re-ingestion, database resets, GitHub token setup, example files, and day-to-day operations.
How do I force a re-ingestion?
The Ingestion Watcher runs as a CronJob (default: every 6 hours). It deduplicates files by SHA-256 hash — only new or changed SBOMs are enqueued. There are two scenarios:
Ingest new/changed files immediately
Trigger the CronJob manually instead of waiting for the next scheduled run:
kubectl create job --from=cronjob/<RELEASE>-ingestion-watcher manual-ingest-$(date +%s) -n <NAMESPACE>
Replace <RELEASE> with your Helm release name (e.g. bomhort) and <NAMESPACE> with your namespace.
Example:
kubectl create job --from=cronjob/bomhort-ingestion-watcher manual-ingest-$(date +%s) -n bomhort
This creates a one-off Job from the CronJob spec. The Watcher scans all sources (S3 buckets + local PVC), skips already-processed files, and enqueues anything new.
Full re-ingestion from scratch
If you need to reprocess everything (e.g. after changing the license policy, enabling OSV scanning, or upgrading parsing logic), you must first truncate all data tables and then trigger re-ingestion:
Step 1 — Truncate all data tables:
kubectl exec -n <NAMESPACE> <CLICKHOUSE_POD> -c clickhouse -- \
clickhouse-client --database=bomhort --password="<PASSWORD>" --multiquery \
--query "TRUNCATE TABLE ingestion_queue; \
TRUNCATE TABLE license_compliance; \
TRUNCATE TABLE vulnerabilities; \
TRUNCATE TABLE sbom_packages; \
TRUNCATE TABLE sboms; \
TRUNCATE TABLE vex_statements;"
The default ClickHouse pod name follows this pattern:
chi-<RELEASE>-clickhouse-<CLUSTER_NAME>-0-0-0
For a typical installation this would be:
kubectl exec -n bomhort chi-bomhort-clickhouse-bomhort-cluster-0-0-0 -c clickhouse -- \
clickhouse-client --database=bomhort --password="$CLICKHOUSE_PASSWORD" --multiquery \
--query "TRUNCATE TABLE ingestion_queue; \
TRUNCATE TABLE license_compliance; \
TRUNCATE TABLE vulnerabilities; \
TRUNCATE TABLE sbom_packages; \
TRUNCATE TABLE sboms; \
TRUNCATE TABLE vex_statements;"
Step 2 — Trigger re-ingestion:
kubectl create job --from=cronjob/bomhort-ingestion-watcher reingest-$(date +%s) -n bomhort
Step 3 — Monitor progress:
# Watch the watcher job
kubectl logs -n bomhort -l job-name=reingest-<TIMESTAMP> -f
# Watch the parsing workers
kubectl logs -n bomhort -l app.kubernetes.io/component=parsing-worker --tail=20 -f
# Check dashboard stats via API
kubectl exec -n bomhort deploy/bomhort-api-gateway -- \
wget -qO- http://localhost:8080/api/v1/stats/dashboard | jq '.total_sboms, .total_packages'
If you use the Kind local development setup, you can use the Makefile shortcut instead:
make kind-reingest
This truncates all tables and triggers re-ingestion in one step.
When do I need a full re-ingestion?
A full re-ingestion (truncate + re-ingest) is required when:
| Scenario | Why |
|---|---|
Changed the license policy (license-policy.json) | Existing packages need reclassification |
Enabled/disabled OSV scanning (skipOSV) | Vulnerability data needs to be fetched or cleared |
Enabled/disabled GitHub license resolution (skipGitHubResolve) | Unknown licenses need re-resolution |
Enabled/disabled npm / NuGet / deps.dev / Packagist / PyPI license resolution (skipNPMResolve, skipNuGetResolve, skipDepsDevResolve, skipPackagistResolve, skipPyPIResolve) | Unknown package-registry licenses need re-resolution |
| Upgraded parsing logic (new image version) | Existing SBOMs may parse differently (e.g., new in-toto attestation support or improved license resolution with well-known Go module mappings) |
Added a GitHub token (GITHUB_TOKEN) | Previously rate-limited resolution may have missed packages — a re-ingestion with the token resolves all licenses |
For these changes, a simple incremental re-trigger will not reprocess existing files because the SHA-256 hashes haven’t changed.
Changed license exceptions are not on this list: the workers re-check the stored results of every SBOM when the rules change, without re-ingesting anything. See Retroactive application.
Changes to VEX files do not require a full re-ingestion. VEX matching is applied at query time. Simply add or update your .openvex.json files and trigger an incremental re-ingestion to pick them up. Note that statements are scoped to the SBOM whose product they describe (#350): the worker maps the OpenVEX product @id to an SBOM (or use ?sbom_id= on upload); statements whose product matches no SBOM are stored unscoped and suppress nothing — there is no global or fleet-wide VEX scope. Check the worker logs for the warning. If the product’s SBOM merely had not been ingested yet, the VEX rescue pass (migration 020) scopes the statement automatically once it arrives.
Why are my SBOMs named tmp.*?
Some producer pipelines put a temporary checkout-directory name into the SPDX
document’s top-level name, even when the S3 object key and described root package
are correct. Renaming only the object does not correct these source metadata.
BOMHort’s built-in and protobom parsers now replace empty/temporary document names with an unambiguous described package name and version, or a source label when that is unavailable. Good names and original downloaded documents are not changed. See Document name fallback.
If old entries still display tmp.*, deploy worker images containing the fix and
plan re-processing of those SBOMs. An ordinary watcher run or Argo sync does not
rewrite stored rows because unchanged files are deduplicated. Review project-scoped
license exceptions against the new exact document_name, including its version.
The producer should still fix its generated document name rather than relying only
on downstream display fallbacks.
How do I check ingestion progress?
# Job queue status
kubectl exec -n bomhort <CLICKHOUSE_POD> -c clickhouse -- \
clickhouse-client --database=bomhort --password="$CLICKHOUSE_PASSWORD" \
--query "SELECT argMax(status, created_at) AS status, count() AS cnt \
FROM ingestion_queue \
GROUP BY job_id \
HAVING status != '' \
GROUP BY status \
ORDER BY status" \
--format=PrettyCompact
# Data summary
kubectl exec -n bomhort <CLICKHOUSE_POD> -c clickhouse -- \
clickhouse-client --database=bomhort --password="$CLICKHOUSE_PASSWORD" \
--query "SELECT 'sboms' AS table, count() AS rows FROM sboms FINAL \
UNION ALL SELECT 'packages', count() FROM sbom_packages FINAL \
UNION ALL SELECT 'vulns', count() FROM vulnerabilities FINAL \
UNION ALL SELECT 'licenses', count() FROM license_compliance FINAL" \
--format=PrettyCompact
Or use the API endpoint:
curl -s http://<API_HOST>/api/v1/stats/dashboard | jq .
How do I clear / reset the database?
Docker Compose (local development)
The quickest way to start from scratch:
make dev-reset
This stops all containers, deletes the ClickHouse data volume, and restarts everything. All SBOMs will be re-ingested from the sboms/ directory.
If you only want to wipe specific tables (e.g. vulnerabilities and licenses) without losing SBOM data:
make ch-shell
TRUNCATE TABLE vulnerabilities;
TRUNCATE TABLE license_compliance;
Then re-trigger processing:
make re-scan
Kubernetes
See Full re-ingestion from scratch above — it covers truncating all tables and re-triggering the Ingestion Watcher.
Should I use a GitHub token?
Yes — we strongly recommend setting GITHUB_TOKEN, especially when ingesting container-image SBOMs (e.g. those generated by Syft/BuildKit).
Why?
Many container-image SBOMs contain packages with NOASSERTION as the declared license. BOMHort’s parsing worker resolves these by querying the GitHub API. Without a token, GitHub enforces a rate limit of 60 requests per hour — which is quickly exhausted when processing SBOMs with hundreds of packages.
| Without token | With token | |
|---|---|---|
| Rate limit | 60 req/h | 5,000 req/h |
| License resolution | Slow — jobs wait for the next reset | Complete |
| Recommended | Only for small test runs | Always |
When the limit is hit, BOMHort does not fall back to a guess: the affected SBOM job is deferred (ingestion_queue.retry_after) until GitHub’s reset time and picked up again automatically. Nothing is cached for the package, so a later run resolves it correctly — but a large backlog without a token advances only ~60 lookups per hour.
How to set it up
Create a GitHub Personal Access Token (classic) at github.com/settings/tokens. No scopes are required — the token only needs unauthenticated-level access to public repository metadata.
Set the token:
Docker Compose (.env):
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Kind (local Kubernetes):
# local/secrets.env
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Kubernetes (Helm values):
github:
token: "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Or via --set:
helm upgrade bomhort deploy/helm/bomhort/ --set github.token="ghp_..."
If you initially ingested SBOMs without a GitHub token, many packages may have unresolved licenses. After adding the token, you need a full re-ingestion to resolve them:
# Docker Compose:
make dev-reset
# Kubernetes:
# See "Full re-ingestion from scratch" above
What example files are included?
The sboms/ directory ships with several example files for testing and demonstration:
| File | Type | Purpose |
|---|---|---|
_example.spdx.json | Plain SPDX JSON | A minimal SPDX 2.3 document with packages, licenses, and PURLs. Good starting point for testing the basic pipeline. |
_example-intoto.spdx.json | In-toto attestation envelope | An SPDX document wrapped in an in-toto attestation predicate field — the format generated by Syft/BuildKit for container images. Tests the parser’s auto-detection and unwrapping. |
_example-violations.spdx.json | Plain SPDX JSON | Contains packages with copyleft licenses (GPL, AGPL) to test license compliance violations and exception handling. |
_example.cdx.json | CycloneDX JSON | A CycloneDX 1.5 BOM demonstrating CycloneDX format support. Tests auto-detection and component/license/dependency mapping. |
_example.openvex.json | OpenVEX | A VEX document that suppresses specific CVEs for demonstration. Tests the VEX ingestion and dashboard effective-vulnerability counts. |
golang-common.openvex.json | OpenVEX | Real-world VEX statements for common Go ecosystem false positives. |
otel-protobuf.openvex.json | OpenVEX | VEX statements for OpenTelemetry protobuf-related findings. |
license-policy.json | License Policy | The active license classification policy (permissive vs. copyleft SPDX IDs). Based on the CNCF Allowed Third-Party License Policy. |
license-exceptions.json | License Exceptions | Empty by default. Add only organization-approved exceptions. An inactive structure example lives in examples/license-exceptions/; no CNCF approvals are automatically imported. |
Files prefixed with _ are treated as examples and are skipped by the scanner (configurable via SBOM_IGNORE_PREFIX). To include them, set SBOM_IGNORE_PREFIX= (empty). You can place any .json file in the sboms/ directory — the format (SPDX, CycloneDX, in-toto) is auto-detected at parse time.
How do I trigger a CVE refresh?
The CVE Refresher runs as a CronJob (default: daily at 2 AM). To trigger it immediately:
kubectl create job --from=cronjob/bomhort-cve-refresher manual-cve-refresh-$(date +%s) -n bomhort
This checks all known PURLs against the OSV database for newly disclosed vulnerabilities.
Docker Compose:
make cve-refresh
Why is a license “unknown” or “NOASSERTION”?
Many SBOM generators write NOASSERTION instead of a license. BOMHort fills
these gaps at ingest time from public sources (npm, NuGet, deps.dev, Packagist,
PyPI, GitHub) and records for every package where its license came from — or
why it is still missing:
| Reason | Meaning |
|---|---|
not-published | Not on any public registry — usually an internal or private package |
no-license-upstream | The registry knows the package, but no license is declared upstream (e.g. Maven artifacts that inherit it from a parent POM) |
unsupported-ecosystem | No resolver exists for this package type (e.g. pkg:generic files such as libc.so.6) |
no-purl | The SBOM has no package URL, so there is nothing to look up |
unresolved | Every lookup failed or matched nothing |
first-party | The project’s own code — excluded from license compliance |
The Licenses page shows the result in the License Resolution panel: how many packages the SBOMs left without a license, how many BOMHort resolved, and the remaining unknowns per reason with examples and what you can do. Hover a license in an SBOM’s dependency list to see its source. All rules — and the ones BOMHort deliberately does not apply — are on the License Resolution page.
SBOMs ingested before license sources were recorded show as unrecorded;
re-process them to fill it in (see full re-ingestion).
What do “direct”, “transitive” and “unknown” mean next to a package?
The package’s position in the SBOM’s dependency graph, computed at ingest
(see Dependency depth): root is the
product itself, direct is declared by the product, transitive is pulled in
by another dependency (the badge’s tooltip shows the depth). Vulnerability
pages filter on it (?scope=), so you can triage CVEs in your own
dependencies before the deep ones.
Everything is “direct”. The SBOM generator listed every module as a
DEPENDS_ON of the root — a star graph, common for Go module SBOMs. BOMHort
reports the declared graph; it does not reconstruct one from go.mod.
Everything is “unknown”. Either the document carries no relationships
(a flat package list), several packages could be the root, or the SBOM was
ingested before depths were recorded (migration 025). The latter clears up
with a full re-ingestion.
What does “Not Approved” mean?
The SBOM declares a license, but it is not on the policy allow-list — for
example LicenseRef-…, an ambiguous BSD or Public Domain. BOMHort never
guesses which license such a name means, because that would approve a license
nobody approved. Add the exact SPDX ID to the license policy,
or exempt the package via license exceptions.