cartography 0.106.0rc2__py3-none-any.whl → 0.107.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 +131 -2
- cartography/config.py +42 -0
- cartography/driftdetect/cli.py +3 -2
- cartography/intel/airbyte/__init__.py +105 -0
- cartography/intel/airbyte/connections.py +120 -0
- cartography/intel/airbyte/destinations.py +81 -0
- cartography/intel/airbyte/organizations.py +59 -0
- cartography/intel/airbyte/sources.py +78 -0
- cartography/intel/airbyte/tags.py +64 -0
- cartography/intel/airbyte/users.py +106 -0
- cartography/intel/airbyte/util.py +122 -0
- cartography/intel/airbyte/workspaces.py +63 -0
- cartography/intel/aws/__init__.py +1 -0
- cartography/intel/aws/cloudtrail_management_events.py +364 -0
- cartography/intel/aws/cloudwatch.py +77 -0
- cartography/intel/aws/codebuild.py +132 -0
- cartography/intel/aws/ec2/subnets.py +1 -1
- cartography/intel/aws/ecs.py +17 -0
- cartography/intel/aws/inspector.py +77 -48
- cartography/intel/aws/resources.py +4 -0
- cartography/intel/aws/sns.py +62 -2
- cartography/intel/entra/users.py +84 -42
- cartography/intel/scaleway/__init__.py +127 -0
- cartography/intel/scaleway/iam/__init__.py +0 -0
- cartography/intel/scaleway/iam/apikeys.py +71 -0
- cartography/intel/scaleway/iam/applications.py +71 -0
- cartography/intel/scaleway/iam/groups.py +71 -0
- cartography/intel/scaleway/iam/users.py +71 -0
- cartography/intel/scaleway/instances/__init__.py +0 -0
- cartography/intel/scaleway/instances/flexibleips.py +86 -0
- cartography/intel/scaleway/instances/instances.py +92 -0
- cartography/intel/scaleway/projects.py +79 -0
- cartography/intel/scaleway/storage/__init__.py +0 -0
- cartography/intel/scaleway/storage/snapshots.py +86 -0
- cartography/intel/scaleway/storage/volumes.py +84 -0
- cartography/intel/scaleway/utils.py +37 -0
- cartography/intel/sentinelone/__init__.py +69 -0
- cartography/intel/sentinelone/account.py +140 -0
- cartography/intel/sentinelone/agent.py +139 -0
- cartography/intel/sentinelone/api.py +113 -0
- cartography/intel/sentinelone/application.py +248 -0
- cartography/intel/sentinelone/utils.py +28 -0
- cartography/models/airbyte/__init__.py +0 -0
- cartography/models/airbyte/connection.py +138 -0
- cartography/models/airbyte/destination.py +75 -0
- cartography/models/airbyte/organization.py +19 -0
- cartography/models/airbyte/source.py +75 -0
- cartography/models/airbyte/stream.py +74 -0
- cartography/models/airbyte/tag.py +69 -0
- cartography/models/airbyte/user.py +111 -0
- cartography/models/airbyte/workspace.py +46 -0
- cartography/models/aws/cloudtrail/management_events.py +64 -0
- cartography/models/aws/cloudwatch/log_metric_filter.py +79 -0
- cartography/models/aws/codebuild/__init__.py +0 -0
- cartography/models/aws/codebuild/project.py +49 -0
- cartography/models/aws/ec2/networkinterfaces.py +2 -0
- cartography/models/aws/ec2/subnet_instance.py +2 -0
- cartography/models/aws/ec2/subnet_networkinterface.py +2 -0
- cartography/models/aws/ecs/containers.py +19 -0
- cartography/models/aws/ecs/task_definitions.py +38 -0
- cartography/models/aws/ecs/tasks.py +24 -1
- cartography/models/aws/inspector/findings.py +37 -0
- cartography/models/aws/inspector/packages.py +1 -31
- cartography/models/aws/sns/topic_subscription.py +74 -0
- cartography/models/entra/user.py +17 -51
- cartography/models/scaleway/__init__.py +0 -0
- cartography/models/scaleway/iam/__init__.py +0 -0
- cartography/models/scaleway/iam/apikey.py +96 -0
- cartography/models/scaleway/iam/application.py +52 -0
- cartography/models/scaleway/iam/group.py +95 -0
- cartography/models/scaleway/iam/user.py +60 -0
- cartography/models/scaleway/instance/__init__.py +0 -0
- cartography/models/scaleway/instance/flexibleip.py +52 -0
- cartography/models/scaleway/instance/instance.py +118 -0
- cartography/models/scaleway/organization.py +19 -0
- cartography/models/scaleway/project.py +48 -0
- cartography/models/scaleway/storage/__init__.py +0 -0
- cartography/models/scaleway/storage/snapshot.py +78 -0
- cartography/models/scaleway/storage/volume.py +51 -0
- cartography/models/sentinelone/__init__.py +1 -0
- cartography/models/sentinelone/account.py +40 -0
- cartography/models/sentinelone/agent.py +50 -0
- cartography/models/sentinelone/application.py +44 -0
- cartography/models/sentinelone/application_version.py +96 -0
- cartography/sync.py +11 -4
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/METADATA +20 -16
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/RECORD +92 -28
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/WHEEL +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/top_level.txt +0 -0
|
@@ -3,14 +3,17 @@ from typing import Any
|
|
|
3
3
|
from typing import Dict
|
|
4
4
|
from typing import Iterator
|
|
5
5
|
from typing import List
|
|
6
|
+
from typing import Set
|
|
6
7
|
from typing import Tuple
|
|
7
8
|
|
|
8
9
|
import boto3
|
|
9
10
|
import neo4j
|
|
10
11
|
|
|
11
12
|
from cartography.client.core.tx import load
|
|
13
|
+
from cartography.client.core.tx import load_matchlinks
|
|
12
14
|
from cartography.graph.job import GraphJob
|
|
13
15
|
from cartography.models.aws.inspector.findings import AWSInspectorFindingSchema
|
|
16
|
+
from cartography.models.aws.inspector.findings import InspectorFindingToPackageMatchLink
|
|
14
17
|
from cartography.models.aws.inspector.packages import AWSInspectorPackageSchema
|
|
15
18
|
from cartography.util import aws_handle_regions
|
|
16
19
|
from cartography.util import aws_paginate
|
|
@@ -107,9 +110,10 @@ def get_inspector_findings(
|
|
|
107
110
|
|
|
108
111
|
def transform_inspector_findings(
|
|
109
112
|
results: List[Dict[str, Any]],
|
|
110
|
-
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
|
113
|
+
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, str]]]:
|
|
111
114
|
findings_list: List[Dict] = []
|
|
112
|
-
|
|
115
|
+
packages_set: Set[frozenset] = set()
|
|
116
|
+
finding_to_package_map: List[Dict[str, str]] = []
|
|
113
117
|
|
|
114
118
|
for f in results:
|
|
115
119
|
finding: Dict = {}
|
|
@@ -163,55 +167,45 @@ def transform_inspector_findings(
|
|
|
163
167
|
"vendorUpdatedAt",
|
|
164
168
|
)
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
packages = transform_inspector_packages(f["packageVulnerabilityDetails"])
|
|
171
|
+
finding["vulnerablepackageids"] = list(packages.keys())
|
|
172
|
+
for package_id, package in packages.items():
|
|
173
|
+
finding_to_package_map.append(
|
|
174
|
+
{
|
|
175
|
+
"findingarn": finding["id"],
|
|
176
|
+
"packageid": package_id,
|
|
177
|
+
"remediation": package.get("remediation"),
|
|
178
|
+
"fixedInVersion": package.get("fixedInVersion"),
|
|
179
|
+
"filePath": package.get("filePath"),
|
|
180
|
+
"sourceLayerHash": package.get("sourceLayerHash"),
|
|
181
|
+
"sourceLambdaLayerArn": package.get("sourceLambdaLayerArn"),
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
packages_set.add(frozenset(package.items()))
|
|
174
185
|
findings_list.append(finding)
|
|
175
|
-
packages_list =
|
|
176
|
-
return findings_list, packages_list
|
|
177
|
-
|
|
186
|
+
packages_list = [dict(p) for p in packages_set]
|
|
187
|
+
return findings_list, packages_list, finding_to_package_map
|
|
178
188
|
|
|
179
|
-
def transform_inspector_packages(packages: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
180
|
-
packages_list: List[Dict] = []
|
|
181
|
-
for package_id in packages.keys():
|
|
182
|
-
packages_list.append(packages[package_id])
|
|
183
189
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def _process_packages(
|
|
190
|
+
def transform_inspector_packages(
|
|
188
191
|
package_details: Dict[str, Any],
|
|
189
|
-
aws_account_id: str,
|
|
190
|
-
finding_arn: str,
|
|
191
192
|
) -> Dict[str, Any]:
|
|
192
193
|
packages: Dict[str, Any] = {}
|
|
193
194
|
for package in package_details["vulnerablePackages"]:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
f"{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
new_package["filepath"] = package.get("filePath")
|
|
209
|
-
new_package["fixedinversion"] = package.get("fixedInVersion")
|
|
210
|
-
new_package["sourcelayerhash"] = package.get("sourceLayerHash")
|
|
211
|
-
new_package["awsaccount"] = aws_account_id
|
|
212
|
-
new_package["findingarn"] = finding_arn
|
|
213
|
-
|
|
214
|
-
packages[new_package["id"]] = new_package
|
|
195
|
+
# Following RPM package naming convention for consistency
|
|
196
|
+
name = package["name"] # Mandatory field
|
|
197
|
+
epoch = str(package.get("epoch", ""))
|
|
198
|
+
if epoch:
|
|
199
|
+
epoch = f"{epoch}:"
|
|
200
|
+
version = package["version"] # Mandatory field
|
|
201
|
+
release = package.get("release", "")
|
|
202
|
+
if release:
|
|
203
|
+
release = f"-{release}"
|
|
204
|
+
arch = package.get("arch", "")
|
|
205
|
+
if arch:
|
|
206
|
+
arch = f".{arch}"
|
|
207
|
+
id = f"{name}|{epoch}{version}{release}{arch}"
|
|
208
|
+
packages[id] = {**package, "id": id}
|
|
215
209
|
|
|
216
210
|
return packages
|
|
217
211
|
|
|
@@ -244,7 +238,6 @@ def load_inspector_findings(
|
|
|
244
238
|
def load_inspector_packages(
|
|
245
239
|
neo4j_session: neo4j.Session,
|
|
246
240
|
packages: List[Dict[str, Any]],
|
|
247
|
-
region: str,
|
|
248
241
|
aws_update_tag: int,
|
|
249
242
|
current_aws_account_id: str,
|
|
250
243
|
) -> None:
|
|
@@ -252,12 +245,28 @@ def load_inspector_packages(
|
|
|
252
245
|
neo4j_session,
|
|
253
246
|
AWSInspectorPackageSchema(),
|
|
254
247
|
packages,
|
|
255
|
-
Region=region,
|
|
256
248
|
AWS_ID=current_aws_account_id,
|
|
257
249
|
lastupdated=aws_update_tag,
|
|
258
250
|
)
|
|
259
251
|
|
|
260
252
|
|
|
253
|
+
@timeit
|
|
254
|
+
def load_inspector_finding_to_package_match_links(
|
|
255
|
+
neo4j_session: neo4j.Session,
|
|
256
|
+
finding_to_package_map: List[Dict[str, str]],
|
|
257
|
+
aws_update_tag: int,
|
|
258
|
+
current_aws_account_id: str,
|
|
259
|
+
) -> None:
|
|
260
|
+
load_matchlinks(
|
|
261
|
+
neo4j_session,
|
|
262
|
+
InspectorFindingToPackageMatchLink(),
|
|
263
|
+
finding_to_package_map,
|
|
264
|
+
lastupdated=aws_update_tag,
|
|
265
|
+
_sub_resource_label="AWSAccount",
|
|
266
|
+
_sub_resource_id=current_aws_account_id,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
261
270
|
@timeit
|
|
262
271
|
def cleanup(
|
|
263
272
|
neo4j_session: neo4j.Session,
|
|
@@ -270,6 +279,14 @@ def cleanup(
|
|
|
270
279
|
GraphJob.from_node_schema(AWSInspectorPackageSchema(), common_job_parameters).run(
|
|
271
280
|
neo4j_session,
|
|
272
281
|
)
|
|
282
|
+
GraphJob.from_matchlink(
|
|
283
|
+
InspectorFindingToPackageMatchLink(),
|
|
284
|
+
"AWSAccount",
|
|
285
|
+
common_job_parameters["ACCOUNT_ID"],
|
|
286
|
+
common_job_parameters["UPDATE_TAG"],
|
|
287
|
+
).run(
|
|
288
|
+
neo4j_session,
|
|
289
|
+
)
|
|
273
290
|
|
|
274
291
|
|
|
275
292
|
def _sync_findings_for_account(
|
|
@@ -288,7 +305,9 @@ def _sync_findings_for_account(
|
|
|
288
305
|
logger.info(f"No findings to sync for account {account_id} in region {region}")
|
|
289
306
|
return
|
|
290
307
|
for f_batch in findings:
|
|
291
|
-
finding_data, package_data =
|
|
308
|
+
finding_data, package_data, finding_to_package_map = (
|
|
309
|
+
transform_inspector_findings(f_batch)
|
|
310
|
+
)
|
|
292
311
|
logger.info(f"Loading {len(finding_data)} findings from account {account_id}")
|
|
293
312
|
load_inspector_findings(
|
|
294
313
|
neo4j_session,
|
|
@@ -301,7 +320,15 @@ def _sync_findings_for_account(
|
|
|
301
320
|
load_inspector_packages(
|
|
302
321
|
neo4j_session,
|
|
303
322
|
package_data,
|
|
304
|
-
|
|
323
|
+
update_tag,
|
|
324
|
+
current_aws_account_id,
|
|
325
|
+
)
|
|
326
|
+
logger.info(
|
|
327
|
+
f"Loading {len(finding_to_package_map)} finding to package relationships"
|
|
328
|
+
)
|
|
329
|
+
load_inspector_finding_to_package_match_links(
|
|
330
|
+
neo4j_session,
|
|
331
|
+
finding_to_package_map,
|
|
305
332
|
update_tag,
|
|
306
333
|
current_aws_account_id,
|
|
307
334
|
)
|
|
@@ -337,5 +364,7 @@ def sync(
|
|
|
337
364
|
update_tag,
|
|
338
365
|
current_aws_account_id,
|
|
339
366
|
)
|
|
367
|
+
common_job_parameters["ACCOUNT_ID"] = current_aws_account_id
|
|
368
|
+
common_job_parameters["UPDATE_TAG"] = update_tag
|
|
340
369
|
|
|
341
370
|
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -6,7 +6,9 @@ from cartography.intel.aws.ec2.route_tables import sync_route_tables
|
|
|
6
6
|
from . import acm
|
|
7
7
|
from . import apigateway
|
|
8
8
|
from . import cloudtrail
|
|
9
|
+
from . import cloudtrail_management_events
|
|
9
10
|
from . import cloudwatch
|
|
11
|
+
from . import codebuild
|
|
10
12
|
from . import config
|
|
11
13
|
from . import dynamodb
|
|
12
14
|
from . import ecr
|
|
@@ -106,6 +108,8 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
|
|
|
106
108
|
"config": config.sync,
|
|
107
109
|
"identitycenter": identitycenter.sync_identity_center_instances,
|
|
108
110
|
"cloudtrail": cloudtrail.sync,
|
|
111
|
+
"cloudtrail_management_events": cloudtrail_management_events.sync,
|
|
109
112
|
"cloudwatch": cloudwatch.sync,
|
|
110
113
|
"efs": efs.sync,
|
|
114
|
+
"codebuild": codebuild.sync,
|
|
111
115
|
}
|
cartography/intel/aws/sns.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Any
|
|
2
3
|
from typing import Dict
|
|
3
4
|
from typing import List
|
|
4
5
|
from typing import Optional
|
|
@@ -9,6 +10,7 @@ import neo4j
|
|
|
9
10
|
from cartography.client.core.tx import load
|
|
10
11
|
from cartography.graph.job import GraphJob
|
|
11
12
|
from cartography.models.aws.sns.topic import SNSTopicSchema
|
|
13
|
+
from cartography.models.aws.sns.topic_subscription import SNSTopicSubscriptionSchema
|
|
12
14
|
from cartography.stats import get_stats_client
|
|
13
15
|
from cartography.util import aws_handle_regions
|
|
14
16
|
from cartography.util import merge_module_sync_metadata
|
|
@@ -108,6 +110,48 @@ def load_sns_topics(
|
|
|
108
110
|
)
|
|
109
111
|
|
|
110
112
|
|
|
113
|
+
@timeit
|
|
114
|
+
@aws_handle_regions
|
|
115
|
+
def get_subscriptions(
|
|
116
|
+
boto3_session: boto3.session.Session, region: str
|
|
117
|
+
) -> List[Dict[str, Any]]:
|
|
118
|
+
"""
|
|
119
|
+
Get all SNS Topics Subscriptions for a region.
|
|
120
|
+
"""
|
|
121
|
+
client = boto3_session.client("sns", region_name=region)
|
|
122
|
+
paginator = client.get_paginator("list_subscriptions")
|
|
123
|
+
subscriptions = []
|
|
124
|
+
for page in paginator.paginate():
|
|
125
|
+
subscriptions.extend(page.get("Subscriptions", []))
|
|
126
|
+
|
|
127
|
+
return subscriptions
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@timeit
|
|
131
|
+
def load_sns_topic_subscription(
|
|
132
|
+
neo4j_session: neo4j.Session,
|
|
133
|
+
data: List[Dict[str, Any]],
|
|
134
|
+
region: str,
|
|
135
|
+
aws_account_id: str,
|
|
136
|
+
update_tag: int,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Load SNS Topic Subscription information into the graph
|
|
140
|
+
"""
|
|
141
|
+
logger.info(
|
|
142
|
+
f"Loading {len(data)} SNS topic subscription for region {region} into graph."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
load(
|
|
146
|
+
neo4j_session,
|
|
147
|
+
SNSTopicSubscriptionSchema(),
|
|
148
|
+
data,
|
|
149
|
+
lastupdated=update_tag,
|
|
150
|
+
Region=region,
|
|
151
|
+
AWS_ID=aws_account_id,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
111
155
|
@timeit
|
|
112
156
|
def cleanup_sns(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
|
|
113
157
|
"""
|
|
@@ -117,6 +161,11 @@ def cleanup_sns(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> No
|
|
|
117
161
|
cleanup_job = GraphJob.from_node_schema(SNSTopicSchema(), common_job_parameters)
|
|
118
162
|
cleanup_job.run(neo4j_session)
|
|
119
163
|
|
|
164
|
+
cleanup_job = GraphJob.from_node_schema(
|
|
165
|
+
SNSTopicSubscriptionSchema(), common_job_parameters
|
|
166
|
+
)
|
|
167
|
+
cleanup_job.run(neo4j_session)
|
|
168
|
+
|
|
120
169
|
|
|
121
170
|
@timeit
|
|
122
171
|
def sync(
|
|
@@ -128,7 +177,7 @@ def sync(
|
|
|
128
177
|
common_job_parameters: Dict,
|
|
129
178
|
) -> None:
|
|
130
179
|
"""
|
|
131
|
-
Sync SNS Topics for all regions
|
|
180
|
+
Sync SNS Topics and Subscriptions for all regions
|
|
132
181
|
"""
|
|
133
182
|
for region in regions:
|
|
134
183
|
logger.info(
|
|
@@ -153,9 +202,20 @@ def sync(
|
|
|
153
202
|
update_tag,
|
|
154
203
|
)
|
|
155
204
|
|
|
205
|
+
# Get and load subscriptions
|
|
206
|
+
subscriptions = get_subscriptions(boto3_session, region)
|
|
207
|
+
|
|
208
|
+
load_sns_topic_subscription(
|
|
209
|
+
neo4j_session,
|
|
210
|
+
subscriptions,
|
|
211
|
+
region,
|
|
212
|
+
current_aws_account_id,
|
|
213
|
+
update_tag,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Cleanup and metadata update (outside region loop)
|
|
156
217
|
cleanup_sns(neo4j_session, common_job_parameters)
|
|
157
218
|
|
|
158
|
-
# Record that we've synced this module
|
|
159
219
|
merge_module_sync_metadata(
|
|
160
220
|
neo4j_session,
|
|
161
221
|
group_type="AWSAccount",
|
cartography/intel/entra/users.py
CHANGED
|
@@ -15,6 +15,51 @@ from cartography.util import timeit
|
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
|
+
# NOTE:
|
|
19
|
+
# Microsoft Graph imposes limits on the length of the $select clause as well as
|
|
20
|
+
# the number of properties that can be selected in a single request. In
|
|
21
|
+
# practice we have seen 400 Bad Request responses that bubble up as
|
|
22
|
+
# `Microsoft.SharePoint.Client.InvalidClientQueryException` once that limit is
|
|
23
|
+
# breached (Graph internally rewrites the next-link using a SharePoint style
|
|
24
|
+
# `id in (…)` filter which is then rejected).
|
|
25
|
+
#
|
|
26
|
+
# To avoid tripping this bug we only request a *core* subset of user attributes
|
|
27
|
+
# that are most commonly used in downstream analysis. The transform() function
|
|
28
|
+
# tolerates missing attributes (the generated MS Graph SDK simply returns
|
|
29
|
+
# `None` for properties that are not present in the payload), so fetching fewer
|
|
30
|
+
# fields is safe – we merely get more `null` values in the graph.
|
|
31
|
+
#
|
|
32
|
+
# If you need additional attributes in the future, append them here but keep the
|
|
33
|
+
# total character count of the comma-separated list comfortably below 500 and
|
|
34
|
+
# stay within the official v1.0 contract (beta-only fields cause similar
|
|
35
|
+
# failures). 20–25 fields is a good rule-of-thumb.
|
|
36
|
+
#
|
|
37
|
+
# References:
|
|
38
|
+
# • https://learn.microsoft.com/graph/query-parameters#select-parameter
|
|
39
|
+
# • https://learn.microsoft.com/graph/api/user-list?view=graph-rest-1.0
|
|
40
|
+
#
|
|
41
|
+
USER_SELECT_FIELDS = [
|
|
42
|
+
"id",
|
|
43
|
+
"userPrincipalName",
|
|
44
|
+
"displayName",
|
|
45
|
+
"givenName",
|
|
46
|
+
"surname",
|
|
47
|
+
"mail",
|
|
48
|
+
"mobilePhone",
|
|
49
|
+
"businessPhones",
|
|
50
|
+
"jobTitle",
|
|
51
|
+
"department",
|
|
52
|
+
"officeLocation",
|
|
53
|
+
"city",
|
|
54
|
+
"country",
|
|
55
|
+
"companyName",
|
|
56
|
+
"preferredLanguage",
|
|
57
|
+
"employeeId",
|
|
58
|
+
"employeeType",
|
|
59
|
+
"accountEnabled",
|
|
60
|
+
"ageGroup",
|
|
61
|
+
]
|
|
62
|
+
|
|
18
63
|
|
|
19
64
|
@timeit
|
|
20
65
|
async def get_tenant(client: GraphServiceClient) -> Organization:
|
|
@@ -27,14 +72,20 @@ async def get_tenant(client: GraphServiceClient) -> Organization:
|
|
|
27
72
|
|
|
28
73
|
@timeit
|
|
29
74
|
async def get_users(client: GraphServiceClient) -> list[User]:
|
|
75
|
+
"""Fetch all users with their manager reference in as few requests as possible.
|
|
76
|
+
|
|
77
|
+
We leverage `$expand=manager($select=id)` so the manager's *id* is hydrated
|
|
78
|
+
alongside every user record. This avoids making a second round-trip per
|
|
79
|
+
user – vastly reducing latency and eliminating the noisy 404s that occur
|
|
80
|
+
when a user has no manager assigned.
|
|
30
81
|
"""
|
|
31
|
-
|
|
32
|
-
"""
|
|
82
|
+
|
|
33
83
|
all_users: list[User] = []
|
|
34
84
|
request_configuration = client.users.UsersRequestBuilderGetRequestConfiguration(
|
|
35
85
|
query_parameters=client.users.UsersRequestBuilderGetQueryParameters(
|
|
36
|
-
# Request more items per page to reduce number of API calls
|
|
37
86
|
top=999,
|
|
87
|
+
select=USER_SELECT_FIELDS,
|
|
88
|
+
expand=["manager($select=id)"],
|
|
38
89
|
),
|
|
39
90
|
)
|
|
40
91
|
|
|
@@ -43,18 +94,32 @@ async def get_users(client: GraphServiceClient) -> list[User]:
|
|
|
43
94
|
all_users.extend(page.value)
|
|
44
95
|
if not page.odata_next_link:
|
|
45
96
|
break
|
|
46
|
-
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
page = await client.users.with_url(page.odata_next_link).get()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(
|
|
102
|
+
"Failed to fetch next page of Entra ID users – stopping pagination early: %s",
|
|
103
|
+
e,
|
|
104
|
+
)
|
|
105
|
+
break
|
|
47
106
|
|
|
48
107
|
return all_users
|
|
49
108
|
|
|
50
109
|
|
|
51
110
|
@timeit
|
|
111
|
+
# The manager reference is now embedded in the user objects courtesy of the
|
|
112
|
+
# `$expand` we added above, so we no longer need a separate `manager_map`.
|
|
52
113
|
def transform_users(users: list[User]) -> list[dict[str, Any]]:
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
"""
|
|
114
|
+
"""Convert MS Graph SDK `User` models into dicts matching our schema."""
|
|
115
|
+
|
|
56
116
|
result: list[dict[str, Any]] = []
|
|
57
117
|
for user in users:
|
|
118
|
+
manager_id: str | None = None
|
|
119
|
+
if getattr(user, "manager", None) is not None:
|
|
120
|
+
# The SDK materialises `manager` as a DirectoryObject (or subclass)
|
|
121
|
+
manager_id = getattr(user.manager, "id", None)
|
|
122
|
+
|
|
58
123
|
transformed_user = {
|
|
59
124
|
"id": user.id,
|
|
60
125
|
"user_principal_name": user.user_principal_name,
|
|
@@ -62,47 +127,24 @@ def transform_users(users: list[User]) -> list[dict[str, Any]]:
|
|
|
62
127
|
"given_name": user.given_name,
|
|
63
128
|
"surname": user.surname,
|
|
64
129
|
"mail": user.mail,
|
|
65
|
-
"
|
|
66
|
-
"preferred_language": user.preferred_language,
|
|
67
|
-
"preferred_name": user.preferred_name,
|
|
68
|
-
"state": user.state,
|
|
69
|
-
"usage_location": user.usage_location,
|
|
70
|
-
"user_type": user.user_type,
|
|
71
|
-
"show_in_address_list": user.show_in_address_list,
|
|
72
|
-
"sign_in_sessions_valid_from_date_time": user.sign_in_sessions_valid_from_date_time,
|
|
73
|
-
"security_identifier": user.on_premises_security_identifier,
|
|
74
|
-
"account_enabled": user.account_enabled,
|
|
75
|
-
"age_group": user.age_group,
|
|
130
|
+
"mobile_phone": user.mobile_phone,
|
|
76
131
|
"business_phones": user.business_phones,
|
|
132
|
+
"job_title": user.job_title,
|
|
133
|
+
"department": user.department,
|
|
134
|
+
"office_location": user.office_location,
|
|
77
135
|
"city": user.city,
|
|
78
|
-
"
|
|
79
|
-
"consent_provided_for_minor": user.consent_provided_for_minor,
|
|
136
|
+
"state": user.state,
|
|
80
137
|
"country": user.country,
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"deleted_date_time": user.deleted_date_time,
|
|
84
|
-
"department": user.department,
|
|
138
|
+
"company_name": user.company_name,
|
|
139
|
+
"preferred_language": user.preferred_language,
|
|
85
140
|
"employee_id": user.employee_id,
|
|
86
141
|
"employee_type": user.employee_type,
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"is_management_restricted": user.is_management_restricted,
|
|
91
|
-
"is_resource_account": user.is_resource_account,
|
|
92
|
-
"job_title": user.job_title,
|
|
93
|
-
"last_password_change_date_time": user.last_password_change_date_time,
|
|
94
|
-
"mail_nickname": user.mail_nickname,
|
|
95
|
-
"office_location": user.office_location,
|
|
96
|
-
"on_premises_distinguished_name": user.on_premises_distinguished_name,
|
|
97
|
-
"on_premises_domain_name": user.on_premises_domain_name,
|
|
98
|
-
"on_premises_immutable_id": user.on_premises_immutable_id,
|
|
99
|
-
"on_premises_last_sync_date_time": user.on_premises_last_sync_date_time,
|
|
100
|
-
"on_premises_sam_account_name": user.on_premises_sam_account_name,
|
|
101
|
-
"on_premises_security_identifier": user.on_premises_security_identifier,
|
|
102
|
-
"on_premises_sync_enabled": user.on_premises_sync_enabled,
|
|
103
|
-
"on_premises_user_principal_name": user.on_premises_user_principal_name,
|
|
142
|
+
"account_enabled": user.account_enabled,
|
|
143
|
+
"age_group": user.age_group,
|
|
144
|
+
"manager_id": manager_id,
|
|
104
145
|
}
|
|
105
146
|
result.append(transformed_user)
|
|
147
|
+
|
|
106
148
|
return result
|
|
107
149
|
|
|
108
150
|
|
|
@@ -198,7 +240,7 @@ async def sync_entra_users(
|
|
|
198
240
|
credential, scopes=["https://graph.microsoft.com/.default"]
|
|
199
241
|
)
|
|
200
242
|
|
|
201
|
-
#
|
|
243
|
+
# Fetch tenant and users (with manager reference already populated by `$expand`)
|
|
202
244
|
tenant = await get_tenant(client)
|
|
203
245
|
users = await get_users(client)
|
|
204
246
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
import scaleway
|
|
5
|
+
|
|
6
|
+
import cartography.intel.scaleway.iam.apikeys
|
|
7
|
+
import cartography.intel.scaleway.iam.applications
|
|
8
|
+
import cartography.intel.scaleway.iam.groups
|
|
9
|
+
import cartography.intel.scaleway.iam.users
|
|
10
|
+
import cartography.intel.scaleway.instances.flexibleips
|
|
11
|
+
import cartography.intel.scaleway.instances.instances
|
|
12
|
+
import cartography.intel.scaleway.projects
|
|
13
|
+
import cartography.intel.scaleway.storage.snapshots
|
|
14
|
+
import cartography.intel.scaleway.storage.volumes
|
|
15
|
+
from cartography.config import Config
|
|
16
|
+
from cartography.util import timeit
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@timeit
|
|
22
|
+
def start_scaleway_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
23
|
+
"""
|
|
24
|
+
If this module is configured, perform ingestion of Scaleway data. Otherwise warn and exit
|
|
25
|
+
:param neo4j_session: Neo4J session for database interface
|
|
26
|
+
:param config: A cartography.config object
|
|
27
|
+
:return: None
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
not config.scaleway_access_key
|
|
32
|
+
or not config.scaleway_secret_key
|
|
33
|
+
or not config.scaleway_org
|
|
34
|
+
):
|
|
35
|
+
logger.info(
|
|
36
|
+
"Tailscale import is not configured - skipping this module. "
|
|
37
|
+
"See docs to configure.",
|
|
38
|
+
)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Create client
|
|
42
|
+
client = scaleway.Client(
|
|
43
|
+
access_key=config.scaleway_access_key,
|
|
44
|
+
secret_key=config.scaleway_secret_key,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
common_job_parameters = {
|
|
48
|
+
"UPDATE_TAG": config.update_tag,
|
|
49
|
+
"ORG_ID": config.scaleway_org,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Organization level
|
|
53
|
+
projects = cartography.intel.scaleway.projects.sync(
|
|
54
|
+
neo4j_session,
|
|
55
|
+
client,
|
|
56
|
+
common_job_parameters,
|
|
57
|
+
org_id=config.scaleway_org,
|
|
58
|
+
update_tag=config.update_tag,
|
|
59
|
+
)
|
|
60
|
+
projects_id = [project["id"] for project in projects]
|
|
61
|
+
cartography.intel.scaleway.iam.users.sync(
|
|
62
|
+
neo4j_session,
|
|
63
|
+
client,
|
|
64
|
+
common_job_parameters,
|
|
65
|
+
org_id=config.scaleway_org,
|
|
66
|
+
update_tag=config.update_tag,
|
|
67
|
+
)
|
|
68
|
+
cartography.intel.scaleway.iam.applications.sync(
|
|
69
|
+
neo4j_session,
|
|
70
|
+
client,
|
|
71
|
+
common_job_parameters,
|
|
72
|
+
org_id=config.scaleway_org,
|
|
73
|
+
update_tag=config.update_tag,
|
|
74
|
+
)
|
|
75
|
+
cartography.intel.scaleway.iam.groups.sync(
|
|
76
|
+
neo4j_session,
|
|
77
|
+
client,
|
|
78
|
+
common_job_parameters,
|
|
79
|
+
org_id=config.scaleway_org,
|
|
80
|
+
update_tag=config.update_tag,
|
|
81
|
+
)
|
|
82
|
+
cartography.intel.scaleway.iam.apikeys.sync(
|
|
83
|
+
neo4j_session,
|
|
84
|
+
client,
|
|
85
|
+
common_job_parameters,
|
|
86
|
+
org_id=config.scaleway_org,
|
|
87
|
+
update_tag=config.update_tag,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Storage
|
|
91
|
+
cartography.intel.scaleway.storage.volumes.sync(
|
|
92
|
+
neo4j_session,
|
|
93
|
+
client,
|
|
94
|
+
common_job_parameters,
|
|
95
|
+
org_id=config.scaleway_org,
|
|
96
|
+
projects_id=projects_id,
|
|
97
|
+
update_tag=config.update_tag,
|
|
98
|
+
)
|
|
99
|
+
cartography.intel.scaleway.storage.snapshots.sync(
|
|
100
|
+
neo4j_session,
|
|
101
|
+
client,
|
|
102
|
+
common_job_parameters,
|
|
103
|
+
org_id=config.scaleway_org,
|
|
104
|
+
projects_id=projects_id,
|
|
105
|
+
update_tag=config.update_tag,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Instances
|
|
109
|
+
# DISABLED due to https://github.com/scaleway/scaleway-sdk-python/issues/1040
|
|
110
|
+
"""
|
|
111
|
+
cartography.intel.scaleway.instances.flexibleips.sync(
|
|
112
|
+
neo4j_session,
|
|
113
|
+
client,
|
|
114
|
+
common_job_parameters,
|
|
115
|
+
org_id=config.scaleway_org,
|
|
116
|
+
projects_id=projects_id,
|
|
117
|
+
update_tag=config.update_tag,
|
|
118
|
+
)
|
|
119
|
+
"""
|
|
120
|
+
cartography.intel.scaleway.instances.instances.sync(
|
|
121
|
+
neo4j_session,
|
|
122
|
+
client,
|
|
123
|
+
common_job_parameters,
|
|
124
|
+
org_id=config.scaleway_org,
|
|
125
|
+
projects_id=projects_id,
|
|
126
|
+
update_tag=config.update_tag,
|
|
127
|
+
)
|
|
File without changes
|