Drop a release-level SPDX document plus its component SBOMs and navigate the whole supply chain as one tree: release → component → sub-component → container image → package. Everything runs in your browser; files never leave your machine.
Try it: https://sbom-lens.everbright-it.de/app/. The project landing page lives at https://sbom-lens.everbright-it.de/. The VS Code extension is on Open VSX.
Two products ship from this monorepo. This page is about SBOM Lens, the SPDX viewer. Its sibling OCM Lens reads Open Component Model deliveries (CTF and component archives, signature verification included) and has its own product home: repository · landing · Open VSX. Issues for both products live in this repo's tracker.
Regenerate with npm run screenshot -w @sbomlens/web (dev server running).
- Cascading documents are first-class. SPDX 2.3 links documents via
ExternalDocumentRefand cross-document relationships (DocumentRef-X:SPDXRef-Y). SBOM Lens resolves those references across every file you load, by checksum first and then namespace, and renders one continuous, lazily-expanded tree across document boundaries. Unresolved references appear as actionable placeholders: fetch them by URL, drop the file, or confirm a suggested match. - Fast at real-world scale. Multi-megabyte documents (6,500+ packages) parse in a Web Worker; the tree and source views are virtualized; search runs against a prebuilt index with ranked results. No pagination, no jank.
- Private by design. A static, client-only app. SBOMs are parsed locally and never uploaded. URL fetching only happens when you explicitly ask for it.
- Honest about dirty data. Real SBOMs have quirks: checksum spacing variants, duplicate SPDXIDs, references without relationships, unknown relationship types, versions hiding in purls. The parser tolerates all of it and reports what it found as per-document diagnostics instead of refusing to load.
- Answers questions, not just files. Beyond browsing: an exportable cross-cascade inventory, version-conflict detection, release-to-release diffs, and a per-document NTIA quality report.
git clone https://gitlab.com/everbrightit-group/sbom-lens.git
cd sbom-lens
npm ci
npm run dev # → http://localhost:5173Click Load example for a bundled four-document demo cascade, or drop your own
.spdx / .spdx.json files (multi-select and whole folders work).
| Method | Notes |
|---|---|
| Drag & drop | Anywhere in the window; folders are walked recursively |
| Open ▸ Files / Folder | Standard pickers |
| Open ▸ From URL | Fetches a document over HTTP(S), e.g. from a GitLab generic package registry |
?url= deep link |
https://sbom-lens.everbright-it.de/?url=<sbom-url> opens a document straight from the address bar. Repeatable, so a cascade travels in one link |
| Placeholder ▸ Fetch | Each unresolved reference offers a one-click fetch of its recorded URL |
| Fetch all (status bar) | Downloads every referenced document recursively until the cascade is complete. One click for the full tree instead of one per placeholder |
Access tokens: for private registries, add a per-host token in the URL dialog
(GitLab PRIVATE-TOKEN or Authorization: Bearer). Tokens live in
sessionStorage only: they die with the tab and are never persisted.
Deep links make an SBOM linkable: a README, a release note or a registry page can point at "this document, rendered" instead of "download this file, then find a viewer". Repeat the parameter for a whole cascade:
https://sbom-lens.everbright-it.de/?url=<release.spdx.json>&url=<component.spdx.json>
A deep link is input from whoever sent it, so three rules apply, and they are pinned by tests rather than left to review:
- http(s) only, absolute.
javascript:,data:,file:and relative URLs are rejected; a link cannot reach a non-fetch scheme, nor probe the deployment that serves the viewer. - Eight documents per link. Enough for a cascade, not enough to be a way of pointing someone else's browser at a server.
- Stored tokens are never attached. A link someone sent must not spend the recipient's credentials. If the source answers 401/403, the URL dialog opens prefilled instead, so attaching a token stays a deliberate act.
What a deep link deliberately does not do is filter by target address.
http://localhost:8080/sbom.json is a legitimate link during development, so
private ranges stay allowed. The consequence is understood: a crafted link can
make the recipient's browser issue one GET from inside their network. The
browser's same-origin policy keeps the response unreadable, nothing reaches the
attacker and nothing reaches us — and any web page can already do the same with
an image tag. Blocking the ranges would cost the local case and buy no real
protection.
A URL in the address bar ends up in browser history and, on a deployment with analytics, in its page-view record. Do not put a pre-signed or token-carrying URL into a deep link you share.
CORS: the browser can only fetch URLs whose server allows cross-origin requests. When it doesn't, SBOM Lens says so plainly. Download the file and drop it in instead, or self-host the viewer behind the same reverse proxy as your registry so requests are same-origin.
For every ExternalDocumentRef of every loaded document, in order of precedence:
- Checksum: the reference's SHA-1 matches a loaded file's bytes. The strongest signal, and the only one that works when reference URIs are download URLs rather than namespaces.
- Namespace: the reference URI equals a loaded document's
documentNamespace(the spec-blessed path). - Manual: you bind a file to the reference yourself.
Name similarity ("looks like acme-auth-service") is only ever shown as a
one-click suggestion, never auto-bound, because DocumentRef names drift from
actual file versions in the wild.
References that no relationship points into (scan reports, attestations, release notes) are classified as informational: they're listed under External documents without nagging you to resolve them.
| View | What it answers |
|---|---|
| Explore | "What does this release contain?" The cascading tree, detail pane, and raw source. Shift+click a chevron (or press *) to expand an entire subtree including resolved sub-SBOMs; the funnel next to the search box filters the tree in place, leaving matches plus their ancestors and hiding everything else |
| Map | "How is this cascade wired?" The document topology as a collapsible left-to-right tree: documents as nodes, resolved references as method-styled edges, missing documents as dashed stubs. Nodes fold their subtree behind a +N badge (large workspaces start folded), search force-reveals matches, pan/zoom, click selects, double-click jumps into Explore |
| Inventory | "Give me the parts list as a file." One sortable table across all documents, filtered by the same search + facet chips (documents, kinds, purposes, licenses), exportable as CSV/JSON |
| Conflicts | "Which packages ship in more than one version?" Grouped by purl identity across the whole cascade, each occurrence one click from its place in the tree |
| Diff | "What changed between these two releases?" Added, removed and version-changed packages between two cascades (each side is a document plus everything reachable through its resolved references), copyable as Markdown for release notes |
Each document's detail pane additionally shows a quality report oriented on
the NTIA minimum elements: author/timestamp/namespace/relationship checks, plus
per-package coverage of versions, suppliers, unique IDs, checksums, and licenses.
Factual numbers, no invented score. Organizations can go further with
custom compliance profiles: a small JSON file with your own minimum
elements (field presence, patterns, coverage thresholds, recency) that imports
per drag&drop, via the deployment catalog, or from .sbomlens/profile.json
in a VS Code workspace. Reports export as Markdown. See
docs/compliance-profiles.md.
Drop an OpenVEX document next to your SBOMs and the viewer shows what the supplier communicates about known vulnerabilities: per-package statements with status, justification, and action, a VEX column + status filter in the Inventory, and findings riding the exports. Matched by package URL, newest statement wins; it is a communication channel, not a scanner. See docs/vex.md.
Documents are also checked against their own specification. Spec findings report what a consumer downstream would trip over: a relationship type outside the vocabulary, a digest that cannot be a digest, a license expression that does not parse. They cover SPDX 2.x, SPDX 3.0.x, CycloneDX 1.x and OCM descriptors. They stay warnings and sit next to the document, separated from parser notes so the two are never confused. This is a reading aid, not a conformance verdict: for authoritative validation use spdx/tools-java. See docs/spec-findings.md.
| Key | Action |
|---|---|
/ |
Focus search |
↑ ↓ |
Move selection in tree / results |
→ |
Expand node, then first child |
← |
Collapse node, then parent |
* |
Expand entire subtree (also: Shift+click a chevron) |
Enter |
Toggle node / open search result |
Esc |
Clear search, close panels |
? |
Shortcut help |
- SPDX 2.x tag-value (
.spdx), JSON, and YAML: fully supported. Detection is content-based, never by file extension. - SPDX 3.0.x JSON-LD: loads. Packages, files, relationships, hashes, external identifiers (purl, CPE), license relationships, and cross-document imports (ExternalMap) map onto the same views as 2.x: import entries become external document references that resolve through the same cascade machinery, so a 3.x document referencing elements of another 3.x document links up the moment both are loaded. Elements from profiles outside core/software (AI, dataset, build) are counted in a notice rather than shown. Tag-value has no 3.x serialization; other 3.x serializations are not parsed.
- CycloneDX 1.x as JSON: components (including nested assemblies),
dependencies, purl and CPE identity, hashes, licenses, and BOM-Links —
a reference of type
bom(urn:cdx:...) resolves across loaded BOMs through the same cascade machinery. Read-only viewing, no conversion. - Trivy-native JSON: recognized with a pointer to the right conversion
(
trivy --format spdx-json).
The detail views carry the spec with them: hover the info icon next to a field
to read the specification's own documentation for it. SPDX 2.x documents get
the 2.3 texts, distilled at build time from the official JSON schema
(npm run generate:spec-docs); SPDX 3.x documents get a hand-curated 3.0.1
set that speaks the 3.x vocabulary (packageVersion, suppliedBy,
verifiedUsing, ...) and links into the 3.0.1 model pages. Click the icon to
open that field's section in the rendered specification.
Known boundaries, stated plainly so nothing surprises you:
- Format scope. SPDX 2.x in full; SPDX 3.0.x as JSON-LD with the core/software profiles mapped (other profiles are counted, not rendered); CycloneDX 1.x as JSON (services, compositions, embedded VEX data, and XML are out of scope; a YAML-serialized BOM is read tolerantly but is not a CycloneDX serialization). Trivy-native JSON is recognized with a conversion hint, not parsed. Detection is content-based.
- HTTPS or localhost required. Cascade resolution hashes file bytes with
crypto.subtle, which browsers expose only in secure contexts. Over plain HTTP on a non-localhost host, hashing (and therefore checksum-based reference resolution) does not run. - URL loading needs CORS. The browser can only fetch a document whose server allows cross-origin requests. When it can't, SBOM Lens says so; download the file and drop it in, or self-host behind the same origin as your registry (see below).
- Size. No hard cap on a single SPDX document: multi-megabyte files with thousands of packages parse in a Web Worker. In the VS Code extension, the workspace scan skips individual files over 50 MB (open those by hand). Expanding an entire subtree stops at 2,000 nodes with a notice, and the tree walks to a depth of 64.
- Compliance profiles. A profile file is capped at 64 KB and 200 checks; up to 16 imported profiles persist (256 KB total), beyond which they stay for the session only.
- Private, and it stays that way. No upload path, no telemetry. Only preferences and imported profiles are persisted (locally); loaded documents are not. Deep links therefore need addressable sources (a catalog entry or a URL-loaded document), not dropped files.
- Not in scope by design. No license-compliance judgement (license fields are shown, not interpreted) and no vulnerability or VEX overlays in the core model.
SBOM Lens builds to a fully static site (apps/web/dist/) that any web server
can host.
A minimal nginx image (~25 MB) is included:
docker build -f deploy/Dockerfile -t sbomlens .
docker run --rm -p 8080:80 sbomlens
# → http://localhost:8080The bundled nginx config ships hardened security headers (CSP,
nosniff, frame-ancestors 'none') by default. See
deploy/nginx.conf for what the CSP allows and why.
Serve the app over HTTPS (or localhost): the SHA-1 hashing that drives
cascade resolution uses crypto.subtle, which browsers only expose in
secure contexts. When proxying private registries through the same origin,
scope the server-side token read-only and restrict who can reach the proxy
(notes in the config).
The build uses relative asset paths, so it works at any base path, including GitLab or GitHub Pages subpaths. The app is a PWA: once visited, it keeps working offline (including the bundled examples).
A self-hosted instance can ship a curated list of SBOMs so users just open the
viewer and analyze, without hunting for files. Place a sbomlens.catalog.json
next to index.html:
{
"title": "ACME releases",
"sources": [
{
"label": "Platform 1.0 (current release)",
"description": "Release SBOM plus component SBOMs",
"urls": ["sboms/1.0/platform.spdx"],
"loadOnStart": false,
"resolveRefs": true
}
]
}Entries appear on the start screen and in the Open menu; loadOnStart
sources load automatically. With resolveRefs: true you only list the root
document. After it loads, every referenced SBOM is fetched recursively, so one
click gives users the complete tree for analysis. The catalog is only ever read
from this fixed same-origin path (never from a URL parameter), and only
http(s)/relative URLs are accepted.
Reaching private registries (GitLab etc.): browsers block cross-origin
requests unless the server sends CORS headers, and GitLab's API does not. The
robust pattern is a same-origin reverse proxy: the bundled
deploy/nginx.conf contains a commented sample that proxies
/sboms/... to a GitLab generic-package registry and injects a read-only
token server-side. Users need no tokens, nothing is cross-origin, and no secret
ever appears in the catalog file. Never put tokens into
sbomlens.catalog.json. Direct absolute URLs also work where the server allows
CORS; authentication then uses the per-host session tokens in the URL dialog.
npm run dev # dev server
npm test # unit tests (Vitest)
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm run build # typecheck + production buildReleases bump every workspace in lockstep:
npm version 0.X.0 --workspaces --include-workspace-root --no-git-tag-version
git commit -am "release: v0.X.0" && git tag -a v0.X.0 -m "SBOM Lens 0.X.0"The tag pipeline turns the tag into a GitLab release automatically: the notes are that version's CHANGELOG section, the assets are both vsix files and the self-SBOM (served from the package registry, so the links do not expire). Pushing the same tag in the OCM Lens product home creates the matching release there; push it after this repo's tag pipeline finished, so its vsix asset link finds the published package.
Supply-chain hygiene: every push runs an osv-scanner CVE gate, SAST, and
secret detection; releases additionally get a Trivy image scan and ship
their own SPDX SBOM (sbomlens-<tag>.spdx.json, which opens in SBOM Lens).
Dependency updates arrive as Renovate MRs. Details: docs/ci-security.md.
The repository is an npm workspace, layered deliberately:
packages/core/ @sbomlens/core, the framework-free domain: parsers
(tag-value/JSON/YAML), workspace, reference resolution,
graph indexes, tree derivation, search, analysis
(inventory/conflicts/diff/quality), generated spec docs.
Zero React imports, enforced by ESLint.
apps/web/src/worker/ a thin Web Worker shell around core parsing (hashing +
parsing off the UI thread; yaml loads only here).
apps/web/src/app/ zustand store, ingest pipeline (files / folders / URLs),
deployment catalog, memoized selectors.
apps/web/src/ui/ React components: virtualized tree + document map,
detail pane, analysis views, search, diagnostics.
apps/web/src/host/ HostAdapter seam: browser host (fetch, web storage,
module workers) and VS Code webview host (postMessage
bridge, blob workers, editor secret storage).
apps/vscode/ the VS Code extension: custom editor + workspace scan
around the same webview bundle (see its README).
The repository also builds a sibling product from this codebase: OCM Lens,
a viewer for Open Component Model component versions and deliveries
(docs/ocm.md; product home
gitlab.com/everbrightit-group/ocm-lens,
live at ocm-lens.everbright-it.de).
It is a separate concern, and the split is
structural, not cosmetic: descriptor mapping, the tar reader, and gzip live
behind @sbomlens/core/ocm, only OCM Lens wires them in, and a CI gate fails
the build if a byte of that code reaches the SBOM Lens bundle. SBOM Lens is an
SPDX viewer: it recognizes a component descriptor only well enough to tell
you it isn't an SBOM.
packages/core/fixtures/ contains synthetic documents reproducing every
real-world quirk the parser supports;
apps/web/scripts/generate-examples.mjs regenerates the demo cascade. To
validate against a private SBOM collection without committing it:
SBOM_CORPUS_DIR=~/my-sboms npm run check-corpus.
Honest positioning, not a feature race:
| If you need... | Use |
|---|---|
| Generate SBOMs from code or images | syft, trivy, cdxgen — SBOM Lens reads what they produce |
| A server platform with CVE tracking over time | OWASP Dependency-Track |
| Read, link, and verify multi-document SPDX cascades locally | SBOM Lens — no server, no upload, cascade resolution, compliance profiles, VEX overlay |
| Quality scoring in CI | sbomqs; SBOM Lens complements it with the human-readable view |
SBOM Lens deliberately does not scan for CVEs, rate licenses legally, or host anything. It is the reading half of the SBOM workflow.
- SPDX 3.x, deeper: serializations beyond JSON-LD. Curated 3.0.1 field tooltips and external document maps (imports) landed in v0.20.0; loading 3.0.x JSON works since v0.15.0 (see Supported formats).
- Chromium extension ("Open in SBOM Lens" for raw SBOMs in the browser): a thin shell around the same codebase, like the VS Code extension that now lives in apps/vscode ("Open with SBOM Lens", workspace scanning; published on Open VSX). Architecture: docs/extension-architecture.md.
- Workspace persistence (File System Access API), shareable deep links (deep links require addressable sources: catalog or URL-loaded documents)
- CSAF 2.0 as the next vulnerability-communication format (the OpenVEX overlay landed in v0.19.0, kept out of the core model as designed)
Development happens on EverBright's GitLab; changes are mirrored to the public repositories:
- GitLab (canonical public repo): https://gitlab.com/everbrightit-group/sbom-lens
- GitHub mirror: https://github.com/EverBright-IT/SBOM-Lens
Issues and contributions are welcome on either platform; maintainers sync them into the primary repository.
SBOM Lens, OCM Lens, and AIBOM Lens are open-source projects by EverBright IT GmbH (Munich). We help teams with CRA/SBOM readiness and secure AI adoption — get in touch.
Apache-2.0 © EverBright IT GmbH. Maintained by EverBright IT GmbH. Field documentation shown in the UI is derived from the SPDX specification (CC-BY-3.0, © The Linux Foundation and SPDX contributors).

