SAST Prioritization: Why CVSS Can't Rank Your Code
SAST prioritization has a scoring problem that dependency scanning does not. A CVE in an open-source library comes with a CVSS vector, an EPSS probability and, if you are unlucky, an entry in the CISA Known Exploited Vulnerabilities catalog. A SQL injection in your own payments service comes with none of that. There is no CVE for your code, no exploit prediction, no public advisory. There is a rule name, a CWE and a severity the scanner assigned before it knew anything about where the code runs.
So most teams sort SAST findings by that severity, and most SAST backlogs look the same: hundreds of "high" findings in roughly random order, with the ones that matter mixed in among the ones that never will.
The fix is not a better severity. It is a different question. Instead of "how bad is this class of weakness?", ask "who can reach this path, and what happens if they do?" That question has an answer, and the answer lives in where the code runs.
Why severity is the wrong sort key for SAST
CVSS was designed to describe a vulnerability in a product that many people run. It captures properties of the weakness: attack vector, complexity, privileges required, impact. For a library, that is useful, because the library is the same code everywhere.
Your own code is not the same everywhere. The same weakness class means very different things depending on context:
- A command injection in an internet-facing API handler that accepts unauthenticated input is an incident waiting to happen.
- The same command injection in a batch job that reads a config file written by your own deploy pipeline is a code-quality issue.
- The same pattern in a test fixture is not a vulnerability at all.
A scanner that sees only the repository gives all three the same severity, because from inside the repository they look the same. The difference is outside the repository: in the deployment, the network path and the business value of the service.
The three questions that rank a SAST finding
A useful SAST prioritization model answers three questions for every finding. None of them are about the rule.
1. How much does the affected service matter?
This is business impact. A tier 1 payments service in production matters more than a tier 4 internal dashboard, and anything running only in staging, test or a sandbox matters less than either. You need two facts per finding: the tier of the service the code belongs to, and the environment it runs in.
Most teams already have tiers somewhere, in a service catalog, a spreadsheet or someone's head. The hard part is joining them to findings automatically, which requires knowing which service each file in each repository ends up in.
2. How exposed is the vulnerable path?
This is environment impact, and for SAST it has a subtlety that trips up most tools. Your own code runs by definition, so the question is not "is this code reachable?" It is "is the path from untrusted input to this sink reachable from outside?"
Answering that takes several facts together:
- Infrastructure exposure. Does the service run on compute reachable from the internet?
- Network origin of the data flow. Does the traced path start at an inbound network entry point, such as an HTTP handler, or at a file, a queue message or a scheduled job?
- Authentication. Does reaching the handler require a logged-in user?
- Sensitive data. Does the path touch credentials, personal data, auth context or financial data?
The second point is the one most models skip. A public service is not the same as a publicly reachable path. A weakness whose input comes from a local config file is not internet-reachable even when the service it lives in is.
3. How dangerous is this class of weakness right now?
There is no EPSS score for your code, but there is still threat information. Some weakness classes are exploited constantly; the CWE Top 25 exists for a reason. Some CWEs cannot manifest in some languages at all: memory-safety classes do not apply to memory-safe languages. And a weakness pattern that matches an active campaign, or code that is itself an indicator of compromise, is a different category of problem.
Combining the three: a worked example
Take one rule, SQL injection, firing in three places:
- Checkout API, production, tier 1. The data flow starts at an unauthenticated HTTP handler and reaches a query over a table holding customer records. Business impact high, environment impact high, threat medium. This is fix-now.
- Internal reporting service, production, tier 3. The handler requires authentication and the query touches no sensitive table. Business impact medium, environment impact medium. Real, but schedulable within its SLO.
- Data migration script, staging. Input comes from a file the migration reads. Not internet-reachable, non-production. Track it; it will not be the thing that gets you breached.
Same rule, same severity, three different decisions. A team sorting by severity works on these in whatever order the scanner emitted them. A team sorting by context fixes the first one this week and stops arguing about the other two.
What SAST prioritization by context requires
Context-based SAST prioritization is not a sorting trick. It depends on data that most AppSec programs do not have joined together:
- A map from code to deployment. Which repository, and which module inside it, ends up in which running service.
- Service metadata. Tier and environment for every service, kept current as services change.
- An endpoint inventory. The routes in your code, their handlers, their frameworks and whether they require authentication.
- Interprocedural data flow. A trace from source to sink across functions and files, so exposure is judged from the real path.
- Cloud and runtime exposure. Which compute is reachable from the internet.
If you are building this yourself, start with the first two. A reliable repository-to-service map with tiers attached will reorder your backlog more than any rule tuning.
What changes when you rank by context
- The urgent list gets short. Most SAST findings are not on an exposed path to a high-value service. The ones that are become a list a team can finish.
- Priority moves with the environment. A new internet-facing route or a tier change should re-rank existing findings automatically. Static severity cannot do that.
- SLOs become defensible. "Fix urgent findings in seven days" is a commitment engineering can keep when urgent means exposed and valuable. It is not when urgent means "the scanner said high".
- Nothing is dropped. Lower-priority findings are scheduled or tracked, not closed. Context changes the order, not whether a finding gets fixed.
Where context-based ranking goes wrong
Context makes SAST prioritization better, but only if the context is right. Three failure modes are worth guarding against.
- Missing metadata treated as low risk. A service with no tier or environment recorded should be treated as high impact until someone says otherwise. Defaulting unknowns to low is how a production service quietly drops off the urgent list.
- Stale exposure data. Exposure changes when a load balancer rule changes or a new route ships. If exposure is computed once a quarter, your ranking describes last quarter's attack surface.
- Mitigations nobody can see. A recorded risk acceptance or a confirmed false positive should lower a finding's priority, and the reason should be visible on the finding. A finding that drops in priority with no recorded reason is a finding nobody can audit.
The test is simple: pick any finding in your lowest band and ask whether you could explain to an auditor, in one sentence, why it is there. If you cannot, the model is hiding risk instead of ranking it.
How Heeler ranks SAST findings
Heeler's SAST uses interprocedural, cross-file taint analysis to trace untrusted input from source to sink, and it discovers the endpoints in your code along with their handlers and authentication. The Context Engine joins that to where the code is deployed, so every finding carries the tier and environment of the service it lives in.
Each code finding is scored on three impacts, business, environment and threat, and lands in one of three Autotriage bands:
- Urgent: fix now. A dangerous weakness on an exposed, high-value service.
- Plan: schedule it within its SLO window.
- Defer: track it. Not currently exposed, or on a low-value service.
Internet accessibility is judged from two signals together: an active deployment on internet-reachable compute, and a traced flow that starts at an inbound network entry point. A weakness fed from a file or a scheduled job is not treated as internet-accessible even on exposed infrastructure. Threat comes from the weakness class, such as a known-exploited CWE or CWE Top 25 membership, with CWEs that cannot occur in the finding's language dropped. The SAST prioritization docs give the full scoring tables.
Bands are recomputed as services change, so a new internet-facing route or a tier change moves a finding without anyone re-triaging it.
FAQ
Can you use CVSS to prioritize SAST findings?
Only loosely. CVSS describes a weakness in the abstract, and SAST findings in your own code have no CVE or EPSS score, so ranking them needs the context of where the code runs.
What is the most important signal for SAST prioritization?
Whether untrusted input from outside can reach the vulnerable sink on a service that matters. That combines internet exposure, the origin of the data flow, authentication and the service's tier.
Is a finding on a public service always urgent?
No. If the traced data flow starts from a file, a queue or a scheduled job rather than a network handler, the path is not reachable from the internet even when the service is.
Does deprioritizing a SAST finding mean ignoring it?
No. Lower-priority findings are scheduled or tracked. Prioritization decides the order of work, not whether the work happens.
If your SAST backlog is sorted by scanner severity today, it is worth seeing how it reorders once each finding carries its service, tier and exposure. Get a demo


.jpg)
