What to fix first when everything is critical
On this page
Run any scanner across a real system and you will get the same shape of result, a wall of findings, most of them stamped "high" or "critical." Those labels come from CVSS, the Common Vulnerability Scoring System, a 0 to 10 scale published with most vulnerabilities. The word is a band on that scale, and the bands are defined by FIRST in the CVSS v3.1 specification:
| The label | The score | What it took to earn it |
|---|---|---|
| Critical | 9.0 to 10.0 | reachable over a network, by somebody with no account, needing nothing from a user, and it takes confidentiality, integrity and availability together |
| High | 7.0 to 8.9 | most of the above, with one condition attached. An account is needed, or the attack is not straightforward, or one of the three impacts is spared |
| Medium | 4.0 to 6.9 | several conditions at once, or a partial impact. The bulk of what a scanner returns |
| Low | 0.1 to 3.9 | local access, or heavy prerequisites, or an impact that is real and narrow |
That is a useful thing to know and it is not a statement about you. The specification is explicit
about it: a base score reflects severity "assuming the reasonable worst case impact across
deployed environments". So AV:N/PR:N, the pair that makes a 9.8, is a claim that somebody's
deployment exposes this to the internet without authentication. It is not a claim that yours does.
So the score did its job: it told you how severe each issue is in the abstract. What it did not tell you is the only thing you need on Monday morning: which one do I fix first?
Severity is not priority. And the gap between them is where security programs quietly fail.
Why severity can't rank your work
CVSS scores a vulnerability in isolation, as if every place it appears were identical. But the same CVE is not the same risk everywhere:
| The same CVE, sitting on | What it actually is |
|---|---|
| an internet-facing service with no auth | a live attack path |
| an internal tool three network hops away, behind a policy | a someday-maybe |
| the component that takes the platform with it when it falls over | an emergency, whatever the score says |
Feed all of that into one severity number and you get the two failure modes every team knows: either you chase everything marked critical and burn out, or you stop believing the labels and route around the gate. Both end in the same place, with the urgent thing sitting in a backlog next to a hundred things that look exactly as scary.
The two questions a scanner can't answer
To rank findings you need two pieces of context that no scanner can compute, because they aren't properties of the code:
How exposed is it?
Reachable from the internet, or namespace-scoped behind a network policy? This is likelihood, meaning how reachable the weakness is at all.
How much would it cost?
A platform-wide outage, or a dev tool nobody would notice for a week? This is impact, meaning what it costs when it goes wrong.
Real risk is the product of the two, layered on top of raw severity. A scanner sees none of it.
But your app description does
You already know this context. You know which services face the world and which are buried. You know which ones are load-bearing. That knowledge is stable, small, and exactly the kind of thing Draugr already captures in the app descriptor.
So it becomes two more attributes on a component you already describe, its exposure and its business criticality, and prioritization falls out of the description you wrote once.
components:
- name: ingress-gateway
exposure: public # reachable from the internet
criticality: critical # failure = platform-wide outage
- name: dev-dashboard
exposure: restricted # internal, network-scoped
criticality: supporting # nobody pages at 3am for this
Now the same CVE resolves two different ways, so on the gateway it is fix-now and top of the list. On the dashboard it is track-and-move-on. Severity was identical and priority was not, because priority combines the finding's severity with the component's exposure and criticality into a single, ordered answer.
The third input, whether anyone is exploiting it
Exposure and criticality describe your side. There's one more signal, and it comes from the world: exploitability.
A CVE on CISA's KEV catalog is known to be exploited in the wild, not theoretically exploitable, and used against real systems. That escalates a finding whatever its CVSS score says, because a modest score being exploited today outranks a 9.8 nobody has ever weaponized. A high EPSS probability, the likelihood a CVE gets exploited in the next thirty days, bumps a finding up a band on the same logic.
Both are opt-in and read from feeds you provide, so the ranking stays reproducible, because the same inputs give the same order, and you can point at where each escalation came from.
An order is still a list, until it names the work
Ranking gets the right thing to the top and shortens nothing: four hundred findings ranked are still four hundred rows, and the first twelve are often one library. So the last step groups them by the thing that fixes them, into one list for each kind of work:
$ draugr scan . --view actions --top 4
WHAT TO DO 50 actions clear 1154 findings
Bump a dependency 12 actions clear 559 findings
P1 Upgrade jquery 1.8.3 → 1.12.2
component storefront · control sca · 4 findings · web/package-lock.json:12 · NSWG-ECO-329 +3
P1 Upgrade jquery 1.12.2 → 3.5.0 major · beyond policy minor
component storefront · control sca · 5 findings · web/package-lock.json:12 · and 1 more
P1 Upgrade jquery 1.8.3 → 3.5.0 major · beyond policy patch
component api · control sca · 7 findings · app/static/js/jquery.min.js vendored · CVE-2020-11023 +6
P1 Update python:3.8-slim
component api · control images · 520 findings · upstream · CVE-2025-15467 +519
… and 8 actions not listed
⋯That is the demo project, where fifty actions clear 1,154 findings. The upgrades are one list, and
the lines of code to change, the infrastructure settings and the credential to rotate each have
their own, so whoever does one kind of work reads one list. The jquery rows are one library in two
components, each with its own owner, and vendored marks a copy committed to the tree, which
bumping the lockfile leaves as it was.
A row is a remediation, not a finding. Six vulnerabilities in one library are one upgrade; the same misconfiguration in three Dockerfiles is one habit. Listing those separately makes the repetitive work crowd out everything else, which is how a ranked list still ends up unread.
Actions are ordered by the worst thing each one clears, and by how many only near the end. An action clearing a single P1 outranks one clearing forty P4s, because volume is not a reason to do something first and a P1 is not something to trade away for a bigger number. Within a band, an action clearing a flaw on KEV comes first. CVE-2020-11023 is on KEV and none of the image's findings is, so the jquery upgrades sit above an image clearing 520 findings. What to fix first walks through every key, and where the report records each one.
upstream on the image row means the team does not build that image, so the action is to take a
newer one rather than to upgrade a package nobody there can reach. Advice you cannot act on, at the
top of a list called fix first, teaches people the list is not worth reading, and that is a fact
about a contract rather than something a scanner can see. It comes from the descriptor, like
everything else here.
The descriptor also says how far each component moves a dependency between releases. The demo takes minor upgrades, and its API takes only patches:
config:
fixes:
upgrade: minor
components:
- name: api
fixes:
upgrade: patch
So the storefront's jquery becomes two rows, one moving it to 1.12.2, as far as a minor upgrade
reaches, and one labeled major · beyond policy minor for the findings only 3.5.0 clears. None of
the API copy's advisories names a patch release, so its one row carries
major · beyond policy patch. The rows of a split are listed together and ranked by the more urgent
of them, so a P1 that waits on a major upgrade still sits near the top. The
config.fixes reference has the rest.
Grouping is opt-in while that annotation spreads: --view actions turns it on, and the default
still lists one finding per row. The report files always carry findings separately either way, so
an auditor reading the SARIF sees one record per finding whichever way the console was asked to
show them.
What that changes
For a developer
The output stops being a 400-row dashboard and becomes a short list: these three, in this order, today. Clear action, not triage archeology.
For the business
Consistent and defensible, because the same rules apply to every service. "What is our real exposure right now?" has an answer, and the urgent gets a faster response because it is no longer buried under look-alikes.
For your coding assistant
The difference between a list and an order. Point one at Draugr and it answers from the same ranking your pipeline uses. A scanner can hand it a pile of findings; only your description says which one to fix.
This is what Draugr does. Not another dashboard that shows you more, but a gate that tells you less, being the few things that matter, ranked by your own context. Describe your app, and the description doesn't just decide which scanners run. It decides what you fix first.