Cyber threat graph, attack-path analysis

Find every path from a public-facing asset to crown-jewel systems through chained vulnerabilities

All recipes· graph· 10 minutesadvancedcypher

Cyber threat graph, attack-path analysis

Objective

Modern attacks chain low-severity vulnerabilities across many hops to reach high-value targets. Defenders need to ask "from any internet-facing asset, can an attacker reach the customer database?" Modeling assets, network reachability, and CVEs as a graph reduces this to a path search — the basis of tools like BloodHound and Microsoft Defender ASM.

Step 1: Create the graph

// Public-facing assets
MERGE (web:Asset {name: "web-prod-01",   tier: "public",    crown_jewel: false})
MERGE (api:Asset {name: "api-gw-02",     tier: "public",    crown_jewel: false})

// Internal services
MERGE (auth:Asset    {name: "auth-svc-03",   tier: "internal", crown_jewel: false})
MERGE (jobs:Asset    {name: "jobs-svc-04",   tier: "internal", crown_jewel: false})
MERGE (cache:Asset   {name: "cache-05",      tier: "internal", crown_jewel: false})

// Data tier — the crown jewels
MERGE (custdb:Asset  {name: "customers-db",  tier: "data", crown_jewel: true})
MERGE (paydb:Asset   {name: "payments-db",   tier: "data", crown_jewel: true})
MERGE (vault:Asset   {name: "secrets-vault", tier: "data", crown_jewel: true})

// Workstations (lateral-movement waypoints)
MERGE (admin:Asset {name: "admin-laptop", tier: "endpoint", crown_jewel: false})

// Network reachability (CONNECTS_TO is directed: caller -> callee)
MERGE (web)-[:CONNECTS_TO]->(api)
MERGE (api)-[:CONNECTS_TO]->(auth)
MERGE (api)-[:CONNECTS_TO]->(jobs)
MERGE (auth)-[:CONNECTS_TO]->(cache)
MERGE (auth)-[:CONNECTS_TO]->(custdb)
MERGE (jobs)-[:CONNECTS_TO]->(paydb)
MERGE (cache)-[:CONNECTS_TO]->(vault)
MERGE (admin)-[:CONNECTS_TO]->(jobs)
MERGE (admin)-[:CONNECTS_TO]->(auth)

// Vulnerabilities present on assets
MERGE (cve1:CVE {id: "CVE-2024-1101", cvss: 9.8, kind: "RCE"})
MERGE (cve2:CVE {id: "CVE-2024-1102", cvss: 7.5, kind: "AuthBypass"})
MERGE (cve3:CVE {id: "CVE-2024-1103", cvss: 6.4, kind: "SSRF"})
MERGE (cve4:CVE {id: "CVE-2024-1104", cvss: 8.1, kind: "PrivEsc"})

MERGE (web)-[:HAS_VULN]->(cve1)
MERGE (api)-[:HAS_VULN]->(cve3)
MERGE (auth)-[:HAS_VULN]->(cve2)
MERGE (jobs)-[:HAS_VULN]->(cve4)
MERGE (cache)-[:HAS_VULN]->(cve3);

Step 2: Find exploitable attack paths to crown jewels

// Path from any public asset to any crown jewel where every non-target hop
// hosts a vuln with CVSS >= 7.
MATCH path = (entry:Asset {tier: "public"})-[:CONNECTS_TO*1..6]->(target:Asset {crown_jewel: true})
WITH path, entry, target, nodes(path) AS hops
UNWIND hops AS hop
OPTIONAL MATCH (hop)-[:HAS_VULN]->(v:CVE)
WITH path, entry, target, hops, hop,
     max(CASE WHEN v.cvss >= 7.0 THEN v.cvss ELSE 0 END) AS hop_score,
     collect(v.id) AS hop_cves
WITH path, entry, target,
     collect({name: hop.name, score: hop_score, cves: hop_cves, is_target: hop = target}) AS hop_info
WHERE ALL(h IN hop_info WHERE h.is_target OR h.score >= 7.0)
RETURN entry.name  AS entry_point,
       target.name AS crown_jewel,
       [h IN hop_info | h.name] AS hop_names,
       [h IN hop_info WHERE NOT h.is_target | h.cves] AS exploited_cves,
       size(hop_info) - 1 AS hops
ORDER BY hops, entry_point;

What's happening

  • The pattern starts at any tier: "public" asset and walks CONNECTS_TO up to 6 hops to any crown_jewel: true asset — defining "blast surface" structurally.
  • The ALL(... EXISTS {...}) predicate prunes paths to those where every traversed node hosts an exploitable vulnerability (CVSS >= 7) — a defensible attack chain, not a theoretical one.
  • The list comprehension extracts the specific CVE used on each hop, so the output reads like an attacker's playbook: "exploit CVE-X on web, pivot via CVE-Y on api, land in customers-db."
  • This is a graph-native rephrasing of MITRE ATT&CK kill-chain analysis. Patching any one node on a path breaks the chain — graph queries make "minimum cut" thinking practical for SecOps.
  • Compared to SQL: walking 6-hop network connectivity with per-hop predicates would require six recursive joins and complex predicate plumbing — the Cypher version fits on a screen.

Try this next

MATCH (a:Asset)-[:HAS_VULN]->(v:CVE)
WHERE v.cvss >= 8.0
RETURN a.name AS asset, collect(v.id) AS critical_cves
ORDER BY size(critical_cves) DESC;
MATCH path = shortestPath((p:Asset {tier: "public"})-[:CONNECTS_TO*]-(j:Asset {crown_jewel: true}))
RETURN p.name AS from_public, j.name AS to_jewel, length(path) AS hops
ORDER BY hops;
MATCH (a:Asset)-[:CONNECTS_TO]->(b:Asset)
RETURN a.tier AS from_tier, b.tier AS to_tier, count(*) AS edges
ORDER BY edges DESC;

Tags

graphcyphersecurityadvanced

Run this on your own machine

Install SynapCores Community Edition free, paste the SQL or Cypher above into the bundled web UI, and watch it run.

Download Free CE