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>