Tutorials

Getting Started: Safeguard GitHub Actions Gate

Set up the Safeguard GitHub Action to block risky pull requests on dependency vulnerabilities, license violations, and policy breaches before merge.

Shadab Khan
Security Engineer
7 min read

Every serious security tool eventually becomes a required status check on GitHub PRs. The Safeguard GitHub Action is designed for exactly that job — scan the PR, enforce policy, comment with specific remediation, and fail the check when the risk is above threshold. This walkthrough takes a repo from "no checks" to "required status check on all PRs" in about twenty minutes.

What Does the Action Actually Do?

safeguard-sh/gate is a JavaScript action that runs the Safeguard CLI against your PR, produces findings, uploads an SBOM, and posts a human-readable comment on the PR with the top issues and suggested fixes. It exits non-zero on policy violations so you can mark the check required. It's the same scan engine the IDE and CLI use, so a PR's CI result matches what a developer saw locally — no more surprise failures.

Step 1: Create a Project Token

In the Safeguard portal, go to Project → Settings → API Tokens → Create. Give it the ci:scan and sbom:upload scopes. Tokens are scoped to a single project, so you'll create one per repo (or share one across a group of related repos via the org-level token if you prefer).

Store the token as a GitHub Actions secret. In your repo go to Settings → Secrets and variables → Actions → New repository secret. Name it SAFEGUARD_TOKEN and paste the value. For orgs, use an organization secret and scope it to the relevant repos.

Step 2: Add the Workflow

Create .github/workflows/safeguard.yml:

name: safeguard
on:
  pull_request:
    branches: [main]
  push:
    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
          comment-mode: summary-and-top-findings
          sbom-upload: true
          policy: default-strict

fetch-depth: 2 lets the action diff the PR head against its base so the comment can describe what changed in the SBOM, not just the absolute finding count. id-token: write is required for OIDC attestation when you enable it later.

Step 3: Commit and Open a Test PR

Open a PR that adds a deliberately vulnerable dependency, for example lodash@4.17.20 in a Node project. Push it. Within a minute or two the action runs and posts a comment like:

Safeguard found 1 critical, 2 high severity issues.

CRITICAL — CVE-2020-8203 — lodash@4.17.20 — prototype pollution
   Suggested fix: bump to lodash@4.17.21

HIGH    — CVE-2021-23337 — lodash@4.17.20 — command injection
   Suggested fix: bump to lodash@4.17.21

Policy `default-strict` blocks HIGH and above. Run `sg scan` locally to reproduce.

The check shows as failing. If you remove the vulnerable dependency or raise its version, the next push re-runs the action and updates the PR comment in place — no comment spam.

Step 4: Make the Check Required

Go to Settings → Branches → Branch protection rules → main → Edit. Under Require status checks to pass before merging, add safeguard / scan. Once merged, this check becomes non-negotiable — you can't merge without it passing or an admin override.

Step 5: Tune the Fail Threshold

fail-on: high is a good default. For regulated workloads, use critical during onboarding and tighten to high or medium once the backlog is clean. For early-stage projects still finding their feet, start at critical so CI doesn't become a constant red wall — you can't ship fixes if you can't merge them.

You can also set different thresholds on different branches:

- uses: safeguard-sh/gate@v2
  with:
    token: ${{ secrets.SAFEGUARD_TOKEN }}
    fail-on: ${{ github.base_ref == 'main' && 'high' || 'critical' }}

Step 6: Enable SBOM Attestation

For full supply-chain provenance, turn on attestation. The action will sign the SBOM with a GitHub OIDC identity and publish it to the Safeguard transparency log:

- uses: safeguard-sh/gate@v2
  with:
    token: ${{ secrets.SAFEGUARD_TOKEN }}
    fail-on: high
    sbom-upload: true
    sbom-attest: true
    attestation-predicate: cyclonedx

Downstream consumers can verify the attestation with sg attest verify <sha> against your org, proving the SBOM came from your actual CI rather than a manually uploaded one.

How Do I Handle Ignores in CI?

The action reads the same .safeguard/ignore.yaml your CLI and IDE use. Ignores live in the repo, go through code review, and must have expiry dates. Here's a correct entry:

rules:
  - id: CVE-2024-21538
    ecosystem: npm
    package: cross-spawn
    version: "7.0.3"
    reason: "Not reachable from entrypoints; see SEC-1482"
    expires: "2026-04-01"
    reviewer: "shadab@example.com"

When an ignore expires, the action flags it in the PR comment a week before the expiry so you don't get a surprise failure on a random Tuesday. Expired ignores fail the scan immediately so they don't silently become allowlists.

How Do I Scan Multiple Projects in a Monorepo?

Use the matrix strategy with per-project paths:

jobs:
  scan:
    strategy:
      fail-fast: false
      matrix:
        project:
          - services/checkout
          - services/payments
          - apps/web
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: safeguard-sh/gate@v2
        with:
          token: ${{ secrets.SAFEGUARD_TOKEN }}
          working-directory: ${{ matrix.project }}
          fail-on: high
          project-id: ${{ matrix.project }}

Each matrix leg publishes independently to the portal, so you can filter dashboards by project and track per-service posture separately. fail-fast: false ensures one service's failure doesn't cancel the others — you want to see all issues at once, not play whack-a-mole.

How Do I Avoid Slowing Down My CI?

Two things. First, enable the remote cache:

- uses: safeguard-sh/gate@v2
  with:
    token: ${{ secrets.SAFEGUARD_TOKEN }}
    cache: true
    cache-ttl: 3600

The cache stores advisory data and resolution results keyed by lockfile hash. A subsequent run with the same lockfile returns in seconds. Second, run the action on pull_request only — don't run it again on merge unless you have a specific reason. The PR check already gated the merge.

For large monorepos, enable path-based triggering so a UI-only PR doesn't trigger the backend scan:

on:
  pull_request:
    paths:
      - "services/checkout/**"
      - ".safeguard/**"

What About PRs from Forks?

GitHub forks don't get access to secrets by default — a good security property, but it breaks the scan because the action can't authenticate. Use the pull_request_target trigger carefully, or adopt a two-stage flow: the fork's push runs a read-only scan against public advisory data; a maintainer-approved workflow_run then re-scans with the authenticated token. Safeguard publishes a reference two-stage-fork.yml in the docs repo — copy it verbatim rather than rolling your own, since the security sharp edges here are subtle.

How Do I See Org-Wide Trends?

Every action run publishes a result to the Safeguard portal. Open the org dashboard to see which repos are accumulating ignores, which services have rising critical counts, and which engineers are closing the most findings. The dashboard data comes directly from the CI runs you've already wired up — no extra configuration needed once the action is in place.

How Safeguard.sh Helps

The GitHub Actions gate is the enforcement point that makes the rest of Safeguard stop being optional. Your IDE extension catches issues early; your CLI lets you iterate; the gate ensures nothing risky gets to main. Because the action, CLI, and IDE all share one policy file and one ignore list, the behavior is consistent end-to-end. You don't have developers hitting red CI for rules they couldn't have predicted — they saw the same findings locally before pushing. Add the gate once, make it required, and supply chain hygiene stops being a push-and-pray exercise.

Never miss an update

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