# Directory Structure
```
├── .gitattributes
├── .github
│ └── FUNDING.yml
├── BloodHound-MCP.py
├── images
│ └── BloodHound-MCP-Banner.png
├── README.md
└── requirements.txt
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # BloodHound-MCP
2 |
3 | 
4 |
5 | ## Model Context Protocol (MCP) Server for BloodHound
6 |
7 | BloodHound-MCP is a powerful integration that brings the capabilities of Model Context Procotol (MCP) Server to BloodHound, the industry-standard tool for Active Directory security analysis. This integration allows you to analyze BloodHound data using natural language, making complex Active Directory attack path analysis accessible to everyone.
8 |
9 | > 🥇 **First-Ever BloodHound AI Integration!**
10 | > This is the first integration that connects BloodHound with AI through MCP, [originally announced here](https://www.linkedin.com/posts/mor-david-cyber_bloodhound-ai-cybersec-activity-7310921541213470721-N390).
11 |
12 | ## 🔍 What is BloodHound-MCP?
13 |
14 | BloodHound-MCP combines the power of:
15 | - **BloodHound**: Industry-standard tool for visualizing and analyzing Active Directory attack paths
16 | - **Model Context Protocol (MCP)**: An open protocol for creating custom AI tools, compatible with various AI models
17 | - **Neo4j**: Graph database used by BloodHound to store AD relationship data
18 |
19 | With over 75 specialized tools based on the original BloodHound CE Cypher queries, BloodHound-MCP allows security professionals to:
20 | - Query BloodHound data using natural language
21 | - Discover complex attack paths in Active Directory environments
22 | - Assess Active Directory security posture more efficiently
23 | - Generate detailed security reports for stakeholders
24 |
25 | ## 📱 Community
26 |
27 | Join our Telegram channel for updates, tips, and discussion:
28 | - **Telegram**: [root_sec](https://t.me/root_sec)
29 |
30 | ## 🌟 Star History
31 |
32 | [](https://www.star-history.com/#MorDavid/BloodHound-MCP-AI&Date)
33 |
34 | ## ✨ Features
35 |
36 | - **Natural Language Interface**: Query BloodHound data using plain English
37 | - **Comprehensive Analysis Categories**:
38 | - Domain structure mapping
39 | - Privilege escalation paths
40 | - Kerberos security issues (Kerberoasting, AS-REP Roasting)
41 | - Certificate services vulnerabilities
42 | - Active Directory hygiene assessment
43 | - NTLM relay attack vectors
44 | - Delegation abuse opportunities
45 | - And much more!
46 |
47 | ## 📋 Prerequisites
48 |
49 | - BloodHound 4.x+ with data collected from an Active Directory environment
50 | - Neo4j database with BloodHound data loaded
51 | - Python 3.8 or higher
52 | - MCP Client
53 |
54 | ## 🔧 Installation
55 |
56 | 1. Clone this repository:
57 | ```bash
58 | git clone https://github.com/your-username/MCP-BloodHound.git
59 | cd MCP-BloodHound
60 | ```
61 |
62 | 2. Install dependencies:
63 | ```bash
64 | pip install -r requirements.txt
65 | ```
66 | 3. Configure the MCP Server
67 | ```bash
68 | "mcpServers": {
69 | "BloodHound-MCP": {
70 | "command": "python",
71 | "args": [
72 | "<Your_Path>\\BloodHound-MCP.py"
73 | ],
74 | "env": {
75 | "BLOODHOUND_URI": "bolt://localhost:7687",
76 | "BLOODHOUND_USERNAME": "neo4j",
77 | "BLOODHOUND_PASSWORD": "bloodhoundcommunityedition"
78 | }
79 | }
80 | }
81 | ```
82 | ## 🚀 Usage
83 |
84 | Example queries you can ask through the MCP:
85 |
86 | - "Show me all paths from kerberoastable users to Domain Admins"
87 | - "Find computers where Domain Users have local admin rights"
88 | - "Identify Domain Controllers vulnerable to NTLM relay attacks"
89 | - "Map all Active Directory certificate services vulnerabilities"
90 | - "Generate a comprehensive security report for my domain"
91 | - "Find inactive privileged accounts"
92 | - "Show me attack paths to high-value targets"
93 |
94 | ## 🔐 Security Considerations
95 |
96 | This tool is designed for legitimate security assessment purposes. Always:
97 | - Obtain proper authorization before analyzing any Active Directory environment
98 | - Handle BloodHound data as sensitive information
99 | - Follow responsible disclosure practices for any vulnerabilities discovered
100 |
101 | ## 📜 License
102 |
103 | This project is licensed under the MIT License - see the LICENSE file for details.
104 |
105 | ## 🙏 Acknowledgments
106 |
107 | - The BloodHound team for creating an amazing Active Directory security tool
108 | - The security community for continuously advancing AD security practices
109 |
110 | [](https://mseep.ai/app/09d13f50-8965-4ebf-b4bf-d6bb98e8f092)
111 |
112 | ---
113 |
114 | *Note: This is not an official Anthropic product. BloodHound-MCP is a community-driven integration between BloodHound and MCP.*
115 |
```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
```
1 | neo4j
2 | python-dotenv
3 | mcp-server>=0.1.0
4 | fastmcp
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
1 | # These are supported funding model platforms
2 |
3 | github: mordavid
4 | patreon: mordavid
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: mordavid
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
```
--------------------------------------------------------------------------------
/BloodHound-MCP.py:
--------------------------------------------------------------------------------
```python
1 | from mcp.server.fastmcp import FastMCP
2 | from dotenv import load_dotenv
3 | from neo4j import GraphDatabase
4 | import os
5 | import logging
6 |
7 | # Configure logging
8 | logging.basicConfig(level=logging.DEBUG)
9 | logger = logging.getLogger(__name__)
10 |
11 | # Load environment variables
12 | load_dotenv()
13 |
14 | # BloodHound & Neo4j connection details
15 | BLOODHOUND_URI = os.getenv("BLOODHOUND_URI", "bolt://localhost:7687")
16 | BLOODHOUND_USERNAME = os.getenv("BLOODHOUND_USERNAME", "neo4j")
17 | BLOODHOUND_PASSWORD = os.getenv("BLOODHOUND_PASSWORD", "bloodhound")
18 |
19 | logger.debug(f"Using Neo4j connection details:")
20 | logger.debug(f"URI: {BLOODHOUND_URI}")
21 | logger.debug(f"User: {BLOODHOUND_USERNAME}")
22 |
23 | # Create Neo4j driver with BloodHound CE specific settings
24 | driver = GraphDatabase.driver(
25 | BLOODHOUND_URI,
26 | auth=(BLOODHOUND_USERNAME, BLOODHOUND_PASSWORD),
27 | encrypted=False
28 | )
29 |
30 | # Verify connection
31 | def verify_connectivity():
32 | try:
33 | # Try both default and bloodhound databases
34 | databases = ["neo4j", "bloodhound"]
35 | for db in databases:
36 | try:
37 | with driver.session(database=db) as session:
38 | logger.debug(f"Attempting to verify connection to database '{db}'...")
39 | result = session.run("MATCH (n:User) RETURN count(n) as count")
40 | count = result.single()["count"]
41 | logger.info(f"Successfully connected to database '{db}'. Found {count} users.")
42 | return True
43 | except Exception as e:
44 | logger.debug(f"Failed to connect to database '{db}': {str(e)}")
45 | continue
46 | raise Exception("Could not connect to any database")
47 | except Exception as e:
48 | logger.error(f"Failed to connect to Neo4j: {str(e)}")
49 | return False
50 |
51 | # Create FastMCP server for BloodHound
52 | mcp = FastMCP("BH-Examples")
53 |
54 | @mcp.tool()
55 | async def query_bloodhound(query: str):
56 | databases = ["neo4j", "bloodhound"]
57 | last_error = None
58 |
59 | for db in databases:
60 | try:
61 | with driver.session(database=db) as session:
62 | result = session.run(query)
63 | data = [record.data() for record in result]
64 | logger.info(f"Query successful on database '{db}'")
65 | return {"success": True, "data": data}
66 | except Exception as e:
67 | last_error = e
68 | logger.debug(f"Query failed on database '{db}': {str(e)}")
69 | continue
70 |
71 | logger.error(f"Query failed on all databases. Last error: {str(last_error)}")
72 | return {"success": False, "error": str(last_error)}
73 |
74 | # Domain Information
75 | @mcp.tool()
76 | async def find_all_domain_admins():
77 | query = """
78 | MATCH p = (t:Group)<-[:MemberOf*1..]-(a)
79 | WHERE (a:User or a:Computer) and t.objectid ENDS WITH '-512'
80 | RETURN p
81 | LIMIT 1000
82 | """
83 | return await query_bloodhound(query)
84 |
85 | @mcp.tool()
86 | async def map_domain_trusts():
87 | query = """
88 | MATCH p = (:Domain)-[:TrustedBy]->(:Domain)
89 | RETURN p
90 | LIMIT 1000
91 | """
92 | return await query_bloodhound(query)
93 |
94 | @mcp.tool()
95 | async def find_tier_zero_locations():
96 | query = """
97 | MATCH p = (t:Base)<-[:Contains*1..]-(:Domain)
98 | WHERE t.highvalue = true
99 | RETURN p
100 | LIMIT 1000
101 | """
102 | return await query_bloodhound(query)
103 |
104 | @mcp.tool()
105 | async def map_ou_structure():
106 | query = """
107 | MATCH p = (:Domain)-[:Contains*1..]->(:OU)
108 | RETURN p
109 | LIMIT 1000
110 | """
111 | return await query_bloodhound(query)
112 |
113 | # Dangerous Privileges
114 | @mcp.tool()
115 | async def find_dcsync_privileges():
116 | query = """
117 | MATCH p=(:Base)-[:DCSync|AllExtendedRights|GenericAll]->(:Domain)
118 | RETURN p
119 | LIMIT 1000
120 | """
121 | return await query_bloodhound(query)
122 |
123 | @mcp.tool()
124 | async def find_foreign_group_memberships():
125 | query = """
126 | MATCH p=(s:Base)-[:MemberOf]->(t:Group)
127 | WHERE s.domainsid<>t.domainsid
128 | RETURN p
129 | LIMIT 1000
130 | """
131 | return await query_bloodhound(query)
132 |
133 | @mcp.tool()
134 | async def find_domain_users_local_admins():
135 | query = """
136 | MATCH p=(s:Group)-[:AdminTo]->(:Computer)
137 | WHERE s.objectid ENDS WITH '-513'
138 | RETURN p
139 | LIMIT 1000
140 | """
141 | return await query_bloodhound(query)
142 |
143 | @mcp.tool()
144 | async def find_domain_users_laps_readers():
145 | query = """
146 | MATCH p=(s:Group)-[:AllExtendedRights|ReadLAPSPassword]->(:Computer)
147 | WHERE s.objectid ENDS WITH '-513'
148 | RETURN p
149 | LIMIT 1000
150 | """
151 | return await query_bloodhound(query)
152 |
153 | @mcp.tool()
154 | async def find_domain_users_high_value_paths():
155 | query = """
156 | MATCH p=shortestPath((s:Group)-[r*1..]->(t))
157 | WHERE t.highvalue = true AND s.objectid ENDS WITH '-513' AND s<>t
158 | RETURN p
159 | LIMIT 1000
160 | """
161 | return await query_bloodhound(query)
162 |
163 | @mcp.tool()
164 | async def find_domain_users_workstation_rdp():
165 | query = """
166 | MATCH p=(s:Group)-[:CanRDP]->(t:Computer)
167 | WHERE s.objectid ENDS WITH '-513' AND NOT toUpper(t.operatingsystem) CONTAINS 'SERVER'
168 | RETURN p
169 | LIMIT 1000
170 | """
171 | return await query_bloodhound(query)
172 |
173 | @mcp.tool()
174 | async def find_domain_users_server_rdp():
175 | query = """
176 | MATCH p=(s:Group)-[:CanRDP]->(t:Computer)
177 | WHERE s.objectid ENDS WITH '-513' AND toUpper(t.operatingsystem) CONTAINS 'SERVER'
178 | RETURN p
179 | LIMIT 1000
180 | """
181 | return await query_bloodhound(query)
182 |
183 | @mcp.tool()
184 | async def find_domain_users_privileges():
185 | query = """
186 | MATCH p=(s:Group)-[r]->(:Base)
187 | WHERE s.objectid ENDS WITH '-513'
188 | RETURN p
189 | LIMIT 1000
190 | """
191 | return await query_bloodhound(query)
192 |
193 | @mcp.tool()
194 | async def find_domain_admin_non_dc_logons():
195 | query = """
196 | MATCH (s)-[:MemberOf*0..]->(g:Group)
197 | WHERE g.objectid ENDS WITH '-516'
198 | WITH COLLECT(s) AS exclude
199 | MATCH p = (c:Computer)-[:HasSession]->(:User)-[:MemberOf*1..]->(g:Group)
200 | WHERE g.objectid ENDS WITH '-512' AND NOT c IN exclude
201 | RETURN p
202 | LIMIT 1000
203 | """
204 | return await query_bloodhound(query)
205 |
206 | # Kerberos Interaction
207 | @mcp.tool()
208 | async def find_kerberoastable_tier_zero():
209 | query = """
210 | MATCH (u:User)
211 | WHERE u.hasspn=true
212 | AND u.enabled = true
213 | AND NOT u.objectid ENDS WITH '-502'
214 | AND NOT u.gmsa = true
215 | AND NOT u.msa = true
216 | AND u.highvalue = true
217 | RETURN u
218 | LIMIT 100
219 | """
220 | return await query_bloodhound(query)
221 |
222 | @mcp.tool()
223 | async def find_all_kerberoastable_users():
224 | query = """
225 | MATCH (u:User)
226 | WHERE u.hasspn=true
227 | AND u.enabled = true
228 | AND NOT u.objectid ENDS WITH '-502'
229 | AND NOT u.gmsa = true
230 | AND NOT u.msa = true
231 | RETURN u
232 | LIMIT 100
233 | """
234 | return await query_bloodhound(query)
235 |
236 | @mcp.tool()
237 | async def find_kerberoastable_most_admin():
238 | query = """
239 | MATCH (u:User)
240 | WHERE u.hasspn = true
241 | AND u.enabled = true
242 | AND NOT u.objectid ENDS WITH '-502'
243 | AND NOT u.gmsa = true
244 | AND NOT u.msa = true
245 | MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer)
246 | WITH DISTINCT u, COUNT(c) AS adminCount
247 | RETURN u
248 | ORDER BY adminCount DESC
249 | LIMIT 100
250 | """
251 | return await query_bloodhound(query)
252 |
253 | @mcp.tool()
254 | async def find_asreproast_users():
255 | query = """
256 | MATCH (u:User)
257 | WHERE u.dontreqpreauth = true
258 | AND u.enabled = true
259 | RETURN u
260 | LIMIT 100
261 | """
262 | return await query_bloodhound(query)
263 |
264 | # Shortest Paths
265 | @mcp.tool()
266 | async def find_shortest_paths_unconstrained_delegation():
267 | query = """
268 | MATCH p=shortestPath((s)-[r*1..]->(t:Computer))
269 | WHERE t.unconstraineddelegation = true AND s<>t
270 | RETURN p
271 | LIMIT 1000
272 | """
273 | return await query_bloodhound(query)
274 |
275 | @mcp.tool()
276 | async def find_paths_from_kerberoastable_to_da():
277 | query = """
278 | MATCH p=shortestPath((s:User)-[r*1..]->(t:Group))
279 | WHERE s.hasspn=true
280 | AND s.enabled = true
281 | AND NOT s.objectid ENDS WITH '-502'
282 | AND NOT s.gmsa = true
283 | AND NOT s.msa = true
284 | AND t.objectid ENDS WITH '-512'
285 | RETURN p
286 | LIMIT 1000
287 | """
288 | return await query_bloodhound(query)
289 |
290 | @mcp.tool()
291 | async def find_shortest_paths_to_tier_zero():
292 | query = """
293 | MATCH p=shortestPath((s)-[r*1..]->(t))
294 | WHERE t.highvalue = true AND s<>t
295 | RETURN p
296 | LIMIT 1000
297 | """
298 | return await query_bloodhound(query)
299 |
300 | @mcp.tool()
301 | async def find_paths_from_domain_users_to_tier_zero():
302 | query = """
303 | MATCH p=shortestPath((s:Group)-[r*1..]->(t))
304 | WHERE t.highvalue = true AND s.objectid ENDS WITH '-513' AND s<>t
305 | RETURN p
306 | LIMIT 1000
307 | """
308 | return await query_bloodhound(query)
309 |
310 | @mcp.tool()
311 | async def find_shortest_paths_to_domain_admins():
312 | query = """
313 | MATCH p=shortestPath((t:Group)<-[r*1..]-(s:Base))
314 | WHERE t.objectid ENDS WITH '-512' AND s<>t
315 | RETURN p
316 | LIMIT 1000
317 | """
318 | return await query_bloodhound(query)
319 |
320 | @mcp.tool()
321 | async def find_paths_from_owned_objects():
322 | query = """
323 | MATCH p=shortestPath((s:Base)-[r*1..]->(t:Base))
324 | WHERE s.owned = true AND s<>t
325 | RETURN p
326 | LIMIT 1000
327 | """
328 | return await query_bloodhound(query)
329 |
330 | # Active Directory Certificate Services
331 | @mcp.tool()
332 | async def find_pki_hierarchy():
333 | query = """
334 | MATCH p=()-[:HostsCAService|IssuedSignedBy|EnterpriseCAFor|RootCAFor|TrustedForNTAuth|NTAuthStoreFor*..]->(:Domain)
335 | RETURN p
336 | LIMIT 1000
337 | """
338 | return await query_bloodhound(query)
339 |
340 | @mcp.tool()
341 | async def find_public_key_services():
342 | query = """
343 | MATCH p = (c:Container)-[:Contains*..]->(:Base)
344 | WHERE c.distinguishedname starts with 'CN=PUBLIC KEY SERVICES,CN=SERVICES,CN=CONFIGURATION,DC='
345 | RETURN p
346 | LIMIT 1000
347 | """
348 | return await query_bloodhound(query)
349 |
350 | @mcp.tool()
351 | async def find_certificate_enrollment_rights():
352 | query = """
353 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)
354 | RETURN p
355 | LIMIT 1000
356 | """
357 | return await query_bloodhound(query)
358 |
359 | @mcp.tool()
360 | async def find_esc1_vulnerable_templates():
361 | query = """
362 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)
363 | WHERE ct.enrolleesuppliessubject = True
364 | AND ct.authenticationenabled = True
365 | AND ct.requiresmanagerapproval = False
366 | AND (ct.authorizedsignatures = 0 OR ct.schemaversion = 1)
367 | RETURN p
368 | LIMIT 1000
369 | """
370 | return await query_bloodhound(query)
371 |
372 | @mcp.tool()
373 | async def find_esc2_vulnerable_templates():
374 | query = """
375 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(c:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)
376 | WHERE c.requiresmanagerapproval = false
377 | AND (c.effectiveekus = [''] OR '2.5.29.37.0' IN c.effectiveekus)
378 | AND (c.authorizedsignatures = 0 OR c.schemaversion = 1)
379 | RETURN p
380 | LIMIT 1000
381 | """
382 | return await query_bloodhound(query)
383 |
384 | @mcp.tool()
385 | async def find_enrollment_agent_templates():
386 | query = """
387 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)
388 | WHERE '1.3.6.1.4.1.311.20.2.1' IN ct.effectiveekus
389 | OR '2.5.29.37.0' IN ct.effectiveekus
390 | OR SIZE(ct.effectiveekus) = 0
391 | RETURN p
392 | LIMIT 1000
393 | """
394 | return await query_bloodhound(query)
395 |
396 | @mcp.tool()
397 | async def find_dcs_weak_certificate_binding():
398 | query = """
399 | MATCH p = (s:Computer)-[:DCFor]->(:Domain)
400 | WHERE s.strongcertificatebindingenforcementraw = 0 OR s.strongcertificatebindingenforcementraw = 1
401 | RETURN p
402 | LIMIT 1000
403 | """
404 | return await query_bloodhound(query)
405 |
406 | @mcp.tool()
407 | async def find_inactive_tier_zero_principals():
408 | query = """
409 | WITH 60 as inactive_days
410 | MATCH (n:Base)
411 | WHERE n.highvalue = true
412 | AND n.enabled = true
413 | AND n.lastlogontimestamp < (datetime().epochseconds - (inactive_days * 86400))
414 | AND n.lastlogon < (datetime().epochseconds - (inactive_days * 86400))
415 | AND n.whencreated < (datetime().epochseconds - (inactive_days * 86400))
416 | AND NOT n.name STARTS WITH 'AZUREADKERBEROS.'
417 | AND NOT n.objectid ENDS WITH '-500'
418 | AND NOT n.name STARTS WITH 'AZUREADSSOACC.'
419 | RETURN n
420 | """
421 | return await query_bloodhound(query)
422 |
423 | @mcp.tool()
424 | async def find_tier_zero_without_smartcard():
425 | query = """
426 | MATCH (u:User)
427 | WHERE u.highvalue = true
428 | AND u.enabled = true
429 | AND u.smartcardrequired = false
430 | AND NOT u.name STARTS WITH 'MSOL_'
431 | AND NOT u.name STARTS WITH 'PROVAGENTGMSA'
432 | AND NOT u.name STARTS WITH 'ADSYNCMSA_'
433 | RETURN u
434 | """
435 | return await query_bloodhound(query)
436 |
437 | @mcp.tool()
438 | async def find_domains_with_machine_quota():
439 | query = """
440 | MATCH (d:Domain)
441 | WHERE d.machineaccountquota > 0
442 | RETURN d
443 | """
444 | return await query_bloodhound(query)
445 |
446 | @mcp.tool()
447 | async def find_smartcard_dont_expire_domains():
448 | query = """
449 | MATCH (s:Domain)-[:Contains*1..]->(t:Base)
450 | WHERE s.expirepasswordsonsmartcardonlyaccounts = false
451 | AND t.enabled = true
452 | AND t.smartcardrequired = true
453 | RETURN s
454 | """
455 | return await query_bloodhound(query)
456 |
457 | @mcp.tool()
458 | async def find_two_way_forest_trust_delegation():
459 | query = """
460 | MATCH p=(n:Domain)-[r:TrustedBy]->(m:Domain)
461 | WHERE (m)-[:TrustedBy]->(n)
462 | AND r.trusttype = 'Forest'
463 | AND r.tgtdelegationenabled = true
464 | RETURN p
465 | """
466 | return await query_bloodhound(query)
467 |
468 | @mcp.tool()
469 | async def find_unsupported_operating_systems():
470 | query = """
471 | MATCH (c:Computer)
472 | WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|me|nt).*'
473 | RETURN c
474 | LIMIT 100
475 | """
476 | return await query_bloodhound(query)
477 |
478 | @mcp.tool()
479 | async def find_users_with_no_password_required():
480 | query = """
481 | MATCH (u:User)
482 | WHERE u.passwordnotreqd = true
483 | RETURN u
484 | LIMIT 100
485 | """
486 | return await query_bloodhound(query)
487 |
488 | @mcp.tool()
489 | async def find_users_password_not_rotated():
490 | query = """
491 | WITH 365 as days_since_change
492 | MATCH (u:User)
493 | WHERE u.pwdlastset < (datetime().epochseconds - (days_since_change * 86400))
494 | AND NOT u.pwdlastset IN [-1.0, 0.0]
495 | RETURN u
496 | LIMIT 100
497 | """
498 | return await query_bloodhound(query)
499 |
500 | @mcp.tool()
501 | async def find_nested_tier_zero_groups():
502 | query = """
503 | MATCH p=(t:Group)<-[:MemberOf*..]-(s:Group)
504 | WHERE t.highvalue = true
505 | AND NOT s.objectid ENDS WITH '-512'
506 | AND NOT s.objectid ENDS WITH '-519'
507 | RETURN p
508 | LIMIT 1000
509 | """
510 | return await query_bloodhound(query)
511 |
512 | @mcp.tool()
513 | async def find_disabled_tier_zero_principals():
514 | query = """
515 | MATCH (n:Base)
516 | WHERE n.highvalue = true
517 | AND n.enabled = false
518 | AND NOT n.objectid ENDS WITH '-502'
519 | AND NOT n.objectid ENDS WITH '-500'
520 | RETURN n
521 | LIMIT 100
522 | """
523 | return await query_bloodhound(query)
524 |
525 | @mcp.tool()
526 | async def find_principals_reversible_encryption():
527 | query = """
528 | MATCH (n:Base)
529 | WHERE n.encryptedtextpwdallowed = true
530 | RETURN n
531 | """
532 | return await query_bloodhound(query)
533 |
534 | @mcp.tool()
535 | async def find_principals_des_only_kerberos():
536 | query = """
537 | MATCH (n:Base)
538 | WHERE n.enabled = true
539 | AND n.usedeskeyonly = true
540 | RETURN n
541 | """
542 | return await query_bloodhound(query)
543 |
544 | @mcp.tool()
545 | async def find_principals_weak_kerberos_encryption():
546 | query = """
547 | MATCH (u:Base)
548 | WHERE 'DES-CBC-CRC' IN u.supportedencryptiontypes
549 | OR 'DES-CBC-MD5' IN u.supportedencryptiontypes
550 | OR 'RC4-HMAC-MD5' IN u.supportedencryptiontypes
551 | RETURN u
552 | """
553 | return await query_bloodhound(query)
554 |
555 | @mcp.tool()
556 | async def find_tier_zero_non_expiring_passwords():
557 | query = """
558 | MATCH (u:User)
559 | WHERE u.enabled = true
560 | AND u.pwdneverexpires = true
561 | AND u.highvalue = true
562 | RETURN u
563 | LIMIT 100
564 | """
565 | return await query_bloodhound(query)
566 |
567 | # NTLM Relay Attacks
568 | @mcp.tool()
569 | async def find_ntlm_relay_edges():
570 | query = """
571 | MATCH p = (n:Base)-[:CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|CoerceAndRelayNTLMToADCS|CoerceAndRelayNTLMToSMB]->(:Base)
572 | RETURN p LIMIT 500
573 | """
574 | return await query_bloodhound(query)
575 |
576 | @mcp.tool()
577 | async def find_esc8_vulnerable_cas():
578 | query = """
579 | MATCH (n:EnterpriseCA)
580 | WHERE n.hasvulnerableendpoint=true
581 | RETURN n
582 | """
583 | return await query_bloodhound(query)
584 |
585 | @mcp.tool()
586 | async def find_computers_outbound_ntlm_deny():
587 | query = """
588 | MATCH (c:Computer)
589 | WHERE c.restrictoutboundntlm = True
590 | RETURN c LIMIT 1000
591 | """
592 | return await query_bloodhound(query)
593 |
594 | @mcp.tool()
595 | async def find_computers_in_protected_users():
596 | query = """
597 | MATCH p = (:Base)-[:MemberOf*1..]->(g:Group)
598 | WHERE g.objectid ENDS WITH "-525"
599 | RETURN p LIMIT 1000
600 | """
601 | return await query_bloodhound(query)
602 |
603 | @mcp.tool()
604 | async def find_dcs_vulnerable_ntlm_relay():
605 | query = """
606 | MATCH p = (dc:Computer)-[:DCFor]->(:Domain)
607 | WHERE (dc.ldapavailable = True AND dc.ldapsigning = False)
608 | OR (dc.ldapsavailable = True AND dc.ldapsepa = False)
609 | OR (dc.ldapavailable = True AND dc.ldapsavailable = True AND dc.ldapsigning = False and dc.ldapsepa = True)
610 | RETURN p
611 | """
612 | return await query_bloodhound(query)
613 |
614 | @mcp.tool()
615 | async def find_computers_webclient_running():
616 | query = """
617 | MATCH (c:Computer)
618 | WHERE c.webclientrunning = True
619 | RETURN c LIMIT 1000
620 | """
621 | return await query_bloodhound(query)
622 |
623 | @mcp.tool()
624 | async def find_computers_no_smb_signing():
625 | query = """
626 | MATCH (n:Computer)
627 | WHERE n.smbsigning = False
628 | RETURN n
629 | """
630 | return await query_bloodhound(query)
631 |
632 | # Azure - General
633 | @mcp.tool()
634 | async def find_global_administrators():
635 | query = """
636 | MATCH p = (:AZBase)-[:AZGlobalAdmin*1..]->(:AZTenant)
637 | RETURN p
638 | LIMIT 1000
639 | """
640 | return await query_bloodhound(query)
641 |
642 | @mcp.tool()
643 | async def find_high_privileged_role_members():
644 | query = """
645 | MATCH p=(t:AZRole)<-[:AZHasRole|AZMemberOf*1..2]-(:AZBase)
646 | WHERE t.name =~ '(?i)(Global Administrator|User Access Administrator|Privileged Role Administrator|Privileged Authentication Administrator|Partner Tier1 Support|Partner Tier2 Support)'
647 | RETURN p
648 | LIMIT 1000
649 | """
650 | return await query_bloodhound(query)
651 |
652 | # Azure - Shortest Paths
653 | @mcp.tool()
654 | async def find_paths_from_entra_to_tier_zero():
655 | query = """
656 | MATCH p=shortestPath((s:AZUser)-[r*1..]->(t:AZBase))
657 | WHERE t.highvalue = true AND t.name =~ '(?i)(Global Administrator|User Access Administrator|Privileged Role Administrator|Privileged Authentication Administrator|Partner Tier1 Support|Partner Tier2 Support)' AND s<>t
658 | RETURN p
659 | LIMIT 1000
660 | """
661 | return await query_bloodhound(query)
662 |
663 | @mcp.tool()
664 | async def find_paths_to_privileged_roles():
665 | query = """
666 | MATCH p=shortestPath((s:AZBase)-[r*1..]->(t:AZRole))
667 | WHERE t.name =~ '(?i)(Global Administrator|User Access Administrator|Privileged Role Administrator|Privileged Authentication Administrator|Partner Tier1 Support|Partner Tier2 Support)' AND s<>t
668 | RETURN p
669 | LIMIT 1000
670 | """
671 | return await query_bloodhound(query)
672 |
673 | @mcp.tool()
674 | async def find_paths_from_azure_apps_to_tier_zero():
675 | query = """
676 | MATCH p=shortestPath((s:AZApp)-[r*1..]->(t:AZBase))
677 | WHERE t.highvalue = true AND s<>t
678 | RETURN p
679 | LIMIT 1000
680 | """
681 | return await query_bloodhound(query)
682 |
683 | @mcp.tool()
684 | async def find_paths_to_azure_subscriptions():
685 | query = """
686 | MATCH p=shortestPath((s:AZBase)-[r*1..]->(t:AZSubscription))
687 | WHERE s<>t
688 | RETURN p
689 | LIMIT 1000
690 | """
691 | return await query_bloodhound(query)
692 |
693 | # Azure - Microsoft Graph
694 | @mcp.tool("sp_app_role_grant")
695 | async def find_service_principals_with_app_role_grant():
696 | query = """
697 | MATCH p=(:AZServicePrincipal)-[:AZMGGrantAppRoles]->(:AZTenant)
698 | RETURN p
699 | LIMIT 1000
700 | """
701 | return await query_bloodhound(query)
702 |
703 | @mcp.tool("find_sp_graph_assignments")
704 | async def find_service_principals_with_graph_assignments():
705 | query = """
706 | MATCH p=(:AZServicePrincipal)-[:AZMGAppRoleAssignment_ReadWrite_All|AZMGApplication_ReadWrite_All|AZMGDirectory_ReadWrite_All|AZMGGroupMember_ReadWrite_All|AZMGGroup_ReadWrite_All|AZMGRoleManagement_ReadWrite_Directory|AZMGServicePrincipalEndpoint_ReadWrite_All]->(:AZServicePrincipal)
707 | RETURN p
708 | LIMIT 1000
709 | """
710 | return await query_bloodhound(query)
711 |
712 | # Azure - Hygiene
713 | @mcp.tool()
714 | async def find_foreign_tier_zero_principals():
715 | query = """
716 | MATCH (n:AZServicePrincipal)
717 | WHERE n.highvalue = true
718 | AND NOT toUpper(n.appownerorganizationid) = toUpper(n.tenantid)
719 | AND n.appownerorganizationid CONTAINS '-'
720 | RETURN n
721 | LIMIT 100
722 | """
723 | return await query_bloodhound(query)
724 |
725 | @mcp.tool()
726 | async def find_synced_tier_zero_principals():
727 | query = """
728 | MATCH (ENTRA:AZBase)
729 | MATCH (AD:Base)
730 | WHERE ENTRA.onpremsyncenabled = true
731 | AND ENTRA.onpremid = AD.objectid
732 | AND AD.highvalue = true
733 | RETURN ENTRA
734 | LIMIT 100
735 | """
736 | return await query_bloodhound(query)
737 |
738 | @mcp.tool()
739 | async def find_external_tier_zero_users():
740 | query = """
741 | MATCH (n:AZUser)
742 | WHERE n.highvalue = true
743 | AND n.name CONTAINS '#EXT#@'
744 | RETURN n
745 | LIMIT 100
746 | """
747 | return await query_bloodhound(query)
748 |
749 | @mcp.tool()
750 | async def find_disabled_azure_tier_zero_principals():
751 | query = """
752 | MATCH (n:AZBase)
753 | WHERE n.highvalue = true
754 | AND n.enabled = false
755 | RETURN n
756 | LIMIT 100
757 | """
758 | return await query_bloodhound(query)
759 |
760 | @mcp.tool()
761 | async def find_devices_unsupported_os():
762 | query = """
763 | MATCH (n:AZDevice)
764 | WHERE n.operatingsystem CONTAINS 'WINDOWS'
765 | AND n.operatingsystemversion =~ '(10.0.19044|10.0.22000|10.0.19043|10.0.19042|10.0.19041|10.0.18363|10.0.18362|10.0.17763|10.0.17134|10.0.16299|10.0.15063|10.0.14393|10.0.10586|10.0.10240|6.3.9600|6.2.9200|6.1.7601|6.0.6200|5.1.2600|6.0.6003|5.2.3790|5.0.2195).?.*'
766 | RETURN n
767 | LIMIT 100
768 | """
769 | return await query_bloodhound(query)
770 |
771 | # Azure - Cross Platform Attack Paths
772 | @mcp.tool()
773 | async def find_entra_users_in_domain_admins():
774 | query = """
775 | MATCH p = (:AZUser)-[:SyncedToADUser]->(:User)-[:MemberOf]->(t:Group)
776 | WHERE t.objectid ENDS WITH '-512'
777 | RETURN p
778 | LIMIT 1000
779 | """
780 | return await query_bloodhound(query)
781 |
782 | @mcp.tool()
783 | async def find_onprem_users_owning_entra_objects():
784 | query = """
785 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZOwns]->(:AZBase)
786 | RETURN p
787 | LIMIT 1000
788 | """
789 | return await query_bloodhound(query)
790 |
791 | @mcp.tool()
792 | async def find_onprem_users_in_entra_groups():
793 | query = """
794 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZMemberOf]->(:AZGroup)
795 | RETURN p
796 | LIMIT 1000
797 | """
798 | return await query_bloodhound(query)
799 |
800 | @mcp.tool("templates_no_security_ext")
801 | async def find_templates_no_security_extension():
802 | query = """
803 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA)
804 | WHERE ct.nosecurityextension = true
805 | RETURN p
806 | LIMIT 1000
807 | """
808 | return await query_bloodhound(query)
809 |
810 | @mcp.tool("templates_with_user_san")
811 | async def find_templates_with_user_specified_san():
812 | query = """
813 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(eca:EnterpriseCA)
814 | WHERE eca.isuserspecifiessanenabled = True
815 | RETURN p
816 | LIMIT 1000
817 | """
818 | return await query_bloodhound(query)
819 |
820 | @mcp.tool()
821 | async def find_ca_administrators():
822 | query = """
823 | MATCH p = (:Base)-[:ManageCertificates|ManageCA]->(:EnterpriseCA)
824 | RETURN p
825 | LIMIT 1000
826 | """
827 | return await query_bloodhound(query)
828 |
829 | @mcp.tool("onprem_users_direct_entra_roles")
830 | async def find_onprem_users_with_direct_entra_roles():
831 | query = """
832 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZHasRole]->(:AZRole)
833 | RETURN p
834 | LIMIT 1000
835 | """
836 | return await query_bloodhound(query)
837 |
838 | @mcp.tool("onprem_users_group_entra_roles")
839 | async def find_onprem_users_with_group_entra_roles():
840 | query = """
841 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZMemberOf]->(:AZGroup)-[:AZHasRole]->(:AZRole)
842 | RETURN p
843 | LIMIT 1000
844 | """
845 | return await query_bloodhound(query)
846 |
847 | @mcp.tool("onprem_users_direct_azure_roles")
848 | async def find_onprem_users_with_direct_azure_roles():
849 | query = """
850 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZOwner|AZUserAccessAdministrator|AZGetCertificates|AZGetKeys|AZGetSecrets|AZAvereContributor|AZKeyVaultContributor|AZContributor|AZVMAdminLogin|AZVMContributor|AZAKSContributor|AZAutomationContributor|AZLogicAppContributor|AZWebsiteContributor]->(:AZBase)
851 | RETURN p
852 | LIMIT 1000
853 | """
854 | return await query_bloodhound(query)
855 |
856 | @mcp.tool("onprem_users_group_azure_roles")
857 | async def find_onprem_users_with_group_azure_roles():
858 | query = """
859 | MATCH p = (:User)-[:SyncedToEntraUser]->(:AZUser)-[:AZMemberOf]->(:AZGroup)-[:AZOwner|AZUserAccessAdministrator|AZGetCertificates|AZGetKeys|AZGetSecrets|AZAvereContributor|AZKeyVaultContributor|AZContributor|AZVMAdminLogin|AZVMContributor|AZAKSContributor|AZAutomationContributor|AZLogicAppContributor|AZWebsiteContributor]->(:AZBase)
860 | RETURN p
861 | LIMIT 1000
862 | """
863 | return await query_bloodhound(query)
864 |
865 | if __name__ == "__main__":
866 | if verify_connectivity():
867 | try:
868 | logger.info("Starting MCP server...")
869 | mcp.run(transport="stdio")
870 | finally:
871 | driver.close()
872 | else:
873 | logger.error("Failed to establish Neo4j connection. Please check your credentials and connection settings.")
```