Tunas Akara
Back to Blog

SBOMs in Practice: Automating License Compliance Before Delivery

by Rayhan7 min read
licensingsbomcomplianceopen-sourceci-cd
SBOMs in Practice: Automating License Compliance Before Delivery

SBOMs in Practice: Automating License Compliance Before Delivery

A mid-sized Node service pulls in somewhere between 800 and 1,500 packages once transitive dependencies are counted. Every one of them carries a license, and a handful of those licenses carry obligations you have to satisfy at delivery. Reading them by hand is not a plan.

The working answer is an SBOM — a software bill of materials, a generated inventory of everything in the build — plus a policy check in CI that fails the pipeline when something arrives that you don't want to ship. Which licenses actually cause trouble is a separate question I've covered. This article is about knowing what you have.

The manifest is a claim, not a fact

Start with the failure that catches teams who think npm ls is enough: the license field in a package manifest is self-reported metadata. Nobody verifies it.

Packages declare MIT in package.json while shipping vendored source files with GPL headers inside lib/. Packages declare nothing at all. A license field reads SEE LICENSE IN LICENSE.txt and points at a file that never made it into the published tarball.

SPDX, the older of the two SBOM standards, encodes this distinction directly. Every package entry carries both a licenseDeclared field, what the project says it is, and a licenseConcluded field, what your analysis determined it to be. Cheap tooling only fills the first. File-level scanners like ScanCode Toolkit read the actual source headers to produce the second.

For most delivery work, declared-license scanning catches the obvious problems: the AGPL package someone added last sprint, the non-commercial model helper. Full file-level scanning is what you escalate to when a client's legal team wants evidence rather than assurance, or when a codebase is changing hands and the answer carries a price tag.

Two formats, and it doesn't much matter which

SPDX and CycloneDX both do the job. SPDX came out of the Linux Foundation and is standardized as ISO/IEC 5962:2021. CycloneDX came out of OWASP with a security focus and was adopted as ECMA-424 in 2024.

Pick based on what consumes the output. If a client's compliance team already has a workflow, match theirs. If nobody has an opinion, CycloneDX JSON is easier to read and post-process, while SPDX has more legal tooling around it. Conversion between the two is routine, and syft alone will emit either.

The format debate eats more meeting time than it deserves. The value is in generating the file at all, then acting on it.

Generating it

Syft (Apache-2.0) is my default. It reads a source directory, a container image, or a built artifact, and emits SPDX or CycloneDX:

syft dir:. -o cyclonedx-json=sbom.json
syft registry.example.com/app:2.4.1 -o spdx-json=sbom-image.json

Ecosystem-native tools go deeper on their own language and are worth running alongside:

  • cargo deny check licenses — Rust, with allow/deny lists and SPDX expression evaluation built in
  • go-licenses report — Go, resolves the license file per module
  • pip-licenses — Python, reads installed distribution metadata

The Rust one is the model the others should copy: policy and scanning in the same tool, non-zero exit on violation, no glue script required.

The gate is where the value is

An SBOM sitting in a build directory changes nothing. The gate does. Sort licenses into three buckets and wire each to a CI outcome.

Allow silently — MIT, BSD-2-Clause, BSD-3-Clause, ISC, Apache-2.0, Zlib. Permissive, attribution-only obligations that the NOTICE step below handles.

Fail the build — AGPL-3.0, SSPL, BUSL-1.1, the non-commercial Creative Commons variants, and the empty string. AGPL and SSPL because network use triggers source obligations you almost certainly don't want attached to a client deliverable. BUSL because it isn't open source at all during its change period. Non-commercial CC because commercial delivery is exactly what you're doing. The empty string because no license text means no permission granted, which is a worse position than a restrictive license, not a safer one.

Flag for human review — LGPL-2.1, LGPL-3.0, MPL-2.0, EPL-2.0, and GPL-2.0 with the classpath exception. These are conditionally fine. LGPL is workable dynamically linked and a problem statically linked, so the answer depends on how you ship it. MPL is per-file copyleft that rarely bites, but somebody has to confirm which files you modified.

Loading diagram…

One implementation detail breaks naive gates: SPDX license expressions are not plain strings. A package can declare MIT OR Apache-2.0, Apache-2.0 WITH LLVM-exception, or MIT AND BSD-3-Clause. String equality against an allow list rejects MIT OR Apache-2.0, which is acceptable under either half. Use a tool that parses expressions, or you will train the team to ignore the gate within two sprints.

The container image is a second dependency tree

Application-level scanning misses the operating system layer, and a specific class of surprise lives there.

The clearest example is ffmpeg installed from apt on Debian or Ubuntu. It is built with GPL-licensed encoders such as libx264 and libx265, so the resulting binary is distributed under the GPL. The LGPL story people repeat about FFmpeg applies to a build configured without those components. Almost nobody configures their own build. They run apt-get install ffmpeg, and the image inherits GPL binaries that no package.json scan will ever mention.

Whether that matters depends on how you deliver. A GPL binary invoked as a separate process inside a service you operate is a different situation from the same binary shipped in an appliance handed to a client. Either way, the OS layer has to be scanned before the question can even be asked.

Attribution is the obligation you will actually breach

Every permissive license in that allow list still requires something: reproduce the copyright notice and license text in distributions. MIT says so in one sentence. Apache-2.0 spells it out across four clauses, and adds a NOTICE file requirement when the upstream project ships one.

This is the most commonly breached obligation in commercial software, and the cheapest to fix. Generate a third-party attribution file from the SBOM at build time and ship it — in the installer, in an about screen, in a docs folder next to the binary. One pipeline step closes the gap a compliance review is most likely to open with.

What no scanner will find

Three categories stay invisible to tooling and need a human process instead.

Binary SDKs with no manifest. A vendor AAR, a closed DLL, a driver library handed over on a flash drive. No package metadata, no license file, nothing to scan. That is a contract question, settled before integration rather than at build time.

Model weights and datasets. A pip package under Apache-2.0 can download weights under a non-commercial research license on first run. The SBOM sees the package and reports it clean. Code license, weight license, and dataset license are three separate grants.

Copy-pasted code. Snippets from a forum answer, an example from someone else's repository, a function lifted from a blog post. No dependency entry exists, so no scanner reports it. File-level scanners catch some of it when a full license header comes along for the ride, which is one argument for running one before a codebase changes hands.

Keep the SBOM with the release

Generate the SBOM per release and store it as a release artifact next to the binary. Not in a wiki, not in someone's home directory.

The reason is practical. Eighteen months after delivery, a client's auditor asks what version 2.3.1 contained, or a CVE lands in a library and you need to know which deployed versions include it. Regenerating from a git tag gives you an approximation, because lockfiles resolve differently over time and upstream packages get yanked. The SBOM generated at build time is the record of what actually shipped.

Regulation is moving the same direction. The EU Cyber Resilience Act requires manufacturers of products with digital elements to draw up an SBOM covering at least the top-level dependencies. Its vulnerability reporting obligations start applying on 11 September 2026, with full application on 11 December 2027. For anyone selling hardware or software into the EU market, this stops being optional.

The takeaway

Generate an SBOM in CI. Gate it against an allow, deny, and review policy that understands SPDX expressions. Scan the container image as well as the repository. Emit a NOTICE file from the same data, and archive the SBOM alongside the release.

That covers the dependency tree. Binary SDKs, model weights, and pasted code stay a human problem. Knowing the tooling doesn't reach them is most of what keeps them from becoming a delivery problem.

Related Posts

Building something similar?

Hotel Management System Development

Custom ERP-style hotel management software: bookings, room status, invoicing, staff, and WhatsApp automation — built around how your hotel actually runs.

See how I can help