CI/CD integration
This page is for the engineer adding vulkro-sf to a continuous
integration pipeline. The artifact you produce is a PR-time security
gate: every pull request runs the scan, and the build fails on
findings before merge.
The model
The standard pattern is the same across CI providers:
- Install
vulkro-sfinto the runner. On a self-hosted runner bake the binary into the image; on a hosted runner install it as the first step of the job. - Run
vulkro-sf scanover the project source. - Gate on the exit code:
0passes the job,1fails it (findings),2is a hard error. - Upload the SARIF (and optionally the HTML report) as a job artifact, so reviewers can read findings outside the runner log.
The sections below give a minimal working snippet for each major CI provider.
GitHub Actions
The recommended pattern uploads SARIF to GitHub Code Scanning, so findings render as inline PR comments and stay 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
- 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 vulkro-sf.sarif
# `vulkro-sf scan` exits 0 (clean), 1 (findings), 2 (error).
# We let exit 1 propagate so the job fails on findings.
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: vulkro-sf.sarif
category: vulkro-sf
- name: Archive HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: vulkro-sf-report
path: vulkro-sf.sarif
The if: always() on the upload step ensures the SARIF lands in Code
Scanning even when the scan exits 1; otherwise a failing scan would
skip the upload and reviewers would not see what failed.
GitLab CI
GitLab parses SARIF into its Vulnerability Report under the same
report:sast keyword.
# .gitlab-ci.yml
stages:
- security
vulkro-sf-scan:
stage: security
image: ubuntu:24.04
before_script:
- apt-get update && apt-get install -y curl ca-certificates
- curl -fsSL https://dist.vulkro.com/install-sf.sh | bash
- export PATH="/usr/local/bin:$PATH"
- vulkro-sf --version
script:
- vulkro-sf scan ./force-app
--format sarif
--min-confidence high
-o vulkro-sf.sarif
artifacts:
when: always
reports:
sast: vulkro-sf.sarif
paths:
- vulkro-sf.sarif
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
The job fails when vulkro-sf scan exits 1, the SARIF appears under
the Security and Compliance tab of the merge request, and the
artifact is retained for 30 days.
Bitbucket Pipelines
# bitbucket-pipelines.yml
image: ubuntu:24.04
pipelines:
pull-requests:
'**':
- step:
name: Vulkro for Salesforce
script:
- apt-get update && apt-get install -y curl ca-certificates
- curl -fsSL https://dist.vulkro.com/install-sf.sh | bash
- export PATH="/usr/local/bin:$PATH"
- vulkro-sf scan ./force-app
--format sarif
--min-confidence high
-o vulkro-sf.sarif
artifacts:
- vulkro-sf.sarif
Bitbucket does not parse SARIF natively, but the artifact is
downloadable from the pipeline UI. Pair with a SARIF viewer (the
sarif-tools CLI or a JetBrains plugin) for human review.
The exit-code contract
Every vulkro-sf subcommand follows the same contract:
0: success, no findings. CI passes.1: scan completed and findings were reported. CI should fail the job.2: error (bad arguments, IO failure, project not detected, internal crash). CI should fail the job and surface stderr.
In every snippet above the CI provider's default behaviour (fail on
non-zero exit) handles this automatically. The split between 1 and
2 matters when a downstream step needs to distinguish "there are
findings to look at" from "the scan itself broke": gate notifications
or autoreviewer comments on exit 1, page on exit 2.
Starting strict, then loosening
A new pipeline on a mature codebase will fire on every PR if you
start at --min-confidence low. The recommended sequence is the
opposite:
- 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 your remediation backlog clears, rather than dumping every finding on day one. Pair the strategy with a baseline so the existing findings do not block PRs while the team works through them; see Baselines.
Diff scans (PR-only findings)
Most PR pipelines only care about what the PR changed, not the
codebase's whole-history backlog. The --since flag scopes the scan
to files modified relative to a reference:
vulkro-sf scan ./force-app --since main --format sarif -o pr.sarif
The scan walks the diff between the current HEAD and main and
only emits findings whose file changed 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 nightly or scheduled scans, drop --since and run a full pass so
the SARIF in the Security tab reflects the whole project state.
Air-gapped CI
vulkro-sf is a single static binary and does not make outbound
requests during a scan. The standard pattern for air-gapped runners:
- Bake the
vulkro-sfbinary into the runner image (Docker, AMI, or whatever your platform uses). Pin a specific version withVULKRO_SF_VERSIONand download from the public CDN once, when building the image; from then on the image carries the binary. - Set
VULKRO_CDNonly if you also want the installer to talk to your internal mirror at build time. At scan time the binary needs no CDN. - Verify the SHA-256 of the pinned binary against the published
.sha256(the installer does this automatically, but if you fetch directly you must do it yourself).
The live-org subcommands (vulkro-sf org status, org perms, org packages) need outbound HTTPS to your Salesforce instance only.
No call goes to the Vulkro license server, the Vulkro CDN, or any
third party during the scan.
License activation is the one operation that does talk to the Vulkro license server. Activate the runner once when building the image, not on every job.
Per-CI references
The deep-dive integration docs walk through provider-specific features (matrix strategies, multi-org orchestration, the optional GitHub Action wrapper):
Where to go next
- Baselines: accept the current backlog as a baseline, then gate CI on net-new findings only.
- SARIF output: the field-level SARIF contract, for downstream tooling that consumes the JSON directly.
- Live-org setup: wire
org permsandorg packagesinto a scheduled (not PR-gated) workflow.