# Directory Structure
```
├── .gitignore
├── .python-version
├── img
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
├── main.py
├── pyproject.toml
├── README.md
├── server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.13
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # BloodHound MCP
2 | BloodHound MCP (Model Context Protocol) is an innovative extension of the BloodHound tool, designed to enable Large Language Models (LLMs) to interact with and analyze Active Directory (AD) and Azure Active Directory (AAD) environments through natural language queries. By leveraging the power of LLMs, BloodHound MCP allows users to perform complex queries and retrieve insights from their AD/AAD environments using simple, conversational commands.
3 |
4 | ## Features
5 | - **Natural Language Queries**: Use conversational language to query your AD/AAD environment without needing to write Cypher queries manually.
6 | - **LLM-Powered Analysis**: Harness the capabilities of Large Language Models to interpret and execute queries on your behalf.
7 | - **Seamless Integration**: Works with existing BloodHound data stored in Neo4j, providing a user-friendly interface for complex analysis.
8 | - **Customizable**: Easily configure the system to work with your specific environment and tools.
9 |
10 | ## Configure the MCP Server
11 | ```json
12 | {
13 | "mcpServers": {
14 | "BloodHound": {
15 | "name": "BloodHound",
16 | "isActive": true,
17 | "command": "uv",
18 | "args": [
19 | "run",
20 | "--with",
21 | "mcp[cli],neo4j",
22 | "mcp",
23 | "run",
24 | "<PATH_TO_THE_PROJECT>server.py"
25 | ],
26 | "env": {
27 | "BLOODHOUND_URI": "bolt://localhost:7687",
28 | "BLOODHOUND_USERNAME": "neo4j",
29 | "BLOODHOUND_PASSWORD": "bloodhound"
30 | }
31 | }
32 | }
33 | }
34 | ```
35 |
36 |
37 | ## Usage
38 | 
39 | 
40 | 
41 |
42 | ## Configuration
43 | To customize BloodHound MCP, update the configuration file in your MCP-supported tool. Key settings include:
44 |
45 | - Neo4j Database Connection:
46 | - `BLOODHOUND_URI`: The URI of your Neo4j database (e.g., bolt://localhost:7687).
47 | - `BLOODHOUND_USERNAME`: Your Neo4j username.
48 | - `BLOODHOUND_PASSWORD`: Your Neo4j password.
49 | - Server Settings: Adjust the command and args to match your environment and tool requirements.
50 |
51 | ## Contributing
52 | We welcome contributions to BloodHound MCP! To get involved:
53 |
54 | 1. Fork the Repository: Create your own copy on GitHub.
55 | 2. Create a Branch: Work on your feature or fix in a new branch.
56 | 3. Submit a Pull Request: Include a clear description of your changes.
57 |
58 |
59 | ## Special Thanks
60 | Custom queries from : https://github.com/CompassSecurity/BloodHoundQueries
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
1 | def main():
2 | print("Hello from bloodhound-mcp!")
3 |
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "bloodhound-mcp"
3 | version = "0.1.0"
4 | description = "Add your description here"
5 | readme = "README.md"
6 | requires-python = ">=3.13"
7 | dependencies = [
8 | "mcp[cli]>=1.6.0",
9 | "neo4j>=5.28.1",
10 | ]
11 |
```
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
```python
1 | from mcp.server.fastmcp import FastMCP
2 | from neo4j import GraphDatabase
3 | import json
4 | from typing import List, Dict, Any, Optional
5 | import inspect
6 | import os
7 |
8 | # Create an MCP server
9 | mcp = FastMCP("BloodHound", port=3000)
10 |
11 | # 環境變數設定
12 | BLOODHOUND_URI = os.getenv("BLOODHOUND_URI", "bolt://localhost:7687")
13 | BLOODHOUND_USERNAME = os.getenv("BLOODHOUND_USERNAME", "neo4j")
14 | BLOODHOUND_PASSWORD = os.getenv("BLOODHOUND_PASSWORD", "bloodhound")
15 |
16 | @mcp.prompt()
17 | def prompt():
18 | p = """
19 | # System Prompt for MCP Server Script
20 |
21 | You are an AI assistant integrated with the MCP Server, designed to help users query and analyze Active Directory (AD) and Azure Active Directory (AAD) environments using a Neo4j database populated with BloodHound data. Your primary role is to assist users by executing predefined queries or crafting custom queries to retrieve the information they need.
22 |
23 | ## Available Tools
24 |
25 | You have access to a set of predefined tools (Python functions) that correspond to specific BloodHound queries. Each tool is designed to perform a particular task, such as listing users, groups, computers, or finding paths between nodes. These tools are categorized for ease of use:
26 |
27 | - **Top 10**: Queries to find top users, computers, or sessions based on specific criteria.
28 | - **Domain / Macro**: Queries to list domains, trusts, users, computers, and other macro-level information.
29 | - **Owned**: Queries related to owned users, computers, and their relationships.
30 | - **Non-privileged**: Queries focusing on non-privileged users and their permissions.
31 | - **Privilege Escalation / Lateral Movement**: Queries to identify potential privilege escalation paths and lateral movement opportunities.
32 | - **Privileged**: Queries related to privileged users and groups.
33 | - **Persistence**: Queries to find persistence mechanisms like dangerous rights to AdminSDHolder or DCSync capabilities.
34 | - **AAD**: Queries specific to Azure Active Directory, including tenancy, groups, and privileged access.
35 |
36 | Each tool is accessible via a unique identifier, such as `tool://users_with_most_local_admin_rights`, and may require parameters like the domain name.
37 |
38 | ## Using Tools
39 |
40 | When a user asks a question or requests information, your first step is to determine which predefined tool(s) can be used to answer the query. For example:
41 |
42 | - If a user asks for "users with the most local admin rights in domain X," you should use the `users_with_most_local_admin_rights` tool with the specified domain.
43 | - If a user wants to "list all enabled users in domain Y," you should use the `list_enabled_users` tool with the appropriate domain.
44 |
45 | Many tools require a domain parameter. If the user does not specify a domain, you should attempt to infer it from the context or query the database to find relevant domains. If you cannot determine the domain, ask the user for clarification.
46 |
47 | ## Handling Parameters
48 |
49 | - **Domain Parameter**: Most tools require a domain name. If not provided, try to:
50 | - Check if the user has previously mentioned a domain.
51 | - Query the database to list available domains using `list_domains` and ask the user to specify one.
52 | - **Other Parameters**: Some tools may require additional parameters (e.g., specific object names). If these are missing, attempt to resolve them by querying related information or ask the user for more details.
53 |
54 | ## Custom Queries
55 |
56 | If the predefined tools do not suffice for a user's request, you can write and execute custom Neo4j Cypher queries using the `run_query` tool (`tool://run_query`). This allows you to handle complex or unique queries that are not covered by the predefined functions.
57 |
58 | - **When to Use Custom Queries**: Use custom queries when:
59 | - The user's request does not match any predefined tool.
60 | - The request requires combining multiple queries or filtering results in a specific way.
61 | - **Writing Custom Queries**: Ensure that your custom queries are correctly formatted Cypher queries. You can use the Neo4j documentation or examples from the predefined queries as a reference.
62 | **Note**:
63 | When users inquire whether a specific account (e.g., '[email protected]') has permissions to modify other objects (such as other users, computers, etc.), you will need to write a Cypher query similar to the one below to check these permissions. These permissions might include `GenericWrite`, `GenericAll`, `WriteDacl`, `WriteOwner`, etc., and multiple queries may be necessary.
64 |
65 | ### ***Custom Query Example**:
66 | ```cypher
67 | MATCH p=(u1:User {name: '[email protected]'})-[:MemberOf*0..]->()-[:GenericWrite]->(u2:User)
68 | RETURN DISTINCT u2.name```
69 |
70 | **Description**:
71 | This query is used to check if the user '[email protected]' has the `GenericWrite` permission on other users through their group membership relationships. You can adjust the type of permission or the type of target object according to your needs.
72 |
73 |
74 | ## Error Handling and Missing Information
75 |
76 | - **Missing Parameters**: If a required parameter is missing, try to resolve it by:
77 | - Querying the database for possible values (e.g., listing domains or objects).
78 | - Asking the user for the missing information.
79 | - **Query Failures**: If a query fails or returns no results, consider:
80 | - Rephrasing the query or adjusting parameters.
81 | - Checking for typos or incorrect assumptions in the query.
82 | - Informing the user that no results were found and asking for clarification.
83 | - **Ambiguous Requests**: If a user's request is ambiguous, ask for more details to ensure you understand their needs correctly.
84 |
85 | ## Examples
86 |
87 | - **User Request**: "Find users with the most local admin rights in domain CONTOSO."
88 | - **Action**: Use `users_with_most_local_admin_rights` with `domain='CONTOSO'`.
89 | - **User Request**: "List all domains."
90 | - **Action**: Use `list_domains` (no parameters needed).
91 | - **User Request**: "Find paths from owned users to high-value targets in domain FABRIKAM."
92 | - **Action**: Use `route_from_owned_enabled_principals_to_high_value_targets` with `domain='FABRIKAM'`.
93 | - **User Request**: "Show me all users who can perform DCSync."
94 | - **Action**: Since there is no predefined tool, write a custom query using `run_query` to find users with DCSync rights.
95 |
96 | ## Guidelines
97 |
98 | - **Be Proactive**: Try to resolve missing information or ambiguities by querying the database or inferring from context before asking the user.
99 | - **Be Clear**: When asking the user for clarification, be specific about what information is needed.
100 | - **Be Efficient**: Use the most direct tool or query to answer the user's request.
101 | - **Be Informative**: Provide context or explanations when necessary, especially for complex queries or results.
102 | - **注意大小寫,Neo4j 有大小寫之分**
103 | - **優先使用內建的工具,如果沒有再進行 Custom Query**
104 | - route_non_privileged_users_with_dangerous_permissions 是一個非常常使用的工具,請優先使用
105 | - 使用 Custom Query 時,請務必使用不同的 Query String 執行 3 次並統整答案
106 | - 請多使用不同的工具組合,使用越多越好,盡可能不要單純只用一個來完成事情
107 |
108 | ## Tone and Style
109 |
110 | Maintain a professional, helpful, and instructive tone. Your responses should be clear, concise, and focused on assisting the user in understanding and navigating the AD and AAD environments.
111 |
112 | """
113 | return p
114 |
115 | @mcp.tool()
116 | def run_query(query: str, parameters: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
117 | """
118 | 執行Cypher查詢並返回結果
119 |
120 | Args:
121 | query: Cypher查詢字符串
122 | parameters: 查詢參數字典
123 |
124 | Returns:
125 | 查詢結果列表
126 | """
127 | driver = GraphDatabase.driver(
128 | BLOODHOUND_URI,
129 | auth=(BLOODHOUND_USERNAME, BLOODHOUND_PASSWORD)
130 | )
131 | try:
132 | with driver.session() as session:
133 | result = session.run(query, parameters or {})
134 | return [record.data() for record in result]
135 | finally:
136 | driver.close()
137 |
138 | @mcp.tool()
139 | def users_with_most_local_admin_rights(domain):
140 | """
141 | [WIP] Users with Most Local Admin Rights
142 | """
143 | query = "MATCH (n:User {domain: '%s'}),(m:Computer), (n)-[r:AdminTo]->(m) WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON' AND NOT n.name='' WITH n, count(r) as rel_count order by rel_count desc LIMIT 10 MATCH p=(m)<-[r:AdminTo]-(n) RETURN p" % domain
144 | return run_query(query)
145 |
146 | @mcp.tool()
147 | def computers_with_most_sessions(domain):
148 | """
149 | [WIP] Computers with Most Sessions [Required: sessions]
150 | """
151 | # Note: The provided query seems incorrect (matches Users with AdminTo instead of Computers with HasSession).
152 | # Correcting based on the name and intent.
153 | query = "MATCH (m:Computer {domain: '%s'}),(n:User), (m)-[r:HasSession]->(n) WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON' AND NOT n.name='' WITH m, count(r) as rel_count order by rel_count desc LIMIT 10 MATCH p=(m)-[r:HasSession]->(n) RETURN p" % domain
154 | return run_query(query)
155 |
156 | @mcp.tool()
157 | def users_with_most_sessions(domain):
158 | """
159 | [WIP] Users with Most Sessions [Required: sessions]
160 | """
161 | query = "MATCH (n:User {domain: '%s'}),(m:Computer), (n)<-[r:HasSession]-(m) WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON' AND NOT n.name='' WITH n, count(r) as rel_count order by rel_count desc LIMIT 10 MATCH p=(m)-[r:HasSession]->(n) RETURN p" % domain
162 | return run_query(query)
163 |
164 | @mcp.tool()
165 | def non_privileged_users_with_dangerous_permissions(domain):
166 | """
167 | List non-privileged user(s) with dangerous permissions to any node type
168 | """
169 | query = "MATCH (u:User {enabled: true, admincount: false, domain: '%s'})-[r]->(a) RETURN u, COUNT(DISTINCT type(r)) AS permissions ORDER BY permissions DESC LIMIT 10" % domain
170 | return run_query(query)
171 |
172 | @mcp.tool()
173 | def route_non_privileged_users_with_dangerous_permissions(domain):
174 | """
175 | Route non-privileged user(s) with dangerous permissions to any node type
176 | """
177 | query = "MATCH (u:User {enabled: true, admincount: false, domain: '%s'})-[r]->(a) WITH u, COUNT(DISTINCT type(r)) AS permissions ORDER BY permissions DESC LIMIT 10 MATCH p=allshortestpaths((u)-[r]->(a)) WHERE NOT u = a RETURN p" % domain
178 | return run_query(query)
179 |
180 | @mcp.tool()
181 | def users_with_most_cross_domain_sessions(domain):
182 | """
183 | [WIP] Users with most cross-domain sessions [Required: sessions]
184 | """
185 | query = "MATCH p=(g1:Group)<-[:MemberOf*1..]-(u:User {enabled:true, domain: '%s'})<-[r:HasSession]-(c:Computer) WHERE NOT u.domain = c.domain WITH u, count(r) as rel_count order by rel_count desc LIMIT 10 MATCH p=(c:Computer)-[r:HasSession]->(u) WHERE NOT u.domain = c.domain RETURN p ORDER BY c.name" % domain
186 | return run_query(query)
187 |
188 |
189 | @mcp.tool()
190 | def list_high_value_targets(domain):
191 | """
192 | List high value target(s)
193 | """
194 | query = "MATCH (a {highvalue: true, domain: '%s'}) RETURN a" % domain
195 | return run_query(query)
196 |
197 | @mcp.tool()
198 | def list_domains():
199 | """
200 | List domain(s)
201 | """
202 | query = "MATCH (d:Domain) RETURN d"
203 | return run_query(query)
204 |
205 | @mcp.tool()
206 | def list_domain_trusts():
207 | """
208 | List domain trust(s)
209 | """
210 | query = "MATCH p=(n:Domain)-->(m:Domain) RETURN p"
211 | return run_query(query)
212 |
213 | @mcp.tool()
214 | def list_enabled_users(domain):
215 | """
216 | List enabled user(s)
217 | """
218 | query = "MATCH (u:User {enabled:true, domain: '%s'}) RETURN u" % domain
219 | return run_query(query)
220 |
221 | @mcp.tool()
222 | def list_enabled_users_with_email(domain):
223 | """
224 | List enabled user(s) with an email address
225 | """
226 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE exists(u.email) RETURN u" % domain
227 | return run_query(query)
228 |
229 | @mcp.tool()
230 | def list_non_managed_service_accounts(domain):
231 | """
232 | List non-managed service account(s)
233 | """
234 | query = "MATCH (u:User {hasspn:true, domain: '%s'}) WHERE NOT u.name CONTAINS '$' AND NOT u.name CONTAINS 'KRBTGT' RETURN u" % domain
235 | return run_query(query)
236 |
237 | @mcp.tool()
238 | def list_enabled_principals_with_unconstrained_delegation(domain):
239 | """
240 | List enabled principal(s) with \"Unconstrained Delegation\"
241 | """
242 | query = "MATCH (a {unconstraineddelegation: true, enabled: true, domain: '%s'}) RETURN a" % domain
243 | return run_query(query)
244 |
245 | @mcp.tool()
246 | def list_enabled_principals_with_constrained_delegation(domain):
247 | """
248 | List enabled principal(s) with \"Constrained Delegation\"
249 | """
250 | query = "MATCH (a {enabled: true, domain: '%s'}) WHERE exists(a.`allowedtodelegate`) RETURN a" % domain
251 | return run_query(query)
252 |
253 | @mcp.tool()
254 | def list_domain_controllers(domain):
255 | """
256 | List domain controller(s)
257 | """
258 | query = "MATCH (c:Computer {domain: '%s'})-[:MemberOf]->(g:Group) WHERE g.samaccountname CONTAINS 'Domain Controllers' RETURN c" % domain
259 | return run_query(query)
260 |
261 | @mcp.tool()
262 | def list_domain_computers(domain):
263 | """
264 | List domain computer(s)
265 | """
266 | query = "MATCH (c:Computer {domain: '%s'}) RETURN c" % domain
267 | return run_query(query)
268 |
269 | @mcp.tool()
270 | def list_certificate_authority_servers(domain):
271 | """
272 | List Certificate Authority server(s) [Required: Certipy]
273 | """
274 | query = "MATCH (n:GPO {type:'Enrollment Service', domain: '%s'}) RETURN n" % domain
275 | return run_query(query)
276 |
277 | @mcp.tool()
278 | def list_privileges_for_certificate_authority_servers(domain):
279 | """
280 | [WIP] List privileges for Certificate Authority server(s) [Required: Certipy]
281 | """
282 | # Note: This query has an intermediate step requiring a CA selection ($result in final query).
283 | # For simplicity, assuming domain-level execution; adjust if CA name is parameterized differently.
284 | query = "MATCH p=(g)-[:ManageCa|ManageCertificates|Auditor|Operator|Read|Enroll]->(n:GPO {type:'Enrollment Service', domain: '%s'}) return p" % domain
285 | return run_query(query)
286 |
287 | @mcp.tool()
288 | def list_all_certificate_templates(domain):
289 | """
290 | List all Certificate Template(s) [Required: Certipy]
291 | """
292 | query = "MATCH (n:GPO {type:'Certificate Template', domain: '%s'}) RETURN n" % domain
293 | return run_query(query)
294 |
295 | @mcp.tool()
296 | def find_enabled_certificate_templates(domain):
297 | """
298 | Find enabled Certificate Template(s) [Required: Certipy]
299 | """
300 | query = "MATCH (n:GPO {enabled:true, type:'Certificate Template', domain: '%s'}) RETURN n" % domain
301 | return run_query(query)
302 |
303 | @mcp.tool()
304 | def list_all_enrollment_rights_for_certificate_templates(domain):
305 | """
306 | [WIP] List all Enrollment Right(s) for Certificate Template(s)
307 | """
308 | # Note: Requires a template selection step; simplified to domain scope here.
309 | query = "MATCH p=(g)-[:Enroll|AutoEnroll]->(n:GPO {type:'Certificate Template', domain: '%s'}) return p" % domain
310 | return run_query(query)
311 |
312 | @mcp.tool()
313 | def list_computers_without_laps(domain):
314 | """
315 | List computer(s) WITHOUT LAPS
316 | """
317 | query = "MATCH (c:Computer {haslaps:false, domain: '%s'}) RETURN c ORDER BY c.name" % domain
318 | return run_query(query)
319 |
320 | @mcp.tool()
321 | def list_network_shares_ignoring_sysvol(domain):
322 | """
323 | List network share(s), ignoring SYSVOL
324 | """
325 | query = "MATCH (a {domain: '%s'}) WHERE (any(prop in keys(a) where a[prop] contains '\\\\' and not a[prop] contains 'SYSVOL')) RETURN a" % domain
326 | return run_query(query)
327 |
328 | @mcp.tool()
329 | def list_all_groups(domain):
330 | """
331 | List all group(s)
332 | """
333 | query = "MATCH (g:Group {domain: '%s'}) RETURN g" % domain
334 | return run_query(query)
335 |
336 | @mcp.tool()
337 | def list_all_gpos(domain):
338 | """
339 | List all GPO(s)
340 | """
341 | query = "MATCH (g:GPO {domain: '%s'}) RETURN g" % domain
342 | return run_query(query)
343 |
344 | @mcp.tool()
345 | def list_all_principals_with_local_admin_permission(domain):
346 | """
347 | List all principal(s) with \"Local Admin\" permission
348 | """
349 | query = "MATCH p=(a {domain: '%s'})-[:MemberOf|AdminTo*1..]->(c:Computer) RETURN p" % domain
350 | return run_query(query)
351 |
352 | @mcp.tool()
353 | def list_all_principals_with_rdp_permission(domain):
354 | """
355 | List all principal(s) with \"RDP\" permission
356 | """
357 | query = "MATCH p=(a {domain: '%s'})-[:MemberOf|CanRDP*1..]->(c:Computer) RETURN p" % domain
358 | return run_query(query)
359 |
360 | @mcp.tool()
361 | def list_all_principals_with_sqladmin_permission(domain):
362 | """
363 | List all principal(s) with \"SQLAdmin\" permission
364 | """
365 | query = "MATCH p=(a {domain: '%s'})-[:MemberOf|SQLAdmin*1..]->(c:Computer) RETURN p" % domain
366 | return run_query(query)
367 |
368 | @mcp.tool()
369 | def list_all_user_sessions(domain):
370 | """
371 | List all user session(s) [Required: sessions]
372 | """
373 | query = "MATCH p=(u:User {domain: '%s'})<-[r:HasSession]-(c:Computer) RETURN p" % domain
374 | return run_query(query)
375 |
376 | @mcp.tool()
377 | def list_all_users_with_description_field(domain):
378 | """
379 | List all user(s) with description field
380 | """
381 | query = "MATCH (u:User {domain: '%s'}) WHERE u.description IS NOT null RETURN u" % domain
382 | return run_query(query)
383 |
384 | @mcp.tool()
385 | def list_all_enabled_users_with_userpassword_attribute(domain):
386 | """
387 | List all enabled user(s) with \"userpassword\" attribute
388 | """
389 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE u.userpassword IS NOT null RETURN u" % domain
390 | return run_query(query)
391 |
392 | @mcp.tool()
393 | def list_all_enabled_users_with_password_never_expires(domain):
394 | """
395 | List all enabled user(s) with \"password never expires\" attribute
396 | """
397 | query = "MATCH (u:User {pwdneverexpires:true, enabled:true, domain: '%s'}) RETURN u" % domain
398 | return run_query(query)
399 |
400 | @mcp.tool()
401 | def list_enabled_users_pwd_never_expires_unchanged_1yr(domain):
402 | """
403 | List all enabled user(s) with \"password never expires\" attribute and not changed in last year
404 | """
405 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE u.pwdneverexpires=TRUE AND u.pwdlastset < (datetime().epochseconds - (365 * 86400)) and NOT u.pwdlastset IN [-1.0, 0.0] RETURN u" % domain
406 | return run_query(query)
407 |
408 | @mcp.tool()
409 | def list_all_enabled_users_with_no_password_required(domain):
410 | """
411 | List all enabled user(s) with \"don't require passwords\" attribute
412 | """
413 | query = "MATCH (u:User {passwordnotreqd:true, enabled:true, domain: '%s'}) RETURN u" % domain
414 | return run_query(query)
415 |
416 | @mcp.tool()
417 | def list_all_enabled_users_never_logged_in(domain):
418 | """
419 | List all enabled user(s) but never logged in
420 | """
421 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE u.lastlogontimestamp=-1.0 RETURN u" % domain
422 | return run_query(query)
423 |
424 | @mcp.tool()
425 | def list_all_enabled_users_logged_in_last_90_days(domain):
426 | """
427 | List all enabled user(s) that logged in within the last 90 days
428 | """
429 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE u.lastlogon < (datetime().epochseconds - (90 * 86400)) and NOT u.lastlogon IN [-1.0, 0.0] RETURN u" % domain
430 | return run_query(query)
431 |
432 | @mcp.tool()
433 | def list_all_enabled_users_set_password_last_90_days(domain):
434 | """
435 | List all enabled user(s) that set password within the last 90 days
436 | """
437 | query = "MATCH (u:User {enabled:true, domain: '%s'}) WHERE u.pwdlastset < (datetime().epochseconds - (90 * 86400)) and NOT u.pwdlastset IN [-1.0, 0.0] RETURN u" % domain
438 | return run_query(query)
439 |
440 | @mcp.tool()
441 | def list_all_enabled_users_with_foreign_group_membership(domain):
442 | """
443 | List all enabled user(s) with foreign group membership
444 | """
445 | query = "MATCH p=(u:User {enabled:true, domain: '%s'})-[:MemberOf*1..]->(g:Group) WHERE NOT u.domain = g.domain RETURN p" % domain
446 | return run_query(query)
447 |
448 | @mcp.tool()
449 | def list_all_owned_users(domain):
450 | """
451 | List all owned user(s)
452 | """
453 | query = "MATCH (u:User {owned:true, domain: '%s'}) RETURN u" % domain
454 | return run_query(query)
455 |
456 | @mcp.tool()
457 | def list_all_owned_enabled_users(domain):
458 | """
459 | List all owned & enabled user(s)
460 | """
461 | query = "MATCH (u:User {owned:true, enabled:true, domain: '%s'}) RETURN u" % domain
462 | return run_query(query)
463 |
464 | @mcp.tool()
465 | def list_all_owned_enabled_users_with_email(domain):
466 | """
467 | List all owned & enabled user(s) with an email address
468 | """
469 | query = "MATCH (u:User {owned:true, enabled:true, domain: '%s'}) WHERE exists(u.email) RETURN u" % domain
470 | return run_query(query)
471 |
472 | @mcp.tool()
473 | def list_own_en_usrs_local_adm_sess(domain):
474 | """
475 | List all owned & enabled user(s) with \"Local Admin\" permission, and any active sessions and their group membership(s)
476 | """
477 | query = "MATCH p=(u:User {owned:true, enabled: true, domain: '%s'})-[:MemberOf|AdminTo*1..]->(c:Computer) OPTIONAL MATCH p2=(c)-[:HasSession]->(u2:User) OPTIONAL MATCH p3=(u2:User)-[:MemberOf*1..]->(:Group) RETURN p, p2, p3" % domain
478 | return run_query(query)
479 |
480 | @mcp.tool()
481 | def list_all_owned_enabled_users_with_rdp_and_sessions(domain):
482 | """
483 | List all owned & enabled user(s) with \"RDP\" permission, and any active sessions and their group membership(s)
484 | """
485 | query = "MATCH p=(u:User {owned:true, enabled: true, domain: '%s'})-[:MemberOf|CanRDP*1..]->(c:Computer) OPTIONAL MATCH p2=(c)-[:HasSession]->(u2:User) OPTIONAL MATCH p3=(u2:User)-[:MemberOf*1..]->(:Group) RETURN p, p2, p3" % domain
486 | return run_query(query)
487 |
488 | @mcp.tool()
489 | def list_all_owned_enabled_users_with_sqladmin(domain):
490 | """
491 | List all owned & enabled user(s) with \"SQLAdmin\" permission
492 | """
493 | query = "MATCH p=(u:User {owned:true, enabled: true, domain: '%s'})-[:MemberOf|SQLAdmin*1..]->(c:Computer) RETURN p" % domain
494 | return run_query(query)
495 |
496 | @mcp.tool()
497 | def list_all_owned_computers(domain):
498 | """
499 | List all owned computer(s)
500 | """
501 | query = "MATCH (c:Computer {owned:true, domain: '%s'}) RETURN c ORDER BY c.name" % domain
502 | return run_query(query)
503 |
504 | @mcp.tool()
505 | def route_all_owned_enabled_group_memberships(domain):
506 | """
507 | Route all owned & enabled group membership(s)
508 | """
509 | query = "MATCH p=(u:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf*1..]->(g:Group) RETURN p" % domain
510 | return run_query(query)
511 |
512 | @mcp.tool()
513 | def route_all_owned_enabled_non_privileged_group_memberships(domain):
514 | """
515 | Route all owned & enabled non-privileged group(s) membership
516 | """
517 | query = "MATCH p=(u:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf*1..]->(g:Group {admincount:false}) RETURN p" % domain
518 | return run_query(query)
519 |
520 | @mcp.tool()
521 | def route_all_owned_enabled_privileged_group_memberships(domain):
522 | """
523 | Route all owned & enabled privileged group(s) membership
524 | """
525 | query = "MATCH p=(u:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf*1..]->(g:Group {admincount:true}) RETURN p" % domain
526 | return run_query(query)
527 |
528 | @mcp.tool()
529 | def route_owned_users_dangerous_rights_to_any(domain):
530 | """
531 | Route all owned & enabled user(s) with Dangerous Rights to any node type
532 | """
533 | query = "MATCH p=allshortestpaths((u:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a)) WHERE NOT a = u RETURN p" % domain
534 | return run_query(query)
535 |
536 | @mcp.tool()
537 | def route_owned_users_dangerous_rights_to_groups(domain):
538 | """
539 | Route all owned & enabled user(s) with Dangerous Rights to group(s)
540 | """
541 | query = "MATCH p=allshortestpaths((u:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(:Group)) RETURN p" % domain
542 | return run_query(query)
543 |
544 | @mcp.tool()
545 | def route_own_en_usrs_dang_rts_usrs(domain):
546 | """
547 | Route all owned & enabled user(s) with Dangerous Rights to user(s)
548 | """
549 | query = "MATCH p=allshortestpaths((o:User {owned:true, enabled:true, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(u:User)) WHERE NOT o = u RETURN p" % domain
550 | return run_query(query)
551 |
552 | @mcp.tool()
553 | def route_own_en_usrs_unconst_del(domain):
554 | """
555 | Route from owned & enabled user(s) to all principals with \"Unconstrained Delegation\"
556 | """
557 | query = "MATCH p=allshortestpaths((o:User {owned:true, enabled:true, domain: '%s'})-[*]->(a {unconstraineddelegation: true, enabled: true})) WHERE NOT o = a RETURN p" % domain
558 | return run_query(query)
559 |
560 | @mcp.tool()
561 | def route_from_owned_enabled_principals_to_high_value_targets(domain):
562 | """
563 | Route from owned & enabled principals to high value target(s)
564 | """
565 | query = "MATCH p=allShortestPaths((o {owned:true, enabled:true, domain: '%s'})-[*]->(a {highvalue: true})) WHERE NOT o=a RETURN p" % domain
566 | return run_query(query)
567 |
568 | @mcp.tool()
569 | def find_owned_users_with_azure_tenancy_access(domain):
570 | """
571 | Owned: [WIP] Find all owned user with privileged access to Azure Tenancy (Required: azurehound)
572 | """
573 | query = "MATCH p=(n {owned:true, enabled:true, domain: '%s'})-[r:MemberOf|AZGlobalAdmin|AZPrivilegedRoleAdmin*1..]->(:AZTenant) RETURN p" % domain
574 | return run_query(query)
575 |
576 | @mcp.tool()
577 | def find_owned_users_with_group_granted_azure_access(domain):
578 | """
579 | Owned: [WIP] Find all owned user where group membership grants privileged access to Azure Tenancy (Required: azurehound)
580 | """
581 | query = "MATCH p=(n {owned:true, enabled:true, domain: '%s'})-[:MemberOf*1..]->(g:Group)-[r:AZGlobalAdmin|AZPrivilegedRoleAdmin*1..]->(:AZTenant) RETURN p" % domain
582 | return run_query(query)
583 |
584 | @mcp.tool()
585 | def find_azure_app_owners_with_dangerous_rights(domain):
586 | """
587 | Owned: [WIP] Find all Owners of Azure Applications with Owners to Service Principals with Dangerous Rights (Required: azurehound)
588 | """
589 | query = "MATCH p = (n {enabled:true, owned:true, domain: '%s'})-[:AZOwns]->(azapp:AZApp)-[r1]->(azsp:AZServicePrincipal)-[r:AZGlobalAdmin|AZPrivilegedRoleAdmin*1..]->(azt:AZTenant) RETURN p" % domain
590 | return run_query(query)
591 |
592 | @mcp.tool()
593 | def find_all_owned_groups_granting_network_share_access(domain):
594 | """
595 | Find all owned groups that grant access to network shares
596 | """
597 | query = "MATCH p=(u:User {owned:true, domain: '%s'})-[:MemberOf*1..]->(g:Group) where (any(prop in keys(g) where g[prop] contains '\\\\')) RETURN p" % domain
598 | return run_query(query)
599 |
600 | @mcp.tool()
601 | def route_all_sessions_to_computers_without_laps(domain):
602 | """
603 | Route all sessions to computers WITHOUT LAPS (Required: sessions)
604 | """
605 | query = "MATCH p=(u:User {owned:true, domain: '%s'})<-[r:HasSession]-(c:Computer {haslaps:false}) RETURN p ORDER BY c.name" % domain
606 | return run_query(query)
607 |
608 | @mcp.tool()
609 | def route_all_sessions_to_computers(domain):
610 | """
611 | Route all sessions to computers (Required: sessions)
612 | """
613 | query = "MATCH p=(u:User {owned:true, domain: '%s'})<-[r:HasSession]-(c:Computer) RETURN p ORDER BY c.name" % domain
614 | return run_query(query)
615 |
616 | @mcp.tool()
617 | def list_enabled_non_privileged_users_with_local_admin(domain):
618 | """
619 | List enabled non-privileged user(s) with \"Local Admin\" permission
620 | """
621 | query = "MATCH p=(u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|AdminTo*1..]->(c:Computer) RETURN p" % domain
622 | return run_query(query)
623 |
624 | @mcp.tool()
625 | def list_non_priv_users_with_admin_and_sessions(domain):
626 | """
627 | List enabled non-privileged user(s) with \"Local Admin\" permission, and any active sessions and their group membership(s)
628 | """
629 | query = "MATCH p=(u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|AdminTo*1..]->(c:Computer) OPTIONAL MATCH p2=(c)-[:HasSession]->(u2:User) OPTIONAL MATCH p3=(u2:User)-[:MemberOf*1..]->(:Group) RETURN p, p2, p3" % domain
630 | return run_query(query)
631 |
632 | @mcp.tool()
633 | def list_enabled_non_privileged_users_with_rdp(domain):
634 | """
635 | List enabled non-privileged user(s) with \"RDP\" permission
636 | """
637 | query = "MATCH p=(u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|CanRDP*1..]->(c:Computer) RETURN p" % domain
638 | return run_query(query)
639 |
640 | @mcp.tool()
641 | def list_enabled_non_privileged_users_with_rdp_and_sessions(domain):
642 | """
643 | List enabled non-privileged user(s) with \"RDP\" permission, and any active sessions and their group membership(s)
644 | """
645 | query = "MATCH p=(u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|CanRDP*1..]->(c:Computer) OPTIONAL MATCH p2=(c)-[:HasSession]->(u2:User) OPTIONAL MATCH p3=(u2:User)-[:MemberOf*1..]->(:Group) RETURN p, p2, p3" % domain
646 | return run_query(query)
647 |
648 | @mcp.tool()
649 | def list_enabled_non_privileged_users_with_sqladmin(domain):
650 | """
651 | List enabled non-privileged user(s) with \"SQLAdmin\" permission
652 | """
653 | query = "MATCH p=(u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|SQLAdmin*1..]->(c:Computer) RETURN p" % domain
654 | return run_query(query)
655 |
656 | @mcp.tool()
657 | def list_all_domain_users_group_memberships(domain):
658 | """
659 | List all \"Domain Users\" group membership(s)
660 | """
661 | query = "MATCH p=(g1:Group {domain: '%s'})-[:MemberOf*1..]->(g2:Group) WHERE g1.name STARTS WITH 'DOMAIN USERS' RETURN p ORDER BY g2.name" % domain
662 | return run_query(query)
663 |
664 | @mcp.tool()
665 | def list_all_authenticated_users_group_memberships(domain):
666 | """
667 | List all \"Authenticated Users\" group membership(s)
668 | """
669 | query = "MATCH p=(g1:Group {domain: '%s'})-[:MemberOf*1..]->(g2:Group) WHERE g1.name STARTS WITH 'AUTHENTICATED USERS' RETURN p ORDER BY g2.name" % domain
670 | return run_query(query)
671 |
672 | @mcp.tool()
673 | def find_all_enabled_as_rep_roastable_users(domain):
674 | """
675 | Find all enabled AS-REP roastable user(s)
676 | """
677 | query = "MATCH (u:User {dontreqpreauth: true, enabled:true, domain: '%s'}) WHERE NOT u.name CONTAINS '$' and NOT u.name CONTAINS 'KRBTGT' RETURN u" % domain
678 | return run_query(query)
679 |
680 | @mcp.tool()
681 | def find_all_enabled_kerberoastable_users(domain):
682 | """
683 | Find all enabled kerberoastable user(s)
684 | """
685 | query = "MATCH (u:User {hasspn: true, enabled:true, domain: '%s'}) WHERE NOT u.name CONTAINS '$' and NOT u.name CONTAINS 'KRBTGT' RETURN u" % domain
686 | return run_query(query)
687 |
688 | @mcp.tool()
689 | def route_non_privileged_users_with_dangerous_rights_to_users(domain):
690 | """
691 | Route non-privileged user(s) with dangerous rights to user(s) [HIGH RAM]
692 | """
693 | query = "MATCH p=allshortestpaths((u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:User)) WHERE NOT u = a RETURN p" % domain
694 | return run_query(query)
695 |
696 | @mcp.tool()
697 | def route_non_priv_usrs_dang_rts_grps(domain):
698 | """
699 | Route non-privileged user(s) with dangerous rights to group(s) [HIGH RAM]
700 | """
701 | query = "MATCH p=allshortestpaths((u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:Group)) WHERE NOT u = a RETURN p" % domain
702 | return run_query(query)
703 |
704 | @mcp.tool()
705 | def route_non_priv_users_dangerous_rights_to_comps(domain):
706 | """
707 | Route non-privileged user(s) with dangerous rights to computer(s) [HIGH RAM]
708 | """
709 | query = "MATCH p=allshortestpaths((u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:Computer)) WHERE NOT u = a RETURN p" % domain
710 | return run_query(query)
711 |
712 | @mcp.tool()
713 | def route_non_privileged_users_with_dangerous_rights_to_gpos(domain):
714 | """
715 | Route non-privileged user(s) with dangerous rights to GPO(s) [HIGH RAM]
716 | """
717 | query = "MATCH p=allshortestpaths((u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:GPO)) WHERE NOT u = a RETURN p" % domain
718 | return run_query(query)
719 |
720 | @mcp.tool()
721 | def route_non_priv_users_dangerous_rights_to_priv_nodes(domain):
722 | """
723 | Route non-privileged user(s) with dangerous rights to privileged node(s) [HIGH RAM]
724 | """
725 | query = "MATCH p=allshortestpaths((u:User {enabled: true, admincount: false, domain: '%s'})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a {admincount: true})) WHERE NOT u = a RETURN p" % domain
726 | return run_query(query)
727 |
728 | @mcp.tool()
729 | def route_non_priv_comps_dangerous_rights_to_users(domain):
730 | """
731 | Route non-privileged computer(s) with dangerous rights to user(s) [HIGH RAM]
732 | """
733 | query = "MATCH p=allshortestpaths((c:Computer {admincount: false})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:User {domain: '%s'})) WHERE NOT c = a RETURN p" % domain
734 | return run_query(query)
735 |
736 | @mcp.tool()
737 | def route_non_priv_comps_dangerous_rights_to_groups(domain):
738 | """
739 | Route non-privileged computer(s) with dangerous rights to group(s) [HIGH RAM]
740 | """
741 | query = "MATCH p=allshortestpaths((c:Computer {admincount: false})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:Group {domain: '%s'})) WHERE NOT c = a RETURN p" % domain
742 | return run_query(query)
743 |
744 | @mcp.tool()
745 | def route_non_priv_comps_dangerous_rights_to_comps(domain):
746 | """
747 | Route non-privileged computer(s) with dangerous rights to computer(s) [HIGH RAM]
748 | """
749 | query = "MATCH p=allshortestpaths((c:Computer {admincount: false})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:Computer {domain: '%s'})) WHERE NOT c = a RETURN p" % domain
750 | return run_query(query)
751 |
752 | @mcp.tool()
753 | def route_non_priv_comps_dangerous_rights_to_gpos(domain):
754 | """
755 | Route non-privileged computer(s) with dangerous rights to GPO(s) [HIGH RAM]
756 | """
757 | query = "MATCH p=allshortestpaths((c:Computer {admincount: false})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a:GPO {domain: '%s'})) WHERE NOT c = a RETURN p" % domain
758 | return run_query(query)
759 |
760 | @mcp.tool()
761 | def route_non_priv_comps_dangerous_rights_to_priv_nodes(domain):
762 | """
763 | Route non-privileged computer(s) with dangerous rights to privileged node(s) [HIGH RAM]
764 | """
765 | query = "MATCH p=allshortestpaths((c:Computer {admincount: false})-[:MemberOf|Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|AllowedToDelegate|ForceChangePassword|AdminTo*1..]->(a {admincount: true, domain: '%s'})) WHERE NOT c = a RETURN p" % domain
766 | return run_query(query)
767 |
768 | @mcp.tool()
769 | def list_esc1_vulnerable_certificate_templates(domain):
770 | """
771 | List ESC1 vulnerable Certificate Template(s) [Required: Certipy]
772 | """
773 | query = "MATCH (n:GPO {Enabled:true, type:'Certificate Template', `Enrollee Supplies Subject`:true, `Client Authentication`:true, domain: '%s'}) RETURN n" % domain
774 | return run_query(query)
775 |
776 | @mcp.tool()
777 | def list_esc2_vulnerable_certificate_templates(domain):
778 | """
779 | List ESC2 vulnerable Certificate Template(s) [Required: Certipy]
780 | """
781 | query = "MATCH (n:GPO {Enabled:true, type:'Certificate Template', domain: '%s'}) WHERE (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage`) RETURN n" % domain
782 | return run_query(query)
783 |
784 | @mcp.tool()
785 | def list_esc3_vulnerable_certificate_templates(domain):
786 | """
787 | List ESC3 vulnerable Certificate Template(s) [Required: Certipy]
788 | """
789 | query = "MATCH (n:GPO {Enabled:true, type:'Certificate Template', domain: '%s'}) WHERE (n.`Extended Key Usage` = [] or 'Any Purpose' IN n.`Extended Key Usage` or 'Certificate Request Agent' IN n.`Extended Key Usage`) RETURN n" % domain
790 | return run_query(query)
791 |
792 | @mcp.tool()
793 | def list_esc4_vulnerable_certificate_templates(domain):
794 | """
795 | List ESC4 vulnerable Certificate Template(s) [Required: Certipy]
796 | """
797 | query = "MATCH p=shortestPath((g)-[:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner*1..]->(n:GPO {Enabled:true, type:'Certificate Template', domain: '%s'})) WHERE g<>n RETURN p" % domain
798 | return run_query(query)
799 |
800 | @mcp.tool()
801 | def list_esc6_vulnerable_certificate_templates(domain):
802 | """
803 | List ESC6 vulnerable Certificate Template(s) [Required: Certipy]
804 | """
805 | query = "MATCH (n:GPO {type:'Enrollment Service', `User Specified SAN`:'Enabled', domain: '%s'}) RETURN n" % domain
806 | return run_query(query)
807 |
808 | @mcp.tool()
809 | def list_esc7_vulnerable_certificate_templates(domain):
810 | """
811 | List ESC7 vulnerable Certificate Template(s) [Required: Certipy]
812 | """
813 | query = "MATCH p=shortestPath((g)-[r:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ManageCa|ManageCertificates*1..]->(n:GPO {type:'Enrollment Service', domain: '%s'})) WHERE g<>n RETURN p" % domain
814 | return run_query(query)
815 |
816 | @mcp.tool()
817 | def list_esc8_vulnerable_certificate_templates(domain):
818 | """
819 | List ESC8 vulnerable Certificate Template(s) [Required: Certipy]
820 | """
821 | query = "MATCH (n:GPO {type:'Enrollment Service', `Web Enrollment`:'Enabled', domain: '%s'}) RETURN n" % domain
822 | return run_query(query)
823 |
824 | @mcp.tool()
825 | def list_all_cross_domain_user_sessions_and_memberships(domain):
826 | """
827 | List all cross-domain user session(s) and user group membership(s)
828 | """
829 | query = "MATCH p=(g1:Group)<-[:MemberOf*1..]-(u:User {enabled:true, domain: '%s'})<-[:HasSession]-(c:Computer) WHERE NOT u.domain = c.domain RETURN p ORDER BY c.name" % domain
830 | return run_query(query)
831 |
832 | @mcp.tool()
833 | def list_privileged_users_without_protected_users(domain):
834 | """
835 | List privileged user(s) without \"Protected Users\" group membership
836 | """
837 | query = "MATCH (u:User {admincount:true, domain: '%s'}), (c:Computer), (u)-[:MemberOf*1..]->(g) WHERE g.name CONTAINS 'Protected Users' WITH COLLECT(u) AS privilegedUsers MATCH (u2:User {admincount:true}) WHERE NOT u2 IN privilegedUsers RETURN u2" % domain
838 | return run_query(query)
839 |
840 | @mcp.tool()
841 | def list_custom_privileged_groups(domain):
842 | """
843 | List custom privileged group(s)
844 | """
845 | query = "MATCH (g:Group {admincount:true, highvalue:false, domain: '%s'}) WHERE NOT (g.objectid =~ '(?i)S-1-5-.*-512' or g.objectid =~ '(?i)S-1-5-.*-519' or g.objectid =~ '(?i)S-1-5-.*-544' or g.objectid =~ '(?i)S-1-5-.*-548' or g.objectid CONTAINS '-552' or g.objectid =~ '(?i)S-1-5-.*-526' or g.objectid =~ '(?i)S-1-5-.*-521' or g.objectid =~ '(?i)S-1-5-.*-527' or g.objectid =~ '(?i)S-1-5-.*-518') RETURN g" % domain
846 | return run_query(query)
847 |
848 | @mcp.tool()
849 | def list_en_svc_accts_priv_grp_mems(domain):
850 | """
851 | List all enabled SVC account(s) with privileged group membership(s)
852 | """
853 | query = "MATCH p=(u:User {enabled: true, hasspn: true, domain: '%s'})-[:MemberOf*1..]->(g:Group {admincount: true}) RETURN p" % domain
854 | return run_query(query)
855 |
856 | @mcp.tool()
857 | def route_priv_users_sessions_to_non_priv_comps(domain):
858 | """
859 | Route all privileged user(s) with sessions to non-privileged computer(s) [Required: sessions]
860 | """
861 | query = "MATCH (c:Computer), (u:User), (g:Group), (c)-[:MemberOf*1..]->(:Group {admincount:false}) MATCH p=(c)-[:HasSession]->(u {admincount:true, domain: '%s'}) RETURN p" % domain
862 | return run_query(query)
863 |
864 | @mcp.tool()
865 | def find_paths_dangerous_rights_to_adminsdholder(domain):
866 | """
867 | Find allshortestpaths with dangerous rights to AdminSDHolder object
868 | """
869 | query = "MATCH p=allshortestpaths((u:User {enabled:true, admincount:false, domain: '%s'})-[*]->(c:Container)) WHERE c.distinguishedname CONTAINS 'ADMINSDHOLDER' RETURN p" % domain
870 | return run_query(query)
871 |
872 | @mcp.tool()
873 | def find_allshortestpaths_with_dcsync_to_domain(domain):
874 | """
875 | Find allshortestpaths with DCSync to domain object
876 | """
877 | query = "MATCH p=allshortestpaths((u:User {enabled:true, admincount:false, domain: '%s'})-[r:MemberOf|DCSync*1..]->(:Domain)) RETURN p" % domain
878 | return run_query(query)
879 |
880 | @mcp.tool()
881 | def find_allshortestpaths_with_shadow_credential_permission(domain):
882 | """
883 | Find allshortestpaths with Shadow Credential permission to principal(s)
884 | """
885 | query = "MATCH p=allshortestpaths((a {domain: '%s'})-[:MemberOf|AddKeyCredentialLink*1..]->(b)) WHERE NOT a=b RETURN p" % domain
886 | return run_query(query)
887 |
888 | @mcp.tool()
889 | def list_all_tenancy():
890 | """
891 | List all Tenancy (Required: azurehound)
892 | """
893 | query = "MATCH (t:AZTenant) RETURN t"
894 | return run_query(query)
895 |
896 | @mcp.tool()
897 | def list_all_aad_groups_synchronized_with_ad():
898 | """
899 | [WIP] List all AAD Group(s) that are synchronized with AD (Required: azurehound)
900 | """
901 | query = "MATCH (n:Group) WHERE n.objectid CONTAINS 'S-1-5' AND n.azsyncid IS NOT NULL RETURN n"
902 | return run_query(query)
903 |
904 | @mcp.tool()
905 | def list_all_principals_used_for_syncing_ad_and_aad():
906 | """
907 | [WIP] List all principal(s) used for syncing AD and AAD
908 | """
909 | query = "MATCH (u) WHERE (u:User OR u:AZUser) AND (u.name =~ '(?i)^MSOL_|.*AADConnect.*' OR u.userprincipalname =~ '(?i)^sync_.*') OPTIONAL MATCH (u)-[:HasSession]->(s:Session) RETURN u, s"
910 | return run_query(query)
911 |
912 | @mcp.tool()
913 | def list_all_enabled_azure_users():
914 | """
915 | List all enabled Azure User(s) (Required: azurehound)
916 | """
917 | query = "MATCH (u:AZUser {enabled:true}) RETURN u"
918 | return run_query(query)
919 |
920 | @mcp.tool()
921 | def list_all_enabled_azure_users_group_memberships():
922 | """
923 | List all enabled Azure User(s) Azure Group membership(s) (Required: azurehound)
924 | """
925 | query = "MATCH p=(azu:AZUser {enabled:true})-[:MemberOf*1..]->(azg:AZGroup) RETURN p"
926 | return run_query(query)
927 |
928 | @mcp.tool()
929 | def list_all_ad_principals_with_edges_to_azure_principals():
930 | """
931 | [WIP] List all AD principal(s) with edge(s) to Azure principal(s) (Required: azurehound)
932 | """
933 | query = "MATCH p=(u:User)-[r:MemberOf|AZResetPassword|AZOwns|AZUserAccessAdministrator|AZContributor|AZAddMembers|AZGlobalAdmin|AZVMContributor|AZOwnsAZAvereContributor*1..]->(n) WHERE u.objectid CONTAINS 'S-1-5-21' RETURN p"
934 | return run_query(query)
935 |
936 | @mcp.tool()
937 | def list_principals_with_azure_tenancy_access():
938 | """
939 | [WIP] List all principal(s) with privileged access to Azure Tenancy (Required: azurehound)
940 | """
941 | query = "MATCH (a) WHERE (a:User OR a:AZUser) WITH a MATCH p=(a)-[r:MemberOf|AZGlobalAdmin|AZPrivilegedRoleAdmin*1..]->(azt:AZTenant) RETURN p"
942 | return run_query(query)
943 |
944 | @mcp.tool()
945 | def route_principals_to_azure_apps_and_sps():
946 | """
947 | [WIP] Route all principal(s) that have control permissions to Azure Application(s) running as Azure Service Principals (AzSP), and route from privileged ASP to Azure Tenancy (Required: azurehound)
948 | """
949 | query = "MATCH (a) WHERE (a:User OR a:AZUser) WITH a MATCH p=(a)-[:MemberOf|AZOwns|AZAppAdmin*1..]->(azapp:AZApp) OPTIONAL MATCH p2=(azapp)-[:AZRunsAs]->(azsp:AZServicePrincipal) OPTIONAL MATCH p3=(azsp)-[:MemberOf|AZGlobalAdmin|AZPrivilegedRoleAdmin*1..]->(azt:AZTenant) RETURN p, p2, p3"
950 | return run_query(query)
951 |
952 | @mcp.tool()
953 | def route_user_principals_to_azure_service_principals():
954 | """
955 | [WIP] Route all user principal(s) that have control permissions to Azure Service Principals (AzSP), and route from AzSP to principal(s) (Required: azurehound)
956 | """
957 | query = "MATCH (a) WHERE (a:User OR a:AZUser) WITH a MATCH p=allShortestPaths((a)-[*]->(azsp:AZServicePrincipal)-[*]->(b)) WHERE NOT a=b RETURN p"
958 | return run_query(query)
959 |
960 | @mcp.tool()
961 | def route_azure_users_with_dangerous_rights_to_users():
962 | """
963 | [WIP] Route from Azure User principal(s) that have dangerous rights to Azure User and User principal(s) (Required: azurehound)
964 | """
965 | query = "MATCH (a) WHERE (a:User OR a:AZUser) WITH a MATCH p=allShortestPaths((u:AZUser)-[*]->(a)) WHERE NOT a=u RETURN p"
966 | return run_query(query)
967 |
968 | @mcp.tool()
969 | def route_principals_to_azure_vm():
970 | """
971 | [WIP] Route from principal(s) to Azure VM (Required: azurehound)
972 | """
973 | query = "MATCH p=allshortestpaths((a)-[*]->(vm:AZVM)) WHERE NOT a=vm RETURN p"
974 | return run_query(query)
975 |
976 | @mcp.tool()
977 | def route_principals_to_global_administrators():
978 | """
979 | [WIP] Route from principal(s) to principal(s) with Global Administrator permissions (Required: azurehound)
980 | """
981 | query = "MATCH p=(ga)-[:AZGlobalAdmin|AZPrivilegedAdminRole*1..]->(:AZTenant) WHERE (ga:User OR ga:AZUser) WITH ga MATCH p=allshortestpaths((a)-[*]->(ga)) WHERE NOT a=ga RETURN p"
982 | return run_query(query)
983 |
984 |
985 |
986 | if __name__ == '__main__':
987 | try:
988 | mcp.run(transport='stdio')
989 | except Exception as e:
990 | print(f"啟動 MCP 服務器時發生錯誤: {e}")
```