Exam Blueprint · CDP · Ch. 4 of 9 · Software Composition Analysis

Software Composition Analysis in Depth

SAST, DAST & SCA introduced software composition analysis as one-third of a three-tool overview. This chapter isolates it the way this course's own CDP blueprint does — as its own examinable chapter, worth a page on its own because it fails in ways neither SAST nor DAST does. Here we go all the way down: how a dependency graph actually resolves, why the CVE that gets you is never the package you typed into package.json, how to read a CVSS vector instead of just its headline number, how EPSS supplies the likelihood half CVSS doesn't give you, and worked scan output from the three tools this chapter names by name — OWASP Dependency-Check, Safety, and npm audit.

☺ Explain it like I'm 10

Imagine your LEGO set didn't just come from one box — it came from five other kids who each borrowed pieces from five more kids. If one of those far-away pieces turns out to be a choking hazard on a safety recall list, it's still sitting in your bin, whether or not you remember borrowing it, or even know its name. Software composition analysis is the grown-up who dumps out the whole bin, traces every piece back to whichever kid it actually came from, and checks all of them against the recall list — not just the ones you personally picked out.

🐢Your host for this topic: Timmy the Turtle — the guardrail from SAST, DAST & SCA, back for a much closer look at the one-third of that trio that spends its whole life checking parts nobody on the team actually wrote.

Chapter 4 of 9: why SCA earns a page of its own

☺ Like you're 10: This isn't a repeat of the overview page — it's the same topic, but this time we open the hood and look at the actual engine parts instead of just naming them.

A quick honesty note before anything else: Practical DevSecOps doesn't publish a scored breakdown of its five live challenges, so treat the chapter numbering in this Exam Blueprint section as this course's own study framework rather than an official domain weighting — confirm the current exam structure on the vendor's own page. Within that framework, software composition analysis is chapter 4: distinct from SAST and secrets detection (chapter 5) and distinct from dynamic analysis (chapter 6), because the failure mode is genuinely different. A SAST finding points at a line you wrote. An SCA finding points at a library you imported once, two years ago, that itself imported four more libraries you've never heard of.

The scope of this chapter is narrow and deliberate: given a resolved dependency tree, (1) trace a vulnerability to the exact package and depth it lives at, (2) read the CVSS vector well enough to know what it's actually claiming, (3) read the EPSS score well enough to know how urgently to act on that claim, and (4) run the three named tools and interpret what they hand back. What this chapter does not re-cover: the SAST/DAST half of the trio (that's the overview page and chapter 5), and container/base-image composition, which belongs to container & supply-chain security and this blueprint's own infrastructure as code hardening chapter. Pair this page with the CDP study plan for where it sits in a full prep schedule.

How a dependency graph actually resolves

☺ Like you're 10: The list you wrote (package.json) is a wish list with a range of acceptable versions. The lockfile is what your computer actually decided to install after doing the math — and that decision is what an SCA scanner needs to see.

Every ecosystem draws the same distinction, under different file names. A manifestpackage.json, requirements.txt, pom.xml — declares your direct dependencies, usually as a version range, not an exact version: "axios": "^1.2.0" means "any 1.x release at or above 1.2.0." A lockfilepackage-lock.json, Pipfile.lock, the resolved tree Maven computes at build time — records the exact versions the resolver actually picked, direct and transitive. An SCA tool that only reads the manifest is scanning your wish list. An SCA tool worth trusting reads the lockfile, because the lockfile is the only artifact that reflects what's actually going to run.

The resolution algorithm itself differs by ecosystem, and the differences matter for triage, not just trivia:

Your app package-lock.json reporting-sdk needs ^1.14.0 http-client-wrapper needs ^1.15.0 follow-redirects@1.14.9 vulnerable · CVE-2022-0155 nested under reporting-sdk/ follow-redirects@1.15.4 patched nested under http-client-wrapper/ Same package, two resolved copies at two depths — "no known vulnerabilities" at the top level says nothing about the nested one.

That diagram is the whole reason this chapter is harder than it looks on paper. "We updated follow-redirects" can be true and false at the same time, depending on which of the two nested copies someone actually bumped.

Transitive vulnerabilities: the CVE three layers down

☺ Like you're 10: You didn't invite the vulnerable package to the party — your friend brought a plus-one, and that plus-one brought another plus-one. It's still in the room.

A direct dependency is one your manifest names explicitly. A transitive dependency is one that arrived because a direct dependency needed it, and that chain can run several levels deep before it reaches the vulnerable package. The canonical teaching example is Log4Shell (CVE-2021-44228, disclosed December 2021): the vulnerable component was log4j-core, a logging library — and logging libraries get pulled in as a transitive dependency of an enormous share of the Java ecosystem, because so many other libraries log through it without the application team ever writing import log4j themselves. Teams that ran a dependency inventory against their own pom.xml or package.json and found nothing named "log4j" still shipped it — because it wasn't a direct dependency, it was three or four hops down inside something else they trusted.

Two structural reasons this keeps happening:

◆ Key idea

A dependency inventory built from reading package.json or pom.xml by eye only ever shows direct dependencies — commonly a small fraction of what's actually resolved and shipped. SCA tooling exists specifically because that gap is where the exploitable code hides.

Reading a CVSS vector like the exam expects you to

☺ Like you're 10: The single number ("9.8!") is the headline. The string of letters next to it is the actual story — how easy the attack is, whether you need a password, and what it breaks if it works.

The Common Vulnerability Scoring System (CVSS) is what every one of these tools reports a severity from. Treating the base score as a bare number — "it's a 9.8, that's bad" — is exactly the surface-level reading the exam expects you to go past. The base score is computed from named metrics, and every SCA report exposes the full vector string alongside the number. Take Log4Shell's real NVD vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, scoring 10.0 (Critical).

MetricLog4Shell's valueWhat it's actually claiming
Attack Vector (AV)Network (N)Reachable over a network — no local access or physical presence needed.
Attack Complexity (AC)Low (L)No special conditions or race window the attacker has to wait for.
Privileges Required (PR)None (N)The attacker needs no account or credential on the target at all.
User Interaction (UI)None (N)No victim has to click, open, or approve anything.
Scope (S)Changed (C)A successful exploit can affect resources beyond the vulnerable component itself — here, arbitrary code execution on the host.
Confidentiality / Integrity / Availability (C/I/A)High / High / HighTotal loss of confidentiality, integrity, and availability if exploited.

Compare that to Text4Shell (CVE-2022-42889, in Apache Commons Text), vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, scoring 9.8. Nearly identical severity — the one difference is Scope: Unchanged (U) instead of Changed. Two "Critical" findings, same number of digits before the decimal point, describing two meaningfully different blast radii. That's the level of reading the exam — and a real triage call — actually requires.

One more thing worth knowing going in: CVSS v4.0, published by FIRST in late 2023, restructures several of these metrics (it adds an Attack Requirements metric, splits impact into Vulnerable System and Subsequent System, and drops Scope in favor of that split) — but tool and database adoption of v4.0 scoring is still uneven at the time of writing, so expect to see v3.1 vectors as the default in most SCA output for a while yet. Confirm which version a given report is using before you compare two scores.

⚠ Watch out

CVSS measures severity — how bad it is if exploited — not likelihood. A 9.8 sitting in code your application never calls is a lower real-world priority than a 6.1 sitting on an internet-facing endpoint with active exploitation in the wild. Treating the base score alone as a priority ranking, without asking whether the vulnerable path is reachable, is the single most common SCA triage mistake this exam domain is built to catch you making.

EPSS: the likelihood half CVSS doesn't give you

☺ Like you're 10: CVSS tells you how bad a fire would be. EPSS tells you how likely that particular building is to catch fire this month. You want both numbers before deciding which alarm to run to first.

The Exploit Prediction Scoring System (EPSS) is maintained by FIRST (the Forum of Incident Response and Security Teams — the same body that stewards CVSS itself) and answers a different question entirely: given everything currently known about a CVE, what's the probability it will actually be exploited in the wild in the next 30 days? EPSS is a machine-learning model trained on real-world signals — observed scanning and exploitation activity, how much public exploit code exists, how the vulnerability has been discussed — and it outputs a probability from 0 to 1, plus a percentile ranking it against every other scored CVE. Two things make it operationally different from CVSS: it's data-driven rather than structurally derived from the vulnerability's technical properties, and it's recalculated daily, so a CVE's EPSS score can climb sharply the week exploitation actually starts, while its CVSS score never moves at all.

A third signal worth stacking on top of both: CISA's Known Exploited Vulnerabilities (KEV) catalog, a running list of CVEs CISA has confirmed are being actively exploited, with mandated remediation deadlines for U.S. federal agencies under Binding Operational Directive 22-01 — and, increasingly, treated as a hard "patch now" signal industry-wide regardless of sector. Where EPSS is a probability, KEV is a confirmed fact: it's already happening.

CVSS severityEPSSOn CISA KEV?Triage call
Critical / HighHighYesPatch immediately, block the release — the worst-case, most-likely, already-happening quadrant.
Critical / HighLowNoScheduled remediation, tracked with a deadline — severe if it happens, but nothing suggests it's happening yet.
Medium / LowHigh or on KEVPossibleEscalate anyway — active or likely exploitation overrides a modest severity score. This is the pairing CVSS-only triage misses entirely.
Medium / LowLowNoBacklog. Track it, but it isn't what blocks a merge.

There's no single universal EPSS cutoff the way there's a fixed 0–10 CVSS scale — teams commonly draw the "high" line somewhere around the top percentile, or a raw score above roughly 0.1, but that threshold is a deliberate organizational decision, not a published standard. Whatever number you pick, write it down as policy so triage doesn't quietly depend on whoever's on call that day. Current scores for both systems live at nvd.nist.gov and first.org/epss — both update independently of this page, so treat any specific score quoted here as illustrative of the shape of the data, not a live figure.

Worked scans: the three tools this chapter names

☺ Like you're 10: Enough theory — here's what actually typing the command and reading the output looks like, for each of the three tools you're expected to know by name.

The curriculum names three tools specifically, one per major ecosystem shape. All three read a lockfile or resolved artifact set, not just a manifest — consistent with everything above about why the lockfile is the artifact that matters. The output below is condensed and reformatted for readability; run each tool yourself against a real project to see the live shape of your own findings.

OWASP Dependency-Check — Java, .NET, and polyglot projects

Dependency-Check builds a Common Platform Enumeration (CPE) identifier for each artifact it finds and matches that against the National Vulnerability Database. Because CPE matching is string-based rather than exact-package-registry lookup, it's the noisiest of the three on false positives — and the tool ships a suppression.xml mechanism specifically to manage that.

dependency-check.sh \
  --project "checkout-service" \
  --scan ./target/*.jar \
  --format ALL \
  --suppression suppression.xml \
  --failOnCVSS 7 \
  --out ./odc-report
[INFO] Analysis Started
[INFO] Finished Archive Analyzer (0 seconds)
[INFO] Finished Jar Analyzer (4 seconds)
[INFO] Finished Central Analyzer (11 seconds)
[WARN] One or more dependencies were identified with known vulnerabilities.

log4j-core-2.14.1.jar
  pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
  CVE-2021-44228  CRITICAL  CVSSv3: 10.0  [AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H]
  → JNDI lookups in log message formatting allow attacker-controlled LDAP/RMI callbacks.

commons-text-1.9.jar
  pkg:maven/org.apache.commons/commons-text@1.9
  CVE-2022-42889  CRITICAL  CVSSv3: 9.8  [AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H]
  → Uncontrolled recursive interpolation of "script"/"dns"/"url" lookups enables RCE.

7 vulnerabilities found · 2 suppressed via suppression.xml · exit 1 (failOnCVSS=7 breached)

Safety — Python requirements and environments

Safety checks a requirements.txt, a Pipfile, or an active virtual environment against a curated Python vulnerability database. It's narrower in ecosystem scope than the other two, and correspondingly faster and quieter on false positives.

safety scan -r requirements.txt
Safety scanning requirements.txt
-> found and scanned 42 packages

REPORT
------
PyYAML  5.3.1
  CVE-2020-14343  CVSSv3: 9.8 (Critical)  fixed in: >=5.4
  → yaml.load() without an explicit safe Loader permits arbitrary code execution
    from a crafted YAML file.

setuptools  59.6.0
  CVE-2022-40897  CVSSv3: 7.5 (High)  fixed in: >=65.5.1
  → ReDoS in package_index.py's HTML parsing.

2 vulnerabilities found in 2 of 42 packages scanned · exit code non-zero
⚠ Watch out

Safety's CLI has changed shape more than once (safety check vs. the newer safety scan, and its exit-code and JSON-output conventions have shifted across major versions). Pin a specific Safety version in CI and confirm current flags with safety --help before wiring exit codes into a merge-blocking gate — don't assume last year's syntax still applies.

npm audit — Node.js and the JavaScript ecosystem

npm audit reads package-lock.json — the resolved tree, diamond dependencies and all — against the GitHub Advisory Database, and is built into the npm CLI itself since npm 6, so it needs no separate install.

npm audit --omit=dev
# npm audit report

follow-redirects  <1.15.4
Severity: high
Possible credential/authorization leak on cross-origin redirect
fix available via `npm audit fix`
node_modules/follow-redirects
  axios  1.2.0 - 1.5.1
  Depends on vulnerable versions of follow-redirects
  node_modules/axios

semver  <7.5.2
Severity: moderate
Regular Expression Denial of Service
fix available via `npm audit fix --force`
Will install eslint@9.x.x, a SemVer major change
node_modules/semver

2 vulnerabilities (1 moderate, 1 high)

To address issues that do not require attention, run:
  npm audit fix
Some issues need review, and may require choosing a different dependency.

Advisory IDs, exact wording, and severity buckets shown above are illustrative of the real report's shape — run the command against a live project for current identifiers. Two details worth knowing cold for the exam: --omit=dev scopes the audit to production dependencies only, and npm audit fix --force will happily bump a package across a semver-major boundary to close a finding, which is a correctness risk dressed up as a one-line fix — read what it proposes before you run it unattended in CI.

From findings to a merge-blocking gate

☺ Like you're 10: Three reports full of package names and numbers aren't a decision by themselves — you still need one rule that turns "here's what we found" into "ship it" or "don't."

A mature SCA gate combines everything above into one automated call, rather than leaving a human to eyeball three separate tool outputs every time. The shape is consistent across ecosystems: fail the build only on findings that clear a severity floor and either sit on the KEV catalog or clear an EPSS floor, everything else gets filed and tracked rather than blocking. npm audit's own --audit-level flag is the blunt version of this idea — it fails the command's exit code only at or above a chosen severity:

# .ci/pipeline.yml — SCA as a required, merge-blocking check
sca-scan:
  stage: scan
  script:
    - npm audit --omit=dev --audit-level=high   # non-zero exit fails the job
    - dependency-check.sh --scan . --failOnCVSS 7 --suppression suppression.xml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

The blunt version is a reasonable starting gate, but it's exactly the CVSS-only trap the warning above described — it has no concept of EPSS or KEV, and no concept of reachability. A more mature pipeline pipes the tool's JSON output into a policy step (Open Policy Agent and Conftest, covered in security in CI/CD, are the usual mechanism) that cross-references each finding's CVE against a daily-refreshed EPSS/KEV feed before deciding whether it actually blocks. That richer decision, and the backlog process for everything that doesn't block outright, is the subject of this blueprint's own vulnerability management & triage chapter — this page stops at generating a trustworthy finding; that one covers what an organization does with a queue of them.

Common exam traps

☺ Like you're 10: These are the specific ways people who understand SCA in theory still get the practical question wrong.

🎬 At the Shift-Left Squad
🦫

Benny: Dependency-Check flagged one moderate on a transitive jar. Everything else is clean — I'm merging.

🐢

Timmy: What's the CVSS vector, not just the number — and is it anywhere near CISA's KEV list?

🦫

Benny: Uh — 5.3, network vector, no user interaction. Not on KEV as far as I can see.

🦊

Foxy: Then the real question is whether we even call the vulnerable function. Nobody's checked reachability yet.

🐢

Timmy: Right. Moderate CVSS, no KEV hit, unconfirmed reachability — that's "ship it and track it," not "block the merge."

🐿️

Nutty: Which means it goes in the suppression file — with the CVE, the reason, and a review date. I'm not filing a mystery exemption nobody can explain in six months.

🦫

Benny: Fair. Last time I "just suppressed it" I couldn't remember why, three sprints later.

✓ Checkpoint

1. What's the difference between a manifest and a lockfile, and why does an SCA tool need to read the lockfile specifically? 2. Explain the diamond-dependency problem in your own words — why can the same CVE be both "patched" and "still present" in one project at once? 3. Given two CVEs with the same CVSS base score, what one CVSS metric would tell you their real-world blast radius is different, and what does it mean? 4. What does EPSS measure that CVSS does not, and how often is it recalculated? 5. Name one thing wrong with suppressing a finding in suppression.xml with no reason or review date attached.

Check your answers
  1. A manifest (package.json, requirements.txt) declares direct dependencies, often as a version range. A lockfile records the exact resolved versions of every dependency, direct and transitive. An SCA tool scanning only the manifest misses essentially the entire transitive tree — which is where most vulnerable code actually lives.
  2. Two direct dependencies can require different, incompatible version ranges of the same transitive package. A resolver like npm can't satisfy both with one installed copy, so it nests a second copy at a different depth. Patching or upgrading one branch's copy has no effect on the other branch's separately-resolved copy of the same package — so "patched" and "still vulnerable" can both be true simultaneously in one project.
  3. Scope (S). Scope Changed means a successful exploit can affect resources beyond the vulnerable component itself (Log4Shell); Scope Unchanged means the impact is contained to the vulnerable component (Text4Shell) — same base score, meaningfully different real-world blast radius.
  4. EPSS measures the probability a CVE will actually be exploited in the wild in the next 30 days — likelihood, not severity — using a machine-learning model trained on real-world exploitation signals. It's recalculated daily, so it can move sharply even when the CVE's CVSS score (which is structurally derived from the vulnerability's static properties) never changes.
  5. It's indistinguishable from a finding nobody ever actually reviewed — there's no way to audit whether the decision to suppress it was correct, whether conditions have changed since, or when to revisit it, which turns the suppression file into a place vulnerabilities quietly go to be forgotten rather than a documented risk decision.

Chapter 5 covers the other half of static analysis — source code itself and the secrets hiding inside it — in static analysis & secrets detection. For the SBOM half of "what's actually in this build," see software bills of materials and dependency & license risk management. And for the tool itself in more depth, see the OWASP Dependency-Check tool page and the wider tool landscape.