cartography 0.117.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 (107) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +31 -0
  3. cartography/client/core/tx.py +19 -3
  4. cartography/config.py +14 -0
  5. cartography/data/indexes.cypher +0 -6
  6. cartography/graph/job.py +13 -7
  7. cartography/graph/statement.py +4 -0
  8. cartography/intel/aws/__init__.py +22 -9
  9. cartography/intel/aws/apigateway.py +18 -5
  10. cartography/intel/aws/ec2/elastic_ip_addresses.py +3 -1
  11. cartography/intel/aws/ec2/internet_gateways.py +4 -2
  12. cartography/intel/aws/ec2/load_balancer_v2s.py +11 -5
  13. cartography/intel/aws/ec2/network_interfaces.py +4 -0
  14. cartography/intel/aws/ec2/reserved_instances.py +3 -1
  15. cartography/intel/aws/ec2/tgw.py +11 -5
  16. cartography/intel/aws/ec2/volumes.py +1 -1
  17. cartography/intel/aws/ecr.py +209 -26
  18. cartography/intel/aws/ecr_image_layers.py +143 -42
  19. cartography/intel/aws/elasticsearch.py +13 -4
  20. cartography/intel/aws/identitycenter.py +93 -54
  21. cartography/intel/aws/inspector.py +90 -46
  22. cartography/intel/aws/permission_relationships.py +3 -3
  23. cartography/intel/aws/resourcegroupstaggingapi.py +1 -1
  24. cartography/intel/aws/s3.py +26 -13
  25. cartography/intel/aws/ssm.py +3 -5
  26. cartography/intel/azure/compute.py +9 -4
  27. cartography/intel/azure/cosmosdb.py +31 -15
  28. cartography/intel/azure/sql.py +25 -12
  29. cartography/intel/azure/storage.py +19 -9
  30. cartography/intel/azure/subscription.py +3 -1
  31. cartography/intel/crowdstrike/spotlight.py +5 -2
  32. cartography/intel/entra/app_role_assignments.py +9 -2
  33. cartography/intel/gcp/__init__.py +26 -9
  34. cartography/intel/gcp/clients.py +8 -4
  35. cartography/intel/gcp/compute.py +42 -21
  36. cartography/intel/gcp/crm/folders.py +9 -3
  37. cartography/intel/gcp/crm/orgs.py +8 -3
  38. cartography/intel/gcp/crm/projects.py +14 -3
  39. cartography/intel/github/repos.py +23 -5
  40. cartography/intel/gsuite/__init__.py +12 -8
  41. cartography/intel/gsuite/groups.py +291 -0
  42. cartography/intel/gsuite/users.py +142 -0
  43. cartography/intel/jamf/computers.py +7 -1
  44. cartography/intel/oci/iam.py +23 -9
  45. cartography/intel/oci/organizations.py +3 -1
  46. cartography/intel/oci/utils.py +28 -5
  47. cartography/intel/okta/awssaml.py +9 -8
  48. cartography/intel/okta/users.py +1 -1
  49. cartography/intel/ontology/__init__.py +44 -0
  50. cartography/intel/ontology/devices.py +54 -0
  51. cartography/intel/ontology/users.py +54 -0
  52. cartography/intel/ontology/utils.py +121 -0
  53. cartography/intel/pagerduty/escalation_policies.py +13 -6
  54. cartography/intel/pagerduty/schedules.py +9 -4
  55. cartography/intel/pagerduty/services.py +7 -3
  56. cartography/intel/pagerduty/teams.py +5 -2
  57. cartography/intel/pagerduty/users.py +3 -1
  58. cartography/intel/pagerduty/vendors.py +3 -1
  59. cartography/intel/trivy/__init__.py +109 -58
  60. cartography/models/airbyte/user.py +4 -0
  61. cartography/models/anthropic/user.py +4 -0
  62. cartography/models/aws/ec2/networkinterfaces.py +2 -0
  63. cartography/models/aws/ecr/image.py +55 -0
  64. cartography/models/aws/ecr/repository_image.py +1 -1
  65. cartography/models/aws/iam/group_membership.py +3 -2
  66. cartography/models/aws/identitycenter/awsssouser.py +3 -1
  67. cartography/models/bigfix/bigfix_computer.py +1 -1
  68. cartography/models/cloudflare/member.py +4 -0
  69. cartography/models/crowdstrike/hosts.py +1 -1
  70. cartography/models/duo/endpoint.py +1 -1
  71. cartography/models/duo/phone.py +2 -2
  72. cartography/models/duo/user.py +4 -0
  73. cartography/models/entra/user.py +2 -1
  74. cartography/models/github/users.py +4 -0
  75. cartography/models/gsuite/__init__.py +0 -0
  76. cartography/models/gsuite/group.py +218 -0
  77. cartography/models/gsuite/tenant.py +29 -0
  78. cartography/models/gsuite/user.py +107 -0
  79. cartography/models/kandji/device.py +1 -2
  80. cartography/models/keycloak/user.py +4 -0
  81. cartography/models/lastpass/user.py +4 -0
  82. cartography/models/ontology/__init__.py +0 -0
  83. cartography/models/ontology/device.py +125 -0
  84. cartography/models/ontology/mapping/__init__.py +16 -0
  85. cartography/models/ontology/mapping/data/__init__.py +1 -0
  86. cartography/models/ontology/mapping/data/devices.py +160 -0
  87. cartography/models/ontology/mapping/data/users.py +239 -0
  88. cartography/models/ontology/mapping/specs.py +65 -0
  89. cartography/models/ontology/user.py +52 -0
  90. cartography/models/openai/user.py +4 -0
  91. cartography/models/scaleway/iam/user.py +4 -0
  92. cartography/models/snipeit/asset.py +1 -0
  93. cartography/models/snipeit/user.py +4 -0
  94. cartography/models/tailscale/device.py +1 -1
  95. cartography/models/tailscale/user.py +6 -1
  96. cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py +176 -89
  97. cartography/sync.py +4 -1
  98. cartography/util.py +49 -18
  99. {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/METADATA +3 -3
  100. {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/RECORD +104 -89
  101. cartography/data/jobs/cleanup/gsuite_ingest_groups_cleanup.json +0 -23
  102. cartography/data/jobs/cleanup/gsuite_ingest_users_cleanup.json +0 -11
  103. cartography/intel/gsuite/api.py +0 -355
  104. {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/WHEEL +0 -0
  105. {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/entry_points.txt +0 -0
  106. {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/licenses/LICENSE +0 -0
  107. {cartography-0.117.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
@@ -52,7 +53,7 @@ from cartography.util import STATUS_SUCCESS
52
53
  logger = logging.getLogger(__name__)
53
54
 
54
55
 
55
- TOP_LEVEL_MODULES = OrderedDict(
56
+ TOP_LEVEL_MODULES: OrderedDict[str, Callable[..., None]] = OrderedDict(
56
57
  { # preserve order so that the default sync always runs `analysis` at the very end
57
58
  "create-indexes": cartography.intel.create_indexes.run,
58
59
  "airbyte": cartography.intel.airbyte.start_airbyte_ingestion,
@@ -84,6 +85,7 @@ TOP_LEVEL_MODULES = 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
@@ -153,6 +169,9 @@ def merge_module_sync_metadata(
153
169
  :param synced_type: The sub-module's type
154
170
  :param update_tag: Timestamp used to determine data freshness
155
171
  """
172
+ # Import here to avoid circular import with cartography.client.core.tx
173
+ from cartography.client.core.tx import run_write_query
174
+
156
175
  template = Template(
157
176
  """
158
177
  MERGE (n:ModuleSyncMetadata{id:'${group_type}_${group_id}_${synced_type}'})
@@ -164,7 +183,8 @@ def merge_module_sync_metadata(
164
183
  n.lastupdated=$UPDATE_TAG
165
184
  """,
166
185
  )
167
- neo4j_session.run(
186
+ run_write_query(
187
+ neo4j_session,
168
188
  template.safe_substitute(
169
189
  group_type=group_type,
170
190
  group_id=group_id,
@@ -255,6 +275,20 @@ def backoff_handler(details: Dict) -> None:
255
275
  )
256
276
 
257
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
+
258
292
  # TODO Move this to cartography.intel.aws.util.common
259
293
  def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
260
294
  """
@@ -266,17 +300,6 @@ def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
266
300
 
267
301
  This should be used on `get_` functions that normally return a list of items.
268
302
  """
269
- ERROR_CODES = [
270
- "AccessDenied",
271
- "AccessDeniedException",
272
- "AuthFailure",
273
- "AuthorizationError",
274
- "AuthorizationErrorException",
275
- "InvalidClientTokenId",
276
- "UnauthorizedOperation",
277
- "UnrecognizedClientException",
278
- "InternalServerErrorException",
279
- ]
280
303
 
281
304
  @wraps(func)
282
305
  # fix for AWS TooManyRequestsException
@@ -303,12 +326,20 @@ def aws_handle_regions(func: AWSGetFunc) -> AWSGetFunc:
303
326
  ) from e
304
327
  # The account is not authorized to use this service in this region
305
328
  # so we can continue without raising an exception
306
- if error_code in ERROR_CODES:
307
- logger.warning(
308
- "{} in this region. Skipping...".format(
309
- e.response["Error"]["Message"],
310
- ),
311
- )
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
+ )
312
343
  return []
313
344
  else:
314
345
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartography
3
- Version: 0.117.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
@@ -59,7 +59,7 @@ Requires-Dist: python-dateutil
59
59
  Requires-Dist: xmltodict
60
60
  Requires-Dist: duo-client
61
61
  Requires-Dist: cloudflare<5.0.0,>=4.1.0
62
- Requires-Dist: scaleway>=2.9.0
62
+ Requires-Dist: scaleway>=2.10.0
63
63
  Requires-Dist: google-cloud-resource-manager>=1.14.2
64
64
  Requires-Dist: types-aiobotocore-ecr
65
65
  Requires-Dist: typer>=0.9.0
@@ -89,7 +89,7 @@ You can learn more about the story behind Cartography in our [presentation at BS
89
89
 
90
90
  ## Supported platforms
91
91
  - [Airbyte](https://cartography-cncf.github.io/cartography/modules/airbyte/index.html) - Organization, Workspace, User, Source, Destination, Connection, Tag, Stream
92
- - [Amazon Web Services](https://cartography-cncf.github.io/cartography/modules/aws/index.html) - ACM, API Gateway, CloudWatch, CodeBuild, Config, Cognito, EC2, ECS, ECR (including image layers), EFS, Elasticsearch, Elastic Kubernetes Service (EKS), DynamoDB, Glue, GuardDuty, IAM, Inspector, KMS, Lambda, RDS, Redshift, Route53, S3, Secrets Manager(Secret Versions), Security Hub, SNS, SQS, SSM, STS, Tags
92
+ - [Amazon Web Services](https://cartography-cncf.github.io/cartography/modules/aws/index.html) - ACM, API Gateway, CloudWatch, CodeBuild, Config, Cognito, EC2, ECS, ECR (including multi-arch images, image layers, and attestations), EFS, Elasticsearch, Elastic Kubernetes Service (EKS), DynamoDB, Glue, GuardDuty, IAM, Inspector, KMS, Lambda, RDS, Redshift, Route53, S3, Secrets Manager(Secret Versions), Security Hub, SNS, SQS, SSM, STS, Tags
93
93
  - [Anthropic](https://cartography-cncf.github.io/cartography/modules/anthropic/index.html) - Organization, ApiKey, User, Workspace
94
94
  - [BigFix](https://cartography-cncf.github.io/cartography/modules/bigfix/index.html) - Computers
95
95
  - [Cloudflare](https://cartography-cncf.github.io/cartography/modules/cloudflare/index.html) - Account, Role, Member, Zone, DNSRecord