v3.0.0

<bmx-sbom-viewer>

Opens a software bill of materials - CycloneDX 1.2 to 1.6 in JSON or XML, SPDX 2.2 and 2.3 in JSON or tag-value, SPDX 3.0 in JSON-LD - in the page, with nothing sent anywhere. Seven views of the same components: a table to sort, filter and search; the dependency tree, opened branch by branch or by finding a component; a graph of what depends on what; the licences, each SPDX expression judged against the page's own licence policy (AND needs every part allowed, OR one, exceptions and patterns as the policy names them); the vulnerabilities that the page's own CycloneDX, OpenVEX and OSV files say affect them, scored from their CVSS 2.0 to 4.0 vectors, with what the VEX statements decided; what changed since an earlier SBOM of the product; and the checks against the NTIA minimum elements. The side panel shows the selected component and vulnerability in full, the document's own details, and the policy for editing. Exports the components, vulnerabilities and changes as CSV, the SBOM as CycloneDX 1.6 or SPDX 2.3 JSON, the policy, and a PDF report. Nothing is looked up online. CVSS is owned by FIRST and used by permission.

14 properties · 7 events · 13 methods · 4 parts

Example

Try it with your own SBOM files in confidence: they are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.

Show markup
<bmx-sbom-viewer id="ex-sbom" label="Harbour Ledger 2.4 - software bill of materials"></bmx-sbom-viewer>
<p class="note" id="ex-sbom-out" role="status" style="margin: 12px 0 0"></p>

<bmx-sbom-viewer id="ex-spdx" label="Tide Tables 1.0 - SPDX tag-value" view="licences" panel="false" readonly style="margin-block-start: 24px; --bmx-sbom-viewer-height: 22rem"></bmx-sbom-viewer>

<script type="module">
  await customElements.whenDefined('bmx-sbom-viewer');

  // An invented accounting product, Harbour Ledger, as its build would describe it in CycloneDX 1.6.
  // The Vulnerabilities view reads the advisories and VEX below; the Changes view compares with release 2.3.
  // CVSS is owned by FIRST and used by permission.
  // The open-source packages carry their own licences; the Harbour ones are invented, and three of them
  // are there to be found: a PDF engine under the AGPL, a fonts package with no licence or supplier, and
  // a sanitiser offered under a choice of two licences.
  const pkg = (name, version, licence, supplier, deps = [], extra = {}) => ({ name, version, licence, supplier, deps, ...extra });
  const parts = [
    pkg('react', '18.3.1', 'MIT', 'Meta', ['loose-envify']),
    pkg('react-dom', '18.3.1', 'MIT', 'Meta', ['react', 'scheduler', 'loose-envify']),
    pkg('scheduler', '0.23.2', 'MIT', 'Meta', ['loose-envify']),
    pkg('loose-envify', '1.4.0', 'MIT', 'Andres Suarez', ['js-tokens']),
    pkg('js-tokens', '4.0.0', 'MIT', 'Simon Lydell'),
    pkg('date-fns', '3.6.0', 'MIT', 'date-fns'),
    pkg('zod', '3.23.8', 'MIT', 'Colin McDonnell'),
    pkg('rxjs', '7.8.1', 'Apache-2.0', 'RxJS', ['tslib']),
    pkg('tslib', '2.6.2', '0BSD', 'Microsoft'),
    pkg('dompurify', '3.1.6', 'MPL-2.0 OR Apache-2.0', 'Cure53'),
    pkg('@harbour/ledger-core', '2.4.0', 'LicenseRef-Harbour-Proprietary', 'Harbour Software', ['date-fns', 'zod', 'rxjs', '@harbour/money']),
    pkg('@harbour/money', '1.9.2', 'LicenseRef-Harbour-Proprietary', 'Harbour Software'),
    pkg('@harbour/reports', '2.4.0', 'LicenseRef-Harbour-Proprietary', 'Harbour Software', ['@harbour/ledger-core', 'pdf-forge', 'dompurify']),
    pkg('pdf-forge', '0.8.3', 'AGPL-3.0-only', 'Forge Labs', ['barcode-glyphs', 'tslib']),
    pkg('barcode-glyphs', '1.0.0', '', ''),
  ];
  const ref = name => name.replace(/^@/, '').replace('/', '-');
  const hash = name => [...name].reduce((h, c) => (Math.imul(h, 31) + c.charCodeAt(0)) >>> 0, 7).toString(16).padStart(8, '0').repeat(8);
  const cyclonedx = (parts, release, serial, timestamp) => ({
    bomFormat: 'CycloneDX',
    specVersion: '1.6',
    serialNumber: `urn:uuid:${serial}`,
    version: 1,
    metadata: {
      timestamp,
      tools: { components: [{ type: 'application', name: 'harbour-build', version: '5.2' }] },
      authors: [{ name: 'Release engineering, Harbour Software' }],
      component: { type: 'application', 'bom-ref': 'harbour-ledger', name: 'harbour-ledger', version: release, supplier: { name: 'Harbour Software' }, licenses: [{ expression: 'LicenseRef-Harbour-Proprietary' }] },
    },
    components: parts.map(p => {
      const [group, name] = p.name.startsWith('@') ? p.name.split('/') : ['', p.name];
      return {
        type: 'library',
        'bom-ref': ref(p.name),
        ...(group ? { group } : {}),
        name,
        version: p.version,
        ...(p.supplier ? { supplier: { name: p.supplier } } : {}),
        ...(p.licence ? { licenses: /\s|^LicenseRef-/.test(p.licence) ? [{ expression: p.licence }] : [{ license: { id: p.licence } }] } : {}),
        ...(p.supplier ? { purl: `pkg:npm/${p.name.replace('@', '%40')}@${p.version}` } : {}),
        ...(p.name.startsWith('@harbour') || !p.supplier ? {} : { hashes: [{ alg: 'SHA-256', content: hash(p.name) }] }),
      };
    }),
    dependencies: [
      { ref: 'harbour-ledger', dependsOn: ['react', 'react-dom', 'harbour-ledger-core', 'harbour-reports'] },
      ...parts.map(p => ({ ref: ref(p.name), dependsOn: p.deps.filter(d => parts.some(q => q.name === d)).map(ref) })),
    ],
  });
  const sbom = cyclonedx(parts, '2.4.0', '3f6b6c58-2a8e-4d6b-9a52-6f1c2e7d0b41', '2026-10-20T08:30:00Z');

  // The release before, for the Changes view: an older PDF engine and zod, moment still in, no sanitiser yet.
  const earlier = parts
    .filter(p => p.name !== 'dompurify')
    .map(p => (p.name === 'pdf-forge' ? { ...p, version: '0.8.1' } : p.name === 'zod' ? { ...p, version: '3.22.4' } : p.name === '@harbour/reports' ? { ...p, deps: p.deps.filter(d => d !== 'dompurify') } : p))
    .concat([pkg('moment', '2.29.4', 'MIT', 'Moment.js')]);

  // Vulnerability files, as a security team would hand them over: an OSV list from the PDF engine's and Harbour's
  // own advisories (invented, like the packages they are about), and Harbour's OpenVEX statement on its own code.
  const osv = (id, name, summary, vector, fixed, extra = {}) => ({
    schema_version: '1.6.0',
    id,
    modified: '2026-10-15T00:00:00Z',
    published: '2026-10-01T00:00:00Z',
    summary,
    ...(vector ? { severity: [{ type: vector.startsWith('CVSS:4') ? 'CVSS_V4' : 'CVSS_V3', score: vector }] } : {}),
    affected: [{ package: { ecosystem: 'npm', name }, ranges: [{ type: 'SEMVER', events: [{ introduced: '0' }, ...(fixed ? [{ fixed }] : [])] }] }],
    ...extra,
  });
  const advisories = [
    osv('PFSA-2026-0007', 'pdf-forge', 'Out-of-bounds read when a PDF names a malformed embedded font', 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N', '0.8.5', { database_specific: { cwe_ids: ['CWE-125'] } }),
    osv('PFSA-2026-0003', 'pdf-forge', 'Infinite loop on a circular outline', 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H', '0.8.2'),
    osv('HSA-2026-0102', '@harbour/reports', 'Script in a report title is not escaped in the HTML preview', 'CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N', '2.4.1'),
    osv('HSA-2026-0101', '@harbour/money', 'Half-up rounding applied where banker\u2019s rounding was asked for', 'CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N', '1.9.3'),
    osv('BGSA-2026-0001', 'barcode-glyphs', 'Glyph table read past its end for some code points', '', '', { database_specific: { severity: 'MODERATE' } }),
  ];
  const vex = {
    '@context': 'https://openvex.dev/ns/v0.2.0',
    '@id': 'https://harbour.example/vex/ledger-2.4.0',
    author: 'Harbour Software security team',
    timestamp: '2026-10-21T09:00:00Z',
    version: 1,
    statements: [
      { vulnerability: { name: 'HSA-2026-0102' }, products: [{ '@id': 'harbour-ledger', subcomponents: [{ '@id': 'pkg:npm/%40harbour/reports@2.4.0' }] }], status: 'not_affected', justification: 'vulnerable_code_not_in_execute_path', impact_statement: 'Harbour Ledger never shows the HTML preview; reports are printed to PDF.' },
      { vulnerability: { name: 'PFSA-2026-0007' }, products: [{ '@id': 'harbour-ledger', subcomponents: [{ '@id': 'pkg:npm/pdf-forge@0.8.3' }] }], status: 'affected', action_statement: 'Upgrade pdf-forge to 0.8.5 in 2.4.1; until then fonts are taken only from the built-in set.' },
    ],
  };

  const viewer = document.getElementById('ex-sbom');
  viewer.policy = {
    allow: ['MIT', 'Apache-2.0', 'BSD-*', 'ISC', '0BSD', 'LicenseRef-Harbour-*'],
    review: ['MPL-2.0', 'LGPL-*'],
    deny: ['GPL-*', 'AGPL-*'],
    otherwise: 'review',
    missing: 'review',
  };
  viewer.sbom = sbom;
  viewer.vulnerabilities = [advisories, vex];
  viewer.compare = cyclonedx(earlier, '2.3.0', '8d1f2e70-5b3c-4f0a-9e61-2b7c4d5a6e10', '2026-08-04T08:30:00Z');

  const out = document.getElementById('ex-sbom-out');
  const report = async () => {
    const rows = await viewer.getLicences();
    const n = status => rows.filter(r => r.status === status).reduce((sum, r) => sum + r.refs.length, 0);
    const open = (await viewer.getFindings()).filter(f => f.status === 'affected' || f.status === 'under_investigation');
    out.textContent = `Licences: ${n('allowed')} components allowed, ${n('review')} to review, ${n('denied')} denied. Vulnerabilities open: ${open.length}.`;
  };
  viewer.addEventListener('bmxPolicyChange', report);
  viewer.addEventListener('bmxVulnerabilitySelect', e => {
    const f = e.detail.finding;
    if (f) out.textContent = `${f.id}: ${f.vulnerability.summary} (${f.severity || 'not rated'}${f.score !== null ? `, ${f.score}` : ''}) - ${f.status.replace('_', ' ')}.`;
  });
  viewer.addEventListener('bmxSelect', e => {
    const c = e.detail.component;
    if (c) out.textContent = `${c.group ? `${c.group}/` : ''}${c.name} ${c.version}: ${c.declared || 'no licence given'}${c.supplier ? `, from ${c.supplier}` : ''}.`;
  });
  await report();

  // The same kind of document as SPDX 2.3 tag-value, the format many build tools still write.
  document.getElementById('ex-spdx').sbom = [
    'SPDXVersion: SPDX-2.3',
    'DataLicense: CC0-1.0',
    'SPDXID: SPDXRef-DOCUMENT',
    'DocumentName: tide-tables-1.0.0',
    'DocumentNamespace: https://example.org/spdx/tide-tables-1.0.0',
    'Creator: Tool: harbour-build-5.2',
    'Created: 2026-10-20T08:30:00Z',
    '',
    'PackageName: tide-tables',
    'SPDXID: SPDXRef-tide-tables',
    'PackageVersion: 1.0.0',
    'PackageSupplier: Organization: Harbour Software',
    'PackageDownloadLocation: NOASSERTION',
    'FilesAnalyzed: false',
    'PackageLicenseDeclared: LicenseRef-Harbour-Proprietary',
    '',
    'PackageName: suncalc',
    'SPDXID: SPDXRef-suncalc',
    'PackageVersion: 1.9.0',
    'PackageDownloadLocation: NOASSERTION',
    'FilesAnalyzed: false',
    'PackageLicenseDeclared: BSD-2-Clause',
    'ExternalRef: PACKAGE-MANAGER purl pkg:npm/suncalc@1.9.0',
    '',
    'PackageName: chart-glue',
    'SPDXID: SPDXRef-chart-glue',
    'PackageVersion: 0.3.1',
    'PackageDownloadLocation: NOASSERTION',
    'FilesAnalyzed: false',
    'PackageLicenseDeclared: LGPL-2.1-or-later',
    '',
    'Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-tide-tables',
    'Relationship: SPDXRef-tide-tables DEPENDS_ON SPDXRef-suncalc',
    'Relationship: SPDXRef-tide-tables DEPENDS_ON SPDXRef-chart-glue',
  ].join('\n');
  document.getElementById('ex-spdx').policy = viewer.policy;
</script>
<bmx-sbom-viewer src="/sbom/app.cdx.json" vulnerabilities-src="/sbom/app.vex.json /sbom/osv.json"
  policy='{"allow":["MIT","Apache-2.0","BSD-*"],"deny":["GPL-*"]}'></bmx-sbom-viewer>

Properties

PropertyAttributeTypeDefaultDescription
compare compare BmxSbom | Record<string, unknown> | string | null — An earlier SBOM of the same product to compare with: an object, or the text of any format the viewer reads.
compareSrc compare-src string — An address to read the SBOM to compare with from (the page's own server).
label label string — A heading for the viewer (the SBOM's name when not given).
panel panel boolean true Show the side panel. Two-way.
policy policy BmxLicencePolicy | string — The licence policy: { allow, review, deny, otherwise, missing }, as an object or JSON. Two-way: bmxPolicyChange reports every change made in the panel.
readonly readonly boolean false Show the SBOM without letting the reader open another file or change the policy.
sbom sbom BmxSbom | Record<string, unknown> | string | null — The SBOM: CycloneDX or SPDX as an object, or the text of any format the viewer reads.
selected selected string — The selected component's reference. Two-way.
src src string — An address to read the SBOM from (the page's own server).
strings strings Partial<BmxSbomViewerStrings> | string — Words for translation: an object or JSON.
toolbar toolbar boolean true Show the toolbar.
view view BmxSbomViewerView 'components' The view shown. Two-way.
vulnerabilities property only unknown — Vulnerability files: a CycloneDX VEX or BOM, an OpenVEX document or OSV (an entry, a list, or osv-scanner's output) as an object or text, or a list of such files.
vulnerabilitiesSrc vulnerabilities-src string — Addresses to read vulnerability files from (the page's own server), separated by spaces.

Events

EventDetailDescription
bmxCompare { readonly added: number; readonly removed: number; readonly changed: number; readonly unchanged: number; } Fired when a comparison has been made, with its counts.
bmxError { readonly message: string; } Fired when something cannot be read or written.
bmxLoad { readonly format: string; readonly specVersion: string; readonly components: number; readonly notes: readonly string[]; } Fired when an SBOM has been read, with what it is and what was noted.
bmxPolicyChange { readonly policy: BmxLicencePolicy; } Fired when the policy is changed in the panel, with the new policy.
bmxSelect { readonly ref: string | null; readonly component: BmxSbomComponent | null; } Fired when the selected component changes.
bmxVulnerabilityLoad { readonly format: string; readonly name: string; readonly vulnerabilities: number; readonly notes: readonly string[]; } Fired when a vulnerability file has been read.
bmxVulnerabilitySelect { readonly finding: BmxSbomFinding | null; } Fired when a vulnerability is selected, with what was found.

Methods

MethodSignatureDescription
check check() => Promise<BmxSbomCheck[]> What is missing from the SBOM, measured against the NTIA minimum elements.
clearVulnerabilities clearVulnerabilities() => Promise<void> Removes the vulnerability files opened in the viewer or added by loadVulnerabilities (not those the page gave).
compareWith compareWith(data: Blob | BmxSbom | Record<string, unknown> | string | null) => Promise<BmxSbomComparison | null> Compares with an earlier SBOM of the product (a File, text or object), or stops comparing (null). Resolves with the comparison.
export export(kind?: BmxSbomViewerExport) => Promise<Blob> A file of the SBOM: its components, vulnerabilities or changes as CSV, CycloneDX 1.6 JSON, SPDX 2.3 JSON, the viewer's own JSON, the policy, or the PDF report.
getComparison getComparison() => Promise<BmxSbomComparison | null> The comparison with the earlier SBOM, or null when there is none.
getFindings getFindings() => Promise<BmxSbomFinding[]> Every vulnerability found in a component, with its status and score.
getLicences getLicences() => Promise<BmxSbomLicenceRow[]> The components grouped by licence, with the policy's verdict on each.
getSbom getSbom() => Promise<BmxSbom> The SBOM as the viewer read it (a copy): one shape whichever format it came in.
load load(data: Blob | FileList | BmxSbom | Record<string, unknown> | string) => Promise<void> Opens an SBOM: a File or Blob, a FileList (the first file), text in any format the viewer reads, or a CycloneDX or SPDX object.
loadVulnerabilities loadVulnerabilities(data: Blob | Record<string, unknown> | unknown[] | string, name?: string) => Promise<number> Adds a vulnerability file (CycloneDX VEX or BOM, OpenVEX, OSV): a File or Blob, text or an object. Resolves with how many vulnerabilities it held.
save save(kind?: BmxSbomViewerExport, fileName?: string) => Promise<void> Downloads a file of the SBOM (see export).
select select(ref: string | null) => Promise<void> Selects a component by reference (null clears the selection), and shows it in the current view.
selectVulnerability selectVulnerability(key: string | null) => Promise<void> Selects a vulnerability finding by its key (the vulnerability's identifier and the component's reference, joined by a tab), or by identifier alone (its first component).

CSS shadow parts

PartDescription
panel The side panel (component, vulnerability, document, policy).
status The line under the view.
toolbar The bar of commands.
view The area the current view fills.

CSS custom properties

PropertyDescription
--bmx-sbom-viewer-allowed Allowed licences in the graph and legend.
--bmx-sbom-viewer-denied Denied licences in the graph and legend.
--bmx-sbom-viewer-height The height of the view and panel.
--bmx-sbom-viewer-panel-width The side panel's width.
--bmx-sbom-viewer-radius Corner radius of the frame.
--bmx-sbom-viewer-review Licences for review in the graph and legend.