Supply Chain
Published
The question arrives from procurement, or from a customer’s security team, or from whoever has just been handed the software bill of materials requirement. It sounds like it should have one answer. It has two, and which one applies to you depends on a detail most teams have never had a reason to check: whether your SBOM is generated from your dependency graph or by scanning the file you ship.
Two things called the same name
A software bill of materials is a document listing the components in a piece of software. Nothing in that definition says how the list is produced, and in the JavaScript ecosystem there are two entirely different mechanisms in common use.
Graph-derived. The generator reads your lockfile — package-lock.json, yarn.lock, pnpm-lock.yaml — or walks node_modules, and writes out the resolved dependency tree with names, versions and licences. This is what npm sbom produces, and what most CycloneDX and SPDX generators do when pointed at a project directory. The information comes from package metadata that your package manager already resolved.
Artifact-derived. The generator is pointed at a built thing — a dist folder, a container image, a release archive — and infers what is inside by pattern matching. It looks for version strings, banner comments, licence headers and distinctive code shapes belonging to libraries it knows about. This is how binary-scanning inventory tools work, and it is the same technique used by vulnerability scanners that examine shipped bundles.
Both produce a file with the same schema and the same name. Only one of them survives contact with a protection step.
The graph-derived answer: nothing happens
If your SBOM comes from the lockfile, obfuscation is invisible to it, and the reason is ordering. Dependency resolution happens first: the package manager reads your manifest, resolves the tree, writes the lockfile. Bundling happens later, consuming that tree. Protection happens later still, consuming the bundler’s output. A generator reading the lockfile is reading a document written before either of the last two steps existed, so it cannot be affected by them.
This is worth stating plainly to anyone who asks, because the intuition runs the other way. People imagine the SBOM being extracted from the shipped file, in which case obfuscation would obviously interfere. In a normal npm pipeline it is not, and the inventory you hand a customer is exactly as accurate for a protected release as for an unprotected one.
There is one honest caveat. A graph-derived SBOM describes what your build consumed, and a bundler with tree shaking may not have included all of it in the output. That gap exists with or without protection — it is a property of bundling, not of obfuscation — and it is a familiar and accepted limitation. Nobody expects a lockfile-derived inventory to be a byte-level manifest of the artifact.
The artifact-derived answer: it stops working entirely
Now the other half, and here the effect is not partial. An artifact scanner identifies a library by recognising it, and recognition depends on precisely the things a protection pass is designed to remove.
Consider what those tools actually match on. A version string the library assigns to itself, often as a literal near the top of the file. A banner comment with the project name and licence. Characteristic function and variable names that survive minification because they are exported. A distinctive sequence of bytes in a well-known routine. Now walk the protection options against that list: name mangling renames the identifiers, string transforms move literals out of plain view, dead code and control-flow changes disturb the byte sequences, and the minifier removed the comments before any of this started.
The result is not a degraded match. It is no match. The scanner reports a JavaScript file containing no components it recognises, which in most report formats is indistinguishable from a file that genuinely has no dependencies.
The failure mode that costs money
An empty inventory is embarrassing. An empty vulnerability scan is dangerous, and it is the same mechanism.
Software composition analysis tools that examine built artifacts — the ones that scan a dist directory or a deployed bundle rather than reading your lockfile — use the same fingerprinting to decide which advisories apply. Run one against a protected bundle and it finds nothing to match, so it reports nothing to fix. The vulnerable dependency is still there. It is still exploitable in exactly the way it was before, because protection changes how code reads and not what it does. What changed is that your dashboard went green.
This is the general rule, and it is worth carrying beyond SBOM tooling: every control that works by reading code must run before the protection step. Dependency scanning, static analysis, secret scanning, licence compliance, your own code review. A pipeline that protects first and analyses afterwards produces clean reports about a file the analyser cannot understand, which is worse than having no reports, because a green result is acted upon.
The same argument in a different setting is the whole point of obfuscation will not stop a malicious dependency: protecting a bundle that contains a poisoned package hides the payload from your reviewers while leaving it fully operational for its author.
A pipeline order that keeps both honest
The fix is ordering, and it costs nothing once the steps are in the right sequence.
- Resolve and commit the lockfile. Everything downstream depends on a pinned graph.
- Generate the SBOM from the graph. CycloneDX or SPDX, from the lockfile, stored as a release asset. Immune to everything that follows.
- Run every code-reading control on unprotected source. Composition analysis, static analysis, secret scanning, licence review. This is your real gate.
- Bundle. Generate source maps; keep them out of the published output.
- Protect. A committed configuration and a fixed seed, so the step is documented and repeatable.
- Verify the protected build runs. Execute the test suite against the protected artifact, not only the plain one — see testing obfuscated JavaScript.
- Record what shipped. A signed release attestation over source and output hashes, published beside the SBOM.
Two steps deserve emphasis. Step three is the one people get wrong, and it is the one that matters. Step seven is what turns the SBOM from an assertion into something checkable: the inventory says what went in, the attestation records the SHA-256 of every source and output file, and together they let a reviewer confirm that the artifact in their hands corresponds to the inventory they were given — without ever needing to recover components from protected code.
When the customer runs the scan
You may do all of this correctly and still get the email, because your customer scans what you deliver rather than what you built. Their artifact scanner returns an empty component list for your bundle, and some tools go further and flag densely transformed JavaScript as suspicious in its own right — the same heuristic behind the antivirus false positives that protected code occasionally triggers.
Getting ahead of it is straightforward. Publish the graph-derived SBOM as a release asset so the inventory question has a documented answer that does not require reading your code. Say plainly in the release notes that the JavaScript is protected, and why. If you already produce a signed attestation, point at it: a customer who can verify the artifact hash against a signed record generally stops needing to fingerprint the file.
This is also the shape of the answer for regulatory inventory requirements. The Cyber Resilience Act asks for a machine-readable bill of materials covering at least top-level dependencies, and the requirement is satisfied by the graph-derived document — provided you generated it from the graph, and provided your scanning ran before the protection step rather than after it.
The short version
If your SBOM comes from your lockfile, obfuscation does not touch it, and you can say so without hedging. If it comes from scanning the artifact you ship, obfuscation destroys it, and the same mechanism silently destroys any vulnerability scan pointed at the same file. Check which one you have by running your generator against a protected build and comparing the component count to the unprotected one. Then put every code-reading control before the protection step, generate the inventory from the dependency graph, and publish a signed record of what actually shipped so nobody has to reverse-engineer the answer from the bundle.
Frequently asked questions
Does obfuscation break a software bill of materials?
It depends entirely on how the document is produced. An SBOM generated from your lockfile is derived from the dependency graph before bundling and protection ever run, so it is unaffected and remains accurate. An SBOM produced by scanning the built artifact is broken completely, because those scanners identify components by matching version strings and code fingerprints that protection removes. Same file, same release, two opposite outcomes depending on which tool wrote the document.
How do I tell which kind of SBOM my pipeline generates?
Look at what the generator is pointed at. If the command reads package-lock.json, yarn.lock, pnpm-lock.yaml or the node_modules tree, it is graph-derived and safe. If it is pointed at a dist directory, a container image, a release archive or a single bundled file, it is artifact-derived and will be affected. A quick empirical check settles it: run the generator against a protected build and against the unprotected one, and compare the component counts. A large drop tells you which kind you have.
Why does obfuscation defeat artifact scanners specifically?
Because those scanners work by pattern matching against known library signatures. They look for version strings a library assigns to itself, distinctive banner comments, characteristic function and variable names, and licence headers. Name mangling renames identifiers, string transforms move literals out of plain sight, dead code insertion and control flow changes disturb the byte sequences a fingerprint expects, and minification has usually removed the comments already. Nothing the scanner matches on survives, so it reports what looks like a bundle containing no recognisable dependencies.
Does a scanner finding nothing in a protected bundle mean the build is clean?
No, and this is the failure mode that actually costs people something. A vulnerable dependency in a protected bundle is exactly as vulnerable as it was before, because protection changes how the code reads and not what it does. The scan result changes from a list of findings to silence, which reads like success on a dashboard. Any control that works by reading code has to run before the protection step, or it is reporting on a file it cannot understand.
Where should SBOM generation and dependency scanning sit in the pipeline?
Both belong before the protection step, immediately after dependency resolution. Generate the bill of materials from the committed lockfile, run software composition analysis and any other code-reading control against the unprotected source and the resolved graph, then bundle, then protect, then record what shipped. Protection is the last transformation before packaging, and no analysis step should ever read its output expecting to recognise components.
Will a customer's own scan of our protected bundle raise questions?
Frequently, and it is easier to answer if you expect it. A customer or an integrator who runs an artifact scanner over your delivered files will get an empty or nonsensical component list, and some tools additionally flag heavily transformed JavaScript as suspicious. Ship the graph-derived SBOM alongside the release so their inventory question has a documented answer, and be ready to explain the protection step. The same disclosure prevents the antivirus conversation from starting cold.
Can we attest that our SBOM matches the artifact we shipped?
You can bind them together, which is what reviewers usually want. The bill of materials records what went into the build; a signed release attestation records the SHA-256 of every source file and every output file that came out of it. Publishing both against the same release lets someone verify that the artifact in their hands is the one the inventory describes, without needing to recover components from the protected code. A fixed seed makes the protected output reproducible, which strengthens the same claim.
Related reading