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

Guides

Use in CI with the GitHub Action

Add Draugr to a repository's CI with the first-party draugr-dev/draugr action. It downloads a cosign-verified Draugr release, provisions the scanners, and, with its default mode: auto, does the right thing per event from one workflow and one Saga:

  • on push, it runs a full scan and uploads the merged SARIF to GitHub code scanning (the Security tab), via the Saga's github publisher;
  • on a pull request, it scans the PR's base and head and posts one sticky new/fixed comment, with the Saga's publishers suppressed, so it never double-posts alongside a code scanning "GitHub Advanced Security" comment.
name: Security
on:
  push:
    branches: [main]
  pull_request:
permissions:
  contents: read
  security-events: write        # push: upload SARIF to code scanning
  pull-requests: write          # PRs: post the sticky diff comment
jobs:
  draugr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0          # diff mode needs the PR's base commit
      - uses: draugr-dev/draugr@v0   # latest v0.x; pin @vX.Y.Z for reproducible CI (installs Draugr for you)
        with:
          saga: draugr.saga.yaml
          tools: true             # provision the scanners the controls need
          # fail-on: high         # (scan/push) gate the build
          # fail-on-new: high     # (diff/PR)   gate only on findings this PR introduces

Versioning. @v0 is a moving major tag that always points at the newest v0.x release, so you get updates without editing the ref. For fully reproducible CI, pin an exact release instead (draugr-dev/draugr@v0.29.0) and bump it deliberately. (Pre-1.0, a minor bump can be breaking, so @v0 means "latest, possibly-breaking".)

The scanners each control needs (Trivy, Gitleaks, Semgrep, …) still have to be on the runner: set tools: true to let Draugr provision them, install them alongside (e.g. aquasecurity/setup-trivy), or gate their presence with draugr doctor.

doctor also reports any surface the descriptor declares that no enabled control looks at, the scan that passes having never opened your images. On a descriptor meant to be complete, draugr doctor saga.yaml --fail-on-uncovered makes that a failing step instead of a note nobody reads.

Modes#

modeOnWhat it doesNeeds
auto (default)anydiff on pull_request, scan otherwiseboth permissions below
scanpush / schedulefull scan; the Saga's publishers deliver results (e.g. github → code scanning)security-events: write
diffpull requestscan base + head, post one sticky new/fixed comment (publishers suppressed)pull-requests: write, fetch-depth: 0

Prefer the single auto workflow above. It keeps code-scanning uploads off PRs, which is what avoids a second, overlapping PR comment. See gate PRs on new findings and code scanning for each mode in depth.

Action inputs#

InputDefaultDescription
saga, (required)Path to the Saga descriptor to scan.
modeautoauto (diff on PRs, scan otherwise), scan, or diff.
versionlatestDraugr release to use (with or without a leading v). Pin for reproducibility.
fail-onhigh(scan) Severity that fails the gate: critical, high, medium, low.
fail-on-priority.(scan) Also fail on any finding at or above this priority band (P1–P4).
fail-on-new.(diff) Fail on a new finding at or above this severity.
fail-on-new-priority.(diff) Fail on a new finding at or above this priority band.
min-priority,List findings at or above this band in the console output.
cache-dir,Enable content-hash caching in this directory (relative to working-directory).
outputdraugr-outDirectory for report.json, results.sarif and any SBOMs (relative to working-directory). Written in both scan and diff mode.
working-directory.Directory to run Draugr in.
args,Extra raw arguments appended to draugr scan (escape hatch).
verifytrueCosign-verify the release signature (the checksum is always verified).
toolsfalseProvision the external scanners (Trivy, Gitleaks, gosec, Semgrep) with draugr tools install before scanning. Set true when the runner doesn't already have them.
feedsfalseFetch the exploitability datasets (KEV, EPSS) into the runner's cache before scanning. Set true when the Saga's config.exploitability reads them with cache.

Ranking by real-world exploitability#

Set feeds: true when the descriptor asks for KEV or EPSS from the cache:

- uses: draugr-dev/draugr@v0
  with:
    saga: draugr.saga.yaml
    tools: true
    feeds: true          # draugr feeds update, before the scan
    fail-on-priority: P1
# draugr.saga.yaml
config:
  exploitability:
    kev: cache
    epss: cache

Its own step on purpose. The scan then reads the cache and never reaches the network, so the gate stays reproducible, and a feed outage surfaces at the fetch rather than inside a scan.

A fetch that fails keeps the cached copy and reports how old it is. What this guards against is a scan ranking everything as though nothing were exploited, and a cached catalog does not do that: it ranks on data of a known age, which the report then carries. With nothing cached there is no answer to keep, and the step fails. So a pipeline is not blocked by an upstream outage it can already answer around. See config.exploitability.

What code scanning receives#

On a pull request the action runs a diff, and by default the SARIF it hands to code scanning carries only the findings the branch introduced (code-scanning: new). An upload of the whole repository annotates a reviewer with hundreds of findings they did not cause, and the ones they did are indistinguishable among them, which is how a review surface stops being read. Set code-scanning: all for the previous behavior. On a push there is nothing to diff against, so the upload is always the complete scan.

code-scanning-min-priority: P1 narrows it further, to what is urgent. It applies to the diff, not to the scans the diff was computed from, a diff taken from filtered inputs would read every finding the filter removed as fixed.

- uses: draugr-dev/draugr@v0
  id: draugr
  with:
    code-scanning: new             # default. What this PR is answerable for
    code-scanning-min-priority: P1 # and only what is urgent

A narrowed SARIF records the band inside itself, so nothing reading it later mistakes it for a complete scan. The complete report is still written to output, and outputs.report still names it, only outputs.sarif follows the setting.

Outputs: sarif (path to the SARIF to upload, the new-findings one in diff mode) and report (path to report.json, always complete). Both point inside output and are written on pull requests too, so an if: always() upload step finds them whichever mode the action ran in.

Without the action#

If you already have draugr on the runner (e.g. draugr tools install, or a self-hosted image), run it directly. The exit code is the gate:

- name: Draugr scan
  run: draugr scan draugr.saga.yaml -o draugr-out
- name: Upload SARIF to code scanning
  if: always()
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: draugr-out/results.sarif

See the CLI reference for every scan flag.