#
tokens: 10877/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![BloodHound-MCP](/images/BloodHound-MCP-Banner.png)
  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 | [![Star History Chart](https://api.star-history.com/svg?repos=MorDavid/BloodHound-MCP-AI&type=Date)](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 | [![Verified on MseeP](https://mseep.ai/badge.svg)](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.") 
```