CI/CD Integration
Scan every push and pull request from GitHub Actions, GitLab CI/CD, Bitbucket Pipelines, Jenkins, Azure Pipelines or any CI, and fail the build on vulnerabilities
CI/CD Integration
One step in your pipeline runs safeguard ci scan. Each push becomes a new version of its
Safeguard project, named <branch>-<commit> (for example main-a1b2c3d), and the step can
fail the build when the scan finds vulnerabilities at or above a severity you choose.
Supported CI/CD
| CI system | Setup guide | Template |
|---|---|---|
| GitHub Actions | GitHub Actions | github-actions.yml |
| GitLab CI/CD | GitLab CI/CD | safeguard.gitlab-ci.yml |
| Bitbucket Pipelines | Bitbucket Pipelines | bitbucket-pipelines.yml |
| Jenkins | Jenkins | Jenkinsfile |
| Azure Pipelines | Azure Pipelines | azure-pipelines.yml |
| Any other CI | Other CI systems | — |
To scan the container images your pipeline builds, per environment, see Container images.
In the app, the GitHub, GitLab, Bitbucket and Azure DevOps cards under Integrations → Source Control have a Set up CI/CD button, and the Git card has one for Jenkins and any other CI. It opens the setup with your branches and severity filled in; a project's Set up CI/CD does the same with that project's id.
There is no plugin, Action or Pipe to install. Every system runs the CLI in a plain shell step,
on a Linux or macOS runner with bash and curl.
Setup in three steps
- Create a secret key under Settings → Developer with the scopes
scans:triggerandscans:read. You can lock it to one project. - Store it in your CI as a secret named
SAFEGUARD_API_KEY. Each guide says where. - Add the step from your CI's guide, and push.
The first run creates the project, so nothing else is needed to start.
Where the scan runs
| Mode | Command | Needs | Scans |
|---|---|---|---|
| On Safeguard (default) | safeguard ci scan | the repository connected as an integration, or public on github.com, gitlab.com or bitbucket.org | SAST, SBOM, dependency vulnerabilities |
| On the runner | safeguard ci scan --local | a checkout in the job; no integration | SBOM, dependency vulnerabilities |
With --local only the results leave the runner, so it also works for self-hosted and
firewalled Git servers. It reads dependencies from lockfiles and manifests
(package-lock.json, go.sum, requirements.txt, pom.xml, …).
Choose the branches
Every template scans pushes to main and pull requests into it. Change the branches in the
file; wildcards work (release/*). Each guide shows the exact setting. On a pull request the
source branch is scanned and its head commit names the version.
Fail the build
| Flag | Fails the step when |
|---|---|
--wait | the 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 |
SAST findings are scanned and shown in Safeguard but are not part of --fail-on yet. Drop
--fail-on to report without failing.
Other CI systems
Any CI that runs a shell step works. Pass the repository, branch and commit when your CI is not one of the five above (CircleCI is detected too):
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"Pin a CLI version with SAFEGUARD_VERSION=<version> on the install line so a release does not
change a build under you.
Troubleshooting
| Message | What to do |
|---|---|
AUTHENTICATION_REQUIRED or INVALID_SECRET_KEY | SAFEGUARD_API_KEY is missing, or the key is invalid, expired or revoked. Check the secret's name, and that the step can read it (Azure maps it through env). |
MISSING_SCOPE | The key lacks scans:trigger or scans:read. Create one with both. |
PROJECT_NOT_ALLOWED | The key is locked to another project. |
PROJECT_AMBIGUOUS or INTEGRATION_AMBIGUOUS | More than one project or integration fits the repository. Pass --project-id (or --integration-id on the first run). |
PROJECT_NOT_RESCANNABLE | The project has no repository Safeguard can clone (for example, an upload project). Connect the repository under Integrations, or use --local. |
SCAN_NOT_DISPATCHED / SCAN_NOT_STARTED | The platform could not start the scan. Retry; if it persists, contact support with the build URL. |
| The step never runs | The branch is not in the template's branch list, or (pull requests from forks) the CI does not pass secrets to fork builds. |
Scanning on the build machine instead
safeguard scan and safeguard detect run fully on the runner and write files you can keep as
artifacts or upload to a code-scanning tab. They report without failing the build; gate on the
files they write:
safeguard scan --dir . --output out/
jq -e '[.data.matches[].vulnerability.severity | ascii_downcase]
| index("critical") | not' out/*/vuln.jsonsafeguard scan runs syft and grype: install both on the PATH (or point TOOLS_DIR at them)
on a fresh runner.
| Command | Exit code |
|---|---|
safeguard scan | 0 when the scan ran, whatever it found. 2 when results were written but the vulnerability scan did not run. 1 when the command failed. |
safeguard detect | 0 when the scan ran, whatever it found. Non-zero only when the command failed. |