To add Safeguard to Azure Pipelines, create a Safeguard secret key with the scans:trigger and scans:read scopes, store it as a secret pipeline variable named SAFEGUARD_API_KEY, and add one bash step that installs the CLI and runs safeguard ci scan --wait --fail-on critical. Each run builds an SBOM, matches dependency vulnerabilities and can fail the build on a severity or a policy gate.
There is no Azure DevOps extension to install: the integration is a shell step, a secret and, for Azure Repos, a branch policy. Below: the exact configuration from Safeguard's docs, a real policy gate, container images, SBOM files as artifacts, and the limits.
None of the YAML here was run for this article. Snippets are copied from docs.safeguard.sh as fetched on 25 October 2026, or assembled from documented flags where we say so. Check safeguard ci scan --help in the version you pin.
Key takeaways
- The Safeguard CLI (GA) runs in Azure Pipelines as a plain bash step on a Linux or macOS agent. Nothing is installed in Azure DevOps itself.
- SBOM generation and analysis (GA) and SCA with vulnerability management (GA) run on every scan. Each push becomes a new version of its Safeguard project, named after the branch and commit, for example
main-a1b2c3d. --fail-on <severity>fails the run on a dependency vulnerability at or above a severity.--fail-on-gate <gate>fails it when a policy gate is not Passed or Warning. Policies and gates (GA) give you Block, Warn and Notify actions, with exceptions for approved deviations.- SAST (GA) runs when the scan runs on Safeguard against a connected repository, and its findings show in Safeguard, but SAST is not part of
--fail-onyet. - Azure Pipelines does not pass secret variables to scripts automatically; map
SAFEGUARD_API_KEYthroughenv. - For Azure Repos, the
prblock in YAML is ignored. Pull request scans come from a Build validation branch policy on the target branch. - Container image scanning in CI (GA) and the Azure DevOps integration (GA, switched on per workspace) extend the setup. Autonomous remediation (GA) can open fix pull requests and ships switched off, every category set to Manual.
Why a scan belongs in the pipeline at all
A scan that runs on a laptop once a quarter, or a dashboard someone opens before an audit, does not stop a vulnerable dependency from merging on Tuesday and shipping on Wednesday. The pipeline is the one place every change passes through, which is why secure development standards keep pointing at it.
What the standards ask for
The US Executive Order 14028 set the tone in May 2021. Section 4(e) lists the practices federal suppliers are expected to follow, and two of them map straight onto a pipeline step. Clause 4(e)(iv) asks for "automated tools, or comparable processes" that check for known and potential vulnerabilities and remediate them, running regularly or at minimum before a product, version or update is released. Clause 4(e)(vii) asks suppliers to provide a purchaser a software bill of materials for each product, directly or on a public website.
NIST SP 800-218, the Secure Software Development Framework (SSDF) version 1.1, turns those ideas into tasks. Four matter here:
- PO.3.2: follow recommended security practices to deploy, operate and maintain toolchains. Its examples include code-based pipeline configuration, which is what
azure-pipelines.ymlis. - PO.4.2: gather and safeguard the information that supports your security criteria. One example is automating decisions on those criteria, which is what a policy gate does.
- PS.3.2: collect, safeguard, maintain and share provenance data for all components of each release, with an SBOM as the named example.
- PW.4.4: verify that third-party components comply with your requirements throughout their life cycles, including regular checks for publicly known vulnerabilities built into the toolchain.
EO 14028 also directed the Department of Commerce and NTIA to publish the minimum elements for an SBOM, which NTIA did on 12 July 2021. CISA's SBOM page now lists 2025 draft guidance and 2026 updates to those minimum elements, so the target is still moving. An SBOM generated by the build on every change can be regenerated when the expectations change; one assembled by hand before an audit cannot.
The two SBOM formats that matter are both formal standards. CycloneDX is published by Ecma International as ECMA-424, and its current version is 1.7. SPDX is an international open standard, ISO/IEC 5962:2021, with SPDX 3.0 as the current specification. Safeguard generates SBOMs in both.
What the CI security guidance warns about
The OWASP Top 10 CI/CD Security Risks project lists three risks that bear directly on adding a scanner:
- CICD-SEC-1, Insufficient Flow Control Mechanisms. Code reaches production without an enforced check. A scan that reports but never blocks is a flow control gap.
- CICD-SEC-3, Dependency Chain Abuse. Third-party packages enter the build without scrutiny.
- CICD-SEC-6, Insufficient Credential Hygiene. Secrets and tokens spread through the pipeline with too much access. OWASP's guidance is to scope each pipeline's credentials so every step has only the secrets it needs, and to keep secrets out of console output.
That last point shapes how you store the Safeguard key, which is why Step 3 below uses a key locked to one project with two scopes, mapped into exactly one step.
What the Safeguard step does in Azure Pipelines
The Safeguard docs describe the whole CI integration in one sentence: one step in your pipeline runs safeguard ci scan. Everything else is configuration around that step.
On each run, per the CI/CD Integration guide:
- The step installs the Safeguard CLI from
https://cli.safeguard.sh/installinto a folder you choose. safeguard ci scandetects the repository, branch and commit from the Azure Pipelines environment. Azure Pipelines is one of the five CI systems the CLI detects directly; for any other CI you pass--repository-url,--branchand--commit.- The scan creates a new version of the Safeguard project for that repository, named
<branch>-<commit>. On the first run the project is created for you, so nothing else is needed to start. - With
--wait, the step waits for the scan to finish (up to--timeout, default 30 minutes) and then exits with a code that Azure Pipelines turns into a pass or a fail. - With
--fail-on <severity>, it fails when a dependency vulnerability at or above that severity is found. With--fail-on-gate <gate>, it fails when the named policy gate is not Passed or Warning.
The results land in Safeguard as a project version (SBOM, components, vulnerability matches and, where the scan ran on Safeguard, SAST findings) and feed policies, prioritisation and remediation.
Two places the scan can run
The docs offer two modes, and the choice matters more in Azure DevOps than almost anywhere else.
| Mode | Command | Needs | Scans |
|---|---|---|---|
| On Safeguard (default) | safeguard ci scan | The repository connected as an integration, or public on github.com, gitlab.com or bitbucket.org | SAST, SBOM, dependency vulnerabilities |
| On the agent | safeguard ci scan --local | A checkout in the job; no integration | SBOM, dependency vulnerabilities |
With the default mode, the CI step asks Safeguard to scan the repository it already knows about. That means a private Azure Repos repository must be connected through the Azure DevOps integration first (Step 2 below). Azure Repos is not on the list of public hosts the default mode can reach without an integration.
With --local, the scan runs on the Azure Pipelines agent against the checkout the job already has, and only the results leave the agent. The docs say this "also works for self-hosted and firewalled Git servers". It reads dependencies from lockfiles and manifests such as package-lock.json, go.sum, requirements.txt and pom.xml. The trade-off is in the last column: SAST runs only in the default mode.
If your code lives in Azure DevOps Server (the self-hosted product), --local is the route. The Azure DevOps integration guide says plainly that Azure DevOps Server is not supported yet; only Azure DevOps Services at dev.azure.com can be connected.
Before you start
| You need | Where it comes from | Notes |
|---|---|---|
| A Safeguard workspace where you can create secret keys | Settings › Developer | Admins create keys; others ask an admin |
| An Azure DevOps project with Azure Pipelines | dev.azure.com | YAML pipelines; Classic is not covered here |
A Linux or macOS agent with bash and curl | Microsoft-hosted ubuntu-latest, or a self-hosted agent | The docs list Linux and macOS runners only |
| For the default mode, the repository connected in Safeguard | Integrations › Project/Product › Azure DevOps | The card is switched on per workspace; ask your Safeguard contact if you do not see it |
| For Azure Repos pull request scans, permission to edit branch policies | Project settings › Repositories › Policies | Build validation is a branch policy, not YAML |
Step by step: add Safeguard to an Azure Pipelines YAML pipeline
Step 1: Decide where the scan runs
Pick before you configure anything, because it decides whether you need Step 2.
- Azure Repos Git on Azure DevOps Services, and you want SAST too: connect the repository (Step 2) and use the default mode.
- Azure DevOps Server, a firewalled agent pool, or you do not want an integration at all: skip Step 2 and add
--localin Step 5. - A GitHub or Bitbucket repository built in Azure Pipelines: a public repository on github.com or bitbucket.org works in the default mode without an integration; a private one needs its own integration, or
--local.
Step 2 (default mode only): Connect Azure Repos to Safeguard
The Azure DevOps integration guide is the current reference (an older Azure Repos page defers to it). The short version:
- In Azure DevOps, open User settings › Personal access tokens and select New Token.
- Name it, select your organization (not "All accessible organizations"), and set an expiration date. The guide suggests the shortest lifetime that suits you, such as 90 days.
- Select only Code (Read) and Project and Team (Read). That is the least privilege setup for scanning. Add Code (Read & write) only if you want Safeguard to open fix pull requests, and Service Hooks (Read, write & manage) only if you want Safeguard to create push and pull request hooks for you.
- Create the token and copy it; Azure DevOps does not show it again.
- In Safeguard, open Integrations, choose the Project/Product tab and click Connect on the Azure DevOps card.
- Choose Private, enter a name, keep Personal Access Token under Sign in with, and enter the organization URL, for example
https://dev.azure.com/contoso. - Paste the token and click Check access. A warning about Code (Read & write) means scanning works but fix pull requests will not open.
- Click Next, choose a project (or All projects), select repositories and the branch or tag for each, and finish with Connect.
A personal access token scoped to "All accessible organizations" is not recommended: the guide notes Microsoft is retiring global tokens and that they stop working on 1 December 2026. Create an organization-scoped token instead.
For rotation and service hook setup, follow the Azure DevOps integration guide.
Step 3: Create a Safeguard secret key for the pipeline
The pipeline does not use your Azure DevOps token. It uses a Safeguard secret key.
- In Safeguard, open Settings › Developer.
- Create a secret key with the scopes
scans:triggerandscans:read. Those are the two scopes the CI step needs. - Lock it to one project; the CI/CD guide says you can. A key per pipeline, locked to its project, is the credential hygiene OWASP CICD-SEC-6 asks for: a leaked key cannot reach anything else.
The Azure DevOps card under Integrations › Source Control also has a Set up CI/CD button that opens this setup with your branches and severity filled in.
Step 4: Store the key as a secret variable in Azure Pipelines
From the Safeguard Azure Pipelines guide:
- In Azure DevOps, edit the pipeline and open Variables.
- Add a variable named
SAFEGUARD_API_KEY, paste the key, and turn on Keep this value secret.
A variable group works too. Microsoft recommends setting secrets in the pipeline UI, a variable group, or a variable group linked to Azure Key Vault, not with logging commands in scripts. Pipeline variables are scoped to one pipeline; variable groups share across pipelines.
The one behaviour that trips almost everyone: Microsoft's docs say secret variables, unlike normal ones, "are not automatically decrypted into environment variables for scripts." You must map them explicitly. The Safeguard YAML in the next step does that with an env block.
Step 5: Add the pipeline YAML
This is the Azure Pipelines file from the Safeguard docs, exactly as published. Save it as azure-pipelines.yml:
trigger:
branches:
include: [main]
pr:
branches:
include: [main]
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
- bash: |
set -eu
export SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
displayName: Safeguard scan
env:
SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)
What each part does:
triggerandprscan pushes tomainand pull requests into it. For Azure Repos,pris ignored (Step 6).pool: vmImage: ubuntu-latestruns on a Microsoft-hosted Ubuntu agent.set -eustops on the first error or unset variable.SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"installs the binary into the agent's temporary directory, which Azure Pipelines cleans up after the job. Nothing is left on a self-hosted agent between runs.curl -fsSL https://cli.safeguard.sh/install | bashinstalls the CLI. The installation docs say the installer detects the OS and architecture, resolves the release, verifies the download against the published checksums, and on macOS and Linux fails closed if the checksums file is missing.ci scan --wait --fail-on criticalruns the scan, waits for it, and fails the step on any critical dependency vulnerability.env: SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)maps the secret into the script's environment. The docs note: "Secret variables reach a script only through env".
The installation page itself says to read a script before you pipe it into a shell. The CLI is a single static binary with no runtime alongside it. If curl | bash is not allowed in your pipelines, mirror the reviewed installer and archives internally and pin a version (Step 8).
Step 6: Choose the branches, and set up pull request scans for Azure Repos
trigger.branches.include lists the branches whose pushes are scanned. pr.branches.include lists the target branches of pull requests that are scanned. Wildcards work, for example [main, release/*]. Use pr: none to scan pushes only.
For Azure Repos, Safeguard's guide says the pr block is ignored, and Microsoft's documentation agrees: for an Azure Repos Git repository you cannot configure a PR trigger in YAML. You set it as a branch policy instead.
- In Azure DevOps, open Project settings › Repositories, select the repository and open Policies, then the branch policies for the target branch (for example
main). - Under Build validation, add a policy and select the pipeline that contains the Safeguard step.
- Set the trigger to Automatic so a build queues when a pull request is created or updated.
- Set the policy to Required if you want a failing Safeguard step to block the pull request from completing. Optional gives you the signal without the block.
Microsoft's documentation notes that a build validation policy runs the pipeline on the merge commit of the source and target branches. Safeguard's guide says that on a pull request the source branch is scanned and its commit names the version. In practice, read the version name in Safeguard to see exactly what was scanned, and expect it to follow the branch and commit the pipeline reports.
This branch policy is what turns the scan into a pull request check in Azure Repos. Without it, --fail-on only fails post-merge builds on main, which tells you about a problem after it has already landed.
Step 7: Run it and read the result
Push the file to main, or open a pull request against a branch with the build validation policy. On the first run Safeguard creates the project. In the Safeguard app, open Projects and you will see a version named after the branch and commit, such as main-a1b2c3d, with its findings summary and vulnerability count.
In Azure Pipelines, the Safeguard scan step:
- passes when the scan finished and nothing met the
--fail-onthreshold; - fails when a dependency vulnerability at or above the threshold was found;
- fails when the scan itself failed or exceeded
--timeout.
Open the version to explore its SBOM, components and findings. The full analysis lives in Safeguard; the pipeline log shows the outcome.
Step 8: Tune the step
The Azure Pipelines guide lists these documented changes:
| Change | How |
|---|---|
| Report only | Drop --fail-on critical |
| Another threshold | --fail-on high (or medium, low) |
| Scan on the agent | Add --local (the job already has the checkout) |
| A specific project | Add --project-id <id> |
| Pin the CLI version | export SAFEGUARD_VERSION=<version> before the install line |
Pin the CLI so a release does not change a build under you, as the CI/CD guide puts it, and move the pin on purpose after reading the release notes.
--project-id matters when one repository maps to more than one Safeguard project, or more than one integration fits it. The CLI then stops with PROJECT_AMBIGUOUS or INTEGRATION_AMBIGUOUS, and the fix is to name the project (or --integration-id on the first run).
Turn the severity threshold into a policy gate
--fail-on critical knows one thing: dependency vulnerability severity. Real release rules also block a disallowed license, warn on abandoned packages or notify on medium findings. That is what Safeguard policies and gates (GA) are for.
Policies and gates in Safeguard's terms
The Policies and Gates docs separate the two:
- Policies are rules that evaluate your SBOMs and generate findings. They define acceptable risk thresholds and required controls, and they can be advisory or blocking.
- Gates are checkpoints that enforce policies. They integrate with CI/CD pipelines, block deployments when policies fail, and require approvals for exceptions.
To create a policy, open Settings › Policies, click Create Policy, define conditions with the visual builder or YAML, set severity and actions, then save and enable it. The docs list conditions on vulnerability severity, CVSS score, EPSS score, license, component age, maintenance and SLSA level, and pre-built templates such as No Critical Vulnerabilities, License Compliance, EO 14028 Compliance, SLSA Level 2 and No Abandoned Packages. If you would rather describe a rule in words, plain English policy creation (GA) turns a prompt into a policy; the related post on plain English policies covers that flow.
A note on CISA's Known Exploited Vulnerabilities catalog: Safeguard enriches findings with KEV status, and KEV works as a filter when you review findings. It is not listed as a policy condition in the gates docs, so do not plan a "block anything on KEV" gate around it unless the docs in your workspace show that condition.
Block, Warn and Notify
Each policy has one Action that decides what a failed policy does to the gate. From the Policy outcomes and actions page:
| Action | When the policy fails |
|---|---|
| Block | The gate fails |
| Warn | The gate still passes, with a warning that names the policy |
| Notify | The gate still passes, and Safeguard notifies your workspace's configured channels: the in-app bell, connected chat integrations, and email according to each person's preferences |
Notify is deliberately quiet about repeats. You are notified when a policy goes from passing (or never evaluated) to failing, and again when a failing policy finds new matching issues. A policy that keeps failing with the same matches is not notified again.
How a gate's outcome is decided
The docs give the order Safeguard uses to work out a gate's outcome from its linked policies:
- No policies linked: Not evaluated.
- Any Block policy failed: Failed.
- Otherwise, any policy could not be evaluated: Error, with the reason.
- Otherwise, any policy not evaluated yet: Not evaluated.
- Otherwise, any Warn policy failed: Warning. The gate passes, and the warning names the policy.
- Otherwise: Passed. Failed Notify policies are listed as notified.
An Error is never counted as a pass, and a gate that has never been evaluated shows Not evaluated, never Passed, so a gate cannot silently pass because nothing was checked. New policies have no threshold: any match fails them.
Wire the gate into Azure Pipelines
The docs give one command for failing a CI job on a gate:
safeguard ci scan --wait --fail-on-gate <gate name or id | all>
It waits for the scan and for the gate's policies to be evaluated on the new data, then:
- exits 0 when the gate is Passed or Warning, and prints warnings and notified policies;
- exits non-zero when the gate is Failed, and prints the Block policies that failed;
- exits non-zero when the gate is Error or Not evaluated, or its policies were not evaluated before
--timeout, with a message that says why.
The docs also say --fail-on <severity> keeps working and can be combined with --fail-on-gate. Here is the Safeguard step from Step 5 with both flags. We assembled this line from the two documented flags; it is not a published template and was not run:
- bash: |
set -eu
export SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical --fail-on-gate all
displayName: Safeguard scan and gate
env:
SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)
One prerequisite that is easy to miss: gate outcomes depend on a workspace setting. The docs say policy outcomes are turned on per workspace by Safeguard. If your Gates and Policies tables do not show an Outcome column yet, ask your Safeguard contact to turn it on before you rely on --fail-on-gate. While it is off, policies and gates behave as they did before.
In the app, the Outcome column under Policies › Gates and Policies › Policies shows the same results, and expanding a gate shows each linked policy and the evaluation history.
When a gate blocks: exceptions, not bypasses
A gate that blocks a legitimate release will be bypassed unless there is a better route. Safeguard's route is the exception workflow from the Policies and Gates docs:
- Click Request Exception.
- Select the policy and the finding.
- Provide a business justification.
- Set an expiration date.
- Submit for approval.
Approvers are notified, one decides, and the decision goes into the audit trail. That is the answer to "the gate blocked a hotfix": a time-boxed exception with a reason and an accountable approver, not someone deleting --fail-on from azure-pipelines.yml and forgetting to put it back.
For a deeper treatment of rule design, read the related post on plain English security policies to release gates.
Scan the container images your pipeline builds
Scanning the repository misses the operating system packages a Dockerfile adds. Container image scanning in CI (GA) covers them with one more step after the build.
From the Container images guide, the command is:
safeguard ci scan --image "ghcr.io/acme/api:$TAG" --environment prod --wait --fail-on critical
It scans the image on the agent and records it in Safeguard under the image's repository, with one version per environment and tag. For example, ghcr.io/acme/api:1.4.2 with --environment prod becomes version prod-1.4.2; the same image with staging becomes staging-1.4.2; and a latest or missing tag uses the build's commit, for example dev-<commit>.
Where the image is read from:
- The local Docker daemon, when the step runs on the agent that built the image. Nothing to set up.
- A registry, otherwise. Set
SAFEGUARD_REGISTRY_USERNAMEandSAFEGUARD_REGISTRY_PASSWORDas secrets so the scan can pull it.
Only the results leave the agent, and no repository integration is needed. The Azure Container Registry card under Integrations also has a Set up CI/CD button: pick your CI, the image and the environments, and it gives you the steps.
The Container images guide shows its example in GitHub Actions syntax only. Here is the same step in the Azure Pipelines shape from the Azure guide. We assembled it from the two docs pages and did not run it; replace the registry and image name with your own, and keep the build step that produces the image before it:
- bash: |
set -eu
export SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --image "myregistry.azurecr.io/api:$(Build.SourceVersion)" --environment prod --wait --fail-on critical
displayName: Safeguard image scan (prod)
env:
SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)
Use one step per environment, each with its own --environment (letters, digits and dashes). The docs suggest failing only where it matters: --fail-on critical in production, none in development.
The related post on tracking container images per environment goes further into environments and thresholds.
Keep SBOM files as pipeline artifacts
safeguard ci scan puts the SBOM and results into Safeguard. Some teams also need the SBOM as a file attached to the build, for a release bundle, a customer request or an internal archive. The docs cover this with a different command, safeguard scan, which runs fully on the agent and writes files.
From the CI/CD guide:
safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
| index("critical") | not' out/*/vuln.json
The CLI usage page lists what lands in the output folder: a folder per scan with vuln.json, license.json and collection.json among others. Three things to know before you use it in Azure Pipelines:
- It does not fail on findings.
safeguard scanexits 0 when the scan ran, whatever it found, 1 when the command itself failed, and 2 when results were written but the vulnerability scan did not run. Exit code 2 exists so a script never mistakes a skipped scan for a clean pass. If you want this route to block, gate on the files, as thejqline above does. - It needs its scanners on the PATH. The docs say
safeguard scanruns syft and grype, two open source tools. The interactive terminal downloads them on first use, but a fresh CI agent has never opened the terminal, so install both on thePATHfirst or pointTOOLS_DIRat a directory that holds them. - It is separate from the platform record. Use
ci scanfor the gate and the project version, andscanwhen you also want local files.
An older Azure Repos page in the docs shows the publishing half in Azure Pipelines syntax, with a PublishBuildArtifacts@1 task:
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: './safeguard-results'
artifactName: 'safeguard-results'
displayName: 'Publish scan results'
Treat it as a pattern, and take the scan step itself from the current CI/CD guide.
For SBOMs you receive, safeguard validate sbom.json --strict checks CycloneDX, SPDX and VEX documents against their specifications and treats warnings as errors, and safeguard scan --sbom sbom.json matches an existing SBOM against vulnerability databases.
For what a good SBOM contains and how Safeguard handles SPDX and CycloneDX, read SBOM generation and analysis in Safeguard.
Where attestation fits, and where it does not yet
SSDF PS.3.2 and EO 14028 both talk about provenance as well as SBOMs. Safeguard's attestation and signing capability (GA) produces in-toto attestations, including SLSA provenance, CycloneDX and SPDX SBOM attestations and a vulnerability scan snapshot. The attestation docs say every attestation Safeguard produces is signed with Safeguard's own key, held in a cloud key management service, and verified with cosign against Safeguard's published public key. Signatures are not written to a public transparency log, so verification passes --insecure-ignore-tlog.
For an Azure Pipelines setup, be precise about what is documented today:
- The CI/CD guide's Azure Pipelines page shows a scan step, not an attestation step, and the CI docs say there is no plugin, Action or Pipe to install.
- The attestation docs say there is no CLI command for signing container images and packages yet, and no CLI command for verifying or listing attestations yet.
- Attestation records for your projects, products and components appear on their Attestation tab in the Safeguard app, where SCAL levels rate each component's attestation from 0 (fully attested) to 5 (unknown origin).
In this pipeline, then, attestation is something you review in the app, not a step in azure-pipelines.yml. If you need signed provenance produced inside Azure Pipelines today, plan it with your own signing tooling. The attestation and provenance post covers SCAL scores and verification in more depth.
Decision table: which setup fits your pipeline
| Your situation | Mode | What to add | What you get |
|---|---|---|---|
| Azure Repos on dev.azure.com, want SAST and pull request checks | Default | Azure DevOps integration, secret key, YAML, Build validation policy | SBOM, dependency vulnerabilities, SAST in Safeguard, PR blocking on --fail-on |
| Azure DevOps Server (self-hosted) | --local | Secret key, YAML with --local | SBOM, dependency vulnerabilities; no SAST |
| GitHub or Bitbucket repository built in Azure Pipelines | Default (public) or integration (private) | Secret key, YAML; the pr block works as shown | Push and PR scans |
| Pipeline builds a container image | Image scan | Image step after the build, per environment | One version per environment and tag |
| You need the SBOM file with the build | On-agent safeguard scan | Scanners on PATH, jq gate, publish artifact | Local JSON files plus your own gate |
| Release rules beyond severity | Default or --local plus a gate | Policies with Block, Warn, Notify; --fail-on-gate | Gate outcome drives the build |
A practical rollout: report only on main for a week, write policies for the rules you actually want, then switch on --fail-on-gate with a Required build validation policy. Starting at --fail-on low on every pull request usually gets the step removed within a day.
Requirement versus what Safeguard does versus what you still own
| Requirement | What Safeguard does in this pipeline | What you still own |
|---|---|---|
| EO 14028 4(e)(iv): automated vulnerability checks before release | safeguard ci scan on every push and pull request, with --fail-on or --fail-on-gate | Making the build validation policy Required, and not bypassing it |
| EO 14028 4(e)(vii): an SBOM for each product | An SBOM per project version, in Safeguard; SPDX and CycloneDX formats (GA) | Deciding how and where you share SBOMs with purchasers |
| SSDF PO.3.2: secure toolchain configuration | Pipeline as code, pinned CLI version, checksum verified installer | Reviewing the installer, mirroring it if required, agent hardening |
| SSDF PO.4.2: automate decisions on security criteria | Policies and gates with Block, Warn and Notify | Writing the criteria and approving exceptions |
| SSDF PS.3.2: provenance and SBOM per release | SBOM per version; attestation records on the Attestation tab | Signing inside your own pipeline where you need it today |
| SSDF PW.4.4: verify third-party components over their life cycle | Matching against vulnerability databases, with EPSS and KEV enrichment, plus continuous rescans of projects | Acting on findings in components you cannot upgrade |
| OWASP CICD-SEC-1: flow control | A failing step plus a Required branch policy | Who can edit branch policies and pipelines |
| OWASP CICD-SEC-6: credential hygiene | A two-scope key, lockable to one project, mapped into one step | Rotation, variable group permissions, fork build settings |
Troubleshooting
The Azure guide and the CI/CD guide list these errors and fixes:
| Message | What it means | What to do |
|---|---|---|
AUTHENTICATION_REQUIRED | The secret is not mapped into env, or the variable has a different name | Check the env block and the variable name |
INVALID_SECRET_KEY | The key is invalid, expired or revoked | Create a new key and update the secret |
MISSING_SCOPE | The key lacks scans:trigger or scans:read | Create a key with both |
PROJECT_NOT_ALLOWED | The key is locked to another project | Use the right key, or add --project-id for the locked project |
PROJECT_AMBIGUOUS or INTEGRATION_AMBIGUOUS | More than one project or integration fits the repository | Pass --project-id (or --integration-id on the first run) |
PROJECT_NOT_RESCANNABLE | The project has no repository Safeguard can clone, for example an upload project | Connect the repository under Integrations, or use --local |
SCAN_NOT_DISPATCHED or SCAN_NOT_STARTED | The platform could not start the scan | Retry; if it persists, contact support with the build URL |
| The step never runs | The branch is not in the trigger list, or a fork build did not receive secrets | Check trigger and pr, and the fork settings |
On fork builds: the Safeguard guide notes that secrets are not passed to fork builds unless you allow it in the pipeline's trigger settings. Microsoft's documentation for GitHub repositories says the same, and adds that when you do allow it, fork builds get a restricted access token by default. Be careful with Make secrets available to builds of forks: a fork pull request can change the pipeline file itself, the poisoned pipeline execution risk OWASP lists as CICD-SEC-4.
For CLI problems, safeguard doctor checks the environment, sign-in and tools; read its output, not its exit code.
What Safeguard does not do
These are the honest limits of this setup today, from the docs we fetched.
- No Azure DevOps extension or Marketplace task. The CI docs say there is no plugin, Action or Pipe to install.
- No Windows agents in the documented CI setup. The CI/CD guide says every system runs the CLI in a plain shell step on a Linux or macOS runner with
bashandcurl. Plan a Linux job for the scan even if your build runs on Windows. - SAST does not fail the build yet. SAST findings are scanned and shown in Safeguard in the default mode, but the docs say they are not part of
--fail-onyet. With--local, SAST does not run at all. - No Azure DevOps Server through the integration. The integration guide says only Azure DevOps Services can be connected. Azure DevOps Server works only through
--localscans on your own agents. - No service principal, managed identity or Microsoft Entra ID sign-in for the integration yet. The guide lists all three as not available; the integration takes a personal access token only.
- Gate outcomes need a workspace switch. Policy outcomes are turned on per workspace by Safeguard. Without them,
--fail-on-gateis not the tool to rely on. - No signing step in this pipeline. There is no CLI command for signing or for verifying attestations yet, so signed provenance produced inside Azure Pipelines is your tooling today.
- The on-agent scan does not fail on findings.
safeguard scanreports and exits 0; you write the gate. - Not every lockfile. The CLI does not read
uv.lock,pdm.lockor Paket files today. For .NET teams using Paket, check what the scan actually found before you trust a clean result. - It does not replace your branch protections or reviews. The Safeguard step is one check. Who may edit the pipeline file, who may change branch policies, and how many reviewers a pull request needs are still Azure DevOps settings you own.
Common mistakes
Forgetting the env mapping. The most common failure. If the log says AUTHENTICATION_REQUIRED, check the env block first.
Expecting the pr block to work on Azure Repos. It is ignored. Without a Build validation policy, you learn about a critical vulnerability only after merge.
Leaving the build validation policy Optional. An optional policy shows a red build and still lets the pull request complete. If the point is to stop a merge, make it Required.
Using a global personal access token for the integration. The guide notes global tokens stop working on 1 December 2026. Create a token for the one organization you connect.
One key for every pipeline. A shared key with broad reach is the CICD-SEC-6 problem in miniature. Use a key per pipeline, locked to its project where you can.
Not pinning the CLI. An unpinned install picks up new releases mid-sprint. Set SAFEGUARD_VERSION.
Treating safeguard scan exit code 0 as clean. It means the scan ran. Exit code 2 means results were written but the vulnerability scan did not run. Gate on the files, or use ci scan.
Forgetting who can bypass policies. Microsoft's docs describe bypass permissions that let a user push directly to a protected branch or complete a pull request that does not satisfy its policies. A Required build validation policy only holds for people who do not have them. Review who does.
Enabling secrets for fork builds on a public repository. It hands your key to anyone who opens a pull request. Keep fork builds report only.
FAQ
Is there a Safeguard extension for Azure DevOps?
No. The CI/CD docs say there is no plugin, Action or Pipe to install. The integration is a bash step that installs the Safeguard CLI and runs safeguard ci scan. The only things you configure in Azure DevOps are a secret variable and, for Azure Repos pull requests, a branch policy.
How do I fail an Azure pipeline on critical vulnerabilities with Safeguard?
Add --wait --fail-on critical to safeguard ci scan, as in the documented YAML. Use high, medium or low for other thresholds, or drop --fail-on to report only.
Why does the Safeguard step fail with AUTHENTICATION_REQUIRED in Azure Pipelines?
Almost always because the secret is not mapped into the step's environment. Azure Pipelines does not decrypt secret variables into environment variables automatically. Add env: SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY) to the bash step and check the variable name matches exactly.
How do I scan pull requests in Azure Repos?
Add a Build validation branch policy on the target branch and select the pipeline that runs the Safeguard step. The YAML pr block is ignored for Azure Repos. Set the policy to Required if a failing scan should block completion.
Can Safeguard scan code in Azure DevOps Server?
Yes, from the pipeline, with safeguard ci scan --local. The scan runs on your agent against the checkout and only the results leave it. The Azure DevOps integration itself does not support Azure DevOps Server yet, so SAST in the default mode is not available for it.
What is the difference between --fail-on and --fail-on-gate?
--fail-on checks one thing: dependency vulnerability severity. --fail-on-gate evaluates a policy gate in Safeguard, which can combine conditions such as CVSS, EPSS, license, component age and maintenance, with Block, Warn and Notify actions. They can be combined in one command.
Does the Safeguard scan generate an SBOM in Azure Pipelines?
Yes. Every safeguard ci scan builds an SBOM for the new project version in Safeguard. If you also want SBOM and result files attached to the build, run safeguard scan --dir . --output on the agent and publish the folder as a pipeline artifact.
Can I scan container images built in Azure Pipelines?
Yes. Add safeguard ci scan --image <ref> --environment <name> --wait after the build step. It reads from the local Docker daemon on the same agent, or pulls from a registry with SAFEGUARD_REGISTRY_USERNAME and SAFEGUARD_REGISTRY_PASSWORD.
Will Safeguard open fix pull requests in Azure Repos?
It can, if you grant Code (Read & write) on the integration token and turn on autonomous remediation. Remediation ships switched off with every category set to Manual, so nothing opens until you choose to enable it, and a person still reviews and completes the pull request under your branch policies.
After the gate: fixing what it finds
A gate tells you a build should not ship. The next question is how fast you can make it shippable again, and that is what the section below covers. Its first option, autonomous remediation, is the same flow that opens fix pull requests in Azure Repos once you grant the write scope and switch it on.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. You install the CLI, run safeguard to open the interactive terminal and ask it to "fix the lodash CVE in this repo". It reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test, prints a summary and pushes nothing. It is the same binary your Azure pipeline installs; on a developer machine it is also the quickest way to clear the finding that just failed the gate.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with its device code, and then scans from the terminal. It shows the same idea as --local in Azure Pipelines: the scanning happens on infrastructure you control, which is the route for Azure DevOps Server and firewalled agent pools.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to Safeguard. You add Safeguard from Claude's connector directory, where it has a community listing, sign in with OAuth, and choose which tools Claude can use. The policy outcomes docs say Safeguard's MCP server has read-only tools for gate outcomes, policy outcomes and evaluation history, so once your pipelines are gated you can ask why a gate failed without opening the app, within the tools your administrator allows.
Click through it at your own pace, or open it full screen.
To try this in your own Azure DevOps organization, install the CLI from https://safeguard.sh/download and follow the CI/CD and Azure DevOps guides at https://docs.safeguard.sh/docs .
Related: choosing between a repository connection, the CLI or the local runner, and how autonomous remediation is configured.
Quick check
Why does a pull request in Azure Repos never trigger the Safeguard scan?
For Azure Repos the YAML pr block is ignored. Add a Build validation branch policy on the target branch that runs the pipeline, and set it to Required if a failing scan should block the pull request.
What does a Warn policy do to a gate when it fails?
The gate still passes, with a warning that names the policy, so safeguard ci scan --fail-on-gate exits 0. Only a failed Block policy fails the gate, and an Error or Not evaluated gate also fails the step.
Sources
- Safeguard docs: Azure Pipelines
- Safeguard docs: CI/CD Integration
- Safeguard docs: Container images in CI
- Safeguard docs: Azure DevOps integration
- Safeguard docs: Azure Repos (ESSCM)
- Safeguard docs: Policies and Gates
- Safeguard docs: Policy outcomes and actions
- Safeguard docs: CLI usage
- Safeguard docs: CLI installation
- Safeguard docs: Generate SBOM
- Safeguard docs: Attestation and Signing
- Safeguard docs: ESSCM Attestation
- Microsoft Learn: Set secret variables
- Microsoft Learn: Variable groups
- Microsoft Learn: Branch policies and settings
- Microsoft Learn: Build Azure Repos Git repositories
- Microsoft Learn: Build GitHub repositories
- Microsoft Learn: Microsoft-hosted agents
- Microsoft Learn: Predefined variables
- Executive Order 14028, Improving the Nation's Cybersecurity (Federal Register)
- NIST SP 800-218, Secure Software Development Framework v1.1
- NTIA: The Minimum Elements for a Software Bill of Materials
- CISA: Software Bill of Materials
- CycloneDX specification overview
- SPDX specifications
- OWASP Top 10 CI/CD Security Risks
- OWASP CICD-SEC-6: Insufficient Credential Hygiene
Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.