GitHub Actions
This page is the integration-specific deep-dive that pairs with the provider-neutral CI/CD integration page. If you have not read the latter, start there: the exit-code contract, the strict-then-loosen gating sequence, and the diff-scan pattern are explained once and apply across every CI provider.
Minimum workflow
The recommended pattern uploads SARIF to GitHub Code Scanning so findings render as inline PR comments and persist in the Security tab.
# .github/workflows/vulkro-sf.yml
name: Vulkro for Salesforce
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
security-events: write # required to upload SARIF
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for --since diff scans
- name: Install vulkro-sf
run: |
curl -fsSL https://dist.vulkro.com/install-sf.sh | bash
vulkro-sf --version
- name: Scan
run: |
vulkro-sf scan ./force-app \
--format sarif \
--min-confidence high \
-o sf.sarif
# `vulkro-sf scan` exits 0 (clean), 1 (findings), 2 (error).
# Letting exit 1 propagate fails the job on findings.
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sf.sarif
category: vulkro-sf
The if: always() on the upload step ensures the SARIF lands in Code
Scanning even when the scan exits 1. Without it, a failing scan would
skip the upload and reviewers would never see what failed.
PR-diff scans
A PR pipeline usually only cares about what the PR changed, not the whole-history backlog. Scope the scan to files modified relative to the base branch:
- name: Scan PR diff
if: github.event_name == 'pull_request'
run: |
vulkro-sf scan ./force-app \
--since origin/${{ github.base_ref }} \
--format sarif \
--min-confidence high \
-o sf.sarif
The --since flag walks the diff between HEAD and the named
reference and only emits findings whose file is in the diff. Combined
with --min-confidence high, this is the right shape for PR-time
gating: the build fails only when the PR itself introduces a new,
high-confidence finding.
For scheduled scans (nightly, weekly), drop --since and run a full
pass so the Security tab reflects the whole project state. The
workflow_dispatch + schedule pattern lives in a separate workflow
file.
Gating strategy
The recommended cadence for a mature codebase landing the pipeline for the first time:
- Week 1:
--min-confidence high. Only the highest-confidence findings gate the build. Fix them first. - Week 2 onward: drop to
--min-confidence medium(the default). - Mature codebase: drop to
--min-confidence lowfor the full surface.
This walks the noise floor down as the remediation backlog clears, instead of dumping every finding on day one. Pair the strategy with a baseline so the existing backlog does not block PRs while the team works through it.
Caching the CVE bundle
vulkro-sf keeps its CVE bundle and rule-pack metadata under
~/.vulkro/data/. Caching that directory between runs shaves the
install + warmup time off every PR scan:
- name: Cache vulkro-sf data
uses: actions/cache@v4
with:
path: ~/.vulkro/data
key: vulkro-sf-data-${{ runner.os }}-v1
restore-keys: |
vulkro-sf-data-${{ runner.os }}-
The bundle version is bumped when a new rule pack ships; rotating the
cache key (e.g. -v2, -v3) forces a refresh on the next run. In
practice the bundle is small (under 50MB) so the cache step is mostly
a latency win, not a bandwidth one.
Air-gapped runners
vulkro-sf is a single static binary and does not make outbound
requests during a scan. For air-gapped self-hosted runners:
- Bake the
vulkro-sfbinary into the runner image (Docker, AMI, or whatever your platform uses). Pin a specific version withVULKRO_SF_VERSIONand fetch fromdist.vulkro.comonce at image build time; from then on the image carries the binary. - Set
VULKRO_OFFLINE=1on the runner. This disables every optional outbound call (telemetry, opportunistic CVE-bundle refresh) and turns any attempted egress into a hard error so a misconfiguration cannot silently fall back to the public CDN. - Mirror the CVE bundle into the runner image at build time (or
set
VULKRO_CDNto your internal mirror at install time). The bundle is the only file the scanner needs that is not in the binary. - Verify the SHA-256 of the pinned binary against the published
.sha256. The installer does this; if you fetch the binary directly for an image build, do the verification yourself.
The license-server activation is the one operation that does talk to Vulkro's infrastructure. Activate the runner once when building the image, not on every job.
Live-org subcommands in CI
The live-org pass (org status, org perms, org packages) is less
commonly run in CI than source-only scan, but a nightly cron job is
the right shape for it. The pattern uses the JWT bearer flow so the
runner can authenticate without a human in the loop.
- Generate a server-key and self-signed certificate for the
Salesforce CLI on a trusted machine (
openssl genrsa, etc.). - Create a Connected App in the org with
Use digital signaturesenabled, paste in the certificate's public key, grant it theManage user data via APIs (api)andPerform requests at any time (refresh_token, offline_access)scopes (noFull). - Store the server-key in a GitHub Actions secret
(
SF_JWT_SERVER_KEY). - Authenticate in the workflow:
- name: sf JWT auth
env:
SF_JWT_SERVER_KEY: ${{ secrets.SF_JWT_SERVER_KEY }}
run: |
echo "$SF_JWT_SERVER_KEY" > server.key
sf org login jwt \
--client-id ${{ secrets.SF_CONSUMER_KEY }} \
--jwt-key-file server.key \
--username ${{ secrets.SF_USERNAME }} \
--alias prod-ci
rm server.key
- name: Live-org perms
run: vulkro-sf org perms --target-org prod-ci
Restrict the Connected App to a single profile (the CI service user)
and a single IP range (your GitHub Actions egress). The CI service
user gets the minimum permissions vulkro-sf needs to read metadata
(no Modify All Data, no DML permissions).
Where to go next
- GitLab CI: the same shape for GitLab pipelines.
- SARIF output: the field-level SARIF contract, for any downstream tool that consumes the JSON directly.
- CI/CD integration: the provider-neutral guide that covers the exit-code contract and the strict-then-loosen sequence.
- The sf CLI handoff: how the live-org subcommands authenticate and what they read, end-to-end.