Skip to content

Commit 11a8873

Browse files
feat(ui): add attack path custom query skill for Lighthouse AI (#10323)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
1 parent 5a3475b commit 11a8873

File tree

4 files changed

+320
-1
lines changed

4 files changed

+320
-1
lines changed

ui/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ All notable changes to the **Prowler UI** are documented in this file.
66

77
### 🚀 Added
88

9-
- Add skill system to Lighthouse AI [(#10322)](https://github.com/prowler-cloud/prowler/pull/10322)
9+
- Skill system to Lighthouse AI [(#10322)](https://github.com/prowler-cloud/prowler/pull/10322)
10+
- Skill for creating custom queries on Attack Paths [(#10323)](https://github.com/prowler-cloud/prowler/pull/10323)
1011

1112
### 🔄 Changed
1213

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import type { SkillDefinition } from "../types";
2+
3+
export const customAttackPathQuerySkill: SkillDefinition = {
4+
metadata: {
5+
id: "attack-path-custom-query",
6+
name: "Attack Paths Custom Query",
7+
description:
8+
"Write an openCypher graph query against Cartography-ingested cloud infrastructure to analyze attack paths, privilege escalation, and network exposure.",
9+
},
10+
instructions: `# Attack Paths Custom Query Skill
11+
12+
This skill provides openCypher syntax guidance and Cartography schema knowledge for writing graph queries against Prowler's cloud infrastructure data.
13+
14+
## Workflow
15+
16+
Follow these steps when the user asks you to write a custom openCypher query:
17+
18+
1. **Find a completed scan**: Use \`prowler_app_list_attack_paths_scans\` (filter by \`state=['completed']\`) to find a scan for the user's provider. You need the \`scan_id\` for the next step.
19+
20+
2. **Fetch the Cartography schema**: Use \`prowler_app_get_attack_paths_cartography_schema\` with the \`scan_id\`. This returns the full schema markdown with all node labels, relationships, and properties for the scan's provider and Cartography version. If this tool fails, use the Cartography Schema Reference section below as a fallback (AWS only).
21+
22+
3. **Analyze the schema**: From \`schema_content\`, identify the node labels, properties, and relationships relevant to the user's request. Cross-reference with the Common openCypher Patterns section below.
23+
24+
4. **Write the query**: Compose the openCypher query following all rules in this skill:
25+
- Scope every MATCH to the root account node (see Provider Isolation)
26+
- Use \`$provider_uid\` and \`$provider_id\` parameters (see Query Parameters)
27+
- Include \`ProwlerFinding\` OPTIONAL MATCH (see Include Prowler Findings)
28+
- Use openCypher v9 compatible syntax only (see openCypher Version 9 Compatibility)
29+
30+
5. **Present the query**: Show the complete query in a \`cypher\` code block with:
31+
- A brief explanation of what the query finds
32+
- The node types and relationships it traverses
33+
- What results to expect
34+
35+
**Note**: Custom queries cannot be executed through the available tools yet. Present the query to the user for review and manual execution.
36+
37+
## Query Parameters
38+
39+
All queries receive these built-in parameters (do NOT hardcode these values):
40+
41+
| Parameter | Matches property | Used on | Purpose |
42+
|-----------|-----------------|---------|---------|
43+
| \`$provider_uid\` | \`id\` | \`AWSAccount\` | Scopes to a specific cloud account |
44+
| \`$provider_id\` | \`_provider_id\` | Any non-account node | Scopes nodes to the provider context |
45+
46+
Use \`$provider_uid\` on account root nodes. Use \`$provider_id\` on other nodes that need provider scoping (e.g., \`Internet\`).
47+
48+
## openCypher Query Guidelines
49+
50+
### Provider Isolation (CRITICAL)
51+
52+
Every query MUST chain from the root account node to prevent cross-provider data leakage.
53+
The tenant database contains data from multiple providers.
54+
55+
\`\`\`cypher
56+
// CORRECT: scoped to the specific account's subgraph
57+
MATCH (aws:AWSAccount {id: $provider_uid})--(role:AWSRole)
58+
WHERE role.name = 'admin'
59+
60+
// WRONG: matches ALL AWSRoles across all providers
61+
MATCH (role:AWSRole) WHERE role.name = 'admin'
62+
\`\`\`
63+
64+
Every \`MATCH\` clause must connect to the \`aws\` variable (or another variable already bound to the account's subgraph). An unanchored \`MATCH\` returns nodes from all providers.
65+
66+
**Exception**: The \`Internet\` sentinel node uses \`OPTIONAL MATCH\` with \`_provider_id\` for scoping instead of chaining from \`aws\`.
67+
68+
### Include Prowler Findings
69+
70+
Always include Prowler findings to enrich results with security context:
71+
72+
\`\`\`cypher
73+
UNWIND nodes(path) as n
74+
OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid})
75+
76+
RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr
77+
\`\`\`
78+
79+
For network exposure queries, also return the internet node and relationship:
80+
81+
\`\`\`cypher
82+
RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr,
83+
internet, can_access
84+
\`\`\`
85+
86+
### openCypher Version 9 Compatibility
87+
88+
Queries must use openCypher Version 9 (compatible with both Neo4j and Amazon Neptune).
89+
90+
| Avoid | Reason | Use instead |
91+
|-------|--------|-------------|
92+
| APOC procedures (\`apoc.*\`) | Neo4j-specific plugin | Real nodes and relationships in the graph |
93+
| Neptune extensions | Not available in Neo4j | Standard openCypher |
94+
| \`reduce()\` function | Not in openCypher spec | \`UNWIND\` + \`collect()\` |
95+
| \`FOREACH\` clause | Not in openCypher spec | \`WITH\` + \`UNWIND\` + \`SET\` |
96+
| Regex operator (\`=~\`) | Not supported in Neptune | \`toLower()\` + exact match, or \`CONTAINS\`/\`STARTS WITH\` |
97+
| \`CALL () { UNION }\` | Complex, hard to maintain | Multi-label OR in WHERE (see patterns below) |
98+
99+
**Supported with limitations:**
100+
- \`CALL\` subqueries require \`WITH\` clause to import variables
101+
102+
## Cartography Schema Reference (Quick Reference / Fallback)
103+
104+
### AWS Node Labels
105+
106+
| Label | Description |
107+
|-------|-------------|
108+
| \`AWSAccount\` | AWS account root node |
109+
| \`AWSPrincipal\` | IAM principal (user, role, service) |
110+
| \`AWSRole\` | IAM role |
111+
| \`AWSUser\` | IAM user |
112+
| \`AWSPolicy\` | IAM policy |
113+
| \`AWSPolicyStatement\` | Policy statement with effect, action, resource |
114+
| \`EC2Instance\` | EC2 instance |
115+
| \`EC2SecurityGroup\` | Security group |
116+
| \`EC2PrivateIp\` | EC2 private IP (has \`public_ip\`) |
117+
| \`IpPermissionInbound\` | Inbound security group rule |
118+
| \`IpRange\` | IP range (e.g., \`0.0.0.0/0\`) |
119+
| \`NetworkInterface\` | ENI (has \`public_ip\`) |
120+
| \`ElasticIPAddress\` | Elastic IP (has \`public_ip\`) |
121+
| \`S3Bucket\` | S3 bucket |
122+
| \`RDSInstance\` | RDS database instance |
123+
| \`LoadBalancer\` | Classic ELB |
124+
| \`LoadBalancerV2\` | ALB/NLB |
125+
| \`ELBListener\` | Classic ELB listener |
126+
| \`ELBV2Listener\` | ALB/NLB listener |
127+
| \`LaunchTemplate\` | EC2 launch template |
128+
| \`AWSTag\` | Resource tag with key/value properties |
129+
130+
### Prowler-Specific Labels
131+
132+
| Label | Description |
133+
|-------|-------------|
134+
| \`ProwlerFinding\` | Prowler finding node with \`status\`, \`provider_uid\`, \`severity\` properties |
135+
| \`Internet\` | Internet sentinel node, scoped by \`_provider_id\` (used in network exposure queries) |
136+
137+
### Common Relationships
138+
139+
| Relationship | Description |
140+
|-------------|-------------|
141+
| \`TRUSTS_AWS_PRINCIPAL\` | Role trust relationship |
142+
| \`STS_ASSUMEROLE_ALLOW\` | Can assume role (variable-length for chains) |
143+
| \`CAN_ACCESS\` | Internet-to-resource exposure link |
144+
| \`POLICY\` | Has policy attached |
145+
| \`STATEMENT\` | Policy has statement |
146+
147+
### Key Properties
148+
149+
- \`AWSAccount\`: \`id\` (account ID used with \`$provider_uid\`)
150+
- \`AWSPolicyStatement\`: \`effect\` ('Allow'/'Deny'), \`action\` (list), \`resource\` (list)
151+
- \`EC2Instance\`: \`exposed_internet\` (boolean), \`publicipaddress\`
152+
- \`EC2PrivateIp\`: \`public_ip\`
153+
- \`NetworkInterface\`: \`public_ip\`
154+
- \`ElasticIPAddress\`: \`public_ip\`
155+
- \`EC2SecurityGroup\`: \`name\`, \`id\`
156+
- \`IpPermissionInbound\`: \`toport\`, \`fromport\`, \`protocol\`
157+
- \`S3Bucket\`: \`name\`, \`anonymous_access\` (boolean)
158+
- \`RDSInstance\`: \`storage_encrypted\` (boolean)
159+
- \`ProwlerFinding\`: \`status\` ('FAIL'/'PASS'/'MANUAL'), \`severity\`, \`provider_uid\`
160+
- \`Internet\`: \`_provider_id\` (provider UUID used with \`$provider_id\`)
161+
162+
## Common openCypher Patterns
163+
164+
### Match Account and Principal
165+
166+
\`\`\`cypher
167+
MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement)
168+
\`\`\`
169+
170+
### Check IAM Action Permissions
171+
172+
\`\`\`cypher
173+
WHERE stmt.effect = 'Allow'
174+
AND any(action IN stmt.action WHERE
175+
toLower(action) = 'iam:passrole'
176+
OR toLower(action) = 'iam:*'
177+
OR action = '*'
178+
)
179+
\`\`\`
180+
181+
### Find Roles Trusting a Service
182+
183+
\`\`\`cypher
184+
MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {arn: 'ec2.amazonaws.com'})
185+
\`\`\`
186+
187+
### Check Resource Scope
188+
189+
\`\`\`cypher
190+
WHERE any(resource IN stmt.resource WHERE
191+
resource = '*'
192+
OR target_role.arn CONTAINS resource
193+
OR resource CONTAINS target_role.name
194+
)
195+
\`\`\`
196+
197+
### Match Internet Sentinel Node
198+
199+
Used in network exposure queries. The Internet node is a real graph node, scoped by \`_provider_id\`:
200+
201+
\`\`\`cypher
202+
OPTIONAL MATCH (internet:Internet {_provider_id: $provider_id})
203+
\`\`\`
204+
205+
### Link Internet to Exposed Resource
206+
207+
The \`CAN_ACCESS\` relationship links the Internet node to exposed resources:
208+
209+
\`\`\`cypher
210+
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(resource)
211+
\`\`\`
212+
213+
### Multi-label OR (match multiple resource types)
214+
215+
When a query needs to match different resource types in the same position, use label checks in WHERE:
216+
217+
\`\`\`cypher
218+
MATCH path = (aws:AWSAccount {id: $provider_uid})-[r]-(x)-[q]-(y)
219+
WHERE (x:EC2PrivateIp AND x.public_ip = $ip)
220+
OR (x:EC2Instance AND x.publicipaddress = $ip)
221+
OR (x:NetworkInterface AND x.public_ip = $ip)
222+
OR (x:ElasticIPAddress AND x.public_ip = $ip)
223+
\`\`\`
224+
225+
## Example Query Patterns
226+
227+
### Resource Inventory
228+
229+
\`\`\`cypher
230+
MATCH path = (aws:AWSAccount {id: $provider_uid})--(rds:RDSInstance)
231+
232+
UNWIND nodes(path) as n
233+
OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid})
234+
235+
RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr
236+
\`\`\`
237+
238+
### Network Exposure
239+
240+
\`\`\`cypher
241+
// Match the Internet sentinel node
242+
OPTIONAL MATCH (internet:Internet {_provider_id: $provider_id})
243+
244+
// Match exposed resources (MUST chain from aws)
245+
MATCH path = (aws:AWSAccount {id: $provider_uid})--(resource:EC2Instance)
246+
WHERE resource.exposed_internet = true
247+
248+
// Link Internet to resource
249+
OPTIONAL MATCH (internet)-[can_access:CAN_ACCESS]->(resource)
250+
251+
UNWIND nodes(path) as n
252+
OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid})
253+
254+
RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr,
255+
internet, can_access
256+
\`\`\`
257+
258+
### IAM Permission Check
259+
260+
\`\`\`cypher
261+
MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement)
262+
WHERE stmt.effect = 'Allow'
263+
AND any(action IN stmt.action WHERE
264+
toLower(action) = 'iam:passrole'
265+
OR toLower(action) = 'iam:*'
266+
OR action = '*'
267+
)
268+
269+
UNWIND nodes(path_principal) as n
270+
OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid})
271+
272+
RETURN path_principal, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr
273+
\`\`\`
274+
275+
### Privilege Escalation (Role Assumption Chain)
276+
277+
\`\`\`cypher
278+
// Find principals with iam:PassRole
279+
MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal)--(policy:AWSPolicy)--(stmt:AWSPolicyStatement)
280+
WHERE stmt.effect = 'Allow'
281+
AND any(action IN stmt.action WHERE
282+
toLower(action) = 'iam:passrole'
283+
OR toLower(action) = 'iam:*'
284+
OR action = '*'
285+
)
286+
287+
// Find target roles trusting a service
288+
MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {arn: 'ec2.amazonaws.com'})
289+
WHERE any(resource IN stmt.resource WHERE
290+
resource = '*'
291+
OR target_role.arn CONTAINS resource
292+
OR resource CONTAINS target_role.name
293+
)
294+
295+
UNWIND nodes(path_principal) + nodes(path_target) as n
296+
OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding {status: 'FAIL', provider_uid: $provider_uid})
297+
298+
RETURN path_principal, path_target,
299+
collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr
300+
\`\`\`
301+
302+
## Best Practices
303+
304+
1. **Always scope by provider**: Use \`{id: $provider_uid}\` on \`AWSAccount\` nodes. Use \`{_provider_id: $provider_id}\` on non-account nodes that need provider scoping (e.g., \`Internet\`).
305+
2. **Chain all MATCHes from the root account node**: Every \`MATCH\` must connect to the \`aws\` variable. The \`Internet\` node is the only exception (uses \`OPTIONAL MATCH\` with \`_provider_id\`).
306+
3. **Include Prowler findings**: Always add the \`OPTIONAL MATCH\` for \`ProwlerFinding\` nodes.
307+
4. **Return distinct findings**: Use \`collect(DISTINCT pf)\` to avoid duplicates.
308+
5. **Comment the query purpose**: Add inline comments explaining each \`MATCH\` clause.
309+
6. **Use alternatives for unsupported features**: Replace \`=~\` with \`toLower()\` + exact match or \`CONTAINS\`/\`STARTS WITH\`. Replace \`reduce()\` with \`UNWIND\` + \`collect()\`.
310+
`,
311+
};

ui/lib/lighthouse/skills/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import { customAttackPathQuerySkill } from "./definitions/attack-path-custom-query";
2+
import { registerSkill } from "./registry";
3+
4+
// Explicit registration — tree-shake-proof
5+
registerSkill(customAttackPathQuerySkill);
6+
17
// Re-export registry functions and types
28
export {
39
getAllSkillMetadata,

ui/lib/lighthouse/workflow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ const ALLOWED_TOOLS = new Set([
8787
"prowler_app_list_attack_paths_queries",
8888
"prowler_app_list_attack_paths_scans",
8989
"prowler_app_run_attack_paths_query",
90+
"prowler_app_get_attack_paths_cartography_schema",
9091
]);
9192

9293
/**

0 commit comments

Comments
 (0)