CI/CD recipes

Pepper exposes drop-in pipeline templates for the major CI systems. Each template uploads the source, polls until the scan finishes, downloads SBOMs, signs them with cosign, and fails the build when the project's build gate is breached.

Issue an API key

Pipelines authenticate with an API key (Settings → API Keys → Create), which produces a one-shot token like ppr_xxxxxxxxxxxxxxxxxxxxxxxx. Store it as a CI secret:

You will also need PEPPER_API_URL (your Pepper base URL).

Download a ready-to-use template

Every Pepper instance hosts the latest templates at /api/cicd-templates/<platform>:

curl -sL https://pepper.your-org.com/api/cicd-templates/github   -o .github/workflows/pepper.yml
curl -sL https://pepper.your-org.com/api/cicd-templates/gitlab   -o .gitlab-ci.pepper.yml
curl -sL https://pepper.your-org.com/api/cicd-templates/jenkins  -o Jenkinsfile

GitHub Actions

Save as .github/workflows/pepper.yml:

name: Pepper Security Scan
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write   # required for cosign keyless signing
  pull-requests: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Pepper scan
        id: pepper
        env:
          PEPPER_API_URL: ${{ secrets.PEPPER_API_URL }}
          PEPPER_API_KEY: ${{ secrets.PEPPER_API_KEY }}
        run: |
          set -euo pipefail
          tarball=$(mktemp -u).tar.gz
          tar --exclude-vcs --exclude=node_modules --exclude=dist -czf "$tarball" .
          resp=$(curl -fsSL -X POST \
            -H "Authorization: Bearer $PEPPER_API_KEY" \
            -F "source=@$tarball" \
            -F "scanType=FULL" \
            -F "branch=${GITHUB_REF_NAME}" \
            -F "commitSha=${GITHUB_SHA}" \
            "$PEPPER_API_URL/api/scans")
          scan_id=$(echo "$resp" | jq -r '.scanId')
          echo "scan_id=$scan_id" >> "$GITHUB_OUTPUT"

          for i in $(seq 1 180); do
            scan=$(curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \
              "$PEPPER_API_URL/api/scans/$scan_id")
            status=$(echo "$scan" | jq -r .status)
            [[ "$status" == "COMPLETED" || "$status" == "FAILED" ]] && break
            sleep 10
          done

          gate=$(echo "$scan" | jq -r .gateResult)
          echo "Scan $scan_id status=$status gate=$gate"
          if [[ "$gate" == "FAILED" ]]; then
            echo "::error::Pepper build gate failed"
            exit 1
          fi

      - name: Download SBOM
        if: always()
        env:
          PEPPER_API_URL: ${{ secrets.PEPPER_API_URL }}
          PEPPER_API_KEY: ${{ secrets.PEPPER_API_KEY }}
        run: |
          curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \
            "$PEPPER_API_URL/api/scans/${{ steps.pepper.outputs.scan_id }}/artifacts/cyclonedx" \
            -o sbom.cyclonedx.json || true
          curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \
            "$PEPPER_API_URL/api/scans/${{ steps.pepper.outputs.scan_id }}/artifacts/spdx" \
            -o sbom.spdx.json || true

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: pepper-sbom
          path: |
            sbom.cyclonedx.json
            sbom.spdx.json

      - uses: sigstore/cosign-installer@v3
        if: success()

      - name: Sign SBOM (keyless via Fulcio + Rekor)
        if: success()
        run: |
          COSIGN_EXPERIMENTAL=1 cosign sign-blob --yes \
            --output-signature sbom.cyclonedx.json.sig \
            sbom.cyclonedx.json || true
          COSIGN_EXPERIMENTAL=1 cosign sign-blob --yes \
            --output-signature sbom.spdx.json.sig \
            sbom.spdx.json || true

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: pepper-sbom-signatures
          path: |
            sbom.cyclonedx.json.sig
            sbom.spdx.json.sig

GitLab CI

Save as .gitlab-ci.yml (or merge with your existing one):

pepper_security:
  stage: test
  image: alpine:3
  before_script:
    - apk add --no-cache curl jq tar
  script:
    - tar --exclude=node_modules --exclude=.git -czf /tmp/src.tar.gz .
    - |
      resp=$(curl -fsSL -X POST \
        -H "Authorization: Bearer $PEPPER_API_KEY" \
        -F "source=@/tmp/src.tar.gz" \
        -F "scanType=FULL" \
        -F "branch=$CI_COMMIT_REF_NAME" \
        -F "commitSha=$CI_COMMIT_SHA" \
        "$PEPPER_API_URL/api/scans")
      scan_id=$(echo "$resp" | jq -r '.scanId')
      for i in $(seq 1 180); do
        scan=$(curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \
          "$PEPPER_API_URL/api/scans/$scan_id")
        status=$(echo "$scan" | jq -r .status)
        [ "$status" = "COMPLETED" ] || [ "$status" = "FAILED" ] && break
        sleep 10
      done
      gate=$(echo "$scan" | jq -r .gateResult)
      curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \
        "$PEPPER_API_URL/api/scans/$scan_id/artifacts/cyclonedx" -o sbom.cyclonedx.json || true
      if [ "$gate" = "FAILED" ]; then exit 1; fi
  artifacts:
    when: always
    paths:
      - sbom.cyclonedx.json

Jenkins

pipeline {
  agent any
  environment {
    PEPPER_API_URL = credentials('PEPPER_API_URL')
    PEPPER_API_KEY = credentials('PEPPER_API_KEY')
  }
  stages {
    stage('Pepper Scan') {
      steps {
        sh '''
          set -e
          tar --exclude=node_modules --exclude=.git -czf /tmp/src.tar.gz .
          resp=$(curl -fsSL -X POST \\
            -H "Authorization: Bearer $PEPPER_API_KEY" \\
            -F "source=@/tmp/src.tar.gz" \\
            -F "scanType=FULL" \\
            -F "branch=${BRANCH_NAME:-main}" \\
            "$PEPPER_API_URL/api/scans")
          scan_id=$(echo "$resp" | jq -r '.scanId')
          for i in $(seq 1 180); do
            scan=$(curl -fsSL -H "Authorization: Bearer $PEPPER_API_KEY" \\
              "$PEPPER_API_URL/api/scans/$scan_id")
            status=$(echo "$scan" | jq -r .status)
            [ "$status" = "COMPLETED" ] && break
            sleep 10
          done
          gate=$(echo "$scan" | jq -r .gateResult)
          if [ "$gate" = "FAILED" ]; then exit 1; fi
        '''
      }
    }
  }
}

Pre-commit hook

For developer-machine enforcement before code even reaches CI, install the Pepper pre-commit hook:

curl -fsSL https://pepper.your-org.com/api/precommit/install.sh \
  | bash -s -- https://pepper.your-org.com ppr_xxxxxxxxxxxx

See Pre-commit hook for what it scans and how to tune PEPPER_FAIL_ON.

Tune the build gate

Build gates are per-project. The default is 0 CRITICAL, 5 HIGH, 20 MEDIUM, unlimited LOW, fail-on-new=true. Adjust under Settings → Build Gates or via the API:

curl -X PUT \
  -H "Authorization: Bearer $PEPPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"maxCritical":0,"maxHigh":0,"maxMedium":10,"maxLow":-1,"failOnNew":true}' \
  $PEPPER_API_URL/api/settings/build-gates?projectId=<PROJECT_ID>

Branch-event webhooks (alternative to CI scripts)

If you don't want to run the scan inside CI, point your VCS at Pepper's webhook and Pepper will scan PR/MR head commits on its own infrastructure.

Status checks: Pepper does not yet write back a check-run; consume gate status by polling GET /api/scans/<id> as above.