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:
- GitHub: Repository → Settings → Secrets and variables → Actions
- GitLab: Project → Settings → CI/CD → Variables (mark "Masked" + "Protected")
- Jenkins: Manage Jenkins → Credentials → Secret text
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>
- Set any limit to
-1to mean "unlimited". failOnNewrejects any finding that wasn't in the previous completed scan — useful for "no-new-issues" policies that don't require fixing the existing backlog all at once.
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.
- GitHub: Repo → Settings → Webhooks →
https://pepper.your-org.com/api/webhooks/github. Secret =GITHUB_WEBHOOK_SECRETfrom your Pepper env. Events: pushes + pull requests. - GitLab: Project → Settings → Webhooks →
https://pepper.your-org.com/api/webhooks/gitlab. Trigger: merge request events + push events.
Status checks: Pepper does not yet write back a check-run; consume gate status
by polling GET /api/scans/<id> as above.