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):
| id | source | asset | title | sev | cvss | kev | exposure | criticality |
|---|---|---|---|---|---|---|---|---|
| F001 | Trivy | payments-api | CVE-2021-44228 Log4Shell RCE | Critical | 10.0 | yes | internet | crown-jewel |
| F002 | Nessus | payments-api | Apache Log4j2 Remote Code Execution | Critical | 9.8 | yes | internet | crown-jewel |
| F004 | OWASP ZAP | payments-api | SQL Injection in /orders search | Medium | 6.1 | no | internet | crown-jewel |
| F005 | Nessus | vpn-gateway | CVE-2023-27997 FortiOS heap overflow (pre-auth RCE) | Critical | 9.8 | yes | internet | crown-jewel |
| F006 | Trivy | dev-sandbox | CVE-2024-3094 xz backdoor RCE | Critical | 10.0 | no | isolated | dev |
| F007 | Semgrep | jenkins-ci | Hardcoded AWS secret key in pipeline | Critical | 9.1 | no | internal | standard |
| F008 | OWASP ZAP | marketing-site | SQL Injection (possible) | High | 7.5 | no | internet | standard |
| F009 | Nessus | customer-db | Outdated OpenSSL 1.0.2 | High | 7.5 | no | internal | crown-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 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.
| id | finding | why it is not real |
|---|---|---|
| F007 | Hardcoded AWS secret in the Jenkins pipeline | The string is AKIAIOSFODNN7EXAMPLE, the example key straight out of AWS documentation, used in a smoke test. Not a credential. |
| F008 | SQL Injection on marketing-site | The 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:
| id | scanner | asset | title |
|---|---|---|---|
| F001 | Trivy | payments-api | CVE-2021-44228 Log4Shell RCE |
| F002 | Nessus | payments-api | Apache 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.
| factor | values |
|---|---|
| base (scanner severity) | Critical 4, High 3, Medium 2, Low 1 |
| exposure | internet-facing 2, internal 1, isolated 0.5 |
| asset criticality | crown-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:
| score | priority | fix within |
|---|---|---|
| 12 or more | P1 | 3 days |
| 6 to 11 | P2 | 14 days |
| 3 to 5 | P3 | 30 days |
| under 3 | P4 | 90 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.
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.