Skip to main content

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 low for 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:

  1. Bake the vulkro-sf binary into the runner image. Pin a specific version with VULKRO_SF_VERSION at image-build time; from then on the image carries the binary.
  2. Set VULKRO_OFFLINE=1 on the runner as a project-level CI/CD variable. This disables every optional outbound call and turns any attempted egress into a hard error.
  3. Mirror the CVE bundle into the runner image at build time, or set VULKRO_CDN to your internal mirror.
  4. Verify the SHA-256 of the pinned binary against the published .sha256 at 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:

  1. Generate a server-key and self-signed certificate on a trusted machine (openssl genrsa, etc.).
  2. Create a Connected App in the target org with Use digital signatures enabled, paste in the certificate's public key, grant it Manage user data via APIs (api) and Perform requests at any time (refresh_token, offline_access) scopes (no Full).
  3. Store the server-key in a protected, masked GitLab CI/CD variable (SF_JWT_SERVER_KEY).
  4. 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