API Reference
The BOMHort API is primarily read-only (GET endpoints). One write endpoint exists — POST /api/v1/sboms/upload for push-model CI/CD ingestion — and it is disabled unless AUTH_ENABLED=true, regardless of the global auth default.
Base URL
http://<api-gateway-host>:8080/api/v1
In headless mode (Helm: ui.enabled: false), only the API Gateway is deployed. All endpoints remain the same.
Authentication
Authentication is fully optional and disabled by default. Enable it via the AUTH_ENABLED=true environment variable on the API Gateway. See the Deployment Guide for full setup instructions.
Two modes (combinable)
Service Token — single shared secret, typically used by upstream proxies (oauth2-proxy, Kong, custom auth gateways):
# Either header works:
curl -H "Authorization: Bearer <service-token>" \
https://api.bomhort.example.com/api/v1/stats/dashboard
curl -H "X-Service-Token: <service-token>" \
https://api.bomhort.example.com/api/v1/stats/dashboard
API Keys — multiple pre-shared keys for direct API consumers (CI/CD pipelines, scripts):
curl -H "X-API-Key: <api-key>" \
https://api.bomhort.example.com/api/v1/stats/dashboard
Public endpoints (always accessible)
Even when authentication is enabled, the following endpoints bypass auth so Kubernetes probes and CORS preflight work:
| Endpoint | Reason |
|---|---|
/healthz | K8s health check (legacy, always 200) |
/livez | Liveness probe (always 200 if process alive) |
/readyz | Readiness probe (pings ClickHouse, 503 if unavailable) |
OPTIONS * | CORS preflight |
Responses
| Status | Condition |
|---|---|
200 OK | Auth disabled, or valid token/key presented |
401 Unauthorized (with WWW-Authenticate: Bearer realm="bomhort") | Auth enabled, no credential sent |
401 Unauthorized | Auth enabled, invalid token/key |
All credential comparisons are constant-time (crypto/subtle.ConstantTimeCompare) to prevent timing attacks. Failed auth attempts are logged with sanitized client IPs.
Common Patterns
Pagination
Paginated endpoints accept:
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | uint64 | 1 | — | Page number (1-based) |
page_size | uint64 | 50 | 500 | Items per page |
Paginated responses return:
{
"data": [...],
"total": 1234,
"page": 1,
"page_size": 50
}
Error Responses
All errors return:
{
"error": "Human-readable error message"
}
| HTTP Status | Meaning |
|---|---|
| 400 | Bad Request — invalid parameters (malformed UUID, missing required query param) |
| 429 | Too Many Requests — rate limit exceeded (100 req/10s per IP) |
| 500 | Internal Server Error — database or processing failure |
Rate Limiting
- 100 requests per 10 seconds per source IP
- Returns
429 Too Many Requestswhen exceeded - No
Retry-Afterheader (client should implement exponential backoff)
Health
GET /healthz
Health check endpoint for Kubernetes probes.
Response: 200 OK
{"status": "ok"}
GET /livez
Liveness probe. Returns 200 OK as long as the API Gateway process is running. Used by Kubernetes to determine if the pod should be restarted.
Response: 200 OK
{"status": "ok"}
GET /readyz
Readiness probe. Pings ClickHouse to verify database connectivity. Returns 503 Service Unavailable if the database is unreachable. Kubernetes uses this to remove the pod from service endpoints until it recovers.
Response (healthy): 200 OK
{"status": "ok"}
Response (unhealthy): 503 Service Unavailable
{"error": "ClickHouse unavailable"}
Dashboard & Statistics
GET /api/v1/stats/dashboard
Aggregated platform statistics for the dashboard view.
Response: 200 OK
{
"total_sboms": 142,
"total_packages": 18743,
"total_vulnerabilities": 892,
"effective_vulnerabilities": 756,
"suppressed_by_vex": 136,
"critical_vulns": 23,
"high_vulns": 187,
"medium_vulns": 412,
"low_vulns": 270,
"license_breakdown": {
"Apache-2.0": 8421,
"MIT": 6234,
"BSD-3-Clause": 2100
},
"exempted_packages": 14,
"total_vex_statements": 42,
"last_cve_refresh": "2026-05-20T03:00:00Z",
"new_vulns_since_refresh": 3,
"archived_repos_count": 7
}
GET /api/v1/stats/dependencies
Top-N most used dependencies across all projects.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | uint64 | 50 | Number of top dependencies to return (max 500) |
Response: 200 OK
{
"total_unique_deps": 4521,
"top_dependencies": [
{
"package_name": "golang.org/x/net",
"purl": "pkg:golang/golang.org/x/net",
"project_count": 87,
"versions": ["v0.23.0", "v0.24.0", "v0.25.0"],
"vuln_count": 2
}
]
}
GET /api/v1/stats/version-skew
Packages with inconsistent versions across different projects (version skew detection).
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | uint64 | 1 | Page number |
page_size | uint64 | 50 | Items per page (max 500) |
search | string | — | Filter by package name (case-insensitive substring match) |
Response: 200 OK
{
"total_skewed_packages": 234,
"items": [
{
"package_name": "golang.org/x/crypto",
"purl": "pkg:golang/golang.org/x/crypto",
"version_count": 4,
"project_count": 12,
"is_direct_in_count": 8,
"versions": [
{
"version": "v0.23.0",
"project_count": 5,
"projects": ["prometheus", "grafana", "etcd"]
},
{
"version": "v0.21.0",
"project_count": 3,
"projects": ["containerd", "runc"]
}
]
}
],
"page": 1,
"page_size": 50
}
SBOMs
POST /api/v1/sboms/upload
Push an SBOM or VEX document directly instead of relying on a filesystem/S3 scan — designed for CI/CD pipelines. The upload is enqueued as a normal ingestion job; the parsing worker processes it exactly like any other job, so no ingestion logic is duplicated here.
Storage backend: the upload is routed to whichever push-storage backend is configured, resolved once at API Gateway startup:
- If one of the configured
S3_BUCKETSentries has"skipScan": true, the upload is written there (s3://<bucket>/<prefix>/pushed/<uuid>-<filename>). That bucket is excluded from the ingestion watcher’s periodic scan so the same object is never rediscovered and re-enqueued under a different dedup hash. - Otherwise, it falls back to the local filesystem under
SBOM_DIR/pushed/— this requires the API Gateway’sSBOM_DIRmount to be writable (see the Deployment Guide for the HelmsbomSource.writableflag). - If neither is available, every request gets
503 Service Unavailablerather than a confusing storage error.
This endpoint refuses every request with 403 Forbidden unless AUTH_ENABLED=true on the API Gateway — independent of whether the request would otherwise pass the global auth middleware. A write endpoint open by default is a materially different risk than a read-only API open by default, so this check is enforced by the handler itself.
Headers:
| Header | Required | Description |
|---|---|---|
X-Filename | ✅ | Original filename. Must end in .spdx.json, .cdx.json, .openvex.json, .vex.json, or a generic .json (format auto-detected downstream, same as local scans). Only the base name is used — any directory components are stripped before the file is stored. |
Authorization / X-Service-Token / X-API-Key | ✅ | Same credential options as the rest of the API — see Authentication. |
Body: Raw SBOM or VEX JSON content (not multipart form data). Max size is MAX_UPLOAD_SIZE_MB (Helm: apiGateway.maxUploadSizeMB, default 50 MB); larger bodies are rejected with 413.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
cluster | string | Overrides this instance’s configured CLUSTER_NAME for the resulting ingestion job. |
Response (accepted): 202 Accepted
{
"status": "pending",
"job_id": "d465b76c-5b91-48a1-b069-97069e9759a1",
"sha256_hash": "3a7bd3e2360a3d4b1f8e...",
"job_type": "sbom",
"cluster": "production"
}
Response (already ingested): 200 OK
{
"status": "duplicate",
"sha256_hash": "3a7bd3e2360a3d4b1f8e...",
"message": "Content already ingested, skipping"
}
Errors:
400— MissingX-Filenameheader, unsupported file extension, empty body, or content that fails validation (SBOM uploads must be a single well-formed JSON object; VEX uploads must be a valid OpenVEX document — see below)403—AUTH_ENABLEDis nottrueon this instance413— Body exceedsMAX_UPLOAD_SIZE_MB500— Storage (S3 or local) or ingestion-queue failure503— No push-storage backend is configured (noskipScanS3 bucket, andSBOM_DIRisn’t writable)
VEX uploads are validated with the same OpenVEX parser the worker uses (a @context or at least one statements entry must be present) before anything is written to disk or enqueued. SBOM uploads must be a single, well-formed JSON object — bare scalars, null, top-level arrays, concatenated documents, and trailing data after the document are all rejected with 400. Beyond that the shape is not constrained: format detection (bomFormat/spdxVersion/in-toto predicateType) and full parsing (package/component extraction) still happen in the parsing worker, so an unrecognized-but-well-formed object is accepted here and resolved downstream.
Example:
curl -X POST http://localhost:8080/api/v1/sboms/upload \
-H "X-API-Key: <api-key>" \
-H "X-Filename: my-service.spdx.json" \
--data-binary @my-service.spdx.json
GET /api/v1/sboms
Paginated list of all ingested SBOMs.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | uint64 | 1 | Page number |
page_size | uint64 | 50 | Items per page (max 500) |
search | string | — | Filter by document name or source file (case-insensitive) |
Response: 200 OK
{
"data": [
{
"sbom_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source_file": "containerd-v1.7.2.spdx.json",
"spdx_version": "SPDX-2.3",
"document_name": "containerd-v1.7.2",
"package_count": 245,
"vuln_count": 12,
"ingested_at": "2026-05-20T14:30:00Z"
}
],
"total": 142,
"page": 1,
"page_size": 50
}
GET /api/v1/sboms/{id}/detail
Detailed SBOM information including vulnerability severity breakdown.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | SBOM identifier |
Response: 200 OK
{
"sbom_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source_file": "containerd-v1.7.2.spdx.json",
"spdx_version": "SPDX-2.3",
"document_name": "containerd-v1.7.2",
"package_count": 245,
"vuln_count": 12,
"ingested_at": "2026-05-20T14:30:00Z",
"critical_vulns": 1,
"high_vulns": 3,
"medium_vulns": 6,
"low_vulns": 2
}
Errors:
400— Invalid UUID format
GET /api/v1/sboms/{id}/download
Download the original SBOM JSON file. Streams the file from S3 or local filesystem depending on ingestion source.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | SBOM identifier |
Response: 200 OK — Binary stream with Content-Disposition: attachment
| Header | Value |
|---|---|
Content-Type | application/json |
Content-Disposition | attachment; filename="<original-filename>" |
Errors:
400— Invalid UUID format404— SBOM not found or source file no longer available in storage503— S3 not configured (source is S3 but API Gateway has no S3 credentials)
Example:
curl -O -J \
http://localhost:8080/api/v1/sboms/a1b2c3d4-e5f6-7890-abcd-ef1234567890/download
GET /api/v1/sboms/{id}/vulnerabilities
All vulnerabilities found in a specific SBOM, including VEX status.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | SBOM identifier |
Response: 200 OK
[
{
"vuln_id": "GHSA-xyz-abc-123",
"severity": "HIGH",
"purl": "pkg:golang/golang.org/x/net@v0.17.0",
"summary": "HTTP/2 rapid reset vulnerability",
"fixed_version": "v0.23.0",
"source_file": "containerd-v1.7.2.spdx.json",
"discovered_at": "2026-05-18T09:00:00Z",
"vex_status": "not_affected"
}
]
GET /api/v1/sboms/{id}/licenses
License breakdown for a specific SBOM, grouped by license ID with package lists.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | SBOM identifier |
Response: 200 OK
[
{
"license_id": "Apache-2.0",
"category": "permissive",
"package_count": 120,
"packages": ["github.com/containerd/containerd", "..."],
"exempted_packages": [],
"exemption_reason": ""
},
{
"license_id": "GPL-2.0-only",
"category": "copyleft",
"package_count": 2,
"packages": ["github.com/some/gpl-lib"],
"exempted_packages": ["github.com/some/gpl-lib"],
"exemption_reason": "System library, not linked"
}
]
GET /api/v1/sboms/{id}/dependencies
Dependency tree reconstructed as a flat array with parent→child index references.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | SBOM identifier |
Response: 200 OK
[
{
"index": 0,
"spdx_id": "SPDXRef-Package-containerd",
"name": "containerd",
"version": "1.7.2",
"purl": "pkg:golang/github.com/containerd/containerd@v1.7.2",
"license": "Apache-2.0",
"children": [1, 2, 3]
},
{
"index": 1,
"spdx_id": "SPDXRef-Package-runc",
"name": "runc",
"version": "1.1.12",
"purl": "pkg:golang/github.com/opencontainers/runc@v1.1.12",
"license": "Apache-2.0",
"children": [4, 5]
}
]
The UI reconstructs the tree by following children indices. Root nodes are those not referenced as children by any other node.
Vulnerabilities
GET /api/v1/vulnerabilities
Paginated list of all discovered vulnerabilities across all SBOMs.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | uint64 | 1 | Page number |
page_size | uint64 | 50 | Items per page (max 500) |
vex_filter | string | — | Filter mode: effective = exclude VEX-suppressed vulns |
Response: 200 OK — PaginatedResponse<VulnerabilityListItem>
{
"data": [
{
"vuln_id": "CVE-2024-45338",
"severity": "CRITICAL",
"purl": "pkg:golang/golang.org/x/net@v0.21.0",
"summary": "Denial of service in net/http",
"fixed_version": "v0.23.0",
"source_file": "etcd-v3.5.12.spdx.json",
"discovered_at": "2026-05-15T00:00:00Z",
"vex_status": ""
}
],
"total": 892,
"page": 1,
"page_size": 50
}
GET /api/v1/vulnerabilities/{id}/affected-projects
All projects affected by a specific CVE, including transitive dependency information.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Vulnerability ID (CVE, GHSA, GO-, PYSEC-, etc.) |
Response: 200 OK
[
{
"sbom_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"source_file": "etcd-v3.5.12.spdx.json",
"document_name": "etcd-v3.5.12",
"purl": "pkg:golang/golang.org/x/net@v0.21.0",
"package_name": "golang.org/x/net",
"version": "v0.21.0",
"severity": "CRITICAL",
"vex_status": "",
"is_direct": false
}
]
Errors:
400— Invalid vulnerability ID format
Licenses
GET /api/v1/licenses/compliance
Aggregated license compliance overview across all projects, with exemption details.
Response: 200 OK
[
{
"license_id": "GPL-3.0-only",
"category": "copyleft",
"package_count": 5,
"sbom_count": 3,
"non_compliant_packages": ["github.com/some/gpl3-lib"],
"exempted_packages": ["github.com/some/gpl3-lib"],
"exemption_reason": "Build tool only, not distributed",
"affected_sboms": [
{
"sbom_id": "a1b2c3d4-...",
"document_name": "my-project-v1.0"
}
]
}
]
GET /api/v1/projects
Paginated list of projects grouped by source path or document name. Each project aggregates its SBOMs (versions), total packages, and vulnerability counts.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
page_size | integer | 50 | Items per page (max 500) |
search | string | — | Filter projects by name (ILIKE) |
Response: 200 OK
{
"data": [
{
"project_name": "cncf-project-sboms/containerd",
"sbom_count": 12,
"package_count": 1847,
"vuln_count": 23,
"latest_ingested": "2026-05-28T14:30:00Z",
"latest_sbom_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
],
"total": 142,
"page": 1,
"page_size": 50
}
GET /api/v1/projects/license-compliance
Projects with copyleft or unknown license packages (filtered by active exceptions).
Response: 200 OK
[
{
"sbom_id": "a1b2c3d4-...",
"source_file": "my-project.spdx.json",
"document_name": "my-project-v2.1",
"copyleft_count": 3,
"unknown_count": 1,
"violating_licenses": ["LGPL-2.1-only", "UNKNOWN"],
"non_compliant_packages": ["github.com/some/lgpl-lib", "github.com/unknown/pkg"]
}
]
GET /api/v1/license-exceptions
Active license exceptions (read-only, loaded from config file).
Response: 200 OK
{
"version": "1.0.0",
"blanket_exceptions": [
{
"license": "ISC",
"reason": "ISC is functionally equivalent to MIT"
}
],
"exceptions": [
{
"purl_prefix": "pkg:golang/github.com/some/gpl-lib",
"license": "GPL-2.0-only",
"reason": "Build tool only, not distributed with binary"
}
]
}
GET /api/v1/license-policy
Active license classification policy (permissive vs. copyleft lists).
Response: 200 OK
{
"permissive": [
"MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unlicense", "0BSD"
],
"copyleft": [
"GPL-2.0-only", "GPL-3.0-only", "LGPL-2.1-only", "AGPL-3.0-only", "MPL-2.0"
]
}
VEX (Vulnerability Exploitability eXchange)
GET /api/v1/vex/statements
Paginated list of all ingested VEX statements with affected SBOM cross-references.
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | uint64 | 1 | Page number |
page_size | uint64 | 50 | Items per page (max 500) |
Response: 200 OK — PaginatedResponse<VEXStatementItem>
{
"data": [
{
"vex_id": "https://example.com/vex/2024-001",
"document_id": "https://openvex.dev/docs/example/v1",
"source_file": "golang-common.openvex.json",
"product_purl": "pkg:golang/golang.org/x/net",
"vuln_id": "CVE-2024-45338",
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"impact_statement": "The vulnerable HTTP/2 code path is not used",
"action_statement": "",
"vex_timestamp": "2024-12-01T00:00:00Z",
"ingested_at": "2026-05-20T14:30:00Z",
"affected_sboms": [
{
"sbom_id": "a1b2c3d4-...",
"document_name": "etcd-v3.5.12"
}
]
}
],
"total": 42,
"page": 1,
"page_size": 50
}
Packages
GET /api/v1/packages/search
Search packages by name across all ingested SBOMs.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | ✅ | Search query (case-insensitive substring match) |
page | uint64 | — | Page number (default 1) |
page_size | uint64 | — | Items per page (default 50, max 500) |
Response: 200 OK
{
"total_results": 15,
"items": [
{
"package_name": "golang.org/x/crypto",
"purl": "pkg:golang/golang.org/x/crypto",
"project_count": 87,
"versions": ["v0.21.0", "v0.22.0", "v0.23.0"],
"projects": [
{
"project_name": "etcd-v3.5.12",
"version": "v0.21.0",
"sbom_id": "a1b2c3d4-..."
}
]
}
],
"page": 1,
"page_size": 50,
"query": "crypto"
}
Errors:
400— Missing required query parameterq
GET /api/v1/packages/detail
Detailed information about a specific package, listing all projects that use it.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Exact package name |
page | uint64 | — | Page number (default 1) |
page_size | uint64 | — | Items per page (default 50, max 500) |
Response: 200 OK
{
"package_name": "golang.org/x/crypto",
"total_projects": 87,
"projects": [
{
"project_name": "etcd-v3.5.12",
"version": "v0.21.0",
"sbom_id": "a1b2c3d4-..."
}
],
"page": 1,
"page_size": 50
}
Errors:
400— Missing required query parametername
Global Search
GET /api/v1/search
Faceted search across packages, projects, vulnerabilities, and licenses in a single request. Powers the navbar typeahead and the /search results page in the UI.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | ✅ | Search query, minimum 2 characters (case-insensitive substring match) |
limit | uint64 | — | Max results per facet (default 5, max 50) |
Vulnerabilities match on both the vulnerability ID and the summary text. Projects match on the derived project key (org/repo for S3 sources, document name otherwise).
Response: 200 OK
{
"query": "grpc",
"packages": [
{
"package_name": "google.golang.org/grpc",
"purl": "pkg:golang/google.golang.org/grpc@v1.60.0",
"project_count": 87
}
],
"total_packages": 3,
"projects": [
{
"project_name": "grpc/grpc-go",
"sbom_count": 4,
"latest_sbom_id": "a1b2c3d4-..."
}
],
"total_projects": 1,
"vulnerabilities": [
{
"vuln_id": "CVE-2024-9999",
"severity": "HIGH",
"summary": "gRPC denial of service",
"affected_sboms": 7
}
],
"total_vulnerabilities": 1,
"licenses": [
{
"license_id": "Apache-2.0",
"category": "permissive",
"sbom_count": 100
}
],
"total_licenses": 1
}
Facet arrays are always present (empty when no matches). Each facet returns at most limit items; the total_* fields report the full match counts.
Errors:
400— Query parameterqmissing or shorter than 2 characters
GET /api/v1/packages/archived
Packages from archived/unmaintained GitHub repositories (supply chain risk indicator).
Response: 200 OK
[
{
"package_name": "github.com/abandoned/lib",
"purl": "pkg:golang/github.com/abandoned/lib",
"archived_since": "2023-06-15",
"project_count": 4,
"projects": ["etcd-v3.5.12", "containerd-v1.7.2"]
}
]
Clusters
GET /api/v1/clusters
List all known clusters with summary statistics. Returns an array of clusters sorted by SBOM count (descending).
Response: 200 OK
[
{
"name": "production",
"sbom_count": 85,
"package_count": 12400,
"vuln_count": 423,
"last_ingested": "2026-06-01T14:30:00Z"
},
{
"name": "staging",
"sbom_count": 42,
"package_count": 6100,
"vuln_count": 198,
"last_ingested": "2026-06-01T12:00:00Z"
}
]
Clusters with name: "" represent SBOMs ingested before multi-cluster support was enabled (migration 012). They appear as an empty string in the listing.
GET /api/v1/clusters/{name}/stats
Per-cluster dashboard statistics including severity breakdown and license distribution.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Cluster name (max 200 chars) |
Response: 200 OK
{
"cluster": "production",
"total_sboms": 85,
"total_packages": 12400,
"total_vulnerabilities": 423,
"critical_vulns": 12,
"high_vulns": 89,
"medium_vulns": 210,
"low_vulns": 112,
"license_breakdown": {
"permissive": 9800,
"copyleft": 340,
"unknown": 2260
},
"last_ingested": "2026-06-01T14:30:00Z"
}
Error Responses:
400 Bad Request— cluster name empty or exceeds 200 characters404 Not Found— no SBOMs exist for this cluster
GET /api/v1/clusters/{name}/sboms
Paginated list of SBOMs for a specific cluster.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Cluster name (max 200 chars) |
Query Parameters: Standard pagination (page, page_size)
Response: 200 OK — Standard PaginatedResponse[SBOMListItem]
{
"data": [
{
"sbom_id": "550e8400-e29b-41d4-a716-446655440000",
"source_file": "s3://cncf-project-sboms/containerd/v1.7.spdx.json",
"spdx_version": "SPDX-2.3",
"document_name": "containerd-v1.7",
"package_count": 245,
"vuln_count": 12,
"ingested_at": "2026-06-01T14:30:00Z"
}
],
"total": 85,
"page": 1,
"page_size": 50
}
Security Headers
All responses include:
| Header | Value |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Content-Security-Policy | default-src 'none'; frame-ancestors 'none' |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
CORS
Configured via CORS_ALLOWED_ORIGINS environment variable.
- Default:
*(development) - Production: Set to your frontend domain(s)
- Allowed methods:
GET,OPTIONSon every endpoint;POSTis additionally advertised only on/api/v1/sboms/upload - Allowed headers:
Content-Type,Authorization,X-Service-Token,X-API-Key,X-Filename
Headless Mode
When deployed with ui.enabled: false in Helm values, BOMHort runs as a pure API service:
- All endpoints above remain available
- No UI container is deployed
- No Nginx, no static file serving
- Ideal for CI/CD integrations, custom dashboards, or aggregation by external tools
# values-headless.yaml
ui:
enabled: false
apiGateway:
ingress:
enabled: true
hosts:
- sbom-api.internal.example.com
Usage Examples
curl
# Get dashboard overview
curl -s http://localhost:8080/api/v1/stats/dashboard | jq .
# Search SBOMs
curl -s "http://localhost:8080/api/v1/sboms?search=containerd&page_size=10" | jq .
# Get vulnerabilities (effective only, VEX-filtered)
curl -s "http://localhost:8080/api/v1/vulnerabilities?vex_filter=effective&page_size=100" | jq .
# Check which projects are affected by a CVE
curl -s http://localhost:8080/api/v1/vulnerabilities/CVE-2024-45338/affected-projects | jq .
# Search packages
curl -s "http://localhost:8080/api/v1/packages/search?q=crypto" | jq .
CI/CD Integration (GitHub Actions)
- name: Check for critical vulnerabilities
run: |
CRITICAL=$(curl -sf "$BOMHORT_URL/api/v1/stats/dashboard" | jq '.critical_vulns')
if [ "$CRITICAL" -gt 0 ]; then
echo "::error::$CRITICAL critical vulnerabilities detected"
exit 1
fi