cartography 0.106.0rc1__py3-none-any.whl → 0.106.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.

cartography/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.106.0rc1'
21
- __version_tuple__ = version_tuple = (0, 106, 0, 'rc1')
20
+ __version__ = version = '0.106.0rc2'
21
+ __version_tuple__ = version_tuple = (0, 106, 0, 'rc2')
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from typing import Any
2
3
  from typing import Dict
3
4
  from typing import List
@@ -8,10 +9,15 @@ from typing import Union
8
9
  import neo4j
9
10
 
10
11
  from cartography.graph.querybuilder import build_create_index_queries
12
+ from cartography.graph.querybuilder import build_create_index_queries_for_matchlink
11
13
  from cartography.graph.querybuilder import build_ingestion_query
14
+ from cartography.graph.querybuilder import build_matchlink_query
12
15
  from cartography.models.core.nodes import CartographyNodeSchema
16
+ from cartography.models.core.relationships import CartographyRelSchema
13
17
  from cartography.util import batch
14
18
 
19
+ logger = logging.getLogger(__name__)
20
+
15
21
 
16
22
  def read_list_of_values_tx(
17
23
  tx: neo4j.Transaction,
@@ -255,6 +261,25 @@ def ensure_indexes(
255
261
  neo4j_session.run(query)
256
262
 
257
263
 
264
+ def ensure_indexes_for_matchlinks(
265
+ neo4j_session: neo4j.Session,
266
+ rel_schema: CartographyRelSchema,
267
+ ) -> None:
268
+ """
269
+ Creates indexes for node fields if they don't exist for the given CartographyRelSchema object.
270
+ This is only used for load_rels() where we match on and connect existing nodes.
271
+ This is not used for CartographyNodeSchema objects.
272
+ """
273
+ queries = build_create_index_queries_for_matchlink(rel_schema)
274
+ logger.debug(f"CREATE INDEX queries for {rel_schema.rel_label}: {queries}")
275
+ for query in queries:
276
+ if not query.startswith("CREATE INDEX IF NOT EXISTS"):
277
+ raise ValueError(
278
+ 'Query provided to `ensure_indexes_for_matchlinks()` does not start with "CREATE INDEX IF NOT EXISTS".',
279
+ )
280
+ neo4j_session.run(query)
281
+
282
+
258
283
  def load(
259
284
  neo4j_session: neo4j.Session,
260
285
  node_schema: CartographyNodeSchema,
@@ -276,3 +301,40 @@ def load(
276
301
  ensure_indexes(neo4j_session, node_schema)
277
302
  ingestion_query = build_ingestion_query(node_schema)
278
303
  load_graph_data(neo4j_session, ingestion_query, dict_list, **kwargs)
304
+
305
+
306
+ def load_matchlinks(
307
+ neo4j_session: neo4j.Session,
308
+ rel_schema: CartographyRelSchema,
309
+ dict_list: list[dict[str, Any]],
310
+ **kwargs,
311
+ ) -> None:
312
+ """
313
+ Main entrypoint for intel modules to write relationships to the graph between two existing nodes.
314
+ :param neo4j_session: The Neo4j session
315
+ :param rel_schema: The CartographyRelSchema object to generate a query.
316
+ :param dict_list: The data to load to the graph represented as a list of dicts. The dicts must contain the source and
317
+ target node ids.
318
+ :param kwargs: Allows additional keyword args to be supplied to the Neo4j query.
319
+ :return: None
320
+ """
321
+ if len(dict_list) == 0:
322
+ # If there is no data to load, save some time.
323
+ return
324
+
325
+ # Validate that required kwargs are provided for cleanup queries
326
+ if "_sub_resource_label" not in kwargs:
327
+ raise ValueError(
328
+ f"Required kwarg '_sub_resource_label' not provided for {rel_schema.rel_label}. "
329
+ "This is needed for cleanup queries."
330
+ )
331
+ if "_sub_resource_id" not in kwargs:
332
+ raise ValueError(
333
+ f"Required kwarg '_sub_resource_id' not provided for {rel_schema.rel_label}. "
334
+ "This is needed for cleanup queries."
335
+ )
336
+
337
+ ensure_indexes_for_matchlinks(neo4j_session, rel_schema)
338
+ matchlink_query = build_matchlink_query(rel_schema)
339
+ logger.debug(f"Matchlink query: {matchlink_query}")
340
+ load_graph_data(neo4j_session, matchlink_query, dict_list, **kwargs)
@@ -3,6 +3,7 @@ from string import Template
3
3
  from typing import Dict
4
4
  from typing import List
5
5
 
6
+ from cartography.graph.querybuilder import _asdict_with_validate_relprops
6
7
  from cartography.graph.querybuilder import _build_match_clause
7
8
  from cartography.graph.querybuilder import rel_present_on_node_schema
8
9
  from cartography.models.core.common import PropertyRef
@@ -334,3 +335,49 @@ def _validate_target_node_matcher_for_cleanup_job(tgm: TargetNodeMatcher):
334
335
  f"{key} has set_in_kwargs=False, please check by reviewing the full stack trace to know which object"
335
336
  f"this message was raised from. Debug information: PropertyRef name = {prop_ref.name}.",
336
337
  )
338
+
339
+
340
+ def build_cleanup_query_for_matchlink(rel_schema: CartographyRelSchema) -> str:
341
+ """
342
+ Generates a cleanup query for a matchlink relationship.
343
+ :param rel_schema: The CartographyRelSchema object to generate a query. This CartographyRelSchema object
344
+ - Must have a source_node_matcher and source_node_label defined
345
+ - Must have a CartographyRelProperties object where _sub_resource_label and _sub_resource_id are defined
346
+ :return: A Neo4j query used to clean up stale matchlink relationships.
347
+ """
348
+ if not rel_schema.source_node_matcher:
349
+ raise ValueError(
350
+ f"No source node matcher found for {rel_schema.rel_label}; returning empty list."
351
+ )
352
+
353
+ query_template = Template(
354
+ """
355
+ MATCH (from:$source_node_label)$rel_direction[r:$rel_label]$rel_direction_end(to:$target_node_label)
356
+ WHERE r.lastupdated <> $UPDATE_TAG
357
+ AND r._sub_resource_label = $sub_resource_label
358
+ AND r._sub_resource_id = $sub_resource_id
359
+ WITH r LIMIT $LIMIT_SIZE
360
+ DELETE r;
361
+ """
362
+ )
363
+
364
+ # Determine which way to point the arrow. INWARD is toward the source, otherwise we go toward the target.
365
+ if rel_schema.direction == LinkDirection.INWARD:
366
+ rel_direction = "<-"
367
+ rel_direction_end = "-"
368
+ else:
369
+ rel_direction = "-"
370
+ rel_direction_end = "->"
371
+
372
+ # Small hack: avoid type-checking errors by converting the rel_schema to a dict.
373
+ rel_props_as_dict = _asdict_with_validate_relprops(rel_schema)
374
+
375
+ return query_template.safe_substitute(
376
+ source_node_label=rel_schema.source_node_label,
377
+ target_node_label=rel_schema.target_node_label,
378
+ rel_label=rel_schema.rel_label,
379
+ rel_direction=rel_direction,
380
+ rel_direction_end=rel_direction_end,
381
+ sub_resource_label=rel_props_as_dict["_sub_resource_label"],
382
+ sub_resource_id=rel_props_as_dict["_sub_resource_id"],
383
+ )
cartography/graph/job.py CHANGED
@@ -13,9 +13,11 @@ from typing import Union
13
13
  import neo4j
14
14
 
15
15
  from cartography.graph.cleanupbuilder import build_cleanup_queries
16
+ from cartography.graph.cleanupbuilder import build_cleanup_query_for_matchlink
16
17
  from cartography.graph.statement import get_job_shortname
17
18
  from cartography.graph.statement import GraphStatement
18
19
  from cartography.models.core.nodes import CartographyNodeSchema
20
+ from cartography.models.core.relationships import CartographyRelSchema
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
@@ -176,6 +178,46 @@ class GraphJob:
176
178
  node_schema.label,
177
179
  )
178
180
 
181
+ @classmethod
182
+ def from_matchlink(
183
+ cls,
184
+ rel_schema: CartographyRelSchema,
185
+ sub_resource_label: str,
186
+ sub_resource_id: str,
187
+ update_tag: int,
188
+ ) -> "GraphJob":
189
+ """
190
+ Create a cleanup job from a CartographyRelSchema object (specifically, a MatchLink).
191
+ This is used for cleaning up stale links between nodes created by load_rels(). Do not use for other purposes.
192
+
193
+ Other notes:
194
+ - For a given rel_schema, the fields used in the rel_schema.properties._sub_resource_label.name and
195
+ rel_schema.properties._sub_resource_id.name must be provided as keys and values in the params dict.
196
+ - The rel_schema must have a source_node_matcher and target_node_matcher.
197
+ """
198
+ cleanup_link_query = build_cleanup_query_for_matchlink(rel_schema)
199
+ logger.debug(f"Cleanup query: {cleanup_link_query}")
200
+
201
+ parameters = {
202
+ "UPDATE_TAG": update_tag,
203
+ "_sub_resource_label": sub_resource_label,
204
+ "_sub_resource_id": sub_resource_id,
205
+ }
206
+
207
+ statement = GraphStatement(
208
+ cleanup_link_query,
209
+ parameters=parameters,
210
+ iterative=True,
211
+ iterationsize=100,
212
+ parent_job_name=rel_schema.rel_label,
213
+ )
214
+
215
+ return cls(
216
+ f"Cleanup {rel_schema.rel_label} between {rel_schema.source_node_label} and {rel_schema.target_node_label}",
217
+ [statement],
218
+ rel_schema.rel_label,
219
+ )
220
+
179
221
  @classmethod
180
222
  def from_json_file(cls, file_path: Union[str, Path]) -> "GraphJob":
181
223
  """
@@ -14,6 +14,7 @@ from cartography.models.core.nodes import ExtraNodeLabels
14
14
  from cartography.models.core.relationships import CartographyRelSchema
15
15
  from cartography.models.core.relationships import LinkDirection
16
16
  from cartography.models.core.relationships import OtherRelationships
17
+ from cartography.models.core.relationships import SourceNodeMatcher
17
18
  from cartography.models.core.relationships import TargetNodeMatcher
18
19
 
19
20
  logger = logging.getLogger(__name__)
@@ -109,10 +110,10 @@ def _build_rel_properties_statement(
109
110
  return set_clause
110
111
 
111
112
 
112
- def _build_match_clause(matcher: TargetNodeMatcher) -> str:
113
+ def _build_match_clause(matcher: TargetNodeMatcher | SourceNodeMatcher) -> str:
113
114
  """
114
115
  Generate a Neo4j match statement on one or more keys and values for a given node.
115
- :param matcher: A TargetNodeMatcher object
116
+ :param matcher: A TargetNodeMatcher or SourceNodeMatcher object
116
117
  :return: a Neo4j match clause
117
118
  """
118
119
  match = Template("$Key: $PropRef")
@@ -548,3 +549,136 @@ def build_create_index_queries(node_schema: CartographyNodeSchema) -> List[str]:
548
549
  ],
549
550
  )
550
551
  return result
552
+
553
+
554
+ def build_create_index_queries_for_matchlink(
555
+ rel_schema: CartographyRelSchema,
556
+ ) -> list[str]:
557
+ """
558
+ Generate queries to create indexes for the given CartographyRelSchema and all node types attached to it via its
559
+ relationships.
560
+ :param rel_schema: The CartographyRelSchema object
561
+ :return: A list of queries of the form `CREATE INDEX IF NOT EXISTS FOR (n:$TargetNodeLabel) ON (n.$TargetAttribute)`
562
+ """
563
+ if not rel_schema.source_node_matcher:
564
+ logger.warning(
565
+ f"No source node matcher found for {rel_schema.rel_label}; returning empty list."
566
+ "Please note that build_create_index_queries_for_matchlink() is only used for load_matchlinks() where we match on "
567
+ "and connect existing nodes in the graph."
568
+ )
569
+ return []
570
+
571
+ index_template = Template(
572
+ "CREATE INDEX IF NOT EXISTS FOR (n:$NodeLabel) ON (n.$NodeAttribute);",
573
+ )
574
+
575
+ result = []
576
+ for source_key in asdict(rel_schema.source_node_matcher).keys():
577
+ result.append(
578
+ index_template.safe_substitute(
579
+ NodeLabel=rel_schema.source_node_label,
580
+ NodeAttribute=source_key,
581
+ ),
582
+ )
583
+ for target_key in asdict(rel_schema.target_node_matcher).keys():
584
+ result.append(
585
+ index_template.safe_substitute(
586
+ NodeLabel=rel_schema.target_node_label,
587
+ NodeAttribute=target_key,
588
+ ),
589
+ )
590
+
591
+ # Create a composite index for the relationship between the source and target nodes.
592
+ # https://neo4j.com/docs/cypher-manual/4.3/indexes-for-search-performance/#administration-indexes-create-a-composite-index-for-relationships
593
+ rel_index_template = Template(
594
+ "CREATE INDEX IF NOT EXISTS FOR ()$rel_direction[r:$RelLabel]$rel_direction_end() "
595
+ "ON (r.lastupdated, r._sub_resource_label, r._sub_resource_id);",
596
+ )
597
+ if rel_schema.direction == LinkDirection.INWARD:
598
+ result.append(
599
+ rel_index_template.safe_substitute(
600
+ RelLabel=rel_schema.rel_label,
601
+ rel_direction="<-",
602
+ rel_direction_end="-",
603
+ )
604
+ )
605
+ else:
606
+ result.append(
607
+ rel_index_template.safe_substitute(
608
+ RelLabel=rel_schema.rel_label,
609
+ rel_direction="-",
610
+ rel_direction_end="->",
611
+ )
612
+ )
613
+ return result
614
+
615
+
616
+ def build_matchlink_query(rel_schema: CartographyRelSchema) -> str:
617
+ """
618
+ Generate a Neo4j query to link two existing nodes when given a CartographyRelSchema object.
619
+ This is only used for load_matchlinks().
620
+ :param rel_schema: The CartographyRelSchema object to generate a query. This CartographyRelSchema object
621
+ - Must have a source_node_matcher and source_node_label defined
622
+ - Must have a CartographyRelProperties object where _sub_resource_label and _sub_resource_id are defined
623
+ :return: A Neo4j query that can be used to link two existing nodes.
624
+ """
625
+ if not rel_schema.source_node_matcher or not rel_schema.source_node_label:
626
+ raise ValueError(
627
+ f"No source node matcher or source node label found for {rel_schema.rel_label}. "
628
+ "MatchLink relationships require a source_node_matcher and source_node_label to be defined."
629
+ )
630
+
631
+ rel_props_as_dict = _asdict_with_validate_relprops(rel_schema)
632
+
633
+ # These are needed for the cleanup query
634
+ if "_sub_resource_label" not in rel_props_as_dict:
635
+ raise ValueError(
636
+ f"Expected _sub_resource_label to be defined on {rel_schema.properties.__class__.__name__}"
637
+ "Please include `_sub_resource_label: PropertyRef = PropertyRef('_sub_resource_label', set_in_kwargs=True)`"
638
+ )
639
+ if "_sub_resource_id" not in rel_props_as_dict:
640
+ raise ValueError(
641
+ f"Expected _sub_resource_id to be defined on {rel_schema.properties.__class__.__name__}"
642
+ "Please include `_sub_resource_id: PropertyRef = PropertyRef('_sub_resource_id', set_in_kwargs=True)`"
643
+ )
644
+
645
+ matchlink_query_template = Template(
646
+ """
647
+ UNWIND $DictList as item
648
+ $source_match
649
+ $target_match
650
+ MERGE $rel
651
+ ON CREATE SET r.firstseen = timestamp()
652
+ SET
653
+ $set_rel_properties_statement;
654
+ """
655
+ )
656
+
657
+ source_match = Template(
658
+ "MATCH (from:$source_node_label{$match_clause})"
659
+ ).safe_substitute(
660
+ source_node_label=rel_schema.source_node_label,
661
+ match_clause=_build_match_clause(rel_schema.source_node_matcher),
662
+ )
663
+
664
+ target_match = Template(
665
+ "MATCH (to:$target_node_label{$match_clause})"
666
+ ).safe_substitute(
667
+ target_node_label=rel_schema.target_node_label,
668
+ match_clause=_build_match_clause(rel_schema.target_node_matcher),
669
+ )
670
+
671
+ if rel_schema.direction == LinkDirection.INWARD:
672
+ rel = f"(from)<-[r:{rel_schema.rel_label}]-(to)"
673
+ else:
674
+ rel = f"(from)-[r:{rel_schema.rel_label}]->(to)"
675
+
676
+ return matchlink_query_template.safe_substitute(
677
+ source_match=source_match,
678
+ target_match=target_match,
679
+ rel=rel,
680
+ set_rel_properties_statement=_build_rel_properties_statement(
681
+ "r",
682
+ rel_props_as_dict,
683
+ ),
684
+ )
@@ -56,7 +56,7 @@ class GraphStatement:
56
56
 
57
57
  self.parent_job_name = parent_job_name if parent_job_name else None
58
58
  self.parent_job_sequence_num = (
59
- parent_job_sequence_num if parent_job_sequence_num else None
59
+ parent_job_sequence_num if parent_job_sequence_num else 1
60
60
  )
61
61
 
62
62
  def merge_parameters(self, parameters: Dict) -> None:
@@ -9,6 +9,7 @@ import neo4j
9
9
  from cartography.client.core.tx import load
10
10
  from cartography.graph.job import GraphJob
11
11
  from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.efs.access_point import EfsAccessPointSchema
12
13
  from cartography.models.aws.efs.file_system import EfsFileSystemSchema
13
14
  from cartography.models.aws.efs.mount_target import EfsMountTargetSchema
14
15
  from cartography.util import aws_handle_regions
@@ -44,6 +45,7 @@ def transform_efs_file_systems(
44
45
  transformed_file_system = {
45
46
  "FileSystemId": file_system["FileSystemId"],
46
47
  "FileSystemArn": file_system["FileSystemArn"],
48
+ "Region": region,
47
49
  "OwnerId": file_system.get("OwnerId"),
48
50
  "CreationToken": file_system.get("CreationToken"),
49
51
  "CreationTime": file_system.get("CreationTime"),
@@ -87,6 +89,49 @@ def get_efs_mount_targets(
87
89
  return mountTargets
88
90
 
89
91
 
92
+ @timeit
93
+ @aws_handle_regions
94
+ def get_efs_access_points(
95
+ boto3_session: boto3.Session, region: str
96
+ ) -> List[Dict[str, Any]]:
97
+ client = boto3_session.client(
98
+ "efs", region_name=region, config=get_botocore_config()
99
+ )
100
+
101
+ paginator = client.get_paginator("describe_access_points")
102
+ accessPoints = []
103
+ for page in paginator.paginate():
104
+ accessPoints.extend(page.get("AccessPoints", []))
105
+
106
+ return accessPoints
107
+
108
+
109
+ def transform_efs_access_points(
110
+ accessPoints: List[Dict[str, Any]], region: str
111
+ ) -> List[Dict[str, Any]]:
112
+ """
113
+ Transform Efs Access Points data for ingestion
114
+ """
115
+ transformed = []
116
+ for ap in accessPoints:
117
+ transformed.append(
118
+ {
119
+ "AccessPointArn": ap["AccessPointArn"],
120
+ "AccessPointId": ap["AccessPointId"],
121
+ "Region": region,
122
+ "FileSystemId": ap["FileSystemId"],
123
+ "Name": ap.get("Name"),
124
+ "LifeCycleState": ap.get("LifeCycleState"),
125
+ "OwnerId": ap.get("OwnerId"),
126
+ "Uid": ap.get("PosixUser", {}).get("Uid"),
127
+ "Gid": ap.get("PosixUser", {}).get("Gid"),
128
+ "RootDirectoryPath": ap.get("RootDirectory", {}).get("Path"),
129
+ }
130
+ )
131
+
132
+ return transformed
133
+
134
+
90
135
  @timeit
91
136
  def load_efs_mount_targets(
92
137
  neo4j_session: neo4j.Session,
@@ -129,6 +174,27 @@ def load_efs_file_systems(
129
174
  )
130
175
 
131
176
 
177
+ @timeit
178
+ def load_efs_access_points(
179
+ neo4j_session: neo4j.Session,
180
+ data: List[Dict[str, Any]],
181
+ region: str,
182
+ current_aws_account_id: str,
183
+ aws_update_tag: int,
184
+ ) -> None:
185
+ logger.info(
186
+ f"Loading Efs {len(data)} access points for region '{region}' into graph.",
187
+ )
188
+ load(
189
+ neo4j_session,
190
+ EfsAccessPointSchema(),
191
+ data,
192
+ lastupdated=aws_update_tag,
193
+ Region=region,
194
+ AWS_ID=current_aws_account_id,
195
+ )
196
+
197
+
132
198
  @timeit
133
199
  def cleanup(
134
200
  neo4j_session: neo4j.Session,
@@ -141,6 +207,9 @@ def cleanup(
141
207
  GraphJob.from_node_schema(EfsFileSystemSchema(), common_job_parameters).run(
142
208
  neo4j_session
143
209
  )
210
+ GraphJob.from_node_schema(EfsAccessPointSchema(), common_job_parameters).run(
211
+ neo4j_session
212
+ )
144
213
 
145
214
 
146
215
  @timeit
@@ -178,4 +247,15 @@ def sync(
178
247
  update_tag,
179
248
  )
180
249
 
250
+ accessPoints = get_efs_access_points(boto3_session, region)
251
+ accessPoints_transformed = transform_efs_access_points(accessPoints, region)
252
+
253
+ load_efs_access_points(
254
+ neo4j_session,
255
+ accessPoints_transformed,
256
+ region,
257
+ current_aws_account_id,
258
+ update_tag,
259
+ )
260
+
181
261
  cleanup(neo4j_session, common_job_parameters)
@@ -16,8 +16,6 @@ from cartography.util import aws_handle_regions
16
16
  from cartography.util import aws_paginate
17
17
  from cartography.util import batch
18
18
  from cartography.util import timeit
19
- from cartography.util import to_asynchronous
20
- from cartography.util import to_synchronous
21
19
 
22
20
  logger = logging.getLogger(__name__)
23
21
 
@@ -329,10 +327,9 @@ def sync(
329
327
  member_accounts = get_member_accounts(boto3_session, region)
330
328
  # the current host account may not be considered a "member", but we still fetch its findings
331
329
  member_accounts.append(current_aws_account_id)
332
-
333
- async def async_ingest_findings_for_account(account_id: str) -> None:
334
- await to_asynchronous(
335
- _sync_findings_for_account,
330
+ logger.info(f"Member accounts to be synced: {member_accounts}")
331
+ for account_id in member_accounts:
332
+ _sync_findings_for_account(
336
333
  neo4j_session,
337
334
  boto3_session,
338
335
  region,
@@ -341,11 +338,4 @@ def sync(
341
338
  current_aws_account_id,
342
339
  )
343
340
 
344
- to_synchronous(
345
- *[
346
- async_ingest_findings_for_account(account_id)
347
- for account_id in member_accounts
348
- ]
349
- )
350
-
351
341
  cleanup(neo4j_session, common_job_parameters)
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+
3
+ from cartography.models.core.common import PropertyRef
4
+ from cartography.models.core.nodes import CartographyNodeProperties
5
+ from cartography.models.core.nodes import CartographyNodeSchema
6
+ from cartography.models.core.relationships import CartographyRelProperties
7
+ from cartography.models.core.relationships import CartographyRelSchema
8
+ from cartography.models.core.relationships import LinkDirection
9
+ from cartography.models.core.relationships import make_target_node_matcher
10
+ from cartography.models.core.relationships import OtherRelationships
11
+ from cartography.models.core.relationships import TargetNodeMatcher
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class EfsAccessPointNodeProperties(CartographyNodeProperties):
16
+ id: PropertyRef = PropertyRef("AccessPointArn")
17
+ arn: PropertyRef = PropertyRef("AccessPointArn", extra_index=True)
18
+ region: PropertyRef = PropertyRef("Region", set_in_kwargs=True)
19
+ access_point_id: PropertyRef = PropertyRef("AccessPointId")
20
+ file_system_id: PropertyRef = PropertyRef("FileSystemId")
21
+ lifecycle_state: PropertyRef = PropertyRef("LifeCycleState")
22
+ name: PropertyRef = PropertyRef("Name")
23
+ owner_id: PropertyRef = PropertyRef("OwnerId")
24
+ posix_gid: PropertyRef = PropertyRef("Gid")
25
+ posix_uid: PropertyRef = PropertyRef("Uid")
26
+ root_directory_path: PropertyRef = PropertyRef("RootDirectoryPath")
27
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class EfsAccessPointToAwsAccountRelProperties(CartographyRelProperties):
32
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class EfsAccessPointToAWSAccountRel(CartographyRelSchema):
37
+ target_node_label: str = "AWSAccount"
38
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
39
+ {"id": PropertyRef("AWS_ID", set_in_kwargs=True)},
40
+ )
41
+ direction: LinkDirection = LinkDirection.INWARD
42
+ rel_label: str = "RESOURCE"
43
+ properties: EfsAccessPointToAwsAccountRelProperties = (
44
+ EfsAccessPointToAwsAccountRelProperties()
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class EfsAccessPointToEfsFileSystemRelProperties(CartographyRelProperties):
50
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class EfsAccessPointToEfsFileSystemRel(CartographyRelSchema):
55
+ target_node_label: str = "EfsFileSystem"
56
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
57
+ {"id": PropertyRef("FileSystemId")},
58
+ )
59
+ direction: LinkDirection = LinkDirection.OUTWARD
60
+ rel_label: str = "ACCESS_POINT_OF"
61
+ properties: EfsAccessPointToEfsFileSystemRelProperties = (
62
+ EfsAccessPointToEfsFileSystemRelProperties()
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class EfsAccessPointSchema(CartographyNodeSchema):
68
+ label: str = "EfsAccessPoint"
69
+ properties: EfsAccessPointNodeProperties = EfsAccessPointNodeProperties()
70
+ sub_resource_relationship: EfsAccessPointToAWSAccountRel = (
71
+ EfsAccessPointToAWSAccountRel()
72
+ )
73
+ other_relationships: OtherRelationships = OtherRelationships(
74
+ [
75
+ EfsAccessPointToEfsFileSystemRel(),
76
+ ]
77
+ )
@@ -63,6 +63,7 @@ class PropertyRef:
63
63
  ```
64
64
  This means that as we create AWSInstanceProfile nodes, we will search for AWSRoles to attach to, and we do
65
65
  this by checking if each role's `arn` field is in the `Roles` list of the data dict.
66
+ Note that one_to_many has no effect on matchlinks.
66
67
  """
67
68
  self.name = name
68
69
  self.set_in_kwargs = set_in_kwargs
@@ -42,6 +42,11 @@ class CartographyRelProperties(abc.ABC):
42
42
  Abstract class that represents the properties on a CartographyRelSchema. This is intended to enforce that all
43
43
  subclasses will have a lastupdated field defined on their resulting relationships. These fields are assigned to the
44
44
  relationship in the `SET` clause.
45
+
46
+ If the CartographyRelSchema is used as a MatchLink, the following properties are required to be defined here:
47
+ - lastupdated: A PropertyRef to the update tag of the relationship.
48
+ - _sub_resource_label: A PropertyRef to the label of the sub-resource that the relationship is associated with.
49
+ - _sub_resource_id: A PropertyRef to the id of the sub-resource that the relationship is associated with.
45
50
  """
46
51
 
47
52
  lastupdated: PropertyRef = field(init=False)
@@ -90,6 +95,29 @@ def make_target_node_matcher(key_ref_dict: Dict[str, PropertyRef]) -> TargetNode
90
95
  return make_dataclass(TargetNodeMatcher.__name__, fields, frozen=True)()
91
96
 
92
97
 
98
+ @dataclass(frozen=True)
99
+ class SourceNodeMatcher:
100
+ """
101
+ Same as TargetNodeMatcher, but for the source node; see `make_source_node_matcher()`.
102
+ This object is used only for load_matchlinks() where we match on and connect existing nodes.
103
+ This has no effect on CartographyRelSchema objects that are included in CartographyNodeSchema.
104
+ """
105
+
106
+ pass
107
+
108
+
109
+ def make_source_node_matcher(key_ref_dict: Dict[str, PropertyRef]) -> SourceNodeMatcher:
110
+ """
111
+ :param key_ref_dict: A Dict mapping keys present on the node to PropertyRef objects.
112
+ :return: A SourceNodeMatcher used for CartographyRelSchema to match with other nodes.
113
+ """
114
+ fields = [
115
+ (key, PropertyRef, field(default=prop_ref))
116
+ for key, prop_ref in key_ref_dict.items()
117
+ ]
118
+ return make_dataclass(SourceNodeMatcher.__name__, fields, frozen=True)()
119
+
120
+
93
121
  @dataclass(frozen=True)
94
122
  class CartographyRelSchema(abc.ABC):
95
123
  """
@@ -139,6 +167,22 @@ class CartographyRelSchema(abc.ABC):
139
167
  """
140
168
  pass
141
169
 
170
+ @property
171
+ def source_node_label(self) -> str | None:
172
+ """
173
+ :return: Optional. Only used for load_matchlinks(). The source node label to use for the relationship.
174
+ This does not affect CartographyRelSchema that are included in CartographyNodeSchema objects.
175
+ """
176
+ return None
177
+
178
+ @property
179
+ def source_node_matcher(self) -> SourceNodeMatcher | None:
180
+ """
181
+ :return: Optional. Only used for load_matchlinks(). A SourceNodeMatcher object used to find what node(s) to attach the relationship to.
182
+ This does not affect CartographyRelSchema that are included in CartographyNodeSchema objects.
183
+ """
184
+ return None
185
+
142
186
 
143
187
  @dataclass(frozen=True)
144
188
  class OtherRelationships:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartography
3
- Version: 0.106.0rc1
3
+ Version: 0.106.0rc2
4
4
  Summary: Explore assets and their relationships across your technical infrastructure.
5
5
  Maintainer: Cartography Contributors
6
6
  License: apache2
@@ -1,6 +1,6 @@
1
1
  cartography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cartography/__main__.py,sha256=y5iqUrj4BmqZfjeDT-9balzpXeMARgHeIedRMRI1gr8,100
3
- cartography/_version.py,sha256=GMZa5N9JcxUAslbJ9f4PNbMORn40Ng2KlST6Ofx9W7I,525
3
+ cartography/_version.py,sha256=axH1PjcS4osdPi0BaK-npCXNnQPC_1rYG4YGAn2aH60,525
4
4
  cartography/cli.py,sha256=LIH9uY6LyctL8VDsrllxFvU17f1vAAhXQVsQ83wGvRI,41072
5
5
  cartography/config.py,sha256=Magq4iNynKdIl3hYjrxlikXfVLxryf73t41L2bAyUPI,14298
6
6
  cartography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -12,7 +12,7 @@ cartography/client/aws/__init__.py,sha256=Zj7nX21QQELwPLZlpldm_22IiXZ1VFsEFQbWX_
12
12
  cartography/client/aws/ecr.py,sha256=04IXnuEAauyO5Mx9Wmt79WdUnYDhYsk2QSOnwE5_BeM,1664
13
13
  cartography/client/aws/iam.py,sha256=dYsGikc36DEsSeR2XVOVFFUDwuU9yWj_EVkpgVYCFgM,1293
14
14
  cartography/client/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- cartography/client/core/tx.py,sha256=1kvITtFseyG4C8h5xUxYEvhatejMs71oE0gCjjbDHkQ,10950
15
+ cartography/client/core/tx.py,sha256=Xx6_a5u7PE5pyREuBL_J39ORcafqieFf4KdosaEv1c4,13530
16
16
  cartography/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  cartography/data/indexes.cypher,sha256=vbKOyt87_MAxrDnKU8cbZBuiLnMyrbadSZq6aIaT6Zo,24112
18
18
  cartography/data/permission_relationships.yaml,sha256=RuKGGc_3ZUQ7ag0MssB8k_zaonCkVM5E8I_svBWTmGc,969
@@ -130,11 +130,11 @@ cartography/driftdetect/shortcut.py,sha256=0AvX9vZGNRWeLiRqxU7eOOo77gcfufHx4IHZv
130
130
  cartography/driftdetect/storage.py,sha256=xvDY4qn6jDhDpBvc8hFXXpbcYIbMNqkm3wX5LBf28u0,1549
131
131
  cartography/driftdetect/util.py,sha256=Lqxv8QoFn3_3Fz18qCOjkjJ6yBwgrHjrxXmArBAEdkc,929
132
132
  cartography/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
- cartography/graph/cleanupbuilder.py,sha256=T71aR_IVp4oLu5SQb1miwn9Is8tqg25JJm4SkEdZ-80,14542
133
+ cartography/graph/cleanupbuilder.py,sha256=uMSjMOnfkfTDrgMJwflNK8HAd4oioG8NIU0JeMN7ets,16552
134
134
  cartography/graph/context.py,sha256=RGxGb8EnxowcqjR0nFF86baNhgRHeUF9wjIoFUoG8LU,1230
135
- cartography/graph/job.py,sha256=5e1L6D3RCM2WgEgbm7CUeqEhMkgw-TEh2cl1-xmzlwQ,7739
136
- cartography/graph/querybuilder.py,sha256=Pd_WVJtZB_vh9VR7cm9PAS5Fl4qW783Hvq5njW5XMxA,21814
137
- cartography/graph/statement.py,sha256=bOCAKatvObyeEcqMiq0fUMnqUiZecLVaeWOIWZm7XWs,5386
135
+ cartography/graph/job.py,sha256=5h4FsOV2tHeO5O2Gv-vglI5QUNE7V6p-RNmf9Fz5gvU,9396
136
+ cartography/graph/querybuilder.py,sha256=b_dIScQ8MZfskM3CsY8vOgZjHMV1rzbJuywML1X72mw,27206
137
+ cartography/graph/statement.py,sha256=VtvZFMqzzZk-gDeOnBx6oDj90XYOCM1EWw4d55bm7SU,5383
138
138
  cartography/intel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
139
  cartography/intel/analysis.py,sha256=uWIpmrk2hezkqjfepqz1VV2BopT7VZ-alBj2YekfE_c,1546
140
140
  cartography/intel/create_indexes.py,sha256=6G43Rko8oXsAhHdSb2RbSVMvGMLi_YALlyUQE77jPVw,742
@@ -153,7 +153,7 @@ cartography/intel/aws/config.py,sha256=IIICicLQ0OTT3H3o8LDakIkA1BPUMwSyzpKonet-P
153
153
  cartography/intel/aws/dynamodb.py,sha256=VvvjeUgi1ZrqY9flXIQnfhhaSVSEqXXHW6T9917VLBk,5728
154
154
  cartography/intel/aws/ecr.py,sha256=7a9EHZru6AUIGE_6MJ4h4_ZmrvBxsXaerCHtGr59lJ0,8393
155
155
  cartography/intel/aws/ecs.py,sha256=-RPYdc6KC5fbaosaGsgWRkuLoHnE71euud4ZC1wGJI8,13464
156
- cartography/intel/aws/efs.py,sha256=gzxss1hUXs6C6omxCaFwI3QBu5JXX8G3CTduijrN7vk,5549
156
+ cartography/intel/aws/efs.py,sha256=6ZvFmVHLfZlyo8xx2dlRsC1IoVOpBOtsij_AdFczkDo,7884
157
157
  cartography/intel/aws/eks.py,sha256=bPItyEj5q0nSDltJrr0S5MIrTPV0fK3xkqF6EV8fcqA,3759
158
158
  cartography/intel/aws/elasticache.py,sha256=dpRJCYxgUW2ImgGMt4L54xFil8apUoJxZq6hpewXxAE,4590
159
159
  cartography/intel/aws/elasticsearch.py,sha256=8X_Rp1zejkvXho0Zz_Cr4g-w9IpompdYRc-YS595Aso,8645
@@ -161,7 +161,7 @@ cartography/intel/aws/emr.py,sha256=EJoKjHQP7-F_A1trUNU05Sb42yNR1i0C9VIpGcCfAXw,
161
161
  cartography/intel/aws/iam.py,sha256=bkyIzWw2OC4MHxuoCvTiZ5eEGEQhz2asiUgY_tkX2GY,34322
162
162
  cartography/intel/aws/iam_instance_profiles.py,sha256=QUyu30xid60BFaemp-q0y9FgNsHaIQyQnQ8gHUzWC4k,2211
163
163
  cartography/intel/aws/identitycenter.py,sha256=C2EOfwQO1zBjePAXtreUcJIy7RU__ior3liRT4SpGO8,9544
164
- cartography/intel/aws/inspector.py,sha256=aqxNjnv4CJN-ZyQ16djpoQ6FmIK47vj6eAAQiwjQQv4,11436
164
+ cartography/intel/aws/inspector.py,sha256=rD_O3DUGv5-GSMNqDzb11Ps5bX1sIo_JDq3UTAGvUpE,11168
165
165
  cartography/intel/aws/kms.py,sha256=RtCxNG-Den-f4Il-NO3YL_-BFUmg1qFt4lNucue9SQM,12130
166
166
  cartography/intel/aws/lambda_function.py,sha256=cutCsMnvyJB8vKUP0fHORrhijw944cXSoLKOHYdi6SM,10389
167
167
  cartography/intel/aws/organizations.py,sha256=m5qk60N6pe7iKLN-Rtfg9aYv7OozoloSkcsuePd-RGs,4680
@@ -373,6 +373,7 @@ cartography/models/aws/ecs/services.py,sha256=YPVTS0BB3X6tFCqIX3V1AZXcnHQEADusQk
373
373
  cartography/models/aws/ecs/task_definitions.py,sha256=zZh2SHXEAimp9YgYf1wRU-wNs_4LWNk7uNzDoms8Sy8,4182
374
374
  cartography/models/aws/ecs/tasks.py,sha256=6r2WZDc26LGR2j-U5s5v4d4PbMIvEDSkLAbTtpFdrUM,5013
375
375
  cartography/models/aws/efs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
376
+ cartography/models/aws/efs/access_point.py,sha256=Auk8E0AoPjrCxch8bP8fhT3JZ2WxblTTfYSqamA_p8g,3185
376
377
  cartography/models/aws/efs/file_system.py,sha256=6BNzjQJKZ-rYlDLw1UvDBhVRKre07-9EkcW9xvR3Wh0,2842
377
378
  cartography/models/aws/efs/mount_target.py,sha256=Bdd2zgWp9HtsK9EmEAgoIYpT9AOTs5wzH3sgCq4HLMU,3347
378
379
  cartography/models/aws/eks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -409,9 +410,9 @@ cartography/models/cloudflare/member.py,sha256=s7S4OJjXAOq9vPc-TXFGbxKDjEHB5Y_Ek
409
410
  cartography/models/cloudflare/role.py,sha256=HRjDoLMSs0YJlJosZsXEZV58OE_3vGAWSDN_aGiK2Y8,1855
410
411
  cartography/models/cloudflare/zone.py,sha256=uMHTSHq32eWe47k3b08sbnQBLrbNWPKpL9Osc4jcFD0,2805
411
412
  cartography/models/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
412
- cartography/models/core/common.py,sha256=O8hF-kNdQ5-1NGKJuX5fcOcHf4kDYYc1b9tndPCcwUU,6202
413
+ cartography/models/core/common.py,sha256=YPBpM0q2EBxTLQ-2OahGR1irMsCEFfu0eL-q5Uijxko,6261
413
414
  cartography/models/core/nodes.py,sha256=GFK-optqan5L57D36ZQ7AdylGRELUl9pMyBfZ7l-cz4,4367
414
- cartography/models/core/relationships.py,sha256=_ph52obhjeqc67IZ_rwuAtIbBqMHtKb04ceCP6g4Q04,5151
415
+ cartography/models/core/relationships.py,sha256=TfcU01lIgBxoINjEwTWngIXkbFuHnE-UHGQ9h-V6ZDE,7100
415
416
  cartography/models/crowdstrike/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
416
417
  cartography/models/crowdstrike/hosts.py,sha256=J4UpdsVUNiQXqxnvmWdVkwsFueVagPV-7sTV3D7Lkts,2686
417
418
  cartography/models/cve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -481,9 +482,9 @@ cartography/models/trivy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
481
482
  cartography/models/trivy/findings.py,sha256=SgI_h1aRyR20uAHvuXIZ1T6r4IZJt6SVhxRaF2bTsm0,3085
482
483
  cartography/models/trivy/fix.py,sha256=ho9ENVl9HSXqyggyCwR6ilkOBKDxpQ7rGibo_j21NA4,2587
483
484
  cartography/models/trivy/package.py,sha256=IwO1RZZ-MFRtNbt8Cq6YFl6fdNJMFmULnJkkK8Q4rL4,2809
484
- cartography-0.106.0rc1.dist-info/licenses/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
485
- cartography-0.106.0rc1.dist-info/METADATA,sha256=9XcrmhSHjFUjYenD-w_9n7_q-vZTVf5XLlvVVfh6zYE,12409
486
- cartography-0.106.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
487
- cartography-0.106.0rc1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
488
- cartography-0.106.0rc1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
489
- cartography-0.106.0rc1.dist-info/RECORD,,
485
+ cartography-0.106.0rc2.dist-info/licenses/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
486
+ cartography-0.106.0rc2.dist-info/METADATA,sha256=Y1-Ir7t90DLnZrn2Tq-ee9j6akBX_Ezyc8YeZejQFWY,12409
487
+ cartography-0.106.0rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
488
+ cartography-0.106.0rc2.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
489
+ cartography-0.106.0rc2.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
490
+ cartography-0.106.0rc2.dist-info/RECORD,,