cartography 0.106.0rc1__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 (101) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +131 -2
  3. cartography/client/core/tx.py +62 -0
  4. cartography/config.py +42 -0
  5. cartography/driftdetect/cli.py +3 -2
  6. cartography/graph/cleanupbuilder.py +47 -0
  7. cartography/graph/job.py +42 -0
  8. cartography/graph/querybuilder.py +136 -2
  9. cartography/graph/statement.py +1 -1
  10. cartography/intel/airbyte/__init__.py +105 -0
  11. cartography/intel/airbyte/connections.py +120 -0
  12. cartography/intel/airbyte/destinations.py +81 -0
  13. cartography/intel/airbyte/organizations.py +59 -0
  14. cartography/intel/airbyte/sources.py +78 -0
  15. cartography/intel/airbyte/tags.py +64 -0
  16. cartography/intel/airbyte/users.py +106 -0
  17. cartography/intel/airbyte/util.py +122 -0
  18. cartography/intel/airbyte/workspaces.py +63 -0
  19. cartography/intel/aws/__init__.py +1 -0
  20. cartography/intel/aws/cloudtrail_management_events.py +364 -0
  21. cartography/intel/aws/cloudwatch.py +77 -0
  22. cartography/intel/aws/codebuild.py +132 -0
  23. cartography/intel/aws/ec2/subnets.py +1 -1
  24. cartography/intel/aws/ecs.py +17 -0
  25. cartography/intel/aws/efs.py +80 -0
  26. cartography/intel/aws/inspector.py +80 -61
  27. cartography/intel/aws/resources.py +4 -0
  28. cartography/intel/aws/sns.py +62 -2
  29. cartography/intel/entra/users.py +84 -42
  30. cartography/intel/scaleway/__init__.py +127 -0
  31. cartography/intel/scaleway/iam/__init__.py +0 -0
  32. cartography/intel/scaleway/iam/apikeys.py +71 -0
  33. cartography/intel/scaleway/iam/applications.py +71 -0
  34. cartography/intel/scaleway/iam/groups.py +71 -0
  35. cartography/intel/scaleway/iam/users.py +71 -0
  36. cartography/intel/scaleway/instances/__init__.py +0 -0
  37. cartography/intel/scaleway/instances/flexibleips.py +86 -0
  38. cartography/intel/scaleway/instances/instances.py +92 -0
  39. cartography/intel/scaleway/projects.py +79 -0
  40. cartography/intel/scaleway/storage/__init__.py +0 -0
  41. cartography/intel/scaleway/storage/snapshots.py +86 -0
  42. cartography/intel/scaleway/storage/volumes.py +84 -0
  43. cartography/intel/scaleway/utils.py +37 -0
  44. cartography/intel/sentinelone/__init__.py +69 -0
  45. cartography/intel/sentinelone/account.py +140 -0
  46. cartography/intel/sentinelone/agent.py +139 -0
  47. cartography/intel/sentinelone/api.py +113 -0
  48. cartography/intel/sentinelone/application.py +248 -0
  49. cartography/intel/sentinelone/utils.py +28 -0
  50. cartography/models/airbyte/__init__.py +0 -0
  51. cartography/models/airbyte/connection.py +138 -0
  52. cartography/models/airbyte/destination.py +75 -0
  53. cartography/models/airbyte/organization.py +19 -0
  54. cartography/models/airbyte/source.py +75 -0
  55. cartography/models/airbyte/stream.py +74 -0
  56. cartography/models/airbyte/tag.py +69 -0
  57. cartography/models/airbyte/user.py +111 -0
  58. cartography/models/airbyte/workspace.py +46 -0
  59. cartography/models/aws/cloudtrail/management_events.py +64 -0
  60. cartography/models/aws/cloudwatch/log_metric_filter.py +79 -0
  61. cartography/models/aws/codebuild/__init__.py +0 -0
  62. cartography/models/aws/codebuild/project.py +49 -0
  63. cartography/models/aws/ec2/networkinterfaces.py +2 -0
  64. cartography/models/aws/ec2/subnet_instance.py +2 -0
  65. cartography/models/aws/ec2/subnet_networkinterface.py +2 -0
  66. cartography/models/aws/ecs/containers.py +19 -0
  67. cartography/models/aws/ecs/task_definitions.py +38 -0
  68. cartography/models/aws/ecs/tasks.py +24 -1
  69. cartography/models/aws/efs/access_point.py +77 -0
  70. cartography/models/aws/inspector/findings.py +37 -0
  71. cartography/models/aws/inspector/packages.py +1 -31
  72. cartography/models/aws/sns/topic_subscription.py +74 -0
  73. cartography/models/core/common.py +1 -0
  74. cartography/models/core/relationships.py +44 -0
  75. cartography/models/entra/user.py +17 -51
  76. cartography/models/scaleway/__init__.py +0 -0
  77. cartography/models/scaleway/iam/__init__.py +0 -0
  78. cartography/models/scaleway/iam/apikey.py +96 -0
  79. cartography/models/scaleway/iam/application.py +52 -0
  80. cartography/models/scaleway/iam/group.py +95 -0
  81. cartography/models/scaleway/iam/user.py +60 -0
  82. cartography/models/scaleway/instance/__init__.py +0 -0
  83. cartography/models/scaleway/instance/flexibleip.py +52 -0
  84. cartography/models/scaleway/instance/instance.py +118 -0
  85. cartography/models/scaleway/organization.py +19 -0
  86. cartography/models/scaleway/project.py +48 -0
  87. cartography/models/scaleway/storage/__init__.py +0 -0
  88. cartography/models/scaleway/storage/snapshot.py +78 -0
  89. cartography/models/scaleway/storage/volume.py +51 -0
  90. cartography/models/sentinelone/__init__.py +1 -0
  91. cartography/models/sentinelone/account.py +40 -0
  92. cartography/models/sentinelone/agent.py +50 -0
  93. cartography/models/sentinelone/application.py +44 -0
  94. cartography/models/sentinelone/application_version.py +96 -0
  95. cartography/sync.py +11 -4
  96. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/METADATA +20 -16
  97. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/RECORD +101 -36
  98. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/WHEEL +0 -0
  99. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/entry_points.txt +0 -0
  100. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/licenses/LICENSE +0 -0
  101. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,132 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import boto3
7
+ import neo4j
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.codebuild.project import CodeBuildProjectSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ @aws_handle_regions
21
+ def get_all_codebuild_projects(
22
+ boto3_session: boto3.Session, region: str
23
+ ) -> List[Dict[str, Any]]:
24
+
25
+ client = boto3_session.client(
26
+ "codebuild", region_name=region, config=get_botocore_config()
27
+ )
28
+ paginator = client.get_paginator("list_projects")
29
+
30
+ all_projects = []
31
+
32
+ for page in paginator.paginate():
33
+ project_names = page.get("projects", [])
34
+ if not project_names:
35
+ continue
36
+
37
+ # AWS batch_get_projects accepts up to 100 project names per call as per AWS documentation.
38
+ for i in range(0, len(project_names), 100):
39
+ batch = project_names[i : i + 100]
40
+ response = client.batch_get_projects(names=batch)
41
+ projects = response.get("projects", [])
42
+ all_projects.extend(projects)
43
+ return all_projects
44
+
45
+
46
+ def transform_codebuild_projects(
47
+ projects: List[Dict[str, Any]], region: str
48
+ ) -> List[Dict[str, Any]]:
49
+ """
50
+ Transform CodeBuild project data for ingestion into Neo4j.
51
+
52
+ - Includes all environment variable names.
53
+ - Variables of type 'PLAINTEXT' retain their values.
54
+ - Other types (e.g., 'PARAMETER_STORE', 'SECRETS_MANAGER') have their values redacted.
55
+ """
56
+ transformed_codebuild_projects = []
57
+ for project in projects:
58
+ env_vars = project.get("environment", {}).get("environmentVariables", [])
59
+ env_var_strings = [
60
+ f"{var.get('name')}={var.get('value') if var.get('type') == 'PLAINTEXT' else '<REDACTED>'}"
61
+ for var in env_vars
62
+ ]
63
+ transformed_project = {
64
+ "arn": project["arn"],
65
+ "created": project.get("created"),
66
+ "environmentVariables": env_var_strings,
67
+ "sourceType": project.get("source", {}).get("type"),
68
+ "sourceLocation": project.get("source", {}).get("location"),
69
+ }
70
+ transformed_codebuild_projects.append(transformed_project)
71
+
72
+ return transformed_codebuild_projects
73
+
74
+
75
+ @timeit
76
+ def load_codebuild_projects(
77
+ neo4j_session: neo4j.Session,
78
+ data: List[Dict[str, Any]],
79
+ region: str,
80
+ current_aws_account_id: str,
81
+ aws_update_tag: int,
82
+ ) -> None:
83
+ logger.info(
84
+ f"Loading CodeBuild {len(data)} projects for region '{region}' into graph.",
85
+ )
86
+ load(
87
+ neo4j_session,
88
+ CodeBuildProjectSchema(),
89
+ data,
90
+ lastupdated=aws_update_tag,
91
+ Region=region,
92
+ AWS_ID=current_aws_account_id,
93
+ )
94
+
95
+
96
+ @timeit
97
+ def cleanup(
98
+ neo4j_session: neo4j.Session,
99
+ common_job_parameters: Dict[str, Any],
100
+ ) -> None:
101
+ logger.debug("Running Efs cleanup job.")
102
+ GraphJob.from_node_schema(CodeBuildProjectSchema(), common_job_parameters).run(
103
+ neo4j_session
104
+ )
105
+
106
+
107
+ @timeit
108
+ def sync(
109
+ neo4j_session: neo4j.Session,
110
+ boto3_session: boto3.session.Session,
111
+ regions: List[str],
112
+ current_aws_account_id: str,
113
+ update_tag: int,
114
+ common_job_parameters: Dict[str, Any],
115
+ ) -> None:
116
+ for region in regions:
117
+ logger.info(
118
+ f"Syncing CodeBuild for region '{region}' in account '{current_aws_account_id}'.",
119
+ )
120
+
121
+ projects = get_all_codebuild_projects(boto3_session, region)
122
+ transformed_projects = transform_codebuild_projects(projects, region)
123
+
124
+ load_codebuild_projects(
125
+ neo4j_session,
126
+ transformed_projects,
127
+ region,
128
+ current_aws_account_id,
129
+ update_tag,
130
+ )
131
+
132
+ cleanup(neo4j_session, common_job_parameters)
@@ -53,7 +53,7 @@ def load_subnets(
53
53
  snet.state = subnet.State, snet.assignipv6addressoncreation = subnet.AssignIpv6AddressOnCreation,
54
54
  snet.map_public_ip_on_launch = subnet.MapPublicIpOnLaunch, snet.subnet_arn = subnet.SubnetArn,
55
55
  snet.availability_zone = subnet.AvailabilityZone, snet.availability_zone_id = subnet.AvailabilityZoneId,
56
- snet.subnetid = subnet.SubnetId
56
+ snet.subnet_id = subnet.SubnetId
57
57
  """
58
58
 
59
59
  ingest_subnet_vpc_relations = """
@@ -169,6 +169,22 @@ def _get_containers_from_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, An
169
169
  return containers
170
170
 
171
171
 
172
+ def transform_ecs_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
173
+ """
174
+ Extract network interface ID from task attachments.
175
+ """
176
+ for task in tasks:
177
+ for attachment in task.get("attachments", []):
178
+ if attachment.get("type") == "ElasticNetworkInterface":
179
+ details = attachment.get("details", [])
180
+ for detail in details:
181
+ if detail.get("name") == "networkInterfaceId":
182
+ task["networkInterfaceId"] = detail.get("value")
183
+ break
184
+ break
185
+ return tasks
186
+
187
+
172
188
  @timeit
173
189
  def load_ecs_clusters(
174
190
  neo4j_session: neo4j.Session,
@@ -407,6 +423,7 @@ def _sync_ecs_task_and_container_defns(
407
423
  boto3_session,
408
424
  region,
409
425
  )
426
+ tasks = transform_ecs_tasks(tasks)
410
427
  containers = _get_containers_from_tasks(tasks)
411
428
  load_ecs_tasks(
412
429
  neo4j_session,
@@ -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)
@@ -3,21 +3,22 @@ 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
17
20
  from cartography.util import batch
18
21
  from cartography.util import timeit
19
- from cartography.util import to_asynchronous
20
- from cartography.util import to_synchronous
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
@@ -109,9 +110,10 @@ def get_inspector_findings(
109
110
 
110
111
  def transform_inspector_findings(
111
112
  results: List[Dict[str, Any]],
112
- ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
113
+ ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, str]]]:
113
114
  findings_list: List[Dict] = []
114
- packages: Dict[str, Any] = {}
115
+ packages_set: Set[frozenset] = set()
116
+ finding_to_package_map: List[Dict[str, str]] = []
115
117
 
116
118
  for f in results:
117
119
  finding: Dict = {}
@@ -165,55 +167,45 @@ def transform_inspector_findings(
165
167
  "vendorUpdatedAt",
166
168
  )
167
169
 
168
- new_packages = _process_packages(
169
- f["packageVulnerabilityDetails"],
170
- f["awsAccountId"],
171
- f["findingArn"],
172
- )
173
- finding["vulnerablepackageids"] = list(new_packages.keys())
174
- packages = {**packages, **new_packages}
175
-
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()))
176
185
  findings_list.append(finding)
177
- packages_list = transform_inspector_packages(packages)
178
- return findings_list, packages_list
179
-
180
-
181
- def transform_inspector_packages(packages: Dict[str, Any]) -> List[Dict[str, Any]]:
182
- packages_list: List[Dict] = []
183
- for package_id in packages.keys():
184
- packages_list.append(packages[package_id])
186
+ packages_list = [dict(p) for p in packages_set]
187
+ return findings_list, packages_list, finding_to_package_map
185
188
 
186
- return packages_list
187
189
 
188
-
189
- def _process_packages(
190
+ def transform_inspector_packages(
190
191
  package_details: Dict[str, Any],
191
- aws_account_id: str,
192
- finding_arn: str,
193
192
  ) -> Dict[str, Any]:
194
193
  packages: Dict[str, Any] = {}
195
194
  for package in package_details["vulnerablePackages"]:
196
- new_package = {}
197
- new_package["id"] = (
198
- f"{package.get('name', '')}|"
199
- f"{package.get('arch', '')}|"
200
- f"{package.get('version', '')}|"
201
- f"{package.get('release', '')}|"
202
- f"{package.get('epoch', '')}"
203
- )
204
- new_package["name"] = package.get("name")
205
- new_package["arch"] = package.get("arch")
206
- new_package["version"] = package.get("version")
207
- new_package["release"] = package.get("release")
208
- new_package["epoch"] = package.get("epoch")
209
- new_package["manager"] = package.get("packageManager")
210
- new_package["filepath"] = package.get("filePath")
211
- new_package["fixedinversion"] = package.get("fixedInVersion")
212
- new_package["sourcelayerhash"] = package.get("sourceLayerHash")
213
- new_package["awsaccount"] = aws_account_id
214
- new_package["findingarn"] = finding_arn
215
-
216
- 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}
217
209
 
218
210
  return packages
219
211
 
@@ -246,7 +238,6 @@ def load_inspector_findings(
246
238
  def load_inspector_packages(
247
239
  neo4j_session: neo4j.Session,
248
240
  packages: List[Dict[str, Any]],
249
- region: str,
250
241
  aws_update_tag: int,
251
242
  current_aws_account_id: str,
252
243
  ) -> None:
@@ -254,12 +245,28 @@ def load_inspector_packages(
254
245
  neo4j_session,
255
246
  AWSInspectorPackageSchema(),
256
247
  packages,
257
- Region=region,
258
248
  AWS_ID=current_aws_account_id,
259
249
  lastupdated=aws_update_tag,
260
250
  )
261
251
 
262
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
+
263
270
  @timeit
264
271
  def cleanup(
265
272
  neo4j_session: neo4j.Session,
@@ -272,6 +279,14 @@ def cleanup(
272
279
  GraphJob.from_node_schema(AWSInspectorPackageSchema(), common_job_parameters).run(
273
280
  neo4j_session,
274
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
+ )
275
290
 
276
291
 
277
292
  def _sync_findings_for_account(
@@ -290,7 +305,9 @@ def _sync_findings_for_account(
290
305
  logger.info(f"No findings to sync for account {account_id} in region {region}")
291
306
  return
292
307
  for f_batch in findings:
293
- 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
+ )
294
311
  logger.info(f"Loading {len(finding_data)} findings from account {account_id}")
295
312
  load_inspector_findings(
296
313
  neo4j_session,
@@ -303,7 +320,15 @@ def _sync_findings_for_account(
303
320
  load_inspector_packages(
304
321
  neo4j_session,
305
322
  package_data,
306
- 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,
307
332
  update_tag,
308
333
  current_aws_account_id,
309
334
  )
@@ -329,10 +354,9 @@ def sync(
329
354
  member_accounts = get_member_accounts(boto3_session, region)
330
355
  # the current host account may not be considered a "member", but we still fetch its findings
331
356
  member_accounts.append(current_aws_account_id)
332
-
333
- async def async_ingest_findings_for_account(account_id: str) -> None:
334
- await to_asynchronous(
335
- _sync_findings_for_account,
357
+ logger.info(f"Member accounts to be synced: {member_accounts}")
358
+ for account_id in member_accounts:
359
+ _sync_findings_for_account(
336
360
  neo4j_session,
337
361
  boto3_session,
338
362
  region,
@@ -340,12 +364,7 @@ def sync(
340
364
  update_tag,
341
365
  current_aws_account_id,
342
366
  )
343
-
344
- to_synchronous(
345
- *[
346
- async_ingest_findings_for_account(account_id)
347
- for account_id in member_accounts
348
- ]
349
- )
367
+ common_job_parameters["ACCOUNT_ID"] = current_aws_account_id
368
+ common_job_parameters["UPDATE_TAG"] = update_tag
350
369
 
351
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",