GitHub Actions
Run vulkro-sf on every pull request and upload the findings to GitHub Code Scanning.
# .github/workflows/vulkro-sf.yml
name: Vulkro for Salesforce
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
security-events: write # to upload SARIF
jobs:
scan:
runs-on: ubuntu-latest
env:
VULKRO_ACCOUNT_TOKEN: ${{ secrets.VULKRO_ACCOUNT_TOKEN }} # CI key from the portal
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for --gate-vs
- name: Install vulkro-sf
run: curl -fsSL https://dist.vulkro.com/install-sf.sh | bash
- name: Sign in with the CI key
run: vulkro-sf login
- name: Scan
run: vulkro-sf scan ./force-app --format sarif > sf.sarif
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sf.sarif
category: vulkro-sf
Create the CI key in the portal (app.vulkro.com) and save it as the repository secret VULKRO_ACCOUNT_TOKEN. Without it the scan exits 1 with license_required. if: always() uploads the SARIF even when the scan fails on findings.
Fail only on new findings
On pull requests, replace the Scan step with this one. The SARIF still holds every finding, but the step fails only on findings the pull request introduced:
- name: Scan (fail on new findings only)
run: vulkro-sf scan ./force-app --gate-vs origin/${{ github.base_ref }} --format sarif > sf.sarif
To annotate the pull request with only the new findings, use --format github-annotations instead of sarif (no file redirect needed). See Baselines.
Exit codes
0 passes; 1 fails the job (findings, or no licence); 2 fails it as an error (read stderr).
Air-gapped runners
Bake a pinned binary into the runner image (VULKRO_SF_VERSION), set VULKRO_OFFLINE=1, and license it by setting VULKRO_LICENSE_B64 to a base64 .lic file stored as a secret, or with vulkro-sf activate <file.lic> at image build. See Licensing.
Live-org audits on a schedule
Live-org commands need an sf login on the runner. Use the Salesforce CLI's JWT bearer flow with a dedicated Connected App: certificate-based, scopes api and refresh_token (never Full), limited to one read-only CI user.
- name: Install the Salesforce CLI
run: npm install -g @salesforce/cli
- name: sf JWT login
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 posture report
run: vulkro-sf org report --target-org prod-ci -O posture.html
Run this from a schedule: workflow rather than on every pull request. The CI user needs API Enabled and View Setup and Configuration; see Live-org setup.
See also: CI/CD integration, SARIF output, The sf CLI handoff