The Saga
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, infrastructure) 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 exposed a component is, and what its failure costs, 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
controllers:
secrets: { enabled: true }
components: # the app's parts
- name: web
exposure: public # how reachable it is
criticality: critical # what its failure costs
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, infrastructure. 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 an infrastructure 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 a component is, likelihood. Criticality is what its failure costs, 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 controllers 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 controllers 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 decided | Where you can see it | |
|---|---|---|
config.exclude | whoever owns the descriptor, named in acceptedBy | the descriptor, in review |
| A supplier's VEX | the document's author, on a date they chose | their document |
nosem in the source | whoever was editing the file | nowhere, 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 with | When | What you get |
|---|---|---|
| Nothing | You want output now | draugr scan . runs sca, secrets, sast and iac against a repository with no descriptor at all |
draugr init | You have a repo and want a starting point | Detects the stack and pre-fills sensible controls, Go adds gosec, a Dockerfile adds an images stub |
draugr survey | Something you already run can be enumerated, today a Kubernetes cluster or a GitHub org | Surveyors enumerate the surface and write the components for you |
Zero-config mode is a way to start, not a way to finish: it scans a single repository, and it has no way to know a component's exposure or criticality, so it cannot prioritize. 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 }}. Loading fails
fast if one 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 surveyagainst a live cluster or org adds what has appeared since, so the descriptor tracks reality instead of drifting away from it.
Going deeper#
- Saga reference, every field, with examples
- Your first Saga. Write one end to end
- Controls & scanners, what the descriptor resolves to
- Prioritization. How
exposureandcriticalitybecome P1–P4 - Verdict & gating. How a run turns into pass or fail
- Surveyors. Let discovery write it for you