--- title: SBOM --- ```js import {API} from "./components/config.js"; ``` ```js // Fetch SBOM data on load let _entries = [], _report = {groups: [], copyleft_direct_count: 0}, _repos = [], _domains = [], _snapshots = []; try { [_entries, _report, _repos, _domains, _snapshots] = await Promise.all([ fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []), fetch(`${API}/sbom/report/licences/`).then(r => r.ok ? r.json() : {groups:[], copyleft_direct_count: 0}), fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []), fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []), fetch(`${API}/sbom/snapshots/`).then(r => r.ok ? r.json() : []), ]); } catch {} ``` ```js const entries = _entries ?? []; const report = _report ?? {groups: [], copyleft_direct_count: 0}; const repos = _repos ?? []; const domains = _domains ?? []; const snapshots = _snapshots ?? []; const groups = report.groups ?? []; const riskCount = report.copyleft_direct_count ?? 0; // Domain + repo lookups const domainById = Object.fromEntries(domains.map(d => [d.id, d])); const repoById = Object.fromEntries(repos.map(r => [r.id, r])); const repoDomain = Object.fromEntries(repos.map(r => [r.id, domainById[r.domain_id]?.slug ?? "—"])); const domainSlugs = [...new Set(repos.map(r => repoDomain[r.id]).filter(s => s !== "—"))].sort(); // Copyleft detector (mirrors server-side logic) const COPYLEFT_KW = ["GPL", "AGPL", "LGPL", "EUPL", "CDDL", "MPL"]; const isCopyleft = spdx => spdx && COPYLEFT_KW.some(k => spdx.toUpperCase().includes(k)); ``` # SBOM ```js import {withDocHelp} from "./components/doc-overlay.js"; const _h1 = document.querySelector("#observablehq-main h1"); if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/sbom"); } ``` ## Overview ```js const riskBadge = riskCount === 0 ? html`✓ No copyleft in direct prod deps` : html`⚠ ${riskCount} direct prod dep(s) with copyleft licence`; display(html`

Total Packages

${entries.length}

Repos Scanned

${new Set(entries.map(e => e.repo_id)).size}

Domains Covered

${domainSlugs.length || new Set(Object.values(repoDomain).filter(s => s !== "—")).size}

Licence Risk

${riskCount}

${riskBadge}

Unique Licences

${groups.length}

`); ``` ## By Domain ```js if (entries.length === 0) { display(html`

No SBOM data ingested yet. Run make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=<path>.

`); } else { // Group entries by domain const byDomain = {}; for (const e of entries) { const slug = repoDomain[e.repo_id] ?? "—"; (byDomain[slug] = byDomain[slug] ?? []).push(e); } const domainTableRows = Object.entries(byDomain).map(([slug, es]) => { const dom = domains.find(d => d.slug === slug); const repoCount = new Set(es.map(e => e.repo_id)).size; const directProd = es.filter(e => e.is_direct && !e.is_dev); const copyleftRisk = directProd.filter(e => isCopyleft(e.license_spdx)).length; const ecosystems = [...new Set(es.map(e => e.ecosystem))].sort().join(", "); return { domain: dom?.name ?? slug, repos: repoCount, packages: es.length, direct: directProd.length, copyleft: copyleftRisk, ecosystems, }; }).sort((a, b) => a.domain.localeCompare(b.domain)); display(Inputs.table(domainTableRows, { columns: ["domain", "repos", "packages", "direct", "copyleft", "ecosystems"], header: {domain: "Domain", repos: "Repos", packages: "All Pkgs", direct: "Direct Prod", copyleft: "Copyleft ⚠", ecosystems: "Ecosystems"}, maxWidth: 900, })); } ``` ## Licence Distribution ```js import * as Plot from "npm:@observablehq/plot"; if (groups.length === 0) { display(html`

No SBOM data ingested yet.

`); } else { const plotData = groups.slice(0, 15).map(g => ({ licence: g.license_spdx ?? "(unknown)", count: g.count, copyleft: g.is_copyleft, })); display(Plot.plot({ x: {label: "Packages"}, y: {label: null, domain: plotData.map(d => d.licence)}, color: {domain: [false, true], range: ["steelblue", "#e53935"], legend: true, tickFormat: d => d ? "Copyleft" : "Permissive"}, marks: [ Plot.barX(plotData, {y: "licence", x: "count", fill: "copyleft", tip: true}), Plot.ruleX([0]), ], marginLeft: 130, height: Math.max(80, plotData.length * 30 + 50), width: 600, })); } ``` ## Copyleft Risk Detail ```js const copyleftGroups = groups.filter(g => g.is_copyleft); if (copyleftGroups.length === 0) { display(html`

✓ No copyleft packages found.

`); } else { display(html`
${copyleftGroups.map(g => html`
${g.license_spdx ?? "unknown"} ${g.count} package(s) ${g.repos.join(", ")}
`)}

Note: dual-licensed packages (e.g. "MIT OR GPL-3.0") are flagged conservatively. Review if the non-copyleft variant is used.

`); } ``` ## By Repo ```js // Group entries by repo, sorted by domain then repo name const byRepo = {}; for (const e of entries) { (byRepo[e.repo_id] = byRepo[e.repo_id] ?? []).push(e); } const repoSections = Object.entries(byRepo) .map(([repoId, es]) => { const repo = repoById[repoId]; const domSlug = repoDomain[repoId] ?? "—"; const dom = domains.find(d => d.slug === domSlug); const directProd = es.filter(e => e.is_direct && !e.is_dev); const copyleftRisk = directProd.filter(e => isCopyleft(e.license_spdx)).length; const ecosystems = [...new Set(es.map(e => e.ecosystem))].sort(); return { repoId, repo, dom, domSlug, es, directProd, copyleftRisk, ecosystems }; }) .sort((a, b) => (a.domSlug + a.repo?.slug).localeCompare(b.domSlug + b.repo?.slug)); if (repoSections.length === 0) { display(html`

No repo data.

`); } else { display(html`
${repoSections.map(({repoId, repo, dom, domSlug, es, directProd, copyleftRisk, ecosystems}) => html`
${dom?.name ?? domSlug} ${repo?.slug ?? repoId.slice(0,8)} ${es.length} pkgs · ${ecosystems.join(" + ")} · ${directProd.length} direct ${copyleftRisk > 0 ? html`⚠ ${copyleftRisk} copyleft` : ""}
${Inputs.table(es.slice(0, 200).map(e => ({ Package: e.package_name, Version: e.package_version ?? "—", Ecosystem: e.ecosystem, Licence: e.license_spdx ?? "—", Direct: e.is_direct ? "✓" : "", Dev: e.is_dev ? "✓" : "", })), {maxWidth: 860})} ${es.length > 200 ? html`

Showing first 200 of ${es.length}

` : ""}
`)}
`); } ``` ## Snapshot History ```js if (snapshots.length === 0) { display(html`

No snapshots recorded yet.

`); } else { // Group by repo, sort newest first within each group const snapByRepo = {}; for (const s of snapshots) { (snapByRepo[s.repo_id] = snapByRepo[s.repo_id] ?? []).push(s); } const repoOrder = Object.keys(snapByRepo).sort((a, b) => { const ra = repos.find(r => r.id === a); const rb = repos.find(r => r.id === b); return (ra?.slug ?? a).localeCompare(rb?.slug ?? b); }); display(html`
${repoOrder.map(repoId => { const repo = repos.find(r => r.id === repoId); const domSlug = repo ? domains.find(d => d.id === repo.domain_id)?.slug ?? "—" : "—"; const snaps = snapByRepo[repoId]; // already sorted newest-first by API return html`
${domSlug} ${repo?.slug ?? repoId.slice(0,8)} ${snaps.length} snapshot${snaps.length !== 1 ? "s" : ""}
${Inputs.table(snaps.map(s => ({ "Snapshot At": new Date(s.snapshot_at).toLocaleString(), Packages: s.entry_count, Source: s.source ?? "—", ID: s.id.slice(0, 8) + "…", })), {maxWidth: 700})}
`; })}
`); } ``` ## Package Table ```js // Filters const domainOpts = ["all", ...domainSlugs]; const domainFilter = Inputs.select(domainOpts, {label: "Domain", value: "all"}); const ecoFilter = Inputs.select(["all", "python", "node", "rust", "go", "java", "other"], {label: "Ecosystem", value: "all"}); const directOnly = Inputs.toggle({label: "Direct deps only", value: false}); const prodOnly = Inputs.toggle({label: "Prod deps only (no dev)", value: false}); display(html`
${domainFilter}${ecoFilter}${directOnly}${prodOnly}
`); ``` ```js const filteredEntries = entries.filter(e => (domainFilter.value === "all" || repoDomain[e.repo_id] === domainFilter.value) && (ecoFilter.value === "all" || e.ecosystem === ecoFilter.value) && (!directOnly.value || e.is_direct) && (!prodOnly.value || !e.is_dev) ); display(Inputs.table(filteredEntries.map(e => ({ Package: e.package_name, Version: e.package_version ?? "—", Ecosystem: e.ecosystem, Licence: e.license_spdx ?? "—", Domain: repoDomain[e.repo_id] ?? "—", Repo: repoById[e.repo_id]?.slug ?? e.repo_id?.slice(0, 8) ?? "—", Direct: e.is_direct ? "✓" : "", Dev: e.is_dev ? "✓" : "", })), {maxWidth: 960})); ```