River
Y CombinatorBacked by Y Combinator
FREE TEMPLATE

Vulnerability Management Process Template

A finding register that keeps what each scanner reported beside the severity your own environment gives it, plus the procedure behind it.

Free download  ·  No account needed

Triage Procedure

How this team decides what counts as work

Ten questions, answered once. Every register in the pack reads these.

1. What is in scope, and what is not

The repositories, images, hosts and cloud accounts anybody scans, and the ones deliberately left out. A finding on an asset nobody agreed to scan is a scope question rather than a severity question, and it gets answered here instead of in the queue.

2. Where the exports come from

Each tool by name, what it actually covers, and which of them overlap. Two tools covering one estate is the ordinary case and the reason the register collapses rows.

3. Severity vocabulary

The four or five words this team uses and what each one means here. Not the scanner's words: a scanner reports whatever its advisory source said. This is the vocabulary the reconciled column is checked against, and until it exists the register has nothing to reconcile to.

4. The clock, and what starts it

Days to remediate per severity, whether the clock starts at publication, at first detection or at triage, and which date wins when a published exploitation deadline is shorter than your own. One sentence, decided in advance, because it is decided badly under pressure.

5. What counts as reachability evidence

The forms of proof that justify a downgrade: a lockfile section naming a build-only dependency, a call-path analysis that found no path, a check of what is in the deployed artifact. Anything else is an opinion with a green cell next to it.

6 to 10

Who may accept a risk and up to what severity, the longest an exception may run, what a compensating control has to look like to count, who verifies a fix and how, and where a finding on somebody else's system goes.

A scanner's severity column looks like a property of the vulnerability. It is a property of which database the scanner asked, and in what order. Trivy resolves severity from the distribution's own advisory first, from GitHub's database for GHSA identifiers, and from the National Vulnerability Database only as a fallback. The number is also a base score, which the specification names CVSS-B and hands to you unfinished: applying the threat and environmental metrics is the consumer's job.

The same vulnerability then arrives several times over, and the tools say why. Grype's identity for a finding is the vulnerability id, the advisory namespace it came from, and the package. Change the namespace and the identity changes, so one CVE in one package sourced from two feeds is two rows inside a single scan, before a second scanner is involved. Deduplicate on the CVE and the package at the version present, per asset, and keep the losing row: which tools saw a finding is a question somebody asks later.

So the register carries two severity columns. One transcribes what each tool reported, vector and scanner name attached; the other holds what survives asset criticality, reachability and observed exploitation, with a third column stating why they differ. Where nothing in your inputs says whether the vulnerable code is even called, reading it out of the codebase is the input, and a downgrade that changes the architecture earns its own decision record. Hand River the export nobody has opened, or start from the blank documents and registers.

One export, reconciled, and the four things actually due

The Finding Register, the queue it produces, and the Exception Register that holds everything nobody is going to fix.

Finding Register

Illustrative rows for a fictional payments company, Ardenfield. Every published value is real.

CVEPackage, assetReportedReconciledReconciliationExploitedEPSS
CVE-2021-44228log4j-core 2.14.1
payments-api, internet facing
Critical 10.0
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
criticalagreesYes, due 2021-12-240.99999
CVE-2021-44228log4j-core 2.14.1
payments-api, second scanner
Critical 9.3
AV:N/AC:M/Au:N/C:C/I:C/A:C
closedduplicate, same CVE and packageYes0.99999
CVE-2021-44228log4j-core 2.14.1
design-tokens-build, build only
Critical 10.0lowlowered: not reachableYes0.99999
CVE-2010-0738JBoss JMX-Console
ops-console, internal
Medium 5.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
criticalraised: on the exploited catalogYes, ransomware0.79415
CVE-2015-2328pcre3
edge-proxy
low (distro)
7.5 (national database, v2)
lowscanners disagree, both correctNo0.05244
noneweak cipher suites
edge-proxy, plugin finding
High
no vector
unknownno severity reportedNon/a

Row four is why the register exists. A base score of 5.3 and confirmed exploitation, with ransomware campaigns attached. Sorted by reported severity it sits below every High in the export. Sorted by evidence it is near the top, and the date it is due comes from the exploitation catalog rather than from the severity table.

Row five is one CVE with two correct answers. The distribution rates it low for this package, the national database holds only a version 2 score of 7.5, and neither tool is misreporting. A single severity column has to silently pick one; two columns and a reason keep the disagreement where somebody can see it.

Reported severity, reconciled severity, and why they differ. Overwrite the first with the second and the audit trail is gone.

What is actually due

The same export, ordered by evidence of exploitation rather than by the number the scanner printed.

#FindingReportedClockWhy it is here
1CVE-2021-44228, payments-apiCritical 10.0exploitation catalogConfirmed exploitation, reachable on a request path, internet facing. Nothing about this one is a judgement call.
2CVE-2010-0738, ops-consoleMedium 5.3exploitation catalogRaised. Confirmed exploitation with ransomware use, on an asset holding customer records.
3CVE-2015-2328, edge-proxylowinternal SLAA probability of 0.05 reads as negligible and sits at the 92nd percentile, because most vulnerabilities score near zero.
4Weak ciphers, edge-proxyHighinternal SLANo CVE and no vector, so it cannot be sorted at all until somebody gives it a level. It is a half-hour of work.
-411 remaining findingsHigh and Criticalinternal SLADeduplicated, reconciled, and not due this week. The reasons are in the register rather than in somebody's memory.

The reordering is not a preference, it is measurable. Ask the national database how it rates the CVEs on the exploited catalog and the answer is 486 Critical, 662 High, 135 Medium and 3 Low, with a further 409 carrying no version 3 rating at all. So 547 vulnerabilities under confirmed attack sit at Medium, Low or unrated.

Absence from the catalog is not evidence of safety. Log4Shell's follow-up CVE was added to the same catalog roughly seventeen months after the original entry, with ransomware use attached. A process that reads a missing entry as a clean bill of health is late by exactly that margin.

Four items to work, not four hundred, and a written reason for every row that is not on the list.

Exception Register

Every finding nobody is going to fix, with the date the decision stops being valid.

ReasonCompensating controlApproved byExpiresRenewalsState
not reachable in this deploymentnone neededengineering manager2026-12-040active
fix requires a major framework upgradeedge rule blocking the exploit pathhead of engineering2026-11-011active
vendor disputes the findingnonesecurity lead2026-10-150active
risk accepted, system due for decommissionnoneCTO2026-09-300active
access restricted to the office networknetwork rulesecurity lead2026-06-300expired

The last row is the one an audit finds. The date passed, nobody renewed it, and the finding underneath is open with no owner and no acceptance. A register without an expiry column cannot produce that state at all, which is why every exception in one looks permanently valid.

The renewal count is deliberately visible. A risk accepted twice is a decision to carry it permanently, filed as though it were a deferral. Saying that out loud, in a column, changes the answer more often than the conversation does.

Row four is legitimate exactly once. An exception granted because a system is being switched off expires on the decommission date, so when the decommission slips the exception dies first and somebody has to approve it again.

An exception with no expiry is a decision never to fix it, recorded as a deferral.

What's in the pack

01

Finding Register

One row per finding, deduplicated across tools, carrying the severity each scanner reported beside the one your environment gives it and the reason they differ. Scanner output only: a human-authored assessment is a different input with different failure modes, and it has its own response pack.

02

Remediation Tracker

One row per piece of work rather than per finding, because a single dependency upgrade closes eleven rows, and a configuration change closes one no rescan can confirm.

03

Exception Register

Every accepted risk with its compensating control, its approver, its renewal count and the date it stops being valid. Expired is a queue, not an archive.

04

Triage Procedure

The ten questions a team answers once: what is in scope, the severity vocabulary, when the clock starts, and what evidence justifies a downgrade.

05

Reading a Reported Severity

Why an export is mostly Criticals, read off the specification's own defaults, and what confirmed exploitation, predicted exploitation and reachability each do to the order.

06

Remediation Plan

The plan the register produces: work items with owners and dates, grouped so one upgrade appears once, with capacity stated rather than assumed.

07

Risk Acceptance Format

What an acceptance has to say to be worth filing, including who is allowed to sign it, what the control is, and when it expires.

08

Worked Finding CVE-2021-44228

One finding taken end to end with real published values: three scanners, two numbers, three assets, one downgrade, and the follow-up CVE that arrived seventeen months later.

How to use it

  1. 1

    Open in River, or take it blank

    Install the pack in River and point it at your scanner exports, or take the documents and registers and work through them yourself.

  2. 2

    Answer the procedure first

    The severity vocabulary, the clock and what counts as reachability evidence. Until those three are settled the register has nothing to reconcile against.

  3. 3

    Send every export you have

    Snyk, Dependabot, Trivy, Nessus, a pentest findings table, or all of them over the same estate. Overlap is the point rather than a problem.

  4. 4

    Work the short queue

    What is due, ordered by evidence of exploitation rather than by score. The weekly sweep re-checks every open finding and reports what changed.

Frequently asked questions

Is this template free?

Yes, and free means free: no account, no card, no email gate on the download. Edit with AI is the half you opt into, where the agent takes the exports you already have, collapses the rows describing one vulnerability and fills the register. The rest of the template library works the same way.

What format are the downloaded files?

Five .docx documents and three .csv registers in one zip. Every register column is plain text, so a scanner export pastes in beside the illustrative rows with no import step, and the whole thing opens in Excel, Numbers, Google Sheets, Word or Pages.

Our scanner says Critical. Why would the register say low?

Because a published score assumes the worst case about your environment, and it says so. The working group behind the standard states that base scores are not risk and should not be used alone for patch prioritisation. A downgrade still has to carry its evidence, which is the one rule this space enforces.

Two scanners gave us different severities for one CVE. Which is right?

Both, usually. Trivy's own source records that the distribution rates CVE-2015-2328 unimportant for one package and low for another, while the national database holds only a version 2 score of 7.5 for it. The register keeps every reported severity with the tool that reported it.

Can it tell us what is actually due this week?

Yes, ordered by confirmed exploitation rather than by score, which moves rows a long way. Of the vulnerabilities on the exploited catalog, 135 are rated Medium and three are rated Low, so a queue sorted by severity buries them under hundreds of Highs nobody has attacked.

What happens to the findings we are never going to fix?

They get a row in the Exception Register with a compensating control, a named approver and an expiry date, so the decision resurfaces instead of ageing quietly. If one is later exploited, the incident postmortem has the acceptance and its reasoning already written down. It is also the row a customer's security questionnaire makes you defend.

Our scan output cannot leave our network. Can we still use this?

Yes. A private AI workspace installs this pack inside your own tenancy, so asset names, exploit paths and reachability evidence never cross a boundary your security review has not already approved. Nothing about the registers or the weekly sweep changes. The secure code review checklist installs the same private way, for findings a reviewer catches before code ships at all.

Find out which findings are actually work

Download the documents and registers and fill them in yourself, or open the pack in River and point it at the export nobody wants to read.

Edit with AI