Skip to main content

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:

  1. Install vulkro-sf into 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.
  2. Run vulkro-sf scan over the project source.
  3. Gate on the exit code: 0 passes the job, 1 fails it (findings), 2 is a hard error.
  4. 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:

  1. Week 1: --min-confidence high. Only the highest-confidence findings gate the build. Fix them first.
  2. Week 2 onward: drop to --min-confidence medium (the default).
  3. Mature codebase: drop to --min-confidence low for 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:

  1. Bake the vulkro-sf binary into the runner image (Docker, AMI, or whatever your platform uses). Pin a specific version with VULKRO_SF_VERSION and download from the public CDN once, when building the image; from then on the image carries the binary.
  2. Set VULKRO_CDN only if you also want the installer to talk to your internal mirror at build time. At scan time the binary needs no CDN.
  3. 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 perms and org packages into a scheduled (not PR-gated) workflow.