t0ul

The Risk Rancher Field Manual, Ch. 6: Sorting the Herd

Part of The Risk Rancher Field Manual, a hands-on guide to starting, scaling, and running a vulnerability management program.

This is the chapter the whole book has been walking toward, and it is the one your spreadsheet does not survive. Everything before now was setup: you sized the program, mapped the herd, built the alliance with IT, cast the nets. Now the findings pour in from every scanner and pentest and bug report, thousands of them, and they arrive as one undifferentiated wall of red.

Triage is the act of turning that wall into a short, ordered list of things worth doing today. It is the core of vulnerability management, the whole ballgame, and it is where Risk Rancher was born, because doing it by hand is where I personally lost the most hours of my career.

We are not going to talk about it in the abstract. You are going to do it, on a real file, right now.

The pile we will actually work

Download these two files:

  • sample-findings.csv: 29 findings from a handful of scanners, across eight systems. This is your wall of red in miniature. A real export is fourteen thousand rows of this; the moves are identical.
  • sample-assets.csv: the eight systems, with the criticality and exposure you set back in chapter two.

Open the findings file. It is a mess on purpose: duplicates, false positives, scary numbers on boxes nobody can reach, and quiet mediums on systems that would end your company. The first eight rows (each row also carries a description and a remediation):

idsourceassettitlesevcvsskevexposurecriticality
F001Trivypayments-apiCVE-2021-44228 Log4Shell RCECritical10.0yesinternetcrown-jewel
F002Nessuspayments-apiApache Log4j2 Remote Code ExecutionCritical9.8yesinternetcrown-jewel
F004OWASP ZAPpayments-apiSQL Injection in /orders searchMedium6.1nointernetcrown-jewel
F005Nessusvpn-gatewayCVE-2023-27997 FortiOS heap overflow (pre-auth RCE)Critical9.8yesinternetcrown-jewel
F006Trivydev-sandboxCVE-2024-3094 xz backdoor RCECritical10.0noisolateddev
F007Semgrepjenkins-ciHardcoded AWS secret key in pipelineCritical9.1nointernalstandard
F008OWASP ZAPmarketing-siteSQL Injection (possible)High7.5nointernetstandard
F009Nessuscustomer-dbOutdated OpenSSL 1.0.2High7.5nointernalcrown-jewel

That is what a real export looks like. There are three moves, and you do them in order: kill the noise, merge the duplicates, fix the scores.

The triage funnel: 29 raw findings narrow to 26 units of work, then sort into a prioritized queue of 5 P1, 8 P2, 4 P3, 9 P4.

The whole chapter in one picture. Three passes turn the wall of red into a ranked list, and you work the top.

Move 1: kill the noise

The sea of red is mostly not real. Scanners guess, and a large share of any scan is false positives: a patch applied but invisible to the scanner, a version string that looks vulnerable but was quietly backported, a finding on a service that is not running.

Walk the file and ask one question of each finding: is this actually true, on this actual system, right now? Two in this file are not.

idfindingwhy it is not real
F007Hardcoded AWS secret in the Jenkins pipelineThe string is AKIAIOSFODNN7EXAMPLE, the example key straight out of AWS documentation, used in a smoke test. Not a credential.
F008SQL Injection on marketing-siteThe marketing site is static HTML. No database, no server-side parameters. The scanner saw a query string and guessed.

Kill both. And when you kill one, write down that you killed it, because a false positive you dismiss today comes back on every scan forever unless you record the decision. Log them in removed-before-scoring.csv. A program without a memory re-litigates the same garbage every week.

Twenty-nine findings, two gone. Twenty-seven left.

Move 2: merge the duplicates

The same vulnerability shows up many times: once per host, once per scanner, once per scan. Look at these two:

idscannerassettitle
F001Trivypayments-apiCVE-2021-44228 Log4Shell RCE
F002Nessuspayments-apiApache Log4j2 Remote Code Execution

Same CVE, same host, two scanners, two different titles. That is not two problems. It is one problem with one fix (bump Log4j to 2.17.1 and rebuild), and you work it as one unit. Merge F002 into F001 and note it.

This single move is the difference between a fourteen-thousand-row list and a two-hundred-item list, and it is the highest-leverage thing you do all week. You cannot work fourteen thousand rows. You can work two hundred.

Straight talk. A tool catches the easy half of this for you. Risk Rancher deduplicates on source, asset, and title, so the identical finding landing again on tomorrow’s re-scan never creates a second ticket. What it cannot do is know that Trivy’s “Log4Shell RCE” and Nessus’s “Log4j2 Remote Code Execution” are the same thing, because the titles differ. That judgment is yours. The tool kills the mechanical duplicates; you kill the ones that need a human who knows the two scanners are describing one library.

Twenty-seven findings, one merged. Twenty-six units of work left.

Move 3: fix the scores

Here is the big one, the secret the scanner vendors would rather you not dwell on: the CVSS score on every finding is lying to you. Not out of malice. It is context-free. A CVSS of 9.8 means “this is bad in the abstract.” It has no idea that this instance is on an offline test box nobody can reach, or that this unremarkable Medium is sitting on your public customer database.

So you take the scanner’s severity and bend it with the two things the scanner does not know: how exposed the asset is, and how much you care about it. Here is the rubric. It is deliberately simple, because a rubric you can run in your head on a bad Monday beats a perfect model you never apply.

factorvalues
base (scanner severity)Critical 4, High 3, Medium 2, Low 1
exposureinternet-facing 2, internal 1, isolated 0.5
asset criticalitycrown-jewel 2, standard 1, dev/throwaway 0.5

score = base x exposure x criticality

One override on top: if the finding is on your known-exploited list (your KEV feed), it is a P1 no matter what the math says. The kev column in the sample is not made up; every flag matches the CISA catalog as of October 2026, so you can check it yourself. Something being actively exploited in the wild beats every theoretical number. It is a blunt rule on purpose.

Turn the score into a priority and a clock:

scorepriorityfix within
12 or moreP13 days
6 to 11P214 days
3 to 5P330 days
under 3P490 days

Now watch what the rubric does to two findings that look nothing like their CVSS.

F006, the 9.8 that does not matter. The xz backdoor, CVE-2024-3094, CVSS 10.0, “Critical,” on dev-sandbox. Terrifying header. But the sandbox is an isolated lab box on an air-gapped subnet. Base 4, exposure 0.5, criticality 0.5. Four times a half times a half is 1.0. That is a P4. Patch it when you get to it. A critical on a ghost is not a critical.

F004, the Medium that does. A blind SQL injection on payments-api, scored Medium, CVSS 6.1. The scanner shrugged. But payments-api is internet-facing and holds cardholder data. Base 2, exposure 2, criticality 2. Two times two times two is 8. That is a P2, and it is a direct path to the data that would end your company. A medium on the crown jewels might be your biggest fire.

Contextual scoring quadrant: exposure on one axis, asset criticality on the other. A CVSS 10.0 xz backdoor on an isolated dev box sits bottom-left as a P4; a Medium SQLi on the internet-facing payments database sits top-right as a P2.

The same CVSS number lands in different quadrants. Where a finding lives decides what it is worth.

That is what “context is king” means in arithmetic. Run the number on all 26 and sort descending. Do it in your sheet, or let score.py do the arithmetic; the whole rubric is a few lines, and the two judgment calls the formula cannot make, the false positives and the merge, are written in at the top where you can see them:

BASE = {"Critical": 4, "High": 3, "Medium": 2, "Low": 1}
EXPOSURE = {"internet": 2, "internal": 1, "isolated": 0.5}
CRITICALITY = {"crown-jewel": 2, "standard": 1, "dev": 0.5}

FALSE_POSITIVES = {"F007", "F008"}
DUPLICATE_OF = {"F002": "F001"}

score = BASE[severity] * EXPOSURE[exposure] * CRITICALITY[asset_criticality]
priority = "P1" if kev == "yes" else band(score)   # band: >=12 P1, >=6 P2, >=3 P3, else P4
python3 score.py sample-findings.csv
rank  pri score  id    asset           title
   1  P1     16  F001  payments-api    CVE-2021-44228 Log4Shell RCE (KEV)
   2  P1     16  F005  vpn-gateway     CVE-2023-27997 FortiOS heap overflow (pre-au (KEV)
   3  P1     16  F029  payments-api    CVE-2014-0160 Heartbleed (KEV)
   4  P1     12  F014  payments-api    S3 bucket allows public read
   5  P1      3  F015  jenkins-ci      CVE-2023-44487 HTTP/2 Rapid Reset (KEV)
   6  P2      8  F004  payments-api    SQL Injection in /orders search
   7  P2      8  F016  payments-api    Use of weak hash (MD5) for signature
   8  P2      8  F020  payments-api    IAM user with no MFA
   9  P2      8  F021  vpn-gateway     Deprecated SSL-VPN web mode enabled
  ...
  24  P4      1  F030  internal-wiki   TODO comment left in code
  25  P4   0.75  F019  dev-sandbox     CVE-2023-0286 X.400 type confusion
  26  P4   0.75  F026  dev-sandbox     Security group open to 0.0.0.0/0 on 22

Note rank 5. An internal HTTP/2 denial-of-service you would normally shrug at, floated to P1 by one thing: it is on the known-exploited list. And note the very bottom: a box with SSH open to the entire internet, dead last, because the entire internet cannot route to it. The rubric does not care what the finding is called. It cares where it lives and whether anyone is using it.

Try it. Open triage-sheet.csv and work all 29 findings: mark the false positives, merge the duplicate, score the rest with the rubric, sort by priority. Then check yourself against ranked-queue-answer-key.csv. If your P1s match, you can triage. If a 9.8 is still near your top or a crown-jewel medium is near your bottom, re-read move 3. This is the single most valuable skill in the whole book, and you now have it in your hands, not just your head.

Straight talk. The sorting half of this is the exact work Risk Rancher was built to do, and all of it is free. I ran this very file through it to be sure the book was not selling you a story: the open-source ingest took all 29 rows, created 29 units of work, preserved the severities (7 Critical, 8 High, 8 Medium, 6 Low), and on a second upload of the same file created zero duplicates, because the dedup held. It is not a scanner and will not find anything on its own; you still feed it exports or API data. And it is air-gapped on purpose: it does not phone the CISA KEV catalog or EPSS in real time, because many of the shops that need this most cannot talk to the open internet at all. You bring the threat intel you trust, flag the findings on your known-exploited list, set the adjusted severity on each ticket, and the clock follows from it; nothing leaves your network. The arithmetic above is yours to run, in a sheet or in your head. The tool has no criticality column and does not pretend to know your context; it keeps your answer and does everything downstream of it. Do it in a spreadsheet first, so that when you type a severity into the tool you know exactly why. Chapter nine runs this same file through the tool, end to end.

The output is your program

Sit with what just happened. You started with a wall of red, undifferentiated and paralyzing, and you ended with a short, ordered list where the top item is genuinely the most important thing you could fix today. That list, not the scan, is your vulnerability management program. Everything downstream, every remediation and every metric, runs off it.

So work the top of the list, not the pile. The pile is infinite and it does not care about you. The top of the list is finite, ranked, and real. Which brings us to the next problem, because a prioritized finding is still just a finding until somebody closes it. Next: getting it fixed.

← Back to the blog