GitLab CI
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 pipeline
GitLab consumes SARIF directly through the report:sast keyword;
findings render in the Security and Compliance tab of the merge
request and roll up to the project-level Vulnerability Report.
# .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 sf.sarif
artifacts:
when: always
reports:
sast: sf.sarif
paths:
- sf.sarif
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
The when: always block on artifacts ensures the SARIF lands in
GitLab even when the scan exits 1. Without it, a failing scan would
skip the artifact upload and reviewers would never see what failed.
The two rules entries scope the job to merge-request events and
default-branch pushes; drop the second rule if you want the scan
gating MRs only, with no scheduled pass on main.
MR-diff scans
A merge-request pipeline usually only cares about what the MR changed. Scope the scan to files modified relative to the diff base:
script:
- |
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
vulkro-sf scan ./force-app \
--since "$CI_MERGE_REQUEST_DIFF_BASE_SHA" \
--format sarif \
--min-confidence high \
-o sf.sarif
else
vulkro-sf scan ./force-app \
--format sarif \
--min-confidence high \
-o sf.sarif
fi
CI_MERGE_REQUEST_DIFF_BASE_SHA is the GitLab-provided base commit of
the MR. The branch-of-the-MR is at HEAD. Combined with
--min-confidence high, this is the right shape for MR-time gating:
the build fails only when the MR itself introduces a new,
high-confidence finding.
The fallback path (no CI_MERGE_REQUEST_DIFF_BASE_SHA, e.g. scheduled
pipeline) runs a full scan so the Security tab reflects the whole
project state.
Gating with exit codes
GitLab fails a job on any non-zero exit by default; the
vulkro-sf scan exit contract maps cleanly:
0-> job passes, no findings.1-> scan completed, findings were reported. Job fails. SARIF is uploaded, MR shows the security blockers.2-> error (bad arguments, IO failure, project not detected, internal crash). Job fails. SARIF may be partial or empty; stderr carries the real reason.
The split between 1 and 2 matters when a downstream job
(notification, autoreviewer comment) keys off the failure reason. The
GitLab allow_failure: keyword can be set per-rule to soft-fail a
warm-up week while the team works through the initial backlog.
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.
Pair with a baseline so the existing backlog does not block MRs while the team works through it.
Caching the CVE bundle
The same pattern as GitHub Actions, expressed in GitLab cache syntax:
vulkro-sf-scan:
cache:
key: vulkro-sf-data-v1
paths:
- .vulkro-data/
variables:
VULKRO_DATA_DIR: $CI_PROJECT_DIR/.vulkro-data
# ... rest of the job ...
VULKRO_DATA_DIR overrides the default ~/.vulkro/data/ so the cache
lands inside the GitLab-managed project directory. Rotate the cache
key (-v2, -v3) when a new rule pack ships to force a refresh.
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. Pin a specific version withVULKRO_SF_VERSIONat image-build time; from then on the image carries the binary. - Set
VULKRO_OFFLINE=1on the runner as a project-level CI/CD variable. This disables every optional outbound call and turns any attempted egress into a hard error. - Mirror the CVE bundle into the runner image at build time, or
set
VULKRO_CDNto your internal mirror. - Verify the SHA-256 of the pinned binary against the published
.sha256at image-build time.
The license-server activation is the one operation that does talk to Vulkro's infrastructure. Activate the runner image once during the image build, 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 scheduled pipeline
is the right place for a nightly check. The authentication pattern
uses the Salesforce CLI's JWT bearer flow:
- Generate a server-key and self-signed certificate on a trusted
machine (
openssl genrsa, etc.). - Create a Connected App in the target org with
Use digital signaturesenabled, paste in the certificate's public key, grant itManage user data via APIs (api)andPerform requests at any time (refresh_token, offline_access)scopes (noFull). - Store the server-key in a protected, masked GitLab CI/CD
variable (
SF_JWT_SERVER_KEY). - Authenticate in the job:
vulkro-sf-org-perms:
stage: security
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
before_script:
- apt-get update && apt-get install -y curl ca-certificates nodejs npm
- curl -fsSL https://dist.vulkro.com/install-sf.sh | bash
- npm install -g @salesforce/cli
- echo "$SF_JWT_SERVER_KEY" > server.key
- sf org login jwt
--client-id "$SF_CONSUMER_KEY"
--jwt-key-file server.key
--username "$SF_USERNAME"
--alias prod-ci
- rm server.key
script:
- vulkro-sf org perms --target-org prod-ci --format sarif -o sf-org.sarif
artifacts:
when: always
reports:
sast: sf-org.sarif
expire_in: 30 days
Restrict the Connected App to the CI service user only and to the
GitLab runner's egress IP range. 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
- GitHub Actions: the equivalent for GitHub pipelines.
- SARIF output: the field-level SARIF contract, for any downstream tool that consumes the JSON directly.
- CI/CD integration: the provider-neutral guide.
- The sf CLI handoff: how the live-org subcommands authenticate and what they read end-to-end.