Skip to content

container-registry/bomhort-bridge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bomhort-bridge

A small webhook bridge that ships SBOMs from Harbor into BOMHort — automatically, with zero changes to either product.

image push -> Harbor (Trivy generates the SBOM, stored as an OCI accessory)
           -> SCANNING_COMPLETED webhook -> bomhort-bridge
           -> pulls the SBOM from the registry API
           -> drops <project>__<repo>@<digest>.spdx.json into BOMHort's watched dir
           -> BOMHort ingestion-watcher -> parsing-worker -> dashboard

Once connected, every SBOM Harbor generates appears in BOMHort's analytics: license breakdown, OSV-matched vulnerabilities, cross-project views — all derived from the package list alone, with no vulnerability scan in Harbor.

The bridge is a single Go binary (~350 lines, standard library only) and is stateless: its only output is files in a folder.


Prerequisites

  • Harbor >= v2.13.0, installed with Trivy. Both matter:
    • Below v2.13.0 Harbor never fires webhooks for SBOM scans — the scan succeeds but the event is silently dropped (fixed upstream in goharbor/harbor#21794). There is no error anywhere, so this is easy to lose a day on.
    • Without Trivy (./install.sh --with-trivy) Harbor has no scanner and cannot generate SBOMs at all. Check under Administration -> Interrogation Services that Trivy is registered.
  • Docker + Docker Compose for the BOMHort stack.
  • Go >= 1.24 to run the bridge (or build the binary once and copy it).

Step 1 — Start BOMHort

git clone https://github.com/seebom-labs/BOMHort.git
cd BOMHort
docker compose up --build -d

This starts ClickHouse, the api-gateway (:8080), the parsing-worker, and the UI. The compose file mounts ./sboms as the watched drop directory — that is where the bridge will write.

Port clash: BOMHort's UI maps to host port 8090 by default — the same port a local Harbor often uses. If needed, add a docker-compose.override.yml next to the compose file:

services:
  ui:
    ports: !override
      - "8091:80"

Note: the ingestion-watcher service is one-shot — it scans the directory, enqueues new files, and exits. That is normal; you re-run it after new SBOMs arrive (step 5).

Step 2 — Run the bridge

git clone https://github.com/container-registry/bomhort-bridge.git
cd bomhort-bridge

export HARBOR_URL=http://<harbor-host>:<port>
export HARBOR_USERNAME='robot$bomhort-bridge'   # or admin for a quick test
export HARBOR_PASSWORD='<secret>'
export SBOM_DIR=/path/to/BOMHort/sboms          # the dir BOMHort watches
export WEBHOOK_AUTH='<shared-secret>'           # must match the webhook's auth header
go run .

You should see: bomhort-bridge listening on :9099, dropping SBOMs into ...

  • For anything beyond a quick test, create a robot account with pull permission on the project instead of using admin.
  • LISTEN_ADDR overrides the port; INSECURE_SKIP_VERIFY=true for self-signed HTTPS Harbor.

Step 3 — Register the webhook in Harbor

In the Harbor UI: Project -> Webhooks -> New Webhook

  • Notify type: http
  • Event type: check Scanning finished (fires as SCANNING_COMPLETED)
  • Endpoint URL: http://<bridge-host>:9099/webhook
  • Auth Header: the same value as WEBHOOK_AUTH

Or via API:

curl -u <user>:<pass> -X POST -H 'Content-Type: application/json' \
  http://<harbor-host>:<port>/api/v2.0/projects/<project>/webhook/policies -d '{
  "name": "bomhort-bridge",
  "enabled": true,
  "event_types": ["SCANNING_COMPLETED"],
  "targets": [{"type":"http","address":"http://<bridge-host>:9099/webhook","auth_header":"<shared-secret>"}]
}'

The endpoint must be reachable from inside Harbor's containers. If the bridge runs on the Docker host, localhost will NOT work — inside a container it points at the container itself. Use the Harbor compose network's gateway IP (find it with docker network inspect <harbor-network> --format '{{(index .IPAM.Config 0).Gateway}}') or a real host IP / DNS name.

Verify from inside Harbor: docker exec harbor-core curl -s -o /dev/null -w '%{http_code}' http://<bridge-host>:9099/webhook -X POST -H 'Authorization: <shared-secret>' -d '{}' — expect 200 (and a harmless ignoring event type "" line in the bridge log; that is the bridge correctly discarding the empty test event).

Step 4 — Ship data: push an image and generate its SBOM

docker pull nginx:alpine
docker tag nginx:alpine <harbor-host>:<port>/<project>/nginx:alpine
docker push <harbor-host>:<port>/<project>/nginx:alpine

Generate the SBOM — in the Harbor UI (open the artifact -> Generate SBOM) or via API. To find the artifact digest:

curl -u <user>:<pass> http://<harbor-host>:<port>/api/v2.0/projects/<project>/repositories/<repo>/artifacts \
  | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['digest'])"

Then trigger the scan:

curl -u <user>:<pass> -X POST -H 'Content-Type: application/json' \
  -d '{"scan_type":"sbom"}' \
  http://<harbor-host>:<port>/api/v2.0/projects/<project>/repositories/<repo>/artifacts/<digest>/scan

Tip: enable automatically generate SBOM on push in the project configuration to make the whole pipeline hands-free.

When the scan finishes, the bridge log shows:

dropped <project>__<repo>@<digest>.spdx.json (... bytes)
event SCANNING_COMPLETED: handled 1 SBOM resource(s)

Step 5 — Ingest into BOMHort

cd BOMHort
docker compose run --rm ingestion-watcher

The watcher enqueues the new file; the always-running parsing-worker parses it, matches vulnerabilities via OSV, and classifies licenses. Open the dashboard (default http://localhost:8090, or :8091 with the override) — the SBOM appears with package count, license breakdown, and vulnerabilities.

For continuous operation, run the watcher on a schedule (cron or a systemd timer).


Behavior worth knowing

  • One file per artifact digest. Files are named <project>__<repo>@<digest>.spdx.json, so a re-scan of the same artifact maps to the same file. A new image version has a new digest and correctly becomes a new file.
  • Normalized output — re-scans never create duplicates. Trivy's raw output is not byte-stable: every run stamps a fresh creationInfo.created, a random documentNamespace UUID, and an annotationDate on every package annotation. That would defeat BOMHort's content-hash dedup and re-ingest every re-scan as a duplicate row. The bridge therefore pins these fields to fixed values before writing (documentNamespace becomes urn:harbor:sbom:<digest>, the dates become a fixed epoch). Re-scans of an unchanged artifact produce identical bytes, so dedup works downstream with no state kept anywhere. The SBOM remains valid SPDX; only per-run scan metadata is pinned.
  • Vulnerability-scan events are ignored. The same SCANNING_COMPLETED event type also fires for vulnerability scans (payload without sbom_overview); the bridge only acts on SBOM completions.
  • Half-written files are never ingested. The bridge writes to a _-prefixed temp file first (BOMHort's watcher ignore-prefix), then renames atomically.
  • Delivery guarantees. Webhooks are fire-and-forget: Harbor retries a few times, then the event is lost (e.g. if the bridge is down). Fine for a proof of concept; for production add a periodic reconciliation poll, or wait for the spec-based adapter integration.

Troubleshooting

Symptom Likely cause / fix
SBOM scan succeeds but no webhook arrives; the policy shows zero executions Harbor < v2.13.0 — SBOM scans fire no event on older versions, completely silently. Upgrade Harbor.
Webhook executions show failures in Harbor Endpoint not reachable from inside Harbor's containers (see step 3), or the auth header does not match WEBHOOK_AUTH.
"Generate SBOM" missing / scan fails immediately Trivy is not installed or not the default scanner. Re-run the Harbor installer with --with-trivy.
Bridge logs an error fetching the accessory The Harbor credentials lack pull permission on the project, or HARBOR_URL is not reachable from where the bridge runs.
File appears in sboms/ but nothing shows on the dashboard The one-shot ingestion-watcher has not run since the file arrived — run docker compose run --rm ingestion-watcher, then give the parsing-worker a few seconds.
Dashboard shows 0 vulnerabilities for an image that should have them OSV matching happens in the parsing-worker and needs outbound internet access; check its logs.

Development

go test ./...   # unit tests, including the normalization determinism test
go build .

About

Webhook bridge: ships Harbor SBOMs into BOMHort

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages