TL;DR
CVE feeds are a firehose by design. The only thing that turns them into decisions is a second list: your assets, with version, exposure, and criticality. A vulnerability record that doesn’t answer “do I run this, where, and how much would it hurt” is noise. An asset inventory is the join key between “the world broke something” and “here is what I do about it.” ISO 27001 calls it A.5.9. Practitioners who skip it end up prioritizing patches by CVSS score and wondering why their top ten fixes moved their risk profile by nothing.
This is the post I should have written before the priority posts. Every other piece of vulnerability management — EPSS, KEV matching, SLAs, risk treatment — consumes the inventory as input. If the input is wrong or stale, the output is confidently wrong.
What the inventory is for
A scanner tells you a thing: “you have CVE-2026-XXXX on this host.” A CISA KEV feed tells you another: “this CVE is being exploited in the wild.” The multiplication happens in your head, and your head needs:
- Do we run the affected component? (name, version, configuration)
- Where does it sit? (internet-facing, DMZ, internal, lab, decommissioning)
- What would compromise give an attacker? (data sensitivity, access level, blast radius)
Only the answer to all three produces a priority. A CVSS 9.8 on a box in an isolated lab with no valuable data is a Tuesday. A CVSS 5.4 in the internet-facing component that holds your customer PII is an incident waiting to happen. The numbers alone don’t carry this distinction. The inventory does.
The minimum record
You don’t need a data center of metadata. But a record that can’t survive a real question is decoration. For every asset, at minimum:
| Field | Why it’s load-bearing |
|---|---|
| Identifier (host, app, or service name) | Stable key for joining CVE data |
| Vendor + component + version | The join key to CVE/CPE data. “NetScaler” is not a version |
| Owner | Who knows whether it’s still there, and who to call |
| Exposure | Internet / internal / segmented. This is your biggest lever |
| Criticality / data sensitivity | What the attacker gets on the other side |
| Status | Live, decommissioning, archived — and when |
| Source of truth | Where the version came from (asset tag, CMDB, scanner, package DB) |
The version field is where inventories die. “PostgreSQL 16” is not specific enough to match an advisory that says “before 16.7, patched in 16.7.1.” If your inventory holds minor-and-patch-level versions, CPE and advisory matching starts to work. If it holds product names, you’re doing a keyword search and accepting a wall of false positives.
Why inventories rot
Every inventory fails the same way: drift, not error. Nobody breaks the inventory on purpose. It decays:
- New things appear with no process to add them — the side project, the staging copy, the “temporary” load balancer.
- Things disappear with no process to remove them — the project ends, the host is wiped, and the row stays forever green.
- Versions move under you — patching changes the version, and the inventory still says the old one.
- Ownership drifts — the owner leaves, the row has no owner, and nothing gets fixed.
The practical consequence: your inventory is only as current as the worst of “who last updated it.” Manual inventories converge on “accurate in 2023.” The fix is not discipline; it’s making the inventory a derivative artifact where you can, and a reconciled artifact where you can’t.
Building coverage without pretending to completeness
The trap is thinking you need 100% inventory coverage before the inventory is useful. You don’t. You need the inventory to answer “what’s exposed and what matters” better than you did yesterday. In practice, coverage comes from triangulating at least three sources:
- Declared assets. The CMDB, the app register, the cloud console, the vendor dashboards. This is what people say they run.
- Observed assets. Scanner output, DNS zones, port scans, cloud APIs enumerating real instances and load balancers. This is what’s actually there.
- Consumed assets. Dependency manifests, SBOMs, container images, package databases. This is what your software pulls in — the part that never appears in any app register.
The diff between declared and observed is your shadow IT list — usually more interesting than the union. The consumed list is where most of the modern attack surface lives, and it’s the only source that catches the component nobody knows you’re running because it’s buried three layers inside a vendor appliance or a Docker image.
A useful heuristic: for the components that matter to you, if no source says what version is running, you don’t have an inventory entry — you have a guess.
CPE matching: the join you keep getting wrong
If you match CVEs against assets with CPE strings (as NVD does), two things bite everyone:
- CNA CPEs are sloppy. Vendor-published CPE strings often use wildcards (
*) on version ranges, omit configuration qualifiers, or describe the product line rather than the build. The NVD-enriched versions are better but still imperfect. - Version matching is a range problem, not a string problem. “Affected: before 16.7.1” means every 16.x below the patch level. A naive string match either misses the asset or matches the fixed one.
The working pattern is: normalize your own versions into real version strings (with major/minor/patch), match CVEs by vendor+product first, then apply the advisory’s version-range logic as a second step. Where the advisory is ambiguous, record the ambiguity instead of resolving it silently — “unknown whether FIPS variant is covered” is a legitimate inventory state.
The feed-filter pattern
Here’s the pattern that actually keeps a security feed small enough to read, and I’d encourage you to copy it regardless of what vendor feeds you consume:
Maintain a relevance catalog — a list of the vendors, components, and product lines you actually run (the declared and consumed lists, collapsed to terms), plus a list of vulnerability classes that matter to you even without an exact asset match (auth bypass, IDOR, SSRF, deserialization, request smuggling, unauthenticated RCE with public exploit).
Then every feed item gets reduced through one pipeline:
CVE / advisory → affected component → matches our catalog?
→ yes: why it matters here → action
→ no: does it match a high-signal class or active exploitation?
→ yes: keep, low priority
→ no: suppress
The output is a daily list of zero to a handful of items, each with a reason it survived. Most days the correct output is nothing. A feed that needs to say something every day is telling you about itself, not about your risk.
The same catalog has a second job: it’s what an audit or an incident asks for. When a KEV entry lands on something you run, the catalog tells you which assets to check in the next ten minutes instead of the next two days.
Maintenance: the part everyone skips
An inventory with no maintenance cadence is a snapshot of last quarter. The minimum discipline that keeps one alive:
- Derive what you can. If your CI knows the versions, the scanner knows them, or the cloud API knows them, don’t hand-type. Auto-collect on a schedule; keep the human fields (owner, criticality, exposure) as the manual layer.
- Reconcile on a calendar. Quarterly, diff declared vs. observed. Every diff is either a fix to the inventory or a fix to reality. Both are wins.
- Decommission loudly. The “status: decommissioning, as of ” state is not bureaucracy — it’s what lets an old row stop polluting your priority queue.
- Treat the diff as the product. The most valuable thing an inventory produces is not the current-state report. It’s the list of things that appeared, disappeared, or moved since last check. That list is your exposure delta, and it’s where surprises live.
The compliance angle, briefly
If you run ISO 27001, A.5.9 (“an inventory of information and other associated assets”) is this requirement, and auditors probe exactly the places where inventories rot: shadow systems, stale versions, unowned entries. The control is not “we have an asset register PDF.” It’s “we can demonstrate the register reflects what is actually running, with a defined refresh process.” Same story under NIST 800-53 (CM-8), and the same story under DORA’s requirement that financial entities know their ICT dependency map. The compliance mapping is easy. The operational work is what makes or breaks it — and it’s the same operational work you’d need for vulnerability triage anyway. Do it once, use it everywhere.
Common failure modes
- The spreadsheet of hope. Accurate the day it’s built, unowned by day 90. No source of truth, no refresh, no owner field that means anything.
- The version-less inventory. “We run GitLab.” Which version? Which edition? What’s the patch level? The moment a real CVE hits, you spend a day finding out — the day you wanted to spend patching.
- The internal-only inventory. Every internal system, zero exposure data. You can’t prioritize without knowing what’s reachable from where, and an inventory with no exposure field forces every triage back to “check the firewall,” which is slower than the threat model.
- The 100% coverage obsession. Chasing every USB hub and forgotten laptop before the internet-facing criticals are properly catalogued. Get the high-value, high-exposure slice right first; breadth comes second.
- The inventory without an action. A register nobody reads is a document. The moment an advisory or KEV entry lands, the inventory has to produce a shortlist of assets to check. If it can’t, it’s not wired into your workflow.
What I actually do
Strip out the confidential details and the pattern is:
- A catalog of hosts and services with versions, listening ports, and detected tech, collected automatically on a schedule.
- A normalized allowlist of vendor/product terms derived from that catalog — the relevance filter.
- A separate list of high-signal vulnerability classes that survive even without an asset match.
- A daily reduction step: feed → match → reason → action-or-suppress, with silence as a normal, preferred outcome.
The output is small by construction. The day a critical KEV lands on something in the catalog, the “which of our boxes?” question has an answer before the question is finished. That’s the entire point.
Takeaways
- A vulnerability is only a priority when joined to an asset. The join needs component, version, exposure, and criticality — all four.
- Inventories die by drift, not error. Derive what you can, reconcile on a calendar, and decommission loudly.
- Triangulate declared, observed, and consumed assets. The diffs are your shadow IT.
- Filter feeds through a relevance catalog. A daily list of zero to a handful of reasoned items beats a firehose of CVE IDs.
- The diff between two inventory states is more valuable than either state.
The scanner, the EPSS score, and the KEV entry are all inputs. The inventory is the only one of them you control. Make it honest and the rest of the machinery starts producing decisions instead of noise.
Further reading
- CISA Known Exploited Vulnerabilities catalog — the “actively exploited” signal, and the reason KEV matching is a first-class join key
- ISO/IEC 27001:2022, A.5.9 — Inventory of information and other associated assets
- NIST SP 800-53, CM-8: Security and Privacy Software, Firmware, and Configuration Settings — the configuration-management side of the same problem
- First.org EPSS — exploitation probability as a prioritization input, still only meaningful against known assets
- NVD CPE dictionary — what the version-join problem actually looks like in practice