TT Lab
Get started
Learn Learning paths Courses

The Build Was Green — So Who Put That Library In?

Count all twenty-five by hand and write them as an SBOM

Continue in TT Lab

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

  1. Extract the direct dependencies from /opt/fixtures/sbom/release/package.json and write them to /root/sbom/01-direct.json.
  2. Count everything installed in /opt/fixtures/sbom/release/package-lock.json and write it to /root/sbom/02-installed.json.
  3. Separate production from development-only and write it to /root/sbom/03-scope.json.
  4. Trace the path by which sock-ttl came in and write it to /root/sbom/04-why.json.
  5. Create /root/sbom/05-bom.json in CycloneDX 1.6 format.
  6. Pin the digest of the artifact in /root/sbom/06-subject.json.
  7. Keep only what ships in the deployment and create /root/sbom/07-runtime-bom.json.
  8. Compare against the previous release and create /root/sbom/08-diff.json.

Notes

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.