Application security
Triaging dependency vulnerabilities: what to do with 400 open alerts
A scanner dashboard nobody opens is worse than no scanner. How to sort advisories by whether they can actually be triggered, and how to record the ones you defer.
Switch on dependency scanning against a mid-sized application for the first time and the result is rarely useful. It is a number, usually in the hundreds, attached to a dashboard that a developer opens once and never opens again. The alerts are not wrong. They are simply undifferentiated, and an undifferentiated list of four hundred things produces exactly the same behaviour as a list of zero things.
Triage is the work of converting that list into a small number of items somebody will actually do this week, plus a much larger number of items you have decided not to do and can explain. Both halves matter. The second half is what a dependency and supply-chain audit spends most of its time on, and it is the half that teams skip.
Why the wall exists
The count is high for reasons that have nothing to do with your engineering standards. Modern package ecosystems resolve deep trees, so a project with forty declared dependencies routinely installs several hundred packages, most of them chosen by somebody else. Advisories accumulate against those packages forever, while your lockfile only moves when someone updates it.
On top of that, matching is deliberately generous. A scanner compares an installed version against the affected range recorded in a database such as the National Vulnerability Database or OSV, and reports a hit whenever the range overlaps. It has no view of whether your application ever calls the affected function, whether the vulnerable feature is enabled, or whether an attacker can influence the input that reaches it. Generosity is the correct default for a tool that does not know your code. It is not a prioritisation.
Severity is not priority
The score attached to an advisory is usually a CVSS base score, and a base score describes the intrinsic properties of a flaw in the abstract. It does not know that the affected package is a build-time formatter that never runs in production, or that the vulnerable parser is behind an authentication wall that only your own staff pass. Two identical 9.8s can be an emergency and a non-event in the same repository.
Two public signals sharpen the picture cheaply. The Known Exploited Vulnerabilities catalogue lists issues with confirmed exploitation in the wild, which is a categorically different fact from a high severity rating. EPSS estimates the probability that an issue will be exploited in the near term. Neither replaces reading the advisory, and both are better ordering keys than the base score alone.
A severity score tells you how bad the flaw is for somebody. Triage is the question of whether that somebody is you.
Reachability is the question that does the work
For each advisory that survives the first sort, there is one question worth answering properly: can an input that an attacker controls travel from an entry point in your application to the vulnerable code path? Answering it takes a few minutes per advisory once you know how, and it removes most of the list.
- 01Read the advisory for the affected function or feature, not just the affected version range. Most advisories name something specific: a parser, an option, a serialisation mode, a template helper.
- 02Search your codebase for calls into that surface, directly and through the intermediate package that pulled the dependency in. A transitive dependency you never call yourself is often used by its parent in exactly one narrow way.
- 03Establish whether the affected code runs in production at all. Development tooling, test harnesses, build plugins and documentation generators are all in your lockfile and none of them are exposed to a user.
- 04Establish whether attacker-influenced data reaches it. A YAML parser that only ever reads a file your own deployment writes is a different proposition from one that parses an uploaded document.
- 05Check the preconditions the advisory states. Many require a configuration flag, an optional feature, or a platform you do not run on.
The output is a verdict per advisory rather than a score: exploitable in our context, not reachable, not in production, or unclear. The unclear pile is the honest one, and it should be small enough to escalate.
Record the decisions you do not act on
The most valuable artefact from a triage pass is the written reasoning behind everything you chose to defer. Without it, the same three hundred advisories get re-triaged from scratch by a different engineer next quarter, and the decision quality resets to zero each time.
A deferral note needs four things: the advisory identifier, the verdict, the reason in one sentence, and the condition that would reverse it. That last field matters most, because reasons expire. "Not reachable, the affected parser is never called" stops being true the moment somebody adds a feature that calls it. Machine-readable formats exist for communicating exactly this to customers, and they sit alongside a software bill of materials in formats such as CycloneDX.
Ordering what remains
Once the list is short, order it by risk removed against effort required rather than by severity. In practice that produces three groups, and they behave differently.
Patch releases inside the current major version
These are close to free. Batch them, run the suite, ship them together. If the suite is not trustworthy enough for that to be a routine action, the dependency backlog is a symptom and the test coverage is the actual finding.
Major version jumps
These carry breaking changes and deserve to be scheduled as work with an estimate attached, not slipped into a Friday. They are also where transitive pins fight back, because one package will insist on the old version of a shared dependency.
Advisories with no fixed version
An unmaintained package with an open advisory and no release is a different decision entirely: replace it, vendor and patch it, or accept it with a documented compensating control. None of those three is an upgrade, and treating it as one is how an item sits on a board for a year.
Keeping the list short afterwards
- Fail the build only on the categories you have decided are unacceptable, and let everything else raise a pull request instead. A pipeline that goes red on a low-severity advisory in a build-time tool teaches people to bypass the gate.
- Automate the routine patch updates so the boring half never accumulates. The backlog that becomes unmanageable is the one made mostly of updates nobody objected to.
- Give the advisory inbox a named owner with time to read it. An alert stream with no owner is a queue with no consumer.
- Re-run a full reachability pass after anything that reshapes the tree, such as a framework major version or absorbing a service from another team.
There is a regulatory tailwind here as well. Knowing what you ship and being able to say quickly whether a published advisory affects it is exactly the capability the Cyber Resilience Act reporting clock assumes you have, and it is the same evidence an enterprise buyer asks for when they ask about your security posture. The work is unglamorous either way, and it is much cheaper done before somebody is waiting on the answer.
Quick answers
Common questions
Should the build fail on every high-severity dependency alert?
No, and doing so reliably backfires. A gate that blocks a release over an advisory in a build-time formatter trains the team to bypass the gate, and once bypassing is normal the gate protects nothing. Fail the build on the narrow set of conditions you have decided are genuinely unacceptable, such as a known-exploited issue in a production dependency, and route everything else into a pull request or a review queue with an owner.
What is reachability analysis, in practical terms?
It is checking whether input an attacker can influence is able to travel from one of your entry points into the specific function the advisory names. It is a question about your code rather than about the package, which is why a scanner cannot answer it. The practical method is to read what the advisory actually affects, search for calls into that surface, and establish whether the code runs in production and whether untrusted data reaches it.
Do we need a software bill of materials?
You need the inventory it represents regardless, because you cannot judge whether a new advisory affects you without knowing what you install. Whether you need it in a specific machine-readable format depends on your customers and your sector; that is a contractual question rather than an engineering one. Producing the inventory is the hard part, and exporting it in a standard format afterwards is comparatively trivial.
How often should a full triage pass be repeated?
Annually for most products, and immediately after any change that reshapes the dependency tree. Between passes, automated alerts on newly published advisories are enough provided somebody owns that inbox and the deferral notes from the last pass are still readable. The failure mode is not an infrequent review; it is a review whose reasoning was never written down, so the next one starts from nothing.