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.

Files changed (68) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +20 -0
  3. cartography/client/core/tx.py +19 -3
  4. cartography/config.py +9 -0
  5. cartography/data/indexes.cypher +0 -6
  6. cartography/graph/job.py +7 -5
  7. cartography/intel/aws/__init__.py +21 -9
  8. cartography/intel/aws/ecr.py +7 -0
  9. cartography/intel/aws/ecr_image_layers.py +143 -42
  10. cartography/intel/aws/inspector.py +65 -33
  11. cartography/intel/aws/resourcegroupstaggingapi.py +1 -1
  12. cartography/intel/gcp/compute.py +3 -3
  13. cartography/intel/github/repos.py +23 -5
  14. cartography/intel/gsuite/__init__.py +12 -8
  15. cartography/intel/gsuite/groups.py +291 -0
  16. cartography/intel/gsuite/users.py +142 -0
  17. cartography/intel/okta/awssaml.py +1 -1
  18. cartography/intel/okta/users.py +1 -1
  19. cartography/intel/ontology/__init__.py +44 -0
  20. cartography/intel/ontology/devices.py +54 -0
  21. cartography/intel/ontology/users.py +54 -0
  22. cartography/intel/ontology/utils.py +121 -0
  23. cartography/models/airbyte/user.py +4 -0
  24. cartography/models/anthropic/user.py +4 -0
  25. cartography/models/aws/ecr/image.py +47 -0
  26. cartography/models/aws/iam/group_membership.py +3 -2
  27. cartography/models/aws/identitycenter/awsssouser.py +3 -1
  28. cartography/models/bigfix/bigfix_computer.py +1 -1
  29. cartography/models/cloudflare/member.py +4 -0
  30. cartography/models/crowdstrike/hosts.py +1 -1
  31. cartography/models/duo/endpoint.py +1 -1
  32. cartography/models/duo/phone.py +2 -2
  33. cartography/models/duo/user.py +4 -0
  34. cartography/models/entra/user.py +2 -1
  35. cartography/models/github/users.py +4 -0
  36. cartography/models/gsuite/__init__.py +0 -0
  37. cartography/models/gsuite/group.py +218 -0
  38. cartography/models/gsuite/tenant.py +29 -0
  39. cartography/models/gsuite/user.py +107 -0
  40. cartography/models/kandji/device.py +1 -2
  41. cartography/models/keycloak/user.py +4 -0
  42. cartography/models/lastpass/user.py +4 -0
  43. cartography/models/ontology/__init__.py +0 -0
  44. cartography/models/ontology/device.py +125 -0
  45. cartography/models/ontology/mapping/__init__.py +16 -0
  46. cartography/models/ontology/mapping/data/__init__.py +1 -0
  47. cartography/models/ontology/mapping/data/devices.py +160 -0
  48. cartography/models/ontology/mapping/data/users.py +239 -0
  49. cartography/models/ontology/mapping/specs.py +65 -0
  50. cartography/models/ontology/user.py +52 -0
  51. cartography/models/openai/user.py +4 -0
  52. cartography/models/scaleway/iam/user.py +4 -0
  53. cartography/models/snipeit/asset.py +1 -0
  54. cartography/models/snipeit/user.py +4 -0
  55. cartography/models/tailscale/device.py +1 -1
  56. cartography/models/tailscale/user.py +6 -1
  57. cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py +176 -89
  58. cartography/sync.py +3 -0
  59. cartography/util.py +44 -17
  60. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/METADATA +1 -1
  61. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/RECORD +65 -50
  62. cartography/data/jobs/cleanup/gsuite_ingest_groups_cleanup.json +0 -23
  63. cartography/data/jobs/cleanup/gsuite_ingest_users_cleanup.json +0 -11
  64. cartography/intel/gsuite/api.py +0 -355
  65. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/WHEEL +0 -0
  66. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/entry_points.txt +0 -0
  67. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/licenses/LICENSE +0 -0
  68. {cartography-0.118.0.dist-info → cartography-0.119.0.dist-info}/top_level.txt +0 -0
@@ -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
- // Define the list of IAM actions to match on
24
- [p IN ['iam:Create','iam:Attach','iam:Put','iam:Update','iam:Add'] |
25
- p] AS patterns
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 matched_actions
33
- // Return only statement actions that we matched on
34
- WHERE size(matched_actions) > 0
35
- UNWIND matched_actions AS action
36
- RETURN DISTINCT a.name AS account,
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
- collect(action) as action,
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
- AND stmt.effect = 'Allow'
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
- WITH a, principal, principal_type, stmt,
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:*' OR action = '*'
87
- ] AS matched_actions
88
- WHERE size(matched_actions) > 0
89
- UNWIND matched_actions AS action
90
- RETURN DISTINCT a.name AS account,
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(distinct action) as action,
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]->(stmt:AWSPolicyStatement)
122
- WHERE stmt.effect = 'Allow'
123
- WITH a, ec2, role,
124
- // Define the list of IAM actions to match on
125
- ['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update'] AS patterns,
126
- // Filter on the desired IAM actions
127
- [action IN stmt.action
128
- WHERE ANY(p IN ['iam:Create','iam:Attach','iam:Put','iam:Update', 'iam:Add'] WHERE action STARTS WITH p)
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 actions
132
- // For the instances that are internet open, include the SG and rules
133
- OPTIONAL MATCH (ec2{exposed_internet:True})-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(ip:IpPermissionInbound)
134
- // Return only statement actions that we matched on
135
- WHERE size(actions) > 0
136
- UNWIND actions AS flat_action
137
- WITH a, ec2, role, sg, ip,
138
- collect(DISTINCT flat_action) AS actions
139
- RETURN DISTINCT a.name AS account,
140
- a.id as account_id,
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 as public_ip_address,
194
+ ec2.publicipaddress AS public_ip_address,
144
195
  role.name AS role_name,
145
- collect(actions) as action,
146
- ip.fromport as from_port,
147
- ip.toport as to_port
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]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
184
- WHERE stmt.effect = 'Allow'
185
- WITH a, lambda, role, stmt,
186
- // Define the list of IAM actions to match on
187
- ['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update'] AS patterns,
188
- // Filter on the desired IAM actions
189
- [action IN stmt.action
190
- WHERE ANY(p IN ['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update', 'iam:Add'] WHERE action STARTS WITH p)
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 actions
194
- // Return only statement actions that we matched on
195
- WHERE size(actions) > 0
196
- UNWIND actions AS flat_action
197
- WITH a, lambda, role, stmt,
198
- collect(DISTINCT flat_action) AS actions
199
- RETURN DISTINCT a.name AS account,
200
- a.id as account_id,
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]->(stmt:AWSPolicyStatement)
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
- AND stmt.effect = 'Allow'
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
- // Define the list of IAM actions to match on
245
- [p IN
246
- ['iam:CreatePolicy', 'iam:CreatePolicyVersion', 'iam:AttachUserPolicy', 'iam:AttachRolePolicy', 'iam:AttachGroupPolicy',
247
- 'iam:AttachRolePolicy', 'iam:AttachGroupPolicy', 'iam:DetachUserPolicy', 'iam:DetachRolePolicy', 'iam:DetachGroupPolicy',
248
- 'iam:PutUserPolicy', 'iam:PutRolePolicy', 'iam:PutGroupPolicy'] |
249
- p] AS patterns
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
- // Return only statement actions that we matched on
252
- WITH a, principal, principal_type, stmt,
253
- [action IN stmt.action
254
- WHERE ANY(p IN patterns WHERE action = p)
255
- OR action = 'iam:*' OR action = '*'
256
- ] AS matched_actions
257
- WHERE size(matched_actions) > 0
258
- UNWIND matched_actions AS action
259
- RETURN DISTINCT a.name AS account,
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 AS principal_arn,
347
+ principal.arn AS principal_arn,
262
348
  principal_type,
263
- collect(action) as action,
264
- stmt.resource AS resource
265
- ORDER BY account, principal_name
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 ERROR_CODES:
311
- logger.warning(
312
- "{} in this region. Skipping...".format(
313
- e.response["Error"]["Message"],
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartography
3
- Version: 0.118.0
3
+ Version: 0.119.0
4
4
  Summary: Explore assets and their relationships across your technical infrastructure.
5
5
  Maintainer: Cartography Contributors
6
6
  License-Expression: Apache-2.0