Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowMicrosoft's June 2026 Patch Tuesday delivered a stark message: the era of human-scale vulnerability management is over. With a staggering 206 Common Vulnerabilities and Exposures (CVEs) addressed, including 6 zero-days and 33 critical flaws, security teams globally faced the largest single patch release on record. The sheer volume of vulnerabilities now entering the ecosystem demands a radical shift in how organisations prioritise and respond.
For years, the Common Vulnerability Scoring System (CVSS) has been the bedrock of vulnerability prioritisation. It provides a standardised, numerical representation of a vulnerability's severity. However, CVSS, in isolation, tells only part of the story. It describes the potential impact of a flaw, not its actual risk within a specific operational context.
Two additional data sources have emerged to fill the gap, yet both remain underutilised. The Exploit Prediction Scoring System (EPSS) uses real-world exploit intelligence to estimate the probability that a vulnerability will be exploited in the wild within 30 days. EPSS scores range from 0 to 1 (or 0% to 100%). A CVE with an EPSS score above 0.9 is virtually certain to be weaponised; one below 0.01 is rarely observed in active attacks. The CISA Known Exploited Vulnerabilities (KEV) catalogue tracks vulnerabilities that have been confirmed as actively exploited, providing a binary signal: this flaw is being used in the wild right now.
Even armed with CVSS, EPSS, and CISA KEV, security teams still face a combinatorial explosion. A typical enterprise runs hundreds of applications, each depending on thousands of open-source and commercial components. A single critical CVE in Log4j, OpenSSL, or curl can ripple through 80% of an organisation's software estate. Patching everything with a CVSS ≥ 7.0 is physically impossible inside a single patch window. Teams need to answer three questions for every CVE:
CVSS cannot answer these questions. A knowledge graph can.
To move beyond the limitations of isolated CVSS scores, organisations must adopt a systemic view of their infrastructure. This is where knowledge graphs excel. By modelling your entire software landscape as a property graph, you can represent the intricate relationships between components, services, and data flows.
In this model:
DEPENDS_ON, EXPOSES, CONNECTS_TO, HOSTS, RUNS_ON, ACCESSES, STORES_DATA, etc.This interconnected web allows for a dynamic calculation of "blast radius" — the full extent of systems and data reachable from a given vulnerability. A high-severity CVE in a rarely used, isolated internal component might have a tiny blast radius, while a moderate-severity flaw in a core, internet-facing library could expose your entire critical path.
Before querying, you need a schema. The following Neo4j constraints and indexes enforce data integrity and enable performant traversal:
// Uniqueness constraints — every entity must be identifiable
CREATE CONSTRAINT IF NOT EXISTS FOR (c:CVE) REQUIRE c.id IS UNIQUE;
CREATE CONSTRAINT IF NOT EXISTS FOR (a:Application) REQUIRE a.name IS UNIQUE;
CREATE CONSTRAINT IF NOT EXISTS FOR (h:Host) REQUIRE h.hostname IS UNIQUE;
// Indexes for fast filtering on commonly queried properties
CREATE INDEX IF NOT EXISTS FOR (c:CVE) ON (c.cvssScore);
CREATE INDEX IF NOT EXISTS FOR (c:CVE) ON (c.epssScore);
CREATE INDEX IF NOT EXISTS FOR (c:CVE) ON (c.kevPublished);
CREATE INDEX IF NOT EXISTS FOR (a:Application) ON (a.businessCriticality);
CREATE INDEX IF NOT EXISTS FOR (a:Application) ON (a.isInternetFacing);
The ontology defines four core node labels:
| Label | Purpose | Key Properties |
|---|---|---|
CVE | A known vulnerability identifier | id, cvssScore, epssScore, kevPublished, description, publishedDate |
Component | A software library, package, or OS | name, version, type, language, license |
Application | A business application or service | name, businessCriticality, isInternetFacing, handlesSensitiveData, owner |
Host | A physical or virtual machine | hostname, ipAddress, environment, region, os |
And the edges that connect them:
| Relationship | From | To | Semantics |
|---|---|---|---|
IMPACTS | CVE | Component | This CVE affects this component version |
DEPENDS_ON | Application | Component | Application uses this component (directly or transitively) |
RUNS_ON | Application | Host | Application is deployed on this host |
EXPOSES | Host | Application | Host exposes this application to the network |
MITIGATED_BY | CVE | Component | A compensating control or patch exists |
CONNECTS_TO | Host | Host | Network connectivity between hosts |
This schema captures the minimum viable graph for vulnerability prioritisation. In production, you would extend it with additional labels for network segments, data classifications, and business processes.
Consider a scenario where a newly disclosed CVE impacts a specific software component or library. With a knowledge graph, you can quickly identify all applications and services that directly or indirectly depend on this vulnerable component, and critically, whether those dependent services are exposed or handle sensitive data.
Here is a Cypher query example for Neo4j, demonstrating how to find all applications that depend on a component identified as vulnerable:
MATCH (v:Vulnerability {cveId: 'CVE-2026-XXXX'})
WHERE v.isExploitable = TRUE -- Only consider actively exploitable vulnerabilities
MATCH (v)-[:IMPACTS]->(comp:Component)
WITH comp
MATCH (app:Application)-[:DEPENDS_ON*1..5]->(comp) -- Traverse up to 5 levels of dependency
WHERE app.isInternetFacing = TRUE OR app.handlesSensitiveData = TRUE
RETURN DISTINCT app.name AS VulnerableApplication,
app.businessCriticality AS BusinessCriticality,
collect(DISTINCT comp.name) AS ImpactedComponents
ORDER BY app.businessCriticality DESC
This query does more than just list dependencies. It:
DEPENDS_ON*1..5) to find all applications relying on that component, up to five levels deep.For continuous vulnerability monitoring, automate the prioritisation pipeline:
from neo4j import GraphDatabase
from dataclasses import dataclass, field
@dataclass
class PrioritisedVulnerability:
cve_id: str
cvss_score: float
blast_radius_score: float
affected_apps: list[str]
priority: str
def prioritise_vulnerabilities(driver) -> list[PrioritisedVulnerability]:
"""Score and rank vulnerabilities by contextual risk."""
with driver.session() as session:
results = session.run("""
MATCH (v:Vulnerability)-[:IMPACTS]->(comp:Component)
MATCH (app:Application)-[:DEPENDS_ON*1..3]->(comp)
WITH v, comp, app,
CASE
WHEN app.isInternetFacing AND app.handlesSensitiveData THEN 3
WHEN app.isInternetFacing THEN 2
WHEN app.handlesSensitiveData THEN 1
ELSE 0
END AS exposure_score
RETURN v.cveId AS cve_id,
v.cvssScore AS cvss_score,
sum(exposure_score) AS blast_radius,
collect(DISTINCT app.name) AS affected_apps,
CASE
WHEN v.cvssScore >= 9.0 AND sum(exposure_score) >= 2 THEN 'CRITICAL'
WHEN v.cvssScore >= 7.0 AND sum(exposure_score) >= 1 THEN 'HIGH'
WHEN v.cvssScore >= 4.0 THEN 'MEDIUM'
ELSE 'LOW'
END AS priority
ORDER BY blast_radius DESC, v.cvssScore DESC
""")
return [PrioritisedVulnerability(**record.data()) for record in results]
This pipeline combines CVSS severity with contextual blast radius to produce a single prioritised list. A CVE with CVSS 9.0 in an isolated internal tool drops to MEDIUM priority, while a CVSS 6.5 in an internet-facing customer data service rises to CRITICAL.
Quantifying blast radius is one thing. Finding the shortest exploitable path from an attacker-accessible vulnerability to a critical asset is another level of analysis entirely. The following Cypher query chains together reachability, network connectivity, and data sensitivity to surface the most dangerous attack paths:
// Find the shortest exploitable path from an internet-facing
// vulnerability to a crown-jewel data store
MATCH (cve:CVE)
WHERE cve.cvssScore >= 7.0
AND cve.epssScore > 0.5
AND cve.kevPublished IS NOT NULL
// Step 1: Which components are affected by this CVE?
MATCH (cve)-[:IMPACTS]->(comp:Component)
// Step 2: Which internet-facing applications depend on those components?
MATCH (app:Application)-[:DEPENDS_ON*1..5]->(comp)
WHERE app.isInternetFacing = TRUE
// Step 3: What hosts run those applications?
MATCH (app)-[:RUNS_ON]->(host:Host)
// Step 4: Can attackers traverse from the exposed host to a sensitive asset?
MATCH path = shortestPath(
(host)-[:CONNECTS_TO*1..10]->(target:Host)
)
WHERE ANY (tapp IN [(target)<-[:RUNS_ON]-(tapp:Application)
WHERE tapp.handlesSensitiveData = TRUE]
WHERE true)
RETURN cve.id AS Vulnerability,
app.name AS EntryPoint,
host.hostname AS ExposedHost,
length(path) AS NetworkHopsToTarget,
nodes(path) AS AttackPath,
cve.cvssScore AS CVSS,
cve.epssScore AS EPSS
ORDER BY cve.epssScore DESC, cve.cvssScore DESC
LIMIT 20
This query does not just list vulnerable applications — it traces the actual network path an attacker would follow. The result set tells a security team: "These 20 CVEs represent the shortest paths from the internet to your sensitive data." Each row is a concrete, prioritised remediation target with full provenance.
CVSS tells you how severe a vulnerability is. EPSS tells you how likely it is to be exploited. A knowledge graph can combine both with blast radius context to produce a composite risk score:
// Risk-weighted vulnerability prioritisation
// Score = CVSS × EPSS × blast_radius_multiplier
MATCH (cve:CVE)
WHERE cve.epssScore > 0.1 // Filter out negligible exploit probability
MATCH (cve)-[:IMPACTS]->(comp:Component)
MATCH (app:Application)-[:DEPENDS_ON*1..5]->(comp)
WITH cve, app,
CASE
WHEN app.isInternetFacing = TRUE AND app.handlesSensitiveData = TRUE THEN 3.0
WHEN app.isInternetFacing = TRUE THEN 2.0
WHEN app.handlesSensitiveData = TRUE THEN 1.5
ELSE 1.0
END AS blastRadiusMultiplier
RETURN cve.id AS CVE,
cve.cvssScore AS CVSS,
cve.epssScore AS EPSS,
round(cve.cvssScore * cve.epssScore * blastRadiusMultiplier, 2) AS CompositeRisk,
app.name AS AffectedApp,
app.businessCriticality AS Criticality
ORDER BY CompositeRisk DESC
LIMIT 25
This approach directly addresses the inefficiency described earlier. A CVSS 9.0 vulnerability with EPSS 0.01 (1% exploit probability) in a non-internet-facing internal tool receives a composite risk score of just 0.09 — far lower than a CVSS 6.0 vulnerability with EPSS 0.9 in an internet-facing customer portal (composite risk 16.2). The graph does not just store data — it computes actionable intelligence.
Vulnerability data changes constantly. CVSS scores are revised. EPSS scores update daily. New exploits appear in the CISA KEV catalogue. Patches are released. Your graph schema must model this temporality:
// Track when vulnerability metadata was last updated
MATCH (cve:CVE {id: 'CVE-2026-XXXX'})
SET cve.lastSeen = datetime()
SET cve.epssScore = 0.87 // Updated EPSS score
SET cve.kevPublished = datetime('2026-07-01') // Now in KEV catalogue
// Log the change for audit
CREATE (change:CVEChangeLog {
cveId: cve.id,
changedAt: datetime(),
previousEpss: 0.12,
newEpss: 0.87,
reason: 'Proof-of-concept exploit published'
})
By maintaining a change log as graph nodes, you can query vulnerability drift over time: "Which CVEs in our environment saw the largest EPSS increase in the past week?" This turns a static vulnerability database into a living risk surface.
The shift to a graph-based approach fundamentally alters how security teams operate:
| Feature | Traditional CVSS Prioritisation | Knowledge Graph + CVSS Prioritisation |
|---|---|---|
| Coverage | Limited to individual component context | Full dependency chain, blast radius, reachability |
| False Positives | High, treats all high-CVSS as equally urgent | Lower, focuses on reachable, exploitable, and high-impact |
| Remediation Speed | Slower, reactive, broad, untargeted patching | Faster, proactive, targeted patching based on actual risk |
| Team Alignment | Siloed, security vs. operations/development | Collaborative, shared context for risk and impact |
| Context | Isolated vulnerability score | Systemic risk, business impact, attack path analysis |
| Decision Basis | Severity-driven | Risk-driven (severity + context + impact) |
Imagine facing Microsoft's June 2026 Patch Tuesday with a knowledge graph in place. Instead of a flat list of 206 CVEs, your security team receives an intelligence report:
This level of granular, context-aware prioritisation transforms an unmanageable deluge into an actionable, defensible plan. It empowers teams to focus resources on the vulnerabilities that truly matter to their unique risk posture, rather than chasing every high-CVSS score.
Constructing a vulnerability knowledge graph from scratch may sound daunting, but the incrementally valuable nature of knowledge graphs means you start small and expand. Here is a phased approach:
Start by modelling your application-to-component dependencies. This alone captures the most critical dimension: what depends on what. Use your existing SBOM (Software Bill of Materials) tooling or package lock files as the data source:
# Example: Generate SBOM from npm using CycloneDX
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-format JSON > sbom.json
Write a simple ingestion script that parses the SBOM and creates (:Application)-[:DEPENDS_ON]->(:Component) relationships in Neo4j. At this stage, you can already answer: "Which of our applications would be affected by a Log4j-style zero-day?"
Ingest the National Vulnerability Database (NVD) feed and filter for CVEs affecting your inventoried components. The NVD provides a JSON data feed updated every two hours. Augment each CVE node with EPSS scores from FIRST's daily API:
# Example: Fetch EPSS score for a CVE
import requests
def get_epss_score(cve_id: str) -> float:
resp = requests.get(
f"https://api.first.org/data/v1/epss?cve={cve_id}"
)
data = resp.json()
return float(data["data"][0]["epss"])
Create (:CVE)-[:IMPACTS]->(:Component) relationships and populate epssScore, cvssScore, and kevPublished properties. You now have a graph that can answer: "Which of our CVEs are being actively exploited right now?"
Add your infrastructure layer: (:Host) nodes, (:Application)-[:RUNS_ON]->(:Host), and (:Host)-[:CONNECTS_TO]->(:Host) relationships. Network connectivity data can be sourced from your configuration management database (CMDB), cloud provider APIs, or network scanning tools.
This unlocks attack path analysis: "Show me every internet-accessible host running a component with a known-exploited CVE, and trace every network path to a database with sensitive data."
The final phase connects the graph to your incident response workflow. Configure periodic ingestion jobs that:
CVE nodeskevPublished on matching CVEsAt this stage, your knowledge graph runs continuously, providing a ranked, context-aware vulnerability backlog that updates in lockstep with the threat landscape.
As AI-assisted bug finding tools become more sophisticated and widespread, the rate of vulnerability discovery will only accelerate. The "AI patch tsunami" is not a one-off event but the new normal. Relying solely on static severity scores is no longer sustainable; it is a recipe for burnout and increased organisational risk.
Knowledge graphs for vulnerability prioritisation are shifting from a "nice-to-have" capability to an operational necessity. By providing a living, interconnected map of your digital estate, they empower security teams to identify, understand, and mitigate risk with unparalleled precision and speed. The phased approach — starting with SBOM ingestion, layering in EPSS and CISA KEV intelligence, adding network topology, then automating the pipeline — means any team can begin realising value within a week and scale incrementally from there.
The architecture is proven. The tooling is mature. The data sources are freely available. The question is no longer whether knowledge graphs improve vulnerability prioritisation — it is whether your organisation can afford to keep prioritising without one. The time to build your dependency graph is now.