| name | cypher-query-playbook |
| description | Neo4j Cypher query patterns for analyzing BBOT reconnaissance data in the graph database. Use when you need to analyze scan results, map infrastructure, find anomalies, or synthesize findings from the attack surface graph. |
Cypher Query Playbook
Quick Reference
Schema Compatibility
Run get_db_schema() before using relationship-heavy queries. BBOT-backed
graphs use the BBOT event envelope. Treat .data as the canonical event value;
supporting fields may include id, uuid, type, tags, scope_distance,
module, and scan. Do not assume convenience fields such as .name,
.address, .version, or .provider. Relationship names vary by module and
record type; inspect them before writing analysis queries. Common relationship
types include A, CNAME, httpx, portscan, and nuclei.
Orientation Queries (Run First)
Asset summary:
MATCH (n) RETURN labels(n)[0] as type, count(n) AS count ORDER BY count DESC
Recent scans:
MATCH (s:SCAN) RETURN s.data, s.id ORDER BY s.id DESC LIMIT 10
Database schema:
Use the get_db_schema tool for a complete schema overview.
Finding High-Value Assets
Dev/test/staging subdomains:
MATCH (n:DNS_NAME)
WITH coalesce(n.name, n.data, n.host) AS name
WHERE name =~ '.*(dev|test|stage|uat|vpn|api|admin|internal|staging|qa|sandbox).*'
RETURN name ORDER BY name
Interesting web page titles:
MATCH (n:URL)
WHERE n.status_code = 200
AND n.title =~ '.*(Login|Admin|Dashboard|Unauthorized|Forbidden|Console|Manager|Portal|Panel|Config).*'
RETURN n.name, n.title
Critical and high findings:
MATCH (f:FINDING)
WHERE f.severity IN ['critical', 'high']
RETURN f.type, f.severity, f.description, f.data
Admin panels and login pages:
MATCH (n:URL)
WHERE n.name =~ '.*(admin|panel|dashboard|console|login|signin|auth).*'
AND n.status_code < 400
RETURN n.name, n.status_code, n.title
Infrastructure Mapping
DNS to IP resolution:
MATCH (d:DNS_NAME)-[:RESOLVES_TO]->(ip:IP_ADDRESS)
RETURN d.name, ip.address
ORDER BY ip.address
Find all domains on a specific IP:
MATCH (ip:IP_ADDRESS {address: $ip})<-[:RESOLVES_TO]-(d:DNS_NAME)
RETURN ip.address, collect(d.name) AS domains
Find IPs for a domain:
MATCH (d:DNS_NAME {name: $domain})-[:RESOLVES_TO]->(ip:IP_ADDRESS)
RETURN d.name, ip.address
Shared hosting (IPs with multiple domains):
MATCH (ip:IP_ADDRESS)<-[:RESOLVES_TO]-(d:DNS_NAME)
WITH ip, collect(d.name) AS domains, count(d) as cnt
WHERE cnt > 1
RETURN ip.address, cnt, domains
ORDER BY cnt DESC
Reverse DNS — all domains per IP:
MATCH (ip:IP_ADDRESS)<-[:RESOLVES_TO]-(d:DNS_NAME)
RETURN ip.address, collect(d.name) as domains
ORDER BY size(collect(d.name)) DESC
Service Discovery
Web services responding 200:
MATCH (n:URL)
WHERE n.status_code >= 200 AND n.status_code < 300
RETURN n.name, n.status_code, n.title
LIMIT 50
API endpoints:
MATCH (n:URL)
WHERE n.name CONTAINS '/api/' OR n.name CONTAINS '/v1/' OR n.name CONTAINS '/v2/' OR n.name CONTAINS '/graphql'
RETURN n.name, n.status_code, n.title
Interesting ports (databases, admin services):
MATCH (p:OPEN_TCP_PORT)
WHERE p.port IN [3306, 5432, 6379, 27017, 9200, 8080, 8443, 9090, 3389, 5900, 11211]
MATCH (ip:IP_ADDRESS)-[:HAS_PORT]->(p)
RETURN ip.address, p.port, p.service
Services by port:
MATCH (ip:IP_ADDRESS)-[:HAS_PORT]->(p:OPEN_TCP_PORT)
RETURN p.port, count(ip) as host_count
ORDER BY host_count DESC
LIMIT 20
Technology Analysis
All discovered technologies:
MATCH (t:TECHNOLOGY)
RETURN DISTINCT t.name, t.version, count(*) as usage_count
ORDER BY usage_count DESC
Technology stack for a host:
MATCH (d:DNS_NAME {name: $domain})-[:RESOLVES_TO]->(ip)-[:HAS_PORT]->()-[:HAS_TECHNOLOGY]->(t)
RETURN d.name, t.name, t.version
Technology outliers (old/unusual software):
MATCH (t:TECHNOLOGY)
WITH t, coalesce(t.name, t.data) AS tech_name
WHERE tech_name IN ['JBoss', 'ColdFusion', 'Struts', 'WebLogic', 'Tomcat', 'IIS']
OR t.version =~ '.*[0-4]\\..*'
MATCH (n)-[:HAS_TECHNOLOGY]->(t)
RETURN labels(n)[0] as asset_type, coalesce(n.name, n.data, n.host) AS asset, tech_name, t.version
Assets with a specific technology:
MATCH (t:TECHNOLOGY {name: $tech_name})<-[:HAS_TECHNOLOGY]-(n)
RETURN labels(n)[0] as type, n.name, t.version
Security Analysis
All findings by severity:
MATCH (f:FINDING)
RETURN f.severity, count(f) as count
ORDER BY CASE f.severity
WHEN 'critical' THEN 0
WHEN 'high' THEN 1
WHEN 'medium' THEN 2
WHEN 'low' THEN 3
ELSE 4
END
Findings with affected assets:
MATCH (asset)-[:HAS_FINDING]->(f:FINDING)
RETURN f.type, f.severity, f.description, labels(asset)[0] as asset_type, asset.name
ORDER BY f.severity
Public storage buckets:
MATCH (n:STORAGE_BUCKET)
WHERE n.public = true
RETURN n.name, n.url
Exposed databases:
MATCH (p:OPEN_TCP_PORT)
WHERE p.port IN [3306, 5432, 6379, 27017, 9200, 5984, 11211]
MATCH (ip:IP_ADDRESS)-[:HAS_PORT]->(p)
OPTIONAL MATCH (ip)<-[:RESOLVES_TO]-(d:DNS_NAME)
RETURN ip.address, p.port, p.service, collect(d.name) as hostnames
Cross-Reference & Correlation
Shared infrastructure for high-value assets:
MATCH (d:DNS_NAME)-[:RESOLVES_TO]->(ip:IP_ADDRESS)
WHERE d.name CONTAINS 'dev' OR d.name CONTAINS 'api' OR d.name CONTAINS 'staging' OR d.name CONTAINS 'admin'
WITH ip, collect(d.name) AS domains, count(*) as domainCount
WHERE domainCount > 1
RETURN ip.address, domains
Correlate findings by technology:
MATCH (f:FINDING)<-[:HAS_FINDING]-(root)
MATCH (root)-[:HAS_TECHNOLOGY]->(tech:TECHNOLOGY)
MATCH (other_asset)-[:HAS_TECHNOLOGY]->(tech)
WHERE other_asset <> root
RETURN tech.name, collect(DISTINCT other_asset.name) AS related_assets
Discover naming conventions:
MATCH (d:DNS_NAME)
WHERE d.name =~ '.*(app|srv|db|web|mail|ns|mx)0[0-9].*'
RETURN collect(d.name) AS discovered_pattern
Cross-reference: domains sharing IP with a finding:
MATCH (f:FINDING)<-[:HAS_FINDING]-(asset)
OPTIONAL MATCH (asset)-[:RESOLVES_TO]->(ip:IP_ADDRESS)
OPTIONAL MATCH (ip)<-[:RESOLVES_TO]-(sibling:DNS_NAME)
WHERE sibling <> asset
RETURN f.type, asset.name, ip.address, collect(DISTINCT sibling.name) as co_hosted
Path Analysis
Connection paths from domain to finding:
MATCH p=(d:DNS_NAME)-[*1..3]-(f:FINDING)
WHERE d.name = $domain
RETURN p
All relationships for a specific asset:
MATCH (n)-[r]-(m)
WHERE n.name = $name
RETURN labels(n)[0] as source_type, n.name, type(r) as relationship, labels(m)[0] as target_type, m.name
Shortest path between two assets:
MATCH p=shortestPath((n1:DNS_NAME {name: $start})-[*]-(n2:DNS_NAME {name: $end}))
RETURN p
Aggregation & Statistics
Top ports across all hosts:
MATCH (p:OPEN_TCP_PORT)
RETURN p.port, count(p) as cnt
ORDER BY cnt DESC
LIMIT 10
Domains per IP (distribution):
MATCH (ip:IP_ADDRESS)<-[:RESOLVES_TO]-(d)
RETURN ip.address, count(d) as domain_count
ORDER BY domain_count DESC
LIMIT 20
Email addresses by domain:
MATCH (e:EMAIL_ADDRESS)
WHERE e.address ENDS WITH $domain
RETURN e.address
Cloud provider breakdown:
MATCH (ip:IP_ADDRESS)
WHERE ip.provider IS NOT NULL
RETURN ip.provider, count(ip) as count
ORDER BY count DESC
Tips
- Always use parameters (
$param) for user input to prevent Cypher injection
- Start with small limits (10-20) and increase if needed
- Use regex escaping (
\\) for special characters in patterns
- Combine queries in the Orient phase to build a complete picture before deciding on the next scan
- Date filtering:
WHERE n.created_at > datetime('2024-01-01')
- NOT conditions:
WHERE NOT n.status_code IN [404, 403, 401]
- Case-insensitive regex:
WHERE n.name =~ '(?i).*admin.*'