← All articles

What to fix first when everything is critical

July 12, 2026 · Wilson Santos · updated October 25, 2026
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 labelThe scoreWhat it took to earn it
Critical9.0 to 10.0reachable over a network, by somebody with no account, needing nothing from a user, and it takes confidentiality, integrity and availability together
High7.0 to 8.9most 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
Medium4.0 to 6.9several conditions at once, or a partial impact. The bulk of what a scanner returns
Low0.1 to 3.9local 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 onWhat it actually is
an internet-facing service with no autha live attack path
an internal tool three network hops away, behind a policya someday-maybe
the component that takes the platform with it when it falls overan 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
The same CVE, two priorities A single finding, CVE-2023-30861 at high severity, is ranked separately for each component that carries it. On ingress-gateway, which is public and business-critical, it is P1 and gets fixed now. On dev-dashboard, which is restricted and supporting, the same finding is P3 and goes to the backlog. CVE-2023-30861 severity: high, one finding, one score ingress-gateway exposure: public criticality: critical dev-dashboard exposure: restricted criticality: supporting P1 act now P3 backlog
Same finding, same severity, two answers, because the components differ.

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.

What the two exploitation signals do to an order Sorted by severity alone, a 9.8 critical with no known exploit sits above a 5.9 medium that appears on CISA's Known Exploited Vulnerabilities catalog. Sorted with exploitability, the medium moves above the critical, because it is being used against real systems today and the critical has never been weaponized. SORTED BY SEVERITY ALONE 9.8 critical nobody has ever weaponized it 5.9 medium used against real systems today SORTED WITH EXPLOITABILITY 5.9 medium on CISA KEV 9.8 critical no known exploit THE TWO SIGNALS KEV Binary. CISA publishes it, and a CVE is on the list because it is being used. EPSS A probability. How likely this CVE is to be exploited in the next thirty days.
The severity did not change and neither did the code. What changed is that one of them is being used, which is a fact about the world rather than about your repository, and it is the input almost nobody feeds in.

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.