cartography 0.104.0rc3__py3-none-any.whl → 0.106.0rc1__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 +2 -2
- cartography/cli.py +26 -1
- cartography/client/aws/__init__.py +19 -0
- cartography/client/aws/ecr.py +51 -0
- cartography/config.py +8 -0
- cartography/data/indexes.cypher +0 -37
- cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json +1 -1
- cartography/graph/cleanupbuilder.py +151 -41
- cartography/intel/aws/acm.py +124 -0
- cartography/intel/aws/cloudtrail.py +3 -38
- cartography/intel/aws/ecr.py +8 -2
- cartography/intel/aws/ecs.py +228 -380
- cartography/intel/aws/efs.py +99 -11
- cartography/intel/aws/iam.py +1 -1
- cartography/intel/aws/identitycenter.py +14 -3
- cartography/intel/aws/inspector.py +106 -53
- cartography/intel/aws/lambda_function.py +1 -1
- cartography/intel/aws/rds.py +2 -1
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/s3.py +195 -4
- cartography/intel/aws/sqs.py +36 -90
- cartography/intel/entra/__init__.py +22 -0
- cartography/intel/entra/applications.py +366 -0
- cartography/intel/entra/groups.py +151 -0
- cartography/intel/entra/ou.py +21 -5
- cartography/intel/kubernetes/__init__.py +30 -14
- cartography/intel/kubernetes/clusters.py +86 -0
- cartography/intel/kubernetes/namespaces.py +59 -57
- cartography/intel/kubernetes/pods.py +140 -77
- cartography/intel/kubernetes/secrets.py +95 -45
- cartography/intel/kubernetes/services.py +131 -67
- cartography/intel/kubernetes/util.py +125 -14
- cartography/intel/trivy/__init__.py +161 -0
- cartography/intel/trivy/scanner.py +363 -0
- cartography/models/aws/acm/__init__.py +0 -0
- cartography/models/aws/acm/certificate.py +75 -0
- cartography/models/aws/cloudtrail/trail.py +24 -0
- cartography/models/aws/ecs/__init__.py +0 -0
- cartography/models/aws/ecs/clusters.py +64 -0
- cartography/models/aws/ecs/container_definitions.py +93 -0
- cartography/models/aws/ecs/container_instances.py +84 -0
- cartography/models/aws/ecs/containers.py +80 -0
- cartography/models/aws/ecs/services.py +117 -0
- cartography/models/aws/ecs/task_definitions.py +97 -0
- cartography/models/aws/ecs/tasks.py +110 -0
- cartography/models/aws/efs/file_system.py +60 -0
- cartography/models/aws/efs/mount_target.py +29 -2
- cartography/models/aws/s3/notification.py +24 -0
- cartography/models/aws/secretsmanager/secret_version.py +0 -2
- cartography/models/aws/sqs/__init__.py +0 -0
- cartography/models/aws/sqs/queue.py +89 -0
- cartography/models/core/nodes.py +15 -2
- cartography/models/entra/app_role_assignment.py +115 -0
- cartography/models/entra/application.py +47 -0
- cartography/models/entra/group.py +91 -0
- cartography/models/kubernetes/__init__.py +0 -0
- cartography/models/kubernetes/clusters.py +26 -0
- cartography/models/kubernetes/containers.py +108 -0
- cartography/models/kubernetes/namespaces.py +51 -0
- cartography/models/kubernetes/pods.py +80 -0
- cartography/models/kubernetes/secrets.py +79 -0
- cartography/models/kubernetes/services.py +108 -0
- cartography/models/trivy/__init__.py +0 -0
- cartography/models/trivy/findings.py +66 -0
- cartography/models/trivy/fix.py +66 -0
- cartography/models/trivy/package.py +71 -0
- cartography/sync.py +2 -0
- cartography/util.py +15 -10
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/METADATA +3 -2
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/RECORD +74 -40
- cartography/data/jobs/cleanup/kubernetes_import_cleanup.json +0 -70
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.104.0rc3.dist-info → cartography-0.106.0rc1.dist-info}/top_level.txt +0 -0
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
20
|
+
__version__ = version = '0.106.0rc1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 106, 0, 'rc1')
|
cartography/cli.py
CHANGED
|
@@ -637,6 +637,24 @@ class CLI:
|
|
|
637
637
|
"Required if you are using the Anthropic intel module. Ignored otherwise."
|
|
638
638
|
),
|
|
639
639
|
)
|
|
640
|
+
parser.add_argument(
|
|
641
|
+
"--trivy-s3-bucket",
|
|
642
|
+
type=str,
|
|
643
|
+
default=None,
|
|
644
|
+
help=(
|
|
645
|
+
"The S3 bucket name containing Trivy scan results. "
|
|
646
|
+
"Required if you are using the Trivy module. Ignored otherwise."
|
|
647
|
+
),
|
|
648
|
+
)
|
|
649
|
+
parser.add_argument(
|
|
650
|
+
"--trivy-s3-prefix",
|
|
651
|
+
type=str,
|
|
652
|
+
default=None,
|
|
653
|
+
help=(
|
|
654
|
+
"The S3 prefix path containing Trivy scan results. "
|
|
655
|
+
"Required if you are using the Trivy module. Ignored otherwise."
|
|
656
|
+
),
|
|
657
|
+
)
|
|
640
658
|
|
|
641
659
|
return parser
|
|
642
660
|
|
|
@@ -914,7 +932,7 @@ class CLI:
|
|
|
914
932
|
config.snipeit_token = os.environ.get("SNIPEIT_TOKEN")
|
|
915
933
|
else:
|
|
916
934
|
logger.warning("A SnipeIT base URI was provided but a token was not.")
|
|
917
|
-
config.
|
|
935
|
+
config.snipeit_token = None
|
|
918
936
|
else:
|
|
919
937
|
logger.warning("A SnipeIT base URI was not provided.")
|
|
920
938
|
config.snipeit_base_uri = None
|
|
@@ -955,6 +973,13 @@ class CLI:
|
|
|
955
973
|
else:
|
|
956
974
|
config.anthropic_apikey = None
|
|
957
975
|
|
|
976
|
+
# Trivy config
|
|
977
|
+
if config.trivy_s3_bucket:
|
|
978
|
+
logger.debug(f"Trivy S3 bucket: {config.trivy_s3_bucket}")
|
|
979
|
+
|
|
980
|
+
if config.trivy_s3_prefix:
|
|
981
|
+
logger.debug(f"Trivy S3 prefix: {config.trivy_s3_prefix}")
|
|
982
|
+
|
|
958
983
|
# Run cartography
|
|
959
984
|
try:
|
|
960
985
|
return cartography.sync.run_with_config(self.sync, config)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
|
|
5
|
+
from cartography.client.core.tx import read_list_of_values_tx
|
|
6
|
+
from cartography.util import timeit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@timeit
|
|
10
|
+
def list_accounts(neo4j_session: neo4j.Session) -> List[str]:
|
|
11
|
+
"""
|
|
12
|
+
:param neo4j_session: The neo4j session object.
|
|
13
|
+
:return: A list of all AWS account IDs in the graph
|
|
14
|
+
"""
|
|
15
|
+
# See https://community.neo4j.com/t/extract-list-of-nodes-and-labels-from-path/13665/4
|
|
16
|
+
query = """
|
|
17
|
+
MATCH (a:AWSAccount) RETURN a.id
|
|
18
|
+
"""
|
|
19
|
+
return neo4j_session.read_transaction(read_list_of_values_tx, query)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Set
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
|
|
6
|
+
from cartography.client.core.tx import read_list_of_tuples_tx
|
|
7
|
+
from cartography.util import timeit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@timeit
|
|
11
|
+
def get_ecr_images(
|
|
12
|
+
neo4j_session: neo4j.Session, aws_account_id: str
|
|
13
|
+
) -> Set[Tuple[str, str, str, str, str]]:
|
|
14
|
+
"""
|
|
15
|
+
Queries the graph for all ECR images and their parent images.
|
|
16
|
+
Returns 5-tuples of ECR repository regions, tags, URIs, names, and binary digests. This is used to identify which
|
|
17
|
+
images to scan.
|
|
18
|
+
:param neo4j_session: The neo4j session object.
|
|
19
|
+
:param aws_account_id: The AWS account ID to get ECR repo data for.
|
|
20
|
+
:return: 5-tuples of repo region, image tag, image URI, repo_name, and image_digest.
|
|
21
|
+
"""
|
|
22
|
+
# See https://community.neo4j.com/t/extract-list-of-nodes-and-labels-from-path/13665/4
|
|
23
|
+
query = """
|
|
24
|
+
MATCH (e1:ECRRepositoryImage)<-[:REPO_IMAGE]-(repo:ECRRepository)
|
|
25
|
+
MATCH (repo)<-[:RESOURCE]-(:AWSAccount {id: $AWS_ID})
|
|
26
|
+
|
|
27
|
+
// OPTIONAL traversal of parent hierarchy
|
|
28
|
+
OPTIONAL MATCH path = (e1)-[:PARENT*1..]->(ancestor:ECRRepositoryImage)
|
|
29
|
+
WITH e1,
|
|
30
|
+
CASE
|
|
31
|
+
WHEN path IS NULL THEN [e1]
|
|
32
|
+
ELSE [n IN nodes(path) | n] + [e1]
|
|
33
|
+
END AS repo_img_collection_unflattened
|
|
34
|
+
|
|
35
|
+
// Flatten and dedupe
|
|
36
|
+
UNWIND repo_img_collection_unflattened AS repo_img
|
|
37
|
+
WITH DISTINCT repo_img
|
|
38
|
+
|
|
39
|
+
// Match image metadata
|
|
40
|
+
MATCH (er:ECRRepository)-[:REPO_IMAGE]->(repo_img)-[:IMAGE]->(img:ECRImage)
|
|
41
|
+
|
|
42
|
+
RETURN DISTINCT
|
|
43
|
+
er.region AS region,
|
|
44
|
+
repo_img.tag AS tag,
|
|
45
|
+
repo_img.id AS uri,
|
|
46
|
+
er.name AS repo_name,
|
|
47
|
+
img.digest AS digest
|
|
48
|
+
"""
|
|
49
|
+
return neo4j_session.read_transaction(
|
|
50
|
+
read_list_of_tuples_tx, query, AWS_ID=aws_account_id
|
|
51
|
+
)
|
cartography/config.py
CHANGED
|
@@ -137,6 +137,10 @@ class Config:
|
|
|
137
137
|
:param openai_org_id: OpenAI organization id. Optional.
|
|
138
138
|
:type anthropic_apikey: string
|
|
139
139
|
:param anthropic_apikey: Anthropic API key. Optional.
|
|
140
|
+
:type trivy_s3_bucket: str
|
|
141
|
+
:param trivy_s3_bucket: The S3 bucket name containing Trivy scan results. Optional.
|
|
142
|
+
:type trivy_s3_prefix: str
|
|
143
|
+
:param trivy_s3_prefix: The S3 prefix path containing Trivy scan results. Optional.
|
|
140
144
|
"""
|
|
141
145
|
|
|
142
146
|
def __init__(
|
|
@@ -209,6 +213,8 @@ class Config:
|
|
|
209
213
|
openai_apikey=None,
|
|
210
214
|
openai_org_id=None,
|
|
211
215
|
anthropic_apikey=None,
|
|
216
|
+
trivy_s3_bucket=None,
|
|
217
|
+
trivy_s3_prefix=None,
|
|
212
218
|
):
|
|
213
219
|
self.neo4j_uri = neo4j_uri
|
|
214
220
|
self.neo4j_user = neo4j_user
|
|
@@ -278,3 +284,5 @@ class Config:
|
|
|
278
284
|
self.openai_apikey = openai_apikey
|
|
279
285
|
self.openai_org_id = openai_org_id
|
|
280
286
|
self.anthropic_apikey = anthropic_apikey
|
|
287
|
+
self.trivy_s3_bucket = trivy_s3_bucket
|
|
288
|
+
self.trivy_s3_prefix = trivy_s3_prefix
|
cartography/data/indexes.cypher
CHANGED
|
@@ -99,21 +99,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:ECRRepositoryImage) ON (n.tag);
|
|
|
99
99
|
CREATE INDEX IF NOT EXISTS FOR (n:ECRRepositoryImage) ON (n.lastupdated);
|
|
100
100
|
CREATE INDEX IF NOT EXISTS FOR (n:ECRScanFinding) ON (n.id);
|
|
101
101
|
CREATE INDEX IF NOT EXISTS FOR (n:ECRScanFinding) ON (n.lastupdated);
|
|
102
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSCluster) ON (n.id);
|
|
103
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSCluster) ON (n.lastupdated);
|
|
104
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainerInstance) ON (n.id);
|
|
105
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainerInstance) ON (n.lastupdated);
|
|
106
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSService) ON (n.id);
|
|
107
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSService) ON (n.lastupdated);
|
|
108
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.id);
|
|
109
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.arn);
|
|
110
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSTaskDefinition) ON (n.lastupdated);
|
|
111
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSTask) ON (n.id);
|
|
112
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSTask) ON (n.lastupdated);
|
|
113
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainerDefinition) ON (n.id);
|
|
114
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainerDefinition) ON (n.lastupdated);
|
|
115
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainer) ON (n.id);
|
|
116
|
-
CREATE INDEX IF NOT EXISTS FOR (n:ECSContainer) ON (n.lastupdated);
|
|
117
102
|
CREATE INDEX IF NOT EXISTS FOR (n:ElasticacheCluster) ON (n.id);
|
|
118
103
|
CREATE INDEX IF NOT EXISTS FOR (n:ElasticacheCluster) ON (n.arn);
|
|
119
104
|
CREATE INDEX IF NOT EXISTS FOR (n:ElasticacheCluster) ON (n.lastupdated);
|
|
@@ -227,9 +212,6 @@ CREATE INDEX IF NOT EXISTS FOR (n:OCITenancy) ON (n.lastupdated);
|
|
|
227
212
|
CREATE INDEX IF NOT EXISTS FOR (n:OCIUser) ON (n.ocid);
|
|
228
213
|
CREATE INDEX IF NOT EXISTS FOR (n:OCIUser) ON (n.name);
|
|
229
214
|
CREATE INDEX IF NOT EXISTS FOR (n:OCIUser) ON (n.lastupdated);
|
|
230
|
-
CREATE INDEX IF NOT EXISTS FOR (n:Package) ON (n.id);
|
|
231
|
-
CREATE INDEX IF NOT EXISTS FOR (n:Package) ON (n.name);
|
|
232
|
-
CREATE INDEX IF NOT EXISTS FOR (n:Package) ON (n.lastupdated);
|
|
233
215
|
CREATE INDEX IF NOT EXISTS FOR (n:PagerDutyEscalationPolicy) ON (n.id);
|
|
234
216
|
CREATE INDEX IF NOT EXISTS FOR (n:PagerDutyEscalationPolicy) ON (n.name);
|
|
235
217
|
CREATE INDEX IF NOT EXISTS FOR (n:PagerDutyEscalationPolicy) ON (n.lastupdated);
|
|
@@ -378,22 +360,3 @@ CREATE INDEX IF NOT EXISTS FOR (n:AzureDisk) ON (n.id);
|
|
|
378
360
|
CREATE INDEX IF NOT EXISTS FOR (n:AzureDisk) ON (n.lastupdated);
|
|
379
361
|
CREATE INDEX IF NOT EXISTS FOR (n:AzureSnapshot) ON (n.id);
|
|
380
362
|
CREATE INDEX IF NOT EXISTS FOR (n:AzureSnapshot) ON (n.lastupdated);
|
|
381
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesCluster) ON (n.id);
|
|
382
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesCluster) ON (n.name);
|
|
383
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesCluster) ON (n.lastupdated);
|
|
384
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesNamespace) ON (n.id);
|
|
385
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesNamespace) ON (n.name);
|
|
386
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesNamespace) ON (n.lastupdated);
|
|
387
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesPod) ON (n.id);
|
|
388
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesPod) ON (n.name);
|
|
389
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesPod) ON (n.lastupdated);
|
|
390
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesContainer) ON (n.id);
|
|
391
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesContainer) ON (n.name);
|
|
392
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesContainer) ON (n.image);
|
|
393
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesContainer) ON (n.lastupdated);
|
|
394
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesSecret) ON (n.id);
|
|
395
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesSecret) ON (n.name);
|
|
396
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesSecret) ON (n.lastupdated);
|
|
397
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.id);
|
|
398
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.name);
|
|
399
|
-
CREATE INDEX IF NOT EXISTS FOR (n:KubernetesService) ON (n.lastupdated);
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"iterationsize": 100
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
"query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:AWSLambda)-[r:
|
|
34
|
+
"query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:AWSLambda)-[r:STS_ASSUMEROLE_ALLOW]->(:AWSPrincipal) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE r",
|
|
35
35
|
"iterative": true,
|
|
36
36
|
"iterationsize": 100
|
|
37
37
|
},
|
|
@@ -15,33 +15,40 @@ from cartography.models.core.relationships import TargetNodeMatcher
|
|
|
15
15
|
def build_cleanup_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
16
16
|
"""
|
|
17
17
|
Generates queries to clean up stale nodes and relationships from the given CartographyNodeSchema.
|
|
18
|
+
Properly handles cases where a node schema has a scoped cleanup or not.
|
|
18
19
|
Note that auto-cleanups for a node with no relationships is not currently supported.
|
|
19
|
-
|
|
20
|
-
Algorithm:
|
|
21
|
-
1. If node_schema has no relationships at all, return empty.
|
|
22
|
-
|
|
23
|
-
Otherwise,
|
|
24
|
-
|
|
25
|
-
1. If node_schema doesn't have a sub_resource relationship, generate queries only to clean up its other
|
|
26
|
-
relationships. No nodes will be cleaned up.
|
|
27
|
-
|
|
28
|
-
Otherwise,
|
|
29
|
-
|
|
30
|
-
1. First delete all stale nodes attached to the node_schema's sub resource
|
|
31
|
-
2. Delete all stale node to sub resource relationships
|
|
32
|
-
- We don't expect this to be very common (never for AWS resources, at least), but in case it is possible for an
|
|
33
|
-
asset to change sub resources, we want to handle it properly.
|
|
34
|
-
3. For all relationships defined on the node schema, delete all stale ones.
|
|
35
20
|
:param node_schema: The given CartographyNodeSchema
|
|
36
21
|
:return: A list of Neo4j queries to clean up nodes and relationships.
|
|
37
22
|
"""
|
|
23
|
+
# If the node has no relationships, do not delete the node. Leave this behind for the user to manage.
|
|
24
|
+
# Oftentimes these are SyncMetadata nodes.
|
|
38
25
|
if (
|
|
39
26
|
not node_schema.sub_resource_relationship
|
|
40
27
|
and not node_schema.other_relationships
|
|
41
28
|
):
|
|
42
29
|
return []
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
# Case 1 [Standard]: the node has a sub resource and scoped cleanup is true => clean up stale nodes
|
|
32
|
+
# of this type, scoped to the sub resource. Continue on to clean up the other_relationships too.
|
|
33
|
+
if node_schema.sub_resource_relationship and node_schema.scoped_cleanup:
|
|
34
|
+
queries = _build_cleanup_node_and_rel_queries(
|
|
35
|
+
node_schema,
|
|
36
|
+
node_schema.sub_resource_relationship,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Case 2: The node has a sub resource but scoped cleanup is false => this does not make sense
|
|
40
|
+
# because if have a sub resource, we are implying that we are doing scoped cleanup.
|
|
41
|
+
elif node_schema.sub_resource_relationship and not node_schema.scoped_cleanup:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"This is not expected: {node_schema.label} has a sub_resource_relationship but scoped_cleanup=False."
|
|
44
|
+
"Please check the class definition for this node schema. It doesn't make sense for a node to have a "
|
|
45
|
+
"sub resource relationship and an unscoped cleanup. Doing this will cause all stale nodes of this type "
|
|
46
|
+
"to be deleted regardless of the sub resource they are attached to."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Case 3: The node has no sub resource but scoped cleanup is true => do not delete any nodes, but clean up stale relationships.
|
|
50
|
+
# Return early.
|
|
51
|
+
elif not node_schema.sub_resource_relationship and node_schema.scoped_cleanup:
|
|
45
52
|
queries = []
|
|
46
53
|
other_rels = (
|
|
47
54
|
node_schema.other_relationships.rels
|
|
@@ -53,17 +60,20 @@ def build_cleanup_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
|
53
60
|
queries.append(query)
|
|
54
61
|
return queries
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
node_schema
|
|
59
|
-
|
|
63
|
+
# Case 4: The node has no sub resource and scoped cleanup is false => clean up the stale nodes. Continue on to clean up the other_relationships too.
|
|
64
|
+
else:
|
|
65
|
+
queries = [_build_cleanup_node_query_unscoped(node_schema)]
|
|
66
|
+
|
|
60
67
|
if node_schema.other_relationships:
|
|
61
68
|
for rel in node_schema.other_relationships.rels:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
if node_schema.scoped_cleanup:
|
|
70
|
+
# [0] is the delete node query, [1] is the delete relationship query. We only want the latter.
|
|
71
|
+
_, rel_query = _build_cleanup_node_and_rel_queries(node_schema, rel)
|
|
72
|
+
queries.append(rel_query)
|
|
73
|
+
else:
|
|
74
|
+
queries.append(_build_cleanup_rel_queries_unscoped(node_schema, rel))
|
|
65
75
|
|
|
66
|
-
return
|
|
76
|
+
return queries
|
|
67
77
|
|
|
68
78
|
|
|
69
79
|
def _build_cleanup_rel_query_no_sub_resource(
|
|
@@ -94,6 +104,46 @@ def _build_cleanup_rel_query_no_sub_resource(
|
|
|
94
104
|
)
|
|
95
105
|
|
|
96
106
|
|
|
107
|
+
def _build_match_statement_for_cleanup(node_schema: CartographyNodeSchema) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Helper function to build a MATCH statement for a given node schema for cleanup.
|
|
110
|
+
"""
|
|
111
|
+
if not node_schema.sub_resource_relationship and not node_schema.scoped_cleanup:
|
|
112
|
+
template = Template("MATCH (n:$node_label)")
|
|
113
|
+
return template.safe_substitute(
|
|
114
|
+
node_label=node_schema.label,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# if it has a sub resource relationship defined, we need to match on the sub resource to make sure we only delete
|
|
118
|
+
# nodes that are attached to the sub resource.
|
|
119
|
+
template = Template(
|
|
120
|
+
"MATCH (n:$node_label)$sub_resource_link(:$sub_resource_label{$match_sub_res_clause})"
|
|
121
|
+
)
|
|
122
|
+
sub_resource_link = ""
|
|
123
|
+
sub_resource_label = ""
|
|
124
|
+
match_sub_res_clause = ""
|
|
125
|
+
|
|
126
|
+
if node_schema.sub_resource_relationship:
|
|
127
|
+
# Draw sub resource rel with correct direction
|
|
128
|
+
if node_schema.sub_resource_relationship.direction == LinkDirection.INWARD:
|
|
129
|
+
sub_resource_link_template = Template("<-[s:$SubResourceRelLabel]-")
|
|
130
|
+
else:
|
|
131
|
+
sub_resource_link_template = Template("-[s:$SubResourceRelLabel]->")
|
|
132
|
+
sub_resource_link = sub_resource_link_template.safe_substitute(
|
|
133
|
+
SubResourceRelLabel=node_schema.sub_resource_relationship.rel_label,
|
|
134
|
+
)
|
|
135
|
+
sub_resource_label = node_schema.sub_resource_relationship.target_node_label
|
|
136
|
+
match_sub_res_clause = _build_match_clause(
|
|
137
|
+
node_schema.sub_resource_relationship.target_node_matcher,
|
|
138
|
+
)
|
|
139
|
+
return template.safe_substitute(
|
|
140
|
+
node_label=node_schema.label,
|
|
141
|
+
sub_resource_link=sub_resource_link,
|
|
142
|
+
sub_resource_label=sub_resource_label,
|
|
143
|
+
match_sub_res_clause=match_sub_res_clause,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
97
147
|
def _build_cleanup_node_and_rel_queries(
|
|
98
148
|
node_schema: CartographyNodeSchema,
|
|
99
149
|
selected_relationship: CartographyRelSchema,
|
|
@@ -120,15 +170,6 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
120
170
|
"verify the node class definition for the relationships that it has.",
|
|
121
171
|
)
|
|
122
172
|
|
|
123
|
-
# Draw sub resource rel with correct direction
|
|
124
|
-
if node_schema.sub_resource_relationship.direction == LinkDirection.INWARD:
|
|
125
|
-
sub_resource_link_template = Template("<-[s:$SubResourceRelLabel]-")
|
|
126
|
-
else:
|
|
127
|
-
sub_resource_link_template = Template("-[s:$SubResourceRelLabel]->")
|
|
128
|
-
sub_resource_link = sub_resource_link_template.safe_substitute(
|
|
129
|
-
SubResourceRelLabel=node_schema.sub_resource_relationship.rel_label,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
173
|
# The cleanup node query must always be before the cleanup rel query
|
|
133
174
|
delete_action_clauses = [
|
|
134
175
|
"""
|
|
@@ -161,19 +202,14 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
161
202
|
# Ensure the node is attached to the sub resource and delete the node
|
|
162
203
|
query_template = Template(
|
|
163
204
|
"""
|
|
164
|
-
|
|
205
|
+
$match_statement
|
|
165
206
|
$selected_rel_clause
|
|
166
207
|
$delete_action_clause
|
|
167
208
|
""",
|
|
168
209
|
)
|
|
169
210
|
return [
|
|
170
211
|
query_template.safe_substitute(
|
|
171
|
-
|
|
172
|
-
sub_resource_link=sub_resource_link,
|
|
173
|
-
sub_resource_label=node_schema.sub_resource_relationship.target_node_label,
|
|
174
|
-
match_sub_res_clause=_build_match_clause(
|
|
175
|
-
node_schema.sub_resource_relationship.target_node_matcher,
|
|
176
|
-
),
|
|
212
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
177
213
|
selected_rel_clause=(
|
|
178
214
|
""
|
|
179
215
|
if selected_relationship == node_schema.sub_resource_relationship
|
|
@@ -185,6 +221,80 @@ def _build_cleanup_node_and_rel_queries(
|
|
|
185
221
|
]
|
|
186
222
|
|
|
187
223
|
|
|
224
|
+
def _build_cleanup_node_query_unscoped(
|
|
225
|
+
node_schema: CartographyNodeSchema,
|
|
226
|
+
) -> str:
|
|
227
|
+
"""
|
|
228
|
+
Generates a cleanup query for a node_schema to allow unscoped cleanup.
|
|
229
|
+
"""
|
|
230
|
+
if node_schema.scoped_cleanup:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"_build_cleanup_node_query_for_unscoped_cleanup() failed: '{node_schema.label}' does not have "
|
|
233
|
+
"scoped_cleanup=False, so we cannot generate a query to clean it up. Please verify that the class "
|
|
234
|
+
"definition is what you expect.",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# The cleanup node query must always be before the cleanup rel query
|
|
238
|
+
delete_action_clause = """
|
|
239
|
+
WHERE n.lastupdated <> $UPDATE_TAG
|
|
240
|
+
WITH n LIMIT $LIMIT_SIZE
|
|
241
|
+
DETACH DELETE n;
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Ensure the node is attached to the sub resource and delete the node
|
|
245
|
+
query_template = Template(
|
|
246
|
+
"""
|
|
247
|
+
$match_statement
|
|
248
|
+
$delete_action_clause
|
|
249
|
+
""",
|
|
250
|
+
)
|
|
251
|
+
return query_template.safe_substitute(
|
|
252
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
253
|
+
delete_action_clause=delete_action_clause,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _build_cleanup_rel_queries_unscoped(
|
|
258
|
+
node_schema: CartographyNodeSchema,
|
|
259
|
+
selected_relationship: CartographyRelSchema,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Generates relationship cleanup query for a node_schema with scoped_cleanup=False.
|
|
263
|
+
"""
|
|
264
|
+
if node_schema.scoped_cleanup:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
f"_build_cleanup_node_and_rel_queries_unscoped() failed: '{node_schema.label}' does not have "
|
|
267
|
+
"scoped_cleanup=False, so we cannot generate a query to clean it up. Please verify that the class "
|
|
268
|
+
"definition is what you expect.",
|
|
269
|
+
)
|
|
270
|
+
if not rel_present_on_node_schema(node_schema, selected_relationship):
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"_build_cleanup_node_query(): Attempted to build cleanup query for node '{node_schema.label}' and "
|
|
273
|
+
f"relationship {selected_relationship.rel_label} but that relationship is not present on the node. Please "
|
|
274
|
+
"verify the node class definition for the relationships that it has.",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# The cleanup node query must always be before the cleanup rel query
|
|
278
|
+
delete_action_clause = """WHERE r.lastupdated <> $UPDATE_TAG
|
|
279
|
+
WITH r LIMIT $LIMIT_SIZE
|
|
280
|
+
DELETE r;
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
# Ensure the node is attached to the sub resource and delete the node
|
|
284
|
+
query_template = Template(
|
|
285
|
+
"""
|
|
286
|
+
$match_statement
|
|
287
|
+
$selected_rel_clause
|
|
288
|
+
$delete_action_clause
|
|
289
|
+
""",
|
|
290
|
+
)
|
|
291
|
+
return query_template.safe_substitute(
|
|
292
|
+
match_statement=_build_match_statement_for_cleanup(node_schema),
|
|
293
|
+
selected_rel_clause=_build_selected_rel_clause(selected_relationship),
|
|
294
|
+
delete_action_clause=delete_action_clause,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
188
298
|
def _build_selected_rel_clause(selected_relationship: CartographyRelSchema) -> str:
|
|
189
299
|
"""
|
|
190
300
|
Draw selected relationship with correct direction. Returns a string that looks like either
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
import neo4j
|
|
6
|
+
|
|
7
|
+
from cartography.client.core.tx import load
|
|
8
|
+
from cartography.graph.job import GraphJob
|
|
9
|
+
from cartography.models.aws.acm.certificate import ACMCertificateSchema
|
|
10
|
+
from cartography.stats import get_stats_client
|
|
11
|
+
from cartography.util import aws_handle_regions
|
|
12
|
+
from cartography.util import merge_module_sync_metadata
|
|
13
|
+
from cartography.util import timeit
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
stat_handler = get_stats_client(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@timeit
|
|
20
|
+
@aws_handle_regions
|
|
21
|
+
def get_acm_certificates(
|
|
22
|
+
boto3_session: boto3.session.Session, region: str
|
|
23
|
+
) -> list[dict[str, Any]]:
|
|
24
|
+
client = boto3_session.client("acm", region_name=region)
|
|
25
|
+
paginator = client.get_paginator("list_certificates")
|
|
26
|
+
summaries: list[dict[str, Any]] = []
|
|
27
|
+
for page in paginator.paginate():
|
|
28
|
+
summaries.extend(page.get("CertificateSummaryList", []))
|
|
29
|
+
|
|
30
|
+
details: list[dict[str, Any]] = []
|
|
31
|
+
for summary in summaries:
|
|
32
|
+
arn = summary["CertificateArn"]
|
|
33
|
+
resp = client.describe_certificate(CertificateArn=arn)
|
|
34
|
+
details.append(resp["Certificate"])
|
|
35
|
+
return details
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def transform_acm_certificates(
|
|
39
|
+
certificates: list[dict[str, Any]], region: str
|
|
40
|
+
) -> list[dict[str, Any]]:
|
|
41
|
+
transformed: list[dict[str, Any]] = []
|
|
42
|
+
for cert in certificates:
|
|
43
|
+
item: dict[str, Any] = {
|
|
44
|
+
"Arn": cert["CertificateArn"],
|
|
45
|
+
"DomainName": cert.get("DomainName"),
|
|
46
|
+
"Type": cert.get("Type"),
|
|
47
|
+
"Status": cert.get("Status"),
|
|
48
|
+
"KeyAlgorithm": cert.get("KeyAlgorithm"),
|
|
49
|
+
"SignatureAlgorithm": cert.get("SignatureAlgorithm"),
|
|
50
|
+
"NotBefore": cert.get("NotBefore"),
|
|
51
|
+
"NotAfter": cert.get("NotAfter"),
|
|
52
|
+
"InUseBy": cert.get("InUseBy", []),
|
|
53
|
+
"Region": region,
|
|
54
|
+
}
|
|
55
|
+
# Extract ELBV2 Listener ARNs for relationship creation
|
|
56
|
+
listener_arns = [a for a in item["InUseBy"] if ":listener/" in a]
|
|
57
|
+
if listener_arns:
|
|
58
|
+
item["ELBV2ListenerArns"] = listener_arns
|
|
59
|
+
transformed.append(item)
|
|
60
|
+
return transformed
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@timeit
|
|
64
|
+
def load_acm_certificates(
|
|
65
|
+
neo4j_session: neo4j.Session,
|
|
66
|
+
data: list[dict[str, Any]],
|
|
67
|
+
region: str,
|
|
68
|
+
current_aws_account_id: str,
|
|
69
|
+
update_tag: int,
|
|
70
|
+
) -> None:
|
|
71
|
+
logger.info(f"Loading {len(data)} ACM certificates for region {region} into graph.")
|
|
72
|
+
load(
|
|
73
|
+
neo4j_session,
|
|
74
|
+
ACMCertificateSchema(),
|
|
75
|
+
data,
|
|
76
|
+
lastupdated=update_tag,
|
|
77
|
+
Region=region,
|
|
78
|
+
AWS_ID=current_aws_account_id,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@timeit
|
|
83
|
+
def cleanup_acm_certificates(
|
|
84
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
85
|
+
) -> None:
|
|
86
|
+
logger.debug("Running ACM certificate cleanup job.")
|
|
87
|
+
GraphJob.from_node_schema(ACMCertificateSchema(), common_job_parameters).run(
|
|
88
|
+
neo4j_session
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@timeit
|
|
93
|
+
def sync(
|
|
94
|
+
neo4j_session: neo4j.Session,
|
|
95
|
+
boto3_session: boto3.session.Session,
|
|
96
|
+
regions: list[str],
|
|
97
|
+
current_aws_account_id: str,
|
|
98
|
+
update_tag: int,
|
|
99
|
+
common_job_parameters: dict[str, Any],
|
|
100
|
+
) -> None:
|
|
101
|
+
for region in regions:
|
|
102
|
+
logger.info(
|
|
103
|
+
f"Syncing ACM certificates for region {region} in account {current_aws_account_id}."
|
|
104
|
+
)
|
|
105
|
+
certs = get_acm_certificates(boto3_session, region)
|
|
106
|
+
transformed = transform_acm_certificates(certs, region)
|
|
107
|
+
load_acm_certificates(
|
|
108
|
+
neo4j_session,
|
|
109
|
+
transformed,
|
|
110
|
+
region,
|
|
111
|
+
current_aws_account_id,
|
|
112
|
+
update_tag,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
cleanup_acm_certificates(neo4j_session, common_job_parameters)
|
|
116
|
+
|
|
117
|
+
merge_module_sync_metadata(
|
|
118
|
+
neo4j_session,
|
|
119
|
+
group_type="AWSAccount",
|
|
120
|
+
group_id=current_aws_account_id,
|
|
121
|
+
synced_type="ACMCertificate",
|
|
122
|
+
update_tag=update_tag,
|
|
123
|
+
stat_handler=stat_handler,
|
|
124
|
+
)
|
|
@@ -4,7 +4,6 @@ from typing import Dict
|
|
|
4
4
|
from typing import List
|
|
5
5
|
|
|
6
6
|
import boto3
|
|
7
|
-
import botocore.exceptions
|
|
8
7
|
import neo4j
|
|
9
8
|
|
|
10
9
|
from cartography.client.core.tx import load
|
|
@@ -25,10 +24,8 @@ def get_cloudtrail_trails(
|
|
|
25
24
|
client = boto3_session.client(
|
|
26
25
|
"cloudtrail", region_name=region, config=get_botocore_config()
|
|
27
26
|
)
|
|
28
|
-
|
|
29
|
-
trails = []
|
|
30
|
-
for page in paginator.paginate():
|
|
31
|
-
trails.extend(page["Trails"])
|
|
27
|
+
|
|
28
|
+
trails = client.describe_trails()["trailList"]
|
|
32
29
|
|
|
33
30
|
# CloudTrail multi-region trails are shown in list_trails,
|
|
34
31
|
# but the get_trail call only works in the home region
|
|
@@ -36,28 +33,6 @@ def get_cloudtrail_trails(
|
|
|
36
33
|
return trails_filtered
|
|
37
34
|
|
|
38
35
|
|
|
39
|
-
@timeit
|
|
40
|
-
def get_cloudtrail_trail(
|
|
41
|
-
boto3_session: boto3.Session,
|
|
42
|
-
region: str,
|
|
43
|
-
trail_name: str,
|
|
44
|
-
) -> Dict[str, Any]:
|
|
45
|
-
client = boto3_session.client(
|
|
46
|
-
"cloudtrail", region_name=region, config=get_botocore_config()
|
|
47
|
-
)
|
|
48
|
-
trail_details: Dict[str, Any] = {}
|
|
49
|
-
try:
|
|
50
|
-
response = client.get_trail(Name=trail_name)
|
|
51
|
-
trail_details = response["Trail"]
|
|
52
|
-
except botocore.exceptions.ClientError as e:
|
|
53
|
-
code = e.response["Error"]["Code"]
|
|
54
|
-
msg = e.response["Error"]["Message"]
|
|
55
|
-
logger.warning(
|
|
56
|
-
f"Could not run CloudTrail get_trail due to boto3 error {code}: {msg}. Skipping.",
|
|
57
|
-
)
|
|
58
|
-
return trail_details
|
|
59
|
-
|
|
60
|
-
|
|
61
36
|
@timeit
|
|
62
37
|
def load_cloudtrail_trails(
|
|
63
38
|
neo4j_session: neo4j.Session,
|
|
@@ -105,20 +80,10 @@ def sync(
|
|
|
105
80
|
f"Syncing CloudTrail for region '{region}' in account '{current_aws_account_id}'.",
|
|
106
81
|
)
|
|
107
82
|
trails = get_cloudtrail_trails(boto3_session, region)
|
|
108
|
-
trail_data: List[Dict[str, Any]] = []
|
|
109
|
-
for trail in trails:
|
|
110
|
-
trail_name = trail["Name"]
|
|
111
|
-
trail_details = get_cloudtrail_trail(
|
|
112
|
-
boto3_session,
|
|
113
|
-
region,
|
|
114
|
-
trail_name,
|
|
115
|
-
)
|
|
116
|
-
if trail_details:
|
|
117
|
-
trail_data.append(trail_details)
|
|
118
83
|
|
|
119
84
|
load_cloudtrail_trails(
|
|
120
85
|
neo4j_session,
|
|
121
|
-
|
|
86
|
+
trails,
|
|
122
87
|
region,
|
|
123
88
|
current_aws_account_id,
|
|
124
89
|
update_tag,
|