bomhort

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

bomhort-go

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
Option Purpose
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:

Form Example Returns
ListX ListSBOMs(ctx, opts) one page: Paginated[T]{Data, Total, Page, PageSize}
X SBOMs(ctx, opts) lazy iter.Seq2[T, error] over all pages
AllX AllSBOMs(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

Document Contents
docs/API-COVERAGE.md Every BOMHort endpoint ↔ client method ↔ type, with the BOMHort version it appeared in
docs/VEX.md Upload contract, VEX scoping, product-wide statements and the matching rules
docs/COMPATIBILITY.md Supported Go and BOMHort versions, lockstep versioning, behaviour on older BOMHort
docs/TESTING.md Unit tests, the fake server, integration and E2E tests, API drift detection
docs/MIGRATING-FROM-VEXVIPER.md Moving from VEXViper's internal/bomhort
docs/HOWTO-CI-UPLOAD.md How to: upload SBOM and VEX from CI and wait for ingestion
docs/RELEASING.md Release process
CONTRIBUTING.md · AGENTS.md How to contribute (humans and coding agents)
CHANGELOG.md Release 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

Documentation

Overview

Package bomhort is a Go client for the BOMHort REST API (https://docs.bomhort.dev/docs/api-reference/).

It is stdlib-only and covers the complete public API of the BOMHort api-gateway: SBOMs and their findings, dependency trees, licenses and VEX statements, projects, clusters, namespaces, fleet, statistics, search, the push-model upload endpoint and the source-attribution PATCH.

A Client is safe for concurrent use. Create one per BOMHort instance:

c := bomhort.New("https://bomhort.example.com",
	bomhort.WithAPIKey(os.Getenv("BOMHORT_API_KEY")),
	bomhort.WithRateLimit(90, 10*time.Second),
)
sboms, err := c.AllSBOMs(ctx, nil)

Non-2xx responses are returned as *APIError; use IsNotFound, IsUnauthorized and friends to branch on them. 429 responses are retried with Retry-After / exponential back-off (see WithMaxRetries).

The package bomhorttest provides an in-memory fake gateway for tests.

Example
package main

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

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

// fakeGateway starts a bomhorttest server with one SBOM so the examples
// run without a real BOMHort.
func fakeGateway() *bomhorttest.Server {
	srv := bomhorttest.New("example-key")
	srv.AddSBOM(
		bomhort.SBOM{ID: bomhorttest.UUID(1), DocumentName: "payments-api", SourceFile: "payments-api.spdx.json", Project: "payments"},
		[]bomhort.Vulnerability{
			{VulnID: "GHSA-qppj-fm5r-hxr3", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0", FixedVersion: "0.23.0"},
		},
		nil, nil,
	)
	return srv
}

func main() {
	srv := fakeGateway()
	defer srv.Close()

	c := bomhort.New(srv.URL,
		bomhort.WithAPIKey("example-key"),
		bomhort.WithRateLimit(90, 10*time.Second),
	)
	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: %s %s in %s\n", sbom.DocumentName, v.Severity, v.VulnID, v.PURL)
		}
	}
}
Output:
payments-api: HIGH GHSA-qppj-fm5r-hxr3 in pkg:golang/golang.org/x/net@v0.17.0

Index

Examples

Constants

View Source
const (
	ScopeRoot       = "root"
	ScopeDirect     = "direct"
	ScopeTransitive = "transitive"
	ScopeUnknown    = "unknown"
)

Dependency scopes as sent in Vulnerability.DependencyScope and accepted by VulnerabilityOptions.Scope (#444, >= 0.8.0).

View Source
const DefaultUserAgent = "bomhort-go/" + Version

DefaultUserAgent is sent unless WithUserAgent overrides it.

View Source
const DefaultWalkPageSize = 100

DefaultWalkPageSize is the page size used by the iterators and All* helpers when none is given. 100 keeps a full walk well below BOMHort's rate limit for typical instances while avoiding huge responses.

View Source
const Version = "0.8.0"

Version is the client library version, sent in the default User-Agent.

Variables

This section is empty.

Functions

func Collect

func Collect[T any](seq Seq[T]) ([]T, error)

Collect drains seq into a slice, returning the first error.

func IsBadRequest

func IsBadRequest(err error) bool

IsBadRequest reports whether err is a 400 (validation failure).

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether err is a 403. BOMHort returns it for the write endpoints (upload, PATCH) when AUTH_ENABLED=false.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a 404 from BOMHort. Note that BOMHort also answers 404 for routes an older gateway does not know.

Example
package main

import (
	"context"
	"fmt"

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

// fakeGateway starts a bomhorttest server with one SBOM so the examples
// run without a real BOMHort.
func fakeGateway() *bomhorttest.Server {
	srv := bomhorttest.New("example-key")
	srv.AddSBOM(
		bomhort.SBOM{ID: bomhorttest.UUID(1), DocumentName: "payments-api", SourceFile: "payments-api.spdx.json", Project: "payments"},
		[]bomhort.Vulnerability{
			{VulnID: "GHSA-qppj-fm5r-hxr3", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0", FixedVersion: "0.23.0"},
		},
		nil, nil,
	)
	return srv
}

func main() {
	srv := fakeGateway()
	defer srv.Close()
	c := bomhort.New(srv.URL, bomhort.WithAPIKey("example-key"))

	_, err := c.SBOMDetail(context.Background(), bomhorttest.UUID(404))
	fmt.Println(bomhort.IsNotFound(err))
}
Output:
true

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err is a 429 that survived all retries.

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether err is a 401 (missing/invalid credentials).

func IsUnavailable

func IsUnavailable(err error) bool

IsUnavailable reports whether err is a 503 (e.g. /readyz without ClickHouse, or upload without storage).

func StatusCode

func StatusCode(err error) int

StatusCode returns the HTTP status of err if it is (or wraps) an *APIError, otherwise 0.

func String

func String(s string) *string

String returns a pointer to s, for SourcePatch fields.

func UploadedFilename

func UploadedFilename(sourceFile string) string

UploadedFilename returns the filename a document was uploaded with when sourceFile has the "pushed/<uuid>-<filename>" form BOMHort stores uploads under, and "" otherwise.

Types

type APIError

type APIError struct {
	// StatusCode is the HTTP status code.
	StatusCode int
	// Message is BOMHort's {"error": "..."} text, or the (truncated) body.
	Message string
	// Method and Path identify the failed call (path without query).
	Method string
	Path   string
}

APIError is returned for every non-2xx response from BOMHort.

func (*APIError) Error

func (e *APIError) Error() string

type AffectedProject

type AffectedProject struct {
	SBOMID       string `json:"sbom_id"`
	SourceFile   string `json:"source_file"`
	DocumentName string `json:"document_name"`
	PURL         string `json:"purl"`
	PackageName  string `json:"package_name"`
	Version      string `json:"version"`
	Severity     string `json:"severity"`
	VEXStatus    string `json:"vex_status,omitempty"`
	// IsDirect is true exactly when DependencyScope is "direct"; new
	// clients should read DependencyScope (#444, >= 0.8.0).
	IsDirect        bool    `json:"is_direct"`
	DependencyScope string  `json:"dependency_scope,omitempty"`
	DependencyDepth *uint32 `json:"dependency_depth,omitempty"`
}

AffectedProject is one entry of GET /api/v1/vulnerabilities/{id}/affected-projects.

type ArchivedPackage

type ArchivedPackage struct {
	SBOMID         string    `json:"sbom_id"`
	SourceFile     string    `json:"source_file"`
	ProjectName    string    `json:"project_name"`
	ProjectVersion string    `json:"project_version"`
	PackageName    string    `json:"package_name"`
	PackagePURL    string    `json:"package_purl"`
	Repo           string    `json:"repo"`
	LastPushed     time.Time `json:"last_pushed"`
	Stars          uint32    `json:"stars"`
}

ArchivedPackage is one entry of GET /api/v1/packages/archived: a dependency whose upstream GitHub repository is archived. Server type: clickhouse.ArchivedPackageInfo.

type BlanketLicenseException

type BlanketLicenseException struct {
	ID           string `json:"id"`
	License      string `json:"license"`
	Status       string `json:"status"`
	ApprovedDate string `json:"approvedDate"`
	Scope        string `json:"scope,omitempty"`
	Results      string `json:"results,omitempty"`
	IssueURL     string `json:"issueUrl,omitempty"`
	PackageURL   string `json:"packageUrl,omitempty"`
	Comment      string `json:"comment,omitempty"`
}

BlanketLicenseException exempts an entire license.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to one BOMHort API gateway.

func New

func New(baseURL string, opts ...Option) *Client

New creates a client for baseURL, e.g. "http://localhost:8080". A trailing slash and a trailing "/api/v1" are tolerated.

func (*Client) AffectedProjects

func (c *Client) AffectedProjects(ctx context.Context, vulnID string) ([]AffectedProject, error)

AffectedProjects returns the SBOMs affected by a vulnerability id (CVE-…, GHSA-…, GO-…), including transitive dependencies.

func (*Client) AllSBOMs

func (c *Client) AllSBOMs(ctx context.Context, opts *SBOMListOptions) ([]SBOM, error)

AllSBOMs collects every SBOM matching opts (nil = all SBOMs).

func (*Client) AllVEXStatements

func (c *Client) AllVEXStatements(ctx context.Context) ([]VEXStatement, error)

AllVEXStatements collects every ingested VEX statement.

func (*Client) ArchivedPackages

func (c *Client) ArchivedPackages(ctx context.Context) ([]ArchivedPackage, error)

ArchivedPackages lists dependencies whose upstream GitHub repository is archived.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL returns the configured base URL (without /api/v1).

func (*Client) ClusterStats

func (c *Client) ClusterStats(ctx context.Context, name string) (ClusterStats, error)

ClusterStats returns the statistics of one cluster (404 if unknown).

func (*Client) Clusters

func (c *Client) Clusters(ctx context.Context) ([]Cluster, error)

Clusters lists all clusters with summary statistics.

func (*Client) DashboardStats

func (c *Client) DashboardStats(ctx context.Context) (DashboardStats, error)

DashboardStats returns the instance-wide KPIs.

func (*Client) Dependencies

func (c *Client) Dependencies(ctx context.Context, sbomID string) ([]DependencyNode, error)

Dependencies returns the flat dependency node list of an SBOM (404 for an unknown id since BOMHort 0.8.0, 200 [] before).

func (*Client) DependencyStats

func (c *Client) DependencyStats(ctx context.Context, limit int) (DependencyStats, error)

DependencyStats returns the most used dependencies across all projects. limit <= 0 uses the server default (50, max 500).

func (*Client) DownloadSBOM

func (c *Client) DownloadSBOM(ctx context.Context, sbomID string) ([]byte, error)

DownloadSBOM returns the original SBOM document bytes.

func (*Client) DownloadSBOMTo

func (c *Client) DownloadSBOMTo(ctx context.Context, sbomID string, w io.Writer) (int64, error)

DownloadSBOMTo streams the original SBOM document into w and returns the number of bytes written. Unlike DownloadSBOM it does not buffer the document and does not retry on 429.

func (*Client) FindSBOM

func (c *Client) FindSBOM(ctx context.Context, ref string) (SBOM, error)

FindSBOM returns the first SBOM whose ID, document name or source file equals ref. A source file also matches by its base name, and uploaded documents (stored as "pushed/<uuid>-<filename>") by the filename they were uploaded with. A non-UUID ref narrows the listing with ?search= (BOMHort matches it as a substring of document_name and source_file, so every candidate is in that result); a UUID ref scans the full listing.

func (*Client) Fleet

func (c *Client) Fleet(ctx context.Context) ([]FleetCluster, error)

Fleet returns the whole cluster → namespace → project ownership tree.

func (*Client) Healthy

func (c *Client) Healthy(ctx context.Context) error

Healthy calls GET /healthz (legacy health check, always public).

func (*Client) LicenseCompliance

func (c *Client) LicenseCompliance(ctx context.Context) ([]LicenseCompliance, error)

LicenseCompliance returns the license compliance overview.

func (*Client) LicenseExceptions

func (c *Client) LicenseExceptions(ctx context.Context) (LicenseExceptions, error)

LicenseExceptions returns the operator's license exceptions file.

func (*Client) LicensePolicy

func (c *Client) LicensePolicy(ctx context.Context) (LicensePolicy, error)

LicensePolicy returns the permissive/copyleft classification in effect.

func (*Client) LicenseSources

func (c *Client) LicenseSources(ctx context.Context) ([]LicenseSource, error)

LicenseSources returns where package licenses came from, or why they are missing (GET /api/v1/licenses/sources, >= 0.8.0).

func (*Client) LicenseViolationSummaries

func (c *Client) LicenseViolationSummaries(ctx context.Context) ([]LicenseViolationSummary, error)

LicenseViolationSummaries is LicenseViolations with counts instead of package lists (GET /api/v1/projects/license-compliance?summary=true, >= 0.8.0; older gateways ignore the parameter and the full rows decode into mostly-empty summaries).

func (*Client) LicenseViolations

func (c *Client) LicenseViolations(ctx context.Context) ([]LicenseViolation, error)

LicenseViolations returns the SBOMs with non-compliant licenses after applying the configured exceptions (GET /api/v1/projects/license-compliance). The response lists every non-compliant package name and grows with the instance; see LicenseViolationSummaries for the counts only.

func (*Client) ListClusterSBOMs

func (c *Client) ListClusterSBOMs(ctx context.Context, name string, opts *ListOptions) (Paginated[SBOM], error)

ListClusterSBOMs returns one page of the SBOMs of a cluster.

func (*Client) ListNamespaceSBOMs

func (c *Client) ListNamespaceSBOMs(ctx context.Context, name, cluster string, opts *ListOptions) (Paginated[SBOM], error)

ListNamespaceSBOMs returns one page of the SBOMs of a namespace, optionally within one cluster.

func (*Client) ListProjectGroups

func (c *Client) ListProjectGroups(ctx context.Context, opts *ProjectListOptions) (Paginated[ProjectGroup], error)

ListProjectGroups returns one page of parents with their member projects (GET /api/v1/projects?group_by=parent, >= 0.8.0; older gateways ignore the parameter and return plain projects).

func (*Client) ListProjectPackages

func (c *Client) ListProjectPackages(ctx context.Context, name string, opts *SearchListOptions) (Paginated[ProjectPackage], error)

ListProjectPackages returns one page of the distinct components across a project's SBOMs (GET /api/v1/projects/{name}/packages, #398, >= 0.8.0). opts may be nil.

func (*Client) ListProjectSBOMs

func (c *Client) ListProjectSBOMs(ctx context.Context, name string, opts *ListOptions) (Paginated[SBOM], error)

ListProjectSBOMs returns one page of a project's SBOMs (its versions) (GET /api/v1/projects/{name}/sboms, #398, >= 0.8.0).

func (*Client) ListProjects

func (c *Client) ListProjects(ctx context.Context, opts *ProjectListOptions) (Paginated[Project], error)

ListProjects returns one page of projects (SBOMs grouped by project).

func (*Client) ListSBOMs

func (c *Client) ListSBOMs(ctx context.Context, opts *SBOMListOptions) (Paginated[SBOM], error)

ListSBOMs returns one page of SBOMs. opts may be nil.

func (*Client) ListVEXStatements

func (c *Client) ListVEXStatements(ctx context.Context, opts *ListOptions) (Paginated[VEXStatement], error)

ListVEXStatements returns one page of all ingested VEX statements.

func (*Client) ListVulnerabilities

func (c *Client) ListVulnerabilities(ctx context.Context, opts *VulnerabilityListOptions) (Paginated[Vulnerability], error)

ListVulnerabilities returns one page of all findings across the instance, each with its VEX status attached. opts may be nil.

func (*Client) Live

func (c *Client) Live(ctx context.Context) error

Live calls GET /livez: the gateway process is up.

func (*Client) NamespaceStats

func (c *Client) NamespaceStats(ctx context.Context, name, cluster string) (NamespaceStats, error)

NamespaceStats returns the statistics of a namespace, optionally within one cluster (404 if unknown).

func (*Client) Namespaces

func (c *Client) Namespaces(ctx context.Context, cluster string) ([]Namespace, error)

Namespaces lists namespaces with summary statistics. cluster "" lists them fleet-wide (a namespace name is only unique within a cluster).

func (*Client) PackageDetail

func (c *Client) PackageDetail(ctx context.Context, name string, opts *ListOptions) (PackageDetail, error)

PackageDetail lists the projects using a package (exact name).

func (*Client) PatchSBOMSource

func (c *Client) PatchSBOMSource(ctx context.Context, sbomID string, patch SourcePatch) (SourceAttribution, error)

PatchSBOMSource sets or clears source_repo/source_ref of an existing SBOM (BOMHort #332, >= 0.7.0). Requires AUTH_ENABLED=true on the gateway (403 otherwise) and credentials. The returned values are normalised by BOMHort.

func (*Client) Project

func (c *Client) Project(ctx context.Context, name string) (ProjectDetail, error)

Project returns one project aggregated over all of its SBOMs (GET /api/v1/projects/{name}, #398, >= 0.8.0). Names containing "/" are escaped correctly. A project literally named "license-compliance" is not reachable through this route (BOMHort limitation).

func (*Client) ProjectVulnerabilities

func (c *Client) ProjectVulnerabilities(ctx context.Context, name string, opts *VulnerabilityOptions) ([]Vulnerability, error)

ProjectVulnerabilities returns the distinct findings across a project's SBOMs; Vulnerability.AffectedSBOMs says in how many SBOMs each occurs (GET /api/v1/projects/{name}/vulnerabilities, #398, >= 0.8.0). opts may be nil.

func (*Client) Projects

func (c *Client) Projects(ctx context.Context, opts *ProjectListOptions) Seq[Project]

Projects iterates over all projects matching opts.

func (*Client) Ready

func (c *Client) Ready(ctx context.Context) error

Ready calls GET /readyz: the gateway can reach ClickHouse. A not-ready gateway yields an *APIError with status 503 (see IsUnavailable).

func (*Client) SBOMDetail

func (c *Client) SBOMDetail(ctx context.Context, sbomID string) (SBOMDetail, error)

SBOMDetail returns GET /api/v1/sboms/{id}/detail (metadata plus severity breakdown). Unknown ids yield a 404 *APIError.

func (*Client) SBOMLicenses

func (c *Client) SBOMLicenses(ctx context.Context, sbomID string) ([]SBOMLicense, error)

SBOMLicenses returns the per-license breakdown of an SBOM.

func (*Client) SBOMVEXStatements

func (c *Client) SBOMVEXStatements(ctx context.Context, sbomID string) ([]VEXStatement, error)

SBOMVEXStatements returns the VEX statements scoped to an SBOM, newest first (GET /api/v1/sboms/{id}/vex).

func (*Client) SBOMs

func (c *Client) SBOMs(ctx context.Context, opts *SBOMListOptions) Seq[SBOM]

SBOMs iterates over all SBOMs matching opts, fetching pages lazily. opts.Page is ignored; opts.PageSize defaults to 100.

func (*Client) Search

func (c *Client) Search(ctx context.Context, q string, limit int) (SearchResult, error)

Search runs the faceted global search. q must be at least two characters after trimming; limit <= 0 uses the server default (5 per facet, max 50).

func (*Client) SearchPackages

func (c *Client) SearchPackages(ctx context.Context, q string, opts *ListOptions) (PackageSearch, error)

SearchPackages finds packages by name across all SBOMs.

func (*Client) Tags

func (c *Client) Tags(ctx context.Context) ([]Tag, error)

Tags returns the grouping labels in use with their reach.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, filename string, doc []byte, opts *UploadOptions) (UploadResult, error)

Upload pushes a document through POST /api/v1/sboms/upload. filename is sent as X-Filename and decides how BOMHort classifies the content: it must end in .spdx.json, .cdx.json, .openvex.json, .vex.json or .json.

BOMHort only accepts uploads when AUTH_ENABLED=true (403 otherwise) and needs writable storage (503 otherwise). Ingestion is asynchronous: a "pending" result carries the job id; "duplicate" means identical content was ingested before.

func (*Client) UploadSBOM

func (c *Client) UploadSBOM(ctx context.Context, filename string, doc []byte, opts *UploadOptions) (UploadResult, error)

UploadSBOM uploads an SPDX or CycloneDX JSON document. It is Upload with a check that filename is not a VEX name.

Example
package main

import (
	"context"
	"fmt"
	"log"

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

// fakeGateway starts a bomhorttest server with one SBOM so the examples
// run without a real BOMHort.
func fakeGateway() *bomhorttest.Server {
	srv := bomhorttest.New("example-key")
	srv.AddSBOM(
		bomhort.SBOM{ID: bomhorttest.UUID(1), DocumentName: "payments-api", SourceFile: "payments-api.spdx.json", Project: "payments"},
		[]bomhort.Vulnerability{
			{VulnID: "GHSA-qppj-fm5r-hxr3", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0", FixedVersion: "0.23.0"},
		},
		nil, nil,
	)
	return srv
}

func main() {
	srv := fakeGateway()
	defer srv.Close()
	c := bomhort.New(srv.URL, bomhort.WithAPIKey("example-key"))

	doc := []byte(`{"spdxVersion":"SPDX-2.3","name":"checkout","packages":[]}`)
	res, err := c.UploadSBOM(context.Background(), "checkout.spdx.json", doc, &bomhort.UploadOptions{
		Project:    "checkout",
		Tags:       []string{"payments"},
		SourceRepo: "https://github.com/example/checkout",
		SourceRef:  "v1.4.0",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Status, res.JobType, res.Project)
}
Output:
pending sbom checkout

func (*Client) UploadVEX

func (c *Client) UploadVEX(ctx context.Context, filename string, doc []byte, sbomID string) (UploadResult, error)

UploadVEX uploads an OpenVEX document. filename must end in .openvex.json or .vex.json so BOMHort classifies the job as VEX. A non-empty sbomID scopes every statement to that SBOM (?sbom_id=, #350); with "" BOMHort resolves each product @id against the SBOMs' id, source_repo and document name, and statements whose product resolves to nothing suppress nothing.

Within its SBOM a statement applies to a finding when vulnerability.name == vuln_id and the subcomponent purl == purl (plain string equality); a product without subcomponents covers every finding with that vuln_id. Copy both values verbatim from Vulnerability. See docs/VEX.md.

Example
package main

import (
	"context"
	"fmt"
	"log"

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

// fakeGateway starts a bomhorttest server with one SBOM so the examples
// run without a real BOMHort.
func fakeGateway() *bomhorttest.Server {
	srv := bomhorttest.New("example-key")
	srv.AddSBOM(
		bomhort.SBOM{ID: bomhorttest.UUID(1), DocumentName: "payments-api", SourceFile: "payments-api.spdx.json", Project: "payments"},
		[]bomhort.Vulnerability{
			{VulnID: "GHSA-qppj-fm5r-hxr3", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0", FixedVersion: "0.23.0"},
		},
		nil, nil,
	)
	return srv
}

func main() {
	srv := fakeGateway()
	defer srv.Close()
	c := bomhort.New(srv.URL, bomhort.WithAPIKey("example-key"))
	ctx := context.Background()

	sbom, err := c.FindSBOM(ctx, "payments-api")
	if err != nil {
		log.Fatal(err)
	}
	// The product is the SBOM (scoped explicitly via sbom_id); the
	// subcomponent is the vulnerable package. vulnerability.name and the
	// subcomponent purl must be copied verbatim from the finding: BOMHort
	// matches them by exact string equality.
	doc := []byte(`{
	  "@context": "https://openvex.dev/ns/v0.2.0",
	  "@id": "https://example.com/vex/payments-api-1",
	  "author": "security@example.com",
	  "timestamp": "2026-01-01T00:00:00Z",
	  "version": 1,
	  "statements": [{
	    "vulnerability": {"name": "GHSA-qppj-fm5r-hxr3"},
	    "products": [{
	      "@id": "payments-api",
	      "subcomponents": [{"@id": "pkg:golang/golang.org/x/net@v0.17.0"}]
	    }],
	    "status": "not_affected",
	    "justification": "vulnerable_code_not_in_execute_path"
	  }]
	}`)
	res, err := c.UploadVEX(ctx, "payments-api.openvex.json", doc, sbom.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("upload:", res.Status, res.JobType)

	vulns, _ := c.Vulnerabilities(ctx, sbom.ID, nil)
	fmt.Println("vex_status:", vulns[0].VEXStatus, "scope:", vulns[0].VEXScope)
}
Output:
upload: pending vex
vex_status: not_affected scope: sbom

func (*Client) VEXStatements

func (c *Client) VEXStatements(ctx context.Context, opts *ListOptions) Seq[VEXStatement]

VEXStatements iterates over all ingested VEX statements (opts.PageSize defaults to 100, opts.Page is ignored).

func (*Client) VersionSkew

func (c *Client) VersionSkew(ctx context.Context, opts *SearchListOptions) (VersionSkew, error)

VersionSkew returns packages used in inconsistent versions across projects. opts may be nil.

func (*Client) Vulnerabilities

func (c *Client) Vulnerabilities(ctx context.Context, sbomID string, opts *VulnerabilityOptions) ([]Vulnerability, error)

Vulnerabilities returns all findings of an SBOM (not paginated) with their VEX status, one row per (vuln_id, purl) on BOMHort >= 0.7.0. opts may be nil.

func (*Client) VulnerabilityReportCSV

func (c *Client) VulnerabilityReportCSV(ctx context.Context, opts *VulnerabilityOptions, w io.Writer) (int64, error)

VulnerabilityReportCSV streams GET /api/v1/reports/vulnerabilities ?format=csv (#266, >= 0.8.0) into w and returns the bytes written: the rows of ListVulnerabilities (same scope filter) as CSV with a header line (cve_id, vuln_id, aliases, …, vex_status). Not retried on 429.

type Cluster

type Cluster struct {
	Name         string `json:"name"`
	SBOMCount    uint64 `json:"sbom_count"`
	PackageCount uint64 `json:"package_count"`
	VulnCount    uint64 `json:"vuln_count"`
	LastIngested string `json:"last_ingested,omitempty"`
}

Cluster is one entry of GET /api/v1/clusters. DTO: ClusterListItem.

type ClusterStats

type ClusterStats struct {
	Cluster              string            `json:"cluster"`
	TotalSBOMs           uint64            `json:"total_sboms"`
	TotalPackages        uint64            `json:"total_packages"`
	TotalVulnerabilities uint64            `json:"total_vulnerabilities"`
	CriticalVulns        uint64            `json:"critical_vulns"`
	HighVulns            uint64            `json:"high_vulns"`
	MediumVulns          uint64            `json:"medium_vulns"`
	LowVulns             uint64            `json:"low_vulns"`
	LicenseBreakdown     map[string]uint64 `json:"license_breakdown"`
	LastIngested         string            `json:"last_ingested,omitempty"`
}

ClusterStats is GET /api/v1/clusters/{name}/stats.

type DashboardStats

type DashboardStats struct {
	TotalSBOMs               uint64            `json:"total_sboms"`
	TotalPackages            uint64            `json:"total_packages"`
	TotalVulnerabilities     uint64            `json:"total_vulnerabilities"`
	EffectiveVulnerabilities uint64            `json:"effective_vulnerabilities"`
	SuppressedByVEX          uint64            `json:"suppressed_by_vex"`
	CriticalVulns            uint64            `json:"critical_vulns"`
	HighVulns                uint64            `json:"high_vulns"`
	MediumVulns              uint64            `json:"medium_vulns"`
	LowVulns                 uint64            `json:"low_vulns"`
	LicenseBreakdown         map[string]uint64 `json:"license_breakdown"`
	ExemptedPackages         uint64            `json:"exempted_packages"`
	TotalVEXStatements       uint64            `json:"total_vex_statements"`
	LastCVERefresh           string            `json:"last_cve_refresh,omitempty"`
	NewVulnsSinceRefresh     uint64            `json:"new_vulns_since_refresh"`
	ArchivedReposCount       uint64            `json:"archived_repos_count"`
}

DashboardStats is GET /api/v1/stats/dashboard.

type DependencyNode

type DependencyNode struct {
	Index   uint32 `json:"index"`
	SPDXID  string `json:"spdx_id"`
	Name    string `json:"name"`
	Version string `json:"version"`
	PURL    string `json:"purl"`
	License string `json:"license"`
	// LicenseSource says where License came from or why there is none
	// (>= 0.8.0; empty for SBOMs ingested before it was recorded).
	LicenseSource string   `json:"license_source,omitempty"`
	Children      []uint32 `json:"children"`
	// Stored dependency scope of the node (#444, >= 0.8.0); see Vulnerability.
	DependencyScope string  `json:"dependency_scope,omitempty"`
	DependencyDepth *uint32 `json:"dependency_depth,omitempty"`
}

DependencyNode is one node of GET /api/v1/sboms/{id}/dependencies. The tree is a flat list; Children holds the Index values of child nodes.

type DependencyStat

type DependencyStat struct {
	PackageName  string   `json:"package_name"`
	PURL         string   `json:"purl"`
	ProjectCount uint64   `json:"project_count"`
	Versions     []string `json:"versions"`
	VulnCount    uint64   `json:"vuln_count"`
}

DependencyStat is one cross-project dependency usage entry. DTO: DependencyStatsItem.

type DependencyStats

type DependencyStats struct {
	TotalUniqueDeps uint64           `json:"total_unique_deps"`
	TopDependencies []DependencyStat `json:"top_dependencies"`
}

DependencyStats is GET /api/v1/stats/dependencies. DTO: DependencyStatsResponse.

type FleetCluster

type FleetCluster struct {
	Name       string           `json:"name"`
	SBOMCount  uint64           `json:"sbom_count"`
	VulnCount  uint64           `json:"vuln_count"`
	Namespaces []FleetNamespace `json:"namespaces"`
}

FleetCluster is the root of the ownership tree of GET /api/v1/fleet. Unassigned dimensions appear with an empty name.

type FleetNamespace

type FleetNamespace struct {
	Name      string         `json:"name"`
	SBOMCount uint64         `json:"sbom_count"`
	VulnCount uint64         `json:"vuln_count"`
	Projects  []FleetProject `json:"projects"`
}

FleetNamespace groups projects inside one cluster.

type FleetProject

type FleetProject struct {
	Name         string `json:"name"`
	SBOMCount    uint64 `json:"sbom_count"`
	VulnCount    uint64 `json:"vuln_count"`
	LastIngested string `json:"last_ingested,omitempty"`
}

FleetProject is a leaf of the ownership tree.

type Health

type Health struct {
	Status string `json:"status"`
	Reason string `json:"reason,omitempty"`
}

Health is the body of /healthz, /livez and /readyz.

type LicenseCompliance

type LicenseCompliance struct {
	LicenseID            string     `json:"license_id"`
	Category             string     `json:"category"`
	PackageCount         uint64     `json:"package_count"`
	SBOMCount            int        `json:"sbom_count"`
	NonCompliantPackages []string   `json:"non_compliant_packages,omitempty"`
	ExemptedPackages     []string   `json:"exempted_packages,omitempty"`
	ExemptionReason      string     `json:"exemption_reason,omitempty"`
	AffectedSBOMs        []SBOMLink `json:"affected_sboms,omitempty"`
}

LicenseCompliance is one entry of GET /api/v1/licenses/compliance. DTO: LicenseComplianceItem.

type LicenseException

type LicenseException struct {
	ID           string `json:"id"`
	Package      string `json:"package"`
	License      string `json:"license"`
	Project      string `json:"project,omitempty"`
	Status       string `json:"status"`
	ApprovedDate string `json:"approvedDate"`
	Scope        string `json:"scope,omitempty"`
	Results      string `json:"results,omitempty"`
	IssueURL     string `json:"issueUrl,omitempty"`
	PackageURL   string `json:"packageUrl,omitempty"`
	Comment      string `json:"comment,omitempty"`
}

LicenseException exempts one package+license combination.

type LicenseExceptions

type LicenseExceptions struct {
	Version           string                    `json:"version"`
	LastUpdated       string                    `json:"lastUpdated"`
	Description       string                    `json:"description,omitempty"`
	BlanketExceptions []BlanketLicenseException `json:"blanketExceptions"`
	Exceptions        []LicenseException        `json:"exceptions"`
}

LicenseExceptions is GET /api/v1/license-exceptions (the operator's license-exceptions.json). Server type: license.ExceptionsFile.

type LicensePolicy

type LicensePolicy struct {
	Description    string   `json:"description,omitempty"`
	Permissive     []string `json:"permissive"`
	Copyleft       []string `json:"copyleft"`
	ExpressionMode string   `json:"expressionMode,omitempty"`
}

LicensePolicy is GET /api/v1/license-policy (permissive/copyleft classification). Server type: license.PolicyFile.

type LicenseSource

type LicenseSource struct {
	// Source is the full value ("depsdev+latest", "not-published";
	// "unrecorded" for packages ingested before sources were recorded),
	// Origin the part before "+" and Modifiers the "+"-joined suffixes.
	Source    string   `json:"source"`
	Origin    string   `json:"origin"`
	Modifiers []string `json:"modifiers"`
	// Resolved is false when Source is a reason for a missing license.
	Resolved     bool     `json:"resolved"`
	PackageCount uint64   `json:"package_count"`
	SBOMCount    uint64   `json:"sbom_count"`
	Examples     []string `json:"examples"`
}

LicenseSource is one entry of GET /api/v1/licenses/sources (>= 0.8.0): where package licenses came from, or why they are missing. DTO: LicenseSourceItem.

type LicenseViolation

type LicenseViolation struct {
	SBOMID               string   `json:"sbom_id"`
	SourceFile           string   `json:"source_file"`
	DocumentName         string   `json:"document_name"`
	CopyleftCount        uint64   `json:"copyleft_count"`
	UnapprovedCount      uint64   `json:"unapproved_count"` // >= 0.8.0
	UnknownCount         uint64   `json:"unknown_count"`
	ViolatingLicenses    []string `json:"violating_licenses"`
	NonCompliantPackages []string `json:"non_compliant_packages"`
	// PackageScopes: see SBOMLicense (#444, >= 0.8.0).
	PackageScopes map[string]string `json:"package_scopes,omitempty"`
}

LicenseViolation is one entry of GET /api/v1/projects/license-compliance. DTO: ProjectLicenseViolation.

type LicenseViolationSummary

type LicenseViolationSummary struct {
	SBOMID                   string   `json:"sbom_id"`
	SourceFile               string   `json:"source_file"`
	DocumentName             string   `json:"document_name"`
	CopyleftCount            uint64   `json:"copyleft_count"`
	UnapprovedCount          uint64   `json:"unapproved_count"`
	UnknownCount             uint64   `json:"unknown_count"`
	ViolatingLicenses        []string `json:"violating_licenses"`
	NonCompliantPackageCount uint64   `json:"non_compliant_package_count"`
	DirectCount              uint64   `json:"direct_count"`
	TransitiveCount          uint64   `json:"transitive_count"`
}

LicenseViolationSummary is one entry of GET /api/v1/projects/license-compliance?summary=true (>= 0.8.0): the LicenseViolation counts without the package lists. DTO: ProjectLicenseViolationSummary.

type ListOptions

type ListOptions struct {
	Page     int
	PageSize int
}

ListOptions selects one page of a paginated endpoint. Zero values use the server defaults (page 1, page_size 50; BOMHort caps page_size at 500).

type Namespace

type Namespace struct {
	Name         string `json:"name"`
	Cluster      string `json:"cluster,omitempty"`
	ClusterCount uint64 `json:"cluster_count"`
	SBOMCount    uint64 `json:"sbom_count"`
	PackageCount uint64 `json:"package_count"`
	VulnCount    uint64 `json:"vuln_count"`
	LastIngested string `json:"last_ingested,omitempty"`
}

Namespace is one entry of GET /api/v1/namespaces. A namespace name is only unique inside a cluster; ClusterCount tells how many clusters use it. Cluster is set only when the listing was filtered. DTO: NamespaceListItem.

type NamespaceStats

type NamespaceStats struct {
	Namespace            string            `json:"namespace"`
	Cluster              string            `json:"cluster,omitempty"`
	Clusters             []string          `json:"clusters"`
	TotalSBOMs           uint64            `json:"total_sboms"`
	TotalPackages        uint64            `json:"total_packages"`
	TotalVulnerabilities uint64            `json:"total_vulnerabilities"`
	CriticalVulns        uint64            `json:"critical_vulns"`
	HighVulns            uint64            `json:"high_vulns"`
	MediumVulns          uint64            `json:"medium_vulns"`
	LowVulns             uint64            `json:"low_vulns"`
	LicenseBreakdown     map[string]uint64 `json:"license_breakdown"`
	LastIngested         string            `json:"last_ingested,omitempty"`
}

NamespaceStats is GET /api/v1/namespaces/{name}/stats.

type Option

type Option func(*Client)

Option configures a Client.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey authenticates every request with the X-API-Key header.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient replaces the underlying HTTP client (default: 60 s timeout).

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries bounds the number of retries on HTTP 429 (default 3).

func WithRateLimit

func WithRateLimit(limit int, window time.Duration) Option

WithRateLimit paces requests to at most limit per window on the client side (limit <= 0 disables pacing). BOMHort allows 100 requests per 10 s per client IP; 90 / 10 s leaves headroom for other callers.

func WithRateLimiter

func WithRateLimiter(l *RateLimiter) Option

WithRateLimiter shares one RateLimiter between several clients talking to the same gateway.

func WithServiceToken

func WithServiceToken(token string) Option

WithServiceToken authenticates every request with "Authorization: Bearer <token>" (BOMHort's SERVICE_TOKEN).

func WithStrictDecoding

func WithStrictDecoding() Option

WithStrictDecoding makes response decoding fail on JSON fields the client does not know. Off by default so new server fields never break callers; the integration tests turn it on to detect API drift early.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent overrides the User-Agent header (default DefaultUserAgent).

type PackageDetail

type PackageDetail struct {
	PackageName   string           `json:"package_name"`
	TotalProjects uint64           `json:"total_projects"`
	Projects      []PackageProject `json:"projects"`
	Page          uint64           `json:"page"`
	PageSize      uint64           `json:"page_size"`
}

PackageDetail is GET /api/v1/packages/detail. DTO: PackageDetailResponse.

type PackageProject

type PackageProject struct {
	ProjectName string `json:"project_name"`
	Version     string `json:"version"`
	SBOMID      string `json:"sbom_id"`
}

PackageProject is a project using a package. DTO: DependencySearchProject.

type PackageSearch

type PackageSearch struct {
	TotalResults uint64                `json:"total_results"`
	Items        []PackageSearchResult `json:"items"`
	Page         uint64                `json:"page"`
	PageSize     uint64                `json:"page_size"`
	Query        string                `json:"query"`
}

PackageSearch is GET /api/v1/packages/search. DTO: DependencySearchResponse.

type PackageSearchResult

type PackageSearchResult struct {
	PackageName  string           `json:"package_name"`
	PURL         string           `json:"purl"`
	ProjectCount uint64           `json:"project_count"`
	Versions     []string         `json:"versions"`
	Projects     []PackageProject `json:"projects"`
}

PackageSearchResult is one package found by search. DTO: DependencySearchResult.

type Paginated

type Paginated[T any] struct {
	Data     []T    `json:"data"`
	Total    uint64 `json:"total"`
	Page     uint64 `json:"page"`
	PageSize uint64 `json:"page_size"`
}

Paginated wraps the list responses of paginated endpoints.

type Project

type Project struct {
	ProjectName    string   `json:"project_name"`
	SBOMCount      uint64   `json:"sbom_count"`
	PackageCount   uint64   `json:"package_count"`
	VulnCount      uint64   `json:"vuln_count"`
	LatestIngested string   `json:"latest_ingested"`
	LatestSBOMID   string   `json:"latest_sbom_id"`
	Tags           []string `json:"tags"`
	// Parent is the resolved product this project belongs to and
	// ParentSource how it was resolved (>= 0.8.0).
	Parent       string `json:"parent,omitempty"`
	ParentSource string `json:"parent_source,omitempty"`
}

Project is one entry of GET /api/v1/projects. PackageCount and VulnCount are de-duplicated across the project's SBOMs (#398). DTO: ProjectListItem.

type ProjectDetail

type ProjectDetail struct {
	ProjectName         string            `json:"project_name"`
	Tags                []string          `json:"tags"`
	Parents             []string          `json:"parents"`
	RelatedProjectCount uint64            `json:"related_project_count"`
	SBOMCount           uint64            `json:"sbom_count"`
	PackageCount        uint64            `json:"package_count"`
	VulnCount           uint64            `json:"vuln_count"`
	CriticalVulns       uint64            `json:"critical_vulns"`
	HighVulns           uint64            `json:"high_vulns"`
	MediumVulns         uint64            `json:"medium_vulns"`
	LowVulns            uint64            `json:"low_vulns"`
	LatestIngested      string            `json:"latest_ingested,omitempty"`
	LatestVersion       string            `json:"latest_version,omitempty"`
	LatestSBOMID        string            `json:"latest_sbom_id,omitempty"`
	SourceRepo          string            `json:"source_repo,omitempty"`
	Clusters            []string          `json:"clusters"`
	Namespaces          []string          `json:"namespaces"`
	LicenseBreakdown    map[string]uint64 `json:"license_breakdown"`
	Parent              string            `json:"parent,omitempty"`
	ParentSource        string            `json:"parent_source,omitempty"`
	ParentOwner         string            `json:"parent_owner,omitempty"`
	Children            []string          `json:"children"`
}

ProjectDetail is GET /api/v1/projects/{name} (#398, >= 0.8.0): one project aggregated across all of its SBOMs.

type ProjectGroup

type ProjectGroup struct {
	Name           string    `json:"name"`
	IsProject      bool      `json:"is_project"`
	ProjectCount   uint64    `json:"project_count"`
	SBOMCount      uint64    `json:"sbom_count"`
	PackageCount   uint64    `json:"package_count"`
	VulnCount      uint64    `json:"vuln_count"`
	LatestIngested string    `json:"latest_ingested"`
	Tags           []string  `json:"tags"`
	Sources        []string  `json:"sources"`
	Owner          string    `json:"owner,omitempty"`
	Members        []Project `json:"members"`
}

ProjectGroup is one entry of GET /api/v1/projects?group_by=parent (>= 0.8.0). DTO: ProjectGroupItem.

type ProjectListOptions

type ProjectListOptions struct {
	ListOptions
	// Search is a substring match on the project name.
	Search string
	// Tag narrows the listing to one grouping label. Sub-projects of a
	// parent project P are listed with Tag: P.
	Tag string
}

ProjectListOptions filters GET /api/v1/projects.

type ProjectPackage

type ProjectPackage struct {
	Name      string `json:"name"`
	Version   string `json:"version"`
	PURL      string `json:"purl,omitempty"`
	SBOMCount uint64 `json:"sbom_count"`
	VulnCount uint64 `json:"vuln_count"`
}

ProjectPackage is one distinct component across a project's SBOMs (GET /api/v1/projects/{name}/packages, >= 0.8.0). DTO: ProjectPackageItem.

type RateLimiter

type RateLimiter struct {
	// contains filtered or unexported fields
}

RateLimiter admits at most limit requests per window (sliding). BOMHort's gateway allows 100 requests / 10 s per client IP by default; staying below that avoids 429 back-off storms when several goroutines share one gateway. A RateLimiter is safe for concurrent use and may be shared between clients via WithRateLimiter.

func NewRateLimiter

func NewRateLimiter(limit int, window time.Duration) *RateLimiter

NewRateLimiter returns nil (no limiting) when limit <= 0.

func (*RateLimiter) Wait

func (r *RateLimiter) Wait(ctx context.Context) error

Wait blocks until a request may be sent or ctx is done. A nil limiter never blocks.

type SBOM

type SBOM struct {
	ID           string `json:"sbom_id"`
	SourceFile   string `json:"source_file"`
	SPDXVersion  string `json:"spdx_version"`
	DocumentName string `json:"document_name"`
	// DocumentVersion is the described product's version ("" = unknown).
	DocumentVersion string `json:"document_version,omitempty"`
	PackageCount    uint64 `json:"package_count"`
	VulnCount       uint64 `json:"vuln_count"`
	IngestedAt      string `json:"ingested_at"`
	// SourceRepo and SourceRef are the product's source repository and
	// commit/tag (BOMHort #332, >= 0.7.0); empty when unknown.
	SourceRepo string `json:"source_repo,omitempty"`
	SourceRef  string `json:"source_ref,omitempty"`
	// Ownership dimensions (#177); empty on single-instance deployments.
	Cluster   string `json:"cluster,omitempty"`
	Namespace string `json:"namespace,omitempty"`
	Project   string `json:"project,omitempty"`
}

SBOM is one entry of GET /api/v1/sboms (and the project, cluster and namespace SBOM listings). DTO: SBOMListItem.

type SBOMDetail

type SBOMDetail struct {
	ID              string `json:"sbom_id"`
	SourceFile      string `json:"source_file"`
	SPDXVersion     string `json:"spdx_version"`
	DocumentName    string `json:"document_name"`
	DocumentVersion string `json:"document_version,omitempty"`
	PackageCount    uint64 `json:"package_count"`
	VulnCount       uint64 `json:"vuln_count"`
	IngestedAt      string `json:"ingested_at"`
	SourceRepo      string `json:"source_repo,omitempty"`
	SourceRef       string `json:"source_ref,omitempty"`
	CriticalVulns   uint64 `json:"critical_vulns"`
	HighVulns       uint64 `json:"high_vulns"`
	MediumVulns     uint64 `json:"medium_vulns"`
	LowVulns        uint64 `json:"low_vulns"`
}

SBOMDetail is GET /api/v1/sboms/{id}/detail.

type SBOMLicense

type SBOMLicense struct {
	LicenseID        string   `json:"license_id"`
	Category         string   `json:"category"`
	PackageCount     uint32   `json:"package_count"`
	Packages         []string `json:"packages"`
	ExemptedPackages []string `json:"exempted_packages,omitempty"`
	ExemptionReason  string   `json:"exemption_reason,omitempty"`
	// PackageScopes maps a name from Packages/ExemptedPackages to its
	// dependency scope; absent names are unknown (#444, >= 0.8.0).
	PackageScopes map[string]string `json:"package_scopes,omitempty"`
}

SBOMLicense is one entry of GET /api/v1/sboms/{id}/licenses. DTO: SBOMLicenseBreakdownItem.

type SBOMLink struct {
	SBOMID       string `json:"sbom_id"`
	DocumentName string `json:"document_name"`
}

SBOMLink references an SBOM by id and document name. DTO: VEXAffectedSBOM, LicenseAffectedSBOM.

type SBOMListOptions

type SBOMListOptions struct {
	ListOptions
	// Search is a substring match on document name or source path.
	Search string
	// Project is an exact project name (#398, >= 0.8.0; ignored by
	// older gateways).
	Project string
}

SBOMListOptions filters GET /api/v1/sboms.

type SearchLicense

type SearchLicense struct {
	LicenseID string `json:"license_id"`
	Category  string `json:"category"`
	SBOMCount uint64 `json:"sbom_count"`
}

SearchLicense is a license hit. DTO: GlobalSearchLicense.

type SearchListOptions

type SearchListOptions struct {
	ListOptions
	Search string
}

SearchListOptions is ListOptions plus a substring search term.

type SearchPackage

type SearchPackage struct {
	PackageName  string `json:"package_name"`
	PURL         string `json:"purl"`
	ProjectCount uint64 `json:"project_count"`
}

SearchPackage is a package hit. DTO: GlobalSearchPackage.

type SearchProject

type SearchProject struct {
	ProjectName  string `json:"project_name"`
	SBOMCount    uint64 `json:"sbom_count"`
	LatestSBOMID string `json:"latest_sbom_id"`
}

SearchProject is a project hit. DTO: GlobalSearchProject.

type SearchResult

type SearchResult struct {
	Query                string                `json:"query"`
	Packages             []SearchPackage       `json:"packages"`
	TotalPackages        uint64                `json:"total_packages"`
	Projects             []SearchProject       `json:"projects"`
	TotalProjects        uint64                `json:"total_projects"`
	Vulnerabilities      []SearchVulnerability `json:"vulnerabilities"`
	TotalVulnerabilities uint64                `json:"total_vulnerabilities"`
	Licenses             []SearchLicense       `json:"licenses"`
	TotalLicenses        uint64                `json:"total_licenses"`
}

SearchResult is GET /api/v1/search, a faceted search across packages, projects, vulnerabilities and licenses. DTO: GlobalSearchResponse.

type SearchVulnerability

type SearchVulnerability struct {
	VulnID string `json:"vuln_id"`
	// CVEID and Aliases as on Vulnerability (#412, >= 0.8.0); the search
	// also matches on any alias.
	CVEID         string   `json:"cve_id,omitempty"`
	Aliases       []string `json:"aliases,omitempty"`
	Severity      string   `json:"severity"`
	Summary       string   `json:"summary"`
	AffectedSBOMs uint64   `json:"affected_sboms"`
}

SearchVulnerability is a vulnerability hit. DTO: GlobalSearchVulnerability.

type Seq

type Seq[T any] = iter.Seq2[T, error]

Seq is a lazy sequence of list items. Iteration stops at the first error, which is yielded together with the zero value of T:

for s, err := range c.SBOMs(ctx, nil) {
	if err != nil { return err }
	...
}

func Paginate

func Paginate[T any](ctx context.Context, pageSize int, fetch func(context.Context, ListOptions) (Paginated[T], error)) Seq[T]

Paginate turns any page-wise List* call into a lazy sequence. It walks pages of pageSize (DefaultWalkPageSize if <= 0) until an empty page or the reported total is reached:

vulns := bomhort.Paginate(ctx, 200, func(ctx context.Context, o bomhort.ListOptions) (bomhort.Paginated[bomhort.Vulnerability], error) {
	return c.ListVulnerabilities(ctx, &o)
})
Example
package main

import (
	"context"
	"fmt"
	"log"

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

// fakeGateway starts a bomhorttest server with one SBOM so the examples
// run without a real BOMHort.
func fakeGateway() *bomhorttest.Server {
	srv := bomhorttest.New("example-key")
	srv.AddSBOM(
		bomhort.SBOM{ID: bomhorttest.UUID(1), DocumentName: "payments-api", SourceFile: "payments-api.spdx.json", Project: "payments"},
		[]bomhort.Vulnerability{
			{VulnID: "GHSA-qppj-fm5r-hxr3", Severity: "HIGH", PURL: "pkg:golang/golang.org/x/net@v0.17.0", FixedVersion: "0.23.0"},
		},
		nil, nil,
	)
	return srv
}

func main() {
	srv := fakeGateway()
	defer srv.Close()
	c := bomhort.New(srv.URL, bomhort.WithAPIKey("example-key"))
	ctx := context.Background()

	// Any List* call can be turned into a lazy sequence.
	pages := bomhort.Paginate(ctx, 200, func(ctx context.Context, o bomhort.ListOptions) (bomhort.Paginated[bomhort.SBOM], error) {
		return c.ListSBOMs(ctx, &bomhort.SBOMListOptions{ListOptions: o, Project: "payments"})
	})
	sboms, err := bomhort.Collect(pages)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(sboms), sboms[0].DocumentName)
}
Output:
1 payments-api

type SourceAttribution

type SourceAttribution struct {
	SBOMID     string `json:"sbom_id"`
	SourceRepo string `json:"source_repo"`
	SourceRef  string `json:"source_ref"`
}

SourceAttribution is the response of PATCH /api/v1/sboms/{id}.

type SourcePatch

type SourcePatch struct {
	SourceRepo *string `json:"source_repo,omitempty"`
	SourceRef  *string `json:"source_ref,omitempty"`
}

SourcePatch is the body of PATCH /api/v1/sboms/{id}. A nil field is left unchanged; a pointer to "" clears it.

type Tag

type Tag struct {
	Tag          string `json:"tag"`
	SBOMCount    uint64 `json:"sbom_count"`
	ProjectCount uint64 `json:"project_count"`
	// IsProject is true when the tag is also a project name, i.e. a parent
	// (#398, >= 0.8.0).
	IsProject bool `json:"is_project"`
}

Tag is one entry of GET /api/v1/tags. DTO: TagListItem.

type UploadOptions

type UploadOptions struct {
	// SBOMID scopes every statement of an uploaded VEX document to one
	// SBOM (BOMHort #350, >= 0.7.0). Only valid for VEX uploads; must be a
	// UUID.
	SBOMID string
	// Cluster, Namespace and Project override the gateway's ownership
	// defaults (CLUSTER_NAME, NAMESPACE, PROJECT).
	Cluster   string
	Namespace string
	Project   string
	// Parent names the product the upload belongs to (>= 0.8.0).
	Parent string
	// Tags are merged with the instance's default tags.
	Tags []string
	// SourceRepo (http(s) URL without credentials) and SourceRef (git ref)
	// attribute the SBOM to its source (#332, >= 0.7.0), sent as
	// X-Source-Repo / X-Source-Ref.
	SourceRepo string
	SourceRef  string
}

UploadOptions are the optional parameters of POST /api/v1/sboms/upload. Empty fields are omitted, so the gateway's configured defaults apply.

type UploadResult

type UploadResult struct {
	// Status is "pending" (202, job enqueued) or "duplicate" (200, content
	// with the same SHA-256 was already ingested).
	Status     string `json:"status"`
	JobID      string `json:"job_id,omitempty"`
	SHA256Hash string `json:"sha256_hash"`
	// JobType is "sbom" or "vex".
	JobType   string `json:"job_type,omitempty"`
	Cluster   string `json:"cluster,omitempty"`
	Namespace string `json:"namespace,omitempty"`
	Project   string `json:"project,omitempty"`
	Parent    string `json:"parent,omitempty"` // >= 0.8.0
	Message   string `json:"message,omitempty"`
}

UploadResult is the response of POST /api/v1/sboms/upload.

func (UploadResult) Duplicate

func (r UploadResult) Duplicate() bool

Duplicate reports whether BOMHort skipped the upload as already ingested.

type VEXStatement

type VEXStatement struct {
	VEXID      string `json:"vex_id"`
	DocumentID string `json:"document_id"`
	SourceFile string `json:"source_file"`
	// SBOMID is the SBOM the statement is scoped to (#350). Empty means
	// the product resolved to no SBOM (yet): such statements suppress
	// nothing until a matching SBOM is ingested.
	SBOMID string `json:"sbom_id,omitempty"`
	// ProductPURL is the subcomponent purl the statement is about, or "*"
	// for a product-wide statement (product without subcomponents).
	ProductPURL string `json:"product_purl"`
	VulnID      string `json:"vuln_id"`
	// CVEID and Aliases are the other names of the flaw, looked up from
	// the findings the statement can match (#412, >= 0.8.0). VulnID stays
	// the id the author used; match on it, not on the aliases.
	CVEID           string   `json:"cve_id,omitempty"`
	Aliases         []string `json:"aliases,omitempty"`
	Status          string   `json:"status"`
	Justification   string   `json:"justification"`
	ImpactStatement string   `json:"impact_statement,omitempty"`
	ActionStatement string   `json:"action_statement,omitempty"`
	VEXTimestamp    string   `json:"vex_timestamp"`
	IngestedAt      string   `json:"ingested_at"`
	// Provenance (#334, >= 0.7.0); empty when the document has none.
	Author        string     `json:"author,omitempty"`
	Role          string     `json:"role,omitempty"`
	Tooling       string     `json:"tooling,omitempty"`
	StatusNotes   string     `json:"status_notes,omitempty"`
	AffectedSBOMs []SBOMLink `json:"affected_sboms,omitempty"`
}

VEXStatement is one ingested statement: GET /api/v1/vex/statements and /sboms/{id}/vex. DTO: VEXStatementItem.

type VersionSkew

type VersionSkew struct {
	TotalSkewedPackages uint64            `json:"total_skewed_packages"`
	Items               []VersionSkewItem `json:"items"`
	Page                uint64            `json:"page"`
	PageSize            uint64            `json:"page_size"`
}

VersionSkew is GET /api/v1/stats/version-skew. DTO: VersionSkewResponse.

type VersionSkewDetail

type VersionSkewDetail struct {
	Version      string   `json:"version"`
	ProjectCount uint64   `json:"project_count"`
	Projects     []string `json:"projects"`
}

VersionSkewDetail is the per-version breakdown of a skewed package.

type VersionSkewItem

type VersionSkewItem struct {
	PackageName     string              `json:"package_name"`
	PURL            string              `json:"purl"`
	VersionCount    uint64              `json:"version_count"`
	ProjectCount    uint64              `json:"project_count"`
	IsDirectInCount uint64              `json:"is_direct_in_count"`
	Versions        []VersionSkewDetail `json:"versions"`
}

VersionSkewItem is a package with inconsistent versions across projects.

type Vulnerability

type Vulnerability struct {
	VulnID string `json:"vuln_id"`
	// CVEID is the CVE of the same flaw (VulnID itself when that is a CVE,
	// else the smallest CVE among Aliases); empty when there is none.
	// Aliases are every other OSV id of the flaw, sorted (#412, >= 0.8.0).
	CVEID   string   `json:"cve_id,omitempty"`
	Aliases []string `json:"aliases,omitempty"`
	// DependencyScope is root|direct|transitive|unknown and DependencyDepth
	// the shortest path from the described product (0 root, 1 direct, >= 2
	// transitive; nil when unknown) (#444, >= 0.8.0).
	DependencyScope string  `json:"dependency_scope,omitempty"`
	DependencyDepth *uint32 `json:"dependency_depth,omitempty"`
	Severity        string  `json:"severity"`
	PURL            string  `json:"purl"`
	Summary         string  `json:"summary"`
	FixedVersion    string  `json:"fixed_version"`
	SourceFile      string  `json:"source_file"`
	DiscoveredAt    string  `json:"discovered_at"`
	VEXStatus       string  `json:"vex_status,omitempty"`
	// Effective VEX statement detail (#335, >= 0.7.0): one row per
	// (vuln_id, purl); the statement with the newest vex_timestamp wins.
	VEXJustification string `json:"vex_justification,omitempty"`
	VEXTimestamp     string `json:"vex_timestamp,omitempty"`
	VEXStatementID   string `json:"vex_statement_id,omitempty"`
	VEXAuthor        string `json:"vex_author,omitempty"`
	VEXTooling       string `json:"vex_tooling,omitempty"`
	// VEXScope is "sbom" when a statement scoped to this SBOM applies
	// (#350, >= 0.7.0). BOMHort's DTO also documents "global", but since
	// 0.7.0 unscoped statements never apply, so it is not sent in practice.
	VEXScope string `json:"vex_scope,omitempty"`
	// AffectedSBOMs is set only on project-level listings: how many of the
	// project's SBOMs carry this (vuln_id, purl) pair (#398, >= 0.8.0).
	AffectedSBOMs uint64 `json:"affected_sboms,omitempty"`
}

Vulnerability is one finding: GET /api/v1/vulnerabilities, /sboms/{id}/vulnerabilities and /projects/{name}/vulnerabilities. DTO: VulnerabilityListItem.

A VEX statement applies to a finding when it is scoped to the finding's SBOM, statement.vuln_id == VulnID and statement.product_purl == PURL or "*" (product-wide), all by plain string equality; never normalise VulnID or PURL before writing VEX for them. See docs/VEX.md.

type VulnerabilityListOptions

type VulnerabilityListOptions struct {
	ListOptions
	VulnerabilityOptions
}

VulnerabilityListOptions pages and filters GET /api/v1/vulnerabilities.

type VulnerabilityOptions

type VulnerabilityOptions struct {
	// Scope keeps only findings whose package is pulled in that way: one
	// of ScopeRoot, ScopeDirect, ScopeTransitive, ScopeUnknown (#444,
	// >= 0.8.0; older gateways ignore it). Anything else is a 400.
	Scope string
}

VulnerabilityOptions filters the per-SBOM and per-project finding lists. nil returns every scope.

Directories

Path Synopsis
Package bomhorttest provides an in-memory fake of the BOMHort API gateway for tests of code that talks to BOMHort through bomhort.Client.
Package bomhorttest provides an in-memory fake of the BOMHort API gateway for tests of code that talks to BOMHort through bomhort.Client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL