Let's Connect

A wall of monitors showing security dashboards and a long list of vulnerability findings awaiting triage

To prioritize security fixes you have to read the report, not just sort it by severity and start at the top. A scanner or pentest hands you 200 findings on a Tuesday and you cannot patch all of them today, this week, or possibly this quarter. The skill that matters is not patching everything blindly; it is deciding what actually threatens you and in what order. The fix is a repeatable triage pass: read each finding in YOUR context, fix critical-and-reachable now, schedule the rest by real risk, suppress documented false positives, and refuse to drown in low-severity noise. I have run this pass on reports from Trivy, Snyk, npm/composer audit, and full manual pentests, and the reports that look terrifying at first glance almost always shrink to a handful of things that genuinely need doing this week.

Why a "critical" can be safe and a "medium" can be on fire

The number at the top of a finding is a CVSS Base score, and the single most important thing to understand is what that score is and is not. The CVSS v3.1 specification is explicit: the Base Score "reflects the severity of a vulnerability according to its intrinsic characteristics which are constant over time and assumes the reasonable worst case impact across different deployed environments." Read that last clause again. The Base score is the worst case across every possible deployment, not yours. It does not know whether the vulnerable code path is even reachable in your app, whether the affected service is internet-facing, or whether you already have a mitigation in front of it.

That is exactly why a CVSS 9.8 critical can be a non-event for you and a 5.4 medium can be the thing that gets you breached. A 9.8 in a transitive dependency you import but never call the vulnerable function of, on an internal service with no inbound route from the internet, is a worst-case score for a path that does not exist in your environment. Meanwhile a medium-rated authentication bypass on your public login endpoint, with a working exploit already circulating, is a clear and present problem. The Base score started the conversation. It does not get to finish it.

Base, temporal, and environmental: the three lenses

CVSS has three metric groups and most reports only show you the first one. Temporal metrics, per the spec, "adjust the Base severity of a vulnerability based on factors that change over time, such as the availability of exploit code." Environmental metrics "adjust the Base and Temporal severities to a specific computing environment" and let you weight the score by how important the affected asset actually is to you. When you do triage, you are effectively computing the environmental score in your head: a published exploit pushes a finding up, a non-reachable or non-exposed asset pushes it down.

  • Base: intrinsic, worst-case, environment-agnostic. This is the number the scanner prints. Treat it as an input, not a verdict.
  • Temporal: is there a public proof-of-concept or weaponized exploit, or is this still theoretical? A known exploit in the wild is the single biggest reason to move something up the list.
  • Environmental: in YOUR system, is it reachable, is it internet-facing, and how much do you care about the asset it sits on? This is where most of your real prioritization happens.

What questions do I actually ask of each finding?

For every finding above the noise floor, I run the same four questions. They take about a minute each once you know the codebase, and they collapse a scary-looking report into a short, ordered worklist. The order matters: reachability first, because an unreachable bug needs no further analysis.

  • Is it reachable in our app? Do we actually call the vulnerable function, hit the affected route, or parse the affected file type? A CVE in a library method you never invoke is not your bug today.
  • Is it internet-facing? A flaw on a public endpoint is a different universe of risk from the same flaw on an internal admin tool behind a VPN.
  • What is the blast radius? If exploited, what does the attacker get: read access to one user's data, every user's data, or remote code execution and a foothold? Data exposed and privilege gained are the two axes that matter.
  • Is there a known exploit in the wild? Check CISA KEV and the advisory itself. "Theoretically exploitable" and "Metasploit module exists" deserve very different urgency.

Those four answers give you something the raw CVSS number never could: a risk picture grounded in your architecture. A finding that is reachable, internet-facing, high blast radius, and has a public exploit is a drop-everything item regardless of whether the scanner called it high or critical. A finding that fails the first question outright is a candidate for a documented suppression, not a fix.

A laptop showing analytics dashboards with charts, representing triage of vulnerability findings by risk rather than raw severity
Triage is a sorting problem, not a patching problem. The output of reading the report is an ordered worklist, not 200 simultaneous fires.

A framework I can run on any report

Here is the pass I actually use. It is deliberately simple because a triage process you do not follow is worthless. The goal is to walk the full report once, bucket every finding, and come out with a small set of tickets that have owners and dates.

Vulnerability triage decision flow
FOR each finding in the report:

  1. Reachable in our code/config?
        NO  -> suppress WITH a written reason + re-check date. Stop.
        YES -> continue.

  2. Internet-facing AND (RCE or auth bypass or mass data exposure)?
        YES -> FIX NOW. Open a P1 ticket, assign an owner today.
        NO  -> continue.

  3. Known exploit in the wild (CISA KEV / public PoC)?
        YES -> escalate one level. Schedule inside this sprint.
        NO  -> continue.

  4. Severity >= High AND reachable?
        YES -> schedule by risk (next sprint, with a ticket).
        NO  -> backlog. Batch low-severity into a periodic cleanup,
               do NOT triage each one individually.

OUTPUT: a handful of P1s, a sized sprint queue,
        a documented suppression list, and a low-sev backlog.

Question 3 is the one I automate, because checking every CVE by hand against the exploited-in-the-wild list is exactly the kind of tedium that gets skipped under pressure. CISA publishes its Known Exploited Vulnerabilities catalog as a JSON feed, so a one-liner tells me whether a given CVE is something attackers are actively using right now. Anything that matches jumps straight to the top of the queue, no debate.

Check a CVE against the CISA KEV catalog
# Pull the catalog once, then check any CVE against it.
KEV_URL="https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
curl -sf "$KEV_URL" -o kev.json

CVE="CVE-2021-44228"
if jq -e --arg cve "$CVE" '.vulnerabilities[] | select(.cveID == $cve)' kev.json > /dev/null; then
  echo "$CVE is in CISA KEV -- actively exploited. Escalate now."
else
  echo "$CVE not in KEV. Score it on reachability + exposure as usual."
fi

The two rules that save the most time are at the top and the bottom. Suppressing a non-reachable finding WITH a written reason and a re-check date is not cheating; it is the honest answer, and the written reason is what stops the same finding from re-triaging itself on every scan. Batching low-severity findings into a periodic cleanup instead of triaging each one individually is what keeps the noise from eating your week. If you find yourself spending real attention on a low or informational finding while a reachable high sits untouched, you have inverted your own priorities.

The Base score tells you how bad a vulnerability is in the worst possible universe. Your job is to figure out whether you live in that universe.Md Raihan Hasan

How do I keep this from rotting?

Triage that lives in your head evaporates by Friday. Every fix-now and scheduled item needs a ticket with an owner and a date, and every suppression needs a written justification you would be comfortable showing an auditor. This is also where the distinction between this work and two adjacent jobs matters. Reading and prioritizing a whole report is not the same as the focused, time-critical scramble of responding to a single critical CVE in your dependencies, and it is not the same as understanding the specific tool that generates a lot of these findings, which I cover in how to read and fix a composer audit report. Triage is the layer above both: it decides which of the many findings becomes a CVE-response sprint and which becomes a one-line suppression.

  • Ticket everything you commit to fixing, with a named owner and a due date tied to its risk tier, so "we will get to it" becomes a tracked thing.
  • Record every suppression with the reason (not reachable, false positive, compensating control) and a date to re-evaluate, because reachability changes when the code changes.
  • Re-run the scanner on a schedule and diff against last time. New findings get triaged; unchanged old ones do not get re-litigated.
  • Feed recurring classes of finding back into your baseline. If the same misconfiguration keeps showing up, fix the template, not the symptom, the way I treat the items in my Laravel security checklist.

If you want the canonical reference for what each CVSS metric means before you start overriding scores in your head, the FIRST CVSS v3.1 specification document is the source of truth and worth thirty minutes of reading once. After that, the framework here is muscle memory.

A 200-finding report is not 200 problems. It is usually five or ten real problems wearing a costume made of unreachable code paths, internal-only services, and theoretical exploits that will never be weaponized. Reading the report well is the act of stripping that costume off: take the Base score as a starting point, ask the four questions, run every finding through the same decision flow, and write down what you decided and why. Do that consistently and you stop reacting to the scariest number on the screen and start spending your limited fix-time on the things that can actually hurt you. That is the whole game, and it is a skill you get measurably faster at every time you run the pass.