cartography 0.118.0__py3-none-any.whl → 0.119.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cartography might be problematic. Click here for more details.
- cartography/_version.py +2 -2
- cartography/cli.py +20 -0
- cartography/client/core/tx.py +19 -3
- cartography/config.py +9 -0
- cartography/data/indexes.cypher +0 -6
- cartography/graph/job.py +7 -5
- cartography/intel/aws/__init__.py +21 -9
- cartography/intel/aws/ecr.py +7 -0
- cartography/intel/aws/ecr_image_layers.py +143 -42
- cartography/intel/aws/inspector.py +65 -33
- cartography/intel/aws/resourcegroupstaggingapi.py +1 -1
- cartography/intel/gcp/compute.py +3 -3
- cartography/intel/github/repos.py +23 -5
- cartography/intel/gsuite/__init__.py +12 -8
- cartography/intel/gsuite/groups.py +291 -0
- cartography/intel/gsuite/users.py +142 -0
- cartography/intel/okta/awssaml.py +1 -1
- cartography/intel/okta/users.py +1 -1
- cartography/intel/ontology/__init__.py +44 -0
- cartography/intel/ontology/devices.py +54 -0
- cartography/intel/ontology/users.py +54 -0
- cartography/intel/ontology/utils.py +121 -0
- cartography/models/airbyte/user.py +4 -0
- cartography/models/anthropic/user.py +4 -0
- cartography/models/aws/ecr/image.py +47 -0
- cartography/models/aws/iam/group_membership.py +3 -2
- cartography/models/aws/identitycenter/awsssouser.py +3 -1
- cartography/models/bigfix/bigfix_computer.py +1 -1
- cartography/models/cloudflare/member.py +4 -0
- cartography/models/crowdstrike/hosts.py +1 -1
- cartography/models/duo/endpoint.py +1 -1
- cartography/models/duo/phone.py +2 -2
- cartography/models/duo/user.py +4 -0
- cartography/models/entra/user.py +2 -1
- cartography/models/github/users.py +4 -0
- cartography/models/gsuite/__init__.py +0 -0
- cartography/models/gsuite/group.py +218 -0
- cartography/models/gsuite/tenant.py +29 -0
- cartography/models/gsuite/user.py +107 -0
- cartography/models/kandji/device.py +1 -2
- cartography/models/keycloak/user.py +4 -0
- cartography/models/lastpass/user.py +4 -0
- cartography/models/ontology/__init__.py +0 -0
- cartography/models/ontology/device.py +125 -0
- cartography/models/ontology/mapping/__init__.py +16 -0
- cartography/models/ontology/mapping/data/__init__.py +1 -0
- cartography/models/ontology/mapping/data/devices.py +160 -0
- cartography/models/ontology/mapping/data/users.py +239 -0
- cartography/models/ontology/mapping/specs.py +65 -0
- cartography/models/ontology/user.py +52 -0
- cartography/models/openai/user.py +4 -0
- cartography/models/scaleway/iam/user.py +4 -0
- cartography/models/snipeit/asset.py +1 -0
- cartography/models/snipeit/user.py +4 -0
- cartography/models/tailscale/device.py +1 -1
- cartography/models/tailscale/user.py +6 -1
- cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py +176 -89
- cartography/sync.py +3 -0
- cartography/util.py +44 -17
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/METADATA +1 -1
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/RECORD +65 -50
- cartography/data/jobs/cleanup/gsuite_ingest_groups_cleanup.json +0 -23
- cartography/data/jobs/cleanup/gsuite_ingest_users_cleanup.json +0 -11
- cartography/intel/gsuite/api.py +0 -355
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/WHEEL +0 -0
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/top_level.txt +0 -0
cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py
CHANGED
|
@@ -12,32 +12,43 @@ _aws_account_manipulation_permissions = Fact(
|
|
|
12
12
|
),
|
|
13
13
|
cypher_query="""
|
|
14
14
|
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
15
|
-
MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
15
|
+
MATCH (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
16
16
|
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
17
17
|
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
18
18
|
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
19
19
|
AND stmt.effect = 'Allow'
|
|
20
|
-
WITH a, principal, stmt,
|
|
21
|
-
// Return labels that are not the general "AWSPrincipal" label
|
|
20
|
+
WITH a, principal, stmt, policy,
|
|
22
21
|
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
WITH a, principal, principal_type, stmt,
|
|
27
|
-
// Filter on the desired IAM actions
|
|
22
|
+
[p IN ['iam:Create','iam:Attach','iam:Put','iam:Update','iam:Add'] | p] AS patterns
|
|
23
|
+
|
|
24
|
+
// Match only Allow statements whose actions fit the patterns
|
|
25
|
+
WITH a, principal, principal_type, stmt, policy,
|
|
28
26
|
[action IN stmt.action
|
|
29
27
|
WHERE ANY(prefix IN patterns WHERE action STARTS WITH prefix)
|
|
30
28
|
OR action = 'iam:*'
|
|
31
29
|
OR action = '*'
|
|
32
|
-
] AS
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
] AS matched_allow_actions
|
|
31
|
+
WHERE size(matched_allow_actions) > 0
|
|
32
|
+
|
|
33
|
+
// Find explicit Deny statements for the same principal that overlap
|
|
34
|
+
OPTIONAL MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(deny_stmt:AWSPolicyStatement {effect:"Deny"})
|
|
35
|
+
WHERE ANY(deny_action IN deny_stmt.action
|
|
36
|
+
WHERE deny_action IN matched_allow_actions
|
|
37
|
+
OR deny_action = 'iam:*'
|
|
38
|
+
OR deny_action = '*')
|
|
39
|
+
|
|
40
|
+
// If a deny exists, exclude those principals
|
|
41
|
+
WITH a, principal, principal_type, policy, stmt, matched_allow_actions, deny_stmt
|
|
42
|
+
WHERE deny_stmt IS NULL
|
|
43
|
+
|
|
44
|
+
UNWIND matched_allow_actions AS action
|
|
45
|
+
RETURN DISTINCT
|
|
46
|
+
a.name AS account,
|
|
37
47
|
principal.name AS principal_name,
|
|
38
48
|
principal.arn AS principal_arn,
|
|
39
49
|
principal_type,
|
|
40
|
-
|
|
50
|
+
policy.name AS policy_name,
|
|
51
|
+
collect(DISTINCT action) AS action,
|
|
41
52
|
stmt.resource AS resource
|
|
42
53
|
ORDER BY account, principal_name
|
|
43
54
|
""",
|
|
@@ -71,27 +82,42 @@ _aws_trust_relationship_manipulation = Fact(
|
|
|
71
82
|
),
|
|
72
83
|
cypher_query="""
|
|
73
84
|
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
74
|
-
MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
75
|
-
OPTIONAL MATCH (groupmember:AWSUser)
|
|
85
|
+
MATCH (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement {effect:"Allow"})
|
|
76
86
|
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
77
87
|
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
78
88
|
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
79
|
-
|
|
80
|
-
WITH a, principal, stmt,
|
|
89
|
+
WITH a, principal, policy, stmt,
|
|
81
90
|
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
82
|
-
['iam:UpdateAssumeRolePolicy','iam:CreateRole'] AS patterns
|
|
83
|
-
|
|
91
|
+
['iam:UpdateAssumeRolePolicy', 'iam:CreateRole'] AS patterns
|
|
92
|
+
|
|
93
|
+
// Filter for matching Allow actions
|
|
94
|
+
WITH a, principal, principal_type, stmt, policy,
|
|
84
95
|
[action IN stmt.action
|
|
85
96
|
WHERE ANY(p IN patterns WHERE action = p)
|
|
86
|
-
OR action = 'iam:*'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
OR action = 'iam:*'
|
|
98
|
+
OR action = '*'
|
|
99
|
+
] AS matched_allow_actions
|
|
100
|
+
WHERE size(matched_allow_actions) > 0
|
|
101
|
+
|
|
102
|
+
// Look for any explicit Deny statement on same principal that matches those actions
|
|
103
|
+
OPTIONAL MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(deny_stmt:AWSPolicyStatement {effect:"Deny"})
|
|
104
|
+
WHERE ANY(action IN deny_stmt.action
|
|
105
|
+
WHERE action IN matched_allow_actions
|
|
106
|
+
OR action = 'iam:*'
|
|
107
|
+
OR action = '*')
|
|
108
|
+
|
|
109
|
+
// Exclude principals with an explicit Deny that overlaps
|
|
110
|
+
WITH a, principal, principal_type, policy, stmt, matched_allow_actions, deny_stmt
|
|
111
|
+
WHERE deny_stmt IS NULL
|
|
112
|
+
|
|
113
|
+
UNWIND matched_allow_actions AS action
|
|
114
|
+
RETURN DISTINCT
|
|
115
|
+
a.name AS account,
|
|
91
116
|
principal.name AS principal_name,
|
|
92
117
|
principal.arn AS principal_arn,
|
|
118
|
+
policy.name AS policy_name,
|
|
93
119
|
principal_type,
|
|
94
|
-
collect(
|
|
120
|
+
collect(DISTINCT action) AS action,
|
|
95
121
|
stmt.resource AS resource
|
|
96
122
|
ORDER BY account, principal_name
|
|
97
123
|
""",
|
|
@@ -118,33 +144,58 @@ _aws_service_account_manipulation_via_ec2 = Fact(
|
|
|
118
144
|
MATCH (a:AWSAccount)-[:RESOURCE]->(ec2:EC2Instance)
|
|
119
145
|
MATCH (ec2)-[:INSTANCE_PROFILE]->(profile:AWSInstanceProfile)
|
|
120
146
|
MATCH (profile)-[:ASSOCIATED_WITH]->(role:AWSRole)
|
|
121
|
-
MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
[action IN
|
|
128
|
-
WHERE ANY(p IN
|
|
147
|
+
MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(allow_stmt:AWSPolicyStatement {effect:"Allow"})
|
|
148
|
+
WITH a, ec2, role, allow_stmt,
|
|
149
|
+
['iam:Create','iam:Attach','iam:Put','iam:Update','iam:Add'] AS patterns
|
|
150
|
+
|
|
151
|
+
// Step 1: Collect allowed actions that match IAM modification patterns
|
|
152
|
+
WITH a, ec2, role, patterns,
|
|
153
|
+
[action IN allow_stmt.action
|
|
154
|
+
WHERE ANY(p IN patterns WHERE action STARTS WITH p)
|
|
129
155
|
OR action = 'iam:*'
|
|
130
156
|
OR action = '*'
|
|
131
|
-
] AS
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
collect(
|
|
139
|
-
|
|
140
|
-
|
|
157
|
+
] AS matched_allow_actions
|
|
158
|
+
WHERE size(matched_allow_actions) > 0
|
|
159
|
+
|
|
160
|
+
// Step 2: Collect deny statements for the same role
|
|
161
|
+
OPTIONAL MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(deny_stmt:AWSPolicyStatement {effect:"Deny"})
|
|
162
|
+
WITH a, ec2, role, patterns, matched_allow_actions,
|
|
163
|
+
// Flatten the deny action lists manually
|
|
164
|
+
REDUCE(acc = [], ds IN collect(deny_stmt.action) | acc + ds) AS all_deny_actions
|
|
165
|
+
|
|
166
|
+
// Step 3: Compute effective = allows minus denies
|
|
167
|
+
WITH a, ec2, role, matched_allow_actions, all_deny_actions,
|
|
168
|
+
[action IN matched_allow_actions
|
|
169
|
+
WHERE NOT (
|
|
170
|
+
// Full wildcard Deny *
|
|
171
|
+
'*' IN all_deny_actions OR
|
|
172
|
+
// IAM category wildcard Deny iam:*
|
|
173
|
+
'iam:*' IN all_deny_actions OR
|
|
174
|
+
// Exact match deny
|
|
175
|
+
action IN all_deny_actions OR
|
|
176
|
+
// Prefix wildcards like Deny iam:Update*
|
|
177
|
+
ANY(d IN all_deny_actions WHERE d ENDS WITH('*') AND action STARTS WITH split(d,'*')[0])
|
|
178
|
+
)
|
|
179
|
+
] AS effective_actions
|
|
180
|
+
WHERE size(effective_actions) > 0
|
|
181
|
+
|
|
182
|
+
// Step 4: Optional internet exposure context
|
|
183
|
+
OPTIONAL MATCH (ec2 {exposed_internet: True})
|
|
184
|
+
-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
|
|
185
|
+
<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(ip:IpPermissionInbound)
|
|
186
|
+
|
|
187
|
+
UNWIND effective_actions AS action
|
|
188
|
+
WITH a, ec2, role, sg, ip, COLLECT(DISTINCT action) AS actions
|
|
189
|
+
RETURN DISTINCT
|
|
190
|
+
a.name AS account,
|
|
191
|
+
a.id AS account_id,
|
|
141
192
|
ec2.instanceid AS instance_id,
|
|
142
193
|
ec2.exposed_internet AS internet_accessible,
|
|
143
|
-
ec2.publicipaddress
|
|
194
|
+
ec2.publicipaddress AS public_ip_address,
|
|
144
195
|
role.name AS role_name,
|
|
145
|
-
|
|
146
|
-
ip.fromport
|
|
147
|
-
ip.toport
|
|
196
|
+
actions,
|
|
197
|
+
ip.fromport AS from_port,
|
|
198
|
+
ip.toport AS to_port
|
|
148
199
|
ORDER BY account, instance_id, internet_accessible, from_port
|
|
149
200
|
""",
|
|
150
201
|
cypher_visual_query="""
|
|
@@ -177,27 +228,49 @@ _aws_service_account_manipulation_via_lambda = Fact(
|
|
|
177
228
|
"AWS Lambda functions with IAM roles that can manipulate other AWS accounts."
|
|
178
229
|
),
|
|
179
230
|
cypher_query="""
|
|
180
|
-
// Find Lambda functions with account manipulation capabilities
|
|
231
|
+
// Find Lambda functions with IAM modification or account manipulation capabilities
|
|
181
232
|
MATCH (a:AWSAccount)-[:RESOURCE]->(lambda:AWSLambda)
|
|
182
233
|
MATCH (lambda)-[:STS_ASSUMEROLE_ALLOW]->(role:AWSRole)
|
|
183
|
-
MATCH (role)-[:POLICY]->(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
[action IN
|
|
190
|
-
WHERE ANY(p IN
|
|
234
|
+
MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(allow_stmt:AWSPolicyStatement {effect:"Allow"})
|
|
235
|
+
WITH a, lambda, role, allow_stmt,
|
|
236
|
+
['iam:Create','iam:Attach','iam:Put','iam:Update','iam:Add'] AS patterns
|
|
237
|
+
|
|
238
|
+
// Step 1: Gather allowed actions that match IAM modification patterns
|
|
239
|
+
WITH a, lambda, role, patterns,
|
|
240
|
+
[action IN allow_stmt.action
|
|
241
|
+
WHERE ANY(p IN patterns WHERE action STARTS WITH p)
|
|
191
242
|
OR action = 'iam:*'
|
|
192
243
|
OR action = '*'
|
|
193
|
-
] AS
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
244
|
+
] AS matched_allow_actions
|
|
245
|
+
WHERE size(matched_allow_actions) > 0
|
|
246
|
+
|
|
247
|
+
// Step 2: Gather all deny actions from the same role
|
|
248
|
+
OPTIONAL MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(deny_stmt:AWSPolicyStatement {effect:"Deny"})
|
|
249
|
+
WITH a, lambda, role, patterns, matched_allow_actions,
|
|
250
|
+
REDUCE(acc = [], ds IN collect(deny_stmt.action) | acc + ds) AS all_deny_actions
|
|
251
|
+
|
|
252
|
+
// Step 3: Subtract Deny actions from Allow actions
|
|
253
|
+
WITH a, lambda, role, matched_allow_actions, all_deny_actions,
|
|
254
|
+
[action IN matched_allow_actions
|
|
255
|
+
WHERE NOT (
|
|
256
|
+
// Global wildcard deny
|
|
257
|
+
'*' IN all_deny_actions OR
|
|
258
|
+
// IAM wildcard deny
|
|
259
|
+
'iam:*' IN all_deny_actions OR
|
|
260
|
+
// Exact match deny
|
|
261
|
+
action IN all_deny_actions OR
|
|
262
|
+
// Prefix wildcards like Deny iam:Update*
|
|
263
|
+
ANY(d IN all_deny_actions WHERE d ENDS WITH('*') AND action STARTS WITH split(d,'*')[0])
|
|
264
|
+
)
|
|
265
|
+
] AS effective_actions
|
|
266
|
+
WHERE size(effective_actions) > 0
|
|
267
|
+
|
|
268
|
+
// Step 4: Return only Lambdas with effective IAM modification capabilities
|
|
269
|
+
UNWIND effective_actions AS action
|
|
270
|
+
WITH a, lambda, role, COLLECT(DISTINCT action) AS actions
|
|
271
|
+
RETURN DISTINCT
|
|
272
|
+
a.name AS account,
|
|
273
|
+
a.id AS account_id,
|
|
201
274
|
lambda.arn AS arn,
|
|
202
275
|
lambda.description AS description,
|
|
203
276
|
lambda.anonymous_access AS internet_accessible,
|
|
@@ -232,37 +305,51 @@ _aws_policy_manipulation_capabilities = Fact(
|
|
|
232
305
|
),
|
|
233
306
|
cypher_query="""
|
|
234
307
|
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
235
|
-
MATCH (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(
|
|
308
|
+
MATCH (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(allow_stmt:AWSPolicyStatement {effect:"Allow"})
|
|
236
309
|
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
237
310
|
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
238
311
|
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
WITH a, principal, stmt,
|
|
242
|
-
// Return labels that are not the general "AWSPrincipal" label
|
|
312
|
+
WITH a, principal, policy, allow_stmt,
|
|
243
313
|
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'iam:
|
|
248
|
-
'iam:PutUserPolicy',
|
|
249
|
-
|
|
314
|
+
[
|
|
315
|
+
'iam:CreatePolicy','iam:CreatePolicyVersion',
|
|
316
|
+
'iam:AttachUserPolicy','iam:AttachRolePolicy','iam:AttachGroupPolicy',
|
|
317
|
+
'iam:DetachUserPolicy','iam:DetachRolePolicy','iam:DetachGroupPolicy',
|
|
318
|
+
'iam:PutUserPolicy','iam:PutRolePolicy','iam:PutGroupPolicy'
|
|
319
|
+
] AS patterns
|
|
250
320
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
321
|
+
// Step 1 – Collect (action, resource) pairs for allowed statements
|
|
322
|
+
UNWIND allow_stmt.action AS allow_action
|
|
323
|
+
WITH a, principal, principal_type, policy, allow_stmt, allow_action, patterns
|
|
324
|
+
WHERE ANY(p IN patterns WHERE allow_action = p)
|
|
325
|
+
OR allow_action = 'iam:*'
|
|
326
|
+
OR allow_action = '*'
|
|
327
|
+
WITH a, principal, principal_type, policy, allow_stmt, allow_action, allow_stmt.resource AS allow_resources
|
|
328
|
+
|
|
329
|
+
// Step 2 – Gather all Deny statements for the same principal
|
|
330
|
+
OPTIONAL MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(deny_stmt:AWSPolicyStatement {effect:"Deny"})
|
|
331
|
+
WITH a, principal, principal_type, policy, allow_action, allow_resources,
|
|
332
|
+
REDUCE(acc = [], ds IN collect(deny_stmt.action) | acc + ds) AS all_deny_actions
|
|
333
|
+
|
|
334
|
+
// Step 3 – Filter out denied actions (handles *, iam:*, exact, and prefix wildcards)
|
|
335
|
+
WHERE NOT (
|
|
336
|
+
'*' IN all_deny_actions OR
|
|
337
|
+
'iam:*' IN all_deny_actions OR
|
|
338
|
+
allow_action IN all_deny_actions OR
|
|
339
|
+
ANY(d IN all_deny_actions WHERE d ENDS WITH('*') AND allow_action STARTS WITH split(d,'*')[0])
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
// Step 4 – Preserve (action, resource) mapping
|
|
343
|
+
UNWIND allow_resources AS resource
|
|
344
|
+
RETURN DISTINCT
|
|
345
|
+
a.name AS account,
|
|
260
346
|
principal.name AS principal_name,
|
|
261
|
-
principal.arn
|
|
347
|
+
principal.arn AS principal_arn,
|
|
262
348
|
principal_type,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
349
|
+
policy.name AS policy_name,
|
|
350
|
+
allow_action AS action,
|
|
351
|
+
resource
|
|
352
|
+
ORDER BY account, principal_name, action, resource
|
|
266
353
|
""",
|
|
267
354
|
cypher_visual_query="""
|
|
268
355
|
MATCH p1=(a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
cartography/sync.py
CHANGED
|
@@ -36,6 +36,7 @@ import cartography.intel.kubernetes
|
|
|
36
36
|
import cartography.intel.lastpass
|
|
37
37
|
import cartography.intel.oci
|
|
38
38
|
import cartography.intel.okta
|
|
39
|
+
import cartography.intel.ontology
|
|
39
40
|
import cartography.intel.openai
|
|
40
41
|
import cartography.intel.pagerduty
|
|
41
42
|
import cartography.intel.scaleway
|
|
@@ -84,6 +85,7 @@ TOP_LEVEL_MODULES: OrderedDict[str, Callable[..., None]] = OrderedDict(
|
|
|
84
85
|
"pagerduty": cartography.intel.pagerduty.start_pagerduty_ingestion,
|
|
85
86
|
"trivy": cartography.intel.trivy.start_trivy_ingestion,
|
|
86
87
|
"sentinelone": cartography.intel.sentinelone.start_sentinelone_ingestion,
|
|
88
|
+
"ontology": cartography.intel.ontology.run,
|
|
87
89
|
# Analysis should be the last stage
|
|
88
90
|
"analysis": cartography.intel.analysis.run,
|
|
89
91
|
}
|
|
@@ -212,6 +214,7 @@ class Sync:
|
|
|
212
214
|
intel_module_info.name,
|
|
213
215
|
)
|
|
214
216
|
available_modules[intel_module_info.name] = v
|
|
217
|
+
available_modules["ontology"] = cartography.intel.ontology.run
|
|
215
218
|
available_modules["analysis"] = cartography.intel.analysis.run
|
|
216
219
|
return available_modules
|
|
217
220
|
|
cartography/util.py
CHANGED
|
@@ -35,6 +35,22 @@ from cartography.stats import ScopedStatsClient
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def is_service_control_policy_explicit_deny(
|
|
39
|
+
error: botocore.exceptions.ClientError,
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Return True if the ClientError was caused by an explicit service control policy deny."""
|
|
42
|
+
error_code = error.response.get("Error", {}).get("Code")
|
|
43
|
+
if error_code not in {"AccessDenied", "AccessDeniedException"}:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
message = error.response.get("Error", {}).get("Message")
|
|
47
|
+
if not message:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
lowered = message.lower()
|
|
51
|
+
return "explicit deny" in lowered and "service control policy" in lowered
|
|
52
|
+
|
|
53
|
+
|
|
38
54
|
STATUS_SUCCESS = 0
|
|
39
55
|
STATUS_FAILURE = 1
|
|
40
56
|
STATUS_KEYBOARD_INTERRUPT = 130
|
|
@@ -259,6 +275,20 @@ def backoff_handler(details: Dict) -> None:
|
|
|
259
275
|
)
|
|
260
276
|
|
|
261
277
|
|
|
278
|
+
# Error codes that indicate a service is unavailable in a region or blocked by policies
|
|
279
|
+
AWS_REGION_ACCESS_DENIED_ERROR_CODES = [
|
|
280
|
+
"AccessDenied",
|
|
281
|
+
"AccessDeniedException",
|
|
282
|
+
"AuthFailure",
|
|
283
|
+
"AuthorizationError",
|
|
284
|
+
"AuthorizationErrorException",
|
|
285
|
+
"InvalidClientTokenId",
|
|
286
|
+
"UnauthorizedOperation",
|
|
287
|
+
"UnrecognizedClientException",
|
|
288
|
+
"InternalServerErrorException",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
|
|
262
292
|
# TODO Move this to cartography.intel.aws.util.common
|
|
263
293
|
def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
|
|
264
294
|
"""
|
|
@@ -270,17 +300,6 @@ def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
|
|
|
270
300
|
|
|
271
301
|
This should be used on `get_` functions that normally return a list of items.
|
|
272
302
|
"""
|
|
273
|
-
ERROR_CODES = [
|
|
274
|
-
"AccessDenied",
|
|
275
|
-
"AccessDeniedException",
|
|
276
|
-
"AuthFailure",
|
|
277
|
-
"AuthorizationError",
|
|
278
|
-
"AuthorizationErrorException",
|
|
279
|
-
"InvalidClientTokenId",
|
|
280
|
-
"UnauthorizedOperation",
|
|
281
|
-
"UnrecognizedClientException",
|
|
282
|
-
"InternalServerErrorException",
|
|
283
|
-
]
|
|
284
303
|
|
|
285
304
|
@wraps(func)
|
|
286
305
|
# fix for AWS TooManyRequestsException
|
|
@@ -307,12 +326,20 @@ def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
|
|
|
307
326
|
) from e
|
|
308
327
|
# The account is not authorized to use this service in this region
|
|
309
328
|
# so we can continue without raising an exception
|
|
310
|
-
if error_code in
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
329
|
+
if error_code in AWS_REGION_ACCESS_DENIED_ERROR_CODES:
|
|
330
|
+
error_message = e.response.get("Error", {}).get("Message")
|
|
331
|
+
if is_service_control_policy_explicit_deny(e):
|
|
332
|
+
logger.warning(
|
|
333
|
+
"Service control policy denied access while calling %s: %s",
|
|
334
|
+
func.__name__,
|
|
335
|
+
error_message,
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
logger.warning(
|
|
339
|
+
"{} in this region. Skipping...".format(
|
|
340
|
+
error_message,
|
|
341
|
+
),
|
|
342
|
+
)
|
|
316
343
|
return []
|
|
317
344
|
else:
|
|
318
345
|
raise
|