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.
| Aspect | SAST | SCA |
|---|---|---|
| Target | Code written by the team | Code of the dependencies |
| Method | Patterns and data flows in the code | Versions checked against vulnerability databases |
| Finds | Flaws introduced by your developers | Flaws inherited from libraries |
| Example | SQL injection in your route | lodash 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.
-
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 plainrequirements.txtwithout pinned versions does not say what will actually be installed. -
Dependency tree: the lockfile also contains the transitive dependencies, those of your dependencies, with their versions.
-
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.
-
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.
{ "name": "demo-sca", "version": "1.0.0", "private": true, "dependencies": { "express": "4.17.1", "lodash": "4.17.20" }}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.
# On the workstationtrivy 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.
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 .Same logic on GitLab: pinned image, filtered severities, explicit
exit code. Measured: with --severity CRITICAL,HIGH --exit-code 1, the test
project makes the job fail with code 1.
sca: stage: test image: name: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 entrypoint: [""] script: - trivy fs --scanners vuln --severity CRITICAL,HIGH --exit-code 1 . rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" allow_failure: falseGrype does the same job with the --fail-on option, which sets the
severity from which the scan fails. Measured: --fail-on high on the test
project exits with code 2, a value that the CI interprets as a failure.
grype dir:. --fail-on highThe 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.
| Severity | Action | Indicative deadline |
|---|---|---|
| CRITICAL | Block the build | Immediate fix |
| HIGH | Block the build | Fix before the merge |
| MEDIUM | Ticket with a deadline | Next sprint |
| LOW | Periodic review | Depending 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.
vulnerabilities: - id: CVE-2021-23337 statement: "_.template is never called with external input; reviewed on 2026-12-01" expired_at: 2027-01-01trivy 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.
| Tool | Type | Strength | Use case |
|---|---|---|---|
| Trivy | CLI, free | One binary for dependencies, images and IaC | Universal CI scan |
| Grype | CLI, free | Paired with Syft for SBOMs | Image and SBOM scanning |
| OSV-Scanner | CLI, free | OSV database, very broad coverage | Multi-ecosystem projects |
| Dependency-Track | Platform, free | Continuous tracking from SBOMs | Governance of a fleet |
| Snyk | SaaS, commercial | Integrations, suggested fixes | Teams 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
- SCA checks the inventory of your dependencies against published vulnerabilities; it does not read your code.
- The lockfile is the source of truth: 2 direct dependencies pulled in 51 packages.
- Most of the test project's vulnerabilities, 12 out of 19, were in transitive dependencies.
- You fix a transitive dependency by upgrading the direct dependency that pulls it in.
- Without
--exit-code(Trivy) or--fail-on(Grype), the scan blocks nothing. - An exception is justified and expires:
.trivyignore.yamlrequires--ignorefile. - SCA sees neither 0-days nor the real reachability of a flaw.
- 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Understanding CVE, CVSS and EPSS to prioritise
A new full quiz with random questions
Work again only on the questions you missed