Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

SCA: analysing dependency vulnerabilities

By updated

Improve this page

SCA (Software Composition Analysis) inventories the third-party libraries your application ships, then checks them against published vulnerabilities. It covers the code you did not write, often the largest part of what you deliver. This lesson is for a developer or a DevOps engineer who wants to know what an SCA scanner reads, how to read its report, and how to place it in a pipeline without turning it into a permanent alarm. The examples were captured with Trivy 0.74.0 and Grype 0.119.0 on a real npm project.

What you will learn

  • Understand what an SCA scanner reads and which databases it checks the inventory against
  • Distinguish direct and transitive dependencies from a lockfile
  • Interpret a real report and draw the right fix from it
  • Block a build on serious vulnerabilities, and only those
  • Frame an exception with a justification and an expiry date
  • Link this control to requirement SOCLE-APP-AST-3 (in French) of the SOCLE framework

What is SCA?

SCA answers a simple question: "which external libraries does my application ship, and which of them carry a known flaw?". It does not read your code the way SAST does: it reads the list of components, with their exact versions, and compares it with vulnerability databases. The Equifax breach in 2017 is the textbook case: a known vulnerable version of Apache Struts, a dependency that had not been updated, which an SCA scanner would have flagged.

AspectSASTSCA
TargetCode written by the teamCode of the dependencies
MethodPatterns and data flows in the codeVersions checked against vulnerability databases
FindsFlaws introduced by your developersFlaws inherited from libraries
ExampleSQL injection in your routelodash 4.17.20, CVE-2021-23337

Why has SCA become essential?

Two trends combine. An application pulls in many more components than it declares: measured on the test project, 2 direct dependencies (express and lodash) produce a lockfile of 51 packages. And vulnerabilities are published continuously: a component that was healthy on delivery day can carry a CVE the following month, without a single line of your code having changed. Without an automated inventory, nobody can keep up with this pace by hand.

A CVE (Common Vulnerabilities and Exposures) is the public identifier of a flaw, and its CVSS score estimates its severity. The CVE, CVSS and EPSS lesson explains how to read them and prioritise them.

How does an SCA scanner work?

An SCA scanner does not download the code of your dependencies to analyse it: it works on their inventory. That is what makes it fast, and also what makes it dependent on the quality of that inventory, hence the importance of the lockfile.

  1. Inventory extraction: the scanner reads the dependency files, preferably the lockfiles, which pin the exact versions: package-lock.json, poetry.lock, go.sum, Cargo.lock. A plain requirements.txt without pinned versions does not say what will actually be installed.

  2. Dependency tree: the lockfile also contains the transitive dependencies, those of your dependencies, with their versions.

  3. Comparison with the databases: each (package, version) pair is checked against the public databases, such as NIST's NVD, the GitHub Advisory Database or OSV, which aggregates advisories from many ecosystems.

  4. Report: for each match, the identifier (CVE or GHSA), the severity, the installed version and the fixed version when it exists.

A real SCA report, read line by line

The test project declares two pinned dependencies in its package.json, then npm install --package-lock-only produces the complete lockfile. Both scanners read this lockfile.

package.json
{
"name": "demo-sca",
"version": "1.0.0",
"private": true,
"dependencies": {
"express": "4.17.1",
"lodash": "4.17.20"
}
}
Terminal window
IMG="aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969"
docker run --rm -v "$PWD:/src" -w /src "$IMG" fs --scanners vuln .

Trivy reports 19 vulnerabilities: 7 HIGH, 6 MEDIUM, 6 LOW, no CRITICAL. Grype, on the same lockfile, also finds 19, with the same breakdown. Here are the lodash rows, taken from Trivy's table:

Total: 19 (UNKNOWN: 0, LOW: 6, MEDIUM: 6, HIGH: 7, CRITICAL: 0)
│ lodash │ CVE-2021-23337 │ HIGH │ 4.17.20 │ 4.17.21 │ nodejs-lodash: command injection via template │
│ │ CVE-2026-4800 │ │ │ 4.18.0 │ lodash: lodash: Arbitrary code execution via untrusted input │
│ │ CVE-2020-28500 │ MEDIUM │ │ 4.17.21 │ nodejs-lodash: ReDoS via the toNumber, trim and trimEnd │
│ │ CVE-2025-13465 │ │ │ 4.17.23 │ lodash: prototype pollution in _.unset and _.omit functions │

Three lessons can be read in these four rows. First, the exact nature of the flaw matters: CVE-2021-23337 is a command injection through _.template, not a prototype pollution, and exploiting it assumes that your code passes external input to that function. Second, a single library often carries several vulnerabilities fixed in different versions: moving to 4.17.21 is not enough, the right-hand column leads to 4.18.0. Finally, the table merges identical cells: an empty severity cell repeats the one from the row above, so CVE-2026-4800 is HIGH, not severity-less.

Direct or transitive dependencies?

A direct dependency is declared in your package.json. A transitive dependency is pulled in by one of them. On the test project, 12 of the 19 vulnerabilities are in transitive dependencies, such as qs, send or path-to-regexp, which nobody chose. The lockfile says who pulls them in: qs 6.7.0 is required by both express and body-parser.

demo-sca
├── express 4.17.1 (direct)
│ ├── body-parser (transitive)
│ │ └── qs 6.7.0 (transitive)
│ ├── qs 6.7.0 (transitive)
│ └── send (transitive)
└── lodash 4.17.20 (direct)

The practical consequence: you almost never fix a transitive dependency by adding it by hand. You upgrade the direct dependency that pulls it in, here express, or you force the version with the package manager's mechanism (overrides for npm), and you document it.

Integrating SCA into the pipeline

The SCA scan belongs at three moments: on the workstation before pushing, on every merge request to block, and in continuous monitoring for what has already shipped. The trap is the same as with SAST: without a dedicated option, both scanners exit with code 0 whatever they find. Measured: Trivy without --exit-code exits with 0 despite 19 vulnerabilities, and so does Grype without --fail-on.

Terminal window
# On the workstation
trivy fs --scanners vuln .
grype dir:.

The job uses Trivy's official image, pinned by digest, rather than a third-party action: less code to trust in the pipeline. permissions: {} removes all default permissions, contents: read is enough to clone.

.github/workflows/sca.yml
name: SCA
on:
pull_request:
permissions: {}
jobs:
trivy:
runs-on: ubuntu-24.04
permissions:
contents: read
container:
image: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Trivy, fail on HIGH and CRITICAL
run: trivy fs --scanners vuln --severity CRITICAL,HIGH --exit-code 1 .

The entry point of Trivy's image is the trivy binary itself: on GitLab, entrypoint: [""] neutralises it so that the job's script runs in a shell. On GitHub Actions, a container: job runs its steps in a shell of the container, without this setting.

Container images

SCA also applies to images: they ship the application's dependencies and the packages of the base system. The command changes target, trivy image or grype followed by the image name, and the scanned image is designated by digest, otherwise the report does not say which image it read. Image scanning is detailed in the Trivy lesson (in French) of the containers module.

Continuous monitoring

A CI scan only sees what goes through the pipeline. An image deployed six months ago does not go through CI again, even though new CVEs may concern it. The answer is a kept inventory, the SBOM, which a tool such as Dependency-Track (in French) re-examines every time a vulnerability is published. The SBOM has its own lesson in the supply chain module.

Blocking without paralysing

Blocking on every vulnerability paralyses teams; blocking on none is the same as not scanning. A reasonable policy blocks the CRITICAL and HIGH severities, opens a ticket for MEDIUM, and reviews LOW. Trivy's --ignore-unfixed option sets aside vulnerabilities without a fixed version, so as not to block on what cannot be repaired; on the test project, it removes nothing, because all 19 vulnerabilities have a published fix.

SeverityActionIndicative deadline
CRITICALBlock the buildImmediate fix
HIGHBlock the buildFix before the merge
MEDIUMTicket with a deadlineNext sprint
LOWPeriodic reviewDepending on context

An exception must expire

Some vulnerabilities do not apply to your usage: the vulnerable function is never called, or not with external input. The exception is then legitimate, provided it is justified and dated. Trivy supports this natively, with two files whose behaviour differs.

.trivyignore.yaml
vulnerabilities:
- id: CVE-2021-23337
statement: "_.template is never called with external input; reviewed on 2026-12-01"
expired_at: 2027-01-01
Terminal window
trivy fs --scanners vuln --ignorefile .trivyignore.yaml .

Measured: with this exception, the report goes from 19 to 18 vulnerabilities; with an expired_at date in the past, the CVE reappears on its own. A trap to know: the YAML format is only read with --ignorefile, whereas the old text format, .trivyignore, is read by default. A .trivyignore.yaml dropped in without the option has no effect, and the team wrongly believes its exception is active.

Updating, first and foremost

The best exception is the one you do not need to write. Tools such as Dependabot or Renovate open update merge requests, one per dependency or per group, which the CI validates before the merge. Do not neglect development dependencies: a compromised test or build tool runs in the pipeline, with its secrets.

Choosing an SCA tool

Trivy and Grype, the two scanners measured here, are free and run locally; they cover most of a team's needs. The other tools answer different needs: a platform for tracking over time, or a commercial service with automatic fixes.

ToolTypeStrengthUse case
TrivyCLI, freeOne binary for dependencies, images and IaCUniversal CI scan
GrypeCLI, freePaired with Syft for SBOMsImage and SBOM scanning
OSV-ScannerCLI, freeOSV database, very broad coverageMulti-ecosystem projects
Dependency-TrackPlatform, freeContinuous tracking from SBOMsGovernance of a fleet
SnykSaaS, commercialIntegrations, suggested fixesTeams with a budget

The limits of SCA

SCA only sees what is published: a flaw not yet disclosed, a 0-day, escapes it by design. It reports a vulnerability present in a version, not a vulnerability reachable by your code: a CVE in a function that is never called stays in the report. It also depends on the inventory: without a lockfile, or with a binary copied by hand into the repository, it misses components. It therefore complements SAST and DAST, it does not replace them.

Key points

  1. SCA checks the inventory of your dependencies against published vulnerabilities; it does not read your code.
  2. The lockfile is the source of truth: 2 direct dependencies pulled in 51 packages.
  3. Most of the test project's vulnerabilities, 12 out of 19, were in transitive dependencies.
  4. You fix a transitive dependency by upgrading the direct dependency that pulls it in.
  5. Without --exit-code (Trivy) or --fail-on (Grype), the scan blocks nothing.
  6. An exception is justified and expires: .trivyignore.yaml requires --ignorefile.
  7. SCA sees neither 0-days nor the real reachability of a flaw.
  8. Evidence expected by SOCLE-APP-AST-3: SCA findings tracked in the vulnerability management process.

Knowledge check

Check that you have the essentials of this lesson. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn