GitLab's built-in Dependency Scanning analyzer is gated behind the Ultimate tier — Free and Premium plans don't get it. OSV-Scanner is a free, open-source CLI that drops into a .gitlab-ci.yml job or Jenkinsfile stage in minutes. This guide gives copy-paste configs for both: a merge-request gate that blocks dangerous dependencies, and a scheduled scan that catches CVEs disclosed after merge.
If your team is on GitLab Free or Premium, the built-in "Dependency Scanning" template in GitLab's Secure category is not available to you — it requires an Ultimate subscription. Jenkins has no built-in dependency scanning at all; you're expected to wire up a plugin or a CLI tool yourself.
Neither of those facts means you're stuck with unscanned dependencies. OSV-Scanner — the same open-source database GeekWala uses — runs as a standalone binary or container with no licensing tier attached, covering npm, PyPI, Maven, Go, Cargo, Composer, RubyGems, and NuGet lockfiles. You can build the identical two-mode pattern from our GitHub Actions guide: a merge-request gate that fails fast on dangerous new dependencies, and a scheduled pipeline that catches vulnerabilities disclosed after your last deploy. If you're actually on GitHub, skip the DIY setup and use our GitHub Actions integration directly — for other CI platforms not covered here, our docs list every supported provider.
What We'll Cover
- Why CI-gate scanning is platform-agnostic
- GitLab CI pipeline recipe
- Jenkins pipeline recipe
- Failing builds on EPSS/KEV thresholds, not raw CVE counts
- When CI gates aren't enough: scheduled scans
Why CI-Gate Scanning Is Platform-Agnostic
The mechanics of a dependency scanning gate don't depend on which CI platform runs it. Every implementation needs the same three pieces:
- A trigger that fires when a lockfile changes (merge request) or on a schedule (nightly cron).
- A scanner that resolves the full dependency tree — direct and transitive — against a vulnerability database.
- A decision rule that turns raw findings into a pass/fail signal, ideally weighted by exploitation likelihood rather than raw CVSS severity.
GitHub Actions, GitLab CI, and Jenkins all support scheduled triggers, path-filtered jobs, and shell execution. The only real differences are syntax: YAML rules:/only: blocks in GitLab CI versus YAML on: triggers in GitHub Actions versus Groovy when {} blocks in a Jenkinsfile. Once you pick a scanner that ships a portable CLI or container image — OSV-Scanner qualifies — the same architecture ports cleanly across all three.
This matters because GitLab's Ultimate-gated feature and Jenkins' lack of a native scanner both create the same false impression: that dependency scanning is either expensive or hard to set up outside GitHub. It's neither.
| Platform | Native dependency scanning | Cost to add OSV-Scanner | Trigger syntax | Path/lockfile filter |
|---|---|---|---|---|
| GitHub Actions | No (community actions only) | Free — google/osv-scanner-action | on: pull_request / on: schedule | paths: |
| GitLab CI | Ultimate tier only | Free — ghcr.io/google/osv-scanner container job | rules: - if: | changes: |
| Jenkins | None built in | Free — Docker step or static binary | when { changeRequest() } / triggers { cron() } | changeset |
Same scanner, same EPSS/KEV threshold logic, three different YAML/Groovy dialects. Nothing here requires an Ultimate license or a plugin marketplace subscription.
GitLab CI Pipeline Recipe
GitLab CI reads its configuration from .gitlab-ci.yml at the repo root. Here's a merge-request gate using the official OSV-Scanner container image:
# .gitlab-ci.yml
stages:
- test
- security
dependency-scan-mr:
stage: security
image: ghcr.io/google/osv-scanner:latest
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/package-lock.json"
- "**/yarn.lock"
- "**/pnpm-lock.yaml"
- "**/requirements.txt"
- "**/poetry.lock"
- "**/Pipfile.lock"
- "**/pom.xml"
- "**/go.sum"
- "**/Cargo.lock"
- "**/composer.lock"
- "**/Gemfile.lock"
script:
- osv-scanner --recursive --format=json --output=scan-results.json . || true
- cat scan-results.json
artifacts:
when: always
paths:
- scan-results.json
expire_in: 30 days
allow_failure: false
Three details worth calling out:
changes: replicates the GitHub Actions paths: filter. The job only runs when a matching lockfile is touched in the merge request — documentation and CSS-only MRs skip the scan entirely, keeping pipeline minutes down.
|| true after the scan command prevents OSV-Scanner's own non-zero exit code (it exits 1 when it finds anything) from failing the job before you've had a chance to apply your own threshold logic. You gate on the parsed results, not on the scanner's raw exit code.
allow_failure: false makes this a real merge-request blocker once your threshold script (below) sets a non-zero exit. Flip it to true while you're first rolling this out and want visibility without blocking anyone.
For a scheduled scan against your default branch, add a pipeline schedule in CI/CD → Pipeline schedules targeting a dedicated job:
dependency-scan-nightly:
stage: security
image: ghcr.io/google/osv-scanner:latest
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
script:
- osv-scanner --recursive --format=json --output=scan-results.json . || true
artifacts:
when: always
paths:
- scan-results.json
expire_in: 90 days
GitLab pipeline schedules are configured in the UI (or via the Pipeline Schedules API) rather than in the YAML cron: syntax GitHub Actions uses — set the cadence there, then point it at a branch and let this job's rules: block pick it up.
Jenkins Pipeline Recipe
Jenkins has no equivalent to GitLab's changes: path filter or GitHub Actions' paths: trigger built into the pipeline syntax itself, so the lockfile-changed check happens inside the stage using changeset conditions. Here's a declarative Jenkinsfile with both a PR-triggered gate and a nightly cron stage:
// Jenkinsfile
pipeline {
agent any
triggers {
cron('H 2 * * *') // nightly, Jenkins staggers the exact minute
}
stages {
stage('Dependency Scan (PR gate)') {
when {
allOf {
changeRequest()
anyOf {
changeset "**/package-lock.json"
changeset "**/requirements.txt"
changeset "**/go.sum"
changeset "**/Cargo.lock"
changeset "**/composer.lock"
changeset "**/Gemfile.lock"
changeset "**/pom.xml"
}
}
}
steps {
sh '''
docker run --rm -v "$PWD":/src -w /src \
ghcr.io/google/osv-scanner:latest \
--recursive --format=json --output=/src/scan-results.json . || true
'''
archiveArtifacts artifacts: 'scan-results.json', allowEmptyArchive: true
script {
def exitCode = sh(script: './scripts/check-dependency-thresholds.sh scan-results.json', returnStatus: true)
if (exitCode != 0) {
error('Dependency scan found findings above the configured EPSS/KEV threshold. See scan-results.json.')
}
}
}
}
stage('Dependency Scan (nightly)') {
when {
triggeredBy 'TimerTrigger'
}
steps {
sh '''
docker run --rm -v "$PWD":/src -w /src \
ghcr.io/google/osv-scanner:latest \
--recursive --format=json --output=/src/scan-results.json . || true
'''
archiveArtifacts artifacts: 'scan-results.json', allowEmptyArchive: true
}
}
}
post {
always {
sh 'echo "Dependency scan artifacts archived — review scan-results.json in Jenkins build artifacts."'
}
}
}
Two Jenkins-specific notes:
changeRequest() + changeset is the Jenkins equivalent of GitLab's changes: or GitHub's paths: — it restricts the PR-gate stage to builds triggered by a pull/merge request that actually touched a lockfile. Both conditions are part of Jenkins' standard Declarative Pipeline syntax; verify plugin requirements against your specific Jenkins install's Pipeline Syntax reference since plugin bundling varies across LTS versions.
triggeredBy 'TimerTrigger' scopes the nightly stage to cron-triggered builds only, so a developer manually re-running the pipeline doesn't accidentally re-trigger the "nightly" stage on every retry.
If your Jenkins agents don't have Docker available, install the osv-scanner binary directly on the agent image instead — the OSV-Scanner releases page publishes static binaries for Linux, macOS, and Windows, and the sh steps above become osv-scanner --recursive ... without the docker run wrapper.
Failing Builds on EPSS/KEV Thresholds, Not Raw CVE Counts
Both recipes above write scan output to scan-results.json, but neither one fails the build directly on OSV-Scanner's raw output. That's deliberate. OSV-Scanner reports every match against the OSV database — including CVSS 9.8 findings that have never been exploited in the wild and CVSS 4.0 findings that are actively being weaponized right now.
Raw OSV-Scanner output (unfiltered): After EPSS/KEV filtering:
────────────────────────────────── ──────────────────────────────
47 findings, sorted by CVSS 3 findings, sorted by exploitation risk
├── 12 Critical (CVSS 9.0+) ├── 1 on CISA KEV — patch in 24h
├── 19 High (CVSS 7.0-8.9) ├── 2 EPSS > 0.3 — patch in 1-2 weeks
├── 11 Moderate (CVSS 4.0-6.9) └── (44 findings logged, not blocking)
└── 5 Low (CVSS < 4.0)
A blocking gate that fires on "any CVSS High or above" will fail nearly every merge request in a typical dependency tree — 80% of the average project's vulnerabilities live in transitive dependencies you didn't choose directly, and most of them are theoretical. Teams that ship a gate this noisy end up disabling it within a month.
The fix is the same 3-Signal Triage Method used in the GitHub Actions gate: enrich raw findings with EPSS exploitation probability and CISA KEV status, then gate on that instead of CVSS alone. (The EPSS thresholds here gate the pipeline, so they are deliberately stricter than remediation deadlines; the deadlines follow the default ladder in the guide.)
#!/usr/bin/env bash
# scripts/check-dependency-thresholds.sh
# Usage: ./check-dependency-thresholds.sh scan-results.json
set -euo pipefail
RESULTS_FILE="${1:?Usage: $0 <osv-scanner-json-output>}"
# Extract vulnerability IDs from OSV-Scanner's JSON output, then look up
# EPSS score and KEV status for each one (GeekWala's API, FIRST.org's EPSS
# API, or CISA's KEV JSON feed all work here — swap in your enrichment
# source of choice).
BLOCKING_COUNT=$(jq '[.results[].packages[]?.vulnerabilities[]? |
select(.id != null)] | length' "$RESULTS_FILE")
if [ "$BLOCKING_COUNT" -gt 0 ]; then
echo "Found $BLOCKING_COUNT vulnerabilities. Cross-reference against EPSS/KEV before failing the build."
# Replace this with a real EPSS/KEV lookup + threshold comparison.
# Exit 1 only for findings on CISA KEV or with EPSS above your threshold.
fi
exit 0
Start with a permissive threshold and tighten it over the same four-week rollout described in the GitHub Actions guide: KEV-only in week one, EPSS > 0.3 by week four. The goal is a gate that blocks rarely but means something every time it fires — regardless of whether it's running in GitHub, GitLab, or Jenkins.
When CI Gates Aren't Enough: Scheduled Scans
A merge-request gate only sees dependencies at the moment someone opens an MR or PR. It has no way to catch a CVE disclosed against a package that's already merged and sitting in your default branch — that's what the nightly job in both recipes above is for.
But even a nightly scan leaves a detection gap: the OSV database updates continuously throughout the day, so a critical CVE disclosed at 9 AM won't surface in your pipeline until the next scheduled run. For most teams, the practical complement to CI gates is scheduled monitoring from outside the pipeline — a service that re-scans your registered projects on its own cadence, independent of your GitLab or Jenkins schedule, and emails you what is new and what is resolved after each run. See automated dependency scanning for what that layer looks like in practice.
The practical takeaway for GitLab and Jenkins teams: you don't need the Ultimate tier or a Jenkins plugin marketplace subscription to get PR-gate-plus-nightly-scan coverage equivalent to GitHub's. OSV-Scanner plus a threshold script gets you there in both .gitlab-ci.yml and a Jenkinsfile with the same architecture, the same EPSS/KEV filtering logic, and the same 20-minute setup cost.
Does GitLab Free include any dependency scanning at all?
No. GitLab's native "Dependency Scanning" analyzer — the one integrated into the Security Dashboard and merge request widget — is part of the Ultimate tier only. GitLab Free and Premium do not include it. You can still run OSV-Scanner (or any other open-source SCA tool) as a plain CI job on any tier; you just won't get GitLab's built-in Security Dashboard visualization unless you're on Ultimate.
Can I get GitLab's Security Dashboard UI without the Ultimate tier?
Not the native one — the Security Dashboard widget that visualizes dependency findings inline on merge requests is an Ultimate-only feature regardless of which scanner produces the underlying data. You can approximate it with the artifacts: reports: mechanism on lower tiers for some report types, but full Dependency Scanning dashboard integration requires Ultimate. The OSV-Scanner job in this guide surfaces findings as a pipeline artifact and a failed job instead — less polished, but functionally equivalent for blocking bad merges.
Does Jenkins have an official dependency scanning plugin?
Not a single canonical one. The Jenkins plugin ecosystem includes options like the OWASP Dependency-Check plugin, but adoption and maintenance vary by plugin. Running a scanner CLI (OSV-Scanner, Trivy, or similar) directly inside a pipeline sh step — as shown above — avoids plugin-compatibility churn across Jenkins upgrades and works identically whether you're on Jenkins LTS or a bleeding-edge release.
Will scanning slow down my GitLab or Jenkins pipeline?
Scanning a typical lockfile with OSV-Scanner takes 15-30 seconds, similar to the GitHub Actions numbers. The changes:/changeset filters in both recipes above mean the scan job only runs when a lockfile actually changed, so most merge requests and PRs skip it entirely. The Docker image pull in the Jenkins recipe adds a few seconds on a cold agent; caching the image on your Jenkins agents (or switching to a static binary) eliminates that cost after the first run.
Can I reuse this pattern for other CI platforms besides GitLab and Jenkins?
Yes — that's the point of the architecture in this guide. Any CI platform that supports path-filtered triggers, scheduled jobs, and shell execution (CircleCI, Bitbucket Pipelines, Azure Pipelines, Buildkite) can run the same OSV-Scanner-plus-threshold-script pattern with platform-specific YAML or configuration syntax swapped in. The scanner and the threshold logic don't change — only the trigger syntax does.
See dependency findings ranked by what's actually exploitable — not just how bad they sound.
Scan your dependencies now → — GeekWala enriches every finding with CVSS severity, EPSS exploitation probability, and CISA KEV status, whether your pipeline lives in GitHub Actions, GitLab CI, or Jenkins. No account needed.


