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. 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.
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 today
├── 19 High (CVSS 7.0-8.9) ├── 2 EPSS > 0.3 — patch this sprint
├── 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.
#!/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, closing that gap to hours instead of a full day means layering continuous monitoring on top of CI gates — a service that watches your registered dependency manifests and alerts in near real time when a new CVE lands, independent of your GitLab or Jenkins schedule. 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.


