Skip to Content
IntegrationsGitLab CI

GitLab CI

Drop-in stage that runs on every pipeline + MR, caches the SQLite store, exports a SARIF report, and fails the pipeline on blocking CVEs.

# .gitlab-ci.yml (excerpt) stages: - security packguard: stage: security image: ghcr.io/tmauc/packguard:latest # Same container serves the scanner + the UI; we only need the CLI. cache: # The lockfile-hash key means: "re-scan only when deps actually # change". $CI_COMMIT_REF_SLUG keeps MR branches isolated. key: files: - package-lock.json - pnpm-lock.yaml - yarn.lock - poetry.lock - uv.lock - requirements.txt paths: - .packguard-cache/ fallback_keys: - packguard-${CI_COMMIT_REF_SLUG} - packguard-main variables: # Pack everything into the project dir so GitLab's cache can carry # it across runs. packguard honours $HOME/.packguard/store.db. HOME: "$CI_PROJECT_DIR/.packguard-cache" before_script: - mkdir -p "$HOME/.packguard" script: - packguard scan . - packguard sync # refresh OSV + GHSA + malware intel - packguard report . --format sarif --fail-on-violation > packguard.sarif artifacts: when: always expire_in: 30 days paths: - packguard.sarif reports: # GitLab's SAST panel picks this up automatically. sast: packguard.sarif # MR-only gating. Remove this line to run on every commit. rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

What each piece does

BlockWhy
image: ghcr.io/tmauc/packguard:latestNo runtime install; the image is ~46 MB. Pin to :vX.Y.Z for reproducibility once you’ve picked a version.
cache.key.filesRe-uses the SQLite store when lockfiles haven’t changed. First MR run builds the cache (~30-90s depending on repo), subsequent runs hit it in ~2s.
HOME: "$CI_PROJECT_DIR/.packguard-cache"GitLab can’t cache paths outside the project dir. Redirecting $HOME keeps the cache payload inside what GitLab copies around.
packguard syncSeparate from scan because OSV/GHSA refresh is time-based, not lockfile-based. If you want tighter control, run it in a daily scheduled pipeline and skip it in MR pipelines.
report --fail-on-violationExit code 2 when the policy finds a blocking CVE/malware. GitLab shows a red pipeline.
reports.sastSurfaces findings in GitLab’s Security tab + MR widget, alongside whatever SAST the repo already runs.

Faster MR pipelines

If you want sub-30s MR feedback, split into two jobs:

  • packguard:scan runs on every MR, uses --offline when the cache is hot (no registry calls, just re-evaluate the policy).
  • packguard:sync runs on the default branch nightly, refreshing intel + committing nothing — the cache key propagates forward.
packguard:sync-nightly: stage: security image: ghcr.io/tmauc/packguard:latest rules: - if: $CI_PIPELINE_SOURCE == "schedule" script: - packguard sync cache: key: packguard-intel-nightly paths: - .packguard-cache/ policy: push

Blocking on a specific CVE

Swap --fail-on for --fail-on-cve:

script: - packguard audit . --fail-on-cve CVE-2026-4800 --fail-on-malware

Useful when a vendor advisory just dropped and you want an explicit hard gate for that exact CVE across every repo, independent of the policy’s severity rules.

Variant — gate on prioritized actions (v0.4.0+)

Swap report for actions when you’d rather have the SAST panel list the exact fix commands instead of the policy rule that tripped:

script: - packguard scan . - packguard sync - packguard actions --format sarif > packguard.sarif - packguard actions --fail-on-severity high

Same SARIF panel, richer message.markdown per finding (one result per action, ruleId: packguard/<kind>, level Malware/Critical/High → error). Use --fail-on-severity malware for a paranoia gate that tolerates CVE noise.

Last updated on