The Build Was Green — So Who Put That Library In?
Count all twenty-five by hand and write them as an SBOM
Goal
From a single lockfile, you count "what is in there" yourself, write it out as an SBOM in CycloneDX format, and trace the path by which a package that nobody chose came in.
Why it matters
The first question after an incident is "do we use that?" An organization that cannot answer this question within a few hours burns days even when the answer is "no". The way to get the answer fast is not to search after the incident but to generate the list with every release. Yet most of the list is not something we chose — seven dependencies we wrote by hand pull in twenty-five. You also have to separate whether those twenty-five actually ship in the deployment or are used only in the build, because the list to fix and the urgent list are different lists.
Steps
- Extract the direct dependencies from
/opt/fixtures/sbom/release/package.jsonand write them to/root/sbom/01-direct.json. - Count everything installed in
/opt/fixtures/sbom/release/package-lock.jsonand write it to/root/sbom/02-installed.json. - Separate production from development-only and write it to
/root/sbom/03-scope.json. - Trace the path by which
sock-ttlcame in and write it to/root/sbom/04-why.json. - Create
/root/sbom/05-bom.jsonin CycloneDX 1.6 format. - Pin the digest of the artifact in
/root/sbom/06-subject.json. - Keep only what ships in the deployment and create
/root/sbom/07-runtime-bom.json. - Compare against the previous release and create
/root/sbom/08-diff.json.
Notes
- Do all the work under
/root/sbom. First runmkdir -p /root/sbom. - The materials are in
/opt/fixtures/sbom.MATERIAL-CARD.mdexplains what each item is and why it is there. - The package names and advisory numbers are made up for this lab. They are not the circumstances of real packages.
- This image has no syft or grype, and the Pod has only DNS open. Reading the lockfile directly is the approach of this lab, and that is also how you can see what a tool skips.
- Common mistakes — counting the root entry of the lockfile (the empty-string key), which makes the count one too high,
and wrongly counting entries without the
devflag as development-only. - Do not make up numbers. The grader recomputes from the same materials and compares.
Count what we chose directly first
Read /opt/fixtures/sbom/release/package.json and save the result to /root/sbom/01-direct.json. runtime is an array of the names in dependencies in alphabetical order, and dev is an array of the names in devDependencies in alphabetical order.
package.json lists only "what we chose". Version ranges (^) are attached, so extract only the names. Everything that was installed is not here.
Count what was actually installed
Read /opt/fixtures/sbom/release/package-lock.json and save lockfile_version, total, direct, and transitive to /root/sbom/02-installed.json. total is the number of entries in the packages map excluding the root entry (the key that is an empty string), direct is the number of direct dependencies counted in step 1, and transitive is the rest.
In lockfileVersion 3, packages is a map keyed by install location, and the root project goes in under the empty-string key. If you do not exclude the root when counting, you get one extra.
Separate what ships in the deployment from what is used only in the build
Save runtime, dev, and dev_names to /root/sbom/03-scope.json. An entry in the lockfile with dev set to true is development-only. runtime and dev are counts, and dev_names is an array of the development-only package names in alphabetical order.
The npm docs say dev is true only when the package is 'strictly part of the devDependencies tree'. An entry with no flag at all is on the production side.
Trace how a package nobody chose came in
Follow the dependencies in the lockfile to find why sock-ttl was installed, and save package, version, direct, depth, and paths to /root/sbom/04-why.json. paths is an array of arrays of names starting from the root, and the first element is paygate. depth is the number of edges in that path.
The dependencies in each lockfile entry are the edges. Descend breadth-first from the root and see where that name first appears.
Write the list in CycloneDX format
Save the SBOM to /root/sbom/05-bom.json. bomFormat is "CycloneDX", specVersion is "1.6", version is 1, metadata.component is paygate 1.4.2 (type application, purl pkg:npm/paygate@1.4.2), and components holds the 25 installed packages sorted by name. Each entry has type "library", name, version, purl (pkg:npm/name@version), and scope, where scope is "excluded" if it is development-only and "required" otherwise.
The reason the bomFormat value is pinned to "CycloneDX" is that BOM files have no naming convention. You must be able to tell what format a file is just by looking at it.
Pin down which artifact the list belongs to
Compute the SHA-256 of /opt/fixtures/sbom/release/paygate-1.4.2.js and save name (the file name only), version, bytes, and sha256 to /root/sbom/06-subject.json. sha256 is 64 lowercase hexadecimal characters.
If the list does not name its subject, nobody can prove 'what the list is of'. Hash the file bytes with hashlib.sha256.
Keep only what ships in the deployment artifact
Remove the entries whose scope is excluded from /root/sbom/05-bom.json and save the result to /root/sbom/07-runtime-bom.json. The format is the same as 05-bom.json, and only components gets shorter.
A vulnerability in a development-only dependency does not reach users. If you count the two together, the number to fix is inflated and the truly urgent items get buried.
Count what changed from the previous release
Compare /opt/fixtures/sbom/sbom/paygate-1.4.1.cdx.json with /root/sbom/05-bom.json and save added, removed, changed, and unchanged to /root/sbom/08-diff.json. added and removed are arrays of objects containing name and version (sorted by name), changed is an array of objects containing name, from, and to (sorted by name), and unchanged is the number of entries present on both sides with the same version.
This is the question that release notes do not answer — did anything grow that nobody changed? Look separately at the difference and the intersection of the name sets.