By Adrian De Gendt
Your SCA scanner produced 847 findings. Every single one of them is a real CVE with a real CVSS score. The tool is working correctly. The problem is that you now have 847 items and no reliable signal about which one to open first. CVSS 9.8 gets your attention, but that score was calculated against the vulnerable library in isolation, not against your application's topology. A deserialization gadget chain in a JSON parsing library scores high regardless of whether the library's vulnerable code path is reachable from any external input in your deployment.
This is the fundamental mismatch between what scanners measure and what triage requires. Scanners find. Triage answers the question of whether what was found matters to your specific running application. The two questions call for different analysis, and conflating them is what produces the 847-item backlog that nobody trusts.
What CVSS measures, and what it deliberately does not
CVSS was designed to characterize vulnerabilities, not applications. The base score captures properties of the vulnerability itself: attack vector, attack complexity, privileges required, user interaction, scope, and impact. These are meaningful properties for a vulnerability database. None of them account for whether the vulnerable component is deployed in a context where an attacker can reach it.
The environmental metrics exist in CVSS to allow organizations to adjust scores based on their specific context, but in practice very few pipelines compute them. Most CI integrations report base scores or NVD scores, which encode nothing about your service topology. A CVSS 9.8 on a library that is three levels deep in your transitive dependency tree, and whose vulnerable code path is never invoked by any code you wrote, is not a CVSS 9.8 risk in your application. It is a 9.8 risk in an abstract application that fully exercises the library's attack surface.
Transitive dependencies and the reachability gap
Consider a Node.js API service that depends on a logging library, which depends on a YAML parsing utility, which depends on a YAML schema validation library with a critical CVE in its recursive schema expansion logic. The SCA tool reports the critical CVE. The developer looks at the dependency and sees a three-hop transitive dependency on a YAML schema library. The API service does not accept YAML input from any endpoint. No application code calls the schema validation library. The recursive expansion path is unreachable from any user-controlled input the service handles.
The CVE is real. The library is present in the dependency closure. The CVSS score accurately describes the vulnerability. But the risk to this application, given its actual call patterns, is fundamentally different from the risk to an application that directly processes user-supplied YAML with schema validation. Treating both the same because both have the same library in their dependency tree is not conservative; it is uninformative.
The reachability gap is the difference between "present in the dependency tree" and "on an executable path from attacker-controlled input." SCA tools close the first gap well. They were not designed to close the second.
The call graph is the unit of analysis
Closing the second gap requires a call graph. A call graph represents which functions call which other functions in the application, and crucially, which functions are reachable from which entry points. When you annotate the call graph with dependency boundaries, you can trace whether any code in your application that handles external input eventually calls into a vulnerable dependency function.
This sounds straightforward in a monolith, but it is considerably more involved in a microservice application. The call graph does not stop at service boundaries; an inter-service HTTP call is an edge in the larger application-level graph. Service A calling Service B's internal API is a traversal step, and if Service B uses a vulnerable library in the code path that handles requests from Service A, the vulnerability may be reachable from Service A's entry points even though Service B does not have direct external exposure.
This is why we built Cybret to reason about the application graph at the service topology level, not just at the library dependency level. The reachability question for a microservice application spans service boundaries, and any analysis that treats each service as an isolated unit will misclassify the risk of internally-routed paths.
What "reachable from external input" actually requires
A finding is reachable in the security-relevant sense when two conditions hold. First, there is a code execution path from an entry point that accepts attacker-influenced data (an unauthenticated endpoint, an externally-callable API, a message queue that processes untrusted payloads) to the vulnerable code location. Second, the attacker-controlled data propagates to the vulnerable code location in a way that satisfies the conditions the vulnerability requires, such as reaching a specific function argument or triggering a specific code branch.
The first condition is computable with high precision from static analysis of the call graph. The second condition is harder: data flow tracking, particularly across service boundaries and through serialization/deserialization boundaries, is expensive and has higher false-positive rates than simple reachability. Most practical tools today address the first condition well and approximate the second. That approximation is still substantially more informative than CVSS-ordered triage from a flat finding list.
What finding count does measure
Finding count is not without use. It tells you about the scope of the remediation work ahead if you decide to address every finding. It tells you about your dependency hygiene relative to industry baselines. Over time, a declining total count with stable or growing reachable-critical count is meaningful: your remediation is hitting non-reachable findings while ignoring the relevant ones. That is a signal worth monitoring.
Where finding count breaks down is as a prioritization tool. Giving an engineering team a count of 847 with severity labels and asking them to work through it in order is assigning work based on a signal that contains no information about their specific application's risk surface. Some of those 847 may be critical in their context. Some CVSS 9.8 findings may be irrelevant. The count metric cannot tell the difference.
Reachability does not make CVSS irrelevant
This point is worth being direct about: reachability analysis does not replace CVSS or make vulnerability severity scores useless. Once you have identified the reachable subset of your findings, CVSS severity is exactly the right signal to order them by. A reachable CVSS 9.8 should be addressed before a reachable CVSS 6.5. The problem is using CVSS to sort the full finding set before applying the reachability filter, because that ordering gives equal weight to reachable and unreachable findings.
The correct sequencing is: apply reachability first to reduce the finding set to those that are traversable in your application topology, then apply severity ordering within that reachable set. CVSS does the job it was designed for, inside the boundary defined by your application's graph.
The practical implication for how teams structure their triage backlogs: the "critical" bucket in most tools today means CVSS 9.0+, regardless of reachability. A reachability-aware critical bucket means CVSS 9.0+ AND reachable from an external entry point in the production graph. That set is usually much smaller, and it is the set where immediate remediation has proportionate impact on actual risk.
See reachability analysis on your own code
Connect your repositories and apply a reachability filter to your existing scanner findings. No configuration changes to your current toolchain required.
Request early access