When it comes to shift-left, one of the most common practices is using SCA tools to understand your package ecosystem and see what kind of critical or high vulnerabilities you have. But on its own, this data does not give you a proper prioritization ranking. Yes, you may add the business criticality of that specific application and get a better ranking, but that still does not really answer your actual question: "is this critical vulnerability really critical in our environment, and what is the chance of it getting exploited?"

Today I want to go a bit deeper into this topic and explain what gaps and issues we hit during prioritization, and what some possible ways are to tackle them. Before jumping into that, let's first talk about the basic SCA (Software Composition Analysis) working methodology and what we should realistically expect from it.

Okay, to scan a project's packages, first you need some sort of inventory data to understand what is actually inside that project. That inventory data is called SBOM (Software Bill of Materials). There are different ways to generate it, but the easiest starting point is usually checking the project's dependency files such as requirements.txt, Cargo.toml, package.json, and so on. Still, there is an important detail here: these files do not always tell the full truth. Some projects pin exact versions, some define version ranges, some say "latest" and some resolve additional transitive packages only during build time. So if you want accuracy, lockfiles and build-generated SBOMs usually give you a better picture than plain manifests alone.

One side topic worth mentioning here is binary scanning, because this also gets misunderstood a lot. When some people hear "binary scan," they think antivirus-style heuristic scanning. Others think it is exactly the same as normal SCA on manifests and lockfiles. In reality, binary scanning sits somewhere else in the picture. It is still very relevant for software supply chain security, because now you are looking at the actual built artifact that will be shipped or deployed, not only the dependency declaration files. This can help catch cases where the final artifact contains things that source-only or manifest-only scanning did not fully reflect. But still, this does not magically solve reachability or exploitability either. It is another useful layer, not the final answer.

This matters a lot for prioritization, because what the developer intended to use and what the build actually resolved can be two different things. Anyway, once you have that inventory, the SCA scan starts. In essence, the SCA tool takes your components and compares them against vulnerability intelligence sources. NVD is one of the major ones, but not the only one. Modern SCA tools also use ecosystem advisories, GitHub Advisory Database, OSV-style sources, and other feeds depending on the vendor or open-source stack.

From there it can tell you things like "these packages have critical vulnerabilities" and so on. Up to this point, you do not even need to spend money on a commercial product if all you want is basic package-to-vulnerability matching. The logic itself is not magical. You can already get started with things like Google's open-source OSV-Scanner. But because the logic is simple, the result is also simple: it tells you vulnerable component presence, not much more. It does not really tell you actionability, reachability, or the future of that vulnerability in your environment.

What we really need after this point is a couple of extra layers: prioritization context, reachability, and finally the holy grail of this whole topic, exploitability analysis.

To understand reachability or exploitability, many people go back to CVE/NVD data and expect precise technical references from there. Here comes one of the biggest gaps. CVE records are good at identifying that a vulnerability exists, and usually they also identify which products or versions are affected. But they do not always give you the juicy part: exact symbols, affected functions, vulnerable routines, precise source files, and so on. These fields can exist in modern CVE records, but they are optional and they are not consistently populated. So the problem is not that the format cannot carry them. The problem is that in practice you often do not get them.

Why does this matter? Because without that symbol-level data, proper reachability or exploitability work becomes much harder. If the CVE you are interested in is in a closed-source application, your luck to discover those details may depend on vendor advisories, patch notes, binary diffing, or straight reverse engineering. If it is in an open-source application, then yes, you have better luck. You can inspect fix commits, patch diffs, advisories, changelogs, linked issues, or PRs. But even there, life is not perfect. The fix may be vague, spread across multiple commits, or not clearly linked to the CVE at all. Also, NVD is not the root source of all disclosure. It is more of an enrichment layer on top of CVE data and public references. So if the public trail is weak, your enrichment effort will also be weak.

So yes, there is definitely an archaeology problem here.

Now let's say we somehow have a perfectly enriched CVE database and all the precious symbols are at arm's reach. Even then, detection of a vulnerable package does not guarantee reachability. Reachability is basically the question of whether there is an actual execution path from your application into the vulnerable code. A dependency may exist in your tree, but that does not automatically mean your application can really get to the vulnerable part of it.

That path may be direct, for example when your own code calls the vulnerable function itself. It may also be indirect, when a framework, plugin, or another library eventually routes execution there on your behalf. This is exactly why reachability is more useful than plain dependency presence. It tries to answer whether the vulnerable code is part of a realistic execution flow in your application, instead of just sitting somewhere inside the dependency tree.

There are many reasons why a vulnerable package may still not be reachable in practice. It may come from a transitive dependency that is never touched. It may be imported but never used. The affected function may exist, but your application may only use the package in a safe way and never hit the vulnerable logic. Or the whole vulnerable path may only become active behind feature flags, certain configs, plugins, specific runtime conditions, or environment-specific behavior.

A simple example would be this: imagine a vulnerable XML parser library exists in your application. SCA tells you the library is there. Reachability analysis asks whether your application ever routes execution into the vulnerable parser code. If that parser is only used in an internal offline tool, that is one story. If it processes attacker-controlled input from a public API endpoint, that is a very different story. And even if the code path is technically reachable, that still does not automatically mean it is attacker-reachable, which is where exploitability starts becoming a different question.

This is where static reachability analysis comes into the picture. In very simple terms, it tries to answer whether execution can plausibly flow from your application into the vulnerable code.

At this point I can already hear the objection: "wait, not everything is statically compiled, some things are resolved dynamically or interpreted during runtime." And yes, that is exactly right. But actually I would frame the problem even wider than compiled vs interpreted. Static analysis struggles whenever the application relies on reflection, dynamic imports/loading, plugins, generated code, late binding, runtime-resolved behavior, or framework magic. So static reachability is useful, sometimes very useful, but it is still not the perfect answer we are looking for.

After discussing all this, the obvious next thought is: okay then let's look at the opposite of static analysis. For the sake of the jargon, let's call it runtime analysis.

Runtime analysis helps answer a very different question: "was the vulnerable code path actually loaded, executed, or observed in the running application?" This is powerful, because now we are not only talking about theoretical code flow. We are adding observation. We have the CVE references, maybe the symbols, maybe some static understanding of the application, and now we add another layer: what was actually loaded and what was actually called.

This is where runtime evidence becomes very promising. But it still has limitations:

  • absence of observation is not proof of absence
  • rare paths may never be exercised in staging or even in normal production traffic
  • exploit-triggering inputs may never happen during your monitoring window

So again, runtime analysis definitely helps, but it still does not fully answer exploitability by itself.

Up to this point, what we have actually understood is this:

  • SCA from SBOM: the vulnerable component exists in declared or resolved dependencies
  • Binary/artifact scanning: the vulnerable component exists in the built output we are actually shipping
  • Static reachability analysis: execution can flow to the vulnerable code
  • Runtime execution: that code path was actually observed running
  • Exploitability: an attacker can realistically use that path to create security impact in your environment

And this last part is where many discussions become messy. Exploitability depends on much more than code path access. You need to think in terms of attacker control over input, authentication position, network exposure, config hardening, sandboxing, privilege boundaries, compensating controls, and exploit complexity. So reachable does not automatically mean exploitable. And no, doing exploitability analysis as some blind fully-automated checkbox is not realistic.

The practical goal is not to perfectly prove exploitability for every CVE. The practical goal is to reduce uncertainty enough to prioritize correctly. Package matching tells us what may be present. Static reachability tells us what may be callable. Runtime analysis tells us what was actually exercised. Environmental context tells us whether an attacker could turn that path into impact. And if you want one extra threat layer on top, EPSS can help you understand which CVEs are more likely to see exploitation activity in the wild, while KEV tells you which ones are already known to be exploited. Used together, these methods give you a much more defensible prioritization model than CVE presence alone.