Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: publish-to-ghas description: Uploads a finalized SARIF file to GitHub Advanced Security (GHAS) code scanning using the Sarif.Multitool publish-to-ghas verb, deriving the GitHub target, commit, and ref from the run's version-control provenance. metadata: author: sarif-sdk-maintainers version: "2.0.0" category: sarif severity: medium packages:
- "Sarif.Multitool >= 5.1.0"
triggers:
- "publish sarif"
- "upload sarif"
- "ghas"
- "github advanced security"
- "code scanning"
- "sarif ingestion"
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-severity` on 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.
- `tags` on each AI rule —
["security", "external/cwe/cwe-<n>"]for a CWE rule,["security"]
for a NOVEL- rule. GitHub ignores `security-severity` unless the `security` tag 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 (default GHAS_TOKEN, paralleling publish-to-ghazdo's GHAZDO_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
carrying `security_events` write (repo also 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 from versionControlProvenance; the API host is api.github.com or api.<slug>.ghe.com respectively. A dev.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 to dotnet tool install --global Sarif.Multitool, then invoke as sarif publish-to-ghas ....
- A finalized SARIF whose first run carries a GitHub
repositoryUri,revisionId, and a
refs/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.Multitoolsarif 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 by
emit-finalize --no-repo for 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;
use publish-to-ghazdo.
- Missing `revisionId` or `branch` — 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_sha` not 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 security tag; re-finalize so emit-finalize stamps 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
security tag and security-severity survived 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-`complete` status — After confirming target, ref, commit, and token scope,
capture the analysis errors body the verb prints (it contains no secret) and escalate to the repository's Advanced Security administrator.