cartography 0.108.0rc2__py3-none-any.whl → 0.109.0rc2__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 (63) hide show
  1. cartography/_version.py +2 -2
  2. cartography/data/indexes.cypher +0 -17
  3. cartography/data/jobs/cleanup/gcp_compute_vpc_cleanup.json +0 -12
  4. cartography/intel/aws/cloudtrail_management_events.py +36 -3
  5. cartography/intel/aws/ecr.py +55 -80
  6. cartography/intel/aws/glue.py +117 -0
  7. cartography/intel/aws/identitycenter.py +71 -23
  8. cartography/intel/aws/kms.py +160 -200
  9. cartography/intel/aws/lambda_function.py +206 -190
  10. cartography/intel/aws/rds.py +243 -458
  11. cartography/intel/aws/resourcegroupstaggingapi.py +77 -18
  12. cartography/intel/aws/resources.py +2 -0
  13. cartography/intel/aws/route53.py +334 -332
  14. cartography/intel/aws/secretsmanager.py +62 -44
  15. cartography/intel/entra/groups.py +29 -1
  16. cartography/intel/gcp/__init__.py +10 -0
  17. cartography/intel/gcp/compute.py +19 -42
  18. cartography/models/aws/ecr/__init__.py +0 -0
  19. cartography/models/aws/ecr/image.py +41 -0
  20. cartography/models/aws/ecr/repository.py +72 -0
  21. cartography/models/aws/ecr/repository_image.py +95 -0
  22. cartography/models/aws/glue/__init__.py +0 -0
  23. cartography/models/aws/glue/connection.py +51 -0
  24. cartography/models/aws/identitycenter/awspermissionset.py +44 -0
  25. cartography/models/aws/kms/__init__.py +0 -0
  26. cartography/models/aws/kms/aliases.py +86 -0
  27. cartography/models/aws/kms/grants.py +65 -0
  28. cartography/models/aws/kms/keys.py +88 -0
  29. cartography/models/aws/lambda_function/__init__.py +0 -0
  30. cartography/models/aws/lambda_function/alias.py +74 -0
  31. cartography/models/aws/lambda_function/event_source_mapping.py +88 -0
  32. cartography/models/aws/lambda_function/lambda_function.py +89 -0
  33. cartography/models/aws/lambda_function/layer.py +72 -0
  34. cartography/models/aws/rds/__init__.py +0 -0
  35. cartography/models/aws/rds/cluster.py +89 -0
  36. cartography/models/aws/rds/instance.py +154 -0
  37. cartography/models/aws/rds/snapshot.py +108 -0
  38. cartography/models/aws/rds/subnet_group.py +101 -0
  39. cartography/models/aws/route53/__init__.py +0 -0
  40. cartography/models/aws/route53/dnsrecord.py +214 -0
  41. cartography/models/aws/route53/nameserver.py +63 -0
  42. cartography/models/aws/route53/subzone.py +40 -0
  43. cartography/models/aws/route53/zone.py +47 -0
  44. cartography/models/aws/secretsmanager/secret.py +106 -0
  45. cartography/models/entra/group.py +26 -0
  46. cartography/models/entra/user.py +6 -0
  47. cartography/models/gcp/compute/__init__.py +0 -0
  48. cartography/models/gcp/compute/vpc.py +50 -0
  49. cartography/util.py +8 -1
  50. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/METADATA +2 -2
  51. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/RECORD +55 -34
  52. cartography/data/jobs/cleanup/aws_dns_cleanup.json +0 -65
  53. cartography/data/jobs/cleanup/aws_import_identity_center_cleanup.json +0 -16
  54. cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json +0 -50
  55. cartography/data/jobs/cleanup/aws_import_rds_clusters_cleanup.json +0 -23
  56. cartography/data/jobs/cleanup/aws_import_rds_instances_cleanup.json +0 -47
  57. cartography/data/jobs/cleanup/aws_import_rds_snapshots_cleanup.json +0 -23
  58. cartography/data/jobs/cleanup/aws_import_secrets_cleanup.json +0 -8
  59. cartography/data/jobs/cleanup/aws_kms_details.json +0 -10
  60. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/WHEEL +0 -0
  61. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/entry_points.txt +0 -0
  62. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/licenses/LICENSE +0 -0
  63. {cartography-0.108.0rc2.dist-info → cartography-0.109.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,259 +1,78 @@
1
1
  import logging
2
- from typing import Dict
3
- from typing import List
4
- from typing import Optional
5
- from typing import Tuple
2
+ from collections import namedtuple
3
+ from typing import Any
6
4
 
7
5
  import boto3
8
6
  import botocore
9
7
  import neo4j
10
8
 
11
- from cartography.util import run_cleanup_job
9
+ from cartography.client.core.tx import load
10
+ from cartography.client.core.tx import load_matchlinks
11
+ from cartography.client.core.tx import read_list_of_dicts_tx
12
+ from cartography.graph.job import GraphJob
13
+ from cartography.models.aws.route53.dnsrecord import AWSDNSRecordSchema
14
+ from cartography.models.aws.route53.nameserver import NameServerSchema
15
+ from cartography.models.aws.route53.subzone import AWSDNSZoneSubzoneMatchLink
16
+ from cartography.models.aws.route53.zone import AWSDNSZoneSchema
17
+ from cartography.util import aws_handle_regions
12
18
  from cartography.util import timeit
13
19
 
14
20
  logger = logging.getLogger(__name__)
15
21
 
16
-
17
- @timeit
18
- def link_aws_resources(neo4j_session: neo4j.Session, update_tag: int) -> None:
19
- # find records that point to other records
20
- link_records = """
21
- MATCH (n:AWSDNSRecord) WITH n MATCH (v:AWSDNSRecord{value: n.name})
22
- WHERE NOT n = v
23
- MERGE (v)-[p:DNS_POINTS_TO]->(n)
24
- ON CREATE SET p.firstseen = timestamp()
25
- SET p.lastupdated = $update_tag
26
- """
27
- neo4j_session.run(link_records, update_tag=update_tag)
28
-
29
- # find records that point to AWS LoadBalancers
30
- link_elb = """
31
- MATCH (n:AWSDNSRecord) WITH n MATCH (l:LoadBalancer{dnsname: n.value})
32
- MERGE (n)-[p:DNS_POINTS_TO]->(l)
33
- ON CREATE SET p.firstseen = timestamp()
34
- SET p.lastupdated = $update_tag
35
- """
36
- neo4j_session.run(link_elb, update_tag=update_tag)
37
-
38
- # find records that point to AWS LoadBalancersV2
39
- link_elbv2 = """
40
- MATCH (n:AWSDNSRecord) WITH n MATCH (l:LoadBalancerV2{dnsname: n.value})
41
- MERGE (n)-[p:DNS_POINTS_TO]->(l)
42
- ON CREATE SET p.firstseen = timestamp()
43
- SET p.lastupdated = $update_tag
44
- """
45
- neo4j_session.run(link_elbv2, update_tag=update_tag)
46
-
47
- # find records that point to AWS EC2 Instances
48
- link_ec2 = """
49
- MATCH (n:AWSDNSRecord) WITH n MATCH (e:EC2Instance{publicdnsname: n.value})
50
- MERGE (n)-[p:DNS_POINTS_TO]->(e)
51
- ON CREATE SET p.firstseen = timestamp()
52
- SET p.lastupdated = $update_tag
53
- """
54
- neo4j_session.run(link_ec2, update_tag=update_tag)
55
-
56
-
57
- @timeit
58
- def load_a_records(
59
- neo4j_session: neo4j.Session,
60
- records: List[Dict],
61
- update_tag: int,
62
- ) -> None:
63
- ingest_records = """
64
- UNWIND $records as record
65
- MERGE (a:DNSRecord:AWSDNSRecord{id: record.id})
66
- ON CREATE SET
67
- a.firstseen = timestamp(),
68
- a.name = record.name,
69
- a.type = record.type
70
- SET
71
- a.lastupdated = $update_tag,
72
- a.value = record.value
73
- WITH a,record
74
- MATCH (zone:AWSDNSZone{zoneid: record.zoneid})
75
- MERGE (a)-[r:MEMBER_OF_DNS_ZONE]->(zone)
76
- ON CREATE SET r.firstseen = timestamp()
77
- SET r.lastupdated = $update_tag
78
- """
79
- neo4j_session.run(
80
- ingest_records,
81
- records=records,
82
- update_tag=update_tag,
83
- )
22
+ DnsData = namedtuple(
23
+ "DnsData",
24
+ [
25
+ "zones",
26
+ "a_records",
27
+ "alias_records",
28
+ "cname_records",
29
+ "ns_records",
30
+ "name_servers",
31
+ ],
32
+ )
84
33
 
85
34
 
86
- @timeit
87
- def load_alias_records(
88
- neo4j_session: neo4j.Session,
89
- records: List[Dict],
90
- update_tag: int,
91
- ) -> None:
92
- # create the DNSRecord nodes and link them to matching DNSZone and S3Bucket nodes
93
- ingest_records = """
94
- UNWIND $records as record
95
- MERGE (a:DNSRecord:AWSDNSRecord{id: record.id})
96
- ON CREATE SET
97
- a.firstseen = timestamp(),
98
- a.name = record.name,
99
- a.type = record.type
100
- SET
101
- a.lastupdated = $update_tag,
102
- a.value = record.value
103
- WITH a,record
104
- MATCH (zone:AWSDNSZone{zoneid: record.zoneid})
105
- MERGE (a)-[r:MEMBER_OF_DNS_ZONE]->(zone)
106
- ON CREATE SET r.firstseen = timestamp()
107
- SET r.lastupdated = $update_tag
108
- """
109
- neo4j_session.run(
110
- ingest_records,
111
- records=records,
112
- update_tag=update_tag,
113
- )
35
+ def _create_dns_record_id(zoneid: str, name: str, record_type: str) -> str:
36
+ return "/".join([zoneid, name, record_type])
114
37
 
115
38
 
116
- @timeit
117
- def load_cname_records(
118
- neo4j_session: neo4j.Session,
119
- records: List[Dict],
120
- update_tag: int,
121
- ) -> None:
122
- ingest_records = """
123
- UNWIND $records as record
124
- MERGE (a:DNSRecord:AWSDNSRecord{id: record.id})
125
- ON CREATE SET
126
- a.firstseen = timestamp(),
127
- a.name = record.name,
128
- a.type = record.type
129
- SET
130
- a.lastupdated = $update_tag,
131
- a.value = record.value
132
- WITH a,record
133
- MATCH (zone:AWSDNSZone{zoneid: record.zoneid})
134
- MERGE (a)-[r:MEMBER_OF_DNS_ZONE]->(zone)
135
- ON CREATE SET r.firstseen = timestamp()
136
- SET r.lastupdated = $update_tag
137
- """
138
- neo4j_session.run(
139
- ingest_records,
140
- records=records,
141
- update_tag=update_tag,
142
- )
39
+ def _normalize_dns_address(address: str) -> str:
40
+ return address.rstrip(".")
143
41
 
144
42
 
145
43
  @timeit
146
- def load_zone(
147
- neo4j_session: neo4j.Session,
148
- zone: Dict,
149
- current_aws_id: str,
150
- update_tag: int,
151
- ) -> None:
152
- ingest_z = """
153
- MERGE (zone:DNSZone:AWSDNSZone{zoneid:$ZoneId})
154
- ON CREATE SET
155
- zone.firstseen = timestamp(),
156
- zone.name = $ZoneName
157
- SET
158
- zone.lastupdated = $update_tag,
159
- zone.comment = $Comment,
160
- zone.privatezone = $PrivateZone
161
- WITH zone
162
- MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID})
163
- MERGE (aa)-[r:RESOURCE]->(zone)
164
- ON CREATE SET r.firstseen = timestamp()
165
- SET r.lastupdated = $update_tag
166
- """
167
- neo4j_session.run(
168
- ingest_z,
169
- ZoneName=zone["name"][:-1],
170
- ZoneId=zone["zoneid"],
171
- Comment=zone["comment"],
172
- PrivateZone=zone["privatezone"],
173
- AWS_ACCOUNT_ID=current_aws_id,
174
- update_tag=update_tag,
175
- )
44
+ def get_zone_record_sets(
45
+ client: botocore.client.BaseClient,
46
+ zone_id: str,
47
+ ) -> list[dict[str, Any]]:
48
+ resource_record_sets: list[dict[str, Any]] = []
49
+ paginator = client.get_paginator("list_resource_record_sets")
50
+ pages = paginator.paginate(HostedZoneId=zone_id)
51
+ for page in pages:
52
+ resource_record_sets.extend(page["ResourceRecordSets"])
53
+ return resource_record_sets
176
54
 
177
55
 
56
+ @aws_handle_regions
178
57
  @timeit
179
- def load_ns_records(
180
- neo4j_session: neo4j.Session,
181
- records: List[Dict],
182
- zone_name: str,
183
- update_tag: int,
184
- ) -> None:
185
- ingest_records = """
186
- UNWIND $records as record
187
- MERGE (a:DNSRecord:AWSDNSRecord{id: record.id})
188
- ON CREATE SET
189
- a.firstseen = timestamp(),
190
- a.name = record.name,
191
- a.type = record.type
192
- SET
193
- a.lastupdated = $update_tag,
194
- a.value = record.name
195
- WITH a,record
196
- MATCH (zone:AWSDNSZone{zoneid: record.zoneid})
197
- MERGE (a)-[r:MEMBER_OF_DNS_ZONE]->(zone)
198
- ON CREATE SET r.firstseen = timestamp()
199
- SET r.lastupdated = $update_tag
200
- WITH a,record
201
- UNWIND record.servers as server
202
- MERGE (ns:NameServer{id:server})
203
- ON CREATE SET ns.firstseen = timestamp()
204
- SET
205
- ns.lastupdated = $update_tag,
206
- ns.name = server
207
- MERGE (a)-[pt:DNS_POINTS_TO]->(ns)
208
- SET pt.lastupdated = $update_tag
209
- """
210
- neo4j_session.run(
211
- ingest_records,
212
- records=records,
213
- update_tag=update_tag,
214
- )
215
-
216
- # Map the official name servers for a domain.
217
- map_ns_records = """
218
- UNWIND $servers as server
219
- MATCH (ns:NameServer{id:server})
220
- MATCH (zone:AWSDNSZone{zoneid:$zoneid})
221
- MERGE (ns)<-[r:NAMESERVER]-(zone)
222
- SET r.lastupdated = $update_tag
223
- """
224
- for record in records:
225
- if zone_name == record["name"]:
226
- neo4j_session.run(
227
- map_ns_records,
228
- servers=record["servers"],
229
- zoneid=record["zoneid"],
230
- update_tag=update_tag,
231
- )
232
-
58
+ def get_zones(
59
+ client: botocore.client.BaseClient,
60
+ ) -> list[tuple[dict[str, Any], list[dict[str, Any]]]]:
61
+ paginator = client.get_paginator("list_hosted_zones")
62
+ hosted_zones: list[dict[str, Any]] = []
63
+ for page in paginator.paginate():
64
+ hosted_zones.extend(page["HostedZones"])
233
65
 
234
- @timeit
235
- def link_sub_zones(neo4j_session: neo4j.Session, update_tag: int) -> None:
236
- query = """
237
- match (z:AWSDNSZone)
238
- <-[:MEMBER_OF_DNS_ZONE]-
239
- (record:DNSRecord{type:"NS"})
240
- -[:DNS_POINTS_TO]->
241
- (ns:NameServer)
242
- <-[:NAMESERVER]-
243
- (z2)
244
- WHERE record.name=z2.name AND NOT z=z2
245
- MERGE (z2)<-[r:SUBZONE]-(z)
246
- ON CREATE SET r.firstseen = timestamp()
247
- SET r.lastupdated = $update_tag
248
- """
249
- neo4j_session.run(
250
- query,
251
- update_tag=update_tag,
252
- )
66
+ results: list[tuple[dict[str, Any], list[dict[str, Any]]]] = []
67
+ for hosted_zone in hosted_zones:
68
+ record_sets = get_zone_record_sets(client, hosted_zone["Id"])
69
+ results.append((hosted_zone, record_sets))
70
+ return results
253
71
 
254
72
 
255
- @timeit
256
- def transform_record_set(record_set: Dict, zone_id: str, name: str) -> Optional[Dict]:
73
+ def transform_record_set(
74
+ record_set: dict[str, Any], zone_id: str, name: str
75
+ ) -> dict[str, Any] | None:
257
76
  # process CNAME, ALIAS and A records
258
77
  if record_set["Type"] == "CNAME":
259
78
  if "AliasTarget" in record_set:
@@ -295,27 +114,28 @@ def transform_record_set(record_set: Dict, zone_id: str, name: str) -> Optional[
295
114
  else:
296
115
  # this is a real A record
297
116
  # loop and add each value (IP address) to a comma separated string
298
- # don't forget to trim that trailing comma!
299
- # TODO can this be replaced with a string join?
300
- value = ""
301
- for a_value in record_set["ResourceRecords"]:
302
- value = value + a_value["Value"] + ","
117
+ # TODO if there are many IPs, this string will be long. we should change this.
118
+ ip_addresses = [record["Value"] for record in record_set["ResourceRecords"]]
119
+ value = ",".join(ip_addresses)
303
120
 
304
121
  return {
305
122
  "name": name,
306
123
  "type": "A",
307
124
  "zoneid": zone_id,
308
- "value": value[:-1],
125
+ # Include the IPs for relationships
126
+ "ip_addresses": ip_addresses,
127
+ "value": value,
309
128
  "id": _create_dns_record_id(zone_id, name, "A"),
310
129
  }
311
-
312
- else:
313
- return None
130
+ # This should never happen since we only call this for A and CNAME records,
131
+ # but we'll log it and return None.
132
+ logger.warning(f"Unsupported record type: {record_set['Type']}")
133
+ return None
314
134
 
315
135
 
316
- @timeit
317
- def transform_ns_record_set(record_set: Dict, zone_id: str) -> Optional[Dict]:
318
-
136
+ def transform_ns_record_set(
137
+ record_set: dict[str, Any], zone_id: str
138
+ ) -> dict[str, Any] | None:
319
139
  if "ResourceRecords" in record_set:
320
140
  # Sometimes the value records have a trailing period, sometimes they dont.
321
141
  servers = [
@@ -331,118 +151,267 @@ def transform_ns_record_set(record_set: Dict, zone_id: str) -> Optional[Dict]:
331
151
  "id": _create_dns_record_id(zone_id, record_set["Name"][:-1], "NS"),
332
152
  }
333
153
  else:
154
+ # This should never happen since we only call this for NS records
155
+ # but we'll log it and return None.
156
+ logger.warning(f"NS record set missing ResourceRecords: {record_set}")
334
157
  return None
335
158
 
336
159
 
337
- @timeit
338
- def transform_zone(zone: Dict) -> Dict:
339
- # TODO simplify this
340
- if "Comment" in zone["Config"]:
341
- comment = zone["Config"]["Comment"]
342
- else:
343
- comment = ""
160
+ def transform_zone(zone: dict[str, Any]) -> dict[str, Any]:
161
+ comment = zone["Config"].get("Comment")
162
+
163
+ # Remove trailing dot from name for schema compatibility
164
+ zone_name = zone["Name"]
165
+ if zone_name.endswith("."):
166
+ zone_name = zone_name[:-1]
344
167
 
345
168
  return {
346
169
  "zoneid": zone["Id"],
347
- "name": zone["Name"],
170
+ "name": zone_name,
348
171
  "privatezone": zone["Config"]["PrivateZone"],
349
172
  "comment": comment,
350
173
  "count": zone["ResourceRecordSetCount"],
351
174
  }
352
175
 
353
176
 
177
+ def transform_all_dns_data(
178
+ zones: list[tuple[dict[str, Any], list[dict[str, Any]]]],
179
+ ) -> DnsData:
180
+ """
181
+ Transform all DNS data into flat lists for loading.
182
+ Returns: (zones, a_records, alias_records, cname_records, ns_records)
183
+ """
184
+ transformed_zones = []
185
+ all_a_records = []
186
+ all_alias_records = []
187
+ all_cname_records = []
188
+ all_ns_records = []
189
+ all_name_servers = []
190
+
191
+ for zone, zone_record_sets in zones:
192
+ parsed_zone = transform_zone(zone)
193
+ transformed_zones.append(parsed_zone)
194
+
195
+ zone_id = zone["Id"]
196
+ zone_name = parsed_zone["name"]
197
+
198
+ for rs in zone_record_sets:
199
+ if rs["Type"] == "A" or rs["Type"] == "CNAME":
200
+ transformed_rs = transform_record_set(
201
+ rs,
202
+ zone_id,
203
+ rs["Name"][:-1],
204
+ )
205
+ if transformed_rs is None:
206
+ continue
207
+
208
+ if transformed_rs["type"] == "A":
209
+ all_a_records.append(transformed_rs)
210
+ # TODO consider creating IPs as a first-class node from here.
211
+ # Right now we just match on them from the A record.
212
+ elif transformed_rs["type"] == "ALIAS":
213
+ all_alias_records.append(transformed_rs)
214
+ elif transformed_rs["type"] == "CNAME":
215
+ all_cname_records.append(transformed_rs)
216
+
217
+ elif rs["Type"] == "NS":
218
+ transformed_rs = transform_ns_record_set(rs, zone_id)
219
+ if transformed_rs is None:
220
+ continue
221
+
222
+ # Add zone name to NS records for loading
223
+ transformed_rs["zone_name"] = zone_name
224
+ all_ns_records.append(transformed_rs)
225
+ all_name_servers.extend(
226
+ [
227
+ {"id": server, "zoneid": zone_id}
228
+ for server in transformed_rs["servers"]
229
+ ]
230
+ )
231
+
232
+ return DnsData(
233
+ zones=transformed_zones,
234
+ a_records=all_a_records,
235
+ alias_records=all_alias_records,
236
+ cname_records=all_cname_records,
237
+ ns_records=all_ns_records,
238
+ name_servers=all_name_servers,
239
+ )
240
+
241
+
242
+ @timeit
243
+ def _load_dns_details_flat(
244
+ neo4j_session: neo4j.Session,
245
+ zones: list[dict[str, Any]],
246
+ a_records: list[dict[str, Any]],
247
+ alias_records: list[dict[str, Any]],
248
+ cname_records: list[dict[str, Any]],
249
+ ns_records: list[dict[str, Any]],
250
+ name_servers: list[dict[str, Any]],
251
+ current_aws_id: str,
252
+ update_tag: int,
253
+ ) -> None:
254
+ load_zones(neo4j_session, zones, current_aws_id, update_tag)
255
+ load_a_records(neo4j_session, a_records, update_tag, current_aws_id)
256
+ load_alias_records(neo4j_session, alias_records, update_tag, current_aws_id)
257
+ load_cname_records(neo4j_session, cname_records, update_tag, current_aws_id)
258
+ load_name_servers(neo4j_session, name_servers, update_tag, current_aws_id)
259
+ load_ns_records(neo4j_session, ns_records, update_tag, current_aws_id)
260
+
261
+
354
262
  @timeit
355
263
  def load_dns_details(
356
264
  neo4j_session: neo4j.Session,
357
- dns_details: List[Tuple[Dict, List[Dict]]],
265
+ dns_details: list[tuple[dict[str, Any], list[dict[str, Any]]]],
358
266
  current_aws_id: str,
359
267
  update_tag: int,
360
268
  ) -> None:
361
269
  """
362
- Create the paths
363
- (:AWSAccount)--(:AWSDNSZone)--(:AWSDNSRecord),
364
- (:AWSDNSZone)--(:NameServer),
365
- (:AWSDNSRecord{type:"NS"})-[:DNS_POINTS_TO]->(:NameServer),
366
- (:AWSDNSRecord)-[:DNS_POINTS_TO]->(:AWSDNSRecord).
270
+ Backward-compatible wrapper
367
271
  """
368
- for zone, zone_record_sets in dns_details:
369
- zone_a_records = []
370
- zone_alias_records = []
371
- zone_cname_records = []
372
- zone_ns_records = []
373
- parsed_zone = transform_zone(zone)
272
+ transformed_data = transform_all_dns_data(dns_details)
273
+ _load_dns_details_flat(
274
+ neo4j_session,
275
+ transformed_data.zones,
276
+ transformed_data.a_records,
277
+ transformed_data.alias_records,
278
+ transformed_data.cname_records,
279
+ transformed_data.ns_records,
280
+ transformed_data.name_servers,
281
+ current_aws_id,
282
+ update_tag,
283
+ )
374
284
 
375
- load_zone(neo4j_session, parsed_zone, current_aws_id, update_tag)
376
285
 
377
- for record_set in zone_record_sets:
378
- if record_set["Type"] == "A" or record_set["Type"] == "CNAME":
379
- record = transform_record_set(
380
- record_set,
381
- zone["Id"],
382
- record_set["Name"][:-1],
383
- )
286
+ @timeit
287
+ def load_a_records(
288
+ neo4j_session: neo4j.Session,
289
+ records: list[dict[str, Any]],
290
+ update_tag: int,
291
+ current_aws_id: str,
292
+ ) -> None:
293
+ load(
294
+ neo4j_session,
295
+ AWSDNSRecordSchema(),
296
+ records,
297
+ lastupdated=update_tag,
298
+ AWS_ID=current_aws_id,
299
+ )
384
300
 
385
- if record["type"] == "A":
386
- zone_a_records.append(record)
387
- elif record["type"] == "ALIAS":
388
- zone_alias_records.append(record)
389
- elif record["type"] == "CNAME":
390
- zone_cname_records.append(record)
391
-
392
- if record_set["Type"] == "NS":
393
- record = transform_ns_record_set(record_set, zone["Id"])
394
- zone_ns_records.append(record)
395
- if zone_a_records:
396
- load_a_records(neo4j_session, zone_a_records, update_tag)
397
-
398
- if zone_alias_records:
399
- load_alias_records(neo4j_session, zone_alias_records, update_tag)
400
-
401
- if zone_cname_records:
402
- load_cname_records(neo4j_session, zone_cname_records, update_tag)
403
- if zone_ns_records:
404
- load_ns_records(
405
- neo4j_session,
406
- zone_ns_records,
407
- parsed_zone["name"][:-1],
408
- update_tag,
409
- )
410
- link_aws_resources(neo4j_session, update_tag)
301
+
302
+ @timeit
303
+ def load_alias_records(
304
+ neo4j_session: neo4j.Session,
305
+ records: list[dict[str, Any]],
306
+ update_tag: int,
307
+ current_aws_id: str,
308
+ ) -> None:
309
+ load(
310
+ neo4j_session,
311
+ AWSDNSRecordSchema(),
312
+ records,
313
+ lastupdated=update_tag,
314
+ AWS_ID=current_aws_id,
315
+ )
411
316
 
412
317
 
413
318
  @timeit
414
- def get_zone_record_sets(
415
- client: botocore.client.BaseClient,
416
- zone_id: str,
417
- ) -> List[Dict]:
418
- resource_record_sets: List[Dict] = []
419
- paginator = client.get_paginator("list_resource_record_sets")
420
- pages = paginator.paginate(HostedZoneId=zone_id)
421
- for page in pages:
422
- resource_record_sets.extend(page["ResourceRecordSets"])
423
- return resource_record_sets
319
+ def load_cname_records(
320
+ neo4j_session: neo4j.Session,
321
+ records: list[dict[str, Any]],
322
+ update_tag: int,
323
+ current_aws_id: str,
324
+ ) -> None:
325
+ load(
326
+ neo4j_session,
327
+ AWSDNSRecordSchema(),
328
+ records,
329
+ lastupdated=update_tag,
330
+ AWS_ID=current_aws_id,
331
+ )
424
332
 
425
333
 
426
334
  @timeit
427
- def get_zones(client: botocore.client.BaseClient) -> List[Tuple[Dict, List[Dict]]]:
428
- paginator = client.get_paginator("list_hosted_zones")
429
- hosted_zones: List[Dict] = []
430
- for page in paginator.paginate():
431
- hosted_zones.extend(page["HostedZones"])
335
+ def load_zones(
336
+ neo4j_session: neo4j.Session,
337
+ zones: list[dict[str, Any]],
338
+ current_aws_id: str,
339
+ update_tag: int,
340
+ ) -> None:
341
+ load(
342
+ neo4j_session,
343
+ AWSDNSZoneSchema(),
344
+ zones,
345
+ lastupdated=update_tag,
346
+ AWS_ID=current_aws_id,
347
+ )
432
348
 
433
- results: List[Tuple[Dict, List[Dict]]] = []
434
- for hosted_zone in hosted_zones:
435
- record_sets = get_zone_record_sets(client, hosted_zone["Id"])
436
- results.append((hosted_zone, record_sets))
437
- return results
438
349
 
350
+ @timeit
351
+ def load_ns_records(
352
+ neo4j_session: neo4j.Session,
353
+ records: list[dict[str, Any]],
354
+ update_tag: int,
355
+ current_aws_id: str,
356
+ ) -> None:
357
+ load(
358
+ neo4j_session,
359
+ AWSDNSRecordSchema(),
360
+ records,
361
+ lastupdated=update_tag,
362
+ AWS_ID=current_aws_id,
363
+ )
439
364
 
440
- def _create_dns_record_id(zoneid: str, name: str, record_type: str) -> str:
441
- return "/".join([zoneid, name, record_type])
442
365
 
366
+ @timeit
367
+ def load_name_servers(
368
+ neo4j_session: neo4j.Session,
369
+ name_servers: list[dict[str, Any]],
370
+ update_tag: int,
371
+ current_aws_id: str,
372
+ ) -> None:
373
+ load(
374
+ neo4j_session,
375
+ NameServerSchema(),
376
+ name_servers,
377
+ lastupdated=update_tag,
378
+ AWS_ID=current_aws_id,
379
+ )
443
380
 
444
- def _normalize_dns_address(address: str) -> str:
445
- return address.rstrip(".")
381
+
382
+ @timeit
383
+ def link_sub_zones(
384
+ neo4j_session: neo4j.Session, update_tag: int, current_aws_id: str
385
+ ) -> None:
386
+ """
387
+ Create SUBZONE relationships between DNS zones using matchlinks.
388
+
389
+ A DNS zone B is a sub zone of A if:
390
+ 1. DNS zone A has an NS record that points to a nameserver
391
+ 2. That nameserver is associated with DNS zone B
392
+ 3. The NS record's name matches the name of DNS zone B
393
+
394
+ We use matchlinks instead of a regular relationship because the hierarchy
395
+ isn't known ahead of time.
396
+ """
397
+ query = """
398
+ MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(z:AWSDNSZone)
399
+ <-[:MEMBER_OF_DNS_ZONE]-(record:DNSRecord{type:"NS"})
400
+ -[:DNS_POINTS_TO]->(ns:NameServer)<-[:NAMESERVER]-(z2:AWSDNSZone)
401
+ WHERE record.name=z2.name AND NOT z=z2
402
+ RETURN z.id as zone_id, z2.id as subzone_id
403
+ """
404
+ zone_to_subzone = neo4j_session.read_transaction(
405
+ read_list_of_dicts_tx, query, AWS_ID=current_aws_id
406
+ )
407
+ load_matchlinks(
408
+ neo4j_session,
409
+ AWSDNSZoneSubzoneMatchLink(),
410
+ zone_to_subzone,
411
+ lastupdated=update_tag,
412
+ _sub_resource_label="AWSAccount",
413
+ _sub_resource_id=current_aws_id,
414
+ )
446
415
 
447
416
 
448
417
  @timeit
@@ -451,25 +420,58 @@ def cleanup_route53(
451
420
  current_aws_id: str,
452
421
  update_tag: int,
453
422
  ) -> None:
454
- run_cleanup_job(
455
- "aws_dns_cleanup.json",
456
- neo4j_session,
457
- {"UPDATE_TAG": update_tag, "AWS_ID": current_aws_id},
458
- )
423
+ common_job_parameters = {
424
+ "UPDATE_TAG": update_tag,
425
+ "AWS_ID": current_aws_id,
426
+ }
427
+ GraphJob.from_node_schema(
428
+ AWSDNSRecordSchema(),
429
+ common_job_parameters,
430
+ ).run(neo4j_session)
431
+
432
+ GraphJob.from_node_schema(
433
+ NameServerSchema(),
434
+ common_job_parameters,
435
+ ).run(neo4j_session)
436
+
437
+ GraphJob.from_node_schema(
438
+ AWSDNSZoneSchema(),
439
+ common_job_parameters,
440
+ ).run(neo4j_session)
441
+
442
+ GraphJob.from_matchlink(
443
+ AWSDNSZoneSubzoneMatchLink(),
444
+ "AWSAccount",
445
+ current_aws_id,
446
+ update_tag,
447
+ ).run(neo4j_session)
459
448
 
460
449
 
461
450
  @timeit
462
451
  def sync(
463
452
  neo4j_session: neo4j.Session,
464
453
  boto3_session: boto3.session.Session,
465
- regions: List[str],
454
+ regions: list[str],
466
455
  current_aws_account_id: str,
467
456
  update_tag: int,
468
- common_job_parameters: Dict,
457
+ common_job_parameters: dict[str, Any],
469
458
  ) -> None:
470
459
  logger.info("Syncing Route53 for account '%s'.", current_aws_account_id)
471
460
  client = boto3_session.client("route53")
472
461
  zones = get_zones(client)
473
- load_dns_details(neo4j_session, zones, current_aws_account_id, update_tag)
474
- link_sub_zones(neo4j_session, update_tag)
462
+
463
+ transformed_data = transform_all_dns_data(zones)
464
+
465
+ _load_dns_details_flat(
466
+ neo4j_session,
467
+ transformed_data.zones,
468
+ transformed_data.a_records,
469
+ transformed_data.alias_records,
470
+ transformed_data.cname_records,
471
+ transformed_data.ns_records,
472
+ transformed_data.name_servers,
473
+ current_aws_account_id,
474
+ update_tag,
475
+ )
476
+ link_sub_zones(neo4j_session, update_tag, current_aws_account_id)
475
477
  cleanup_route53(neo4j_session, current_aws_account_id, update_tag)