Bring runZero Exposure Management into BloodHound via OpenGraph.
Read our initial blog post.
-
Ensure that you have Docker or Podman (in Docker-compatibility mode). The command “docker compose ls” should not return an error.
-
Git clone the BloodHound source tree:
git clone https://github.com/SpecterOps/BloodHound.git
-
Open a terminal in the
BloodHound/examples/docker-compoosesubdirectory -
Adjust
docker-compose.ymlto enable thepggraph-db driver
bhe_graph_driver=pg
- Adjust bloodhound.config.json to set the graph_driver to “pg”
"graph_driver": "pg",
- Run “docker compose up” to launch BloodHound
- Copy the initial admin password shown in the output
- Login to http://127.0.0.1:8080/ui/login with username admin and your password
- Change the password to something else and remember or save it
- Hurray! At this point you are ready to load and explore data
- Ensure that you have a recent version of Go installed (1.25+)
- Git clone the runZeroHound source tree
git clone https://github.com/runZeroInc/runZeroHound.git
- Ensure that the tool runs:
go run main.go -h
- Login to your runZero Console
- Navigate to Inventory -> Assets
- Under Export, select “As JSON Lines…”
- Wait for this to download to disk
- Open the runZeroHound directory in your terminal
- Run the convert command to create an OpenGraph JSON
go run main.go convert <runZeroInventory.jsonl> opengraph.json
- Use the Quick Upload option on the left and drag your opengraph.json onto it
- Watch the File Ingest history at http://127.0.0.1:8080/ui/administration/file-ingest
- Once import completes, access Explore and then select the Cypher tab
- Enter a test query to verify your data:
match (n:RZNetwork) where n.network_address = '0.0.0.0' return n
- Confirm that this query shows the RZ-NETWORK-PUBLIC subnet node
Instead of manually dragging the file, you can upload using the upload command.
All BloodHound CE commands (upload, cypher, purge) share the same authentication flags and accept either API token or username/password credentials. Environment variables are used when flags are not set.
| Flag | Environment Variable | Description |
|---|---|---|
--url |
BLOODHOUND_URL |
BloodHound CE base URL |
--token-id |
BLOODHOUND_TOKEN_ID |
API token ID |
--token-key |
BLOODHOUND_TOKEN_KEY |
API token key/secret |
--username |
BLOODHOUND_USERNAME |
BloodHound CE username |
--password |
BLOODHOUND_PASSWORD |
BloodHound CE password |
--insecure |
Skip TLS certificate verification |
To create an API token in BloodHound CE, navigate to Administration → API Tokens.
# Using API token auth
export BLOODHOUND_URL=http://127.0.0.1:8080
export BLOODHOUND_TOKEN_ID=<your-token-id>
export BLOODHOUND_TOKEN_KEY=<your-token-key>
go run main.go upload opengraph.json
# Using username/password auth
export BLOODHOUND_URL=http://127.0.0.1:8080
export BLOODHOUND_USERNAME=admin
export BLOODHOUND_PASSWORD=<your-password>
go run main.go upload opengraph.jsonOr pass credentials as flags:
go run main.go upload \
--url http://127.0.0.1:8080 \
--token-id <id> \
--token-key <key> \
opengraph.jsonUse --wait to block until all ingest jobs finish and the datapipe is idle:
# Upload and wait for ingest to complete
go run main.go upload --wait opengraph.json
# Just wait for pending jobs to finish (no file upload)
go run main.go upload --wait| Flag | Default | Description |
|---|---|---|
--wait |
false |
Wait for all ingest jobs to complete |
--wait-timeout |
300 |
Maximum seconds to wait for ingest completion |
--wait-interval |
5 |
Seconds between status checks |
The cypher command executes a Cypher query against BloodHound CE and prints the raw JSON response to stdout:
go run main.go cypher 'MATCH (n) RETURN labels(n) AS kind, count(n) AS total ORDER BY total DESC'Pipe through jq for pretty-printing or field extraction:
go run main.go cypher 'MATCH (n:RZNetwork) RETURN n.displayname LIMIT 5' | jq .The purge command deletes all collected graph data, file ingest history, and data quality history:
go run main.go purgeThis is useful for starting fresh before loading a new dataset.
BloodHound OpenGraph supports custom icons for specific node types. Setting this up requires a bit of API interaction and we plan to add a helper tool to support this in the future.
runZeroHound converts flat asset data into a richly connected graph with 30+ node types and 50+ edge types spanning IT, OT, and IoT environments.
| Category | Node Types |
|---|---|
| Core | RZAsset, RZService, RZNetwork, RZDomain, RZVLAN |
| Cryptographic Identity | RZSSHKey, RZTLSCert, RZTLSCAChain, RZSMBGUID, RZSerialNumber |
| Network Infrastructure | RZRouter, RZMACAddress, RZMACVendor, RZSwitch, RZSwitchPort, RZSubAsset |
| Protocol Fingerprints | RZSNMPEngineID, RZSNMPDeviceType, RZSNMPCommunity, RZIPMICredential, RZFavicon, RZIKEIdentity |
| OT / Building Automation | RZGateway, RZKNXnetDevice, RZBACnetDevice |
| Time & DNS | RZNTPReference, RZDNSIdentity, RZDNSVersion |
| Windows / AD | RZNTLMDomain, RZNTLMComputer |
| Vulnerabilities | RZVuln |
18 relationship extractors automatically create edges between nodes wherever assets share a cryptographic key, serial number, CA chain, NTP source, NTLM domain, favicon hash, vulnerability finding, or any other correlatable identity.
For the complete schema (every node kind, edge, and property), see QUERIES.md.
All queries run in the BloodHound CE Explore → Cypher tab or via go run main.go cypher '<query>'. For 75+ queries organized by difficulty and category, see QUERIES.md.
Count every node type in the graph:
MATCH (n)
RETURN labels(n) AS kind, count(n) AS total
ORDER BY total DESCTop open ports across all assets:
MATCH (svc:RZService)
WHERE svc.port IS NOT NULL
RETURN svc.port + '/' + svc.transport AS port, count(svc) AS total
ORDER BY total DESC
LIMIT 20SSH key reuse — find keys shared across multiple assets:
MATCH (a:RZAsset)-[:RZHasSSHKey]->(key:RZSSHKey)
WITH key, collect(DISTINCT a.displayname) AS hosts
WHERE size(hosts) > 1
RETURN key.fingerprint, key.key_type, hosts
ORDER BY size(hosts) DESC
LIMIT 20BACnet devices by vendor:
MATCH (dev:RZBACnetDevice)
RETURN dev.vendor_name, dev.vendor_id, count(dev) AS count
ORDER BY count DESCCertificates reused across different subnets:
MATCH (a:RZAsset)-[:RZHasTLSCert]->(cert:RZTLSCert),
(a)-[:RZInsideOfSubnet]->(net:RZNetwork)
WITH cert, collect(DISTINCT net.displayname) AS subnets, count(DISTINCT a) AS hosts
WHERE size(subnets) > 1
RETURN cert.cn, cert.sha1, subnets, hosts
ORDER BY hosts DESC
LIMIT 10NTLM domains spanning multiple subnets (lateral movement surface):
MATCH (a:RZAsset)-[:RZHasNTLMDomain]->(dom:RZNTLMDomain),
(a)-[:RZInsideOfSubnet]->(net:RZNetwork)
WITH dom, collect(DISTINCT net.displayname) AS subnets, count(DISTINCT a) AS hosts
WHERE size(subnets) > 1
RETURN dom.dns_domain, subnets, hosts
ORDER BY hosts DESCMost widespread vulnerabilities (affecting the most hosts):
MATCH (a)-[:RZHasVuln]->(v:RZVuln)
WITH v, count(a) AS affected_hosts
RETURN v.displayname, v.source, v.severity, affected_hosts
ORDER BY affected_hosts DESC
LIMIT 20High-severity vulnerabilities with CVEs:
MATCH (v:RZVuln)
WHERE v.cve IS NOT NULL
RETURN v.displayname, v.cve, v.source
LIMIT 20Relationship richness per asset (how many different identity types link to each host):
MATCH (a:RZAsset)
OPTIONAL MATCH (a)-[:RZHasSSHKey]->(ssh:RZSSHKey)
OPTIONAL MATCH (a)-[:RZHasTLSCert]->(tls:RZTLSCert)
OPTIONAL MATCH (a)-[:RZHasGateway]->(gw:RZGateway)
OPTIONAL MATCH (a)-[:RZHasFavicon]->(fav:RZFavicon)
OPTIONAL MATCH (a)-[:RZHasBACnetDevice]->(bac:RZBACnetDevice)
OPTIONAL MATCH (a)-[:RZHasSerialNumber]->(sn:RZSerialNumber)
WITH a,
count(DISTINCT ssh) AS ssh_keys,
count(DISTINCT tls) AS tls_certs,
count(DISTINCT gw) AS gateways,
count(DISTINCT fav) AS favicons,
count(DISTINCT bac) AS bacnet_devs,
count(DISTINCT sn) AS serials
WHERE ssh_keys + tls_certs + gateways + favicons + bacnet_devs + serials > 0
RETURN a.displayname, ssh_keys, tls_certs, gateways, favicons, bacnet_devs, serials
ORDER BY ssh_keys + tls_certs + gateways + favicons + bacnet_devs + serials DESC
LIMIT 20See QUERIES.md for the full collection including inter-asset relationships and quirky/surprising results.
The test_queries.sh script extracts every query from QUERIES.md and runs each one via the cypher CLI command:
# Full run: convert sample data, purge, upload, wait, then test all queries
./test_queries.sh
# Skip data loading — just test queries against existing data
./test_queries.sh --skip-loadrunZeroHound can ingest data from any of the following sources:
| Source | Format | Detection | Key Data Extracted |
|---|---|---|---|
| runZero | JSONL / JSONL.gz | gzip header or JSON lines | Full asset model with all attributes, services, IPs, MACs, domains, VLANs |
| Nmap | XML (-oX) | <nmaprun XML tag |
IPs, MACs, hostnames, OS, services, SSH keys, TLS certs, SMB GUIDs, SNMP engine IDs, traceroute hops |
| Nessus | .nessus XML | Extension or NessusClientData tag |
IPs, MACs, hostnames, OS, services, SSH keys, TLS certs, SMB GUIDs, SNMP engine IDs, traceroute hops |
| OpenVAS/GVM | XML | <report + openvas/gvm |
IPs, MACs, hostnames, OS, services, SSH keys, TLS certs, SMB GUIDs, SNMP engine IDs |
| Qualys | VM scan XML | <SCAN + <IP tags |
IPs, MACs, hostnames, OS, services, SSH keys, TLS certs, SMB GUIDs, SNMP engine IDs |
| Masscan | XML or JSON | scanner="masscan" or JSON with ip+ports |
IPs, open ports, service banners |
| Shodan | JSONL | ip_str in JSON |
IPs, hostnames, OS, services, TLS certs, SSH keys, vulnerabilities |
| NetBox | JSON API export | count+results JSON |
IPs, hostnames, device types, roles, platforms, sites, racks |
| snmpwalk | Text output | OID = TYPE: VALUE pattern | IPs, MACs, hostnames, OS (sysDescr), SNMP engine IDs, ARP cache, MAC table |
For the best data from Nmap for use with runZeroHound, see docs/nmap-commands.md.
Quick reference for a comprehensive scan:
sudo nmap -sS -sU -sV -O --traceroute \
--script ssh-hostkey,ssl-cert,smb2-security-mode,snmp-info,nbstat \
-p T:22,80,443,445,3389,8080,8443,U:161,137 \
-oX scan.xml 192.168.1.0/24When loading data from multiple sources, runZeroHound automatically correlates assets using shared cryptographic identities:
- SSH host keys — Same host key fingerprint from Nmap, Nessus, and runZero links them as the same device
- TLS certificates — Shared certificate SHA-1 fingerprints connect scanners observing the same endpoint
- SMB GUIDs — Windows machine GUIDs link SMB-visible hosts across sources
- SNMPv3 Engine IDs — Unique SNMP engine identifiers correlate managed network devices
These fingerprints are normalised to a consistent lowercase colon-hex format across all parsers.
runZeroHound is not an officially supported runZero product, but we still want to hear your feedback and bug reports. Please open an issue in this repository or email support[at]runZero.com.