Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: publish-to-ghazdo description: Uploads a finalized SARIF file to GitHub Advanced Security for Azure DevOps (GHAzDO) using the Sarif.Multitool publish-to-ghazdo verb. metadata: author: sarif-sdk-maintainers version: "1.0.0" category: sarif severity: medium packages:
- "Sarif.Multitool >= 5.0.3"
triggers:
- "publish sarif"
- "upload sarif"
- "ghazdo"
- "advanced security azure devops"
- "sarif ingestion"
Publish SARIF to GHAzDO
Context
GitHub Advanced Security for Azure DevOps (GHAzDO) ingests SARIF through the Advanced Security alert API. The `publish-to-ghazdo` verb uploads a finalized SARIF file to that endpoint: it derives the Azure DevOps target from the run's version-control provenance, compresses the body, and posts it under a caller-supplied bearer secret.
Publish a SARIF only after it is finalized — its first run must carry an Azure DevOps repositoryUri and revisionId under versionControlProvenance (see `emit-sarif` and the emit-finalize verb). The target organization, project, and repository are read from that provenance, so an unfinalized file cannot be published.
Security model
This is the security-critical part of the skill. Follow it exactly.
- The secret never appears on the command line. Place the secret in an environment variable and
name that variable with --token-env-var (default 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.
- Two secret kinds, two schemes (auto-detected). An Entra access token is a JSON Web Token and
is sent as Authorization: Bearer <token>. An Azure DevOps personal access token (PAT) is opaque and is sent as Authorization: Basic base64(":" + PAT). The verb detects which by shape; you do not configure the scheme.
- `dev.azure.com` only. The target must be
https://dev.azure.com/<org>/<project>/_git/<repo>.
Legacy <org>.visualstudio.com, SSH, and credential-bearing repository URLs are rejected.
- PAT scope. A PAT must carry Advanced Security (Read & Write). An Entra token must be issued
for the Azure DevOps resource (499b84ac-1321-427f-aa17-267ca6975798).
Prerequisites
- `Sarif.Multitool` ≥ 5.0.3. Recommended invocation:
dotnet dnx Sarif.Multitool --yes -- publish-to-ghazdo ...
(zero-install, requires .NET 10+). Fall back to dotnet tool install --global Sarif.Multitool, then invoke as sarif publish-to-ghazdo ....
- A finalized SARIF file whose first run carries an Azure DevOps
repositoryUri. - A bearer secret (PAT or Entra access token) 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 secret. Default GHAZDO_TOKEN | |
{{API_VERSION}} | No | Advanced Security ingestion API version. Default 7.2-preview.1 |
Step 1 — Set the secret in the environment
Set the variable in the current session only. Do not write the secret to a file, a script, or shell history. Use your platform's mechanism for assigning a value without echoing it.
# PowerShell — paste the secret when prompted; it is not echoed to history.$env:GHAZDO_TOKEN = (Read-Host -AsSecureString | ForEach-Object { [System.Net.NetworkCredential]::new('', $_).Password })
# bash — read without echo.read -rs GHAZDO_TOKEN && export GHAZDO_TOKEN
Step 2 — Dry run (no network)
Always dry-run first. This resolves the target, reports the auth scheme that would be used, and prints the request shape without contacting the server and without printing the secret.
dotnet dnx Sarif.Multitool --yes -- publish-to-ghazdo "{{SARIF_FILE}}" --dry-run
Confirm the reported org/project/repo, the auth scheme (Bearer for an Entra token, Basic for a PAT), and the candidate POST URLs. If the target is wrong, the SARIF was not finalized against the intended Azure DevOps repository — re-finalize it; do not hand-edit the URL.
Step 3 — Validate the ingestion contract offline
The dry-run resolves the target; it does not inspect the SARIF's provenance. GHAzDO rejects a file at ingestion (HTTP 400) when run.automationDetails.id is null or empty, or when the azuredevops/pipeline/build/* build-definition properties are absent. Catch those offline before the round-trip by validating with the GHAzDO rule kind:
dotnet dnx Sarif.Multitool --yes -- validate "{{SARIF_FILE}}" --rule-kind "Sarif;AI;GHAzDO"
The GHAzDO kind activates the ingestion-contract checks — chiefly GHAzDO1014 (required run properties), GHAzDO1019 (the four azuredevops/pipeline/build/* keys), and GHAzDO1020 (automationDetails.id format). A clean GHAzDO-ruleset validation is the offline equivalent of GHAzDO ingestion acceptance. If it reports errors, the SARIF lacks pipeline provenance: re-emit it so the provenance is stamped — emit-run supplies the automationDetails.id and build-definition properties automatically when TF_BUILD=True. Do not hand-edit the file.
Step 4 — Publish
dotnet dnx Sarif.Multitool --yes -- publish-to-ghazdo "{{SARIF_FILE}}"
To name a different environment variable or API version:
dotnet dnx Sarif.Multitool --yes -- publish-to-ghazdo "{{SARIF_FILE}}" --token-env-var "{{TOKEN_ENV_VAR}}" --api-version "{{API_VERSION}}"
The verb posts to advsec.dev.azure.com and falls back to dev.azure.com on a 404. Exit code 0 means the server accepted the upload (HTTP < 400); a non-zero exit code with error: publish failed with HTTP <code> means it did not.
If `dotnet dnx` is not available: install the global tool and use the sarif command name:
dotnet tool install --global Sarif.Multitoolsarif publish-to-ghazdo "{{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-Azure-DevOps target — The repository is GitHub or a legacy
visualstudio.comhost. The
verb rejects it; publish supports dev.azure.com only.
- Secret unset — The named environment variable is empty or missing. The verb fails closed
before any network call. (Dry-run still prints the request shape and reports the scheme as undetermined.)
- 404 on both hosts — The org/project/repo path does not resolve, or the repository is not
onboarded to Advanced Security. Verify the target and PAT scope.
- 401/403 — The secret is invalid, expired, or lacks Advanced Security (Read & Write).
- HTTP 400 "invalid Sarif" (provenance gap) —
run.automationDetails.idis null/empty or the
azuredevops/pipeline/build/* properties are missing. This is detectable offline: the Step 3 GHAzDO-ruleset validation reproduces it as GHAzDO1014/1019/1020 so you never need the round-trip to find it. Re-emit with pipeline provenance (emit-run under TF_BUILD=True) and re-validate.
Validation
After running this skill:
- The secret never appeared in any printed command, log line, or error message.
- The GHAzDO-ruleset validation (
--rule-kind "Sarif;AI;GHAzDO") was clean before the live publish. - The dry-run target matched the intended Azure DevOps repository before the live publish.
- A successful publish returned exit code
0; a rejected one surfaced the HTTP status and a
non-zero exit code.
Escalation
- Multitool not available — Neither
dotnet dnxnor a global install succeeded. Report the gap;
the SARIF cannot be published from this environment.
- Repeated HTTP 4xx — After confirming target and scope, capture the response body the verb
prints (it does not contain the secret) and escalate to the repository's Advanced Security administrator.