Safeguard
Product•

Safeguard Scan Modes in CI: Fast on PRs, Deep on Main, Progressive

Set Safeguard's scan mode per trigger: fast on pull requests, deep on main and releases, progressive on a schedule. Configs for five CI systems and limits.

Safeguard Security Team
42 min read

To run a different Safeguard scan depth at each pipeline stage, add --scan-mode fast, --scan-mode deep or --scan-mode progressive to the safeguard ci scan step, and pick the value from what triggered the run. The common pattern is Fast on pull requests, Deep on pushes to main and on releases (with the gate that can fail the build), and Progressive on a scheduled run that gives you quick results first and full depth after. The flag has been in the Safeguard CLI since v11.2.0, and without it the CI scan uses the mode set on the repository's integration.

This post is the configuration companion to Fast, Deep or Progressive Scans: Which Depth for Each Pipeline Stage, which covers the reasoning about time budgets and what each stage needs. Here the focus is the files you commit: one working layout each for GitHub Actions, GitLab CI/CD, Jenkins, Azure Pipelines and Bitbucket Pipelines, built from the templates in Safeguard's docs, plus the gate wiring and the honest limits.

Key takeaways

  • Safeguard's scan modes (GA) are Fast, Deep and Progressive. The connect menu in the GitHub integration guide describes them as "Fast scan (quicker)", "Deep scan (full)" and "Progressive scan (fast now, full depth after)".
  • In CI, safeguard ci scan --scan-mode fast|deep|progressive picks the mode per run (CLI v11.2.0 and later). Without the flag, the integration's setting applies. The CI/CD docs page does not list the flag yet, so confirm it with safeguard ci scan --help on the version you pin.
  • The Safeguard CLI (GA) and CI/CD integration (GA) run in a plain shell step. The docs say there is no plugin, Action or Pipe to install.
  • Fail the build with --wait plus --fail-on <severity> for dependency vulnerabilities, or --fail-on-gate <gate> for a policy gate. Policies and gates (GA) use three actions: Block, Warn and Notify, with approval handled through exceptions.
  • SAST (GA) findings are scanned and shown in Safeguard but, per the docs, are not part of --fail-on yet.
  • Gate failure in CI depends on policy outcomes, which Safeguard turns on per workspace. Check the Outcome column exists before you rely on --fail-on-gate.
  • The local runner (GA) and --local scans keep source code on your machine and upload only results.
  • Autonomous remediation (GA) can open fix pull requests for what the scans find. It ships switched off, with every category set to Manual.
  • Every snippet below is assembled from Safeguard's documented templates and documented flags. None of them was run for this post. Test each one on a branch first.

Why one scan profile breaks a pipeline

Most teams add a security scan to CI once, with one set of flags, and attach it to every trigger. That single profile then has to serve three jobs that pull in opposite directions.

A pull request scan has to come back while the author is still looking at the change. If it takes long enough that people switch tasks, they stop reading it. If it blocks on findings the author did not introduce, they learn to rerun it or ask for an override, and the check loses its authority.

A scan of main, or of a release candidate, has the opposite job. Nobody is waiting on it at a keyboard, but whatever it misses ships. It should walk the full dependency graph and do the slower analysis, and its result is what the release gate should read.

A scheduled scan has a third job. Code that has not changed can still become vulnerable when a new advisory is published or an existing one is added to an exploited list. Something has to look at unchanged code on a calendar, and it should not need a commit to start.

Scan modes exist so one connected repository can serve all three jobs, and the CI configuration is where you decide which mode runs when.

What the standards ask for

None of the major frameworks prescribes a scan depth per pipeline stage, but several of them describe the outcomes a staged design delivers.

NIST SP 800-218, the Secure Software Development Framework (SSDF), has a practice for defining and using criteria for software security checks. Task PO.4.1 asks you to define criteria for software security checks and track them throughout the lifecycle, and PO.4.2 asks for processes and mechanisms that gather the information those criteria need. The notional examples for PO.4.1 include recording security check approvals, rejections and exception requests in the workflow, and the examples for PO.4.2 include automating decision making based on the criteria. A gate that reads a deep scan on the release commit, with an exception workflow beside it, is a direct implementation of those two tasks.

SSDF task PW.4.4 asks you to verify that acquired open source and other third party components comply with your requirements throughout their life cycles. Its examples include regularly checking for publicly known vulnerabilities and building automatic detection of known vulnerabilities into the toolchain. "Throughout their life cycles" is the scheduled scan: a component you accepted last month still has to be rechecked.

SSDF task RV.1.1 asks you to gather information on potential vulnerabilities in your software and its third party components; one example is automatically reviewing software composition data for all components to find new vulnerabilities.

NIST SP 800-204D, on software supply chain security in DevSecOps CI/CD pipelines, is more specific about depth. In its discussion of open source dependencies it says dependency file detectors should find all dependencies, including transitive ones, with preferably no limit to the depth of nesting that is analysed. That is the argument for running a full depth scan somewhere in the pipeline even if pull requests get a quicker pass. The same document, in its DEPLOY_REQ-4 requirement for container images, makes a point about timing: because scanning tools are continuously updated, more recent scan results are more likely to be accurate and give better assurance than older ones. That is the argument for the schedule.

Two public data sources shape what a scheduled scan should look for. CISA's Known Exploited Vulnerabilities catalog lists vulnerabilities with evidence of exploitation, and its entries now point agencies to BOD 26-04, the risk based directive on prioritising security updates. FIRST's Exploit Prediction Scoring System (EPSS) publishes a daily probability, between 0 and 1, that a published CVE will be exploited in the next 30 days. Both change without any change to your code, which is why a calendar trigger belongs in the design.

The three scan modes, as Safeguard documents them

Safeguard's scan modes are GA. They show up in two places in the docs and the product: the connect step for a source control integration, and the CI scan command.

ModeHow the docs describe itOffering guidance on usePipeline stage it suits
Fast (GA)"Fast scan (quicker)"Quick feedback in CI, pre-merge checksPull requests and merge requests
Deep (GA)"Deep scan (full)"Full transitive analysis, thorough pre-release scansPushes to main, release branches and tags
Progressive (GA)"Progressive scan (fast now, full depth after)"Quick results first, with the full depth result followingScheduled runs, first scans of large repositories

The quoted wording comes from the GitHub integration guide, where the Connect button's menu offers Connect: Fast scan (quicker), Connect: Deep scan (full) and Connect: Progressive scan (fast now, full depth after). The GitLab, Bitbucket and Azure DevOps guides list the same three choices on their Connect buttons.

What the docs do not say about the modes

The public docs do not list what Fast leaves out compared with Deep, and they do not publish a time for either. Do not assume Fast behaves like a generic "quick scan" from another tool, and do not assume Deep takes a particular number of minutes on your code. The practical way to learn the difference is to run both on one of your own repositories, compare the findings and the time each took, and make your stage choices from that measurement.

The docs also do not say whether --wait on a Progressive run returns after the fast pass or after the full depth pass. That matters for gating, and the configs below are built so it does not: Progressive runs on the schedule as report only, and the gated release scan is always Deep.

How the CI step picks a mode

The CI command is safeguard ci scan. Since CLI v11.2.0 it accepts --scan-mode with one of three values:

safeguard ci scan --scan-mode fast
safeguard ci scan --scan-mode deep
safeguard ci scan --scan-mode progressive

When you leave the flag out, the scan uses the mode set on the integration the repository is connected through. That default is what makes a mixed setup possible: connect the repository with the mode you want for anything that does not pass a flag (for example Deep, so manual and scheduled rescans from the app are full depth), then override per trigger in CI.

The CI/CD Integration page in the docs does not list --scan-mode yet. The CLI usage page says safeguard <command> --help lists the flags in the build you have, and that if the page and --help disagree, --help is right. So before you commit any config from this post, run this against the version you plan to pin:

safeguard ci scan --help

If --scan-mode is missing from the output, the installed CLI is older than v11.2.0.

The parts every pipeline config shares

Before the per system files, here is what all five have in common, taken from Safeguard's CI/CD Integration page.

A secret key with two scopes. Create a secret key in Safeguard under Settings › Developer with the scopes scans:trigger and scans:read. You can lock a key to one project, which is a sensible default for CI. Store it in your CI system as a secret named SAFEGUARD_API_KEY. Each CI guide says where that secret lives.

A plain shell step. Every system runs the CLI with bash and curl on a Linux or macOS runner. There is nothing to install from a marketplace. The install line is:

curl -fsSL https://cli.safeguard.sh/install | bash

A pinned CLI version. The docs recommend pinning with SAFEGUARD_VERSION=<version> on the install line so a new release does not change a build under you. For scan modes this matters twice: the pinned version has to be v11.2.0 or later, or --scan-mode will not exist.

Where the scan runs. The docs describe two places:

WhereCommandNeedsWhat it scans, per the docs
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 runnersafeguard ci scan --localA checkout in the job; no integrationSBOM, dependency vulnerabilities

With --local, only the results leave the runner, so it also works for self hosted and firewalled Git servers. The CLI reads dependencies from lockfiles and manifests such as package-lock.json, go.sum, requirements.txt and pom.xml. The docs describe the scan modes for scans that run on Safeguard; they do not say whether --scan-mode changes a --local run, so check --help and your results before you rely on that combination.

How the step fails. These are the documented flags:

FlagFails the step when
--waitThe scan fails or takes longer than --timeout (default 30m)
--fail-on <severity>With --wait: a dependency vulnerability at or above critical, high, medium or low
--fail-on-gate <gate>With --wait: a policy gate (its name, id, or all) is not Passed or Warning

Drop --fail-on to report without failing. Without --wait the step does not wait for the result, so it cannot fail on findings at all.

How results are named. Each push becomes a new version of its Safeguard project, named <branch>-<commit> (the docs' example is main-a1b2c3d). On a pull request the source branch is scanned and its head commit names the version. The first run creates the project, so nothing else is needed to start.

Pull requests from forks. Every CI guide warns about the same thing: CI systems do not hand secrets to builds from forks by default, so a fork's pull request cannot authenticate. On public repositories that take outside contributions, scan pushes and internal pull requests, and do not work around the restriction by running the fork's code with your secrets.

Decision table: which mode, where and how it fails

StageTriggerModeRuns onFailure flagsWhy
Pull requestPR or MR eventfastSafeguard, or --local for firewalled Git--wait --fail-on critical, or report only while tuningQuick answer while the author is still on the change
MainPush to maindeepSafeguard--wait, report only or --fail-on-gate on a warn level gateFull picture of what just merged, feeds triage
ReleasePush to release/* or a tagdeepSafeguard--wait --fail-on-gate <release gate>, with --timeout set to your ceilingThe gate decides on the exact commit you ship
ScheduledCronprogressiveSafeguardNone; report onlyRecheck unchanged code against new advisories, fast results first
Firewalled or air gapped GitAnyNot documented for --localRunner--fail-on per stageSource never leaves the runner

Main runs Deep but usually does not block: once code is on main, the useful outcome is a finding in front of an owner. Blocking belongs on the release job, which runs Deep so the gate reads the full result.

Step by step: GitHub Actions

GitHub's own documentation gives you everything this layout needs. The pull_request, push and schedule events can all start one workflow, and the github.event_name context tells a step which one did. Scheduled workflows use POSIX cron syntax, run in UTC by default, run on the latest commit on the default branch, and cannot run more often than every 5 minutes. GitHub also notes that scheduled runs can be delayed at high load, especially at the start of each hour, and that in a public repository scheduled workflows are disabled after 60 days without repository activity.

  1. Create the secret key. In Safeguard, Settings › Developer, create a key with scans:trigger and scans:read. In GitHub, add it under Settings › Secrets and variables › Actions › New repository secret as SAFEGUARD_API_KEY. An organisation secret works too.

  2. Connect the repository with a default mode. Under Integrations, connect GitHub, select the repository and its branch, and pick a Connect option. Choose Deep if you want any scan that does not pass --scan-mode to be full depth. This step is optional for the on Safeguard path when the repository is public on github.com, but connecting it is what lets you set that default and schedule rescans from the app.

  3. Save the workflow as .github/workflows/safeguard.yml. This is the docs' template with three changes: a release/** branch pattern, a schedule trigger, and a mode chosen from the event.

name: Safeguard
on:
  push:
    branches: [main, 'release/**']
  pull_request:
    branches: [main]
  schedule:
    - cron: "23 4 * * 1"
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Safeguard scan
        env:
          SAFEGUARD_API_KEY: ${{ secrets.SAFEGUARD_API_KEY }}
          SAFEGUARD_INSTALL_DIR: ${{ runner.temp }}/safeguard-bin
          SAFEGUARD_VERSION: "<version 11.2.0 or later>"
          EVENT: ${{ github.event_name }}
          REF: ${{ github.ref_name }}
        run: |
          set -eu
          curl -fsSL https://cli.safeguard.sh/install | bash
          SG="$SAFEGUARD_INSTALL_DIR/safeguard"
          case "$EVENT" in
            pull_request)
              "$SG" ci scan --scan-mode fast --wait --fail-on critical ;;
            push)
              case "$REF" in
                release/*) "$SG" ci scan --scan-mode deep --wait --timeout 45m --fail-on-gate "Release" ;;
                *)         "$SG" ci scan --scan-mode deep --wait ;;
              esac ;;
            schedule)
              "$SG" ci scan --scan-mode progressive ;;
          esac
  1. Read it line by line. The pull request branch runs Fast and fails only on a critical dependency vulnerability. Pushes to main run Deep and wait, so a failed scan is visible, but they do not gate. Pushes to a release branch run Deep, wait up to 45 minutes and fail unless the gate named Release is Passed or Warning. The Monday 04:23 UTC schedule runs Progressive and does not wait, so results land in Safeguard and reach owners through your policies' Notify actions. Replace "Release" with your gate's name or id, and the timeout with your own ceiling.

  2. No checkout step. The docs point out that Safeguard clones the repository for an on Safeguard scan, so the template has no actions/checkout step. If you switch a branch to --local, add - uses: actions/checkout@v4 before the step, as the docs' options table shows.

  3. Push to a test branch first. Open a pull request into main and confirm the run prints the scan result and a link to it in Safeguard. The schedule only fires once the workflow file is on the default branch, and the off-the-hour minute follows GitHub's advice about load at the top of each hour.

Step by step: GitLab CI/CD

Safeguard's GitLab guide offers a hosted job template and a variant without the template. The variant without the template is the easier one to extend with a scan mode, because the command is in your file. GitLab's CI_PIPELINE_SOURCE predefined variable says how a pipeline was started, and GitLab's own rules examples use the values merge_request_event and schedule.

  1. Add the variable. Create the Safeguard key with scans:trigger and scans:read. In GitLab, add it under Settings › CI/CD › Variables as SAFEGUARD_API_KEY with Mask variable on. The Safeguard guide says to leave Protect variable off if unprotected branches or merge requests should be scanned, because GitLab passes protected variables only to protected branches.

  2. Add three jobs to .gitlab-ci.yml, built on the docs' variant without the template:

.safeguard:
  stage: test
  image: alpine:3.20
  variables:
    SAFEGUARD_INSTALL_DIR: /usr/local/bin
    SAFEGUARD_VERSION: "<version 11.2.0 or later>"
  before_script:
    - apk add --no-cache bash curl ca-certificates
    - curl -fsSL https://cli.safeguard.sh/install | bash

safeguard-mr:
  extends: .safeguard
  script:
    - safeguard ci scan --scan-mode fast --wait --fail-on critical
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

safeguard-main:
  extends: .safeguard
  script:
    - safeguard ci scan --scan-mode deep --wait
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"

safeguard-release:
  extends: .safeguard
  script:
    - safeguard ci scan --scan-mode deep --wait --timeout 45m --fail-on-gate "Release"
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^release\//

safeguard-scheduled:
  extends: .safeguard
  script:
    - safeguard ci scan --scan-mode progressive
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  1. Create the schedule in GitLab. GitLab's docs describe this under Build › Pipeline schedules › New schedule. You choose a preconfigured interval or a cron pattern, a target branch or tag, and optional variables. GitLab notes that the schedule's pipeline runs with the schedule owner's permissions, that the owner needs the Developer, Maintainer or Owner role, and that a schedule cannot run more often than the instance's maximum scheduled pipeline frequency. Point the schedule at main.

  2. If your pipeline has no test stage, set stage: on the hidden job, as the Safeguard guide says for its own template.

  3. Using the hosted template instead. The docs' template exposes variables such as SAFEGUARD_FAIL_ON, SAFEGUARD_TIMEOUT, SAFEGUARD_LOCAL and SAFEGUARD_VERSION. The docs do not list a scan mode variable for it, so if you stay on the template, the integration's connect mode decides the depth. Use the variant above when you need a different mode per pipeline source.

  4. Check where merge requests are scanned. On a merge request the source branch is scanned. If the job is missing from a pipeline, the Safeguard guide's first suspect is a rule or branch pattern that does not match, and its second is a protected variable on an unprotected branch.

Step by step: Jenkins

Safeguard's Jenkins guide uses a multibranch pipeline, where Jenkins knows the branch, and a when block with branch and changeRequest conditions. Jenkins' Pipeline syntax reference adds two pieces this layout needs: the triggers directive with cron, and the triggeredBy condition, which runs a stage only when the build was started by a given cause such as TimerTrigger. Jenkins also recommends the H symbol in cron fields so scheduled jobs spread their load instead of all starting at the same minute.

  1. Add the credential. Create the Safeguard key and add it under Manage Jenkins › Credentials as a Secret text credential with the ID safeguard-api-key. The agent needs Linux or macOS with curl; the Safeguard guide says Windows agents are not supported.

  2. Add the stages to your Jenkinsfile:

pipeline {
    agent any
    triggers { cron('H 4 * * 1') }
    stages {
        stage('Safeguard fast (PR)') {
            when {
                allOf {
                    changeRequest target: '^(main)$', comparator: 'REGEXP'
                    not { triggeredBy 'TimerTrigger' }
                }
            }
            steps { safeguardScan('--scan-mode fast --wait --fail-on critical') }
        }
        stage('Safeguard deep (main)') {
            when {
                allOf {
                    branch pattern: '^(main)$', comparator: 'REGEXP'
                    not { triggeredBy 'TimerTrigger' }
                }
            }
            steps { safeguardScan('--scan-mode deep --wait') }
        }
        stage('Safeguard deep (release gate)') {
            when {
                allOf {
                    branch pattern: '^(release/.*)$', comparator: 'REGEXP'
                    not { triggeredBy 'TimerTrigger' }
                }
            }
            steps { safeguardScan('--scan-mode deep --wait --timeout 45m --fail-on-gate Release') }
        }
        stage('Safeguard progressive (scheduled)') {
            when {
                allOf {
                    branch pattern: '^(main)$', comparator: 'REGEXP'
                    triggeredBy 'TimerTrigger'
                }
            }
            steps { safeguardScan('--scan-mode progressive') }
        }
    }
}

def safeguardScan(String args) {
    withCredentials([string(credentialsId: 'safeguard-api-key', variable: 'SAFEGUARD_API_KEY')]) {
        sh """
            set -eu
            export SAFEGUARD_INSTALL_DIR="\$WORKSPACE/.safeguard-bin"
            export SAFEGUARD_VERSION="<version 11.2.0 or later>"
            curl -fsSL https://cli.safeguard.sh/install | bash
            "\$SAFEGUARD_INSTALL_DIR/safeguard" ci scan ${args}
        """
    }
}
  1. Mind the trigger scope. In a multibranch pipeline, the triggers block applies to every branch job that reads this Jenkinsfile, so each branch will start a timed build. The when conditions above make the progressive stage run only on main, and every other stage is skipped on a timer, so other branches and pull request jobs do nothing when the timer fires. If that is noisier than you want, move the scheduled scan into a separate single branch job.

  2. Single branch jobs. The Safeguard guide says that in a freestyle or plain pipeline job you delete the when block, every build is scanned, and the branch comes from GIT_BRANCH. In that case run two jobs: one on SCM changes with Deep, and one on a cron trigger with Progressive.

  3. Check the skip case. The Safeguard guide's first troubleshooting line is "the stage is always skipped", which happens when the job is not multibranch and branch or changeRequest never match.

Step by step: Azure Pipelines

Azure Pipelines needs one detail more than the others. Microsoft's scheduled triggers reference says cron schedules are in UTC, that schedules entries take branches filters, and that the always property defaults to false, which means a scheduled run happens only if there have been source or pipeline changes since the last successful scheduled run. For a security rescan that default defeats the purpose: the whole point is to recheck code that did not change. Set always: true.

The Build.Reason predefined variable says why a run started. Microsoft's list includes IndividualCI and BatchedCI for pushes, Schedule for scheduled triggers, and PullRequest for builds required by a branch policy.

  1. Add the variable. Create the Safeguard key. Edit the pipeline, open Variables, and add SAFEGUARD_API_KEY with Keep this value secret on. The Safeguard guide notes that secret variables reach a script only through env.

  2. Save the pipeline as azure-pipelines.yml:

trigger:
  branches:
    include: [main, release/*]
pr:
  branches:
    include: [main]
schedules:
  - cron: '23 4 * * 1'
    displayName: Weekly Safeguard progressive scan
    branches:
      include: [main]
    always: true
pool:
  vmImage: ubuntu-latest
steps:
  - checkout: self
  - bash: |
      set -eu
      export SAFEGUARD_INSTALL_DIR="$(Agent.TempDirectory)/safeguard-bin"
      export SAFEGUARD_VERSION="<version 11.2.0 or later>"
      curl -fsSL https://cli.safeguard.sh/install | bash
      SG="$SAFEGUARD_INSTALL_DIR/safeguard"
      case "$REASON" in
        PullRequest) "$SG" ci scan --scan-mode fast --wait --fail-on critical ;;
        Schedule)    "$SG" ci scan --scan-mode progressive ;;
        *)
          case "$BRANCH" in
            refs/heads/release/*) "$SG" ci scan --scan-mode deep --wait --timeout 45m --fail-on-gate "Release" ;;
            *)                    "$SG" ci scan --scan-mode deep --wait ;;
          esac ;;
      esac
    displayName: Safeguard scan
    env:
      SAFEGUARD_API_KEY: $(SAFEGUARD_API_KEY)
      REASON: $(Build.Reason)
      BRANCH: $(Build.SourceBranch)
  1. Azure Repos pull requests. The Safeguard guide says the pr block is ignored for Azure Repos, and you add a Build validation branch policy on the target branch instead. That is also why the case statement keys on PullRequest, the reason Microsoft lists for a build a branch policy requires. For GitHub and Bitbucket repositories built in Azure Pipelines, the pr block works as shown.

  2. Consider the service hooks. The Azure DevOps integration's optional service hooks rescan on push and start a pull request review when a pull request is opened or updated. If you turn them on, decide whether they or your pipeline own the gate.

Step by step: Bitbucket Pipelines

Bitbucket separates the scheduled part cleanly. A custom pipeline does not run on commits; it runs only when triggered by hand or by a schedule. Atlassian's docs describe creating schedules under Repository › Pipelines › Schedules, choosing a branch, the pipeline to run, and an hourly, daily or weekly frequency, with the run executed in UTC.

  1. Add the variable. Create the Safeguard key. In Bitbucket, add it under Repository settings › Repository variables as SAFEGUARD_API_KEY with Secured on. A workspace variable works too.

  2. Save the file as bitbucket-pipelines.yml, built from the docs' template:

pipelines:
  pull-requests:
    '**':
      - step:
          name: Safeguard fast
          image: alpine:3.20
          script:
            - apk add --no-cache bash curl ca-certificates
            - export SAFEGUARD_VERSION="<version 11.2.0 or later>"
            - curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_INSTALL_DIR=/usr/local/bin bash
            - safeguard ci scan --scan-mode fast --wait --fail-on critical
  branches:
    main:
      - step:
          name: Safeguard deep
          image: alpine:3.20
          script:
            - apk add --no-cache bash curl ca-certificates
            - export SAFEGUARD_VERSION="<version 11.2.0 or later>"
            - curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_INSTALL_DIR=/usr/local/bin bash
            - safeguard ci scan --scan-mode deep --wait
    'release/*':
      - step:
          name: Safeguard deep release gate
          image: alpine:3.20
          script:
            - apk add --no-cache bash curl ca-certificates
            - export SAFEGUARD_VERSION="<version 11.2.0 or later>"
            - curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_INSTALL_DIR=/usr/local/bin bash
            - safeguard ci scan --scan-mode deep --wait --timeout 45m --fail-on-gate "Release"
  custom:
    safeguard-progressive:
      - step:
          name: Safeguard progressive
          image: alpine:3.20
          script:
            - apk add --no-cache bash curl ca-certificates
            - export SAFEGUARD_VERSION="<version 11.2.0 or later>"
            - curl -fsSL https://cli.safeguard.sh/install | SAFEGUARD_INSTALL_DIR=/usr/local/bin bash
            - safeguard ci scan --scan-mode progressive

Replace the version placeholder with v11.2.0 or a later release. The docs' template puts the step in a definitions anchor and reuses it; this layout writes the steps out because each one passes a different mode. If you prefer anchors, keep one shared step and pass the mode through a variable you set per pipeline.

  1. Create the schedule. In Bitbucket, Repository › Pipelines › Schedules › Create schedule. Pick main as the branch, safeguard-progressive as the pipeline, and a weekly or daily frequency.

  2. Know the pull request pattern's limit. Bitbucket matches the pull-requests pattern against the source branch, so '**' scans every pull request whatever its target.

Any other CI system

The Safeguard docs show the generic form for any CI that runs a shell step. Pass the repository, branch and commit when your CI is not one of the five above:

curl -fsSL https://cli.safeguard.sh/install | bash
SAFEGUARD_API_KEY=... safeguard ci scan --wait --fail-on critical \
  --repository-url "$REPO_URL" --branch "$BRANCH" --commit "$COMMIT"

Add --scan-mode to that line the same way as above, and choose the value from whatever your CI exposes about the trigger. The docs say CircleCI is detected too.

Progressive on a schedule: what to expect

Progressive is the mode the title puts on a schedule, and it is worth being precise about why and about what is not documented.

The docs describe Progressive as fast now, full depth after. On a scheduled run, that shape fits well. You get an early answer for the common case, a newly published advisory that matches a component you already ship, and the full depth result follows without anyone having to remember to run a second scan.

Three things are not in the docs, and the configs above are written around them:

  • When --wait returns. The docs do not say whether a Progressive run with --wait finishes after the fast pass or after the full depth pass. That is why every scheduled job above omits --wait and every failure flag. The scheduled job's job is to put fresh results in Safeguard, not to turn a pipeline red at 4am.
  • What the platform's own schedule uses. Safeguard can rescan a connected repository on a schedule you set under Integrations › Scheduled, or when you choose Rescan from the connection's menu under Project Configurations. The integration guides do not say which mode those rescans use. If you want the depth fixed, the CI cron route in this post sets it explicitly on every run.
  • What new advisories trigger by themselves. The GitHub, GitLab and Bitbucket integration guides say those connections have no webhooks to set up, and that new commits are rescanned by the schedule you set or by Rescan. Do not assume that a newly published advisory starts a fresh scan of your repository on its own. When a high profile advisory lands, choose Rescan on the affected projects or run the scheduled pipeline by hand.

Scheduled results still need to reach someone. Give the policies you care about the Notify action. The policy outcomes docs say Notify sends to the workspace's configured channels, the in-app bell, connected chat integrations and email according to each person's preferences, and that it notifies when a policy goes from passing to failing and again when a failing policy finds new matches, not on every evaluation. That keeps a weekly scheduled scan from repeating last week's news.

How often to schedule depends on how quickly you need to know. Weekly is a reasonable start for most repositories. Repositories that ship to production often, or that carry many direct dependencies, can justify daily. Schedule each supported release branch as well as main.

Wiring the release gate

The release job's --fail-on-gate only works if there is a gate to read. Safeguard's Policies & Gates docs describe the pieces.

  1. Create policies. Under Settings › Policies, Create Policy, then define conditions with the visual builder or YAML, set severity and action, and save. The documented conditions include vulnerability severity, CVSS score, EPSS score, license, component age, maintenance and SLSA level. Templates include No Critical Vulnerabilities, License Compliance, SLSA Level 2 and No Abandoned Packages.

  2. Choose each policy's action. Each policy has one Action, and the docs list three. Block fails the gate. Warn lets the gate pass with a warning that names the policy. Notify lets the gate pass and sends a notification. Use Block sparingly and for the release gate; Warn and Notify suit rules you want visible without stopping work.

  3. Link policies to a gate and name it. The CI step refers to the gate by name or id, or all. The examples here use a gate named Release.

  4. Confirm policy outcomes are on. The docs say policy outcomes are turned on per workspace by Safeguard. If the Gates and Policies tables have no Outcome column, ask your Safeguard contact to turn it on. Until then, --fail-on-gate has nothing to read.

  5. Understand what makes the step fail. Per the policy outcomes docs, safeguard ci scan --wait --fail-on-gate exits 0 when the gate is Passed or Warning, and exits non-zero when the gate is Failed (printing the Block policies that failed), when it is Error or Not evaluated, or when its policies were not evaluated before --timeout. A gate with no linked policies is Not evaluated, never Passed, and an Error is never counted as a pass. So a release scan that runs out of time fails closed. That is the behaviour you want at release, and it is why the release jobs above set an explicit --timeout.

  6. Use exceptions for approvals. When a release needs to go out with a known finding, the documented route is a policy exception: Request Exception, select the policy and finding, give a business justification, set an expiry date and submit. Approvers are notified, decide, and the decision goes into an audit trail. That is the record SSDF task PO.4.1's example about recording approvals, rejections and exception requests is asking for.

  7. Gate on documented conditions only. CISA KEV status is a filter you can use when triaging findings, not a documented gate condition. Build release gates from the conditions the docs list, and use KEV and EPSS to decide what to fix first. The post on prioritising with reachability, EPSS and KEV covers that triage side.

For a fuller walk through writing policies in plain language and turning them into release gates, see plain English security policies to release gates in Safeguard.

When a stage cannot reach Safeguard

Some pipelines cannot let Safeguard clone the repository: a self hosted Git server behind a firewall, or a policy that source code never leaves the build network. There are two documented routes.

safeguard ci scan --local. The CLI scans the checkout on the runner and uploads only the results. It needs a checkout step in the job and no integration. Per the CI docs it covers SBOM and dependency vulnerabilities, not SAST. The same failure flags apply. In the GitHub workflow, add actions/checkout@v4 before the step; in Jenkins, Azure and Bitbucket the job already has the checkout.

safeguard scan on the build machine. This runs fully on the runner and writes JSON files under --output (default ./output), including vuln.json. It does not fail on findings: per the CLI usage docs it exits 0 when the scan ran, whatever it found, 1 when the command failed, and 2 when results were written but the vulnerability scan did not run, so a script cannot mistake that for a clean pass. There is no severity threshold flag. The CI docs show gating on the file instead:

safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
       | index("critical") | not' out/*/vuln.json

safeguard scan relies on scanner tools that the interactive terminal downloads the first time it runs. On a fresh runner the docs say to put them on the PATH or point TOOLS_DIR at a directory that holds them. Cache that directory between runs. For depth on this path, the deep dependency scanning docs show --resolve-transitive with a depth flag; check safeguard scan --help for the current default and maximum on your version before you depend on a number.

The local runner. For developers who want the same scan before they push, the local runner (GA) is part of the CLI. Open safeguard, type /login, approve the session, and /scan . scans the current folder from the interactive terminal while signed in, uploading the SBOM and findings, never the source. The post on choosing how Safeguard touches your code compares the connection, CLI and runner routes in detail.

Rolling it out without breaking builds

Turning on a blocking gate across many repositories on one afternoon is how security checks get disabled on the next one. A staged rollout avoids that.

  1. Week one, report only everywhere. Ship the config with every failure flag removed except --wait on main, so failed scans show but findings do not block. Let the scheduled Progressive job run at least once.

  2. Compare Fast and Deep on your code. Pick two or three representative repositories and run both modes on the same commit. Write down how long each took and whether the findings differ. That measurement, not a vendor estimate, is what your pull request budget should come from.

  3. Turn on the pull request threshold. Add --fail-on critical to the pull request job. Leave high, medium and low as report only until owners have cleared the backlog, or new pull requests will fail for findings they did not introduce.

  4. Build and test the release gate. Create the Release gate with one Block policy, link it, confirm the Outcome column shows results, and run the release job against a test branch. Then try a deliberately failing case so you have seen the non-zero exit and the printed policy before it happens on a real release.

  5. Tighten slowly. Move individual policies from Warn to Block as their finding counts reach zero. Each move is a small, explainable change instead of a cliff.

  6. Turn on fixes when you trust them. Autonomous remediation (GA) can open tested, policy gated pull requests for the findings these scans surface. It ships with every category set to Manual, so you choose which categories, if any, run on their own. The post on autonomous remediation in Safeguard explains the categories and strategies.

What Safeguard does not do

  • It does not publish scan times or a definition of what Fast skips. The docs describe Fast as quicker and Deep as full, and Progressive as fast now with full depth after. Measure both on your own repositories before you set budgets.
  • It does not document --scan-mode on the CI/CD page yet. The flag ships in CLI v11.2.0 and later. Confirm it with safeguard ci scan --help on the version you pin, and remember the docs say --help wins when the two disagree.
  • It does not document how --wait behaves on a Progressive run. Keep failure flags off scheduled Progressive jobs, and gate releases on Deep.
  • It does not fail a build on SAST findings through --fail-on yet. The CI docs say SAST findings are scanned and shown in Safeguard but are not part of the severity check. --local scans do not include SAST at all.
  • It does not let safeguard scan fail on findings. That command reports; gate on its JSON or use safeguard ci scan.
  • It does not gate before policy outcomes are on. Safeguard turns them on per workspace, and --fail-on-gate depends on them.
  • It does not scan fork pull requests that your CI will not give secrets to. That is the CI system's rule, and the docs advise against working around it with the fork's code.
  • It does not run your pipeline. Cron syntax, job timeouts, branch filters, caching and runner images belong to your CI system. The configs here use each vendor's documented features; check them against your CI version.
  • It does not run on Windows CI agents. The docs say every system needs a Linux or macOS runner with bash and curl, and the Jenkins guide says Windows agents are not supported.
  • It does not decide your risk appetite. Which severities block, which policies warn and who approves exceptions are your choices.

Common mistakes

  • Pinning a CLI older than v11.2.0. The step then fails on an unknown flag. Pin to v11.2.0 or later and run --help once on that version.
  • Gating on a Progressive run. Without documented --wait behaviour you do not know which pass the gate saw. Gate on Deep.
  • Leaving the scheduled job's always at false in Azure Pipelines. The schedule then skips weeks when nobody committed, which are exactly the weeks a recheck is for.
  • Letting the GitHub schedule die quietly. In public repositories GitHub disables scheduled workflows after 60 days without activity. Check the Actions tab on quiet repositories.
  • Scheduling every job at minute zero. GitHub warns about load at the top of the hour, and Jenkins recommends H to spread scheduled jobs. Pick an off minute.
  • Blocking pull requests on high and medium from day one. Authors inherit the whole backlog and learn to bypass the check. Start with critical, and ratchet.
  • Assuming a webhook will rescan new commits. Outside Azure DevOps service hooks, the integration guides say rescans of new commits come from a schedule, a Rescan or your CI step.
  • No --timeout on the release job. The 30 minute default may be too long or too short for your release window. Set your own ceiling so a slow scan fails closed when you expect it to.
  • Protecting the GitLab variable and scanning merge requests. GitLab passes protected variables only to protected branches, so the merge request job fails authentication.

FAQ

How do I set the Safeguard scan mode in CI?

Add --scan-mode fast, --scan-mode deep or --scan-mode progressive to safeguard ci scan. The flag exists in CLI v11.2.0 and later. Without it, the scan uses the mode set on the repository's integration.

What is the difference between Fast, Deep and Progressive in Safeguard?

The integration guides describe Fast as the quicker scan, Deep as the full scan, and Progressive as fast now with full depth after. Safeguard's own guidance is Fast for quick feedback and pre-merge checks, and Deep for full transitive analysis and thorough pre-release scans. The docs do not list what Fast leaves out, so compare both on your code.

Which scan mode should run on pull requests?

Fast, with --wait --fail-on critical once you have tuned it, or report only while you are still measuring. Keep the stricter checks for the release gate.

Can a Safeguard scan fail my build?

Yes, through safeguard ci scan with --wait. Add --fail-on <severity> to fail on a dependency vulnerability at or above a severity, or --fail-on-gate <gate> to fail unless a policy gate is Passed or Warning. safeguard scan on its own never fails on findings.

Why does my release job fail with Not evaluated?

The gate's policies were not evaluated on the new scan before --timeout, the gate has no linked policies, or policy outcomes are not on for your workspace. The docs say all three make --fail-on-gate exit non-zero, by design.

How do I schedule a Safeguard scan in GitHub Actions?

Add a schedule trigger with a cron line to the workflow, and run safeguard ci scan --scan-mode progressive when github.event_name is schedule. Scheduled workflows run on the default branch in UTC unless you set a timezone.

Does Safeguard have a GitHub Action, GitLab component or Jenkins plugin?

No. The docs say there is no plugin, Action or Pipe to install. Every system runs the CLI in a plain shell step.

Can I use scan modes without letting Safeguard clone my code?

safeguard ci scan --local scans on the runner and uploads only results, but the docs do not say whether --scan-mode changes a local run. Check --help and compare results. The local runner is another option for developer machines.

Does a new CVE trigger a rescan automatically?

The integration guides for GitHub, GitLab and Bitbucket say new commits are rescanned by a schedule or Rescan, and they do not describe advisory triggered rescans. Set a schedule, and choose Rescan when a major advisory lands.

A staged pipeline tells you sooner that a vulnerable dependency is in a build, and the scheduled run catches the ones that became vulnerable while nobody was pushing. The next question is how quickly the fix lands, which the section below covers.

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. It installs the CLI, opens the interactive terminal with safeguard, and asks it to fix the lodash CVE in a repository: the assistant reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing. It ends with /permissions, /model and /scan .. It is the same CLI your pipeline installs, so it is a quick way to see what a finding from a scheduled or release scan looks like when someone picks it up.

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. That is the route for developers who want a scan before they push, and for code that cannot be cloned by a hosted service.

Click through it at your own pace, or open it full screen.

To try this on your own pipeline, download the CLI at safeguard.sh/download and keep the Safeguard docs open beside your CI file.

Quick check

Your scheduled GitHub workflow runs safeguard ci scan --scan-mode progressive --wait --fail-on critical. Why is that a risky choice?

The docs do not say whether --wait on a Progressive run returns after the fast pass or after the full depth pass, so you cannot be sure which result the failure flag judged. Run scheduled Progressive scans as report only, route results with Notify policies, and keep the gate on a Deep release scan.

Your release job uses --fail-on-gate Release and fails with Not evaluated, although the scan finished. What should you check first?

Check that policy outcomes are on for your workspace (the Gates and Policies tables show an Outcome column), that the Release gate has linked policies, and that --timeout left enough time for the policies to be evaluated on the new data. Any of those makes the step exit non-zero by design.

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.