📎 Unreleased docs. This is the next version (from main), features here may change or not be in a release yet.See the latest release →

Core concepts

What to fix first

A scan of a real project produces hundreds of findings. Ranking them by severity produces a correctly ordered list that is still no use, because the top of it is usually work nobody reading it can do.

Draugr's fix list answers a different question: what should I do, and what will it clear?

Fix first · <n> actions clear <m> findings:
  <band>  <the action>  <control> · <n> findings · <builtBy, when not yours>
          <the rule it names, and how many more it clears>

A schematic rather than a scan, because the shape is what this page is about and a pasted run carries package names and counts that go stale without anything noticing. draugr scan . prints the real thing.

Rows are actions, not findings#

One remediation usually clears many findings. Eight vulnerabilities in one library are one upgrade; the same misconfiguration in three Dockerfiles is one habit. Listing those as eight and three rows makes the repetitive work crowd out everything else.

ControlGrouped byThe action
scapackageupgrade this dependency
images, when you build itpackageupgrade it in your image
images, when somebody else builds itimagetake a newer image
images, OS layer past end of lifeoperating system releasemove the base
secretsfileremove the credential
iacruleapply one fix across N files
infrastructurecheckone cluster setting

Only where the fix genuinely is one fix. Twelve benchmark checks against one cluster are twelve things to change, and folding them together because they share a prefix would hide eleven of them.

Grouping is opt-in for now. draugr scan --group action turns it on; the default lists one finding per row.

Not because the list is the better view, but because grouping is only right once a descriptor says which images the team builds and which infrastructure it operates. Without that, an action row states a fix nobody can apply. upgrade this library, inside an image somebody else publishes, where a finding row merely reports something true that a reader can look up. Stating wrong advice is worse than listing a fact, so the annotations come first and the default follows them.

Grouping is a rendering either way. The report files always carry findings separately, an auditor reading results.sarif sees one record per finding whichever way the console was asked to show them.

Findings you can act on come first within their band#

Priority decides the order. Where two findings share a band, the one somebody can act on is listed first, a package with a fix, then a release that can be moved, then one with no published fix, and last the ones that belong to a provider.

This does not change the priority itself. Priority feeds the gate, so demoting a finding because nobody here can fix it would weaken a build gate as a side effect of annotating a descriptor, and the risk is unchanged either way: a vulnerable control plane is exactly as dangerous whether or not the fix is yours to apply. Deciding that a finding is acceptable is what exclusions are for, and they record who decided.

So operatedBy and builtBy change what is recommended and in what order, never what a finding is worth.

Ranking is by the worst thing an action clears#

Actions are ordered by the highest priority among their findings, and only then by how many they clear. An action clearing one P1 outranks one clearing forty P4s.

Volume never promotes: a P1 is not something to trade away for a bigger number.

Some findings are true and not yours to act on. Telling somebody to change a file on a control plane they cannot reach, or to upgrade a library inside an image they do not build, is advice they cannot take, at the top of a list called fix first, which teaches them the list is not worth reading.

Draugr cannot work out which case it is looking at. Whether a cluster is managed, or an image is somebody else's, is a fact about a contract rather than something visible in what a scanner reads, the same argument that puts exposure and criticality in the descriptor.

operatedBy. Who runs this infrastructure#

infrastructure:
  - kind: kubernetes
    ref: prod-cluster
    operatedBy: provider     # self (default), or provider

On provider, findings about the parts a managed platform runs are reported and counted but never presented as work to do: the API server, etcd, the controller manager, and kube-proxy, which every managed platform runs as a DaemonSet it owns.

It narrows deliberately. The kubelet stays yours, because node pool settings usually reach it. So do RBAC, Pod Security and network policy, which are yours whoever runs the cluster underneath, and are usually the findings that matter.

builtBy, who publishes this image#

images:
  - image: registry.example.com/vendor/redis:8.2.2
    builtBy: upstream        # self (default), or upstream

On upstream, every finding in the image becomes one action. take a newer image, instead of one row per vulnerable library. Nobody can upgrade a package inside an image they do not build; the fix is a newer image, or a wait for whoever publishes it.

On self (the default), a package inside the image is yours, and the rows say to upgrade it.

Why both default to self#

A descriptor written by hand describes what a team builds and runs. One written by draugr survey describes a cluster full of things they only run, and that is the case worth declaring.

The default is also the safe direction. Marking something as somebody else's when it is yours hides work you could have done; the reverse costs a row you skip.

What is left out of the list, and where it goes#

  • Not yours to fix, reported, counted, and named on its own line rather than ranked among the work.
  • No fix published anywhere, including an operating system past end of service life, where the release itself is the action.
  • Nothing at all. A control that could not run says so; it found nothing by looking at nothing, and a component whose whole surface went unscanned reports ERROR rather than pass.

Reading a row#

The row carriesWhat it is
the bandthe highest band among the findings this one action clears
the actionwhat to do, naming the thing and the version in hand
the controlwhich control the findings came from
the counthow many findings this action clears
upstreampresent only where the component is somebody else's to fix
the second linethe releases that carry the fix, or why none is named

The target version appears only when every advisory agrees on one. Where they disagree, Draugr does not choose: version ordering belongs to the ecosystem. 5.10 is above 5.9 in most schemes and below it as a string, and naming the wrong release as sufficient reads as do this and you are done while leaving findings behind.

Each row names one rule identifier, linked to whatever the scanner published about it, and counts the rest. To read what a check means and how to fix it:

draugr explain kube-bench/cis/4.3.1

That prints the remediation the scanner published, so understanding a finding does not depend on searching for its identifier.