cartography 0.106.0rc1__py3-none-any.whl → 0.107.0rc1__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/client/core/tx.py +62 -0
- cartography/config.py +42 -0
- cartography/driftdetect/cli.py +3 -2
- cartography/graph/cleanupbuilder.py +47 -0
- cartography/graph/job.py +42 -0
- cartography/graph/querybuilder.py +136 -2
- cartography/graph/statement.py +1 -1
- 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/codebuild.py +132 -0
- cartography/intel/aws/efs.py +80 -0
- cartography/intel/aws/inspector.py +3 -13
- 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 +63 -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/utils.py +9 -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/codebuild/__init__.py +0 -0
- cartography/models/aws/codebuild/project.py +49 -0
- cartography/models/aws/ecs/containers.py +19 -0
- cartography/models/aws/ecs/task_definitions.py +38 -0
- cartography/models/aws/efs/access_point.py +77 -0
- cartography/models/aws/sns/topic_subscription.py +74 -0
- cartography/models/core/common.py +1 -0
- cartography/models/core/relationships.py +44 -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/sync.py +11 -4
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/METADATA +20 -16
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/RECORD +88 -27
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.106.0rc1.dist-info → cartography-0.107.0rc1.dist-info}/top_level.txt +0 -0
cartography/intel/aws/efs.py
CHANGED
|
@@ -9,6 +9,7 @@ import neo4j
|
|
|
9
9
|
from cartography.client.core.tx import load
|
|
10
10
|
from cartography.graph.job import GraphJob
|
|
11
11
|
from cartography.intel.aws.ec2.util import get_botocore_config
|
|
12
|
+
from cartography.models.aws.efs.access_point import EfsAccessPointSchema
|
|
12
13
|
from cartography.models.aws.efs.file_system import EfsFileSystemSchema
|
|
13
14
|
from cartography.models.aws.efs.mount_target import EfsMountTargetSchema
|
|
14
15
|
from cartography.util import aws_handle_regions
|
|
@@ -44,6 +45,7 @@ def transform_efs_file_systems(
|
|
|
44
45
|
transformed_file_system = {
|
|
45
46
|
"FileSystemId": file_system["FileSystemId"],
|
|
46
47
|
"FileSystemArn": file_system["FileSystemArn"],
|
|
48
|
+
"Region": region,
|
|
47
49
|
"OwnerId": file_system.get("OwnerId"),
|
|
48
50
|
"CreationToken": file_system.get("CreationToken"),
|
|
49
51
|
"CreationTime": file_system.get("CreationTime"),
|
|
@@ -87,6 +89,49 @@ def get_efs_mount_targets(
|
|
|
87
89
|
return mountTargets
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
@timeit
|
|
93
|
+
@aws_handle_regions
|
|
94
|
+
def get_efs_access_points(
|
|
95
|
+
boto3_session: boto3.Session, region: str
|
|
96
|
+
) -> List[Dict[str, Any]]:
|
|
97
|
+
client = boto3_session.client(
|
|
98
|
+
"efs", region_name=region, config=get_botocore_config()
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
paginator = client.get_paginator("describe_access_points")
|
|
102
|
+
accessPoints = []
|
|
103
|
+
for page in paginator.paginate():
|
|
104
|
+
accessPoints.extend(page.get("AccessPoints", []))
|
|
105
|
+
|
|
106
|
+
return accessPoints
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def transform_efs_access_points(
|
|
110
|
+
accessPoints: List[Dict[str, Any]], region: str
|
|
111
|
+
) -> List[Dict[str, Any]]:
|
|
112
|
+
"""
|
|
113
|
+
Transform Efs Access Points data for ingestion
|
|
114
|
+
"""
|
|
115
|
+
transformed = []
|
|
116
|
+
for ap in accessPoints:
|
|
117
|
+
transformed.append(
|
|
118
|
+
{
|
|
119
|
+
"AccessPointArn": ap["AccessPointArn"],
|
|
120
|
+
"AccessPointId": ap["AccessPointId"],
|
|
121
|
+
"Region": region,
|
|
122
|
+
"FileSystemId": ap["FileSystemId"],
|
|
123
|
+
"Name": ap.get("Name"),
|
|
124
|
+
"LifeCycleState": ap.get("LifeCycleState"),
|
|
125
|
+
"OwnerId": ap.get("OwnerId"),
|
|
126
|
+
"Uid": ap.get("PosixUser", {}).get("Uid"),
|
|
127
|
+
"Gid": ap.get("PosixUser", {}).get("Gid"),
|
|
128
|
+
"RootDirectoryPath": ap.get("RootDirectory", {}).get("Path"),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return transformed
|
|
133
|
+
|
|
134
|
+
|
|
90
135
|
@timeit
|
|
91
136
|
def load_efs_mount_targets(
|
|
92
137
|
neo4j_session: neo4j.Session,
|
|
@@ -129,6 +174,27 @@ def load_efs_file_systems(
|
|
|
129
174
|
)
|
|
130
175
|
|
|
131
176
|
|
|
177
|
+
@timeit
|
|
178
|
+
def load_efs_access_points(
|
|
179
|
+
neo4j_session: neo4j.Session,
|
|
180
|
+
data: List[Dict[str, Any]],
|
|
181
|
+
region: str,
|
|
182
|
+
current_aws_account_id: str,
|
|
183
|
+
aws_update_tag: int,
|
|
184
|
+
) -> None:
|
|
185
|
+
logger.info(
|
|
186
|
+
f"Loading Efs {len(data)} access points for region '{region}' into graph.",
|
|
187
|
+
)
|
|
188
|
+
load(
|
|
189
|
+
neo4j_session,
|
|
190
|
+
EfsAccessPointSchema(),
|
|
191
|
+
data,
|
|
192
|
+
lastupdated=aws_update_tag,
|
|
193
|
+
Region=region,
|
|
194
|
+
AWS_ID=current_aws_account_id,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
132
198
|
@timeit
|
|
133
199
|
def cleanup(
|
|
134
200
|
neo4j_session: neo4j.Session,
|
|
@@ -141,6 +207,9 @@ def cleanup(
|
|
|
141
207
|
GraphJob.from_node_schema(EfsFileSystemSchema(), common_job_parameters).run(
|
|
142
208
|
neo4j_session
|
|
143
209
|
)
|
|
210
|
+
GraphJob.from_node_schema(EfsAccessPointSchema(), common_job_parameters).run(
|
|
211
|
+
neo4j_session
|
|
212
|
+
)
|
|
144
213
|
|
|
145
214
|
|
|
146
215
|
@timeit
|
|
@@ -178,4 +247,15 @@ def sync(
|
|
|
178
247
|
update_tag,
|
|
179
248
|
)
|
|
180
249
|
|
|
250
|
+
accessPoints = get_efs_access_points(boto3_session, region)
|
|
251
|
+
accessPoints_transformed = transform_efs_access_points(accessPoints, region)
|
|
252
|
+
|
|
253
|
+
load_efs_access_points(
|
|
254
|
+
neo4j_session,
|
|
255
|
+
accessPoints_transformed,
|
|
256
|
+
region,
|
|
257
|
+
current_aws_account_id,
|
|
258
|
+
update_tag,
|
|
259
|
+
)
|
|
260
|
+
|
|
181
261
|
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -16,8 +16,6 @@ from cartography.util import aws_handle_regions
|
|
|
16
16
|
from cartography.util import aws_paginate
|
|
17
17
|
from cartography.util import batch
|
|
18
18
|
from cartography.util import timeit
|
|
19
|
-
from cartography.util import to_asynchronous
|
|
20
|
-
from cartography.util import to_synchronous
|
|
21
19
|
|
|
22
20
|
logger = logging.getLogger(__name__)
|
|
23
21
|
|
|
@@ -329,10 +327,9 @@ def sync(
|
|
|
329
327
|
member_accounts = get_member_accounts(boto3_session, region)
|
|
330
328
|
# the current host account may not be considered a "member", but we still fetch its findings
|
|
331
329
|
member_accounts.append(current_aws_account_id)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
_sync_findings_for_account,
|
|
330
|
+
logger.info(f"Member accounts to be synced: {member_accounts}")
|
|
331
|
+
for account_id in member_accounts:
|
|
332
|
+
_sync_findings_for_account(
|
|
336
333
|
neo4j_session,
|
|
337
334
|
boto3_session,
|
|
338
335
|
region,
|
|
@@ -341,11 +338,4 @@ def sync(
|
|
|
341
338
|
current_aws_account_id,
|
|
342
339
|
)
|
|
343
340
|
|
|
344
|
-
to_synchronous(
|
|
345
|
-
*[
|
|
346
|
-
async_ingest_findings_for_account(account_id)
|
|
347
|
-
for account_id in member_accounts
|
|
348
|
-
]
|
|
349
|
-
)
|
|
350
|
-
|
|
351
341
|
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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
import scaleway
|
|
6
|
+
from scaleway.iam.v1alpha1 import APIKey
|
|
7
|
+
from scaleway.iam.v1alpha1 import IamV1Alpha1API
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.intel.scaleway.utils import scaleway_obj_to_dict
|
|
12
|
+
from cartography.models.scaleway.iam.apikey import ScalewayApiKeySchema
|
|
13
|
+
from cartography.util import timeit
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@timeit
|
|
19
|
+
def sync(
|
|
20
|
+
neo4j_session: neo4j.Session,
|
|
21
|
+
client: scaleway.Client,
|
|
22
|
+
common_job_parameters: dict[str, Any],
|
|
23
|
+
org_id: str,
|
|
24
|
+
update_tag: int,
|
|
25
|
+
) -> None:
|
|
26
|
+
apikeys = get(client, org_id)
|
|
27
|
+
formatted_apikeys = transform_apikeys(apikeys)
|
|
28
|
+
load_apikeys(neo4j_session, formatted_apikeys, org_id, update_tag)
|
|
29
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@timeit
|
|
33
|
+
def get(
|
|
34
|
+
client: scaleway.Client,
|
|
35
|
+
org_id: str,
|
|
36
|
+
) -> list[APIKey]:
|
|
37
|
+
api = IamV1Alpha1API(client)
|
|
38
|
+
return api.list_api_keys_all(organization_id=org_id)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transform_apikeys(apikeys: list[APIKey]) -> list[dict[str, Any]]:
|
|
42
|
+
formatted_apikeys = []
|
|
43
|
+
for apikey in apikeys:
|
|
44
|
+
formatted_apikeys.append(scaleway_obj_to_dict(apikey))
|
|
45
|
+
return formatted_apikeys
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@timeit
|
|
49
|
+
def load_apikeys(
|
|
50
|
+
neo4j_session: neo4j.Session,
|
|
51
|
+
data: list[dict[str, Any]],
|
|
52
|
+
org_id: str,
|
|
53
|
+
update_tag: int,
|
|
54
|
+
) -> None:
|
|
55
|
+
logger.info("Loading %d Scaleway ApiKeys into Neo4j.", len(data))
|
|
56
|
+
load(
|
|
57
|
+
neo4j_session,
|
|
58
|
+
ScalewayApiKeySchema(),
|
|
59
|
+
data,
|
|
60
|
+
lastupdated=update_tag,
|
|
61
|
+
ORG_ID=org_id,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@timeit
|
|
66
|
+
def cleanup(
|
|
67
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
68
|
+
) -> None:
|
|
69
|
+
GraphJob.from_node_schema(ScalewayApiKeySchema(), common_job_parameters).run(
|
|
70
|
+
neo4j_session
|
|
71
|
+
)
|