Safeguard
Tutorials

How to Add Security Scanning to Your CI/CD Pipeline

Wire dependency, container, and secret scanning into GitHub Actions or GitLab CI as a required check that blocks risky merges — with working workflow files and sensible thresholds.

Daniel Osei
DevSecOps Engineer
6 min read

Security scanning that runs on someone's laptop, sometimes, catches nothing reliably. The only scan that changes outcomes is the one that runs automatically on every change and blocks the merge when the risk is too high. The challenge is doing that without turning CI into a red wall that developers learn to ignore or bypass. This guide wires dependency and container scanning into your pipeline as a required status check, with thresholds you can actually live with and caching so it stays fast.

Prerequisites

  • A repository on GitHub or GitLab with an existing CI pipeline (even an empty one).
  • A Safeguard project token with ci:scan and sbom:upload scopes, created under Project → Settings → API Tokens.
  • Admin access to configure branch protection or merge-request rules.

Step 1: Store the token as a secret

On GitHub, go to Settings → Secrets and variables → Actions → New repository secret, name it SAFEGUARD_TOKEN, and paste the value. On GitLab, add it under Settings → CI/CD → Variables as a masked, protected variable with the same name. Never commit a token to the repo.

Step 2: Add the workflow (GitHub Actions)

Create .github/workflows/safeguard.yml:

name: safeguard
on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write
  id-token: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - uses: safeguard-sh/gate@v2
        with:
          token: ${{ secrets.SAFEGUARD_TOKEN }}
          fail-on: high
          sbom-upload: true
          comment-mode: summary-and-top-findings
          cache: true

fetch-depth: 2 lets the action diff the PR head against its base so its comment explains what the change introduced. cache: true reuses advisory data keyed by lockfile hash so repeat runs finish in seconds.

Step 3: Or add it to GitLab CI

If you are on GitLab, add this to .gitlab-ci.yml instead:

safeguard-scan:
  image: alpine:3.19
  script:
    - curl -sSfL https://get.safeguard.sh/install.sh | sh
    - export PATH="$HOME/.safeguard/bin:$PATH"
    - sg scan --fail-on high --sbom-upload
  variables:
    SAFEGUARD_TOKEN: $SAFEGUARD_TOKEN
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

The exit code drives the pipeline: 0 is clean, 2 is policy violations, 3 is a scan error.

Step 4: Open a test pull request

Add a known-vulnerable dependency on a branch — for example lodash@4.17.20 — and open a PR. Within a minute the scan runs and posts a comment naming each finding, its severity, and the fixed version, and the check shows as failing. Bump the dependency, push again, and the same check goes green. That round trip is the whole point: fast, specific feedback tied to the change.

Step 5: Make the check required

A check that can be skipped is advisory, not enforcement. On GitHub, go to Settings → Branches → Branch protection rules → main and add safeguard / scan under Require status checks to pass before merging. On GitLab, set the scan job as a required approval in Settings → Merge requests. Now no one can merge to main without a passing scan.

Step 6: Tune the threshold for your team

Start permissive and tighten as the backlog clears, so CI is credible from day one:

# Strict on main, lenient on everything else
fail-on: ${{ github.base_ref == 'main' && 'high' || 'critical' }}

Begin at critical during onboarding, move to high once the existing findings are triaged, and reserve medium for mature, low-noise repos.

Verify the result

Confirm enforcement actually works:

# Reproduce the CI result locally before pushing
sg scan --fail-on high

# Confirm the check is registered as required (GitHub CLI)
gh api repos/:owner/:repo/branches/main/protection/required_status_checks

If a PR with a critical finding can still be merged, the status check name in branch protection does not match the job name — align them exactly.

How Safeguard streamlines this

The reason pipeline scanning usually fails is inconsistency: the local tool, the CI tool, and the policy all drift apart, so developers get blocked by rules they could not have predicted. Safeguard runs one engine everywhere. The CLI a developer runs locally, the CI gate, and the policy file are the same, so a passing local scan means a passing CI check — no surprise red. When the gate does block a PR, Auto-Fix can open a remediation pull request automatically, turning a failed check into a one-click merge instead of a research task. Caching keyed to your lockfile keeps repeat runs to seconds, and every run publishes to a dashboard so you can see posture trends across repos. If you are weighing options, the comparison hub shows how this single-engine approach differs from bolting several scanners together. See plans on the pricing page.

Ready to gate your pipeline? Connect a repository at app.safeguard.sh/register.

Frequently Asked Questions

Where in the pipeline should the security scan run? On pull or merge requests, before the merge to your protected branch, configured as a required status check. That placement blocks risky code from entering main while giving developers feedback on the change they are actively working on, which is when a fix is cheapest.

Will scanning slow down my builds? Not meaningfully once caching is enabled. Advisory data and resolution results are cached by lockfile hash, so an unchanged dependency set returns in seconds. Only a genuine change to your dependencies triggers a full re-resolution.

How do I stop CI from becoming a wall of red? Start with a critical-only threshold so the pipeline is green except for the issues nobody disputes, clear the backlog, then tighten to high. Pairing the gate with automated fix pull requests also means a block comes with a ready-made path to green rather than just a rejection.

Do scans work on pull requests from forks? They can, but forks do not receive your secrets by default, which is a good security property. The standard pattern is a two-stage flow: a read-only scan on the fork's push against public advisory data, then a maintainer-approved run that re-scans with the authenticated token.

Can I scan a monorepo with multiple services? Yes. Use a CI matrix with one leg per service directory, each pointing at its own path and project. Every leg reports independently, so you can track and gate each service separately without one failure canceling the others.


Explore the Safeguard CLI and Auto-Fix, compare approaches on the comparison hub, or review plans on the pricing page. Full pipeline recipes live in the Safeguard docs.

Never miss an update

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