Dependency scanning (SCA)
The CVEs you inherited from your libraries.
Your Node service pins [email protected] - a version you locked eighteen months ago and never touched, because it works. It does work, and it is also vulnerable to CVE-2021-23337, a command-injection flaw in lodash's template() helper that was fixed in 4.17.21. Nobody on your team wrote that bug. You inherited it the instant you ran npm install. Now multiply it by the hundreds of packages a typical service drags in transitively, and the uncomfortable truth is that most of your attack surface is code you have never read. Software Composition Analysis (SCA) - GitLab calls it Dependency Scanning - is the pipeline stage that finds those inherited CVEs before an attacker who reads vulnerability feeds for a living does.
What dependency scanning actually reads
First, two terms. A direct dependency is one you named yourself in package.json (or requirements.txt, go.mod, pom.xml); a transitive dependency is one your dependencies pulled in - the package three levels down you have never heard of. A lockfile - package-lock.json, yarn.lock, Gemfile.lock, poetry.lock, go.sum - records the exact resolved version of every package in that tree. GitLab's analyzer, gemnasium, parses the lockfile, reconstructs the full graph, and matches each pinned version against the GitLab Advisory Database: a curated feed of known vulnerabilities keyed to a package name and an affected version range. It runs none of your code and never contacts your package registry - it is a fast, deterministic lookup against a file already in your repo. Every match is written to a report artifact that GitLab ingests to populate the merge request security widget. (Gemnasium was deprecated in GitLab 17.9 - proposed for removal in 20.0 - in favour of the newer SBOM-based analyzer added by Jobs/Dependency-Scanning.v2.gitlab-ci.yml, whose job is named dependency-scanning rather than gemnasium-dependency_scanning; the gl-dependency-scanning-report.json format and the report-then-gate workflow below are unchanged.)
include:- template: Jobs/Dependency-Scanning.gitlab-ci.yml# The template already defines the gemnasium-dependency_scanning job.# Re-declaring it by name MERGES into the template job - here we# scope it with rules and drop dev-only packages that never ship.gemnasium-dependency_scanning:variables:DS_INCLUDE_DEV_DEPENDENCIES: "false"rules:- if: $CI_PIPELINE_SOURCE == "merge_request_event"- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
$ /analyzer run[INFO] [gemnasium] Detecting project dependencies from lockfiles[INFO] [gemnasium] Found package-lock.json (npm)[INFO] [gemnasium] Analyzing 1 lockfile(s), 214 dependencies[WARN] [gemnasium] Found 1 vulnerabilityUploading artifacts for successful jobgl-dependency-scanning-report.json: found 1 matching filesJob succeeded
Notice the last line: the job succeeded. This trips up almost everyone. The gemnasium analyzer exits 0 whether it finds zero vulnerabilities or fifty - including the template does not, by itself, block anything. Findings flow into the report artifact and the MR security widget for humans to see; enforcement is a separate decision. That split is deliberate. A large dependency tree always carries some open CVEs, many with no fix available, and failing every pipeline on them just teaches developers to delete the scanner. So GitLab separates reporting (always on, everything visible) from gating (a policy you opt into, scoped to the actionable subset). We will wire the gate after we read the finding.
{"version": "15.0.6","scan": {"scanner": { "id": "gemnasium", "name": "Gemnasium", "version": "5.4.0","vendor": { "name": "GitLab" } },"type": "dependency_scanning","status": "success","end_time": "2026-07-14T09:13:05"},"vulnerabilities": [{"name": "Command Injection in lodash","severity": "High","location": {"file": "package-lock.json","dependency": { "package": { "name": "lodash" }, "version": "4.17.20" }},"identifiers": [{ "type": "cve", "name": "CVE-2021-23337","url": "https://nvd.nist.gov/vuln/detail/CVE-2021-23337" },{ "type": "ghsa", "name": "GHSA-35jh-r3h4-6jhm" }],"solution": "Upgrade lodash to 4.17.21 or later."}]}
Everything you need to act is right there: the vulnerable package and exact version (lodash 4.17.20), where it entered the tree (package-lock.json), the severity, the CVE and GitHub advisory IDs for triage, and - the part that makes SCA actionable - a solution with the fixed version. gemnasium only fills in solution when a patched release exists, which is precisely the finding a developer can close today rather than merely log.
Fixing it is a version bump
The remediation for a known CVE is almost always the same shape: move to a version outside the vulnerable range. You bump the dependency, regenerate the lockfile so the whole transitive graph re-resolves, and push. Do it deliberately - read the changelog for breaking changes and run your tests - but the mechanical step is small. Renovate and Dependabot automate exactly this, opening one merge request per upgrade so the fix arrives as a reviewable, testable change instead of a manual chore. Here it is by hand:
$ npm install [email protected] # rewrites package.json + package-lock.jsonadded 0 packages, changed 1 package, audited 214 packages in 2s$ git commit -am "fix(deps): bump lodash 4.17.20 -> 4.17.21 (CVE-2021-23337)"$ git push -o merge_request.create # push option opens the MR in one step# ...the MR pipeline re-runs gemnasium against the new lockfile:[INFO] [gemnasium] Analyzing 1 lockfile(s), 214 dependencies[INFO] [gemnasium] Found 0 vulnerabilitiesJob succeeded
Trust the widget, but verify from the outside - especially when you are building an automated gate. GitLab serves every job artifact over its REST API, so you can pull the report from any pipeline and count exactly what matters:
# Pull the DS report from the latest main pipeline, count High/Critical findings$ curl -sf --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \"https://gitlab.example.com/api/v4/projects/1024/jobs/artifacts/main/raw/gl-dependency-scanning-report.json?job=gemnasium-dependency_scanning" \| jq '[.vulnerabilities[] | select(.severity=="Critical" or .severity=="High")] | length'0
That one number is your evidence. For native enforcement, GitLab's Merge Request Approval Policies read the same report and require approval - or block the merge - when a pipeline introduces a new High or Critical dependency-scanning finding, comparing the MR against its target branch so a pre-existing CVE on main does not wall off unrelated work. Report everything; gate only on new, fixable, High-and-above results. It is the same two-tier pattern every scanner in this course uses.
At scale the friction is triage, not detection. A monorepo with a dozen lockfiles produces a long report, and not every High is reachable - the vulnerable function may sit in a code path you never call. gemnasium matches by version, not by reachability, so some findings are present but not exploitable in your usage; dismiss those in the vulnerability report with a written justification (it stays audited and will not re-alert) rather than adding blanket severity overrides that also hide real issues. Point DS_MAX_DEPTH at nested projects so every lockfile is discovered, and treat forked-MR pipelines carefully: a contributor's fork can rewrite the lockfile, so scan the merged result and never on a runner that holds production credentials.
SCA is not container scanning
It is tempting to assume dependency scanning and the container scanning of the next lesson are the same check run twice. They read different inputs. SCA reads your source repository - the manifests and lockfiles you committed, which describe the full declared dependency graph (dev packages included) before any image is even built. Container scanning reads the built image's filesystem: the operating-system packages from your base image (glibc, openssl, the distro's curl) plus whatever ended up in the final layers. The overlap is your application's libraries; the gaps are the whole point. SCA catches a vulnerable transitive package for projects that ship no container at all, and sees dev-time dependencies an image never contains. Container scanning catches base-image OS CVEs that appear in no lockfile you wrote. They are complementary, not redundant - which is exactly why the next lesson adds the second scan, on the image you are about to push.
Try this
Run npm install [email protected] # rewrites package.json + package-lock.json on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: no lockfile, no findings - a silent zero. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.