TT Lab
Get started
Learn Learning paths Courses

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

Get the boundaries right and keep only today's fixes

Continue in TT Lab

Goal

You check an SBOM against vulnerability data to pick out only what is really affected, then split on two axes, "is there a fixed version" and "does it reach users", and write a policy gate as a program.

Why it matters

The first time you run a scanner, hundreds of red lines come out. If you send that list as is to the development team, nothing happens — because everyone knows it cannot all be fixed. There are two places where the list shrinks. First, the range judgment must be accurate. If false positives that flag a version that has already been fixed get mixed in, trust in the whole list collapses. Second, things that have a fixed version and things that do not are different matters. If there is a fixed version, you can upgrade today, and if there is none, you write down a mitigation and a deadline and manage it with a waiver. If you mix the two, the meeting revolves only around "severity is high / low" and nothing changes.

Steps

  1. Read /opt/fixtures/sbom/feeds/advisories.json and write the shape of the data to /root/adv/01-feed.json.
  2. Flatten the components of /opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json and write them to /root/adv/02-components.json.
  3. Build a version comparer in /root/adv/semver_cmp.py.
  4. Get the boundary values right and write only the affected ones to /root/adv/04-findings.json.
  5. Move only the ones that have a fixed version to /root/adv/05-upgrade-plan.json.
  6. Separate what reaches users from what ends in the build in /root/adv/06-shipped.json.
  7. Write the policy in /root/adv/gate-vuln.py and leave the result in /root/adv/07-verdict.json.
  8. Apply the waivers in /root/adv/waivers.json and judge again into /root/adv/08-verdict.json.

Notes

Start by reading what the data says

Read /opt/fixtures/sbom/feeds/advisories.json and save advisories, with_fixed, without_fixed, and packages to /root/adv/01-feed.json. with_fixed is the number of advisories that have at least one fixed in the events of their ranges, without_fixed is the rest, and packages is an array of the package names in affected in alphabetical order.

OSV's events is a timeline that records 'introduced / fixed / affected up to here' in chronological order. The existence of advisories with no fixed is itself the subject of this lab.

Set up the list of what to check

From the components of /opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json, keep only name, version, and scope and save them to /root/adv/02-components.json as an array sorted by name. Copy scope exactly as written in the SBOM.

This list is the left side of the comparison and the vulnerability data is the right side. If you flatten the left side first, all the later steps get easier.

Pin down in code why versions must not be compared as strings

Create a compare(a, b) function in /root/adv/semver_cmp.py. Both arguments are strings of the form X.Y.Z, and it returns -1 if a is smaller, 0 if equal, and 1 if larger, exactly. You must compare the three parts each as integers.

If you compare as strings, "1.9.0" comes out greater than "1.10.0". The grader tries several cases directly, including such pairs.

Get the boundary values exactly right

Using the comparison in /root/adv/semver_cmp.py, check /opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json against /opt/fixtures/sbom/feeds/advisories.json and save only what is actually affected to /root/adv/04-findings.json as an array sorted by id. Each entry has id, package, version, severity, scope, fix_available, and fixed_in. For severity, copy severity[0].score, and for scope, copy the value from the SBOM. fixed_in is the fixed value that closes the range that version belongs to, or null if there is none, and fix_available is the boolean of that. The "0" in introduced is a special value meaning earlier than any version.

A range is introduced or later and before fixed — a version equal to fixed has already been fixed. last_affected is the ceiling confirmed up to that day, so that value itself is still affected. An advisory can also have two or more ranges.

Move only what can be fixed today into a plan

From /root/adv/04-findings.json, pick only the entries where fix_available is true and save them to /root/adv/05-upgrade-plan.json as an array sorted by package. Each entry has package, from, to, and advisory, where to is fixed_in and advisory is the advisory id.

'Is there a fixed version' is a different axis from severity. The meeting ends only once you first separate what you can upgrade today from what you cannot.

Separate what reaches users from what ends in the build

Save shipped and dev_only to /root/adv/06-shipped.json. Both are arrays of advisory ids in alphabetical order, and findings whose scope is excluded go to dev_only.

A flaw in a development-only dependency does not ship in the deployment artifact. That does not mean you can ignore it, but if you put it in the same bucket as what reaches users, the truly urgent items get buried.

Write the policy as a program, not in human language

Create /root/adv/gate-vuln.py. When called as python3 /root/adv/gate-vuln.py <SBOM> <자료> (the two arguments are the SBOM file and the vulnerability data file), it prints to standard output one JSON object containing blocking (an array of the blocking advisory ids in alphabetical order) and verdict ("pass" or "fail"), and exits with 1 if there is anything to block and 0 if not. The policy is 'findings that ship in the deployment (scope required) and have severity HIGH or CRITICAL'. Then save the output of python3 /root/adv/gate-vuln.py /opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json /opt/fixtures/sbom/feeds/advisories.json to /root/adv/07-verdict.json.

If the policy exists only in a document, each person reads it differently. The grader also calls this script with other inputs — on a list with nothing to block, it must exit with 0.

Grant waivers only to what cannot be fixed

Write three waivers to /root/adv/waivers.json. Each entry has id, owner, expires, and reason, and the three are LABHUB-2026-0002 (expires 2026-12-31), LABHUB-2026-0001 (expires 2026-12-31), and LABHUB-2026-0007 (expires 2026-08-31), with payments-platform as the owner of all of them. Then apply the waivers with the reference date 2026-09-11 and save as_of, waived, rejected, blocking, and verdict to /root/adv/08-verdict.json. A waiver applies only to findings that have not expired on the reference date and whose fix_available is false, and the rejected ones are listed by id and reason ("expired" or "fix_available") sorted by id.

A waiver is the field that records 'we cannot fix it yet', not where you write 'we do not want to fix it'. If a waiver works even when there is a fixed version, the gate is decoration from that day on.