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.
- cartography/_version.py +2 -2
- cartography/cli.py +31 -0
- cartography/client/core/tx.py +19 -3
- cartography/config.py +14 -0
- cartography/data/indexes.cypher +0 -6
- cartography/graph/job.py +13 -7
- cartography/graph/statement.py +4 -0
- cartography/intel/aws/__init__.py +22 -9
- cartography/intel/aws/apigateway.py +18 -5
- cartography/intel/aws/ec2/elastic_ip_addresses.py +3 -1
- cartography/intel/aws/ec2/internet_gateways.py +4 -2
- cartography/intel/aws/ec2/load_balancer_v2s.py +11 -5
- cartography/intel/aws/ec2/network_interfaces.py +4 -0
- cartography/intel/aws/ec2/reserved_instances.py +3 -1
- cartography/intel/aws/ec2/tgw.py +11 -5
- cartography/intel/aws/ec2/volumes.py +1 -1
- cartography/intel/aws/ecr.py +209 -26
- cartography/intel/aws/ecr_image_layers.py +143 -42
- cartography/intel/aws/elasticsearch.py +13 -4
- cartography/intel/aws/identitycenter.py +93 -54
- cartography/intel/aws/inspector.py +90 -46
- cartography/intel/aws/permission_relationships.py +3 -3
- cartography/intel/aws/resourcegroupstaggingapi.py +1 -1
- cartography/intel/aws/s3.py +26 -13
- cartography/intel/aws/ssm.py +3 -5
- cartography/intel/azure/compute.py +9 -4
- cartography/intel/azure/cosmosdb.py +31 -15
- cartography/intel/azure/sql.py +25 -12
- cartography/intel/azure/storage.py +19 -9
- cartography/intel/azure/subscription.py +3 -1
- cartography/intel/crowdstrike/spotlight.py +5 -2
- cartography/intel/entra/app_role_assignments.py +9 -2
- cartography/intel/gcp/__init__.py +26 -9
- cartography/intel/gcp/clients.py +8 -4
- cartography/intel/gcp/compute.py +42 -21
- cartography/intel/gcp/crm/folders.py +9 -3
- cartography/intel/gcp/crm/orgs.py +8 -3
- cartography/intel/gcp/crm/projects.py +14 -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/jamf/computers.py +7 -1
- cartography/intel/oci/iam.py +23 -9
- cartography/intel/oci/organizations.py +3 -1
- cartography/intel/oci/utils.py +28 -5
- cartography/intel/okta/awssaml.py +9 -8
- 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/intel/pagerduty/escalation_policies.py +13 -6
- cartography/intel/pagerduty/schedules.py +9 -4
- cartography/intel/pagerduty/services.py +7 -3
- cartography/intel/pagerduty/teams.py +5 -2
- cartography/intel/pagerduty/users.py +3 -1
- cartography/intel/pagerduty/vendors.py +3 -1
- cartography/intel/trivy/__init__.py +109 -58
- cartography/models/airbyte/user.py +4 -0
- cartography/models/anthropic/user.py +4 -0
- cartography/models/aws/ec2/networkinterfaces.py +2 -0
- cartography/models/aws/ecr/image.py +55 -0
- cartography/models/aws/ecr/repository_image.py +1 -1
- 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 +4 -1
- cartography/util.py +49 -18
- {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/METADATA +3 -3
- {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/RECORD +104 -89
- 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.117.0.dist-info → cartography-0.119.0.dist-info}/WHEEL +0 -0
- {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.117.0.dist-info → cartography-0.119.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.117.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
|
|
@@ -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
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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.
|
|
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.
|
|
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
|