Open your scanner. Count the criticals. If you are like most teams, the number is somewhere between a few hundred and a few thousand, and it grows every week. Now answer a harder question: of all those findings, which ones are actually running in production, reachable by an attacker, and sitting in front of something worth stealing?
If you cannot answer that quickly, you are not alone — and the gap is not a reflection of your team. It is a structural limitation of how most scanners work. A standard SAST, SCA, or image scanner reports a vulnerability the moment the vulnerable code or dependency exists in your repository. It does not know whether that code path is ever executed, whether the artifact built from that repo is ever deployed, whether the workload running it is exposed to the internet, or whether the identity attached to that workload can reach sensitive data. So it reports everything, scores it by a generic rule, and leaves the prioritisation to you.
This article explains why “exists in the code” and “exploitable in production” are two very different statements, how to tell them apart, and what tooling actually closes the gap. The goal is simple: by the end you should be able to look at a backlog of thousands of findings and confidently separate the handful that can hurt you from the noise that cannot.
The core problem: a finding is not a risk
A vulnerability finding answers one question: does a known-bad pattern or dependency appear in this code? That is useful, but it is not the same as risk. Real risk requires several things to be true at the same time:
- The vulnerable code is reachable — some execution path in your application actually calls into it.
- The artifact is deployed — the repository or dependency actually ships in something that runs.
- The workload is exposed — the thing running it is reachable by an attacker, directly or through a chain.
- There is something behind it — the compromised workload can reach data, credentials, or further systems worth the attacker’s effort.
A traditional scanner checks only the first half of condition 1 (the pattern exists) and nothing about conditions 2, 3, or 4. That is why a CVSS 9.8 in an abandoned prototype branch and a CVSS 9.8 in your internet-facing payments service look identical in the dashboard — even though one is irrelevant and the other could end your quarter.
The consequence is a well-documented industry pattern: teams drown in critical-severity findings, remediation velocity drops because everything is “urgent,” and the genuinely dangerous issue sits at position 400 in a list sorted by a score that ignores your actual environment. Prioritising by generic severity is prioritising by a number that was never computed with your infrastructure in mind.
Why generic severity scores mislead you
CVSS (the Common Vulnerability Scoring System) describes a vulnerability’s characteristics in the abstract — how it could be exploited, what it could affect, assuming a worst-case deployment. It is a property of the vulnerability, not of your system. Two facts follow from that:
- CVSS does not know your topology. It cannot tell that the vulnerable library is only loaded in a batch job that runs in an isolated VPC with no inbound routes. It assumes the worst, every time.
- CVSS does not change when your environment changes. Patch the network path, remove the public load balancer, scope down the IAM role — the CVSS score stays exactly the same, because it was never about your network path, your load balancer, or your role in the first place.
This is not a criticism of CVSS; it is a reminder of what it is for. CVSS is a shared vocabulary for describing severity. It was never meant to be a work-prioritisation queue for a specific company. When teams use it as one, they get a backlog ordered by hypothetical worst cases rather than by what is exploitable in their estate.
Two refinements help, and both are worth adopting before anything else:
- EPSS (Exploit Prediction Scoring System) estimates the probability that a given vulnerability will be exploited in the wild in the near future. It moves you from “how bad could this be” toward “how likely is this to actually be attacked.” We cover the mechanics in what is EPSS.
- CISA KEV (Known Exploited Vulnerabilities) catalog is a list of vulnerabilities that are confirmed to be exploited in the wild right now. A finding that matches KEV deserves attention regardless of its CVSS. See what is the CISA KEV catalog.
KEV and EPSS sharpen the likelihood side of the equation. But even a KEV-listed, high-EPSS vulnerability is not a fire in your environment if the code is never reached, never deployed, or sits behind five network boundaries with nothing valuable behind it. To get the rest of the way, you need two more capabilities: reachability and correlation.
Reachability analysis: does the vulnerable code even run?
Reachability analysis asks whether a vulnerable function is actually invoked along some path from your application’s entry points. It is the difference between “this dependency contains a vulnerable function” and “your code calls that function, or calls something that calls it.”
This matters enormously for open-source dependencies. A modern application pulls in hundreds of direct and transitive packages. A vulnerability in a transitive dependency is extremely common — but in a large fraction of cases, your code never touches the vulnerable function at all. The dependency is present because something else needed a different part of the same package.
There are a few levels of reachability, and it helps to know which one a tool actually gives you:
- Dependency presence — the vulnerable package appears in your lockfile. This is what basic software composition analysis (SCA) reports. It is the weakest signal and the source of most SCA noise.
- Package reachability — your code imports or loads the vulnerable package at all.
- Function/symbol reachability — there is a concrete call path from an entry point in your code to the specific vulnerable function. This is the strongest static signal and the one that eliminates the most false positives.
When reachability is applied well, the “critical” dependency list shrinks dramatically, because the majority of transitive CVEs turn out to be unreachable in your specific code. That is not the scanner being lax — it is the scanner finally telling you the truth about your application rather than about the package in the abstract.
Reachability has an honest limit, though: it tells you the vulnerable code can run, not whether the workload running it is exposed or valuable. A reachable SQL-injection sink in an internal admin tool used by three people on a VPN is reachable — but it is not the same risk as the identical sink in a public signup API. To close that final gap, static reachability has to meet the runtime world.
Correlation: connecting the finding to where it actually runs
This is the step almost every standalone code scanner skips, because the information it needs lives outside the repository. To know whether a reachable vulnerability is exploitable in production, you have to connect four layers that are normally owned by different tools and different teams:
┌────────────┐ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐
│ Repository │──▶│ Build / │──▶│ Workload │──▶│ Identity / │──▶│ Data │
│ + commit │ │ image │ │ (where it │ │ IAM role │ │ (what the │
│ (the CVE) │ │ (artifact) │ │ runs) │ │ (what it can │ │ role reaches)│
│ │ │ │ │ exposure? │ │ reach) │ │ │
└────────────┘ └────────────┘ └─────────────┘ └──────────────┘ └───────────────┘
Reading that chain left to right answers the questions a severity score cannot:
- Is the artifact deployed? If the repository builds an image that nothing runs, the finding is theoretical. If it builds the image behind your public API, it is live.
- Is the workload exposed? A workload behind a public load balancer with an open security group is a very different story from one with no inbound path.
- What can the identity reach? If the workload’s IAM role has
AdministratorAccessor direct read on a sensitive data store, a foothold there is catastrophic. If it is scoped to one queue, the blast radius is small. - Is there sensitive data behind it? A path that ends at an unencrypted store of customer PII is the one you fix tonight. A path that ends nowhere interesting can wait.
When those layers are connected on a single graph, “severity” stops being a static label and becomes a computed property of the real path: public endpoint → reachable vulnerability → over-permissive role → sensitive data. That specific, end-to-end chain is what the industry calls an attack path, and it is the correct unit of prioritisation. For the deeper mechanics of how these paths are modelled and traversed, see what is attack path analysis.
The important architectural point: this correlation is only possible when the code side and the cloud side sit on the same asset graph. If your SAST/SCA findings live in one product and your cloud posture and IAM graph live in another, nothing can join “this CVE” to “that internet-facing workload with this role.” The join is the whole value, and it cannot be bolted on after the fact with a CSV export. This is the practice we describe in code to cloud security.
A worked example
Consider a single scenario and watch how the prioritisation changes as you add context.
The raw findings, in three separate tools:
- SAST: a SQL-injection sink in
auth.py→ Critical (by rule class) - SCA: a vulnerable transitive dependency in the service → High (by CVSS)
- CSPM: the RDS database the service uses is unencrypted → High
- CIEM: the service’s task role has broad database permissions → Medium
Four findings, four backlogs, four owners. Nothing in any single tool says these are the same story. A reasonable team might start with the CSPM list simply because it is the longest.
Now add reachability. The SCA dependency’s vulnerable function is never called from any entry point — it is unreachable. One “High” just evaporated, correctly. The SQL-injection sink in auth.py, however, is reachable from the login endpoint.
Now add correlation. The repo builds the image that runs as an ECS task. The task sits behind a public application load balancer. Its IAM role can read the production RDS instance. That instance holds unencrypted customer PII.
The four disconnected findings collapse into one sentence: a public login endpoint has a reachable SQL-injection flaw, running with a role that can read an unencrypted store of customer data. That is not four medium-to-high items to schedule across four teams. It is one critical, exploitable attack path — and the single highest-value fix is the SQL injection, because removing it breaks the whole chain.
Nothing about the individual vulnerabilities changed. What changed is that you can finally see them as a path, and the path tells you both the priority and the most efficient place to intervene.
What to actually do — a practical sequence
You do not need to boil the ocean. The following order moves you from “everything is critical” to “these three things are exploitable” with increasing precision:
- Overlay KEV and EPSS onto your existing findings. This is the cheapest win. Immediately deprioritise anything with negligible exploit likelihood and surface anything confirmed-exploited in the wild. See how to prioritize vulnerabilities based on business risk.
- Turn on reachability for your dependency findings. Expect the “critical” SCA list to shrink substantially once unreachable transitive CVEs are filtered out. This alone recovers a large amount of engineering time.
- Connect code findings to runtime. Join each remaining finding to the artifact, workload, exposure, identity, and data behind it, so severity reflects the real environment rather than a worst-case assumption. This is the step that requires code and cloud to share one graph.
- Prioritise by attack path, not by finding. Work the complete internet-to-data chains first, and within each chain fix the link that collapses the whole path most cheaply (often the code-level flaw).
- Catch it earlier next time. Push the same reachability-and-context logic left into the pull request via a PR quality gate, so the next exploitable path is flagged before the image ships rather than discovered in a production scan months later. This is the heart of shift-left security.
A note on scope, so you do not over-claim internally: reachability and correlation tell you what is exploitable through the paths your tooling can see. Code-to-runtime context is strongest when the code, image, and runtime layers are actually connected for a given service — it is not automatic for every repository the moment you plug it in. And static analysis plus runtime context does not replace dynamic application testing (DAST) or penetration testing; it tells you which issues are reachable and running, and it complements black-box testing rather than substituting for it.
How Cloudanix answers the question
Cloudanix Code Security is built so that a code finding is never evaluated in a vacuum. It runs SAST, SCA, secrets, repo-threat, and SBOM scans, and matches dependency vulnerabilities against a 28-feed vulnerability database enriched with CISA KEV and EPSS — so likelihood is baked into the finding from the start, not added later by hand.
The differentiator is the join. Because Cloudanix is a CNAPP+ with code and cloud on one asset graph, a code finding is traced along the real chain — repository → image → workload → IAM role → data — and its severity is computed from that path rather than from rule class alone. When the layers are connected, the platform can tell you not just “this CVE exists” but “this reachable CVE runs in an internet-facing ECS task whose role reaches an unencrypted PII store” — which is the sentence that actually decides what you do tonight. The result is one prioritised queue of true attack paths shared across AppSec and cloud teams, instead of two half-pictures that never meet.
If you want to see this on your own environment, the fastest route is to connect a repository and a cloud account and watch the correlation resolve: book a free assessment.