Safeguard
Product•

Add Safeguard to Azure Pipelines: SBOM, Scan and Policy Gate

Add one bash step to azure-pipelines.yml, map a Safeguard secret key through env, and fail the run on a severity or a policy gate. Config and limits.

Safeguard Security Team
40 min read

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-on yet.
  • Azure Pipelines does not pass secret variables to scripts automatically; map SAFEGUARD_API_KEY through env.
  • For Azure Repos, the pr block 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.yml is.
  • 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:

  1. The step installs the Safeguard CLI from https://cli.safeguard.sh/install into a folder you choose.
  2. safeguard ci scan detects 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, --branch and --commit.
  3. 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.
  4. 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.
  5. 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.

ModeCommandNeedsScans
On Safeguard (default)safeguard ci scanThe repository connected as an integration, or public on github.com, gitlab.com or bitbucket.orgSAST, SBOM, dependency vulnerabilities
On the agentsafeguard ci scan --localA checkout in the job; no integrationSBOM, 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 needWhere it comes fromNotes
A Safeguard workspace where you can create secret keysSettings › DeveloperAdmins create keys; others ask an admin
An Azure DevOps project with Azure Pipelinesdev.azure.comYAML pipelines; Classic is not covered here
A Linux or macOS agent with bash and curlMicrosoft-hosted ubuntu-latest, or a self-hosted agentThe docs list Linux and macOS runners only
For the default mode, the repository connected in SafeguardIntegrations › Project/Product › Azure DevOpsThe 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 policiesProject settings › Repositories › PoliciesBuild 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 --local in 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:

  1. In Azure DevOps, open User settings › Personal access tokens and select New Token.
  2. 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.
  3. 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.
  4. Create the token and copy it; Azure DevOps does not show it again.
  5. In Safeguard, open Integrations, choose the Project/Product tab and click Connect on the Azure DevOps card.
  6. 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.
  7. Paste the token and click Check access. A warning about Code (Read & write) means scanning works but fix pull requests will not open.
  8. 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.

  1. In Safeguard, open Settings › Developer.
  2. Create a secret key with the scopes scans:trigger and scans:read. Those are the two scopes the CI step needs.
  3. 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:

  1. In Azure DevOps, edit the pipeline and open Variables.
  2. 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:

  • trigger and pr scan pushes to main and pull requests into it. For Azure Repos, pr is ignored (Step 6).
  • pool: vmImage: ubuntu-latest runs on a Microsoft-hosted Ubuntu agent.
  • set -eu stops 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 | bash installs 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 critical runs 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.

  1. In Azure DevOps, open Project settings › Repositories, select the repository and open Policies, then the branch policies for the target branch (for example main).
  2. Under Build validation, add a policy and select the pipeline that contains the Safeguard step.
  3. Set the trigger to Automatic so a build queues when a pull request is created or updated.
  4. 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-on threshold;
  • 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:

ChangeHow
Report onlyDrop --fail-on critical
Another threshold--fail-on high (or medium, low)
Scan on the agentAdd --local (the job already has the checkout)
A specific projectAdd --project-id <id>
Pin the CLI versionexport 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:

ActionWhen the policy fails
BlockThe gate fails
WarnThe gate still passes, with a warning that names the policy
NotifyThe 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:

  1. No policies linked: Not evaluated.
  2. Any Block policy failed: Failed.
  3. Otherwise, any policy could not be evaluated: Error, with the reason.
  4. Otherwise, any policy not evaluated yet: Not evaluated.
  5. Otherwise, any Warn policy failed: Warning. The gate passes, and the warning names the policy.
  6. 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:

  1. Click Request Exception.
  2. Select the policy and the finding.
  3. Provide a business justification.
  4. Set an expiration date.
  5. 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_USERNAME and SAFEGUARD_REGISTRY_PASSWORD as 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 scan exits 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 the jq line above does.
  • It needs its scanners on the PATH. The docs say safeguard scan runs 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 the PATH first or point TOOLS_DIR at a directory that holds them.
  • It is separate from the platform record. Use ci scan for the gate and the project version, and scan when 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 situationModeWhat to addWhat you get
Azure Repos on dev.azure.com, want SAST and pull request checksDefaultAzure DevOps integration, secret key, YAML, Build validation policySBOM, dependency vulnerabilities, SAST in Safeguard, PR blocking on --fail-on
Azure DevOps Server (self-hosted)--localSecret key, YAML with --localSBOM, dependency vulnerabilities; no SAST
GitHub or Bitbucket repository built in Azure PipelinesDefault (public) or integration (private)Secret key, YAML; the pr block works as shownPush and PR scans
Pipeline builds a container imageImage scanImage step after the build, per environmentOne version per environment and tag
You need the SBOM file with the buildOn-agent safeguard scanScanners on PATH, jq gate, publish artifactLocal JSON files plus your own gate
Release rules beyond severityDefault or --local plus a gatePolicies with Block, Warn, Notify; --fail-on-gateGate 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

RequirementWhat Safeguard does in this pipelineWhat you still own
EO 14028 4(e)(iv): automated vulnerability checks before releasesafeguard ci scan on every push and pull request, with --fail-on or --fail-on-gateMaking the build validation policy Required, and not bypassing it
EO 14028 4(e)(vii): an SBOM for each productAn 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 configurationPipeline as code, pinned CLI version, checksum verified installerReviewing the installer, mirroring it if required, agent hardening
SSDF PO.4.2: automate decisions on security criteriaPolicies and gates with Block, Warn and NotifyWriting the criteria and approving exceptions
SSDF PS.3.2: provenance and SBOM per releaseSBOM per version; attestation records on the Attestation tabSigning inside your own pipeline where you need it today
SSDF PW.4.4: verify third-party components over their life cycleMatching against vulnerability databases, with EPSS and KEV enrichment, plus continuous rescans of projectsActing on findings in components you cannot upgrade
OWASP CICD-SEC-1: flow controlA failing step plus a Required branch policyWho can edit branch policies and pipelines
OWASP CICD-SEC-6: credential hygieneA two-scope key, lockable to one project, mapped into one stepRotation, variable group permissions, fork build settings

Troubleshooting

The Azure guide and the CI/CD guide list these errors and fixes:

MessageWhat it meansWhat to do
AUTHENTICATION_REQUIREDThe secret is not mapped into env, or the variable has a different nameCheck the env block and the variable name
INVALID_SECRET_KEYThe key is invalid, expired or revokedCreate a new key and update the secret
MISSING_SCOPEThe key lacks scans:trigger or scans:readCreate a key with both
PROJECT_NOT_ALLOWEDThe key is locked to another projectUse the right key, or add --project-id for the locked project
PROJECT_AMBIGUOUS or INTEGRATION_AMBIGUOUSMore than one project or integration fits the repositoryPass --project-id (or --integration-id on the first run)
PROJECT_NOT_RESCANNABLEThe project has no repository Safeguard can clone, for example an upload projectConnect the repository under Integrations, or use --local
SCAN_NOT_DISPATCHED or SCAN_NOT_STARTEDThe platform could not start the scanRetry; if it persists, contact support with the build URL
The step never runsThe branch is not in the trigger list, or a fork build did not receive secretsCheck 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 bash and curl. 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-on yet. 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 --local scans 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-gate is 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 scan reports and exits 0; you write the gate.
  • Not every lockfile. The CLI does not read uv.lock, pdm.lock or 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 situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull 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

Last reviewed: 25 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.