A zero-install viewer for CycloneDX and SPDX SBOM files. Drop a bom.json, get a clean searchable view of your dependencies, hand it to legal as a CSV or PDF.
Looking for the GitHub Action? It turns an SBOM into a single self-contained HTML report you can attach to a release — jump to Release reports (CI).
Privacy: every byte stays in your browser. No upload, no phone-home, no telemetry. The page works with the network cable unplugged.
The fastest way to run blitsbom in your own environment.
# Grab the release artifact + checksum
mkdir blitsbom && cd blitsbom
curl -fLO https://github.com/no42-org/blitsbom/releases/download/v0.8.0/blitsbom-0.8.0.zip
curl -fLO https://github.com/no42-org/blitsbom/releases/download/v0.8.0/blitsbom-0.8.0.sha512
# Verify integrity (exits non-zero if the bundle was tampered with)
sha512sum -c blitsbom-0.8.0.sha512
Scripting it? gh resolves the current release itself, so nothing needs editing when a new one lands:
gh release download --repo no42-org/blitsbom --pattern 'blitsbom-*.zip' --pattern 'blitsbom-*.sha512'
sha512sum -c blitsbom-*.sha512
unzip blitsbom-*.zip
open index.html # macOS
xdg-open index.html # Linux
It runs straight from a file:// URL with no server. The bundle includes everything it needs — no CDNs, no fetched fonts, no external resources.
# Stable release
docker run --rm -p 8080:3000 ghcr.io/no42-org/blitsbom:latest
# A specific version
docker run --rm -p 8080:3000 ghcr.io/no42-org/blitsbom:0.8.0
# Bleeding-edge build from main (release-candidate)
docker run --rm -p 8080:3000 ghcr.io/no42-org/blitsbom:rcThen open http://localhost:8080 and drop your bom.json / sbom.json onto the page.
make docker-build # build the local image
make docker-run # serve it on http://localhost:8080Equivalent without Make:
docker build -t blitsbom:latest .
docker run --rm -p 8080:3000 blitsbom:latestTurn an SBOM into a single self-contained HTML file you can attach to a release or hand to an auditor, supplier, or vendor for a quick license review. It is the full blitsbom app with the SBOM already loaded, plus a provenance header identifying the release — and it works offline, from a file:// URL, with no server and no network.
- uses: no42-org/blitsbom@v0.8.0
id: sbom-report
with:
sbom: bom.json # path to your CycloneDX or SPDX SBOM
# image defaults to :report-0.8.0, matching the action tag
# version/project/commit/build-url default from the workflow context
- uses: actions/upload-artifact@v4
with:
name: sbom-report
path: ${{ steps.sbom-report.outputs.report }}Pin the action however your policy requires — image defaults to the generator matching whatever the action itself came from, so the two cannot drift:
| Action ref | Generator image |
|---|---|
@v0.8.0 — a release tag |
:report-0.8.0 |
| a commit SHA, e.g. Dependabot's pin | :report-<that commit's version> |
@main or another branch |
:report-rc, the generator built from main — the matching pair for a ref that tracks main |
| anything else, or a version that cannot be read | :report-rc |
When it derives the image, the action logs which one and why; passing image explicitly skips that. Set image to override — to pin a digest, or to run a branch ref against a released generator.
Two limits worth knowing. A SHA pinned to a commit between releases resolves that commit's package.json version, which is the last release — so the generator is a little older than the action. And a fork's SHA resolves against ghcr.io/no42-org/blitsbom, not the fork's own registry; pass image if you publish your own.
Moved in v0.6.0. The action was
no42-org/blitsbom/report@v0.5.0and is now at the repository root,no42-org/blitsbom, so it can be listed on the GitHub Marketplace.Pins to
@v0.5.0or any earlier tag keep working — tags are immutable. What breaks is a ref that followsmain:no42-org/blitsbom/report@main, or a SHA pin updated to a commit after the move. Those need the/reportsuffix dropped.
# A specific release (reproducible)
docker run --rm -v "$PWD:/work" ghcr.io/no42-org/blitsbom:report-0.8.0 \
/work/bom.json --project acme-platform --version 2.4.1 \
--output /work/acme-platform-2.4.1-sbom.html
# Bleeding-edge build from main
# ghcr.io/no42-org/blitsbom:report-rcThe generator exits non-zero (and writes nothing) if the SBOM does not parse, so a broken SBOM fails the pipeline instead of shipping a report that greets the recipient with an error. It applies no policy judgment — no license or severity gate; that is deliberately left to tools like grype or osv-scanner.
A CycloneDX VEX can be merged in at generation time with --vex vex.json; the report then shows the vulnerability data, while the embedded SBOM stays byte-identical to your source.
The report offers Download original SBOM (the verbatim source, machine-readable) alongside Export CSV (for legal). The provenance header shows the sha256: digest of the source SBOM — a recipient confirms the report matches the SBOM they were given with:
sha256sum bom.json # compare against the digest shown in the report headerThe report embeds your SBOM verbatim — every component, internal registry URL, and publisher field goes with it. Remove anything that must not leave your organization before generating the report; blitsbom does not redact.
Reports over ~2 MB are gzip-compressed inside the file and need a browser with
DecompressionStream(Chrome/Edge 80+, Firefox 113+, Safari 16.4+ — all 2023 or earlier). Smaller reports embed the SBOM as raw JSON and open anywhere.
| Format | Versions | Status |
|---|---|---|
| CycloneDX JSON | 1.4 – 1.7 | Verified — schema-checked against the fields blitsbom reads, with fixtures from real generators |
| CycloneDX JSON | 1.8+ | Accepted — CycloneDX minors are additive, so newer ones load; the header marks them "newer than this build" until verified |
| SPDX JSON | 2.x (2.2 / 2.3) | Supported |
| CycloneDX 1.0 – 1.3 | — | Rejected with a clear error (open an issue if you need it) |
| CycloneDX 2.x | — | Rejected — a major version is a spec break, not a minor addition |
| CycloneDX XML | — | Not yet — open an issue |
| SPDX 3.x | — | Not yet — open an issue |
Format is auto-detected from the document's top-level keys (bomFormat: "CycloneDX" vs spdxVersion: "SPDX-2.x"); no format selector required.
SPDX 2.x is a closed line — 2.3 was the last, superseded by 3.0 — so unlike CycloneDX there is no "newer than this build" case to mark.
Licenses by Originator answers "whose terms am I accepting". How much of it is populated depends on your generator and on the ecosystems in your tree, and neither is something blitsbom can change: it reports what the document says.
Declared originator. blitsbom reads the first of originator, then supplier (SPDX), or publisher, supplier.name, manufacturer.name, authors[0].name / author, then group (CycloneDX). Whether any are present is the generator's business:
| Generator | Declares an originator? |
|---|---|
| syft, Java/jar scan | Only from the jar manifest's Implementation-Vendor. Netty, Apache, Eclipse and FasterXML set it; most projects do not |
| syft, npm lockfile scan | No. package-lock.json has no author field to read, so the field is absent for every component |
| syft, Go scan | No |
@cyclonedx/cyclonedx-npm |
Yes, from each installed package.json — roughly two thirds of a typical tree |
Derived from the package namespace. When nothing is declared, blitsbom falls back to the purl namespace, which is why most components are attributed even from syft output. (A CycloneDX component carrying group is attributed from that first, so it is covered even without a purl.) The namespace fallback works only where the ecosystem has a namespace:
| Ecosystem | Derived originator | Example |
|---|---|---|
| Maven | groupId | com.google.guava |
| Go | module path minus the package | github.com/gorilla |
| npm, scoped | the scope | @angular |
| Debian / RPM / Alpine | the distribution | debian |
| npm, unscoped | none — stays Unknown | left-pad has no namespace |
| PyPI, Cargo, OCI, generic | none — stays Unknown | these purls carry no namespace in practice |
The extraction is structural, not a per-ecosystem table: blitsbom returns whatever namespace the purl carries. The rows above describe what these ecosystems emit in practice, not a restriction blitsbom imposes.
So a Java or Go tree attributes almost completely, a scoped-npm tree partially, and a plain-npm or Python tree may stay largely Unknown no matter which generator produced it. That is the SBOM's limit, not the viewer's.
Two consequences worth knowing:
- A derived originator is a namespace, not an attestation.
com.github.ben-manes.caffeineis a namespace whose owner is a person. - One organisation can appear twice when it declares a vendor for some artifacts and not others —
FasterXMLalongsidecom.fasterxml.jackson.jr. Merging those would need a hardcoded vendor map, which would go stale; blitsbom does not attempt it.
The donut chart classifies each component's primary license into one of six categories, sourced from the Free Software Foundation's license list:
| Category | Color | Examples |
|---|---|---|
| Public Domain | light green | CC0-1.0, Unlicense, WTFPL |
| Permissive | green | Apache-2.0, MIT, BSD-2/3-Clause, ISC, Zlib |
| Copyleft | yellow | LGPL-2.1/3.0, MPL-2.0, EPL-2.0, CDDL-1.0 |
| Strong Copyleft | orange | GPL-2.0/3.0, AGPL-3.0 |
| Proprietary | red | unknown licenses, free-form names, SPDX expressions |
| Undeclared | grey | NOASSERTION, missing, empty |
Click a donut segment (or a legend row) to filter the table to that category, then drill down to individual licenses inside the category. Disputes about a placement are resolved by linking the FSF page for that license — see src/license/classify.ts for the source.
CycloneDX and SPDX both accept compound expressions like (MIT OR Apache-2.0) as license entries. In v1, blitsbom shows expressions verbatim and classifies them as Proprietary (since they aren't single SPDX ids and can't be reliably bucketed). Single-id licenses work as you'd expect.
SPDX documents can use LicenseRef-* identifiers backed by the document's hasExtractedLicensingInfos block. blitsbom resolves these by matching the actual extracted text against signature regexes for ~13 common licenses (Apache-2.0, MIT, BSD-*, GPL-*, LGPL-*, AGPL-3.0, MPL-2.0, EPL-*, CDDL-1.0, ISC), falling back to canonical license URLs in seeAlsos and to encoded ids in the LicenseRef name itself. Anything that can't be recognized stays as a verbatim name and classifies as Proprietary.
After loading an SBOM, you can optionally drop a CycloneDX VEX file (or any CycloneDX document with a populated vulnerabilities[] array) on top to overlay vulnerability data on the components. VEX is purely additive — the SBOM-only flow stays exactly the same when no VEX is loaded.
Generate a compatible VEX from any SBOM:
# Anchore Grype — input: any SBOM; output: CycloneDX with vulnerabilities[]
grype sbom:./bom.json -o cyclonedx-json > vex.json
# Aqua Trivy
trivy sbom ./bom.json --format cyclonedx --output vex.json
# Google OSV-Scanner
osv-scanner --sbom=./bom.json --format=cyclonedx > vex.jsonDrop the file via the Load vulnerabilities (VEX)… button next to the summary header. blitsbom joins each vulnerability to a component by canonical purl (with bom-ref as a fallback) and surfaces:
- A fifth summary tile with the live vulnerability count.
- A severity-coloured badge column in the components table — click any badge to drill down to per-component CVE detail.
- A severity facet in the filter bar (
critical / high / medium / low / unknown / none), URL-encoded as?severity=…. - Provenance info (VEX filename + timestamp) above the summary tiles, plus an "N unmatched" hint when a VEX entry's
affects[].refdoesn't resolve to any component.
VEX analysis.state is honored: entries marked not_affected, false_positive, or resolved are hidden by default. A "Show suppressed (N)" toggle reveals them when present.
Everything stays offline — no network call, no online lookup against OSV.dev or NVD. The existing purity-check build guard still enforces this.
- Drag-and-drop or pick a
bom.json/sbom.json(CycloneDX or SPDX) - Reading-progress indicator for large SBOMs (
Reading X.X MB / Y.Y MB→Parsing…) - Summary header: total components, distinct licenses, distinct types, vulnerability count
- License donut chart with FSF-classified categories (Permissive / Copyleft / Strong Copyleft / Public Domain / Proprietary / Undeclared)
- Click a donut segment → filter the table to that category, drill down to individual licenses inside it
- Sortable component table with name / version / license / scope / type / purl, paginated for large SBOMs (500 rows visible by default with show-more)
- Free-text search across name, version, license, scope, type, group, publisher, description, purl
- Click-to-toggle filter chips (category, license, scope, type, severity)
- Optional VEX overlay: drop a CycloneDX VEX file to attach CVE data to components, with severity badges, drilldown, and severity filter (see Vulnerabilities (VEX) above)
- Filter state encoded in the URL — copy the address to share a view (
?category=permissive&license=Apache-2.0&severity=high) - CSV export of the filtered view (RFC 4180, Excel-compatible)
make install # npm install
make dev # vite dev server
make build # build static dist/
make report SBOM=bom.json VERSION=2.4.1 OUT=report.html # generate a CI report
make sbom # regenerate the release SBOM of this tree (needs Docker)
make test # vitest
make verify # lint + tests + network-purity check
make size-check # fail if gzipped JS exceeds 60 KB
make smoke # headless Chromium loading dist/index.html via file://
make dist-zip # build and zip dist/ as blitsbom-<version>.zip for self-hostersCI invokes make targets, never the underlying npm scripts directly, so the developer and CI commands stay in sync.
- Pushing to
maintriggers.github/workflows/docker.yml, which builds and pushesghcr.io/no42-org/blitsbom:rc(and:main-<short-sha>). - Pushing a tag matching
v*triggers.github/workflows/release.yml(producesblitsbom-X.Y.Z.zip+blitsbom-X.Y.Z.sha512and attaches them to the GitHub Release) and.github/workflows/docker.yml(publishes:X.Y.Zand:X.Yto GHCR — not yet:latest). release.ymlis triggered by thev*tag push: in a single job it builds the bundle, attaches the archive + checksum + Sigstore bundle to a freshly-created GitHub Release (make_latest: legacydefers to GitHub's Latest-release algorithm), and — when the tag is not a prerelease — its final step callsgh workflow run docker.yml --ref <tag> -f promote_tag=X.Y.Zto dispatch thepromote-latestjob. That job re-tags the already-pushed:X.Y.Zimage as:latest— no rebuild, same digest, same cosign signature. (We useworkflow_dispatchrather than therelease: [released]event because GitHub suppresses downstream events triggered by GITHUB_TOKEN;workflow_dispatchis the documented exception.)
All third-party Actions are pinned to immutable commit SHAs and kept current by Dependabot.
See RELEASING.md for the full release workflow — versioning policy, cutting a release, hotfixes, and troubleshooting.
public/imprint.html and public/privacy.html ship as anonymised templates with [PLACEHOLDER] fields and a visible "Placeholder — replace before deploying" banner. They are not legally valid as-is.
Before serving blitsbom publicly, the operator of the deployment must:
- Replace every
[PLACEHOLDER]in both files with the operator's own details (name, address, contact, hosting provider, log retention period, certificate authority, applicable legal basis, etc.). - Adapt the section structure to the applicable jurisdiction. The templates follow the structure typically required by German law (§ 5 TMG, § 18 (2) MStV, GDPR); operators elsewhere should add, remove, or rename sections as needed.
- Remove the yellow "Placeholder" banner once the content has been verified.
The footer links in the app (src/ui/AppShell.svelte) point at /imprint.html and /privacy.html, so no code change is needed — just edit the two HTML files.
Operators who want to keep their personal version out of source control can override the files at runtime — for example with a Docker bind mount:
docker run --rm -p 8080:3000 \
-v ./my-imprint.html:/home/static/imprint.html:ro \
-v ./my-privacy.html:/home/static/privacy.html:ro \
ghcr.io/no42-org/blitsbom:latestsrc/
parse/ CycloneDX + SPDX parsers, format detection, LicenseRef resolution,
VEX merge + purl canonicalization
license/ FSF-sourced classification table
state/ Svelte store, filter combinator (incl. category facet), URL state
ui/ Svelte components (AppShell, DropZone, SummaryHeader,
LicenseDonut, LicenseDrilldown, ComponentsTable, ...)
export/ CSV writer, original-SBOM download
generator/ CI report generator (reuses parse/; built to a Node ESM CLI)
styles/ Tailwind v4 CSS entry (@theme static design tokens)
action.yml Composite GitHub Action wrapping the report generator image
scripts/ size-check, purity-check, file-smoke, e2e
samples/ Real-world SBOMs used as test corpus (not bundled into dist/)
- CONTRIBUTING.md — development setup, commit conventions, DCO sign-off and the AI-assistance policy.
- SUPPORT.md — where to ask a question, and what to include so it gets answered.
- SECURITY.md — how to report a vulnerability privately. Please do not open a public issue for one.
- RELEASING.md — how releases are cut and how to verify the signatures.
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1.
blitsbom is free and open source under the MIT license — a zero-install, no-cloud SBOM viewer that keeps every byte in your browser. If it saved you a spreadsheet or a subscription, a one-time donation helps keep it maintained: releases, dependency upkeep, and staying genuinely local-first.
- GitHub Sponsors: https://github.com/sponsors/indigo423
- Ko-fi: https://ko-fi.com/indigo423
No paid tiers or perks — the tool stays free for everyone. A ⭐ or a good bug report helps just as much, and there's a spot on SPONSORS.md if you'd like one. ❤️
MIT — see LICENSE.