The Build Was Green — So Who Put That Library In?
Six red lines — which one can you fix today?
In one line
What makes scan results useful is not severity but two judgments — whether this version truly falls in that range, and whether there is a fixed version.
Why this matters
The first time you run a vulnerability scanner, hundreds of red lines come out. If you send that list as is to the development team, nothing happens. Both the sender and the receiver know that it cannot all be fixed. So the list stays as an attachment, gets generated again next month, and again nothing happens.
There are only two places where the list shrinks. The first is false positives. If a version that has already been fixed still gets flagged, even one or two such entries destroy trust in the whole list. Once "we told you last time it wasn't" has been said, nobody opens it from then on. The second is whether a fixed version exists. If a version to upgrade to is out, you can upgrade today; if there is none yet, you write down a mitigation and a deadline and keep watching. These two are completely different kinds of work, yet if you rank by severity alone, they get mixed in the same bucket.
How it works
The format for writing vulnerability data in a machine-readable way is the OSV schema. The core is the affected
array, and ranges inside it records a timeline of versions. Each events entry is one of
four: introduced (it came in here), fixed (it was fixed here),
last_affected (confirmed affected up to here), and limit (look only up to here).
The documentation explicitly forbids putting two kinds in one event object, and
states that the events array must contain at least one introduced
(OSV schema).
There are three places where practitioners often go wrong.
경계 구간은 introduced 이상 fixed 미만이다.
fixed 와 같은 버전은 이미 고쳐진 것이라 걸리면 안 된다.
last_affected fixed 가 없을 때 쓰는 천장이라 그 값 **자체는 아직 영향받는다.**
경계의 포함 여부가 fixed 와 반대다. 문서는 가능하면 fixed 를 쓰라고
강하게 권한다 — last_affected 는 거짓 음성을 만들 여지가 있다.
여러 구간 한 권고에 구간이 둘 이상일 수 있다. 1.x 에서 고쳐지고 2.1 에서 다시
들어온 경우가 그렇다. 첫 구간만 보고 끝내면 판정이 뒤집힌다.
On top of that, version comparison itself is a trap. The OSV SEMVER range type says to follow SemVer 2.0 ordering without a leading v,
but if you compare as strings, "1.9.0" comes out greater than "1.10.0".
You must compare numbers, not digits. For ecosystems that do not enforce SemVer, there is a separate
ECOSYSTEM type, and in that case the sorting rules differ by ecosystem.
Finally, you must separate where it ships. CycloneDX expresses this with a component's
scope, and marks components that do not reach any runtime call path as excluded
(CycloneDX 1.6 JSON). Even at the same severity, something that ships in the deployment
artifact and something that ends on the build machine are different matters.
What it looks like in the field
The most common failure is the waiver becoming a master key. It starts as "we can't fix it now, so let's write a deadline and move on", but after a few months even things that have a fixed version get passed through as waivers. From that day on, the gate is decoration. A waiver keeps its character only if it is given solely to things that cannot be fixed, and always comes with a deadline, an owner, and a mitigation.
The second is reading the reference date with date. If you judge waiver expiry by today's date, a build that passed yesterday
fails today. That behavior is correct in itself, but for a check that must be reproducible,
the reference date has to be an input so that the same input gives the same answer.
The third is data quietly going stale. On a network with no internet access, vulnerability data is mirrored internally, and without a mechanism to notice that the mirror has stopped, scans keep showing green. You cannot tell whether it is green because nothing matched or because the data is three months old. So it is safer to record a generation time in the data and make the gate check that time as well.
And one more thing — a scanner saying "affected" is different from "exploitable". If we never call the flawed function, the real risk is low. The format that records this distinction in a machine-readable way is VEX, and CycloneDX lets a BOM express vulnerability information together with its exploitability (CycloneDX overview). However, this judgment comes only from a person reading the code, so touching it before the list is tidied up just wastes time.
What you will do in the next lab
You build a version comparer yourself and run the boundary values through it, then check against the OSV timeline and pick out only what actually matches. Then you split on two axes, "is there a fixed version" and "does it reach users", and write a policy gate as a program, pinning down in code how far waivers should reach.