Publish SARIF to GHAS (GitHub code scanning)
Context
GitHub Advanced Security (GHAS) ingests SARIF through the GitHub code-scanning SARIF API
(POST /repos/{owner}/{repo}/code-scanning/sarifs).
The publish-to-ghas verb
uploads a finalized SARIF file to that endpoint: it derives the GitHub owner/repo, the commit_sha,
and the ref from the run's version-control provenance, gzip-compresses and base64-encodes the body
into the JSON payload, and posts it under a caller-supplied bearer token. The verb is the companion of
publish-to-ghazdo for Azure DevOps targets, and shares its
log-level refusal of an unpublishable (--no-repo) log.
Publish a SARIF only after it is finalized — its first run must carry a GitHub repositoryUri,
revisionId, and branch under versionControlProvenance (see emit-sarif
and the emit-finalize verb). The target owner/repo, the commit_sha, and the ref are read from
that provenance, so an unfinalized file cannot be published.
Why finalize matters for GHAS specifically
emit-finalize stamps the metadata GHAS needs to treat a finding as a security alert:
security-severityon each AI rule (curated per-CWE prior, or a neutral medium default for uncurated content). GitHub buckets the alert critical/high/medium/low from this value.tagson each AI rule —["security", "external/cwe/cwe-<n>"]for a CWE rule,["security"]for aNOVEL-rule. GitHub ignoressecurity-severityunless thesecuritytag is present, so an unfinalized file uploads as plain code-scanning results with no security severity.
These tags are emitted only for GitHub-hosted runs; an Azure DevOps-hosted SARIF will not carry them
(publish that one with publish-to-ghazdo instead).
Security model
This is the security-critical part of the skill. Follow it exactly.
- The token never appears on the command line. Place the token in an environment variable and name
that variable with
--token-env-var(defaultGHAS_TOKEN, parallelingpublish-to-ghazdo'sGHAZDO_TOKEN). The verb reads the value from the environment; it is never an argument, and it is never printed — not in dry-run output, not in error messages, and it is redacted from any server response body or exception text. - Always sent as
Authorization: Bearer. A classic or fine-grained GitHub personal access token carryingsecurity_eventswrite (repoalso grants it for private repositories) is sent as a bearer token. Less scope yields HTTP 403. - GitHub hosts only. The target must resolve to
github.comor a<slug>.ghe.comdata-residency host fromversionControlProvenance; the API host isapi.github.comorapi.<slug>.ghe.comrespectively. Adev.azure.com(GHAzDO) target, a legacy host, and credential-bearing repository URLs are rejected. A GitHub SSH/scp clone URL is normalized to its https identity.
Prerequisites
Sarif.Multitool≥ 5.1.0. Recommended invocation:dotnet dnx Sarif.Multitool --yes -- publish-to-ghas ...(zero-install, requires .NET 10+). Fall back todotnet tool install --global Sarif.Multitool, then invoke assarif publish-to-ghas ....- A finalized SARIF whose first run carries a GitHub
repositoryUri,revisionId, and arefs/heads/...branch. - A GitHub personal access token with
security_eventswrite, stored in an environment variable.
Detection
Inputs
| Parameter | Required | Description |
|---|---|---|
{{SARIF_FILE}} |
Yes | Path to the finalized .sarif file to upload |
{{TOKEN_ENV_VAR}} |
No | Name of the environment variable holding the token. Default GHAS_TOKEN |
Step 1 — Set the token in the environment
Set the variable in the current session only. Do not write the token to a file, a script, or shell history. Use your platform's mechanism for assigning a value without echoing it.
# PowerShell — paste the token when prompted; it is not echoed to history.
$env:GHAS_TOKEN = (Read-Host -AsSecureString | ForEach-Object { [System.Net.NetworkCredential]::new('', $_).Password })
# bash — read without echo.
read -rs GHAS_TOKEN && export GHAS_TOKEN
Step 2 — Validate pre-flight (no network)
GHAS ingestion-blocking defects are detectable offline. A clean validation under the GHAS rule kinds is the offline equivalent of upload acceptance — run it before the round-trip.
dotnet dnx Sarif.Multitool --yes -- validate "{{SARIF_FILE}}" --rule-kind "Sarif;AI;Ghas"
Resolve any error-level findings before publishing. In particular, GHAS will not surface a
security severity unless each AI rule carries the security tag — re-finalize (emit-finalize) the
SARIF rather than hand-editing tags.
Step 3 — Dry run (no network)
Always dry-run first. This resolves the GitHub target, the ref, and the commit_sha from the
SARIF's provenance and prints the request shape without contacting the server and without printing
the token.
dotnet dnx Sarif.Multitool --yes -- publish-to-ghas "{{SARIF_FILE}}" --dry-run
Confirm the reported owner/repo, ref, and commit_sha. If the target is wrong, the SARIF was not
finalized against the intended GitHub repository — re-finalize it; do not hand-edit these values.
Step 4 — Publish
dotnet dnx Sarif.Multitool --yes -- publish-to-ghas "{{SARIF_FILE}}"
To name a different environment variable:
dotnet dnx Sarif.Multitool --yes -- publish-to-ghas "{{SARIF_FILE}}" --token-env-var "{{TOKEN_ENV_VAR}}"
The verb posts to api.github.com (or api.<slug>.ghe.com). Exit code 0 means the server accepted
the upload (HTTP 2xx — GitHub answers 202 Accepted); a non-zero exit code with error: publish failed with HTTP <code> means it did not. The verb prints the analysis response body (with the token
redacted) — its url is the analysis status endpoint, which you can poll with gh api until
"processing_status": "complete".
If dotnet dnx is not available: install the global tool and use the sarif command name:
dotnet tool install --global Sarif.Multitool
sarif publish-to-ghas "{{SARIF_FILE}}"
Edge Cases
- No provenance — The run carries no
versionControlProvenance[0].repositoryUri. The verb fails closed. Finalize the SARIF (emit-finalize) before publishing. - Unpublishable (repo-less) log — Any run carries
properties.unpublishable = true, stamped byemit-finalize --no-repofor a scan outside version control. The verb refuses the whole file up front (publishing ingests every run): a non-version-controlled scan has no repository or commit to anchor alerts to. Publish a log whose runs were all finalized against a checked-out repository (without--no-repo); split a merged log first if only some runs are unpublishable. - Non-GitHub target — The repository is
dev.azure.comor a legacy host. The verb rejects it; usepublish-to-ghazdo. - Missing
revisionIdorbranch— GHAS anchors an analysis to a commit and a fully-qualified ref (refs/heads/...). The verb fails closed if either is absent; re-finalize with both present. commit_shanot in the repo — GitHub rejects an analysis whose commit it cannot find. Publish against a SARIF finalized at a commit that has been pushed.- HTTP 403 — The token lacks
security_events(orrepofor a private repo), or GHAS / code scanning is not enabled on the repository. - HTTP 404 —
owner/repodoes not resolve, or the token cannot see it. - No security severity on alerts — The alerts appear but without critical/high/medium/low. The
rules are missing the
securitytag; re-finalize soemit-finalizestamps it (Step 2 catches this offline).
Validation
After running this skill:
- The token never appeared in any printed command, log line, or error message.
- The GHAS-ruleset validation (
--rule-kind "Sarif;AI;Ghas") was clean before the live publish. - The dry-run
owner/repo+ref+commit_shamatched the intended GitHub repository. - A successful publish returned exit code
0and the analysis reachedprocessing_status: complete. - The resulting alerts carry a security severity (critical/high/medium/low) — confirming the
securitytag andsecurity-severitysurvived finalize.
Escalation
- Multitool not available — Neither
dotnet dnxnor a global install succeeded. Report the gap; the SARIF cannot be published from this environment. - Repeated 4xx / non-
completestatus — After confirming target, ref, commit, and token scope, capture the analysiserrorsbody the verb prints (it contains no secret) and escalate to the repository's Advanced Security administrator.