Skip to content
academia.sh

Lesson 06 / 11

Software Bill of Materials

A component inventory is measured with a set of questions: how many of twelve questions the inventory's fields can answer, how many go unanswered altogether, how many of the answered ones come with an incomplete field, and how many records the inventory gets wrong at query time depending on how often it is regenerated.

Published Updated

In this lesson

  • Generates the component inventory for three repositories and counts, for each field, how many records it is filled in for
  • Runs twelve questions against the inventory: how many are answered, how many go unanswered for lack of a field
  • Changes the inventory's generation frequency and measures how many records are wrong at query time
Contents

The previous topic counted what got scanned on every change and what slipped through. All of those checks shared one limit: they all looked at code the team itself wrote. Most of what ships to production, though, is made of components produced elsewhere, and none of those components has passed through a code review, a rule set, or a pattern scan.

This lesson turns that layer into an inventory. Throughout the course the measure is evidence, and in this lesson the unit of evidence is the question: the number of questions the inventory can answer. In the Continuous Integration and Delivery course the inventory was measured as a pipeline output — its coverage and its generation cost were counted there; that measurement is not repeated here. The object here is the inventory’s structure: which fields it carries, which questions those fields can answer, and which they cannot.

SC1. The fictional regional measurement network is three repositories — the subscriber portal, the meter intake service, and the shared library the two of them use together; the dependency tree is generated with node and is a model. SC2. The inventory carries five fields: component name, version, which repository it entered through, license, and the dependency relationship (which component brought this one in). No inventory format is named; what is measured is these five fields themselves. SC3. The resolver fails to record the license for roughly one in six records and the introduced-by edge for one in eight; the field exists, the value does not. The generator’s seed, 20260731, is visible.

Generating the Inventory

// measurement-network/inventory.mjs — component inventory for three repos is generated (a model)
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";

const BASE = join(process.env.TMPDIR ?? "/tmp", "measurement-network-sc");
mkdirSync(BASE, { recursive: true });
const rng = (seed) => () =>
  (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
const r = rng(20260731);                                     // seed is visible
const pick = (d) => d[Math.floor(r() * d.length)];

// Fictional regional measurement network: the subscriber portal and the meter intake service share a common library.
const REPOS = [["subscriber-portal", 11], ["meter-intake", 9], ["shared-library", 6]];
const POOL = Array.from({ length: 70 }, (_, i) => `component-${String(i + 1).padStart(2, "0")}`);
const VERSIONS = ["1.2", "1.9", "2.0", "2.4", "3.0", "3.6", "4.1"];
const LICENSES = ["permissive", "permissive", "permissive", "weak-copyleft", "copyleft"];

const records = [];
for (const [repo, directCount] of REPOS) {
  const taken = new Set();
  const next = () => { let a; do { a = pick(POOL); } while (taken.has(a)); taken.add(a); return a; };
  const add = (name, introducedBy) => {
    // The resolver fails to record the license 18% of the time and the introducing edge 12% of the time: the field exists, the value does not.
    records.push({ name, version: pick(VERSIONS), repo,
      license: r() < 0.18 ? "" : pick(LICENSES),
      introducedBy: introducedBy === null ? repo : (r() < 0.12 ? "" : introducedBy) });
  };
  for (let i = 0; i < directCount; i++) {
    const root = next();
    add(root, null);
    for (let k = 0, n = Math.floor(r() * 4); k < n; k++) {   // transitive children
      const child = next();
      add(child, root);
      if (r() < 0.35) add(next(), child);                    // second level
    }
  }
}
writeFileSync(join(BASE, "inventory.json"), JSON.stringify(records));

const FIELDS = ["name", "version", "repo", "license", "introducedBy"];
const filled = (a) => records.filter((k) => k[a] !== "").length;
const direct = records.filter((k) => REPOS.some(([d]) => d === k.introducedBy)).length;
console.log(`seed 20260731; repos ${REPOS.length}, pool ${POOL.length} components`);
console.log(`inventory records ${records.length} -> direct ${direct}, ` +
  `transitive ${records.length - direct}; unique components ${new Set(records.map((k) => k.name)).size}`);
console.log(`inventory fields ${FIELDS.length}: ${FIELDS.join(", ")}`);
for (const a of FIELDS)
  console.log(`  ${a.padEnd(13)} filled ${String(filled(a)).padStart(3)}/${records.length}` +
    ` (${(100 * filled(a) / records.length).toFixed(0)}%)`);
seed 20260731; repos 3, pool 70 components
inventory records 73 -> direct 26, transitive 47; unique components 48
inventory fields 5: name, version, repo, license, introducedBy
  name          filled  73/73 (100%)
  version       filled  73/73 (100%)
  repo          filled  73/73 (100%)
  license       filled  64/73 (88%)
  introducedBy  filled  67/73 (92%)

The three repositories declare 26 direct dependencies; the inventory produces 73 records. The other 47 records are transitive components no team chose. The unique component count is 48: the same component appears in more than one repository, and each appearance is a separate record.

The fill rate of the fields is the inventory’s real measure. The first three fields are filled in every record, because the resolver cannot produce the record at all without knowing them. The last two fields are different: license is empty in 9 records, introduced-by in 6. An empty field does not mean absence — the component has a license, the inventory just does not record it. This distinction shapes the whole next section.

Twelve Questions

An inventory’s value is measured not at the moment it is generated, but at the moment it is queried. The unit of measure here is also the question: if a question can be answered with the inventory’s fields, the inventory covers that question; if it cannot, the question falls outside the inventory.

SC4. The question set is fictional and consists of twelve questions; each question declares the fields it needs. A question is answered if all the fields it needs exist in the inventory; if any of those fields carries an empty record, the answer counts as incomplete.

// measurement-network/questions.mjs — twelve questions are asked of the inventory: how many are answered
import { readFileSync } from "node:fs";
import { join } from "node:path";

const BASE = join(process.env.TMPDIR ?? "/tmp", "measurement-network-sc");
const records = JSON.parse(readFileSync(join(BASE, "inventory.json")));
const FIELDS = new Set(["name", "version", "repo", "license", "introducedBy"]);
const REPOS = ["subscriber-portal", "meter-intake", "shared-library"];
const H = "component-19";                                // component queried in the examples
const names = [...new Set(records.map((k) => k.name))];
const distinct = (name, field) => new Set(records.filter((k) => k.name === name).map((k) => k[field]));

const QUESTIONS = [
  ["do we have this component", ["name"], () => (names.includes(H) ? "yes" : "no")],
  ["which versions", ["name", "version"], () => [...distinct(H, "version")].sort().join(", ")],
  ["which products does it feed into", ["name", "repo"], () => distinct(H, "repo").size + " repos"],
  ["who brought it in", ["name", "introducedBy"], () => [...distinct(H, "introducedBy")].join(", ")],
  ["are there restricted-license components", ["license"],
    () => records.filter((k) => k.license === "copyleft").length + " records"],
  ["how many components run in two versions", ["name", "version"],
    () => names.filter((a) => distinct(a, "version").size > 1).length + " components"],
  ["how many components are shared across two repos", ["name", "repo"],
    () => names.filter((a) => distinct(a, "repo").size > 1).length + " components"],
  ["direct or transitive", ["introducedBy"],
    () => records.filter((k) => REPOS.includes(k.introducedBy)).length + " direct"],
  ["is this component called at run time", ["callable"], null],
  ["when did this version enter the inventory", ["entryDate"], null],
  ["how stale is the last release, how many maintainers does it have", ["maintenance"], null],
  ["which build artifact does this inventory describe", ["build"], null],
];

const emptyCount = (field) => records.filter((k) => k[field] === "").length;
const S = [65, 19, 9, 31, 0];
const row = (s) => console.log(s.map((v, i) => String(v).padEnd(S[i])).join(""));
row(["question", "fields needed", "present", "answer", "complete"]);
let answered = 0, complete = 0;
for (const [q, needed, run] of QUESTIONS) {
  const hasFields = needed.every((a) => FIELDS.has(a));
  const empty = hasFields ? Math.max(...needed.map(emptyCount)) : 0;
  if (hasFields) { answered++; if (empty === 0) complete++; }
  row([q, needed.join("+"), hasFields ? "yes" : "NO",
    hasFields ? run() : "-", hasFields ? (empty ? `${empty} records empty` : "complete") : "-"]);
}
console.log("");
console.log(`${answered} of twelve questions are answered with the inventory's fields, ` +
  `${QUESTIONS.length - answered} go unanswered for lack of a field`);
console.log(`of the ${answered} answered questions, ${complete} are complete, ${answered - complete} ` +
  `come with an incomplete field (license empty in ${emptyCount("license")} records, introducedBy empty in ${emptyCount("introducedBy")} records)`);
question                                                         fields needed      present  answer                         complete
do we have this component                                        name               yes      yes                            complete
which versions                                                   name+version       yes      1.2, 3.6                       complete
which products does it feed into                                 name+repo          yes      2 repos                        complete
who brought it in                                                name+introducedBy  yes      component-01, component-23     6 records empty
are there restricted-license components                          license            yes      10 records                     9 records empty
how many components run in two versions                          name+version       yes      17 components                  complete
how many components are shared across two repos                  name+repo          yes      20 components                  complete
direct or transitive                                             introducedBy       yes      26 direct                      6 records empty
is this component called at run time                             callable           NO       -                              -
when did this version enter the inventory                        entryDate          NO       -                              -
how stale is the last release, how many maintainers does it have maintenance        NO       -                              -
which build artifact does this inventory describe                build              NO       -                              -

8 of twelve questions are answered with the inventory's fields, 4 go unanswered for lack of a field
of the 8 answered questions, 5 are complete, 3 come with an incomplete field (license empty in 9 records, introducedBy empty in 6 records)

The first four questions are the four most commonly asked when discussing an inventory, and all four are answered: the component is present, it runs in two versions, it has entered two repositories, two separate components brought it in. Five fields cover eight questions; the inventory’s coverage ratio is 8/12.

The four unanswered questions belong to the same class: none of them is a question the inventory answers wrong; all four are questions the inventory cannot be asked. Whether a component gets called at run time requires call resolution, when a version entered the inventory requires a date field, a package’s maintenance status requires an external record, and which build artifact the inventory describes requires a link. These are addable fields, and until they are added, the question cannot be asked. An inventory’s limit is the set of fields it carries.

The truly dangerous column is the last one. Of the eight answered questions, three are answered with an incomplete field, and the incompleteness does not show up in the shape of the answer. The answer to “are there restricted-license components” is 10 records; but the license field is empty in 9 records, so the real number is somewhere between 10 and 19. The inventory says “10,” not a range. In the same way, “who brought it in” returns two names, even though the introduced-by edge is never written in six records — those six components sit in the inventory with a broken chain. An empty field does not show up as a gap; it shows up as a small number.

When Was the Inventory Generated

The fourth unanswered question opens up the next measurement: which build artifact does the inventory describe? Without that link, the inventory’s accuracy no longer rests on description — it rests on freshness.

SC5. The sequence of twenty builds is fictional; each build changes between two and five records, a quarter of the time dropping a component, the rest of the time upgrading its version. The 41-second pipeline time per generation is given as data, not measured; the seed 551903 is visible.

// measurement-network/freshness.mjs — how many records the inventory gets wrong at query time, depending on generation frequency
import { readFileSync } from "node:fs";
import { join } from "node:path";

const BASE = join(process.env.TMPDIR ?? "/tmp", "measurement-network-sc");
const initial = JSON.parse(readFileSync(join(BASE, "inventory.json")));
const rng = (seed) => () =>
  (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
const r = rng(551903);                                   // seed is visible
const VERSIONS = ["1.2", "1.9", "2.0", "2.4", "3.0", "3.6", "4.1"];
const key = (k) => `${k.repo}|${k.name}`;

// Fictional build sequence: twenty builds. Each build changes a few records; the changes are fictional too.
const BUILDS = 20, GENERATION_SECONDS = 41;              // time is given as data, not measured
const state = new Map(initial.map((k) => [key(k), { ...k }]));
const history = [];
for (let b = 1; b <= BUILDS; b++) {
  const records = [...state.values()];
  for (let i = 0, n = 2 + Math.floor(r() * 4); i < n; i++) {
    const k = records[Math.floor(r() * records.length)];
    if (r() < 0.25) state.delete(key(k));                // component dropped
    else k.version = VERSIONS[Math.floor(r() * VERSIONS.length)];
  }
  history.push(new Map([...state].map(([a, k]) => [a, { ...k }])));
}

// Three generation frequencies. The query comes after every build; the inventory is whichever version was last generated.
const FREQUENCIES = [["every build", 1], ["every 5th build", 5], ["every 20th build", 20]];
const S = [22, 13, 15, 16, 0];
const row = (s) => console.log(s.map((v, i) => String(v).padEnd(S[i])).join(""));
row(["generation frequency", "generations", "pipeline time", "wrong records", "worst case"]);
for (const [name, step] of FREQUENCIES) {
  let totalWrong = 0, worst = 0, generated = 0;
  for (let b = 0; b < BUILDS; b++) {
    if (b % step === 0) generated++;
    const held = history[Math.floor(b / step) * step];    // the inventory currently on hand
    const now = history[b];
    let wrong = 0;
    for (const [a, k] of now) if (!held.has(a) || held.get(a).version !== k.version) wrong++;
    for (const a of held.keys()) if (!now.has(a)) wrong++;  // record that no longer exists
    totalWrong += wrong;
    worst = Math.max(worst, wrong);
  }
  row([name, generated, `${generated * GENERATION_SECONDS} s`,
    `average ${(totalWrong / BUILDS).toFixed(1)}`, `${worst} records`]);
}
console.log("");
console.log(`twenty builds: records ${initial.length} -> ${history[BUILDS - 1].size}; ` +
  `generation costs ${GENERATION_SECONDS} s as given data, seed 551903`);
generation frequency  generations  pipeline time  wrong records   worst case
every build           20           820 s          average 0.0     0 records
every 5th build       4            164 s          average 6.2     17 records
every 20th build      1            41 s           average 22.1    41 records

twenty builds: records 73 -> 56; generation costs 41 s as given data, seed 551903

An inventory generated on every build never gets a query wrong and costs 820 seconds of pipeline time. An inventory generated every fifth build costs 164 seconds and gets an average of 6.2 records wrong; at its worst, 17 records are wrong. An inventory generated every twentieth build drops to 41 seconds and gets an average of 22.1 records wrong — out of a set of fifty-six records.

The three rows are three settings of the same gate, and pipeline time and wrong-answer count are not inversely proportional — they are stepped: when frequency drops to a fifth, time drops to a fifth, but wrong records jump from zero to 6.2. The first step is the most expensive one, because any number above zero turns the inventory’s answer into an estimate.

The last row also writes down the measurement’s own limit: over twenty builds, the record count drops from 73 to 56. A stale inventory is not just incomplete, it is also excessive — it shows seventeen components as still running that no longer are. These two errors do not cancel out; both are counted in the same column, and both lead to the same conclusion: the number the inventory gives does not describe what it claims to describe.

Summary

  • The three repositories declare 26 direct dependencies; the inventory produces 73 records and 48 unique components. 47 records are transitive components no team chose.
  • Three of the inventory’s five fields are filled in every record; license is empty in 9 records, introduced-by in 6. An empty field is not absence, it is an unrecorded value.
  • 8 of twelve questions are answered with the inventory’s fields, 4 go unanswered altogether: whether it is called, its entry date into the inventory, the package’s maintenance status, and which build artifact the inventory belongs to. An inventory’s limit is the set of fields it carries.
  • 3 of the 8 answered questions are answered with an incomplete field, and the incompleteness does not show up in the shape of the answer: the real value behind a “10 records” answer is somewhere between 10 and 19.
  • Generating on every build takes 820 seconds and never gets a query wrong; generating every fifth build takes 164 seconds and gets an average of 6.2 records wrong; generating every twentieth build drops to 41 seconds and gets an average of 22.1 records wrong. A stale inventory is not just incomplete — it is also excessive, by as many as the seventeen dropped components.

Next Step

The inventory has been generated, its coverage counted, and the cost of its freshness written down. The fourth unanswered question is still sitting there: which build artifact does this inventory describe? The inventory is a separate file; another inventory can be dropped next to it, a record can be altered, it can be paired with an entirely different artifact. None of the 73 records says that the bytes it describes are truly those bytes.

The next lesson establishes that link with a real key pair and splits a single question into two: what does a signature prove, and what does it not prove? For an artifact that passes verification, how many separate claims can be made, and how many of them are claims the signature actually rests on — and once that key is compromised, how many artifacts become suspect retroactively?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close