├── requirements.txt ├── .gitattributes ├── images └── BloodHound-MCP-Banner.png ├── .github └── FUNDING.yml ├── README.md └── BloodHound-MCP.py /requirements.txt: -------------------------------------------------------------------------------- 1 | neo4j 2 | python-dotenv 3 | mcp-server>=0.1.0 4 | fastmcp -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /images/BloodHound-MCP-Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorDavid/BloodHound-MCP-AI/HEAD/images/BloodHound-MCP-Banner.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | "\\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 | -------------------------------------------------------------------------------- /BloodHound-MCP.py: -------------------------------------------------------------------------------- 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.") --------------------------------------------------------------------------------