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/intel/gsuite/api.py
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from typing import Dict
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
|
-
import neo4j
|
|
6
|
-
from googleapiclient.discovery import Resource
|
|
7
|
-
from googleapiclient.errors import HttpError
|
|
8
|
-
|
|
9
|
-
from cartography.client.core.tx import run_write_query
|
|
10
|
-
from cartography.util import run_cleanup_job
|
|
11
|
-
from cartography.util import timeit
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
GOOGLE_API_NUM_RETRIES = 5
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@timeit
|
|
20
|
-
def get_all_groups(admin: Resource) -> List[Dict]:
|
|
21
|
-
"""
|
|
22
|
-
Return list of Google Groups in your organization
|
|
23
|
-
Returns empty list if we are unable to enumerate the groups for any reasons
|
|
24
|
-
|
|
25
|
-
googleapiclient.discovery.build('admin', 'directory_v1', credentials=credentials, cache_discovery=False)
|
|
26
|
-
|
|
27
|
-
:param admin: google's apiclient discovery resource object. From googleapiclient.discovery.build
|
|
28
|
-
See https://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build.
|
|
29
|
-
:return: List of Google groups in domain
|
|
30
|
-
"""
|
|
31
|
-
request = admin.groups().list(
|
|
32
|
-
customer="my_customer",
|
|
33
|
-
maxResults=20,
|
|
34
|
-
orderBy="email",
|
|
35
|
-
)
|
|
36
|
-
response_objects = []
|
|
37
|
-
while request is not None:
|
|
38
|
-
try:
|
|
39
|
-
resp = request.execute(num_retries=GOOGLE_API_NUM_RETRIES)
|
|
40
|
-
response_objects.append(resp)
|
|
41
|
-
request = admin.groups().list_next(request, resp)
|
|
42
|
-
except HttpError as e:
|
|
43
|
-
if (
|
|
44
|
-
e.resp.status == 403
|
|
45
|
-
and "Request had insufficient authentication scopes" in str(e)
|
|
46
|
-
):
|
|
47
|
-
logger.error(
|
|
48
|
-
"Missing required GSuite scopes. If using the gcloud CLI, ",
|
|
49
|
-
"run: gcloud auth application-default login --scopes="
|
|
50
|
-
'"https://www.googleapis.com/auth/admin.directory.user.readonly,'
|
|
51
|
-
"https://www.googleapis.com/auth/admin.directory.group.readonly,"
|
|
52
|
-
"https://www.googleapis.com/auth/admin.directory.group.member.readonly,"
|
|
53
|
-
'https://www.googleapis.com/auth/cloud-platform"',
|
|
54
|
-
)
|
|
55
|
-
raise
|
|
56
|
-
return response_objects
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@timeit
|
|
60
|
-
def transform_groups(response_objects: List[Dict]) -> List[Dict]:
|
|
61
|
-
"""Strips list of API response objects to return list of group objects only
|
|
62
|
-
|
|
63
|
-
:param response_objects:
|
|
64
|
-
:return: list of dictionary objects as defined in /docs/root/modules/gsuite/schema.md
|
|
65
|
-
"""
|
|
66
|
-
groups: List[Dict] = []
|
|
67
|
-
for response_object in response_objects:
|
|
68
|
-
for group in response_object["groups"]:
|
|
69
|
-
groups.append(group)
|
|
70
|
-
return groups
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@timeit
|
|
74
|
-
def transform_users(response_objects: List[Dict]) -> List[Dict]:
|
|
75
|
-
"""Strips list of API response objects to return list of group objects only
|
|
76
|
-
:param response_objects:
|
|
77
|
-
:return: list of dictionary objects as defined in /docs/root/modules/gsuite/schema.md
|
|
78
|
-
"""
|
|
79
|
-
users: List[Dict] = []
|
|
80
|
-
for response_object in response_objects:
|
|
81
|
-
for user in response_object["users"]:
|
|
82
|
-
users.append(user)
|
|
83
|
-
return users
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@timeit
|
|
87
|
-
def get_all_groups_for_email(admin: Resource, email: str) -> List[Dict]:
|
|
88
|
-
"""Fetch all groups of which the given group is a member
|
|
89
|
-
|
|
90
|
-
Arguments:
|
|
91
|
-
email: A string representing the email address for the group
|
|
92
|
-
|
|
93
|
-
Returns a list of Group models
|
|
94
|
-
Throws GoogleException
|
|
95
|
-
"""
|
|
96
|
-
request = admin.groups().list(userKey=email, maxResults=500)
|
|
97
|
-
groups: List[Dict] = []
|
|
98
|
-
while request is not None:
|
|
99
|
-
resp = request.execute(num_retries=GOOGLE_API_NUM_RETRIES)
|
|
100
|
-
groups = groups + resp.get("groups", [])
|
|
101
|
-
request = admin.groups().list_next(request, resp)
|
|
102
|
-
return groups
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@timeit
|
|
106
|
-
def get_members_for_group(admin: Resource, group_email: str) -> List[Dict]:
|
|
107
|
-
"""Get all members for a google group
|
|
108
|
-
|
|
109
|
-
:param group_email: A string representing the email address for the group
|
|
110
|
-
|
|
111
|
-
:return: List of dictionaries representing Users or Groups.
|
|
112
|
-
"""
|
|
113
|
-
request = admin.members().list(
|
|
114
|
-
groupKey=group_email,
|
|
115
|
-
maxResults=500,
|
|
116
|
-
)
|
|
117
|
-
members: List[Dict] = []
|
|
118
|
-
while request is not None:
|
|
119
|
-
resp = request.execute(num_retries=GOOGLE_API_NUM_RETRIES)
|
|
120
|
-
members = members + resp.get("members", [])
|
|
121
|
-
request = admin.members().list_next(request, resp)
|
|
122
|
-
|
|
123
|
-
return members
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@timeit
|
|
127
|
-
def get_all_users(admin: Resource) -> List[Dict]:
|
|
128
|
-
"""
|
|
129
|
-
Return list of Google Users in your organization
|
|
130
|
-
Returns empty list if we are unable to enumerate the groups for any reasons
|
|
131
|
-
https://developers.google.com/admin-sdk/directory/v1/guides/manage-users
|
|
132
|
-
|
|
133
|
-
:param admin: apiclient discovery resource object
|
|
134
|
-
see
|
|
135
|
-
:return: List of Google users in domain
|
|
136
|
-
see https://developers.google.com/admin-sdk/directory/v1/guides/manage-users#get_all_domain_users
|
|
137
|
-
"""
|
|
138
|
-
request = admin.users().list(
|
|
139
|
-
customer="my_customer",
|
|
140
|
-
maxResults=500,
|
|
141
|
-
orderBy="email",
|
|
142
|
-
)
|
|
143
|
-
response_objects = []
|
|
144
|
-
while request is not None:
|
|
145
|
-
resp = request.execute(num_retries=GOOGLE_API_NUM_RETRIES)
|
|
146
|
-
response_objects.append(resp)
|
|
147
|
-
request = admin.users().list_next(request, resp)
|
|
148
|
-
return response_objects
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@timeit
|
|
152
|
-
def load_gsuite_groups(
|
|
153
|
-
neo4j_session: neo4j.Session,
|
|
154
|
-
groups: List[Dict],
|
|
155
|
-
gsuite_update_tag: int,
|
|
156
|
-
) -> None:
|
|
157
|
-
ingestion_qry = """
|
|
158
|
-
UNWIND $GroupData as group
|
|
159
|
-
MERGE (g:GSuiteGroup{id: group.id})
|
|
160
|
-
ON CREATE SET
|
|
161
|
-
g.firstseen = $UpdateTag,
|
|
162
|
-
g.group_id = group.id
|
|
163
|
-
SET
|
|
164
|
-
g.admin_created = group.adminCreated,
|
|
165
|
-
g.description = group.description,
|
|
166
|
-
g.direct_members_count = group.directMembersCount,
|
|
167
|
-
g.email = group.email,
|
|
168
|
-
g.etag = group.etag,
|
|
169
|
-
g.kind = group.kind,
|
|
170
|
-
g.name = group.name,
|
|
171
|
-
g:GCPPrincipal,
|
|
172
|
-
g.lastupdated = $UpdateTag
|
|
173
|
-
"""
|
|
174
|
-
logger.info(f"Ingesting {len(groups)} gsuite groups")
|
|
175
|
-
run_write_query(
|
|
176
|
-
neo4j_session,
|
|
177
|
-
ingestion_qry,
|
|
178
|
-
GroupData=groups,
|
|
179
|
-
UpdateTag=gsuite_update_tag,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@timeit
|
|
184
|
-
def load_gsuite_users(
|
|
185
|
-
neo4j_session: neo4j.Session,
|
|
186
|
-
users: List[Dict],
|
|
187
|
-
gsuite_update_tag: int,
|
|
188
|
-
) -> None:
|
|
189
|
-
ingestion_qry = """
|
|
190
|
-
UNWIND $UserData as user
|
|
191
|
-
MERGE (u:GSuiteUser{id: user.id})
|
|
192
|
-
ON CREATE SET
|
|
193
|
-
u.user_id = user.id,
|
|
194
|
-
u.firstseen = $UpdateTag
|
|
195
|
-
SET
|
|
196
|
-
u.agreed_to_terms = user.agreedToTerms,
|
|
197
|
-
u.archived = user.archived,
|
|
198
|
-
u.change_password_at_next_login = user.changePasswordAtNextLogin,
|
|
199
|
-
u.creation_time = user.creationTime,
|
|
200
|
-
u.customer_id = user.customerId,
|
|
201
|
-
u.etag = user.etag,
|
|
202
|
-
u.include_in_global_address_list = user.includeInGlobalAddressList,
|
|
203
|
-
u.ip_whitelisted = user.ipWhitelisted,
|
|
204
|
-
u.is_admin = user.isAdmin,
|
|
205
|
-
u.is_delegated_admin = user.isDelegatedAdmin,
|
|
206
|
-
u.is_enforced_in_2_sv = user.isEnforcedIn2Sv,
|
|
207
|
-
u.is_enrolled_in_2_sv = user.isEnrolledIn2Sv,
|
|
208
|
-
u.is_mailbox_setup = user.isMailboxSetup,
|
|
209
|
-
u.kind = user.kind,
|
|
210
|
-
u.last_login_time = user.lastLoginTime,
|
|
211
|
-
u.name = user.name.fullName,
|
|
212
|
-
u.family_name = user.name.familyName,
|
|
213
|
-
u.given_name = user.name.givenName,
|
|
214
|
-
u.org_unit_path = user.orgUnitPath,
|
|
215
|
-
u.primary_email = user.primaryEmail,
|
|
216
|
-
u.email = user.primaryEmail,
|
|
217
|
-
u.suspended = user.suspended,
|
|
218
|
-
u.thumbnail_photo_etag = user.thumbnailPhotoEtag,
|
|
219
|
-
u.thumbnail_photo_url = user.thumbnailPhotoUrl,
|
|
220
|
-
u:GCPPrincipal,
|
|
221
|
-
u.lastupdated = $UpdateTag
|
|
222
|
-
"""
|
|
223
|
-
logger.info(f"Ingesting {len(users)} gsuite users")
|
|
224
|
-
run_write_query(
|
|
225
|
-
neo4j_session,
|
|
226
|
-
ingestion_qry,
|
|
227
|
-
UserData=users,
|
|
228
|
-
UpdateTag=gsuite_update_tag,
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@timeit
|
|
233
|
-
def load_gsuite_members(
|
|
234
|
-
neo4j_session: neo4j.Session,
|
|
235
|
-
group: Dict,
|
|
236
|
-
members: List[Dict],
|
|
237
|
-
gsuite_update_tag: int,
|
|
238
|
-
) -> None:
|
|
239
|
-
ingestion_qry = """
|
|
240
|
-
UNWIND $MemberData as member
|
|
241
|
-
MATCH (user:GSuiteUser {id: member.id}),(group:GSuiteGroup {id: $GroupID })
|
|
242
|
-
MERGE (user)-[r:MEMBER_GSUITE_GROUP]->(group)
|
|
243
|
-
ON CREATE SET
|
|
244
|
-
r.firstseen = $UpdateTag
|
|
245
|
-
SET
|
|
246
|
-
r.lastupdated = $UpdateTag
|
|
247
|
-
"""
|
|
248
|
-
run_write_query(
|
|
249
|
-
neo4j_session,
|
|
250
|
-
ingestion_qry,
|
|
251
|
-
MemberData=members,
|
|
252
|
-
GroupID=group.get("id"),
|
|
253
|
-
UpdateTag=gsuite_update_tag,
|
|
254
|
-
)
|
|
255
|
-
membership_qry = """
|
|
256
|
-
UNWIND $MemberData as member
|
|
257
|
-
MATCH(group_1: GSuiteGroup{id: member.id}), (group_2:GSuiteGroup {id: $GroupID})
|
|
258
|
-
MERGE (group_1)-[r:MEMBER_GSUITE_GROUP]->(group_2)
|
|
259
|
-
ON CREATE SET
|
|
260
|
-
r.firstseen = $UpdateTag
|
|
261
|
-
SET
|
|
262
|
-
r.lastupdated = $UpdateTag
|
|
263
|
-
"""
|
|
264
|
-
run_write_query(
|
|
265
|
-
neo4j_session,
|
|
266
|
-
membership_qry,
|
|
267
|
-
MemberData=members,
|
|
268
|
-
GroupID=group.get("id"),
|
|
269
|
-
UpdateTag=gsuite_update_tag,
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
@timeit
|
|
274
|
-
def cleanup_gsuite_users(
|
|
275
|
-
neo4j_session: neo4j.Session,
|
|
276
|
-
common_job_parameters: Dict,
|
|
277
|
-
) -> None:
|
|
278
|
-
run_cleanup_job(
|
|
279
|
-
"gsuite_ingest_users_cleanup.json",
|
|
280
|
-
neo4j_session,
|
|
281
|
-
common_job_parameters,
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@timeit
|
|
286
|
-
def cleanup_gsuite_groups(
|
|
287
|
-
neo4j_session: neo4j.Session,
|
|
288
|
-
common_job_parameters: Dict,
|
|
289
|
-
) -> None:
|
|
290
|
-
run_cleanup_job(
|
|
291
|
-
"gsuite_ingest_groups_cleanup.json",
|
|
292
|
-
neo4j_session,
|
|
293
|
-
common_job_parameters,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
@timeit
|
|
298
|
-
def sync_gsuite_users(
|
|
299
|
-
neo4j_session: neo4j.Session,
|
|
300
|
-
admin: Resource,
|
|
301
|
-
gsuite_update_tag: int,
|
|
302
|
-
common_job_parameters: Dict,
|
|
303
|
-
) -> None:
|
|
304
|
-
"""
|
|
305
|
-
GET GSuite user objects using the google admin api resource, load the data into Neo4j and clean up stale nodes.
|
|
306
|
-
|
|
307
|
-
:param session: The Neo4j session
|
|
308
|
-
:param admin: Google admin resource object created by `googleapiclient.discovery.build()`.
|
|
309
|
-
See https://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build.
|
|
310
|
-
:param gcp_update_tag: The timestamp value to set our new Neo4j nodes with
|
|
311
|
-
:param common_job_parameters: Parameters to carry to the Neo4j jobs
|
|
312
|
-
:return: Nothing
|
|
313
|
-
"""
|
|
314
|
-
logger.debug("Syncing GSuite Users")
|
|
315
|
-
resp_objs = get_all_users(admin)
|
|
316
|
-
users = transform_users(resp_objs)
|
|
317
|
-
load_gsuite_users(neo4j_session, users, gsuite_update_tag)
|
|
318
|
-
cleanup_gsuite_users(neo4j_session, common_job_parameters)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
@timeit
|
|
322
|
-
def sync_gsuite_groups(
|
|
323
|
-
neo4j_session: neo4j.Session,
|
|
324
|
-
admin: Resource,
|
|
325
|
-
gsuite_update_tag: int,
|
|
326
|
-
common_job_parameters: Dict,
|
|
327
|
-
) -> None:
|
|
328
|
-
"""
|
|
329
|
-
GET GSuite group objects using the google admin api resource, load the data into Neo4j and clean up stale nodes.
|
|
330
|
-
|
|
331
|
-
:param neo4j_session: The Neo4j session
|
|
332
|
-
:param admin: Google admin resource object created by `googleapiclient.discovery.build()`.
|
|
333
|
-
See https://googleapis.github.io/google-api-python-client/docs/epy/googleapiclient.discovery-module.html#build.
|
|
334
|
-
:param gcp_update_tag: The timestamp value to set our new Neo4j nodes with
|
|
335
|
-
:param common_job_parameters: Parameters to carry to the Neo4j jobs
|
|
336
|
-
:return: Nothing
|
|
337
|
-
"""
|
|
338
|
-
logger.debug("Syncing GSuite Groups")
|
|
339
|
-
resp_objs = get_all_groups(admin)
|
|
340
|
-
groups = transform_groups(resp_objs)
|
|
341
|
-
load_gsuite_groups(neo4j_session, groups, gsuite_update_tag)
|
|
342
|
-
cleanup_gsuite_groups(neo4j_session, common_job_parameters)
|
|
343
|
-
sync_gsuite_members(groups, neo4j_session, admin, gsuite_update_tag)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
@timeit
|
|
347
|
-
def sync_gsuite_members(
|
|
348
|
-
groups: List[Dict],
|
|
349
|
-
neo4j_session: neo4j.Session,
|
|
350
|
-
admin: Resource,
|
|
351
|
-
gsuite_update_tag: int,
|
|
352
|
-
) -> None:
|
|
353
|
-
for group in groups:
|
|
354
|
-
members = get_members_for_group(admin, group["email"])
|
|
355
|
-
load_gsuite_members(neo4j_session, group, members, gsuite_update_tag)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|