Overview

Install, quick start and what the client covers.

Maintained in seebom-labs/bomhort-go — edit it there.

CI E2E Go Reference

The Go client for the BOMHort REST API.

  • Complete: every public endpoint of the BOMHort api-gateway — SBOMs, findings, dependency trees, licenses, VEX, projects, clusters, namespaces, fleet, statistics, search, push-model uploads and source attribution (coverage).
  • Zero dependencies: standard library only.
  • Production behaviour built in: API key / service token auth, client-side rate limiting, 429 retries honouring Retry-After, typed errors, iterator-based pagination, streaming downloads.
  • Testable: bomhorttest is an in-memory fake gateway with BOMHort’s real upload, VEX scoping and matching semantics.
  • Verified against real BOMHort: every PR runs the integration suite with strict decoding against the pinned BOMHort release; a weekly job also runs it against BOMHort main to catch API drift.
  • Versioned with BOMHort: bomhort-go v0.8.x is the client for BOMHort 0.8.x (compatibility).

Extracted from VEXViper’s internal/bomhort (why).

Install

go get github.com/seebom-labs/bomhort-go

Requires Go 1.25+. See compatibility for supported BOMHort versions.

Quick start

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	bomhort "github.com/seebom-labs/bomhort-go"
)

func main() {
	c := bomhort.New("https://bomhort.example.com",
		bomhort.WithAPIKey(os.Getenv("BOMHORT_API_KEY")),
		bomhort.WithRateLimit(90, 10*time.Second), // BOMHort allows 100 req / 10 s per IP
	)
	ctx := context.Background()

	for sbom, err := range c.SBOMs(ctx, nil) {
		if err != nil {
			log.Fatal(err)
		}
		vulns, err := c.Vulnerabilities(ctx, sbom.ID, nil)
		if err != nil {
			log.Fatal(err)
		}
		for _, v := range vulns {
			fmt.Printf("%s\t%s\t%s\t%s\n", sbom.DocumentName, v.Severity, v.VulnID, v.PURL)
		}
	}
}

More runnable examples are in example_test.go and on pkg.go.dev.

Usage

Client options

OptionPurpose
WithAPIKey(key)Sends X-API-Key (BOMHort API_KEYS)
WithServiceToken(tok)Sends Authorization: Bearer (BOMHort SERVICE_TOKEN)
WithRateLimit(n, window) / WithRateLimiter(rl)Client-side pacing; share one RateLimiter between clients that hit the same instance
WithMaxRetries(n)Retries on HTTP 429 (default 3); honours Retry-After, else exponential back-off
WithHTTPClient(hc)Custom transport, proxy, TLS, timeout (default timeout 60 s)
WithUserAgent(ua)Default bomhort-go/<version>
WithStrictDecoding()Fail on JSON fields the client does not know — for contract tests, not production

New accepts the instance root, with or without a trailing /api/v1.

Pagination

Paginated endpoints come in three flavours:

FormExampleReturns
ListXListSBOMs(ctx, opts)one page: Paginated[T]{Data, Total, Page, PageSize}
XSBOMs(ctx, opts)lazy iter.Seq2[T, error] over all pages
AllXAllSBOMs(ctx, opts)every item as a slice

bomhort.Paginate turns any List* call into an iterator, bomhort.Collect drains one.

Errors

Non-2xx responses are *bomhort.APIError (status, server message, method, path):

d, err := c.SBOMDetail(ctx, id)
switch {
case bomhort.IsNotFound(err):     // 404
case bomhort.IsUnauthorized(err): // 401: missing/invalid key
case bomhort.IsForbidden(err):    // 403: e.g. upload with AUTH_ENABLED=false
case bomhort.IsRateLimited(err):  // 429 after all retries
case bomhort.IsUnavailable(err):  // 503: storage / ClickHouse down
case err != nil:                  // transport error, decode error, ...
}

Uploading SBOMs and VEX

res, err := c.UploadSBOM(ctx, "checkout.spdx.json", doc, &bomhort.UploadOptions{
	Project:    "checkout",
	Tags:       []string{"payments"},
	SourceRepo: "https://github.com/example/checkout",
	SourceRef:  "v1.4.0",
})
// res.Status == "pending" (job enqueued) or res.Duplicate() == true

res, err = c.UploadVEX(ctx, "checkout.openvex.json", vexDoc, sbom.ID) // scoped to one SBOM

Uploads require AUTH_ENABLED=true on the BOMHort side. Ingestion is asynchronous — poll the SBOM or its findings until the result shows up.

Read docs/VEX.md before writing VEX for BOMHort. It documents exactly how BOMHort scopes statements to SBOMs and matches them to findings; getting the product/subcomponent shape wrong silently suppresses nothing (or more than intended).

Testing your code

srv := bomhorttest.New("test-key")
defer srv.Close()
sbom := srv.AddSBOM(bomhort.SBOM{DocumentName: "app"}, []bomhort.Vulnerability{
	{VulnID: "GHSA-xxxx", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0"},
}, nil, nil)
srv.Handle("GET /api/v1/fleet", 200, []bomhort.FleetCluster{{Name: "prod"}}) // canned response

c := bomhort.New(srv.URL, bomhort.WithAPIKey("test-key"))
// ... exercise your code, then inspect srv.Snapshot() / srv.Requests()

The fake implements health probes, auth, pagination and filters, SBOM reads, downloads, uploads (ingesting SBOMs, applying VEX with BOMHort’s scoping and matching rules), duplicate detection and the source PATCH. Any other endpoint can be served with Handle. See docs/TESTING.md.

Documentation

DocumentContents
docs/API-COVERAGE.mdEvery BOMHort endpoint ↔ client method ↔ type, with the BOMHort version it appeared in
docs/VEX.mdUpload contract, VEX scoping, product-wide statements and the matching rules
docs/COMPATIBILITY.mdSupported Go and BOMHort versions, lockstep versioning, behaviour on older BOMHort
docs/TESTING.mdUnit tests, the fake server, integration and E2E tests, API drift detection
docs/MIGRATING-FROM-VEXVIPER.mdMoving from VEXViper’s internal/bomhort
docs/HOWTO-CI-UPLOAD.mdHow to: upload SBOM and VEX from CI and wait for ingestion
docs/RELEASING.mdRelease process
CONTRIBUTING.md · AGENTS.mdHow to contribute (humans and coding agents)
CHANGELOG.mdRelease notes

Development

make check            # lint (gofmt, vet, staticcheck, no-deps) + race tests + build
make cover            # tests with coverage gate
BOMHORT_SRC=~/src/BOMHort BOMHORT_BUILD=1 make e2e   # real BOMHort via docker compose

License

Apache-2.0