📎 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

On this page

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: P1           # (scan/push) gate the build; the default
          # fail-on-new: P1       # (diff/PR)   gate only on findings this PR introduces
          # diff-view: actions    # (diff/PR)   the comment lists what to do, not every finding

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 request, merge queuescan base + head, post one sticky new/fixed comment (publishers suppressed). On merge_group: base is the commit the queue built the entry on, no commentpull-requests: write, fetch-depth: 0

In a merge queue, auto picks scan for a merge_group event, which gates the entry on everything in the tree, findings already on main included. To gate an entry on the findings it adds, as diff gates its pull request, choose diff on merge_group:

on:
  pull_request:
  merge_group:
    types: [checks_requested]
jobs:
  draugr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: draugr-dev/draugr@v0
        with:
          mode: ${{ github.event_name == 'merge_group' && 'diff' || 'auto' }}
          fail-on-new: P1

A pull request that adds the repository's first descriptor has none at its base. Diff mode then compares the head against no findings, so every finding is new and fail-on-new gates on them. A pull request with no descriptor at the head fails with a message saying so, because there is nothing to scan.

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-onP1(scan) What fails the gate: a priority band (P1–P4), which folds in what the descriptor declares about the component, or a severity (critical, high, medium, low).
fail-on-priorityDeprecated: write the band in fail-on, which takes either vocabulary.
fail-on-new(diff) Fail on a new finding at or above this: a priority band (P1-P4) or a severity. Empty applies the descriptor's config.gate.failOnNew, which a release without draugr diff --saga ignores.
fail-on-new-priorityDeprecated: write the band in fail-on-new, which takes either vocabulary.
diff-viewfindings(diff) What the pull-request comment says. findings is a row per finding; actions groups them into the things somebody would do, so six advisories in one library are one upgrade.
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 with draugr tools install --saga, which fetches the ones this descriptor's controls will run and leaves the rest. 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: 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.