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
githubpublisher; - 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
mode | On | What it does | Needs |
|---|---|---|---|
auto (default) | any | diff on pull_request, scan otherwise | both permissions below |
scan | push / schedule | full scan; the Saga's publishers deliver results (e.g. github → code scanning) | security-events: write |
diff | pull request, merge queue | scan base + head, post one sticky new/fixed comment (publishers suppressed). On merge_group: base is the commit the queue built the entry on, no comment | pull-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
| Input | Default | Description |
|---|---|---|
saga | , (required) | Path to the Saga descriptor to scan. |
mode | auto | auto (diff on PRs, scan otherwise), scan, or diff. |
version | latest | Draugr release to use (with or without a leading v). Pin for reproducibility. |
fail-on | P1 | (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-priority | Deprecated: 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-priority | Deprecated: write the band in fail-on-new, which takes either vocabulary. | |
diff-view | findings | (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). |
output | draugr-out | Directory 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). |
verify | true | Cosign-verify the release signature (the checksum is always verified). |
tools | false | Provision 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. |
feeds | false | Fetch 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.