Builds a CycloneDX JSON SBOM from already-collected package data and validates it against the CycloneDX JSON schema before returning it.
This is a library only, with no command line interface. It does no dependency discovery of
its own: it reads no files, walks no node_modules, and resolves no version ranges.
Everything it needs is passed in, so the caller decides where the package data comes from.
npm install --save "@volumegraphics/license-info-sbom"
const { buildSbom } = require("@volumegraphics/license-info-sbom");
const result = await buildSbom({
product: { name: "my-product", version: "1.2.3" },
packages: [
{ name: "some-library", version: "4.5.6", license: "MIT", homepage: "https://example.com" }
],
licenseTextByName: new Map(),
tool: { name: "my-generator", version: "1.0.0" },
options: { specVersion: "1.6" }
});
if (result.type === "Error") {
console.error(result.errors.join("\n"));
process.exit(1);
}
fs.writeFileSync("bom.json", result.json);buildSbom returns the errors instead of a document whenever either the input data or the
resulting document is invalid, so the caller can abort before writing anything to disk.
| Type | Description |
|---|---|
buildSbom(input: SbomInput): Promise<SbomResult> |
Builds and validates the document. |
SbomInput |
product, packages, licenseTextByName, tool, options. |
SbomPackage |
The subset of collected package data the SBOM needs: name, version, and optionally bomRef, purlOptions, license, description, homepage, packageJson, and the resolved packageDependencies / packageDevDependencies / packageOptionalDependencies arrays. |
SbomPurlOptions |
One mandatory property, purl: a package URL, or null for none. See the package URL section below. |
SbomOptions |
See the options table below. |
SbomResult |
{ type: "Sbom", json } or { type: "Error", errors }. |
| Option | Default | Description |
|---|---|---|
specVersion |
"1.6" |
"1.6", "1.5" or "1.4". Black Duck only gained 1.6 support in release 2025.1.0. |
includeLicenseText |
false |
Embed license texts as base64 attachments. Multiplies the file size, and Black Duck does not read them. |
serialNumber |
true |
Emit a randomly generated serialNumber. |
timestamp |
true |
Emit metadata.timestamp. |
serialNumber and timestamp are the only non-deterministic parts of the document:
everything else is sorted and content-derived. Set both to false and two runs over
identical input produce byte-identical output.
bom-ref defaults to name@version. If your name is a human-readable display name
rather than an identifier, supply bomRef explicitly so that bom-refs and dependency edges
key on something canonical while the rendered name stays human-readable.
The package URL is decided per component, through purlOptions on the SbomPackage — the
product included, since that is an SbomPackage too.
purlOptions |
Result |
|---|---|
| left out | pkg:npm/<name>@<version> is built from the name and the version. |
{ purl: "pkg:conan/zlib@1.3.1?channel=stable" } |
That purl is used verbatim. |
{ purl: null } |
No purl is emitted for this component. |
purl is the only property, and it is mandatory: naming the component is the only thing the
options are for. Options without a usable purl are reported as an error rather than quietly
falling back to the npm default — leave the options out to ask for that, and set purl to
null to ask for no package URL at all.
The built purl is always an npm one, and scoped names of the form @scope/name are split
into a purl namespace and name accordingly.
A given purl is taken as it stands and is not checked, so it is the way to name a component
whose ecosystem needs qualifiers, or whose real name differs from the name in the document.
Prefer a null purl over one you are not sure of. The purl is what Black Duck matches on,
so a wrong one produces confidently wrong matches — worse than no purl, where matching falls
back to name and version. Being a per-component decision, a dependency set drawn from several
ecosystems does not have to make that trade for all of it at once: name each component the
way it can be named, and leave the rest without a purl.
Every component must have a name and a version, and all problems are reported at once rather than only the first. A component without a name makes Black Duck reject the entire SBOM; without a version the package URL is incomplete and component matching degrades.
Per component: type, name, group for scoped packages, version, a purl unless that
component asked for none, a unique bom-ref, the licenses entry, and the homepage as an
external reference. The document carries a serialNumber, metadata.timestamp,
metadata.tools, metadata.component for the product, and a dependencies graph built
from the resolved dependency arrays.
Licenses follow the CycloneDX convention: a valid SPDX identifier becomes license.id, a
valid SPDX expression becomes expression, and anything else becomes free text in
license.name. So a value like "Public Domain", which is not an SPDX identifier, still
produces a valid document.
The document is validated against the CycloneDX JSON schema before it is returned. Validation is never skipped: if the validator itself cannot be set up, that is reported as an error too, because "validated" must not silently degrade into "not validated".
Validation is performed here rather than through CDX.Validation.JsonValidator, using this
package's own ajv. @cyclonedx/cyclonedx-library declares ajv as an optional peer
dependency and then resolves it from its own location, so in a project whose dependency
tree hoists a different ajv major, the library silently picks that one up and fails deep
inside ajv-formats with TypeError: Cannot read properties of undefined (reading 'code').
It offers no way to inject an instance. Because ajv is a real dependency of this package,
npm always places a compatible copy where this module can reach it — so validation works
regardless of what the consuming project hoists. The ajv options and the schema files are
taken from the library itself, so behaviour and schema version match what it would have
done.
Note that serialNumber and metadata.timestamp differ on every run, so the output is not
byte-for-byte reproducible even though all lists are sorted.
npm test
This compiles src and tests into out-test and runs the compiled JavaScript with node's
built-in test runner, so the tests need no framework and no dependency beyond the TypeScript
compiler that is here anyway.
They drive buildSbom through its public interface and assert on the serialized document,
because those are the bytes the caller writes to disk. Any test that gets a document back has
also asserted that the document validates against the CycloneDX schema, since buildSbom
returns errors instead of a document whenever it does not.
To debug them in VS Code, pick one of the two launch configurations. Debug the open test
file runs the compiled counterpart of the test file in the editor directly, so every test
executes in a single process and breakpoints always bind — that is the one to reach for while
working on a test. Debug all tests goes through npm test, where node's test runner puts
every test file in a child process of its own and the debugger attaches to each. Breakpoints
in src work in both, by way of the source maps the test build emits.
@cyclonedx/cyclonedx-library declares ajv, ajv-formats and ajv-formats-draft2019
only as optional peer dependencies, which npm does not install automatically. They are
therefore real dependencies here — without them schema validation is unavailable, which
buildSbom treats as a failure.