📎 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

Publish to GitHub code scanning

On this page

Draugr can upload its merged SARIF straight to GitHub code scanning (the Security tab) with the native github publisher, no separate upload-sarif step. Code scanning is free for public repos; private repos need GitHub Advanced Security.

On GitLab? There is no upload: GitLab reads its own schema from build artifacts rather than SARIF, so the equivalent is a set of report formats and a template that collects them. See use in CI with GitLab.

1. Declare the publisher in your Saga

The github publisher renders the sarif report for itself, so kind: github is the whole of it. It never stores a secret in the descriptor, repo/commit/ref default from the GitHub Actions environment, and the token comes from $GITHUB_TOKEN. It no-ops outside Actions, so the same Saga still runs locally.

config:
  publishers:
    - kind: github         # repo/commit/ref default to the GitHub Actions env
      # repo: owner/name   # optional overrides ($GITHUB_REPOSITORY / $GITHUB_SHA / $GITHUB_REF)
      # ref: refs/heads/main
      # tokenEnv: GITHUB_TOKEN   # the token is read from this env var, never the Saga

Choosing what a reviewer sees

An alert on every finding in the repository is an alert on nothing: a reviewer opening a pull request is annotated with hundreds they did not cause, and the ones they did are indistinguishable among them. Two ways to narrow it, and they compose.

What this change introduced. The GitHub Action runs a diff on a pull request and, by default, uploads only the findings the branch added:

- uses: draugr-dev/draugr@v0
  with:
    code-scanning: new   # the default; `all` uploads every finding

What is urgent. A band narrows it further, either per workflow or in the descriptor beside the rest of your policy:

config:
  publishers:
    - kind: github
      reports:
        - format: sarif
          minPriority: P1    # this report only; the JSON beside it stays complete

A descriptor that describes more than one repository

A component may hold several repositories, and a fragment may contribute one from another project. Their findings carry paths relative to their own repository, so uploading them here would annotate a same-named file in this one, a squiggle on a line that does not have that problem.

The Action drops them from the upload automatically. It is not a setting, because there is no case where annotating another repository's finding on this checkout is what you want. They stay in the scan, in the artifacts and in the pull-request comment, where a regression in a fragment-contributed repository is real news a reviewer should see. Findings that belong to no repository, an image's, a host's, are unaffected, because they were never anchored to a checkout.

What narrowing does to existing alerts

Code scanning resolves any alert missing from an upload as fixed, within the ref and category that upload belongs to. Both halves matter, and the second is what makes narrowing a pull request safe.

A pull-request run uploads against the pull request's own ref, not your default branch. Its alerts are the ones shown on that pull request, and narrowing them changes nothing about main: measured on a repository with 96 open alerts on main, a PR upload carrying zero findings created an analysis on refs/pull/N/merge and left all 96 open.

So on a pull request, narrowing costs you nothing you were relying on. On a default branch it would, an upload narrowed to P1 there resolves the P2–P4 alerts, and they are the ones nobody is looking at but somebody may still need. That is why a push is never narrowed: there is nothing to diff against, so it uploads the complete scan.

A narrowed SARIF records the band it was narrowed to, so nothing reading it later, including draugr diff, which reads a missing finding as a fixed one, mistakes it for a complete scan.

Accepted findings

A finding accepted in Draugr, by config.exclude, an inline #nosec or a VEX statement, is dismissed in code scanning as won't fix, with the reason as the comment. A config.exclude entry's comment also names its file and line, (draugr.saga.yaml:14), where the line's git history records who wrote it. Code scanning does not read the SARIF suppressions the report carries, so the publisher sets each alert itself once GitHub has processed the upload.

  • An alert Draugr dismissed is reopened when its finding is no longer accepted, such as when an exclusion expires.
  • An alert a person dismissed stays as they left it.
  • The publish waits up to five minutes for GitHub to process the upload. Past that, or when GitHub refuses a dismissal, the publish fails and names what was not updated. Re-run it once the token has security-events: write or GitHub has finished processing.

The token needs no permission beyond the security-events: write the upload already uses. An upload made with github/codeql-action/upload-sarif instead of the publisher leaves accepted findings open.

Two products in one repository

The category an upload belongs to is what separates one analysis of a commit from another, and Draugr sets it from the descriptor. Scan azure.saga.yaml and gcp.saga.yaml over the same commit and each product keeps its own alerts; without a category the second upload would replace the first, and both runs would report success.

It is written into the SARIF as runs[].automationDetails.id, so it travels with the file and applies whether Draugr uploads it or github/codeql-action/upload-sarif does. The upload endpoint has no category of its own to correct it with.

The descriptorWhat the run coveredThe category
project: acme-azureall of itacme-azure
project: acme-gcpall of itacme-gcp
project: acme-azure--components web --controls scaacme-azure/components:web/controls:sca

A matrix leg is its own category for the same reason a product is, so splitting a scan across jobs by component or by control does not leave each job erasing the last one's alerts.

The category comes from what was asked for rather than from what was found, so it is the same on every push of the same product, and that is what lets code scanning close an alert when the finding behind it goes away. Vary the narrowing between pushes of one product and each variant gets its own category, which leaves the old alerts open forever and opens the new ones from scratch.

See examples/publishing.saga.yaml for a fuller, multi-format, multi-publisher Saga.

2. Run it from a workflow

The action forwards GITHUB_TOKEN to the scan, and the job grants security-events: write so the upload is allowed. This mirrors examples/github-actions-code-scanning.yml:

name: Draugr
on:
  push:
    branches: [main]
  pull_request:
permissions:
  contents: read
  security-events: write   # push: the github publisher uploads SARIF
  pull-requests: write     # PR: the sticky diff comment
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0     # so PR diff mode can reach the base commit
      - name: Draugr scan + publish to code scanning
        uses: draugr-dev/draugr@v0   # latest v0.x; pin @vX.Y.Z for reproducible CI
        with:
          saga: draugr.saga.yaml          # a Saga with a `github` publisher in config.publishers
          tools: true
          fail-on: high                   # optional: fail the build on high-severity findings

Because the publisher lives in the Saga, findings are uploaded even on a FAIL verdict, so you always get evidence in the Security tab. Draugr dogfoods this itself in .draugr/self.saga.yaml plus its self-scan workflow.

Why the same workflow handles PRs without a duplicate comment. With the action's default mode: auto, code-scanning upload happens on push, while pull requests get Draugr's own sticky diff comment instead (publishers suppressed). If you upload to code scanning on PRs too, GitHub's own "GitHub Advanced Security" bot also comments, so you'd see two overlapping comments. Keeping the upload to push (the default) avoids that. See gate PRs on new findings.

For the plain upload-sarif alternative (no publisher in the Saga), see the GitHub Action guide; for the full list of report formats and publishers, see reports & publishers. To get the same findings inline in your editor rather than on a pull request, see see findings in your editor.