Safeguard
DevSecOps

Software Composition Analysis Best Practices for Engineering Teams

CVE-2017-5638 was patched by Apache in March 2017, two months before Equifax was breached through it. Point-in-time SCA scans miss exactly this kind of drift.

Safeguard Research Team
Research
7 min read

Apache disclosed CVE-2017-5638, a remote-code-execution flaw in the Struts 2 Jakarta Multipart parser, on March 7, 2017, and shipped a fix the same day. Equifax's systems remained on the vulnerable version when attackers began exploiting it in mid-May 2017, ultimately exposing the personal data of roughly 147 million people. The patch existed for over two months before the breach started — the failure wasn't a missing fix, it was a scanning and patching process that never caught up to it. Four years later, CVE-2021-44228 (Log4Shell) showed the opposite failure mode: the vulnerable code wasn't missing from scans, it was buried so deep in transitive dependency trees that teams didn't know Log4j 2 was present at all. Both incidents point to the same conclusion: software composition analysis (SCA) run as an occasional audit — a quarterly scan, a pre-release check — cannot keep pace with how dependency trees actually change. This piece covers four practices that separate SCA programs that generate a triage backlog from ones that generate a fix queue: continuous scanning instead of point-in-time snapshots, reachability-based prioritization instead of raw CVE counts, SBOM freshness as dependencies update, and policy gates instead of manual review.

Why isn't a point-in-time SCA scan enough?

A point-in-time scan is a snapshot of risk at the moment it ran, and that snapshot starts decaying immediately. Dependencies aren't static: a package.json or pom.xml that was clean at last month's audit can become vulnerable the day a new CVE is published against a library you already shipped, with no code change on your side at all. Log4Shell is the standard illustration — CVE-2021-44228 was disclosed on December 9, 2021, with a CVSS score of 10.0, and teams running quarterly or even monthly SCA scans had no way to know within hours whether Log4j 2 sat two or five levels deep in a service they'd shipped a year earlier. Continuous scanning closes this gap by re-evaluating your existing inventory every time an external signal changes — a new CVE lands in the National Vulnerability Database, a CVE is added to CISA's Known Exploited Vulnerabilities (KEV) catalog, or an EPSS exploit-probability score shifts — rather than waiting for the next scheduled run. The unit of work also changes: instead of re-scanning everything from scratch, a continuous system matches new advisories against components already indexed in a dependency graph, so only affected assets get rescored.

Why prioritize by reachability instead of raw CVE count?

Raw CVE counts overwhelm teams with findings that can never actually be exploited in their application. A standard SCA scan matches your manifest or lockfile against a vulnerability database and reports every match — it does not check whether your code ever calls the vulnerable function. Multiple vendor studies published between 2023 and 2024 estimated that a large majority of CVEs flagged by SCA tools in typical applications sit in code paths that are never invoked at runtime, which is why a 400-item open-CVE backlog is common and a 400-item fix queue is not realistic. Reachability analysis closes that gap by building a call graph — static, and ideally dynamic, based on what actually executes in production — and checking whether execution can reach the vulnerable line at all. Safeguard's reachability engine classifies each finding as reachable, conditionally reachable (behind a flag or route not currently enabled), or unreachable, and combines that classification with EPSS probability, CISA KEV status, and runtime/business context into a single 0-100 priority score. Based on aggregate data across its customer tenants, Safeguard reports this filtering typically removes 60-80% of findings from the "urgent" queue without dropping real risk — turning a 200-500 item backlog into the 20-60 items a team can actually work through in a week.

What does SBOM freshness actually require?

SBOM freshness requires treating the bill of materials as a continuously updated index, not a document generated once at release time. A Software Bill of Materials, typically in CycloneDX or SPDX format, lists every direct and transitive dependency with its resolved version and license, and Executive Order 14028 (May 2021) pushed SBOM generation into U.S. federal procurement requirements, with NIST's Secure Software Development Framework (SP 800-218) referencing SBOMs as a supporting practice. A stale SBOM — one generated at the last release and never updated — answers "what did we ship" but not "what are we exposed to today," because dependency versions, transitive resolution, and even licenses can all shift between releases without a new build. The practical requirement is regenerating or incrementally updating the SBOM on every build and every dependency-lockfile change, and querying it the moment a new CVE drops rather than re-scanning repositories from scratch. This matters most for transitive depth: most SCA tools resolve dependency trees to roughly 50-60 levels, while real production dependency graphs — the kind that hid Log4Shell and the SolarWinds SUNBURST implant several levels down — can go deeper, so an SBOM that stops resolving too early can look current while still missing the component that actually matters.

Why do policy gates matter more than manual triage?

Policy gates matter because manual triage doesn't scale past the point where a security team is reviewing every finding by hand, and that point arrives faster than most teams expect. A gate is a checkpoint — typically wired into CI/CD — that evaluates a policy (no critical CVEs, no CVSS above a threshold, no actively-exploited EPSS/KEV matches, no disallowed licenses, a minimum SLSA attestation level) and takes an automatic action: block the build, warn and allow, notify a channel, or require manual approval for an exception. This shifts triage from "a human reviews every new finding" to "a human reviews only the exceptions a policy explicitly can't resolve," which is the only version of the workflow that survives a team's dependency count growing. It also produces something manual triage never does on its own: an audit trail. When an exception is requested, it should carry a business justification and an expiration date, and get logged for the next compliance review — which matters directly for frameworks referencing EO 14028 or SLSA attestation levels. A gate condition combining reachability with exploitability — for example, blocking only when a finding is both reachable and KEV-listed in a production environment — is what keeps gates from becoming as noisy as the raw CVE list they're meant to replace.

How Safeguard Helps

Safeguard's SCA pipeline is built around these four practices rather than treating them as separate add-ons. Continuous scanning re-evaluates every asset — source repositories, container images, and both your own and vendor-supplied SBOMs — against vulnerability-feed deltas, with new CVEs typically reflected in scored findings in under two minutes and CISA KEV additions in under sixty seconds. Deep dependency scanning resolves transitive trees to 100 levels, deeper than the 50-60 level norm, specifically to catch components hiding at the depth where Log4Shell and the xz-utils backdoor (CVE-2024-3094, discovered in March 2024 when a Microsoft engineer noticed anomalous SSH latency) were found. Reachability analysis then filters that inventory down to a priority score combining static and dynamic call-graph reachability, EPSS, and KEV status, and policy gates let teams enforce conditions like "no reachable, KEV-listed finding in production" directly in CI/CD — turning the output into a fix queue instead of a spreadsheet.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.