Part 3 of a 6-post series on vulnerability management. Part 1 covered why most VMPs fail; Part 2 walked through the scanner architecture. This one is about the prioritization layer — how raw findings become an ordered queue engineering can work.
A vulnerability scanner produces findings. A vulnerability management program produces ordered work. The gap between those two things is the prioritization layer, and it’s the layer where most programs quietly fail.
The default is to sort by CVSS. CVSS is fine as a component. It’s a bad answer on its own. A CVSS 9.8 in a sandbox project with no exploit code is not a higher priority than a CVSS 7.5 with active exploitation in an internet-exposed service that handles customer PII. Sorting by CVSS treats them as the same problem. Engineering learns quickly that the “critical” queue is full of theoretical severity, tunes out, and then misses the one that actually mattered.
This is the prioritization model I built for a fully custom, budget-constrained VMP I recently ran end to end. It’s a rules-based system, not a machine-learning risk score. It fits on a whiteboard. And it produces a queue engineering trusts.
Why CVSS Alone Doesn’t Work
CVSS was designed as a vulnerability severity score, not a prioritization score. It measures the theoretical worst case a vulnerability enables — the attack complexity, the attack vector, the impact on confidentiality, integrity, and availability. It says nothing about:
- Whether an exploit actually exists in the wild
- Whether the affected service is reachable from the internet
- Whether the data touched by that service is sensitive
- Whether the fix is a one-line dependency bump or a six-month migration
Two vulnerabilities with the same CVSS can have wildly different actual risk. A CVSS 9.8 in a package that runs in a build container behind three layers of network isolation, with no known exploit and no data exposure, is a paperwork item. A CVSS 7.5 with an active exploit against an internet-facing service that handles payment data is a fire.
If your prioritization treats them as equal, engineering can’t tell the fires from the paperwork. Everything above CVSS 7 becomes “high priority,” which means nothing is high priority.
The fix is not to abandon CVSS. It’s to add the context CVSS deliberately excludes and let a rules layer combine everything into a tier engineering can act on.
The Inputs: What Gets Scored
Three inputs feed the prioritization model:
CVSS stays as the base severity signal. It’s not the answer, but it’s a useful anchor. The CVSS 3.1 score comes in with the finding from the scanner.
EPSS — the Exploit Prediction Scoring System — gives you a probability that a CVE will be exploited in the next 30 days, based on real-world exploitation data. This is the single most useful thing to add on top of CVSS. It separates “high-severity theoretical” from “high-severity actively exploited,” and the split matters. Most CVEs never get exploited. The ones that do are heavily overrepresented among the ones with high EPSS scores. Sorting by EPSS alone would miss things (novel exploits don’t show up in the training data yet), but combining it with CVSS surfaces the CVEs that are both severe and being actively used.
Context tags on the affected asset. This is where the biggest reranking happens. Each repository or workload carries tags describing its exposure and data posture:
internet-exposed— the service is reachable from the internethandles-pii— the service processes personally identifiable informationhandles-payment-data— payment card or financial data flows through itsandbox— a test or scratch environment with no real users or datainternal-only— reachable only inside the networkdependency-of-critical— used by a service that itself is critical
These tags aren’t inferred from scans. They come from the asset itself — declared by the owning team, ideally as code that lives in the repository (see Part 2 for why that placement matters). The scoring engine reads them at scoring time and uses them to adjust the tier.
The Rules Engine
The scoring model is a rules engine, not a formula. There’s no priority = 0.4 * CVSS + 0.3 * EPSS + 0.3 * exposure — that kind of continuous score is opaque, hard to explain to engineering, and hard to defend in a review.
Instead, threshold rules do the initial classification, and tag-based adjustments bump the tier up or down.
Rough shape:
- Base tier from CVSS. A CVSS range maps to a starting tier — roughly, 9.0+ starts Critical, 7.0-8.9 starts High, 4.0-6.9 starts Medium, below that starts Low.
- EPSS override. If EPSS crosses a threshold (indicating active exploitation), the tier jumps regardless of CVSS. A CVSS 6.5 with EPSS 0.7 jumps from Medium to High or Critical, because it’s being actively exploited right now.
- Tag-driven adjustments. Certain tags bump the tier up, others bump it down:
internet-exposed+handles-pii→ up one tierhandles-payment-data→ up one tiersandbox→ down one or two tiers (depending on the base)internal-onlywith no sensitive-data tag → down one tier
The specific threshold values and tag effects are tuned to the environment. What matters is the shape: CVSS sets the starting point, EPSS handles exploitability, and tags handle the context CVSS deliberately doesn’t know about.
Every finding ends up in one of four tiers: Critical, High, Medium, Low. Nothing continuous. Nothing to interpret. Just the tier.
What Engineering Actually Sees
The scoring engine writes to Jira. Engineering sees a ticket. That ticket carries:
- Severity — Critical / High / Medium / Low. Only the tier. The raw CVSS, EPSS, and tag inputs are hidden from the ticket surface by default.
- CVE identifier — so anyone can cross-reference it
- Link to the vulnerability — the NVD entry or the vendor advisory
- Explanation — a short human-readable summary of what the vulnerability is and what it enables
- Affected resources — the specific repositories, images, or workloads the finding was raised against
- Remediation recommendation — the concrete action to take, when a canonical one exists (upgrade to version X, apply this config change, etc.)
The ticket is auto-created and auto-assigned by an n8n workflow. The SLA clock starts the moment the ticket lands in the team’s queue with an owner attached. This matters — an unassigned ticket sitting in a triage backlog with a running SLA is a broken process. Assignment and clock-start happen together.
The reason engineering sees only the tier, not the underlying score, is deliberate. The tier is what the process operates on. If engineering can see and argue with the raw numbers, every ticket becomes a negotiation about the score instead of about the fix. The scoring model is defensible — it’s rules, it’s documented, it can be audited — but the day-to-day surface is the tier, not the arithmetic.
The SLA Layer
Each tier carries an SLA — a maximum time between ticket creation and resolution. The specific durations depend on the organization’s risk tolerance and engineering capacity. What matters more than the durations is how the SLAs behave when reality gets in the way.
The gotcha here is the biggest lesson I’ve taken from running this: have strict SLAs, but document the ways they can be bent.
Two failure modes to avoid:
- SLAs that get breached constantly. If the Critical SLA is 7 days and half of Critical tickets breach it, the SLA becomes noise. Engineering learns that the deadline is fictional. Nothing gets prioritized. The security team is either quietly frustrated or loudly nagging, and neither works.
- SLAs with zero flexibility. If the Critical SLA is 7 days and there is no way to say “this one legitimately needs 10 days because the fix involves a data migration,” you’re forcing engineering to either lie about the completion date or burn out trying to hit an impossible target. Neither of those produces good work.
The middle path is what I’d recommend: strict SLAs paired with a documented exception process. A ticket can request a bend for specific documented reasons — a fix that requires a coordinated release, a fix that requires vendor action, a dependency that has no patched version yet. The exception is time-limited, reviewed, and logged. This defends a strong SLA (the exception isn’t “we didn’t have time,” it’s a specific documented reason) while acknowledging that not every fix fits on the same calendar.
I’ll go deeper on SLA design in Part 4 of this series. The prioritization post’s job is just to say: the tier alone isn’t the whole system. The tier drives an SLA, and the SLA drives the actual work. If either half is broken, the prioritization model produces the wrong queue.
Common Failure Modes
A few ways this model can go wrong in practice, worth naming so you can avoid them:
Tag inflation. If teams can declare their own exposure/sensitivity tags without any review, every service will end up tagged sandbox or internal-only and nothing will ever escalate. Tags need lightweight review — either by a security engineer, or via a rule that says “if the service is in the internet-facing account, internet-exposed is required regardless of what the tag file says.”
EPSS lag. EPSS is trained on public exploitation data. For a brand-new CVE, EPSS might be low even when active exploitation is starting. Pair EPSS with a mechanism for a security engineer to manually escalate a finding based on threat intelligence — override the model when the model’s inputs haven’t caught up.
Tier drift from CVSS-only thinking. If someone starts saying “the tier should really just be the CVSS score, we’re overcomplicating this,” resist. The whole point is that CVSS alone produces the wrong queue. Show them the specific findings where the tier changed the answer — a CVSS 6 that got escalated because of internet + PII, or a CVSS 9 that got deprioritized because of sandbox. The examples defend the model better than the theory does.
Vendor-severity contamination. Some scanners emit their own severity classification, distinct from CVSS. Ignore it. Vendor severity is a black box tuned to the vendor’s business model, not to your environment. Score off CVSS + EPSS + your tags. The scanner’s severity field is background information at most.
Key Takeaway
The prioritization model that produces a queue engineering trusts has three properties: it’s rules-based (so it’s explainable), it combines multiple inputs (so it reflects actual risk instead of theoretical severity), and it lands as a small number of tiers (so engineering doesn’t have to interpret numbers).
The specific inputs I use — CVSS as the base signal, EPSS for exploitability, context tags for exposure and data sensitivity — are portable across most environments. The specific thresholds and tag effects aren’t. They need tuning to how your organization actually looks, and they need review every few months as reality shifts.
The tier is what engineering sees. The SLA is what makes the tier real. And the exception process is what keeps the SLA from becoming theater.
Next in this series: SLA design and how to enforce it without burning engineering out.
Related Reading
- Why Most Vulnerability Management Programs Fail — Part 1 of this series
- Vulnerability Scanner Architecture — Part 2, on the coverage and dedup decisions upstream of prioritization
- AWS Detecting Privilege Escalation — the parallel “context beats raw signal” pattern in detection
- How to Detect AWS Root Account Usage — high-signal alerting done through the same “small tiered surface” model