📎 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

The Saga

On this page

Draugr turns a description of your app into trustworthy, audit-ready security evidence. The starting point is the Saga.

draugr.saga.yaml is the source of truth, a security bill of materials for a running application. It lists your components (repositories, images, hosts, Kubernetes clusters) and which controls must pass. You write what you know; Draugr works out the rest.

Why a descriptor at all

The alternative is wiring each scanner into each pipeline by hand. That works until you have more than a few services, and then the pipelines drift: one repo runs secret scanning and its neighbor doesn't, a threshold is stricter in one place than another, and nobody can answer "which checks actually ran against this release" without reading a dozen CI files.

A descriptor moves that decision out of the pipeline and into a file that lives with the app:

  • One place to look. What the app is made of, and what must pass, in a file you review and diff like any other.
  • Checks follow the surface. Declare an image and image scanning applies; declare a host and the HTTP header and TLS controls do. There's no second list of which scanner runs where to keep in step with the first.
  • Context a scanner cannot compute. How reachable a component is, and the business impact if it fails, are not in the code. Declaring them is what lets Draugr turn a pile of "criticals" into an ordered list.
  • The evidence knows what it covered. A run's report is anchored to the descriptor, so the auditor's question. What was in scope, has an answer that isn't "whatever CI happened to do that day".

Anatomy

project: acme-platform
release:                      # required. What is being qualified
  version: "1.4.0"

config:                       # optional, controls, thresholds, reports, publishers
  controls:
    secrets: { enabled: true }

components:                   # the app's parts
  - name: web
    exposure: public          # how reachable the component is
    criticality: critical     # the business impact if it fails
    repositories:
      - url: https://github.com/acme/web.git
    images:
      - image: registry.example.com/acme/web:1.4.0
        digest: sha256:…
    hosts:
      - url: https://acme.example.com

references: []                # optional. Links to manual or human-run controls

Every field is covered in the Saga reference. The four that shape a run most are below.

release

What this run qualifies. version is required. It's what the evidence is filed under.

components

One entry per logical part of the app, each listing whatever surface applies: repositories, images, hosts, kubernetes. All are optional and a component may have only one. A Kubernetes cluster with no code of its own is a perfectly good component with nothing but a kubernetes entry.

Surface is what drives the plan. Draugr expands each enabled control across the components it applies to, so adding an image to a component is what makes image scanning run against it.

exposure and criticality

The two axes of prioritization, and the clearest reason a descriptor beats a pipeline flag. Exposure is how reachable the component is (likelihood). Criticality is the business impact if it fails (impact). Neither is in the source code, so no scanner can infer them, and without them a scanner can only ever tell you severity in the abstract.

The same CVE is act-now on a public, business-critical gateway and backlog on a restricted internal tool. That distinction is yours to declare, and it's what turns a wall of findings into a short list. Both are optional; a component may stay unclassified. draugr classify walks you through setting them.

config

Project-wide settings: which controls run and how they're tuned, the gate thresholds, exclusions, reports and publishers, SBOM generation, and allowEffects for scanners that would otherwise change something in your environment. A component's own controls block overrides the project default for that component.

Exclusions are not deletions

An excluded finding stays in the report, marked suppressed, carrying the reason someone gave. It doesn't count toward the verdict, and it doesn't disappear.

This is deliberate. A finding that vanishes is indistinguishable from one that was never made, and the question an auditor asks is never "did the scanner run". It's "who decided this was acceptable, and when". See config.exclude.

The same holds for acceptances Draugr did not make. A supplier's VEX claim and a comment in the code, a Semgrep nosem, a linter pragma, both set a finding aside, and both are reported rather than dropped. They are counted on their own lines because the three have different people at the end of them:

Who decidedWhere you can see it
config.excludewhoever committed the exclusion, in the git history of its linethe descriptor, in review, and the report
A supplier's VEXthe document's author, on a date they chosetheir document
nosem in the sourcewhoever was editing the filenowhere, until Draugr prints it

The last one is the weakest and the easiest to add without anybody noticing, which is exactly why it gets its own line instead of being folded into a total with the others.

Writing one

Three ways in, in rough order of how much you already know:

Start withWhenWhat you get
NothingYou want output nowdraugr scan . scans a repository with no descriptor at all, enabling what draugr init would for the same tree
draugr initYou have a repo and want a starting pointReads the tree and enables the scanners it calls for, naming the files behind each
draugr surveySomething you already run can be enumerated, today a Kubernetes cluster, or a GitHub, GitLab or Azure DevOps organizationSurveyors enumerate the surface and write the components for you

Zero-config mode scans a single repository as one component, with the controls and scanners draugr init would write for it and none of what init leaves commented out, so no image and no host. It cannot know the component's exposure or criticality, so it reads the component as public and critical and its bands rank severity alone. The descriptor is what upgrades a scan into a qualification.

How Draugr finds it

draugr scan takes a file. The Saga to run, or a directory, which triggers zero-config mode against that repository. With no argument at all it uses the current directory.

draugr scan                     # zero-config, current repo
draugr scan draugr.saga.yaml    # run the descriptor

Any string value may reference an environment variable with ${{ VAR_NAME }}, if it is one of the built-in CI names or one the pipeline lists in $DRAUGR_SAGA_ENVS. Loading fails fast on any other name, or on one that is unset, rather than scanning on with an empty value.

Keeping it honest

A descriptor is only worth what its accuracy is worth, and two things protect that:

  • Editor support. A # yaml-language-server: line gives you autocomplete, hover documentation and validation as you type. See editor support.
  • Re-survey. draugr survey against a live cluster or org adds what has appeared since, so the descriptor tracks reality instead of drifting away from it.

Going deeper