# Software Composition Analysis

> Software composition analysis (SCA) inventories the third-party code in an application and checks each component for vulnerabilities, licenses, and malware.

Also known as: SCA, dependency scanning, source composition analysis, software composition analysis tool, open source dependency scanning

Source: https://socket.dev/glossary/software-composition-analysis

Last updated: 2026-10-26

Software composition analysis (SCA), also called dependency scanning, is the automated inventory of the third-party code inside an application, followed by checks on each component for known vulnerabilities, license obligations, and malicious behavior. SCA tools build that inventory from manifests, lockfiles, or built artifacts, so they can only judge the components they can see.

Every SCA tool performs two operations. First it resolves an inventory: a list of components, each with an ecosystem, a name, a version, and ideally a content hash. Then it joins that inventory against outside data: advisory databases for vulnerabilities, license texts for obligations, and in newer tools, analysis of the package code itself. The join is the easy part. The inventory is where SCA succeeds or fails. When we installed express 5.3.0 into an empty npm project on October 26, 2026, one declared dependency produced 65 packages in the lockfile. Most of the code an application ships was never chosen by name.

Our position: SCA as most teams run it answers one question, "do we ship a version someone has publicly reported as bad?" That question is worth asking, and it misses the attacks that matter most. Malicious packages rarely get a CVE. Native libraries bundled inside packages, and tools fetched at run time, never enter the inventory. Most matched CVEs are never reached by your code. A useful SCA tool inspects what each package actually contains, runs before install rather than after deploy, and ranks vulnerabilities by reachability instead of severity score.

## What is software composition analysis?

Software composition analysis is the practice of knowing, and governing, the code you did not write. [Static application security testing](https://socket.dev/glossary/static-application-security-testing) (SAST) analyzes your own source for bugs. SCA treats each third-party component as a unit and asks four questions about it:

1. What is it? The exact name, version, and origin, including every [transitive dependency](https://socket.dev/glossary/transitive-dependency).
2. Is it known to be vulnerable? A match against CVE, GHSA, and OSV records.
3. What may we do with it? Its license terms and attribution duties.
4. Is it safe to run? Whether its code does something users would not expect, such as an install script that downloads a binary.

Classic SCA products answered the first three questions. The fourth is newer, and it is the one recent attacks exploit. A [software bill of materials](https://socket.dev/glossary/software-bill-of-materials) (SBOM) is the written answer to the first question, so SBOM generation is an SCA feature rather than a separate discipline. Some older pages call the practice "source composition analysis." The name is misleading, because SCA inputs are usually lockfiles and built packages rather than source code, and the established term is software composition analysis.

OWASP's history shows how the scope has shifted. The 2013 OWASP Top 10 called this risk "Using Components with Known Vulnerabilities." The [OWASP Top 10:2025](https://owasp.org/Top10/2025/A03_2025-Software_Supply_Chain_Failures/) lists it as Software Supply Chain Failures and widens it to compromised packages, build systems, and developer tools, a change we covered when [OWASP added Software Supply Chain Failures](https://socket.dev/blog/owasp-2025-top-10-adds-software-supply-chain-failures). OWASP's own data shows the gap with CVE-centric tooling. Exactly 50% of survey respondents ranked the category first, yet only 11 CVEs mapped to its weaknesses.

## How does software composition analysis work?

Software composition analysis works by reconstructing the dependency graph a package manager would produce, then identifying every node precisely enough to look it up. The hard part follows from how package managers resolve dependencies.

A manifest declares intent, usually as a version range:

```json
"dependencies": {
  "express": "^5.3.0"
}
```

The range `^5.3.0` accepts any 5.x release at or above 5.3.0. At install time the package manager picks the newest matching release, then repeats the process for each of that release's dependencies. Ranges exist on purpose, because they let many packages share one copy of a common dependency. The cost is that a manifest alone cannot tell an SCA tool what will be installed. The answer depends on the registry's contents at the moment of install.

A [lockfile](https://socket.dev/glossary/lockfile) records the result of resolution:

```bash
$ npm install express
added 65 packages in 14s

$ node -e "const l=require('./package-lock.json');
  console.log(Object.keys(l.packages).filter(Boolean).length)"
65
```

Each lockfile entry carries an exact version, a tarball URL, and an integrity hash. That makes the lockfile the best inventory source a repository offers. SCA tools convert each entry into a package URL (purl), the identifier that OSV and GitHub Security Advisory (GHSA) records use. npm can emit the whole inventory as a CycloneDX SBOM. Here is the entry for qs, a transitive dependency nobody in this project chose:

```bash
$ npm sbom --sbom-format cyclonedx \
    | jq '.components[] | select(.name=="qs") | {purl, hashes, licenses}'
{
  "purl": "pkg:npm/qs@6.16.1",
  "hashes": [
    {
      "alg": "SHA-512",
      "content": "e4fc7a27badf1c1006521350ddeb3863370e741070d120dc56d4e126697845d9cac38b4ab367a903490f67e2bc5fe5a21e4b53f7744755c43fdf29b96930a0b6"
    }
  ],
  "licenses": [
    {
      "license": {
        "id": "BSD-3-Clause"
      }
    }
  ]
}
```

The purl is the join key. A vulnerability lookup is a query for `pkg:npm/qs@6.16.1` against the version ranges in advisory records, the matching step we cover in [vulnerability scanning](https://socket.dev/glossary/vulnerability-scanning). The license field is the package's declared license, copied from one field of its package.json. Every SCA finding is only as accurate as the inventory and metadata behind it.

> Four layers of evidence an SCA tool can build its inventory from: manifest, lockfile, installed tree, and package contents, with an npm and a PyPI example at each layer and what each layer cannot see

The figure shows the four layers of evidence an SCA tool can read, from declared intent down to the code that runs:

| Layer | npm example | PyPI example | What the layer cannot show |
| --- | --- | --- | --- |
| 1. Manifest (declared intent) | package.json declares "express": "^5.3.0", one entry with a version range | requirements.txt declares cryptography\>=42, one entry with a version range | Exact versions and every transitive dependency |
| 2. Lockfile (resolved graph) | package-lock.json lists 65 packages with exact versions and SHA-512 integrity hashes | uv.lock lists cryptography 50.0.2 with 1 sdist hash and 52 wheel hashes | What install scripts do, and code run by npx or an agent |
| 3. Installed tree (what is on disk) | node\_modules/plain-crypto-js reports version 4.2.0, rewritten by the 4.2.1 dropper after it ran | pip list shows 4 packages: cffi, cryptography, numpy, pycparser | Metadata changed by code that already ran |
| 4. Package contents (what runs) | setup.js is a postinstall dropper that hides 18 strings behind two encoding layers | \_rust.abi3.so carries OpenSSL 4.0.3, and numpy.libs carries OpenBLAS | Little about the package itself; this layer shows behavior, bundled native code, and the license files actually shipped |

Code fetched by npx, global installs, and agent sandboxes sits outside all four layers, because it never enters the repository an SCA tool scans.

## What a lockfile cannot see

Lockfile-based SCA sees what the package manager recorded, not what each package contains. Three gaps follow, and each has produced real misses.

### Native code inside a package

A package name and version identify a release, not the bytes you run. As of October 2026, PyPI hosts 59 files for cryptography 50.0.2: one source distribution and 58 prebuilt wheels. The wheels contain a statically linked copy of OpenSSL. The package list shows four Python packages. The binaries say otherwise:

```bash
$ pip install cryptography==50.0.2 numpy==2.5.4
$ pip list --exclude pip
Package      Version
------------ -------
cffi         2.1.1
cryptography 50.0.2
numpy        2.5.4
pycparser    3.12

$ python -c "from cryptography.hazmat.backends.openssl.backend import backend
print(backend.openssl_version_text())"
OpenSSL 4.0.3 29 Sep 2026

$ SITE=$(python -c "import site; print(site.getsitepackages()[0])")
$ ldd $SITE/cryptography/hazmat/bindings/_rust.abi3.so | grep -i ssl
# no output: the extension does not load a system libssl

$ openssl version
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)

$ ls $SITE/numpy.libs
libgfortran-83c28eba-468e71e5.so.5.0.0
libquadmath-2284e583-a9307bba.so.0.0.0
libscipy_openblas64_-f48b354e.so
```

The cryptography extension links no system libssl, so the process runs the OpenSSL 4.0.3 compiled into the wheel. The host's OpenSSL 3.0.13 is irrelevant to it. numpy ships OpenBLAS and the GNU Fortran runtime in its numpy.libs directory. No lockfile entry names any of these libraries.

The Python ecosystem patches this gap by hand. The cryptography maintainers file advisories against their own package when an OpenSSL fix affects their wheels. We ran pip-audit 2.10.1 against cryptography 42.0.7:

```bash
$ echo "cryptography==42.0.7" > req.txt
$ pip-audit -r req.txt --no-deps --disable-pip
Found 11 known vulnerabilities in 1 package
Name         Version ID                  Fix Versions
------------ ------- ------------------- ------------
cryptography 42.0.7  PYSEC-2026-1284     44.0.1
cryptography 42.0.7  PYSEC-2026-2141     46.0.5
cryptography 42.0.7  PYSEC-2026-35       46.0.6
cryptography 42.0.7  PYSEC-2026-35       46.0.6
cryptography 42.0.7  PYSEC-2026-1284     44.0.1
cryptography 42.0.7  PYSEC-2026-2141     46.0.5
cryptography 42.0.7  PYSEC-2026-3553     49.0.0
cryptography 42.0.7  PYSEC-2026-3553     49.0.0
cryptography 42.0.7  PYSEC-2026-3554     49.0.0
cryptography 42.0.7  GHSA-h4gh-qq45-vh27 43.0.1
cryptography 42.0.7  GHSA-537c-gmf6-5ccf 48.0.1
```

The 11 rows hold 7 distinct advisories. Three of the seven concern bundled OpenSSL, not Python code: PYSEC-2026-1284 (an alias of CVE-2024-12797), [GHSA-h4gh-qq45-vh27](https://osv.dev/vulnerability/GHSA-h4gh-qq45-vh27), and GHSA-537c-gmf6-5ccf. The last two have no CVE alias at all, so a scanner that matches on CVE identifiers never sees them. The advisory text, written by the cryptography maintainers, also shows why name-and-version matching is imprecise: "Only users installing from wheels built by the cryptography project (i.e., those distributed on PyPI) need to update their cryptography versions." A team that builds cryptography from source against its own patched OpenSSL gets the same three findings, and all three are false positives. Lockfiles such as uv.lock do record the exact file installed, with one sdist hash and 52 wheel hashes for cryptography 50.0.2. Advisory records still key on name and version.

The fix works only where maintainers choose to file. Nothing in the numpy wheel's metadata tells an SCA tool which OpenBLAS release it carries.

### Metadata an install script can rewrite

Inventories built from installed trees, such as node\_modules or site-packages, trust metadata that executed code can change. In the [axios compromise](https://socket.dev/blog/axios-npm-package-compromised), the malicious dependency plain-crypto-js@4.2.1 ran a [postinstall script](https://socket.dev/glossary/postinstall-scripts) dropper. The dropper then deleted its own package.json and renamed a decoy file into its place. Afterward, node\_modules reported plain-crypto-js 4.2.0, a clean version with no install script. An SCA scan of the installed tree inventories a harmless package. The lockfile still records 4.2.1. Comparing the two inventories catches this. Trusting either one alone does not.

### Code that never enters the repository

SCA usually scans a repository, and much of the code a team executes never lands in one. Tools run with npx, global installs, ephemeral CI jobs, and packages an AI agent installs in a sandbox all resolve dependencies at execution time. During the axios exposure window, we saw a CI pipeline run a CLI through npx and pull in the malicious dependency through an ordinary semver range. As we wrote in [the hidden blast radius of the axios compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise), a clean lockfile checked afterward does not prove you were safe. Vendored copies fail the same way. Socket found a package shipping a tampered axios 1.14.1 inside its own node\_modules directory, where no manifest the consumer controls could list it.

## Can software composition analysis detect malware?

Software composition analysis detects malware before an advisory exists only if it reads the package code. CVE-matching SCA detects malware by lookup, after someone files a record. A malicious package is not a vulnerability. It is working code doing what its author intended, so it rarely receives a CVE, and the first installs run the payload before any database has a record to match.

Behavioral analysis inverts the order. Instead of asking whether a version appears in a database, it inspects the published artifact for capabilities and changes: a new install script, network or shell access, obfuscated strings, or a dependency that first appears in a patch release. Socket's [malware detection](https://socket.dev/glossary/malware-detection) works this way. It flagged plain-crypto-js@4.2.1 at 00:05:41 UTC on March 31, 2026, six minutes after the package was published. The dropper hid 18 strings, including its command-and-control URL, behind two layers of encoding. A scanner that matches names and versions had nothing to match in those six minutes. A scanner that reads setup.js did.

Reading contents matters because attackers route around metadata. In the [Miasma Mini Shai-Hulud](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack) waves of June 2026, compromised npm packages shipped a binding.gyp file that made node-gyp run `node index.js` during install. The payload executed with no preinstall or postinstall entry in package.json. A tool that checks only the declared lifecycle scripts sees nothing. npm v12 now blocks that implicit node-gyp rebuild by default, as we reported when [npm v12 shipped with install scripts off](https://socket.dev/blog/npm-12).

Behavioral analysis has real costs. Install scripts and network access are also how legitimate packages compile native code and download binaries, so a policy that blocks on capability alone blocks real work. AI-assisted classification produces false positives. Socket reports those detections under a separate alert, AI-detected potential malware, so teams can warn on them instead of blocking. Analysis of the registry artifact also cannot see a second-stage payload fetched from a server at run time, only the code that fetches it.

Where the check runs matters as much as how it works. A CI scan that fails a pull request after `npm install` has already let install scripts run on the runner. As of October 2026, npm v12 and pnpm 10 no longer run dependency install scripts by default, which narrows that window without closing it: code still runs when the application first imports the package. Socket Firewall moves the decision before download. It proxies package manager requests, checks each requested package, including transitive ones, against Socket's data, and blocks known malware before it reaches disk, according to the [Socket Firewall documentation](https://docs.socket.dev/docs/socket-firewall-overview). A proxy sees only the traffic routed through it, so laptops and agents that bypass it stay uncovered.

## Why do SCA tools report so many false positives?

SCA tools report many false positives because a version match proves a component is present, not that it is exploitable. Two causes dominate.

The first is identity. Tools that map files to Common Platform Enumeration (CPE) names have to guess. OWASP Dependency-Check collects vendor, product, and version "evidence" from files and matches it against an index of NVD CPE entries. Its [documentation](https://jeremylong.github.io/DependencyCheck/general/internals.html) states that "both false positives and false negatives may exist" as a result. Package-native identifiers such as purls remove most of the guessing, which is why we recommend purl-based matching for open source dependencies.

The second is reachability. A vulnerable function in a dependency matters only if your code can call it. [Reachability analysis](https://socket.dev/glossary/reachability-analysis) builds a call graph from your code into each dependency to check. Socket's [reachability documentation](https://docs.socket.dev/docs/reachability-analysis) describes three levels. Dependency Reachability works at the package level and eliminates up to 35% of false positives. Precomputed Reachability works at the function level inside dependency code and eliminates up to 80% in transitive dependencies. Full Application Reachability also analyzes your application code and eliminates up to 90%. The analysis is conservative and still imperfect. It marks a vulnerability unreachable only with strong confidence, treats reflection and dynamic loading on a best-effort basis, and by its own documentation "does not consider uses of dependencies that happen outside of code," such as a package run only as a command-line tool.

License data fails in the opposite direction, by missing obligations. Registry license fields usually come from one metadata field. When we analyzed Django 5.1.1, its files implicated seven distinct licenses across ten license files, including CC-BY-SA-4.0, a copyleft license. PyPI listed only BSD-3-Clause, as we documented when [introducing Socket's license analysis](https://socket.dev/blog/socket-license-analysis). [License compliance](https://socket.dev/glossary/license-compliance) built on the registry field undercounts what you have agreed to.

## Which software composition analysis tool is best?

The best software composition analysis tool is the one whose inventory matches what you actually ship and run. No single approach answers all four questions well. The approaches differ more than the products within each one.

| Approach | Examples | What it inventories | What it misses | Use it for |
| --- | --- | --- | --- | --- |
| Package manager audit | npm audit, pip-audit, Safety | A lockfile or installed environment, matched by name and version | Malware before an advisory, bundled native code, license files, reachability | Patch hygiene on every lockfile |
| CPE evidence matching | OWASP Dependency-Check | Files in the build, mapped to inferred CPE names | Advisories with no CVE, and the correct identity of many packages | Avoid for open source packages |
| Artifact and image scanning | Syft with Grype, Trivy | OS packages and manifests inside a container image or filesystem | Malware before an advisory, and code fetched at run time | Every container image you deploy |
| Content and behavior analysis | Socket | Each package's published files, plus manifests and lockfiles | Second-stage payloads fetched at run time, and installs that bypass the check | Every install path: CI, laptops, and agents |
| Verdict | Combine three approaches | Lockfiles, images, and package contents each need their own check | No single approach answers all four questions | Audit every lockfile, scan every image, and analyze package contents before install. Skip CPE matching for open source packages. |

Safety, a Python-only scanner, belongs in the first row. It checks Python requirements against its own Safety DB. As of October 2026, version 3.8.1 prompts for an account when unauthenticated, and its [PyPI page](https://pypi.org/project/safety/) says paid plans track three times as many vulnerabilities and malicious packages as the free plan. Like every tool in that row, it finds malware only after a record exists.

Whatever you run, read every SCA result as a statement about the inventory it scanned. A clean report on a lockfile says nothing about the OpenSSL inside a wheel, a tool run by npx, or a package that rewrote its own metadata after install.

The unsolved problem is identity below the package. A purl names a release, while cryptography 50.0.2 is 59 different files, 58 of them wheels carrying a statically linked OpenSSL that the purl does not mention. Until advisory records and SBOMs routinely identify the artifact and the native code inside it, every SCA tool reports on the label rather than the contents. Reading the contents is the only check that does not wait for someone else to file a record.
