cartography 0.73.1__py3-none-any.whl → 0.75.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 (34) hide show
  1. cartography/client/core/tx.py +42 -0
  2. cartography/data/indexes.cypher +1 -3
  3. cartography/data/jobs/analysis/aws_ec2_asset_exposure.json +5 -5
  4. cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
  5. cartography/data/jobs/analysis/aws_eks_asset_exposure.json +1 -1
  6. cartography/data/jobs/analysis/aws_foreign_accounts.json +2 -2
  7. cartography/data/jobs/analysis/gcp_compute_asset_inet_exposure.json +4 -4
  8. cartography/data/jobs/analysis/gcp_gke_asset_exposure.json +1 -1
  9. cartography/data/jobs/analysis/gcp_gke_basic_auth.json +2 -2
  10. cartography/data/jobs/cleanup/aws_apigateway_details.json +1 -1
  11. cartography/data/jobs/cleanup/aws_kms_details.json +1 -1
  12. cartography/data/jobs/cleanup/aws_s3_details.json +1 -1
  13. cartography/graph/cleanupbuilder.py +18 -38
  14. cartography/graph/job.py +1 -3
  15. cartography/graph/querybuilder.py +80 -3
  16. cartography/graph/statement.py +14 -3
  17. cartography/intel/aws/apigateway.py +1 -1
  18. cartography/intel/aws/ecs.py +26 -19
  19. cartography/intel/aws/emr.py +17 -22
  20. cartography/intel/aws/iam.py +15 -10
  21. cartography/intel/aws/kms.py +1 -1
  22. cartography/intel/aws/rds.py +1 -1
  23. cartography/intel/aws/s3.py +3 -3
  24. cartography/intel/azure/util/credentials.py +13 -3
  25. cartography/intel/oci/iam.py +1 -1
  26. cartography/models/aws/emr.py +1 -1
  27. cartography/models/core/common.py +21 -1
  28. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/METADATA +1 -1
  29. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/RECORD +34 -34
  30. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/WHEEL +1 -1
  31. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/LICENSE +0 -0
  32. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/NOTICE +0 -0
  33. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/entry_points.txt +0 -0
  34. {cartography-0.73.1.dist-info → cartography-0.75.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,9 @@ from typing import Union
7
7
 
8
8
  import neo4j
9
9
 
10
+ from cartography.graph.querybuilder import build_create_index_queries
11
+ from cartography.graph.querybuilder import build_ingestion_query
12
+ from cartography.models.core.nodes import CartographyNodeSchema
10
13
  from cartography.util import batch
11
14
 
12
15
 
@@ -210,3 +213,42 @@ def load_graph_data(
210
213
  DictList=data_batch,
211
214
  **kwargs,
212
215
  )
216
+
217
+
218
+ def ensure_indexes(neo4j_session: neo4j.Session, node_schema: CartographyNodeSchema) -> None:
219
+ """
220
+ Creates indexes if they don't exist for the given CartographyNodeSchema object, as well as for all of the
221
+ relationships defined on its `other_relationships` and `sub_resource_relationship` fields. This operation is
222
+ idempotent.
223
+
224
+ This ensures that every time we need to MATCH on a node to draw a relationship to it, the field used for the MATCH
225
+ will be indexed, making the operation fast.
226
+ :param neo4j_session: The neo4j session
227
+ :param node_schema: The node_schema object to create indexes for.
228
+ """
229
+ queries = build_create_index_queries(node_schema)
230
+
231
+ for query in queries:
232
+ if not query.startswith('CREATE INDEX IF NOT EXISTS'):
233
+ raise ValueError('Query provided to `ensure_indexes()` does not start with "CREATE INDEX IF NOT EXISTS".')
234
+ neo4j_session.run(query)
235
+
236
+
237
+ def load(
238
+ neo4j_session: neo4j.Session,
239
+ node_schema: CartographyNodeSchema,
240
+ dict_list: List[Dict[str, Any]],
241
+ **kwargs,
242
+ ) -> None:
243
+ """
244
+ Main entrypoint for intel modules to write data to the graph. Ensures that indexes exist for the datatypes loaded
245
+ to the graph and then performs the load operation.
246
+ :param neo4j_session: The Neo4j session
247
+ :param node_schema: The CartographyNodeSchema object to create indexes for and generate a query.
248
+ :param dict_list: The data to load to the graph represented as a list of dicts.
249
+ :param kwargs: Allows additional keyword args to be supplied to the Neo4j query.
250
+ :return: None
251
+ """
252
+ ensure_indexes(neo4j_session, node_schema)
253
+ ingestion_query = build_ingestion_query(node_schema)
254
+ load_graph_data(neo4j_session, ingestion_query, dict_list, **kwargs)
@@ -140,6 +140,7 @@ CREATE INDEX IF NOT EXISTS FOR (n:ECSContainerInstance) ON (n.lastupdated);
140
140
  CREATE INDEX IF NOT EXISTS FOR (n:ECSService) ON (n.id);
141
141
  CREATE INDEX IF NOT EXISTS FOR (n:ECSService) ON (n.lastupdated);
142
142
  CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.id);
143
+ CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.arn);
143
144
  CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.lastupdated);
144
145
  CREATE INDEX IF NOT EXISTS FOR (n:ECSTask) ON (n.id);
145
146
  CREATE INDEX IF NOT EXISTS FOR (n:ECSTask) ON (n.lastupdated);
@@ -158,9 +159,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:ELBListener) ON (n.id);
158
159
  CREATE INDEX IF NOT EXISTS FOR (n:ELBListener) ON (n.lastupdated);
159
160
  CREATE INDEX IF NOT EXISTS FOR (n:ELBV2Listener) ON (n.id);
160
161
  CREATE INDEX IF NOT EXISTS FOR (n:ELBV2Listener) ON (n.lastupdated);
161
- CREATE INDEX IF NOT EXISTS FOR (n:EMRCluster) ON (n.id);
162
- CREATE INDEX IF NOT EXISTS FOR (n:EMRCluster) ON (n.arn);
163
- CREATE INDEX IF NOT EXISTS FOR (n:EMRCluster) ON (n.lastupdated);
164
162
  CREATE INDEX IF NOT EXISTS FOR (n:Endpoint) ON (n.id);
165
163
  CREATE INDEX IF NOT EXISTS FOR (n:Endpoint) ON (n.lastupdated);
166
164
  CREATE INDEX IF NOT EXISTS FOR (n:ESDomain) ON (n.arn);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "statements": [
3
3
  {
4
- "query": "MATCH (n) where EXISTS(n.exposed_internet) AND labels(n) IN ['AutoScalingGroup', 'EC2Instance', 'LoadBalancer', 'LoadBalancerV2'] WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type return COUNT(*) as TotalCompleted",
4
+ "query": "MATCH (n) where n.exposed_internet IS NOT NULL AND labels(n) IN ['AutoScalingGroup', 'EC2Instance', 'LoadBalancer', 'LoadBalancerV2'] WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type return COUNT(*) as TotalCompleted",
5
5
  "iterative": true,
6
6
  "iterationsize": 1000
7
7
  },
8
8
  {
9
- "query": "MATCH (:IpRange{id: '0.0.0.0/0'})-[:MEMBER_OF_IP_RULE]->(:IpPermissionInbound)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(group:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP|NETWORK_INTERFACE*..2]-(instance:EC2Instance)\nWITH instance\nWHERE (EXISTS(instance.publicipaddress)) AND (NOT EXISTS(instance.exposed_internet_type)) OR (NOT 'direct' IN instance.exposed_internet_type)\nSET instance.exposed_internet = true, instance.exposed_internet_type = coalesce(instance.exposed_internet_type , []) + 'direct';",
9
+ "query": "MATCH (:IpRange{id: '0.0.0.0/0'})-[:MEMBER_OF_IP_RULE]->(:IpPermissionInbound)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(group:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP|NETWORK_INTERFACE*..2]-(instance:EC2Instance)\nWITH instance\nWHERE (instance.publicipaddress IS NOT NULL) AND (instance.exposed_internet_type IS NULL) OR (NOT 'direct' IN instance.exposed_internet_type)\nSET instance.exposed_internet = true, instance.exposed_internet_type = coalesce(instance.exposed_internet_type , []) + 'direct';",
10
10
  "iterative": false
11
11
  },
12
12
  {
@@ -18,15 +18,15 @@
18
18
  "iterative": false
19
19
  },
20
20
  {
21
- "query": "MATCH (elb:LoadBalancer{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (NOT EXISTS(e.exposed_internet_type)) OR (NOT 'elb' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elb'",
21
+ "query": "MATCH (elb:LoadBalancer{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (e.exposed_internet_type IS NULL) OR (NOT 'elb' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elb'",
22
22
  "iterative": false
23
23
  },
24
24
  {
25
- "query": "MATCH (elbv2:LoadBalancerV2{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (NOT EXISTS(e.exposed_internet_type)) OR (NOT 'elbv2' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elbv2'",
25
+ "query": "MATCH (elbv2:LoadBalancerV2{exposed_internet: true})-[:EXPOSE]->(e:EC2Instance)\nWITH e\nWHERE (e.exposed_internet_type IS NULL) OR (NOT 'elbv2' IN e.exposed_internet_type)\nSET e.exposed_internet = true, e.exposed_internet_type = coalesce(e.exposed_internet_type, []) + 'elbv2'",
26
26
  "iterative": false
27
27
  },
28
28
  {
29
- "query": "MATCH (instance:EC2Instance{exposed_internet: true})-[:MEMBER_AUTO_SCALE_GROUP]->(asg:AutoScalingGroup)\nWITH distinct instance.exposed_internet_type as types, asg\nUNWIND types as type\nWITH type, asg\nWHERE NOT EXISTS(asg.exposed_internet_type) OR (NOT type IN asg.exposed_internet_type)\nSET asg.exposed_internet = true, asg.exposed_internet_type = coalesce(asg.exposed_internet_type, []) + type;",
29
+ "query": "MATCH (instance:EC2Instance{exposed_internet: true})-[:MEMBER_AUTO_SCALE_GROUP]->(asg:AutoScalingGroup)\nWITH distinct instance.exposed_internet_type as types, asg\nUNWIND types as type\nWITH type, asg\nWHERE asg.exposed_internet_type IS NULL OR (NOT type IN asg.exposed_internet_type)\nSET asg.exposed_internet = true, asg.exposed_internet_type = coalesce(asg.exposed_internet_type, []) + type;",
30
30
  "iterative": false
31
31
  }
32
32
  ],
@@ -3,12 +3,12 @@
3
3
  "statements": [
4
4
  {
5
5
  "__comment__": "Delete the attribute user_uploaded",
6
- "query": "MATCH (k:EC2KeyPair) WHERE EXISTS (k.user_uploaded) REMOVE k.user_uploaded return COUNT(*) as TotalCompleted",
6
+ "query": "MATCH (k:EC2KeyPair) WHERE k.user_uploaded IS NOT NULL REMOVE k.user_uploaded return COUNT(*) as TotalCompleted",
7
7
  "iterative": false
8
8
  },
9
9
  {
10
10
  "__comment__": "Delete the attribute duplicate_keyfingerprint",
11
- "query": "MATCH (k:EC2KeyPair) WHERE EXISTS (k.duplicate_keyfingerprint) REMOVE k.duplicate_keyfingerprint return COUNT(*) as TotalCompleted",
11
+ "query": "MATCH (k:EC2KeyPair) WHERE k.duplicate_keyfingerprint IS NOT NULL REMOVE k.duplicate_keyfingerprint return COUNT(*) as TotalCompleted",
12
12
  "iterative": false
13
13
  },
14
14
  {
@@ -2,7 +2,7 @@
2
2
  "statements": [
3
3
  {
4
4
  "__comment": "This is a clean-up statement to remove custom attributes",
5
- "query": "MATCH (cluster:EKSCluster) WHERE EXISTS(cluster.exposed_internet) REMOVE cluster.exposed_internet return COUNT(*) as TotalCompleted",
5
+ "query": "MATCH (cluster:EKSCluster) WHERE cluster.exposed_internet IS NOT NULL REMOVE cluster.exposed_internet return COUNT(*) as TotalCompleted",
6
6
  "iterative": false
7
7
  },
8
8
  {
@@ -2,12 +2,12 @@
2
2
  "statements": [
3
3
  {
4
4
  "__comment": "This analyze AWS accounts we created and tag the ones that are foreign. Foreign accounts are ones that were not in the sync scope",
5
- "query": "MATCH (foreign:AWSAccount) where NOT EXISTS(foreign.inscope) SET foreign.foreign = true",
5
+ "query": "MATCH (foreign:AWSAccount) where foreign.inscope IS NULL SET foreign.foreign = true",
6
6
  "iterative": false
7
7
  },
8
8
  {
9
9
  "__comment": "Remove accounts that were set with foreign and inscope. This can happen as we finish the list of sync accounts through assume role mapping and vpc peering",
10
- "query": "MATCH (a:AWSAccount) where EXISTS(a.inscope) AND EXISTS(a.foreign) REMOVE a.foreign",
10
+ "query": "MATCH (a:AWSAccount) where a.inscope IS NOT NULL AND a.foreign IS NOT NULL REMOVE a.foreign",
11
11
  "iterative": false
12
12
  }
13
13
  ],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "statements": [
3
3
  {
4
- "query": "MATCH (n) where EXISTS(n.exposed_internet) AND labels(n) IN ['GCPInstance'] WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type return COUNT(*) as TotalCompleted",
4
+ "query": "MATCH (n) where n.exposed_internet IS NOT NULL AND labels(n) IN ['GCPInstance'] WITH n LIMIT $LIMIT_SIZE REMOVE n.exposed_internet, n.exposed_internet_type return COUNT(*) as TotalCompleted",
5
5
  "iterative": true,
6
6
  "iterationsize": 1000,
7
7
  "__comment__": "Delete exposed_internet off nodes so we can start fresh"
@@ -22,17 +22,17 @@
22
22
  "__comment__": "Delete stale firewall ingress relationships"
23
23
  },
24
24
  {
25
- "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'tcp'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'tcp'})\nWHERE exists(ac.public_ip) and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
25
+ "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'tcp'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'tcp'})\nWHERE ac.public_ip IS NOT NULL and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
26
26
  "iterative": false,
27
27
  "__comment__": "Mark a GCP instance with exposed_internet = True and exposed_internet_type = 'direct' if its attached firewalls and TCP rules expose it to the internet."
28
28
  },
29
29
  {
30
- "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'udp'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'udp'})\nWHERE exists(ac.public_ip) and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
30
+ "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'udp'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'udp'})\nWHERE ac.public_ip IS NOT NULL and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
31
31
  "iterative": false,
32
32
  "__comment__": "Mark a GCP instance with exposed_internet = True and exposed_internet_type = 'direct' if its attached firewalls and UDP rules expose it to the internet."
33
33
  },
34
34
  {
35
- "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'all'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'all'})\nWHERE exists(ac.public_ip) and exists(allow_rule.fromport) and exists(allow_rule.toport) and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
35
+ "query": "MATCH (ac:GCPNicAccessConfig)<-[:RESOURCE]-(:GCPNetworkInterface)<-[:NETWORK_INTERFACE]-(n:GCPInstance)<-[:FIREWALL_INGRESS]-(firewall_a:GCPFirewall)<-[:ALLOWED_BY]-(allow_rule:GCPIpRule{protocol:'all'})<-[:MEMBER_OF_IP_RULE]-(:IpRange{id:\"0.0.0.0/0\"})\nOPTIONAL MATCH (n)<-[:FIREWALL_INGRESS]-(firewall_b:GCPFirewall)<-[:DENIED_BY]-(deny_rule:GCPIpRule{protocol:'all'})\nWHERE ac.public_ip IS NOT NULL and allow_rule.fromport IS NOT NULL and allow_rule.toport IS NOT NULL and (\n\tdeny_rule is NULL\n\tOR firewall_b.priority > firewall_a.priority\n\tOR NOT allow_rule.fromport IN RANGE(deny_rule.fromport, deny_rule.toport)\n\tOR NOT allow_rule.toport IN RANGE(deny_rule.fromport, deny_rule.toport)\n)\nSET n.exposed_internet = True, n.exposed_internet_type='direct'\nRETURN count(*) as TotalCompleted",
36
36
  "iterative": false,
37
37
  "__comment__": "Mark a GCP instance with exposed_internet = True and exposed_internet_type = 'direct' if its attached firewalls and ALL rules expose it to the internet."
38
38
  }
@@ -2,7 +2,7 @@
2
2
  "statements": [
3
3
  {
4
4
  "__comment": "This is a clean-up statement to remove custom attributes",
5
- "query": "MATCH (cluster:GKECluster) WHERE EXISTS(cluster.exposed_internet) REMOVE cluster.exposed_internet return COUNT(*) as TotalCompleted",
5
+ "query": "MATCH (cluster:GKECluster) WHERE cluster.exposed_internet IS NOT NULL REMOVE cluster.exposed_internet return COUNT(*) as TotalCompleted",
6
6
  "iterative": false
7
7
  },
8
8
  {
@@ -2,12 +2,12 @@
2
2
  "statements": [
3
3
  {
4
4
  "__comment": "This is a clean-up statement to remove custom attributes",
5
- "query": "MATCH (cluster:GKECluster) WHERE EXISTS(cluster.basic_auth) REMOVE cluster.basic_auth return COUNT(*) as TotalCompleted",
5
+ "query": "MATCH (cluster:GKECluster) WHERE cluster.basic_auth IS NOT NULL REMOVE cluster.basic_auth return COUNT(*) as TotalCompleted",
6
6
  "iterative": false
7
7
  },
8
8
  {
9
9
  "__comment": "This sets the basic_auth attribute",
10
- "query": "MATCH (cluster:GKECluster) WHERE (EXISTS(cluster.masterauth_username) AND NOT cluster.masterauth_username = '') AND (EXISTS(cluster.masterauth_password) AND NOT cluster.masterauth.password = '') SET cluster.basic_auth = true",
10
+ "query": "MATCH (cluster:GKECluster) WHERE (cluster.masterauth_username IS NOT NULL AND NOT cluster.masterauth_username = '') AND (cluster.masterauth_password IS NOT NULL AND NOT cluster.masterauth.password = '') SET cluster.basic_auth = true",
11
11
  "iterative": false
12
12
  }
13
13
  ],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "statements": [
3
3
  {
4
- "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:RestAPI) WHERE EXISTS(s.anonymous_access)\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
4
+ "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:RestAPI) WHERE s.anonymous_access IS NOT NULL\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
5
5
  "iterative": true,
6
6
  "iterationsize": 100
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "statements": [
3
3
  {
4
- "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:KMSKey) WHERE EXISTS(s.anonymous_access)\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
4
+ "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:KMSKey) WHERE s.anonymous_access IS NOT NULL\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
5
5
  "iterative": true,
6
6
  "iterationsize": 100
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "statements": [
3
3
  {
4
- "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) WHERE EXISTS(s.anonymous_access)\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
4
+ "query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) WHERE s.anonymous_access IS NOT NULL\n WITH s LIMIT $LIMIT_SIZE\nREMOVE s.anonymous_access, s.anonymous_actions",
5
5
  "iterative": true,
6
6
  "iterationsize": 100
7
7
  }
@@ -1,11 +1,8 @@
1
1
  from dataclasses import asdict
2
2
  from string import Template
3
3
  from typing import List
4
- from typing import Optional
5
- from typing import Set
6
4
 
7
5
  from cartography.graph.querybuilder import _build_match_clause
8
- from cartography.graph.querybuilder import filter_selected_relationships
9
6
  from cartography.graph.querybuilder import rel_present_on_node_schema
10
7
  from cartography.models.core.nodes import CartographyNodeSchema
11
8
  from cartography.models.core.relationships import CartographyRelSchema
@@ -13,49 +10,32 @@ from cartography.models.core.relationships import LinkDirection
13
10
  from cartography.models.core.relationships import TargetNodeMatcher
14
11
 
15
12
 
16
- def build_cleanup_queries(
17
- node_schema: CartographyNodeSchema,
18
- selected_rels: Optional[Set[CartographyRelSchema]] = None,
19
- ) -> List[str]:
13
+ def build_cleanup_queries(node_schema: CartographyNodeSchema) -> List[str]:
20
14
  """
21
15
  Generates queries to clean up stale nodes and relationships from the given CartographyNodeSchema.
22
- :param node_schema: The given CartographyNodeSchema to generate cleanup queries for.
23
- :param selected_rels: Optional. If specified, only generate cleanup queries where the `node_schema` is bound to this
24
- given set of selected relationships. Raises an exception if any of the rels in `selected_rels` aren't actually
25
- defined on the `node_schema`.
26
- If `selected_rels` is not specified (default), we generate cleanup queries against all relationships defined on the
27
- `node_schema`.
28
- :return: A list of Neo4j queries to clean up nodes and relationships. Order matters: we always clean up the sub
29
- resource relationship last because we only clean up stale nodes and rels that are associated with a given sub
30
- resource, so if we delete the sub resource first then we will not be able to reach the stale nodes and rels, thus
31
- leaving orphaned objects behind.
32
- Note also that we return the empty list if the node_schema has no relationships. Doing cleanups of nodes without
33
- relationships can be resource expensive for a large graph, and you might risk deleting unintended objects. Please
34
- write a manual cleanup job if you wish to do this.
16
+ Note that auto-cleanups for a node with no relationships is not currently supported.
17
+ Algorithm:
18
+ 1. First delete all stale nodes attached to the node_schema's sub resource
19
+ 2. Delete all stale node to sub resource relationships
20
+ - We don't expect this to be very common (never for AWS resources, at least), but in case it is possible for an
21
+ asset to change sub resources, we want to handle it properly.
22
+ 3. For all relationships defined on the node schema, delete all stale ones.
23
+ :param node_schema: The given CartographyNodeSchema
24
+ :return: A list of Neo4j queries to clean up nodes and relationships.
35
25
  """
36
- other_rels = node_schema.other_relationships
37
- sub_resource_rel = node_schema.sub_resource_relationship
38
-
39
- if selected_rels:
40
- # Ensure that the selected rels actually exist on the node_schema
41
- sub_resource_rel, other_rels = filter_selected_relationships(node_schema, selected_rels)
42
-
43
- if not sub_resource_rel:
26
+ if not node_schema.sub_resource_relationship:
44
27
  raise ValueError(
45
28
  "Auto-creating a cleanup job for a node_schema without a sub resource relationship is not supported. "
46
- f'Please check the class definition of "{node_schema.__class__.__name__}". If the optional `selected_rels` '
47
- 'param was specified to build_cleanup_queries(), then ensure that the sub resource relationship is '
48
- 'present.',
29
+ f'Please check the class definition of "{node_schema.__class__.__name__}".',
49
30
  )
50
31
 
51
- result = []
52
- if other_rels:
53
- for rel in other_rels.rels:
54
- result.extend(_build_cleanup_node_and_rel_queries(node_schema, rel))
32
+ result = _build_cleanup_node_and_rel_queries(node_schema, node_schema.sub_resource_relationship)
33
+ if node_schema.other_relationships:
34
+ for rel in node_schema.other_relationships.rels:
35
+ # [0] is the delete node query, [1] is the delete relationship query. We only want the latter.
36
+ _, rel_query = _build_cleanup_node_and_rel_queries(node_schema, rel)
37
+ result.append(rel_query)
55
38
 
56
- # Make sure that the sub resource cleanup job is last in the list; order matters.
57
- result.extend(_build_cleanup_node_and_rel_queries(node_schema, sub_resource_rel))
58
- # Note that auto-cleanups for a node with no relationships does not happen at all - we don't support it.
59
39
  return result
60
40
 
61
41
 
cartography/graph/job.py CHANGED
@@ -16,7 +16,6 @@ from cartography.graph.cleanupbuilder import build_cleanup_queries
16
16
  from cartography.graph.statement import get_job_shortname
17
17
  from cartography.graph.statement import GraphStatement
18
18
  from cartography.models.core.nodes import CartographyNodeSchema
19
- from cartography.models.core.relationships import CartographyRelSchema
20
19
 
21
20
  logger = logging.getLogger(__name__)
22
21
 
@@ -129,14 +128,13 @@ class GraphJob:
129
128
  cls,
130
129
  node_schema: CartographyNodeSchema,
131
130
  parameters: Dict[str, Any],
132
- selected_rels: Optional[Set[CartographyRelSchema]] = None,
133
131
  ) -> 'GraphJob':
134
132
  """
135
133
  Create a cleanup job from a CartographyNodeSchema object.
136
134
  For a given node, the fields used in the node_schema.sub_resource_relationship.target_node_node_matcher.keys()
137
135
  must be provided as keys and values in the params dict.
138
136
  """
139
- queries: List[str] = build_cleanup_queries(node_schema, selected_rels)
137
+ queries: List[str] = build_cleanup_queries(node_schema)
140
138
 
141
139
  expected_param_keys: Set[str] = get_parameters(queries)
142
140
  actual_param_keys: Set[str] = set(parameters.keys())
@@ -2,6 +2,7 @@ import logging
2
2
  from dataclasses import asdict
3
3
  from string import Template
4
4
  from typing import Dict
5
+ from typing import List
5
6
  from typing import Optional
6
7
  from typing import Set
7
8
  from typing import Tuple
@@ -108,6 +109,28 @@ def _build_match_clause(matcher: TargetNodeMatcher) -> str:
108
109
  return ', '.join(match.safe_substitute(Key=key, PropRef=prop_ref) for key, prop_ref in matcher_asdict.items())
109
110
 
110
111
 
112
+ def _build_where_clause_for_rel_match(node_var: str, matcher: TargetNodeMatcher) -> str:
113
+ """
114
+ Same as _build_match_clause, but puts the matching logic in a WHERE clause.
115
+ This is intended specifically to use for joining with relationships where we need a case-insensitive match.
116
+ :param matcher: A TargetNodeMatcher object
117
+ :return: a Neo4j where clause
118
+ """
119
+ match = Template("$node_var.$key = $prop_ref")
120
+ case_insensitive_match = Template("toLower($node_var.$key) = toLower($prop_ref)")
121
+
122
+ matcher_asdict = asdict(matcher)
123
+
124
+ result = []
125
+ for key, prop_ref in matcher_asdict.items():
126
+ if prop_ref.ignore_case:
127
+ prop_line = case_insensitive_match.safe_substitute(node_var=node_var, key=key, prop_ref=prop_ref)
128
+ else:
129
+ prop_line = match.safe_substitute(node_var=node_var, key=key, prop_ref=prop_ref)
130
+ result.append(prop_line)
131
+ return ' AND\n'.join(result)
132
+
133
+
111
134
  def _asdict_with_validate_relprops(link: CartographyRelSchema) -> Dict[str, PropertyRef]:
112
135
  """
113
136
  Give a helpful error message when forgetting to put `()` when instantiating a CartographyRelSchema, as this
@@ -145,6 +168,7 @@ def _build_attach_sub_resource_statement(sub_resource_link: Optional[Cartography
145
168
 
146
169
  sub_resource_attach_template = Template(
147
170
  """
171
+ WITH i, item
148
172
  OPTIONAL MATCH (j:$SubResourceLabel{$MatchClause})
149
173
  WITH i, item, j WHERE j IS NOT NULL
150
174
  $RelMergeClause
@@ -191,7 +215,9 @@ def _build_attach_additional_links_statement(
191
215
  additional_links_template = Template(
192
216
  """
193
217
  WITH i, item
194
- OPTIONAL MATCH ($node_var:$AddlLabel{$MatchClause})
218
+ OPTIONAL MATCH ($node_var:$AddlLabel)
219
+ WHERE
220
+ $WhereClause
195
221
  WITH i, item, $node_var WHERE $node_var IS NOT NULL
196
222
  $RelMerge
197
223
  ON CREATE SET $rel_var.firstseen = timestamp()
@@ -219,7 +245,7 @@ def _build_attach_additional_links_statement(
219
245
 
220
246
  additional_ref = additional_links_template.safe_substitute(
221
247
  AddlLabel=link.target_node_label,
222
- MatchClause=_build_match_clause(link.target_node_matcher),
248
+ WhereClause=_build_where_clause_for_rel_match(node_var, link.target_node_matcher),
223
249
  node_var=node_var,
224
250
  rel_var=rel_var,
225
251
  RelMerge=rel_merge,
@@ -258,7 +284,6 @@ def _build_attach_relationships_statement(
258
284
  """
259
285
  WITH i, item
260
286
  CALL {
261
- WITH i, item
262
287
  $attach_relationships_statement
263
288
  }
264
289
  """,
@@ -374,3 +399,55 @@ def build_ingestion_query(
374
399
  attach_relationships_statement=_build_attach_relationships_statement(sub_resource_rel, other_rels),
375
400
  )
376
401
  return ingest_query
402
+
403
+
404
+ def build_create_index_queries(node_schema: CartographyNodeSchema) -> List[str]:
405
+ """
406
+ Generate queries to create indexes for the given CartographyNodeSchema and all node types attached to it via its
407
+ relationships.
408
+ :param node_schema: The Cartography node_schema object
409
+ :return: A list of queries of the form `CREATE INDEX IF NOT EXISTS FOR (n:$TargetNodeLabel) ON (n.$TargetAttribute)`
410
+ """
411
+ index_template = Template('CREATE INDEX IF NOT EXISTS FOR (n:$TargetNodeLabel) ON (n.$TargetAttribute);')
412
+
413
+ # First ensure an index exists for the node_schema and all extra labels on the `id` and `lastupdated` fields
414
+ result = [
415
+ index_template.safe_substitute(
416
+ TargetNodeLabel=node_schema.label,
417
+ TargetAttribute='id',
418
+ ),
419
+ index_template.safe_substitute(
420
+ TargetNodeLabel=node_schema.label,
421
+ TargetAttribute='lastupdated',
422
+ ),
423
+ ]
424
+ if node_schema.extra_node_labels:
425
+ result.extend([
426
+ index_template.safe_substitute(
427
+ TargetNodeLabel=label,
428
+ TargetAttribute='id', # Precondition: 'id' is defined on all cartography node_schema objects.
429
+ ) for label in node_schema.extra_node_labels.labels
430
+ ])
431
+
432
+ # Next, for all relationships possible out of this node, ensure that indexes exist for all target nodes' properties
433
+ # as specified in their TargetNodeMatchers.
434
+ rel_schemas = []
435
+ if node_schema.sub_resource_relationship:
436
+ rel_schemas.extend([node_schema.sub_resource_relationship])
437
+ if node_schema.other_relationships:
438
+ rel_schemas.extend(node_schema.other_relationships.rels)
439
+ for rs in rel_schemas:
440
+ for target_key in asdict(rs.target_node_matcher).keys():
441
+ result.append(
442
+ index_template.safe_substitute(TargetNodeLabel=rs.target_node_label, TargetAttribute=target_key),
443
+ )
444
+
445
+ # Now, include extra indexes defined by the module author on the node schema's property refs.
446
+ node_props_as_dict: Dict[str, PropertyRef] = asdict(node_schema.properties)
447
+ result.extend([
448
+ index_template.safe_substitute(
449
+ TargetNodeLabel=node_schema.label,
450
+ TargetAttribute=prop_name,
451
+ ) for prop_name, prop_ref in node_props_as_dict.items() if prop_ref.extra_index
452
+ ])
453
+ return result
@@ -4,6 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
  from typing import Dict
7
+ from typing import Optional
7
8
  from typing import Union
8
9
 
9
10
  import neo4j
@@ -40,8 +41,13 @@ class GraphStatement:
40
41
  """
41
42
 
42
43
  def __init__(
43
- self, query: str, parameters: Dict = None, iterative: bool = False, iterationsize: int = 0,
44
- parent_job_name: str = None, parent_job_sequence_num: int = None,
44
+ self,
45
+ query: str,
46
+ parameters: Optional[Dict[Any, Any]] = None,
47
+ iterative: bool = False,
48
+ iterationsize: int = 0,
49
+ parent_job_name: Optional[str] = None,
50
+ parent_job_sequence_num: Optional[int] = None,
45
51
  ):
46
52
  self.query = query
47
53
  self.parameters = parameters or {}
@@ -122,7 +128,12 @@ class GraphStatement:
122
128
  result.consume()
123
129
 
124
130
  @classmethod
125
- def create_from_json(cls, json_obj: Dict, short_job_name: str = None, job_sequence_num: int = None):
131
+ def create_from_json(
132
+ cls,
133
+ json_obj: Dict[str, Any],
134
+ short_job_name: Optional[str] = None,
135
+ job_sequence_num: Optional[int] = None,
136
+ ):
126
137
  """
127
138
  Create a statement from a JSON blob.
128
139
  """
@@ -171,7 +171,7 @@ def _load_apigateway_policies(
171
171
  def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> None:
172
172
  set_defaults = """
173
173
  MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(restApi:APIGatewayRestAPI)
174
- where NOT EXISTS(restApi.anonymous_actions)
174
+ where restApi.anonymous_actions IS NULL
175
175
  SET restApi.anonymous_access = false, restApi.anonymous_actions = []
176
176
  """
177
177
 
@@ -91,16 +91,16 @@ def get_ecs_services(cluster_arn: str, boto3_session: boto3.session.Session, reg
91
91
 
92
92
  @timeit
93
93
  @aws_handle_regions
94
- def get_ecs_task_definitions(boto3_session: boto3.session.Session, region: str) -> List[Dict[str, Any]]:
94
+ def get_ecs_task_definitions(
95
+ boto3_session: boto3.session.Session,
96
+ region: str,
97
+ tasks: List[Dict[str, Any]],
98
+ ) -> List[Dict[str, Any]]:
95
99
  client = boto3_session.client('ecs', region_name=region)
96
- paginator = client.get_paginator('list_task_definitions')
97
100
  task_definitions: List[Dict[str, Any]] = []
98
- task_definition_arns: List[str] = []
99
- for page in paginator.paginate():
100
- task_definition_arns.extend(page.get('taskDefinitionArns', []))
101
- for arn in task_definition_arns:
101
+ for task in tasks:
102
102
  task_definition = client.describe_task_definition(
103
- taskDefinition=arn,
103
+ taskDefinition=task['taskDefinitionArn'],
104
104
  )
105
105
  task_definitions.append(task_definition['taskDefinition'])
106
106
  return task_definitions
@@ -294,7 +294,8 @@ def load_ecs_task_definitions(
294
294
  UNWIND $Definitions AS def
295
295
  MERGE (d:ECSTaskDefinition{id: def.taskDefinitionArn})
296
296
  ON CREATE SET d.firstseen = timestamp()
297
- SET d.arn = def.taskDefinitionArn, d.region = $Region,
297
+ SET d.arn = def.taskDefinitionArn,
298
+ d.region = $Region,
298
299
  d.family = def.family,
299
300
  d.task_role_arn = def.taskRoleArn,
300
301
  d.execution_role_arn = def.executionRoleArn,
@@ -317,6 +318,11 @@ def load_ecs_task_definitions(
317
318
  d.ephemeral_storage_size_in_gib = def.ephemeralStorage.sizeInGiB,
318
319
  d.lastupdated = $aws_update_tag
319
320
  WITH d
321
+ MATCH (task:ECSTask{task_definition_arn: d.arn})
322
+ MERGE (task)-[r:HAS_TASK_DEFINITION]->(d)
323
+ ON CREATE SET r.firstseen = timestamp()
324
+ SET r.lastupdated = $aws_update_tag
325
+ WITH d
320
326
  MATCH (owner:AWSAccount{id: $AWS_ACCOUNT_ID})
321
327
  MERGE (owner)-[r:RESOURCE]->(d)
322
328
  ON CREATE SET r.firstseen = timestamp()
@@ -565,17 +571,6 @@ def sync(
565
571
  current_aws_account_id,
566
572
  update_tag,
567
573
  )
568
- task_definitions = get_ecs_task_definitions(
569
- boto3_session,
570
- region,
571
- )
572
- load_ecs_task_definitions(
573
- neo4j_session,
574
- task_definitions,
575
- region,
576
- current_aws_account_id,
577
- update_tag,
578
- )
579
574
  services = get_ecs_services(
580
575
  cluster_arn,
581
576
  boto3_session,
@@ -602,4 +597,16 @@ def sync(
602
597
  current_aws_account_id,
603
598
  update_tag,
604
599
  )
600
+ task_definitions = get_ecs_task_definitions(
601
+ boto3_session,
602
+ region,
603
+ tasks,
604
+ )
605
+ load_ecs_task_definitions(
606
+ neo4j_session,
607
+ task_definitions,
608
+ region,
609
+ current_aws_account_id,
610
+ update_tag,
611
+ )
605
612
  cleanup_ecs(neo4j_session, common_job_parameters)
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import time
3
+ from typing import Any
3
4
  from typing import Dict
4
5
  from typing import List
5
6
 
@@ -7,9 +8,8 @@ import boto3
7
8
  import botocore.exceptions
8
9
  import neo4j
9
10
 
10
- from cartography.client.core.tx import load_graph_data
11
+ from cartography.client.core.tx import load
11
12
  from cartography.graph.job import GraphJob
12
- from cartography.graph.querybuilder import build_ingestion_query
13
13
  from cartography.intel.aws.ec2.util import get_botocore_config
14
14
  from cartography.models.aws.emr import EMRClusterSchema
15
15
  from cartography.util import aws_handle_regions
@@ -24,9 +24,9 @@ DESCRIBE_SLEEP = 1
24
24
 
25
25
  @timeit
26
26
  @aws_handle_regions
27
- def get_emr_clusters(boto3_session: boto3.session.Session, region: str) -> List[Dict]:
27
+ def get_emr_clusters(boto3_session: boto3.session.Session, region: str) -> List[Dict[str, Any]]:
28
28
  client = boto3_session.client('emr', region_name=region, config=get_botocore_config())
29
- clusters: List[Dict] = []
29
+ clusters: List[Dict[str, Any]] = []
30
30
  paginator = client.get_paginator('list_clusters')
31
31
  for page in paginator.paginate():
32
32
  cluster = page['Clusters']
@@ -36,36 +36,31 @@ def get_emr_clusters(boto3_session: boto3.session.Session, region: str) -> List[
36
36
 
37
37
 
38
38
  @timeit
39
- def get_emr_describe_cluster(boto3_session: boto3.session.Session, region: str, cluster_id: str) -> Dict:
39
+ def get_emr_describe_cluster(boto3_session: boto3.session.Session, region: str, cluster_id: str) -> Dict[str, Any]:
40
40
  client = boto3_session.client('emr', region_name=region, config=get_botocore_config())
41
- cluster_details: Dict = {}
41
+ cluster_details: Dict[str, Any] = {}
42
42
  try:
43
43
  response = client.describe_cluster(ClusterId=cluster_id)
44
44
  cluster_details = response['Cluster']
45
45
  except botocore.exceptions.ClientError as e:
46
- logger.warning(
47
- "Could not run EMR describe_cluster due to boto3 error %s: %s. Skipping.",
48
- e.response['Error']['Code'],
49
- e.response['Error']['Message'],
50
- )
46
+ code = e.response['Error']['Code']
47
+ msg = e.response['Error']['Message']
48
+ logger.warning(f"Could not run EMR describe_cluster due to boto3 error {code}: {msg}. Skipping.")
51
49
  return cluster_details
52
50
 
53
51
 
54
52
  @timeit
55
53
  def load_emr_clusters(
56
54
  neo4j_session: neo4j.Session,
57
- cluster_data: List[Dict],
55
+ cluster_data: List[Dict[str, Any]],
58
56
  region: str,
59
57
  current_aws_account_id: str,
60
58
  aws_update_tag: int,
61
59
  ) -> None:
62
- logger.info("Loading EMR %d clusters for region '%s' into graph.", len(cluster_data), region)
63
-
64
- ingestion_query = build_ingestion_query(EMRClusterSchema())
65
-
66
- load_graph_data(
60
+ logger.info(f"Loading EMR {len(cluster_data)} clusters for region '{region}' into graph.")
61
+ load(
67
62
  neo4j_session,
68
- ingestion_query,
63
+ EMRClusterSchema(),
69
64
  cluster_data,
70
65
  lastupdated=aws_update_tag,
71
66
  Region=region,
@@ -74,7 +69,7 @@ def load_emr_clusters(
74
69
 
75
70
 
76
71
  @timeit
77
- def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
72
+ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
78
73
  logger.debug("Running EMR cleanup job.")
79
74
  cleanup_job = GraphJob.from_node_schema(EMRClusterSchema(), common_job_parameters)
80
75
  cleanup_job.run(neo4j_session)
@@ -83,14 +78,14 @@ def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict) -> None:
83
78
  @timeit
84
79
  def sync(
85
80
  neo4j_session: neo4j.Session, boto3_session: boto3.session.Session, regions: List[str], current_aws_account_id: str,
86
- update_tag: int, common_job_parameters: Dict,
81
+ update_tag: int, common_job_parameters: Dict[str, Any],
87
82
  ) -> None:
88
83
  for region in regions:
89
- logger.info("Syncing EMR for region '%s' in account '%s'.", region, current_aws_account_id)
84
+ logger.info(f"Syncing EMR for region '{region}' in account '{current_aws_account_id}'.")
90
85
 
91
86
  clusters = get_emr_clusters(boto3_session, region)
92
87
 
93
- cluster_data: List[Dict] = []
88
+ cluster_data: List[Dict[str, Any]] = []
94
89
  for cluster in clusters:
95
90
  cluster_id = cluster['Id']
96
91
  cluster_details = get_emr_describe_cluster(boto3_session, region, cluster_id)
@@ -309,11 +309,15 @@ def load_roles(
309
309
  neo4j_session: neo4j.Session, roles: List[Dict], current_aws_account_id: str, aws_update_tag: int,
310
310
  ) -> None:
311
311
  ingest_role = """
312
- MERGE (rnode:AWSRole{arn: $Arn})
313
- ON CREATE SET rnode:AWSPrincipal, rnode.roleid = $RoleId, rnode.firstseen = timestamp(),
314
- rnode.createdate = $CreateDate
315
- ON MATCH SET rnode.name = $RoleName, rnode.path = $Path
316
- SET rnode.lastupdated = $aws_update_tag
312
+ MERGE (rnode:AWSPrincipal{arn: $Arn})
313
+ ON CREATE SET rnode.firstseen = timestamp()
314
+ SET
315
+ rnode:AWSRole,
316
+ rnode.roleid = $RoleId,
317
+ rnode.createdate = $CreateDate,
318
+ rnode.name = $RoleName,
319
+ rnode.path = $Path,
320
+ rnode.lastupdated = $aws_update_tag
317
321
  WITH rnode
318
322
  MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID})
319
323
  MERGE (aa)-[r:RESOURCE]->(rnode)
@@ -540,11 +544,12 @@ def transform_policy_data(policy_map: Dict, policy_type: str) -> None:
540
544
  for principal_arn, policy_statement_map in policy_map.items():
541
545
  logger.debug(f"Transforming IAM {policy_type} policies for principal {principal_arn}")
542
546
  for policy_key, statements in policy_statement_map.items():
543
- policy_id = transform_policy_id(principal_arn, policy_type, policy_key) \
544
- if policy_type == PolicyType.inline.value else policy_key
545
- statements = _transform_policy_statements(
546
- statements, policy_id,
547
- )
547
+ policy_id = transform_policy_id(
548
+ principal_arn,
549
+ policy_type,
550
+ policy_key,
551
+ ) if policy_type == PolicyType.inline.value else policy_key
552
+ policy_statement_map[policy_key] = _transform_policy_statements(statements, policy_id)
548
553
 
549
554
 
550
555
  def transform_policy_id(principal_arn: str, policy_type: str, name: str) -> str:
@@ -189,7 +189,7 @@ def _load_kms_key_policies(neo4j_session: neo4j.Session, policies: List[Dict], u
189
189
 
190
190
  def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> None:
191
191
  set_defaults = """
192
- MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(kmskey:KMSKey) where NOT EXISTS(kmskey.anonymous_actions)
192
+ MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(kmskey:KMSKey) where kmskey.anonymous_actions IS NULL
193
193
  SET kmskey.anonymous_access = false, kmskey.anonymous_actions = []
194
194
  """
195
195
 
@@ -284,7 +284,7 @@ def load_rds_snapshots(
284
284
 
285
285
  neo4j_session.run(
286
286
  ingest_rds_snapshot,
287
- Snapshots=data,
287
+ Snapshots=snapshots,
288
288
  Region=region,
289
289
  AWS_ACCOUNT_ID=current_aws_account_id,
290
290
  aws_update_tag=aws_update_tag,
@@ -345,7 +345,7 @@ def _load_s3_public_access_block(
345
345
  MATCH (s:S3Bucket) where s.name = public_access_block.bucket
346
346
  SET s.block_public_acls = public_access_block.block_public_acls,
347
347
  s.ignore_public_acls = public_access_block.ignore_public_acls,
348
- s.block_public_acls = public_access_block.block_public_acls,
348
+ s.block_public_policy = public_access_block.block_public_policy,
349
349
  s.restrict_public_buckets = public_access_block.restrict_public_buckets,
350
350
  s.lastupdated = $UpdateTag
351
351
  """
@@ -359,7 +359,7 @@ def _load_s3_public_access_block(
359
359
 
360
360
  def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> None:
361
361
  set_defaults = """
362
- MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) where NOT EXISTS(s.anonymous_actions)
362
+ MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) where s.anonymous_actions IS NULL
363
363
  SET s.anonymous_access = false, s.anonymous_actions = []
364
364
  """
365
365
  neo4j_session.run(
@@ -368,7 +368,7 @@ def _set_default_values(neo4j_session: neo4j.Session, aws_account_id: str) -> No
368
368
  )
369
369
 
370
370
  set_encryption_defaults = """
371
- MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) where NOT EXISTS(s.default_encryption)
371
+ MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(s:S3Bucket) where s.default_encryption IS NULL
372
372
  SET s.default_encryption = false
373
373
  """
374
374
  neo4j_session.run(
@@ -19,8 +19,13 @@ AUTHORITY_HOST_URI = 'https://login.microsoftonline.com'
19
19
  class Credentials:
20
20
 
21
21
  def __init__(
22
- self, arm_credentials: Any, aad_graph_credentials: Any, tenant_id: str = None, subscription_id: str = None,
23
- context: adal.AuthenticationContext = None, current_user: str = None,
22
+ self,
23
+ arm_credentials: Any,
24
+ aad_graph_credentials: Any,
25
+ tenant_id: Optional[str] = None,
26
+ subscription_id: Optional[str] = None,
27
+ context: Optional[adal.AuthenticationContext] = None,
28
+ current_user: Optional[str] = None,
24
29
  ) -> None:
25
30
  self.arm_credentials = arm_credentials # Azure Resource Manager API credentials
26
31
  self.aad_graph_credentials = aad_graph_credentials # Azure AD Graph API credentials
@@ -129,7 +134,12 @@ class Authenticator:
129
134
 
130
135
  raise e
131
136
 
132
- def authenticate_sp(self, tenant_id: str = None, client_id: str = None, client_secret: str = None) -> Credentials:
137
+ def authenticate_sp(
138
+ self,
139
+ tenant_id: Optional[str] = None,
140
+ client_id: Optional[str] = None,
141
+ client_secret: Optional[str] = None,
142
+ ) -> Credentials:
133
143
  """
134
144
  Implements authentication for the Azure provider
135
145
  """
@@ -316,7 +316,7 @@ def sync_policies(
316
316
  "Syncing OCI policies for compartment '%s' in account '%s'.", compartment['ocid'], current_tenancy_id,
317
317
  )
318
318
  data = get_policy_list_data(iam, compartment["ocid"])
319
- if(data["Policies"]):
319
+ if (data["Policies"]):
320
320
  load_policies(neo4j_session, data["Policies"], current_tenancy_id, oci_update_tag)
321
321
  run_cleanup_job('oci_import_policies_cleanup.json', neo4j_session, common_job_parameters)
322
322
 
@@ -12,7 +12,7 @@ from cartography.models.core.relationships import TargetNodeMatcher
12
12
 
13
13
  @dataclass(frozen=True)
14
14
  class EMRClusterNodeProperties(CartographyNodeProperties):
15
- arn: PropertyRef = PropertyRef('ClusterArn')
15
+ arn: PropertyRef = PropertyRef('ClusterArn', extra_index=True)
16
16
  auto_terminate: PropertyRef = PropertyRef('AutoTerminate')
17
17
  autoscaling_role: PropertyRef = PropertyRef('AutoScalingRole')
18
18
  custom_ami_id: PropertyRef = PropertyRef('CustomAmiId')
@@ -8,16 +8,36 @@ class PropertyRef:
8
8
  (PropertyRef.set_in_kwargs=True).
9
9
  """
10
10
 
11
- def __init__(self, name: str, set_in_kwargs=False):
11
+ def __init__(self, name: str, set_in_kwargs=False, extra_index=False, ignore_case=False):
12
12
  """
13
13
  :param name: The name of the property
14
14
  :param set_in_kwargs: Optional. If True, the property is not defined on the data dict, and we expect to find the
15
15
  property in the kwargs.
16
16
  If False, looks for the property in the data dict.
17
17
  Defaults to False.
18
+ :param extra_index: If True, make sure that we create an index for this property name.
19
+ Notes:
20
+ - extra_index is available for the case where you anticipate a property will be queried frequently.
21
+ - The `id` and `lastupdated` properties will always have indexes created for them automatically by
22
+ `ensure_indexes()`.
23
+ - All properties included in target node matchers will always have indexes created for them.
24
+ Defaults to False.
25
+ :param ignore_case: If True, performs a case-insensitive match when comparing the value of this property during
26
+ relationship creation. Defaults to False. This only has effect as part of a TargetNodeMatcher, and this is not
27
+ supported for the sub resource relationship.
28
+ Example on why you would set this to True:
29
+ GitHub usernames can have both uppercase and lowercase characters, but GitHub itself treats usernames as
30
+ case-insensitive. Suppose your company's internal personnel database stores GitHub usernames all as
31
+ lowercase. If you wanted to map your company's employees to their GitHub identities, you would need to
32
+ perform a case-insensitive match between your company's record of a user's GitHub username and your
33
+ cartography catalog of GitHubUser nodes. Therefore, you would need `ignore_case=True` in the PropertyRef
34
+ that points to the GitHubUser node's name field, otherwise if one of your employees' GitHub usernames
35
+ contains capital letters, you would not be able to map them properly to a GitHubUser node in your graph.
18
36
  """
19
37
  self.name = name
20
38
  self.set_in_kwargs = set_in_kwargs
39
+ self.extra_index = extra_index
40
+ self.ignore_case = ignore_case
21
41
 
22
42
  def _parameterize_name(self) -> str:
23
43
  return f"${self.name}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cartography
3
- Version: 0.73.1
3
+ Version: 0.75.0
4
4
  Summary: Explore assets and their relationships across your technical infrastructure.
5
5
  Home-page: https://www.github.com/lyft/cartography
6
6
  Maintainer: Lyft
@@ -10,27 +10,27 @@ cartography/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
10
10
  cartography/client/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  cartography/client/aws/iam.py,sha256=dYsGikc36DEsSeR2XVOVFFUDwuU9yWj_EVkpgVYCFgM,1293
12
12
  cartography/client/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- cartography/client/core/tx.py,sha256=skfsvpEO9UgLPUreuKnVh4jYmdMW8-ivf-dX0H3jruI,8815
13
+ cartography/client/core/tx.py,sha256=4_kTBxrtlwsOM-e8Xtjf7wmmzwZ-DGRJL0rPFp0Xj0Q,10805
14
14
  cartography/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- cartography/data/indexes.cypher,sha256=lWqq3olY3vLNNucH3uZAVQsdvqluUkQjP7o1L-FxNBw,30369
15
+ cartography/data/indexes.cypher,sha256=7whG4zv9GpDwqhf46w6NN3RrJgRiaVj9es_2sMOegrM,30253
16
16
  cartography/data/permission_relationships.yaml,sha256=RuKGGc_3ZUQ7ag0MssB8k_zaonCkVM5E8I_svBWTmGc,969
17
17
  cartography/data/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  cartography/data/jobs/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- cartography/data/jobs/analysis/aws_ec2_asset_exposure.json,sha256=0QUmQOx3Zsde9drJpup3Tx_glYw5iwsRkbjIH2dGmuE,2734
19
+ cartography/data/jobs/analysis/aws_ec2_asset_exposure.json,sha256=PRM7TJwO3gLkGtez-hg5v5avDkuAcF-bQQCK6stecLg,2726
20
20
  cartography/data/jobs/analysis/aws_ec2_iaminstance.json,sha256=98wfe8pjwS-iQIjUpM1rxuASdxk7BGl9ooigrwfpmvU,500
21
21
  cartography/data/jobs/analysis/aws_ec2_iaminstanceprofile.json,sha256=7M8k0p0SvSYVzCk358FnUQ865IX25TyumtHTXqZS-t8,526
22
- cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json,sha256=UTEIirvD7lljIcwC_QRIsbGG_Mlg9yg02yx66PCjHC8,1735
23
- cartography/data/jobs/analysis/aws_eks_asset_exposure.json,sha256=69jZjghVao-COLoiUaHdD0ClbIcLtt_4Xx90IdVsWP8,557
24
- cartography/data/jobs/analysis/aws_foreign_accounts.json,sha256=hMmvFH5PTt_j5IlgNT5pE6n8pEejFaySUtmeuSA2rkM,682
22
+ cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json,sha256=_ZBczunZbx5NHAbfVc4v6vJsbyl1xaePLzj4cyxW-4A,1741
23
+ cartography/data/jobs/analysis/aws_eks_asset_exposure.json,sha256=Z6z4YTNJJqUJl2JqONeAYAvfH_2A9qEBxkFn-aou8D8,561
24
+ cartography/data/jobs/analysis/aws_foreign_accounts.json,sha256=b8Li_KQLwIvNXxWt0F1bVTV1Vg9dxsbr7ZE8LH2-woc,686
25
25
  cartography/data/jobs/analysis/aws_lambda_ecr.json,sha256=wM10Gn0HoNP-sOj3S_Sjqh4mLsh-f2QkonkFuOohs_U,641
26
26
  cartography/data/jobs/analysis/aws_s3acl_analysis.json,sha256=EDf1VhDoP7khhGf1QTI9GIsp08foyPL4wE9mB9JQDH4,2493
27
- cartography/data/jobs/analysis/gcp_compute_asset_inet_exposure.json,sha256=cGN8eiID7NcyT0u96h_HKJxJBkIHMoqg5KFH9iG8uio,4671
28
- cartography/data/jobs/analysis/gcp_gke_asset_exposure.json,sha256=j7yPfLC74jQNZl73V2z3P-sWO2nj2gRJhE5yx7pH_qg,639
29
- cartography/data/jobs/analysis/gcp_gke_basic_auth.json,sha256=31gtyp5Yqgcfg16g6PndL5anOOS_oMX-L2e4vQYqbRY,669
27
+ cartography/data/jobs/analysis/gcp_compute_asset_inet_exposure.json,sha256=lIurpVOAHZ3u_pfpRhcC5zprxXKqOWG4miGIkMeCfcE,4695
28
+ cartography/data/jobs/analysis/gcp_gke_asset_exposure.json,sha256=7SJc9TeeIWFMDmHOWgmjgaIzjmCClLjJTPS4bGlaEF0,643
29
+ cartography/data/jobs/analysis/gcp_gke_basic_auth.json,sha256=qLkrw1eZvV9ETtkIQN3v9hXnYN3ujAMyfIpqUj5YGo8,681
30
30
  cartography/data/jobs/analysis/gsuite_human_link.json,sha256=ArWA51fNIeeXwYiroJbKnuqN8y-TLrndOq0ZdC9q5os,541
31
31
  cartography/data/jobs/cleanup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  cartography/data/jobs/cleanup/aws_account_cleanup.json,sha256=DEB4h6Z4NSpYaLXECPhNk8HHbT0XNlqUA01pXIskBxc,473
33
- cartography/data/jobs/cleanup/aws_apigateway_details.json,sha256=QuE7M0jsrysQ1bi6Bef0Q92aR9AGSfJq_Uw7aAHtbck,319
33
+ cartography/data/jobs/cleanup/aws_apigateway_details.json,sha256=rh7cMTebjyUoEu_s7nKl3m-GLQ3gXgw0s3kMJfWKjro,323
34
34
  cartography/data/jobs/cleanup/aws_dns_cleanup.json,sha256=H5uMZV4tN6HcjTYwh2I1dENzNGqu2D2Rs2q-5CK5e0Y,3498
35
35
  cartography/data/jobs/cleanup/aws_import_account_access_key_cleanup.json,sha256=dZBpz_yY1M_XpEueAjFq14rf2HMu0paUibsaXrWlF3w,305
36
36
  cartography/data/jobs/cleanup/aws_import_apigateway_cleanup.json,sha256=wCV95ydo3dmlhK7VrDrxCqrP6dbhCCMTzcz_qaJQ4Jo,2189
@@ -81,9 +81,9 @@ cartography/data/jobs/cleanup/aws_ingest_load_balancers_cleanup.json,sha256=1vid
81
81
  cartography/data/jobs/cleanup/aws_ingest_load_balancers_v2_cleanup.json,sha256=kIMc5YTkWDOLdHCOAHPb7Q8Oz9pP6GzGO9gjBo0ZhR0,1596
82
82
  cartography/data/jobs/cleanup/aws_ingest_network_interfaces_cleanup.json,sha256=Qkx0VhiEwUqc_6Jq6U5ov7VEPximGy1y3MxsZZjIoog,2241
83
83
  cartography/data/jobs/cleanup/aws_ingest_subnets_cleanup.json,sha256=HtTXrA50PzBJ6LFnKZkO4hnnKxKqF6xUwqUrtFrp29Y,527
84
- cartography/data/jobs/cleanup/aws_kms_details.json,sha256=J0NNFA7WBhDDRnDetHl6DVUtr4VoGdcyspTBCjVtpAw,315
84
+ cartography/data/jobs/cleanup/aws_kms_details.json,sha256=awluz0DhiAcdABiTYL9M8EUQz2FAuXVpu8lICRpTEnU,319
85
85
  cartography/data/jobs/cleanup/aws_post_ingestion_principals_cleanup.json,sha256=h3A6jNk7DGLFq6ob06-V9qzuYN-CltyHq_r2Gj8DqCI,266
86
- cartography/data/jobs/cleanup/aws_s3_details.json,sha256=LmiIH2KfLNIG6opFmEp88ebXqrW_3Mop4xMrIPqsiIc,312
86
+ cartography/data/jobs/cleanup/aws_s3_details.json,sha256=-GEnBicRjLBNEDS2k2MzE2PUFbQ5WmxXUbYeVTvlJzQ,316
87
87
  cartography/data/jobs/cleanup/azure_cosmosdb_cassandra_keyspace_cleanup.json,sha256=_WIFvR-OGvG2q163NKsNXPqbDATz7rv7FLUTlWtnvK4,1358
88
88
  cartography/data/jobs/cleanup/azure_cosmosdb_cors_details.json,sha256=meF4119r0TzUwiU0ecygJRc2ZikSFJ9SSCKiayEXRCU,662
89
89
  cartography/data/jobs/cleanup/azure_cosmosdb_mongodb_database_cleanup.json,sha256=7PLCkq5HWvQ43ct3dZ0biUaOu7NcYPAMP5WAFpyhSlw,1354
@@ -141,37 +141,37 @@ cartography/driftdetect/shortcut.py,sha256=0AvX9vZGNRWeLiRqxU7eOOo77gcfufHx4IHZv
141
141
  cartography/driftdetect/storage.py,sha256=6ziscwJh34_1ylrSAK6B-U6o09xDhIa7Yb-V1kisveQ,1549
142
142
  cartography/driftdetect/util.py,sha256=Lqxv8QoFn3_3Fz18qCOjkjJ6yBwgrHjrxXmArBAEdkc,929
143
143
  cartography/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
144
- cartography/graph/cleanupbuilder.py,sha256=jmnA5XfbGtW_knzVly5mLOLLfbfCnY-xCSBV7BAcM1o,8954
144
+ cartography/graph/cleanupbuilder.py,sha256=Kj6vNQYGlwrNuz2bExgtZzeXMBLoasHNTgqrRiALPgM,7760
145
145
  cartography/graph/context.py,sha256=RGxGb8EnxowcqjR0nFF86baNhgRHeUF9wjIoFUoG8LU,1230
146
- cartography/graph/job.py,sha256=NzYvl04E8piozVhe_WMucG2FH6mQ6vdoCdilDcoVVlU,7445
147
- cartography/graph/querybuilder.py,sha256=LcQa1yE8d_AIvGeWFjl9nmYLlQ1ZDMZG_TOUfskJ3DM,16744
148
- cartography/graph/statement.py,sha256=cgPOAkuWjJvkWnwr0YwvzMiJwvbcPNpkWWmrdJlm61I,5124
146
+ cartography/graph/job.py,sha256=VBKc0VLbDz1zm5jslF49nbPbQS7DkdQwfPG7rdLSc1w,7288
147
+ cartography/graph/querybuilder.py,sha256=MMXzUEg4td-YmHMNM97KAqDZ6-1wNClO2jmJoG47BTY,20108
148
+ cartography/graph/statement.py,sha256=VsqG46ty_Mm87fr8YdIwfr6a82OUXU7yZe6S-Py9hZg,5345
149
149
  cartography/intel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
150
  cartography/intel/analysis.py,sha256=gHtN42NqqLL1G5MOm2Q6rMyg-V5lU_wqbnKp5hbOOao,1499
151
151
  cartography/intel/create_indexes.py,sha256=HM2jB2v3NZvGqgVDtNoBQRVpkei_JXcYXqM14w8Rjss,741
152
152
  cartography/intel/dns.py,sha256=M-WSiGQoxWZsl0sg-2SDR8OD8e1Rexkt2Tbb2OpeioA,5596
153
153
  cartography/intel/aws/__init__.py,sha256=q98Jaw0nNI6QKUcp36ZuMs9C-ISlfo8OoPYTVoRf9eY,10295
154
- cartography/intel/aws/apigateway.py,sha256=4PzbIxICwaRq5-J4WBVXAYGSABjkAB_2EJDzJogYN2c,12768
154
+ cartography/intel/aws/apigateway.py,sha256=se0plc9ZnkGQAEtlfqgrJGTa2JeWljvwC3E3JFM7YbI,12764
155
155
  cartography/intel/aws/config.py,sha256=wrZbz7bc8vImLmRvTLkPcWnjjPzk3tOG4bB_BFS2lq8,7329
156
156
  cartography/intel/aws/dynamodb.py,sha256=OrFHKNrYDJzyMjkgFW4K4suy01OCP9E3Yqu_rnlsYXw,5106
157
157
  cartography/intel/aws/ecr.py,sha256=xTYnefMyt4_dr903rBPvknVoH4S7NTrNe0WWEJHfEuo,5988
158
- cartography/intel/aws/ecs.py,sha256=cnXSnC73J0LPBzHDe17nnxE_8Cw-gtatRFuk662xzPQ,23566
158
+ cartography/intel/aws/ecs.py,sha256=gulrIZ--iEFLpkkPH58MJIkctsxWeWdO2ofM9amDNZA,23654
159
159
  cartography/intel/aws/eks.py,sha256=6ey28ifnPvGMReVvFczBWBwBBSyrUWpuBiv8-NahNwY,4118
160
160
  cartography/intel/aws/elasticache.py,sha256=fCI47aDFmIDyE26GiReKYb6XIZUwrzcvsXBQ4ruFhuI,4427
161
161
  cartography/intel/aws/elasticsearch.py,sha256=ZL7MkXF_bXRSoXuDSI1dwGckRLG2zDB8LuAD07vSLnE,8374
162
- cartography/intel/aws/emr.py,sha256=Iqm1U7iEHAK8vB25i0631YqYRKZxfV-UUQh8_Cnaxh8,3437
163
- cartography/intel/aws/iam.py,sha256=chLz5upvM7r6f2-bK2TtHIbBLCqAQ3oNNApHIkVrrYk,31361
162
+ cartography/intel/aws/emr.py,sha256=xhWBVZngxJRFjMEDxwq3G6SgytRGLq0v2a_CeDvByR0,3372
163
+ cartography/intel/aws/iam.py,sha256=GhiRzsw0S87fdLDMVPdnhuFNArQW6C22VJM4DP5_wCw,31425
164
164
  cartography/intel/aws/inspector.py,sha256=mL57NQrpOMF0pvbqtxGIn_NG6-yM5D0opoXoxyhMR1Y,12491
165
- cartography/intel/aws/kms.py,sha256=bvwuUQBi-dFZqlRsyVdc4ov7-YD7c806xfe0MyUHCF8,11735
165
+ cartography/intel/aws/kms.py,sha256=bZUzMxAH_DsAcGTJBs08gg2tLKYu-QWjvMvV9C-6v50,11731
166
166
  cartography/intel/aws/lambda_function.py,sha256=KKTyn53xpaMI9WvIqxmsOASFwflHt-2_5ow-zUFc2wg,9890
167
167
  cartography/intel/aws/organizations.py,sha256=HaQZ3J5XF15BuykuDypqFORDYpnoHuRRr4DuceewH4s,4485
168
168
  cartography/intel/aws/permission_relationships.py,sha256=PhOnag0a1gZHtUg82546MKhj-8IcGJ7wLbvPASUBXlg,14792
169
- cartography/intel/aws/rds.py,sha256=k8ccugX7LVx8BH37xWlsLOtiVqJtmHWeJu9eBfk2QYs,25247
169
+ cartography/intel/aws/rds.py,sha256=vnlNYmrO2Cc0PNn31CeG2QwYhwjVosbQFE9Ol1vQyLE,25252
170
170
  cartography/intel/aws/redshift.py,sha256=KOqiXIllHmtPTeaNGl-cX4srY5pFE6o12j8MQ5-zWpc,6694
171
171
  cartography/intel/aws/resourcegroupstaggingapi.py,sha256=aq4kPF6t8QZZoTxdkQVLXH65Di41CDJVM9llJNe6iaY,10278
172
172
  cartography/intel/aws/resources.py,sha256=exmPQXk9V75ubwgzL7sksVI9mKIdfEbNSSXGW206fvg,3181
173
173
  cartography/intel/aws/route53.py,sha256=IYqeQud1HuHnf11A7T-Jeif5DWgjpaaU-Jfr2cLUc_o,14099
174
- cartography/intel/aws/s3.py,sha256=gib7MgqzYMgWNbPlUvMAv1wjzB6qF45V96lCnMaOArY,26385
174
+ cartography/intel/aws/s3.py,sha256=6hBtQKrclOpEmKQx0fYQHfjCjssLz2qZQbfB-tpHPeI,26381
175
175
  cartography/intel/aws/secretsmanager.py,sha256=zhgQS-Js8Co_9UIothN-uKQvg8oqLE3kdgoQRvDDUVM,3272
176
176
  cartography/intel/aws/securityhub.py,sha256=8FF7vW0ykdqn07xGExtsOLxYTyCTTbDiRuA1pxiRNlM,2266
177
177
  cartography/intel/aws/sqs.py,sha256=cosScBKxAm_6GsM9zzg4U12KvAjXUzxpJ1zGv0lsVZI,6199
@@ -206,7 +206,7 @@ cartography/intel/azure/storage.py,sha256=56QyHafWTNproCOjITyAaWsmsqKhyFwJQlVEfw
206
206
  cartography/intel/azure/subscription.py,sha256=aEyCYYWD4tFG5hBu_na7B4bohzlgiRr2aQ2B1YJ5bWw,3409
207
207
  cartography/intel/azure/tenant.py,sha256=7Tz85e2isPRsq5krScQLdh_dYubM_pzOtj_sRx-nyDE,1454
208
208
  cartography/intel/azure/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
209
- cartography/intel/azure/util/credentials.py,sha256=aDoS-VJDjBC7X-IPhS7BYHAD4jL57A3FfaV7LKUiK2I,7223
209
+ cartography/intel/azure/util/credentials.py,sha256=99PjTs0vZ2iu0tHD7TohN1VJYjuXYstfMg27F4CE0xU,7416
210
210
  cartography/intel/crowdstrike/__init__.py,sha256=dAtgI-0vZAQZ3cTFQhMEzzt7aqiNSNuiIYiw0qbut-I,1896
211
211
  cartography/intel/crowdstrike/endpoints.py,sha256=o45309ZDNdlYdG0YicIiBain-nkr3bQahBaRx3N9Mow,3738
212
212
  cartography/intel/crowdstrike/spotlight.py,sha256=yNhj44-RYF6ubck-hHMKhKiNU0fCfhQf4Oagopc31EM,4754
@@ -241,7 +241,7 @@ cartography/intel/kubernetes/secrets.py,sha256=oaX90O4SFfrqObEzNaXCREiz98I5Y7tMG
241
241
  cartography/intel/kubernetes/services.py,sha256=kzx8QoB_p5q6syAl7SZscGxubYEdWDdmWu5-ZZUg7YM,3516
242
242
  cartography/intel/kubernetes/util.py,sha256=pal5MyGrPjShqml_wdEIM8h7M2hMacVM5FA39wg2Dvs,1512
243
243
  cartography/intel/oci/__init__.py,sha256=AZmRX6EO4LUnynDtIKHxtZ_Ab2-CYPPc2u5d0Q2S3S4,7926
244
- cartography/intel/oci/iam.py,sha256=tR-fp8_QwcikhIQoPC3SgUY6bXvi-GqdheAF2OHR6NY,17711
244
+ cartography/intel/oci/iam.py,sha256=zPrJeoMoO3ZkjBfWbTttjrcUvxxMuWquLTmsDH5MgOI,17712
245
245
  cartography/intel/oci/organizations.py,sha256=tzQkZfE4LPoS-6lXBRQGyhq8aJLZUJ1_q75Q9eTBke0,4086
246
246
  cartography/intel/oci/utils.py,sha256=UbX9jib4sWEdKeAt2CeCo4k9shUiWY08oTfQz_nDvjA,3223
247
247
  cartography/intel/okta/__init__.py,sha256=HYw9wlE27dHJ2fwSlHgbJyHcxhdFzbYWBcZdQ6bqfIo,3813
@@ -264,15 +264,15 @@ cartography/intel/pagerduty/users.py,sha256=oltGssxrnzYsV6QTGP1SsPoA1rCUDStj6vGl
264
264
  cartography/intel/pagerduty/vendors.py,sha256=WlDHExrWRBegDQKtxBV5nJiYgwoTLxNee4HrQDJ-Pdg,1559
265
265
  cartography/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
266
266
  cartography/models/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
267
- cartography/models/aws/emr.py,sha256=gr9ddEv4P4pRdceYNZTPX5h3864wD_MTvohOxW9_TrU,3019
267
+ cartography/models/aws/emr.py,sha256=TkuwoZnw_VHbJ5bwkac7-ZfwSLe_TeK3gxkuwGQOUk4,3037
268
268
  cartography/models/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
269
- cartography/models/core/common.py,sha256=7oWLQrGF-AFiWcfb6zUx5QhAIFmj0iq6TzKzDDAqWE0,1819
269
+ cartography/models/core/common.py,sha256=twXdP5gprpmDJYpBmSYL5GWaWDu1Xn-0EVzFsRJ42QQ,3583
270
270
  cartography/models/core/nodes.py,sha256=h5dwBOk_a2uCHZWeQz3pidr7gkqMKf7buIZgl6M1Ox4,3699
271
271
  cartography/models/core/relationships.py,sha256=6AwXvk0dq48BxqyxBpHyBXZ3dJNm65t1y4vNg4n25uA,5103
272
- cartography-0.73.1.dist-info/LICENSE,sha256=489ZXeW9G90up6ep-D1n-lJgk9ciNT2yxXpFgRSidtk,11341
273
- cartography-0.73.1.dist-info/METADATA,sha256=prRlYzJYE12EgcRKE0l95TqF-3giTx0mMaZqgGM2xoY,1988
274
- cartography-0.73.1.dist-info/NOTICE,sha256=YOGAsjFtbyKj5tslYIg6V5jEYRuEvnSsIuDOUKj0Qj4,97
275
- cartography-0.73.1.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
276
- cartography-0.73.1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
277
- cartography-0.73.1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
278
- cartography-0.73.1.dist-info/RECORD,,
272
+ cartography-0.75.0.dist-info/LICENSE,sha256=489ZXeW9G90up6ep-D1n-lJgk9ciNT2yxXpFgRSidtk,11341
273
+ cartography-0.75.0.dist-info/METADATA,sha256=Wpv0TT2-zbALE2LE5ds3jjWo9rFs65VrfckhnWMHC9s,1988
274
+ cartography-0.75.0.dist-info/NOTICE,sha256=YOGAsjFtbyKj5tslYIg6V5jEYRuEvnSsIuDOUKj0Qj4,97
275
+ cartography-0.75.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
276
+ cartography-0.75.0.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
277
+ cartography-0.75.0.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
278
+ cartography-0.75.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.40.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5