cartography 0.110.0rc2__py3-none-any.whl → 0.111.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 (54) hide show
  1. cartography/_version.py +16 -3
  2. cartography/cli.py +46 -0
  3. cartography/config.py +16 -0
  4. cartography/data/indexes.cypher +0 -2
  5. cartography/data/jobs/analysis/keycloak_inheritance.json +30 -0
  6. cartography/graph/querybuilder.py +70 -0
  7. cartography/intel/aws/apigateway.py +113 -4
  8. cartography/intel/aws/ec2/vpc.py +140 -124
  9. cartography/intel/aws/eventbridge.py +73 -0
  10. cartography/intel/github/repos.py +28 -12
  11. cartography/intel/github/util.py +12 -0
  12. cartography/intel/keycloak/__init__.py +153 -0
  13. cartography/intel/keycloak/authenticationexecutions.py +322 -0
  14. cartography/intel/keycloak/authenticationflows.py +77 -0
  15. cartography/intel/keycloak/clients.py +187 -0
  16. cartography/intel/keycloak/groups.py +126 -0
  17. cartography/intel/keycloak/identityproviders.py +94 -0
  18. cartography/intel/keycloak/organizations.py +163 -0
  19. cartography/intel/keycloak/realms.py +61 -0
  20. cartography/intel/keycloak/roles.py +202 -0
  21. cartography/intel/keycloak/scopes.py +73 -0
  22. cartography/intel/keycloak/users.py +70 -0
  23. cartography/intel/keycloak/util.py +47 -0
  24. cartography/models/aws/apigateway/apigatewaydeployment.py +74 -0
  25. cartography/models/aws/ec2/vpc.py +46 -0
  26. cartography/models/aws/ec2/vpc_cidr.py +102 -0
  27. cartography/models/aws/eventbridge/target.py +71 -0
  28. cartography/models/keycloak/__init__.py +0 -0
  29. cartography/models/keycloak/authenticationexecution.py +160 -0
  30. cartography/models/keycloak/authenticationflow.py +54 -0
  31. cartography/models/keycloak/client.py +177 -0
  32. cartography/models/keycloak/group.py +101 -0
  33. cartography/models/keycloak/identityprovider.py +89 -0
  34. cartography/models/keycloak/organization.py +116 -0
  35. cartography/models/keycloak/organizationdomain.py +73 -0
  36. cartography/models/keycloak/realm.py +173 -0
  37. cartography/models/keycloak/role.py +126 -0
  38. cartography/models/keycloak/scope.py +73 -0
  39. cartography/models/keycloak/user.py +51 -0
  40. cartography/models/tailscale/device.py +1 -0
  41. cartography/sync.py +2 -0
  42. cartography/util.py +8 -0
  43. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/METADATA +2 -1
  44. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/RECORD +53 -25
  45. cartography/data/jobs/cleanup/aws_import_vpc_cleanup.json +0 -23
  46. /cartography/models/aws/{__init__.py → apigateway/__init__.py} +0 -0
  47. /cartography/models/aws/{apigateway.py → apigateway/apigateway.py} +0 -0
  48. /cartography/models/aws/{apigatewaycertificate.py → apigateway/apigatewaycertificate.py} +0 -0
  49. /cartography/models/aws/{apigatewayresource.py → apigateway/apigatewayresource.py} +0 -0
  50. /cartography/models/aws/{apigatewaystage.py → apigateway/apigatewaystage.py} +0 -0
  51. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/WHEEL +0 -0
  52. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/entry_points.txt +0 -0
  53. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/licenses/LICENSE +0 -0
  54. {cartography-0.110.0rc2.dist-info → cartography-0.111.0.dist-info}/top_level.txt +0 -0
cartography/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '0.110.0rc2'
21
- __version_tuple__ = version_tuple = (0, 110, 0, 'rc2')
31
+ __version__ = version = '0.111.0'
32
+ __version_tuple__ = version_tuple = (0, 111, 0)
33
+
34
+ __commit_id__ = commit_id = None
cartography/cli.py CHANGED
@@ -762,6 +762,42 @@ class CLI:
762
762
  "Required if you are using the SentinelOne intel module. Ignored otherwise."
763
763
  ),
764
764
  )
765
+ parser.add_argument(
766
+ "--keycloak-client-id",
767
+ type=str,
768
+ default=None,
769
+ help=(
770
+ "The Keycloak client ID to sync. "
771
+ "Required if you are using the Keycloak intel module. Ignored otherwise."
772
+ ),
773
+ )
774
+ parser.add_argument(
775
+ "--keycloak-client-secret-env-var",
776
+ type=str,
777
+ default="KEYCLOAK_CLIENT_SECRET",
778
+ help=(
779
+ "The name of an environment variable containing the Keycloak client secret. "
780
+ "Required if you are using the Keycloak intel module. Ignored otherwise."
781
+ ),
782
+ )
783
+ parser.add_argument(
784
+ "--keycloak-url",
785
+ type=str,
786
+ help=(
787
+ "The base URL for the Keycloak instance. "
788
+ "Required if you are using the Keycloak intel module. Ignored otherwise. "
789
+ ),
790
+ )
791
+ parser.add_argument(
792
+ "--keycloak-realm",
793
+ type=str,
794
+ default="master",
795
+ help=(
796
+ "The Keycloak realm used for authentication (note: all available realms will be synced). "
797
+ "Should be `master` (default value) in most of the cases. "
798
+ "Required if you are using the Keycloak intel module. Ignored otherwise. "
799
+ ),
800
+ )
765
801
 
766
802
  return parser
767
803
 
@@ -1133,6 +1169,16 @@ class CLI:
1133
1169
  else:
1134
1170
  config.sentinelone_api_token = None
1135
1171
 
1172
+ if config.keycloak_client_secret_env_var:
1173
+ logger.debug(
1174
+ f"Reading Client Secret for Keycloak from environment variable {config.keycloak_client_secret_env_var}",
1175
+ )
1176
+ config.keycloak_client_secret = os.environ.get(
1177
+ config.keycloak_client_secret_env_var
1178
+ )
1179
+ else:
1180
+ config.keycloak_client_secret = None
1181
+
1136
1182
  # Run cartography
1137
1183
  try:
1138
1184
  return cartography.sync.run_with_config(self.sync, config)
cartography/config.py CHANGED
@@ -166,6 +166,14 @@ class Config:
166
166
  :param sentinelone_api_token: SentinelOne API token for authentication. Optional.
167
167
  :type sentinelone_account_ids: list[str]
168
168
  :param sentinelone_account_ids: List of SentinelOne account IDs to sync. Optional.
169
+ :type keycloak_client_id: str
170
+ :param keycloak_client_id: Keycloak client ID for API authentication. Optional.
171
+ :type keycloak_client_secret: str
172
+ :param keycloak_client_secret: Keycloak client secret for API authentication. Optional.
173
+ :type keycloak_realm: str
174
+ :param keycloak_realm: Keycloak realm for authentication (all realms will be synced). Optional.
175
+ :type keycloak_url: str
176
+ :param keycloak_url: Keycloak base URL, e.g. https://keycloak.example.com. Optional.
169
177
  """
170
178
 
171
179
  def __init__(
@@ -252,6 +260,10 @@ class Config:
252
260
  sentinelone_api_url=None,
253
261
  sentinelone_api_token=None,
254
262
  sentinelone_account_ids=None,
263
+ keycloak_client_id=None,
264
+ keycloak_client_secret=None,
265
+ keycloak_realm=None,
266
+ keycloak_url=None,
255
267
  ):
256
268
  self.neo4j_uri = neo4j_uri
257
269
  self.neo4j_user = neo4j_user
@@ -337,3 +349,7 @@ class Config:
337
349
  self.sentinelone_api_url = sentinelone_api_url
338
350
  self.sentinelone_api_token = sentinelone_api_token
339
351
  self.sentinelone_account_ids = sentinelone_account_ids
352
+ self.keycloak_client_id = keycloak_client_id
353
+ self.keycloak_client_secret = keycloak_client_secret
354
+ self.keycloak_realm = keycloak_realm
355
+ self.keycloak_url = keycloak_url
@@ -51,8 +51,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:AWSTransitGatewayAttachment) ON (n.lastupdated
51
51
  CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.arn);
52
52
  CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.name);
53
53
  CREATE INDEX IF NOT EXISTS FOR (n:AWSUser) ON (n.lastupdated);
54
- CREATE INDEX IF NOT EXISTS FOR (n:AWSVpc) ON (n.id);
55
- CREATE INDEX IF NOT EXISTS FOR (n:AWSVpc) ON (n.lastupdated);
56
54
  CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.accesskeyid);
57
55
  CREATE INDEX IF NOT EXISTS FOR (n:AccountAccessKey) ON (n.lastupdated);
58
56
  CREATE INDEX IF NOT EXISTS FOR (n:AutoScalingGroup) ON (n.arn);
@@ -0,0 +1,30 @@
1
+ {
2
+ "statements": [
3
+ {
4
+ "__comment__": "Inherit group memberships from subgroups to parent groups",
5
+ "query": "MATCH (u:KeycloakUser)-[:MEMBER_OF]->(g:KeycloakGroup)-[:SUBGROUP_OF*1..5]->(pg:KeycloakGroup) MERGE (u)-[r:INHERITED_MEMBER_OF]->(pg) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG",
6
+ "iterative": false
7
+ },
8
+ {
9
+ "__comment__": "Assign roles to users based on group memberships",
10
+ "query": "MATCH (u:KeycloakUser)-[:MEMBER_OF|INHERITED_MEMBER_OR]->(g:KeycloakGroup)-[:GRANTS]->(r:KeycloakRole) MERGE (u)-[r0:ASSUME_ROLE]-(r) ON CREATE SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
11
+ "iterative": false
12
+ },
13
+ {
14
+ "__comment__": "Propagate role grants to composite roles",
15
+ "query": "MATCH (r:KeycloakRole)-[:INCLUDES*1..5]->(c:KeycloakRole)-[:GRANTS]->(s:KeycloakScope) MERGE (r)-[r0:INDIRECT_GRANTS]-(s) ON CREATE SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
16
+ "iterative": false
17
+ },
18
+ {
19
+ "__comment__": "Identify legitimate scopes for users based on assumed roles",
20
+ "query": "MATCH (u:KeycloakUser)-[:ASSUME_ROLE]-(:KeycloakRole)-[:GRANTS|INDIRECT_GRANTS]->(s:KeycloakScope) MERGE (u)-[r:ASSUME_SCOPE]->(s) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG",
21
+ "iterative": false
22
+ },
23
+ {
24
+ "__comment__": "Assign assumed scopes to users for orphan scopes (scopes not granted by any role)",
25
+ "query": "MATCH (s:KeycloakScope)<-[:RESOURCE]-(r:KeycloakRealm) MATCH (u:KeycloakUser)<-[:RESOURCE]-(r) WHERE NOT (s)<-[:GRANTS|INDIRECT_GRANTS]-(:KeycloakRole) MERGE (u)-[r0:ASSUME_SCOPE]->(s) SET r0.firstseen = $UPDATE_TAG SET r0.lastupdated = $UPDATE_TAG",
26
+ "iterative": false
27
+ }
28
+ ],
29
+ "name": "Keycloak inheritance analysis"
30
+ }
@@ -1,5 +1,7 @@
1
1
  import logging
2
2
  from dataclasses import asdict
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version
3
5
  from string import Template
4
6
  from typing import Dict
5
7
  from typing import List
@@ -223,6 +225,8 @@ def _build_attach_sub_resource_statement(
223
225
  $RelMergeClause
224
226
  ON CREATE SET r.firstseen = timestamp()
225
227
  SET
228
+ r._module_name = "$module_name",
229
+ r._module_version = "$module_version",
226
230
  $set_rel_properties_statement
227
231
  """,
228
232
  )
@@ -244,6 +248,8 @@ def _build_attach_sub_resource_statement(
244
248
  SubResourceLabel=sub_resource_link.target_node_label,
245
249
  MatchClause=_build_match_clause(sub_resource_link.target_node_matcher),
246
250
  RelMergeClause=rel_merge_clause,
251
+ module_name=_get_module_from_schema(sub_resource_link),
252
+ module_version=_get_cartography_version(),
247
253
  SubResourceRelLabel=sub_resource_link.rel_label,
248
254
  set_rel_properties_statement=_build_rel_properties_statement(
249
255
  "r",
@@ -278,6 +284,8 @@ def _build_attach_additional_links_statement(
278
284
  $RelMerge
279
285
  ON CREATE SET $rel_var.firstseen = timestamp()
280
286
  SET
287
+ $rel_var._module_name = "$module_name",
288
+ $rel_var._module_version = "$module_version",
281
289
  $set_rel_properties_statement
282
290
  """,
283
291
  )
@@ -312,6 +320,8 @@ def _build_attach_additional_links_statement(
312
320
  node_var=node_var,
313
321
  rel_var=rel_var,
314
322
  RelMerge=rel_merge,
323
+ module_name=_get_module_from_schema(link),
324
+ module_version=_get_cartography_version(),
315
325
  set_rel_properties_statement=_build_rel_properties_statement(
316
326
  rel_var,
317
327
  rel_props_as_dict,
@@ -453,6 +463,8 @@ def build_ingestion_query(
453
463
  MERGE (i:$node_label{id: $dict_id_field})
454
464
  ON CREATE SET i.firstseen = timestamp()
455
465
  SET
466
+ i._module_name = "$module_name",
467
+ i._module_version = "$module_version",
456
468
  $set_node_properties_statement
457
469
  $attach_relationships_statement
458
470
  """,
@@ -475,6 +487,8 @@ def build_ingestion_query(
475
487
  ingest_query = query_template.safe_substitute(
476
488
  node_label=node_schema.label,
477
489
  dict_id_field=node_props.id,
490
+ module_name=_get_module_from_schema(node_schema),
491
+ module_version=_get_cartography_version(),
478
492
  set_node_properties_statement=_build_node_properties_statement(
479
493
  node_props_as_dict,
480
494
  node_schema.extra_node_labels,
@@ -650,6 +664,8 @@ def build_matchlink_query(rel_schema: CartographyRelSchema) -> str:
650
664
  MERGE $rel
651
665
  ON CREATE SET r.firstseen = timestamp()
652
666
  SET
667
+ r._module_name = "$module_name",
668
+ r._module_version = "$module_version",
653
669
  $set_rel_properties_statement;
654
670
  """
655
671
  )
@@ -677,8 +693,62 @@ def build_matchlink_query(rel_schema: CartographyRelSchema) -> str:
677
693
  source_match=source_match,
678
694
  target_match=target_match,
679
695
  rel=rel,
696
+ module_name=_get_module_from_schema(rel_schema),
697
+ module_version=_get_cartography_version(),
680
698
  set_rel_properties_statement=_build_rel_properties_statement(
681
699
  "r",
682
700
  rel_props_as_dict,
683
701
  ),
684
702
  )
703
+
704
+
705
+ def _get_cartography_version() -> str:
706
+ """
707
+ Get the current version of the cartography package.
708
+
709
+ This function attempts to retrieve the version of the installed cartography package
710
+ using importlib.metadata. If the package is not found (typically in development
711
+ or testing environments), it returns 'dev' as a fallback.
712
+
713
+ Returns:
714
+ The version string of the cartography package, or 'dev' if not found
715
+ """
716
+ try:
717
+ return version("cartography")
718
+ except PackageNotFoundError:
719
+ # This can occured if the cartography package is not installed in the environment, typically in development or testing environments.
720
+ logger.warning("cartography package not found. Returning 'dev' version.")
721
+ # Fallback to reading the VERSION file if the package is not found
722
+ return "dev"
723
+
724
+
725
+ def _get_module_from_schema(
726
+ schema, #: "CartographyNodeSchema" | "CartographyRelSchema",
727
+ ) -> str:
728
+ """
729
+ Extract the module name from a Cartography schema object.
730
+
731
+ This function extracts and formats the module name from a CartographyNodeSchema
732
+ or CartographyRelSchema object. It expects schemas to be part of the official
733
+ cartography.models package hierarchy and returns a formatted string indicating
734
+ the specific cartography module.
735
+
736
+ Args:
737
+ schema: A CartographyNodeSchema or CartographyRelSchema object
738
+
739
+ Returns:
740
+ A formatted module name string in the format 'cartography:<module_name>'
741
+ or 'unknown:<full_module_path>' if the schema is not from cartography.models
742
+ """
743
+ # If the entity schema does not belong to the cartography.models package,
744
+ # we log a warning and return the full module path.
745
+ if not schema.__module__.startswith("cartography.models."):
746
+ logger.warning(
747
+ "The schema %s does not start with 'cartography.models.'. "
748
+ "This may indicate that the schema is not part of the official cartography models.",
749
+ schema.__module__,
750
+ )
751
+ return f"unknown:{schema.__module__}"
752
+ # Otherwise, we return the module path as a string.
753
+ parts = schema.__module__.split(".")
754
+ return f"cartography:{parts[2]}"
@@ -14,12 +14,18 @@ from policyuniverse.policy import Policy
14
14
 
15
15
  from cartography.client.core.tx import load
16
16
  from cartography.graph.job import GraphJob
17
- from cartography.models.aws.apigateway import APIGatewayRestAPISchema
18
- from cartography.models.aws.apigatewaycertificate import (
17
+ from cartography.intel.aws.ec2.util import get_botocore_config
18
+ from cartography.models.aws.apigateway.apigateway import APIGatewayRestAPISchema
19
+ from cartography.models.aws.apigateway.apigatewaycertificate import (
19
20
  APIGatewayClientCertificateSchema,
20
21
  )
21
- from cartography.models.aws.apigatewayresource import APIGatewayResourceSchema
22
- from cartography.models.aws.apigatewaystage import APIGatewayStageSchema
22
+ from cartography.models.aws.apigateway.apigatewaydeployment import (
23
+ APIGatewayDeploymentSchema,
24
+ )
25
+ from cartography.models.aws.apigateway.apigatewayresource import (
26
+ APIGatewayResourceSchema,
27
+ )
28
+ from cartography.models.aws.apigateway.apigatewaystage import APIGatewayStageSchema
23
29
  from cartography.util import aws_handle_regions
24
30
  from cartography.util import timeit
25
31
 
@@ -40,6 +46,38 @@ def get_apigateway_rest_apis(
40
46
  return apis
41
47
 
42
48
 
49
+ def get_rest_api_ids(
50
+ rest_apis: List[Dict],
51
+ ) -> List[str]:
52
+ """
53
+ Extracts the IDs of the REST APIs from the provided list.
54
+ """
55
+ return [api["id"] for api in rest_apis if "id" in api]
56
+
57
+
58
+ @timeit
59
+ @aws_handle_regions
60
+ def get_rest_api_deployments(
61
+ boto3_session: boto3.session.Session,
62
+ rest_api_ids: List[str],
63
+ region: str,
64
+ ) -> List[Dict[str, Any]]:
65
+ """
66
+ Retrieves the deployments for each REST API in the provided list.
67
+ """
68
+ client = boto3_session.client(
69
+ "apigateway", region_name=region, config=get_botocore_config()
70
+ )
71
+ deployments: List[Dict[str, Any]] = []
72
+ for api_id in rest_api_ids:
73
+ paginator = client.get_paginator("get_deployments")
74
+ for page in paginator.paginate(restApiId=api_id):
75
+ for deployment in page.get("items", []):
76
+ deployment["api_id"] = api_id
77
+ deployments.append(deployment)
78
+ return deployments
79
+
80
+
43
81
  @timeit
44
82
  @aws_handle_regions
45
83
  def get_rest_api_details(
@@ -63,6 +101,7 @@ def get_rest_api_details(
63
101
 
64
102
 
65
103
  @timeit
104
+ @aws_handle_regions
66
105
  def get_rest_api_stages(api: Dict, client: botocore.client.BaseClient) -> Any:
67
106
  """
68
107
  Gets the REST API Stage Resources.
@@ -104,6 +143,7 @@ def get_rest_api_client_certificate(
104
143
 
105
144
 
106
145
  @timeit
146
+ @aws_handle_regions
107
147
  def get_rest_api_resources(api: Dict, client: botocore.client.BaseClient) -> List[Any]:
108
148
  """
109
149
  Gets the collection of Resource resources.
@@ -244,6 +284,25 @@ def transform_rest_api_details(
244
284
  return stages, certificates, resources
245
285
 
246
286
 
287
+ def transform_apigateway_deployments(
288
+ deployments: List[Dict[str, Any]],
289
+ region: str,
290
+ ) -> List[Dict[str, Any]]:
291
+ """
292
+ Transform API Gateway Deployment data for ingestion
293
+ """
294
+ transformed_deployments = []
295
+ for deployment in deployments:
296
+ transformed_deployment = {
297
+ "id": f"{deployment['api_id']}/{deployment['id']}",
298
+ "api_id": deployment["api_id"],
299
+ "description": deployment.get("description"),
300
+ "region": region,
301
+ }
302
+ transformed_deployments.append(transformed_deployment)
303
+ return transformed_deployments
304
+
305
+
247
306
  @timeit
248
307
  def load_rest_api_details(
249
308
  neo4j_session: neo4j.Session,
@@ -283,6 +342,30 @@ def load_rest_api_details(
283
342
  )
284
343
 
285
344
 
345
+ @timeit
346
+ def load_apigateway_deployments(
347
+ neo4j_session: neo4j.Session,
348
+ data: List[Dict[str, Any]],
349
+ region: str,
350
+ current_aws_account_id: str,
351
+ aws_update_tag: int,
352
+ ) -> None:
353
+ """
354
+ Load API Gateway Deployment data into neo4j.
355
+ """
356
+ logger.info(
357
+ f"Loading API Gateway {len(data)} deployments for region '{region}' into graph.",
358
+ )
359
+ load(
360
+ neo4j_session,
361
+ APIGatewayDeploymentSchema(),
362
+ data,
363
+ region=region,
364
+ lastupdated=aws_update_tag,
365
+ AWS_ID=current_aws_account_id,
366
+ )
367
+
368
+
286
369
  @timeit
287
370
  def parse_policy(api_id: str, policy: Policy) -> Optional[Dict[Any, Any]]:
288
371
  """
@@ -345,6 +428,12 @@ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
345
428
  )
346
429
  cleanup_job.run(neo4j_session)
347
430
 
431
+ cleanup_job = GraphJob.from_node_schema(
432
+ APIGatewayDeploymentSchema(),
433
+ common_job_parameters,
434
+ )
435
+ cleanup_job.run(neo4j_session)
436
+
348
437
 
349
438
  @timeit
350
439
  def sync_apigateway_rest_apis(
@@ -375,6 +464,19 @@ def sync_apigateway_rest_apis(
375
464
  current_aws_account_id,
376
465
  aws_update_tag,
377
466
  )
467
+
468
+ api_ids = get_rest_api_ids(rest_apis)
469
+ deployments = get_rest_api_deployments(
470
+ boto3_session,
471
+ api_ids,
472
+ region,
473
+ )
474
+
475
+ transformed_deployments = transform_apigateway_deployments(
476
+ deployments,
477
+ region,
478
+ )
479
+
378
480
  load_apigateway_rest_apis(
379
481
  neo4j_session,
380
482
  transformed_apis,
@@ -388,6 +490,13 @@ def sync_apigateway_rest_apis(
388
490
  current_aws_account_id,
389
491
  aws_update_tag,
390
492
  )
493
+ load_apigateway_deployments(
494
+ neo4j_session,
495
+ transformed_deployments,
496
+ region,
497
+ current_aws_account_id,
498
+ aws_update_tag,
499
+ )
391
500
 
392
501
 
393
502
  @timeit