← All guides

Vulnerability prioritization

Cross-cutting
On this page

In plain terms

Run every security control on a real system and you won't get a tidy report, you'll get thousands of findings, a large share labeled "high" or "critical." Vulnerability prioritization is the discipline of turning that flood into a short, ordered list of what deserves attention first. It's the hardest and most valuable problem in application security, because effort is finite and most findings will never be exploited.

Why it matters

When everything is critical, nothing is. A backlog of 2,000 "criticals" isn't a security program, it's a way to guarantee the actually dangerous ones get lost in the noise. Teams either burn out chasing low-value findings or, more often, tune the whole thing out. Good prioritization is what makes security actionable.

The inputs

Severity alone (CVSS) is not priority. A useful ranking folds in context the scanner can't see, and these inputs line up neatly with the CVSS dimensions (base, environmental, threat):

The inputWhat it measures
SeverityHow bad the issue is in the abstract. (the CVSS base score)
ExposureIs the affected component internet-facing, authenticated-only, internal, or fully restricted? A flaw nobody can reach is a very different risk from one on your login page. (environmental)
CriticalityThe business impact if the component fails, from an outage or data loss down to limited impact that is easily worked around. (environmental)
ExploitabilityIs it being exploited? (threat. See EPSS & KEV)
The two facts a component declares Exposure is how reachable a component is: public, anyone on the internet, no sign-in; authenticated, on the internet, behind a login; internal, only from inside your network or VPN; restricted, inside your network and locked down further. The rings are brightest at the edge, where reach is widest. Criticality is the business impact if it fails: critical, an outage or data loss for the business; important, degraded service, but no outage; supporting, limited impact, easily worked around. EXPOSURE · HOW REACHABLE IT IS public the internet, no sign-in authenticated behind a login internal your network or VPN restricted locked down further CRITICALITY · WHAT HAPPENS WHEN IT FAILS critical an outage or data loss for the business for example, checkout or sign-in important degraded service, but no outage for example, search or notifications supporting limited impact, easily worked around for example, an internal wiki
The wider a component's reach and the greater the business impact if it fails, the higher a flaw on it ranks. Both are declared next to the code, because no scanner can see either.

Multiply those together and the list reorders dramatically: a "medium" on a public, business-critical service jumps above a "critical" on an internal batch job.

The same flaw, four components, four answers Rows are the tier a component falls into once its exposure and criticality are known. Columns are the severity of the flaw itself. A high-severity flaw is P1 on a public, business-critical component and P3 on an internal supporting one. Of the forty-eight combinations of exposure, criticality and severity, nine reach P1. THE FLAW'S OWN SEVERITY THE COMPONENT, FOR EXAMPLE critical high medium low C1 public, critical P1 P1 P2 P3 C2 internal, critical P1 P2 P2 P3 C3 internal, important P2 P3 P3 P4 C4 restricted, supporting P2 P3 P4 P4 Of the 48 combinations of exposure, criticality and severity, 9 reach P1. A band a third of findings reach is not a queue.
Read a row for one component and the same flaw changes band as you cross it. Read a column for one flaw and it changes band as the component does. Neither axis alone decides anything.

What moves a finding along the severity axis

The grid above has two axes and you supply one of them. Exposure and criticality are declared once per component, in your descriptor, and they do not change from one finding to the next. That is the whole point of declaring them: they are the half of the answer no scanner can work out.

The other axis is how bad the flaw is, and that is not simply what the scanner called it. Signals can move a finding along it before it reaches the grid, and each takes what the one above it produced.

1↑
Control floorA kind of finding can have a minimum its control insists on. Secret scanning has one: a leaked credential is never rated below high, whatever score the scanner gave it.
built in
2↑
Being exploitedOn the KEV catalog goes straight to critical. A high EPSS score raises it one band. Somebody is using this, or is likely to.
CISA · FIRST
3↑
The dependency itselfA malicious package goes straight to critical. One that its publisher has deprecated rises a band. This is about what you depend on rather than about the flaw.
OSSF · the publisher
4↓
Nothing can reach itAnalysis followed the call paths and found none that arrive at the vulnerable code. Ranks down a band.
govulncheck

None of them rewrites what the scanner reported. The report shows the scanner's own rating beside the band it was ranked at, and names the signal that moved it, so the distance between the two is always visible.

Whatever severity comes out of those four is what enters the grid above, on the column axis. Your component's exposure and criticality decide the row. Priority is where they meet.

Here is one real finding making that trip, from a scan of our public sandbox:

CVE-2020-11023 · jquery 1.8.3 · component storefront
The scanner rated itmedium
↑ on CISA's list of vulnerabilities being exploitedcritical
crossed with what the component ispublic × important
PriorityP1 was P2 with no signals at all

Three things are worth reading off that. The scanner's own rating is still medium and the report still shows it. The band moved because something true about the world met something true about the component. And neither half would have been enough alone: the same critical on an internal, supporting component lands at P2.

That finding is also in a package its publisher has deprecated, which on its own would have taken it to high. Being exploited takes it to critical, so that is the one that decides and the one the report names. Which is the first of the two collision rules, on a real finding.

Secret scanning ignores the row

Secret scanning is the exception, and it is worth knowing because it is the only place Draugr overrides something you declared. A leaked credential is ranked as though the component were the most exposed and most critical one you have, whatever you classified it as. The report says so on the finding: a leaked credential is high priority wherever it is found.

The reason is that the other four inputs assume a flaw's reach is bounded by the component it is in, and a credential is not bounded by anything: it is valid wherever it is valid, in a cloud account, a registry, an artifact store. Git history is also read by more people than most services are reachable by, so internal can understate who can get hold of it.

Without that override the report contradicts itself. The floor says a credential is serious, exposure and criticality damp it back to P3, and the same six lines tell you nothing is urgent and that you cannot ship.

When two of them land on the same finding

Two that both raise it: the higher one wins. Being exploited and being a bad dependency answer different questions, and neither is automatically the stronger claim, so the larger move decides and the report names which signal made it.

One that raises against one that lowers: it depends what the raise is claiming. "Nothing can reach it" is a statement that something is absent, and absences are the easiest thing to be wrong about. A call path can be created by reflection, by dynamic dispatch, or by somebody writing one line tomorrow. So a signal saying this is dangerous right now beats it, and a signal saying something else does not.

SignalAgainst "nothing can reach it"Why
On the KEV catalogwinssomebody is exploiting it, and a missed path is the expensive mistake
High EPSSwinsthe same question, answered by prediction rather than by observation
Malicious packagewinsa hostile dependency is a problem whichever of its functions you call
Deprecated packageyieldsit says who maintains the package, not whether this flaw can fire in your code

That last row is worth reading twice, because it is the one people get wrong. A vulnerability nothing can reach, in a package whose publisher has walked away, still ranks down. The deprecation is a good argument for replacing that dependency and it stays visible in the report. It is not evidence that this particular flaw is live.

Where it fits

Prioritization sits after your controls run and before your team acts, it's the layer that consumes findings from SCA, SAST, image scanning, and the rest, and produces the ranked worklist. It's also where gating decisions live: fail a build only on what matters, not on every warning.

Common pitfalls

The pitfallWhy it bites
Sorting by severity and calling it doneThat's the default that creates the backlog problem in the first place.
Context that lives only in someone's headExposure and criticality have to be captured (ideally as data next to the code) to be usable automatically.
Gating too broadlyBlocking every finding trains people to bypass the gate; block on priority, surface the rest.
How Draugr fits. This is Draugr's core idea. You declare each component's exposure and criticality in your app descriptor (or let the draugr classify wizard help), and Draugr ranks every finding into priority bands P1–P4, --min-priority to focus the list, and a gate that blocks CI on P1 by default rather than on raw severity. Severity is just one input; context does the rest. See how Draugr ranks findings in the docs.

Keep learning