Run security scans in CI without slowing every build by matching scan depth to the stage: a quick, cached dependency check on each pull request, a full transitive scan on the main branch and before release, and rescans on a schedule you set after the code ships. Give each stage a written time budget, make the quick scan report rather than decide anything it cannot see, and make the release gate refuse to pass unless the full scan for that exact commit has run.
The rest of this post works that plan through stage by stage, with the standards behind it, a method you can run with any tooling, and then how Safeguard's scan modes, CI step and gates map onto it.
Key takeaways
- Scan time goes into resolution, transitive depth, advisory matching, reachability, container layers, code analysis and queueing. Each stage can afford a different share.
- Pull requests want fast feedback on what changed. Main branch and release candidates want the full graph. Rescans on a schedule you set cover the days between code changes, when new advisories land on code you already shipped.
- A fast scan may miss things only if a deeper scan is guaranteed to run before release. Write that guarantee into the release gate.
- Time budgets belong in the pipeline file as job timeouts. Fail closed on a timeout at release; report and retry earlier.
- Safeguard has three scan modes you choose when you connect a repository: Fast (quicker) and Deep (full) are GA, and Progressive (fast results now, full depth after) is generally available. Since CLI v11.2.0,
safeguard ci scan --scan-modepicks the mode for one CI run. - Safeguard rescans on the schedule you set per integration, when you choose Rescan, and on each CI run. Scheduled rescans are GA. Azure DevOps can also rescan on push through optional service hooks. Software composition analysis (SCA) and deep dependency scanning are GA. The CLI, the local runner and the CI step are GA. Policies and gates are GA, with the documented actions Block, Warn and Notify.
- Static analysis (SAST) is generally available, and the docs say SAST findings are not part of the CI step's
--fail-onseverity check yet. - Reachability analysis is GA. Autonomous remediation is GA and ships switched off, with every category set to Manual.
The problem: one scan profile for every stage
Most teams that complain about slow security scans run one scan profile everywhere: on a two line documentation change, on a merge to main and on the release tag. If that job is thorough, pull requests wait and developers resent it. If it is quick, the release ships on a scan that never looked past the first few layers of the dependency tree.
The second failure is the dangerous one. A team that tunes its scan down for pull requests often never adds the slower scan back anywhere. Nobody decides to stop deep scanning; it just stops, and the gate keeps going green.
The fix is not a faster scanner. It is a pipeline design where each stage runs the depth it can afford, the stages together cover the whole graph, and the final gate checks that the full scan ran on the thing being released.
What the standards say about where checks run
No standard tells you how many minutes a scan may take. Several tell you what has to be true by the time software ships, and that is what sets the floor for your deepest stage.
NIST SP 800-218, the Secure Software Development Framework
The SSDF (NIST SP 800-218, version 1.1) has three practices that bear on scan depth by stage:
- PO.4, Define and Use Criteria for Software Security Checks. PO.4.1 asks you to define criteria for security checks and track them throughout the lifecycle; PO.4.2 asks for mechanisms that gather the information those criteria need. A per stage scan plan with written thresholds is a PO.4 artifact.
- PW.7, Review and/or Analyze Human-Readable Code. PW.7.1 asks you to decide whether code review, code analysis or both are used, and PW.7.2 asks you to perform it and record and triage what it finds. The aim is to find vulnerabilities "before the software is released".
- PW.8, Test Executable Code. PW.8.1 asks you to decide whether executable code testing should be performed and which types, and its second example is choosing testing methods based on the stage of the software. That is close to a direct instruction to vary depth by stage.
None of these says "scan every pull request deeply". They say: decide, write down the decision, do it before release, and keep the record.
NIST SP 800-204D, supply chain security in CI/CD pipelines
NIST SP 800-204D (February 2024) is the most specific federal text on scanning inside pipelines. Three passages matter here.
Section 5.1.4 says dependencies must be enumerated and evaluated for policy, possibly with SCA tools, and that dependency detectors should find all dependencies, including transitive ones, "with preferably no limit to the depth" of nesting. That sets the bar for at least one stage in your pipeline: somewhere, the full depth has to be walked.
Section 5.2 includes DEPLOY_REQ-4: there should be evidence that a container image was scanned for vulnerabilities, and the time a scan ran matters because scanners keep learning about new vulnerabilities, so recent results give better assurance than old ones. That is the argument for rescanning on a schedule and at release, not only at build.
The same section, under DEPLOY_REQ-5, says a container image should be scanned as soon as it is built, even before it is pushed to a registry. That supports an early, fast scan as well as a late, deep one.
Section 3 also lists, among attack vectors, that checks such as scanning "may have been bypassed in the CI/CD process". A fast scan that quietly replaced the deep one is a self inflicted version of that bypass.
SLSA v1.0
SLSA's build track is about provenance, not vulnerability scanning: Build L1 means provenance exists, L2 means signed provenance from a hosted build platform, and L3 means a hardened build platform. It matters here because provenance is how you tie a scan result to the exact artifact you release. If your deep scan ran on commit A and you shipped an artifact built from commit B, provenance is what lets an auditor, or your own gate, notice.
OWASP Software Component Verification Standard
The OWASP SCVS organises component verification requirements into three levels so organisations can adopt them gradually. It is reasonable to meet a higher level at release than on every pull request, as long as the release level is met every time.
What the exploited vulnerability sources add
CISA calls its Known Exploited Vulnerabilities (KEV) catalog the authoritative source of vulnerabilities exploited in the wild. FIRST describes EPSS as estimating the probability that a published CVE is exploited in the next 30 days. Both change independently of your commits, so a pipeline that only scans when code changes will always be behind.
What a security scan actually spends its time on
Before you can budget a scan, you need to know where its minutes go. Every dependency and container scan does some version of the steps below, and each has a different cost profile.
1. Getting the code or artifact
The scanner needs the source tree, the lockfiles or the image. A fresh clone of a large repository, or a pull of a large image, can cost more than the analysis itself. Scanning in the job that already checked the code out avoids paying this twice.
2. Resolving the dependency graph
With a complete lockfile, working out exact versions is mostly parsing. Without one, or when the scanner asks a registry about transitive versions, it becomes network bound and can dominate the run, and results drift if versions float.
3. Walking transitive depth
Direct dependencies are a small fraction of what ships. A shallow walk is fast because it stops early; a full walk reaches the long tail of utility packages and shims nobody chose directly. Deduplicating shared subgraphs helps, but the graph still has to be traversed.
4. Matching against advisories
Every resolved package and version is compared with advisories. A scanner that downloads a fresh database every run pays a fixed cost each time; one that keeps a warm database or matches server side pays far less.
5. Reachability and prioritisation
Working out whether your code calls the vulnerable function means building a call graph, far more expensive than matching versions, and it is what turns a long list into a short one. Many teams run it on main or before release only.
6. Container layers
An image scan unpacks layers and inventories operating system packages and language packages inside them. Base image layers rarely change between builds, so caching by layer digest saves most of the work, but the first scan of a new base is slow.
7. Code analysis
Static analysis of your own source works on a different axis from dependency scanning: its cost grows with lines of code and the depth of its data flow analysis, not with the dependency graph. It usually has its own time profile and deserves its own budget.
8. Queueing, uploading and policy evaluation
On a shared service the job may wait for a worker, upload results, then wait for policies to be evaluated. None of that is analysis, but all of it is pipeline time.
When a developer says the scan is slow, ask which of these eight phases it is. The answer decides whether you need caching, a different stage, or a different scan.
Which stage needs which depth
The table below is a starting design. Its time budgets are illustrative placeholders for your own measurements, not benchmarks of any tool.
| Stage | Trigger | What the scan must answer | Depth | Illustrative budget | Gate behaviour |
|---|---|---|---|---|---|
| Developer machine | Before push, on demand | Did I just add something obviously bad? | Fast, changed manifests only | A minute or so | Advisory only |
| Pull request | Every PR into main or release branches | Does this change add a new vulnerable or disallowed dependency? | Fast: lockfile resolution, direct plus cached transitive results, no reachability | Under five minutes, illustrative | Block only on clear, high confidence rules; warn on the rest |
| Main branch | Every merge | What is the full state of main now? | Deep: full transitive graph, container layers, reachability where available | Tens of minutes is acceptable off the critical path, illustrative | Record and notify; open fixes |
| Release candidate | Release branch or tag | Is this exact commit fit to ship? | Deep, on the release commit, plus provenance | Whatever it takes, within a hard ceiling | Block; exceptions need a named approver |
| Scheduled | Nightly or weekly, on a schedule you set | Has anything new been disclosed against what is on main or deployed? | Deep, or a rescore of the stored inventory | Off the critical path | Notify owners; open fixes |
| On demand | A high profile advisory or KEV addition you decide to act on | Which projects contain the newly affected component? | Lookup in stored SBOMs, then a rescan of affected projects only | Minutes, off the critical path | Notify; escalate by exploitation status |
Pull request: fast feedback on the change
A pull request scan has one job: tell the author, while they remember the change, whether it made things worse. The question is not "is this repository clean" but "did this change add a vulnerable or disallowed dependency".
That framing cuts most of the work. Resolve from the lockfile, compare against cached base branch results, and report the difference. Skip reachability unless it runs incrementally, and skip the image scan unless the change touches the Dockerfile or base image.
Block only on rules that are both important and unambiguous: a new dependency with a critical vulnerability that has a fixed version available, or a license your policy forbids. Everything else should warn. Developers accept a fast scan that blocks rarely and correctly. They route around one that blocks often on findings they did not introduce.
Main branch: the full picture, off the critical path
Once code is merged, nobody is waiting on the scan. Run the deep scan here: the full transitive graph, the container image, reachability where you have it, and code analysis if you run it. A long run is acceptable, because the result feeds triage and fix pull requests, not a developer's next five minutes.
The main branch scan is also where you build the baseline that makes pull request scans cheap. If you store the full result per commit on main, a pull request scan can compare against it instead of recomputing it.
Release candidate: depth on the exact commit you ship
The release gate is where "fast" stops being acceptable. The scan that decides release must run on the release commit or the built release artifact, at full depth, and its result must be recorded against that commit.
Two details catch teams out. First, a deep scan of main from yesterday is not a scan of today's release tag if anything merged in between. Second, a scan of the source tree is not a scan of the image if the image adds a base layer with its own packages. Scan what you ship.
Give this stage a hard ceiling rather than a target. If the deep scan cannot finish inside the ceiling, the release waits and someone looks at why. Shipping anyway because the scan was slow is the exact bypass that NIST SP 800-204D warns about.
Scheduled scans: catching what changed in the world
A repository nobody touched for three weeks still accumulates risk as new advisories land against packages it contains. A nightly or weekly job rescans main and deployed release tags, or rescores their stored inventories.
Scheduled runs have quirks. One widely used CI platform's docs say scheduled workflows run only on the default branch and can be delayed or dropped under high load, with the start of every hour named as busy. Schedule off the hour, and cover release branches explicitly.
On demand: when a major advisory lands
Some advisories cannot wait for next week's run. The cheapest response rebuilds nothing. If you keep an SBOM per project and version, search those stored inventories for the affected component first, then rescan only the projects that contain it. That answers "are we affected?" the same day without kicking off a hundred pipelines. Name who watches the KEV catalog and makes that call.
What you lose with a fast pre-merge scan, and where you get it back
Every shortcut in a fast scan has a cost. The design question is where each cost is recovered.
| What the fast scan skips | What can slip through | Where you recover it |
|---|---|---|
| Deep transitive walk | A vulnerable package several levels down, reached through a dependency you did not change | Main branch deep scan, release gate |
| Fresh resolution of floating versions | A transitive version that changed upstream since the lockfile was written | Lockfile discipline; deep scan with fresh resolution on main |
| Reachability | Nothing is missed, but noise goes up and real issues get buried | Main branch and release scans with reachability |
| Container image scan | Operating system packages in the base image | Image scan when the Dockerfile or base changes; release image scan |
| Code analysis | Flaws in your own code | A separate code analysis job with its own budget |
| Advisory updates after merge | A new CVE against a package already on main | Scheduled rescans, plus an on demand rescan for major advisories |
| Policy evaluation on full data | Rules that depend on the whole graph, like depth or license combinations | Release gate |
The pattern is simple: every row in the left column must have a named recovery point in the right column that runs automatically. If one does not, the fast scan is quietly your only scan for that class of issue.
Setting and enforcing a time budget per stage
A time budget is a number in the pipeline file, not a feeling in a retrospective. Here is a method that works with any CI system.
Step 1: measure before you cut
Record scan duration per stage for two weeks, split at least into clone, resolve, scan and wait. Slow scan complaints can turn out to be clone time, cold databases or queue waits, fixed by caching and placement rather than by scanning less.
Step 2: write the budget as a job timeout
Most CI platforms let you set a timeout per job. One platform's workflow syntax documents a job level timeout-minutes with a default of 360 minutes. Another documents a job level timeout that overrides the project setting but cannot exceed the runner's own timeout. Set these explicitly per stage. Six hours is not a budget.
Step 3: decide what a timeout means at each stage
A timeout is a result, and it needs a rule:
- Pull request: a timed out scan should report "not scanned" visibly and not block, then retry on merge. Blocking on a timeout teaches people to rerun until it passes.
- Main branch: a timeout should alert the owning team and rerun. It should not be silent.
- Release: a timeout must fail closed. No result is not a pass.
Step 4: only scan when it can matter
Path filters keep a dependency scan from running on changes that cannot affect dependencies. One CI platform's rules:changes keyword adds a job only when listed files change, and its reference notes that it compares against the target branch in merge request pipelines and against the previous commit in branch pipelines, and that it always evaluates to true for new branches or when there is no push event. Use filters on pull request scans; never on the release scan.
Step 5: cache what does not change
Cache the vulnerability database, the resolved dependency graph keyed by lockfile hash, and image layer results keyed by digest. Pin your scanner version so a new release does not change timing or results under you.
Step 6: run in parallel, not in series
Dependency scanning, image scanning and code analysis are independent. Run them as parallel jobs with separate budgets so the slowest one does not hold the others, and so you can see which one is slow.
How gates should behave when only a fast scan has run
This is the question that decides whether your design is honest.
A gate that only has fast scan data should say so. There are three workable patterns:
- Scope the rule to what was scanned. A pull request gate blocks on "new critical vulnerability with a fix available in a dependency this change added". It does not claim the branch is clean.
- Warn on everything else. Findings that need full depth or reachability to judge fairly become warnings, so the author sees them without being blocked on someone else's debt.
- Make the release gate demand the deep result. The release gate checks that a full scan exists for the release commit and that its policies passed. If the deep scan did not run, or did not finish, the gate does not pass. "Not evaluated" must never read as "passed".
The third pattern is the one teams forget. If the only automated check before release is the pull request gate, then the fast scan is your release gate, whatever your design document says.
Keeping the SBOM current between code changes
An SBOM generated at build time describes the artifact at that moment. Its component list stays true as long as the artifact does not change. Its vulnerability picture does not, because advisories keep arriving.
Three mechanisms keep it current:
- At release. Generate the SBOM from the release build and store it with the release. This is the inventory you will be asked about later.
- On a schedule. Rescan or rescore main and every supported release on a fixed cadence, so a quiet repository is not a stale one. NIST SP 800-204D's point about scan time applies directly: a recent result is worth more than an old one.
- When a major advisory lands. Search the stored SBOMs for the affected component, rescan the projects that contain it, and notify their owners. The lookup needs no rebuild and no pipeline run.
Together these mean the answer to "what is in production and what is it exposed to today" does not depend on whether someone happened to push code this week.
Stopping the fast scan from becoming the only scan
The drift from "fast on pull requests" to "fast everywhere" happens through small, reasonable changes: a shortened main branch job, a pull request job copied into a new template, a nightly run disabled because it was noisy. Guard against it with checks that do not depend on memory:
- Name the jobs by depth. A job called
deps-fast-prand one calleddeps-deep-releaseare hard to confuse. - Make the release gate check for the deep result, by commit, not just for "a scan passed".
- Alert on missing scans. If a project has had no deep scan in a set number of days, that is a finding in its own right.
- Review the pipeline template, not each pipeline. Put the stage plan in a shared template so one review covers every repository.
- Track coverage, not just findings. Report the share of projects with a recent deep scan alongside vulnerability counts.
How Safeguard does this job, step by step
Everything above works with any tooling. This section shows how Safeguard's documented features map to the plan, with commands exactly as Safeguard's docs show them, and with each capability's maturity label.
What the scan modes are, and where the docs describe them
Safeguard has three scan modes, and the source control integration guides describe the choice at the moment you connect repositories. The GitHub integration guide says the Connect button's menu offers "Fast scan (quicker)", "Deep scan (full)" and "Progressive scan (fast now, full depth after)". The other integration guides list the same three choices.
- Fast (GA): the quicker option.
- Deep (GA): the full scan.
- Progressive (GA): fast results now, full depth after. It is this post's quick answer first, full answer later pattern in one option, and it is generally available.
The docs do not spell out what Fast leaves out compared with Deep, so do not assume it matches the generic fast scan described earlier in this post. Before you settle on a mode per stage, compare a Fast and a Deep result on one of your own repositories. Whatever you run on pull requests, keep a Deep scan on the release commit.
In CI, the mode is a flag. Since CLI v11.2.0, safeguard ci scan takes --scan-mode fast, --scan-mode deep or --scan-mode progressive. Without the flag, the CI scan uses the mode set on the integration. That lets one connected repository run Fast on pull requests and Deep on the release branch. The CI/CD docs page does not list the flag yet, so confirm it with safeguard ci scan --help on the version you install.
Step 1: connect the repository and pick the initial depth
In the app, connect your source control under Integrations, select the repositories and the branch or tag of each, and choose a Connect option: Fast, Deep or Progressive (GA). For a large repository you want results from quickly, Progressive matches the plan in this post: a fast first answer, then the full depth result behind it. CI scans use the mode set on the integration unless you pass --scan-mode.
Step 2: add the CI step for pull requests and main
Safeguard's CI integration (GA) runs the CLI (GA) in a plain shell step; the docs say there is no plugin, Action or Pipe to install. Create a secret key under Settings › Developer with the scopes scans:trigger and scans:read, store it as SAFEGUARD_API_KEY in your CI, and add the step. The GitHub workflow the docs show scans pushes to main and pull requests into main:
name: Safeguard
on:
push:
branches: [main]
pull_request:
branches: [main]
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
run: |
set -eu
curl -fsSL https://cli.safeguard.sh/install | bash
"$SAFEGUARD_INSTALL_DIR/safeguard" ci scan --wait --fail-on critical
Each push becomes a new version of its Safeguard project, named after the branch and commit (the docs' example is main-a1b2c3d). On a pull request the head branch is scanned and the version is named after its head commit. Branch lists take wildcards, so a release pattern such as [main, 'release/**'] is a documented option. Add a job level timeout from your own budget; that setting belongs to your CI, not to Safeguard.
Step 3: choose where the scan runs for each stage
The docs describe two places a CI scan can run:
| Where | Command | What it scans, per the docs | Fits which stage |
|---|---|---|---|
| On Safeguard (default) | safeguard ci scan | SAST, SBOM, dependency vulnerabilities | Main branch and release, when the repository is connected as an integration |
| On the runner | safeguard ci scan --local | SBOM, dependency vulnerabilities | Pull requests, self hosted or firewalled Git servers |
With --local, only the results leave the runner, and the CLI reads dependencies from lockfiles and manifests. No clone happens on Safeguard's side, which removes one of the cost phases described earlier. SAST is generally available, so the code analysis part of an on Safeguard scan is too.
Step 4: set the failure rules per stage
The CI step's documented flags:
| Flag | Fails the step when |
|---|---|
--wait | The scan fails or takes longer than --timeout (default 30 minutes) |
--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 |
Two limits from the docs shape how you use these. SAST findings are scanned and shown in Safeguard but are not part of --fail-on yet. And dropping --fail-on gives you report only behaviour, which is the right default for a fast pull request scan you are still tuning.
A reasonable mapping: pull requests run --local --wait --fail-on critical (or report only at first), or --scan-mode fast --wait --fail-on critical on Safeguard if the repository is connected. Main runs on Safeguard with --scan-mode deep --wait and no failure flag, so results flow to triage. Release branches run on Safeguard with --scan-mode deep --wait --fail-on-gate on your release gate. Set --timeout on the release job to your hard ceiling, because the docs say --wait fails the step when the scan exceeds it, which is the fail closed behaviour you want at release.
Step 5: build the release gate from policies
Policies, gates and guardrails are GA. In Safeguard, policies evaluate scan data and produce findings; gates are checkpoints that enforce them. Create policies under Settings › Policies with the visual builder or YAML. The documented conditions include vulnerability severity, CVSS score, EPSS score, license, component age, maintenance and SLSA level, and there are templates such as "No Critical Vulnerabilities".
Each policy has one Action, and the docs list three: Block (the gate fails), Warn (the gate passes with a warning naming the policy) and Notify (the gate passes and the workspace's configured channels are notified). Keep Block for the release gate; use Warn and Notify for rules you want visible on pull requests.
Three documented details matter for the "only a fast scan has run" problem:
- A gate with no evaluated policies shows Not evaluated, never Passed, and an Error is never counted as a pass.
safeguard ci scan --wait --fail-on-gateexits non zero when the gate is Failed, Error or Not evaluated, or when its policies were not evaluated before--timeout. A missing deep result therefore fails the release step instead of slipping through.- Policy outcomes are turned on per workspace by Safeguard. While they are off, the Outcome column does not appear and gate failure in CI through
--fail-on-gatedepends on them, so ask your Safeguard contact to turn them on before you rely on this step.
The docs say gates are evaluated by the platform on the scans it runs for your connected repositories, which is another reason to run the release scan on Safeguard rather than with --local.
KEV status and reachability are not documented gate conditions. Use them as triage filters in the Vulnerabilities view, not in the release gate.
Step 6: walk the full depth when you scan locally
For a deep scan on the build machine, the deep dependency scanning docs show:
safeguard scan --dir . --resolve-transitive --transitive-depth 100
Deep dependency scanning is GA. Safeguard markets it as reaching up to 100 levels of transitive dependencies, and its docs describe deduplicating shared subgraphs and caching paths with no known vulnerabilities. The CLI usage page also lists --scanners <list> (run only these scanners) and says safeguard --help wins if the two disagree.
Note that safeguard scan does not gate: per the docs it exits 0 whenever the scan ran, whatever it found, and it has no severity threshold flag. Gate on the JSON it writes under ./output, or use safeguard ci scan. On a fresh CI container, the docs say to put the scanner tools on the PATH or point TOOLS_DIR at them, since the interactive terminal normally downloads them on first run. Cache that download.
Step 7: run scans on your own machine with the local runner
The local runner (GA) is part of the CLI. Open safeguard, type /login, approve the session in the browser, and the machine starts polling the platform for scan jobs assigned to it. It runs each job locally and uploads only the SBOM and findings, never source code. From the interactive terminal, /scan . scans the current folder and /jobs shows recent scan history. This suits the developer machine stage in the table, and private repositories you cannot connect to a cloud service.
Step 8: schedule rescans for the days between commits
Safeguard rescans a project in three ways: on the schedule you set, when you choose Rescan, and on each CI run of safeguard ci scan. Set the schedule per integration under Integrations › Scheduled: once, every 1 to 24 hours, daily, weekly, or with a cron expression. The GitHub integration guide says that integration has no webhooks to set up, and that to rescan new commits you set a schedule or choose Rescan. Azure DevOps is the exception: its optional service hooks rescan on push and start a pull request review when a pull request is opened or updated.
For the plan in this post, that means a scheduled rescan of main and of each supported release, weekly for instance, on top of the CI step on every push. Connect those branches with Deep, or, to set the depth on each run, schedule a CI job that runs safeguard ci scan --scan-mode deep --wait.
This post does not assume that a new advisory starts a rescan on its own. When a high profile advisory lands, choose Rescan on the affected projects instead of waiting for the next scheduled run.
Step 9: route the results and fix them
To get findings in front of their owners, give a policy the Notify action, which sends its matches to the workspace's configured channels. For fixes, autonomous remediation (GA) opens tested, policy gated pull requests; it ships switched off with every category set to Manual, so you decide what runs on its own. Reachability analysis (GA) ranks which findings to fix first.
How do you prove to auditors that the deep scan ran before release?
An auditor will sample releases and ask, for each one: which check ran, on what, when, what it decided, and who approved any exception. Design so each answer comes from a record, not a recollection.
The evidence for each sampled release
| Question | Evidence | Where it comes from in a Safeguard setup |
|---|---|---|
| Which commit was released? | Release tag and commit hash | Your source control and CI |
| Did a full scan run on that commit? | Project version named after branch and commit, with its scan result | The CI step creates a version per push, named <branch>-<commit> |
| What did the gate decide? | Gate outcome and each policy's outcome, Action and matches | The gate's expanded view and its evaluation history, with policy outcomes on |
| Did the pipeline obey the gate? | CI job log showing the step's exit status | Your CI, from safeguard ci scan --wait --fail-on-gate |
| Was there an exception? | Request, justification, expiry, approver | The policy exception workflow, which records an audit trail |
| Is the artifact the one scanned? | Build provenance | Your build system; Safeguard's attestation and signing features (GA) where you use them |
Three habits that make the evidence hold up
- Bind the scan to the commit, not the branch. "Main was scanned on Tuesday" does not prove the Thursday release was.
- Keep the CI log and the gate history together. One shows what Safeguard decided, the other that the pipeline acted on it.
- Make exceptions expire. The documented exception flow asks for a justification and an expiry date.
What Safeguard does not do
- It does not make a slow build fast. Scan time depends on repository size, ecosystems, network and where the scan runs, and a pipeline that is slow for other reasons stays slow.
- It does not publish scan time guarantees, and the docs do not define what Fast leaves out compared with Deep. This post's budgets are illustrative; measure your own.
- Progressive is generally available. Keep a Deep scan on the release commit for the gate.
- Most connections have no push webhook. Outside Azure DevOps service hooks, new commits are rescanned by your schedule, a manual Rescan or your CI step, so set at least one of them for every repository.
- SAST does not gate builds through
--fail-onyet. SAST is generally available, and the docs say its findings are shown but not part of the severity check. - KEV and reachability are not documented gate conditions. They help you triage in the Vulnerabilities view; your release gate should use the documented conditions.
- Policy outcomes are off until Safeguard turns them on for your workspace, and
--fail-on-gatedepends on them. - The scanning REST API is not available yet. The Continuous Scanning docs page's
/v1endpoints return not found today, per its own banner, so use the app, the CLI and CI. - It does not run your pipeline. Job timeouts, path filters, caching and release branch rules are your CI system's settings. The only schedule Safeguard runs is the one you set under Integrations › Scheduled.
- It does not decide your risk appetite. Which severities block, which warn, and who approves an exception are your policy choices.
Common mistakes
- One scan profile for every stage. Either pull requests crawl or releases ship on a shallow scan.
- No recovery point for what the fast scan skips. Every shortcut needs a named, automatic later scan.
- Blocking pull requests on findings the author did not introduce. People learn to rerun or bypass.
- Treating a timed out release scan as a pass. No result is not a pass. Fail closed.
- Path filters on the release scan. Filters belong on pull requests; the release scan always runs.
- Scanning main and shipping a tag. Scan the release commit or the built artifact you actually ship.
- Leaving CI job timeouts at the platform default. A six hour default is not a budget.
- Assuming gate failure works before policy outcomes are on. Check the Outcome column exists before you rely on
--fail-on-gate.
FAQ
How do I run security scans in CI without slowing every build?
Split scans by stage. Run a fast, cached, report first dependency scan on pull requests, a full depth scan on main and on release candidates, and rescans on a schedule you set afterwards. Put each stage's budget in the pipeline as a job timeout and decide in advance what a timeout means at each stage.
What is the difference between a fast scan and a deep scan?
A fast scan trades completeness for speed: it resolves from lockfiles, relies on cached results and usually skips reachability and full transitive depth. A deep scan walks the whole transitive graph and does the slower analysis. In Safeguard's docs the connect options are described as "Fast scan (quicker)" and "Deep scan (full)", plus Progressive, which gives fast results now and full depth after and is generally available. In CI, safeguard ci scan --scan-mode picks one per run.
How long should a security scan take in a pull request?
There is no standard number. Measure what your scans take today, then set a budget your developers accept, often a few minutes. Any figure in this post is illustrative. What matters more is that the deeper scan runs later, automatically.
Should a pull request scan block the merge?
Block only on rules that are important, unambiguous and caused by the change, such as a newly added dependency with a critical vulnerability and a fix available. Warn on the rest. Keep blocking strictness for the release gate.
Do I still need a deep scan if I scan every pull request?
Yes. Pull request scans are usually incremental and shallow, and new advisories arrive after merge. NIST SP 800-204D says dependency detectors should find transitive dependencies with preferably no depth limit, and that more recent scans give better assurance. Somewhere before and after release, the full graph has to be checked.
How do I make sure the deep scan ran before a release?
Run the release scan on the release commit, make the release job wait for it, and fail the job if the gate is Failed, Error or Not evaluated. With Safeguard, safeguard ci scan --wait --fail-on-gate <gate> does that once policy outcomes are on for your workspace.
Can Safeguard fail a build on SAST findings?
Not through --fail-on today. The docs say SAST findings are scanned and shown in Safeguard but are not part of --fail-on yet, and SAST is generally available.
Can I gate on KEV status or reachability?
Safeguard's docs do not list them as gate conditions. Use them as filters when triaging in the Vulnerabilities view, and gate on documented conditions such as severity, CVSS, EPSS, license, component age, maintenance and SLSA level.
Related reading
- How long can a security scan take in CI? Time budgets
- Does the scanner slow down CI? Scan time questions
- Is CI time your vulnerability remediation bottleneck?
- SAST scan time budget: what to measure for CI
A well staged pipeline tells you sooner and more reliably that a vulnerable dependency is in your build, but finding it is only half the job. The faster your scans surface a problem, the more the time to an actual fix matters, and that is what 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 situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
The first demo is a real Safeguard CLI session. You install the CLI, run safeguard to open the interactive terminal, and ask it to "fix the lodash CVE in this repo". It reads the finding and the advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. It ends with /permissions, /model and /scan ., the same scan you would run as the developer machine stage in this post.
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 is how you would run the developer machine stage, or scan private code without a cloud clone.
Click through it at your own pace, or open it full screen.
To try the stage plan on your own repositories, get the CLI from safeguard.sh/download and read the CI and policy guides in the Safeguard docs.
Quick check
A release scan hits its timeout before finishing. Should the release gate pass?
No. At release, no result must be treated as a failure. Fail closed, find out why the scan was slow, and rerun. With Safeguard, safeguard ci scan --wait --fail-on-gate exits non zero when the gate's policies were not evaluated before --timeout.
Your pull request scan skips transitive depth and reachability. What must be true for that to be safe?
A deeper scan must run automatically later, on main and on the release commit, and the release gate must require its result. Rescans on a schedule you set then catch new advisories against code you already shipped.
Sources
- Safeguard docs: CLI Usage
- Safeguard docs: Safeguard CLI
- Safeguard docs: Slash commands
- Safeguard docs: Local Runner
- Safeguard docs: CI/CD Integration
- Safeguard docs: GitHub Actions
- Safeguard docs: CI/CD Security
- Safeguard docs: GitHub integration (Fast, Deep and Progressive connect options)
- Safeguard docs: Azure DevOps integration (service hooks, Integrations > Scheduled)
- Safeguard docs: Continuous Scanning (API banner)
- Safeguard docs: Deep Dependency Scanning
- Safeguard docs: Software Composition Analysis
- Safeguard docs: Policies and Gates
- Safeguard docs: Policy outcomes and actions
- NIST SP 800-218, Secure Software Development Framework v1.1
- NIST SP 800-218 PDF
- NIST SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines
- NIST SP 800-204D PDF
- SLSA v1.0 specification
- SLSA v1.0 security levels
- OWASP Software Component Verification Standard (project page)
- OWASP SCVS
- CISA Known Exploited Vulnerabilities Catalog
- FIRST Exploit Prediction Scoring System (EPSS)
- GitHub Docs: Workflow syntax for GitHub Actions (timeout-minutes)
- GitHub Docs: Events that trigger workflows (schedule)
- GitLab Docs: CI/CD YAML syntax reference (timeout, rules:changes)
Last reviewed: 24 October 2026. Product capabilities change; check docs.safeguard.sh for the current state.