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: writeor 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 descriptor | What the run covered | The category |
|---|---|---|
project: acme-azure | all of it | acme-azure |
project: acme-gcp | all of it | acme-gcp |
project: acme-azure | --components web --controls sca | acme-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.