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.

Files changed (92) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +131 -2
  3. cartography/config.py +42 -0
  4. cartography/driftdetect/cli.py +3 -2
  5. cartography/intel/airbyte/__init__.py +105 -0
  6. cartography/intel/airbyte/connections.py +120 -0
  7. cartography/intel/airbyte/destinations.py +81 -0
  8. cartography/intel/airbyte/organizations.py +59 -0
  9. cartography/intel/airbyte/sources.py +78 -0
  10. cartography/intel/airbyte/tags.py +64 -0
  11. cartography/intel/airbyte/users.py +106 -0
  12. cartography/intel/airbyte/util.py +122 -0
  13. cartography/intel/airbyte/workspaces.py +63 -0
  14. cartography/intel/aws/__init__.py +1 -0
  15. cartography/intel/aws/cloudtrail_management_events.py +364 -0
  16. cartography/intel/aws/cloudwatch.py +77 -0
  17. cartography/intel/aws/codebuild.py +132 -0
  18. cartography/intel/aws/ec2/subnets.py +1 -1
  19. cartography/intel/aws/ecs.py +17 -0
  20. cartography/intel/aws/inspector.py +77 -48
  21. cartography/intel/aws/resources.py +4 -0
  22. cartography/intel/aws/sns.py +62 -2
  23. cartography/intel/entra/users.py +84 -42
  24. cartography/intel/scaleway/__init__.py +127 -0
  25. cartography/intel/scaleway/iam/__init__.py +0 -0
  26. cartography/intel/scaleway/iam/apikeys.py +71 -0
  27. cartography/intel/scaleway/iam/applications.py +71 -0
  28. cartography/intel/scaleway/iam/groups.py +71 -0
  29. cartography/intel/scaleway/iam/users.py +71 -0
  30. cartography/intel/scaleway/instances/__init__.py +0 -0
  31. cartography/intel/scaleway/instances/flexibleips.py +86 -0
  32. cartography/intel/scaleway/instances/instances.py +92 -0
  33. cartography/intel/scaleway/projects.py +79 -0
  34. cartography/intel/scaleway/storage/__init__.py +0 -0
  35. cartography/intel/scaleway/storage/snapshots.py +86 -0
  36. cartography/intel/scaleway/storage/volumes.py +84 -0
  37. cartography/intel/scaleway/utils.py +37 -0
  38. cartography/intel/sentinelone/__init__.py +69 -0
  39. cartography/intel/sentinelone/account.py +140 -0
  40. cartography/intel/sentinelone/agent.py +139 -0
  41. cartography/intel/sentinelone/api.py +113 -0
  42. cartography/intel/sentinelone/application.py +248 -0
  43. cartography/intel/sentinelone/utils.py +28 -0
  44. cartography/models/airbyte/__init__.py +0 -0
  45. cartography/models/airbyte/connection.py +138 -0
  46. cartography/models/airbyte/destination.py +75 -0
  47. cartography/models/airbyte/organization.py +19 -0
  48. cartography/models/airbyte/source.py +75 -0
  49. cartography/models/airbyte/stream.py +74 -0
  50. cartography/models/airbyte/tag.py +69 -0
  51. cartography/models/airbyte/user.py +111 -0
  52. cartography/models/airbyte/workspace.py +46 -0
  53. cartography/models/aws/cloudtrail/management_events.py +64 -0
  54. cartography/models/aws/cloudwatch/log_metric_filter.py +79 -0
  55. cartography/models/aws/codebuild/__init__.py +0 -0
  56. cartography/models/aws/codebuild/project.py +49 -0
  57. cartography/models/aws/ec2/networkinterfaces.py +2 -0
  58. cartography/models/aws/ec2/subnet_instance.py +2 -0
  59. cartography/models/aws/ec2/subnet_networkinterface.py +2 -0
  60. cartography/models/aws/ecs/containers.py +19 -0
  61. cartography/models/aws/ecs/task_definitions.py +38 -0
  62. cartography/models/aws/ecs/tasks.py +24 -1
  63. cartography/models/aws/inspector/findings.py +37 -0
  64. cartography/models/aws/inspector/packages.py +1 -31
  65. cartography/models/aws/sns/topic_subscription.py +74 -0
  66. cartography/models/entra/user.py +17 -51
  67. cartography/models/scaleway/__init__.py +0 -0
  68. cartography/models/scaleway/iam/__init__.py +0 -0
  69. cartography/models/scaleway/iam/apikey.py +96 -0
  70. cartography/models/scaleway/iam/application.py +52 -0
  71. cartography/models/scaleway/iam/group.py +95 -0
  72. cartography/models/scaleway/iam/user.py +60 -0
  73. cartography/models/scaleway/instance/__init__.py +0 -0
  74. cartography/models/scaleway/instance/flexibleip.py +52 -0
  75. cartography/models/scaleway/instance/instance.py +118 -0
  76. cartography/models/scaleway/organization.py +19 -0
  77. cartography/models/scaleway/project.py +48 -0
  78. cartography/models/scaleway/storage/__init__.py +0 -0
  79. cartography/models/scaleway/storage/snapshot.py +78 -0
  80. cartography/models/scaleway/storage/volume.py +51 -0
  81. cartography/models/sentinelone/__init__.py +1 -0
  82. cartography/models/sentinelone/account.py +40 -0
  83. cartography/models/sentinelone/agent.py +50 -0
  84. cartography/models/sentinelone/application.py +44 -0
  85. cartography/models/sentinelone/application_version.py +96 -0
  86. cartography/sync.py +11 -4
  87. {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/METADATA +20 -16
  88. {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/RECORD +92 -28
  89. {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/WHEEL +0 -0
  90. {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/entry_points.txt +0 -0
  91. {cartography-0.106.0rc2.dist-info → cartography-0.107.0.dist-info}/licenses/LICENSE +0 -0
  92. {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
- packages: Dict[str, Any] = {}
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
- new_packages = _process_packages(
167
- f["packageVulnerabilityDetails"],
168
- f["awsAccountId"],
169
- f["findingArn"],
170
- )
171
- finding["vulnerablepackageids"] = list(new_packages.keys())
172
- packages = {**packages, **new_packages}
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 = transform_inspector_packages(packages)
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
- return packages_list
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
- new_package = {}
195
- new_package["id"] = (
196
- f"{package.get('name', '')}|"
197
- f"{package.get('arch', '')}|"
198
- f"{package.get('version', '')}|"
199
- f"{package.get('release', '')}|"
200
- f"{package.get('epoch', '')}"
201
- )
202
- new_package["name"] = package.get("name")
203
- new_package["arch"] = package.get("arch")
204
- new_package["version"] = package.get("version")
205
- new_package["release"] = package.get("release")
206
- new_package["epoch"] = package.get("epoch")
207
- new_package["manager"] = package.get("packageManager")
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 = transform_inspector_findings(f_batch)
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
- region,
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
  }
@@ -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",
@@ -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
- Get all users from Microsoft Graph API with pagination support
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
- page = await client.users.with_url(page.odata_next_link).get()
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
- Transform the API response into the format expected by our schema
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
- "other_mails": user.other_mails,
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
- "company_name": user.company_name,
79
- "consent_provided_for_minor": user.consent_provided_for_minor,
136
+ "state": user.state,
80
137
  "country": user.country,
81
- "created_date_time": user.created_date_time,
82
- "creation_type": user.creation_type,
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
- "external_user_state": user.external_user_state,
88
- "external_user_state_change_date_time": user.external_user_state_change_date_time,
89
- "hire_date": user.hire_date,
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
- # Get tenant information
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