Skip to main content

GitHub Actions

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 workflow

The recommended pattern uploads SARIF to GitHub Code Scanning so findings render as inline PR comments and persist 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
with:
fetch-depth: 0 # required for --since diff scans

- 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 sf.sarif
# `vulkro-sf scan` exits 0 (clean), 1 (findings), 2 (error).
# Letting exit 1 propagate fails the job on findings.

- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: sf.sarif
category: vulkro-sf

The if: always() on the upload step ensures the SARIF lands in Code Scanning even when the scan exits 1. Without it, a failing scan would skip the upload and reviewers would never see what failed.

PR-diff scans

A PR pipeline usually only cares about what the PR changed, not the whole-history backlog. Scope the scan to files modified relative to the base branch:

- name: Scan PR diff
if: github.event_name == 'pull_request'
run: |
vulkro-sf scan ./force-app \
--since origin/${{ github.base_ref }} \
--format sarif \
--min-confidence high \
-o sf.sarif

The --since flag walks the diff between HEAD and the named reference and only emits findings whose file is 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 scheduled scans (nightly, weekly), drop --since and run a full pass so the Security tab reflects the whole project state. The workflow_dispatch + schedule pattern lives in a separate workflow file.

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.

This walks the noise floor down as the remediation backlog clears, instead of dumping every finding on day one. Pair the strategy with a baseline so the existing backlog does not block PRs while the team works through it.

Caching the CVE bundle

vulkro-sf keeps its CVE bundle and rule-pack metadata under ~/.vulkro/data/. Caching that directory between runs shaves the install + warmup time off every PR scan:

- name: Cache vulkro-sf data
uses: actions/cache@v4
with:
path: ~/.vulkro/data
key: vulkro-sf-data-${{ runner.os }}-v1
restore-keys: |
vulkro-sf-data-${{ runner.os }}-

The bundle version is bumped when a new rule pack ships; rotating the cache key (e.g. -v2, -v3) forces a refresh on the next run. In practice the bundle is small (under 50MB) so the cache step is mostly a latency win, not a bandwidth one.

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 (Docker, AMI, or whatever your platform uses). Pin a specific version with VULKRO_SF_VERSION and fetch from dist.vulkro.com once at image build time; from then on the image carries the binary.
  2. Set VULKRO_OFFLINE=1 on the runner. This disables every optional outbound call (telemetry, opportunistic CVE-bundle refresh) and turns any attempted egress into a hard error so a misconfiguration cannot silently fall back to the public CDN.
  3. Mirror the CVE bundle into the runner image at build time (or set VULKRO_CDN to your internal mirror at install time). The bundle is the only file the scanner needs that is not in the binary.
  4. Verify the SHA-256 of the pinned binary against the published .sha256. The installer does this; if you fetch the binary directly for an image build, do the verification yourself.

The license-server activation is the one operation that does talk to Vulkro's infrastructure. Activate the runner once when building the image, 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 nightly cron job is the right shape for it. The pattern uses the JWT bearer flow so the runner can authenticate without a human in the loop.

  1. Generate a server-key and self-signed certificate for the Salesforce CLI on a trusted machine (openssl genrsa, etc.).
  2. Create a Connected App in the org with Use digital signatures enabled, paste in the certificate's public key, grant it the 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 GitHub Actions secret (SF_JWT_SERVER_KEY).
  4. Authenticate in the workflow:
- name: sf JWT auth
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 perms
run: vulkro-sf org perms --target-org prod-ci

Restrict the Connected App to a single profile (the CI service user) and a single IP range (your GitHub Actions egress). 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

  • GitLab CI: the same shape for GitLab pipelines.
  • SARIF output: the field-level SARIF contract, for any downstream tool that consumes the JSON directly.
  • CI/CD integration: the provider-neutral guide that covers the exit-code contract and the strict-then-loosen sequence.
  • The sf CLI handoff: how the live-org subcommands authenticate and what they read, end-to-end.