cartography 0.110.0rc1__py3-none-any.whl → 0.110.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cartography might be problematic. Click here for more details.
- cartography/_version.py +2 -2
- cartography/cli.py +0 -8
- cartography/config.py +0 -9
- cartography/data/jobs/analysis/aws_ec2_keypair_analysis.json +2 -2
- cartography/intel/aws/cognito.py +201 -0
- cartography/intel/aws/ecs.py +7 -1
- cartography/intel/aws/glue.py +64 -0
- cartography/intel/aws/kms.py +13 -1
- cartography/intel/aws/rds.py +105 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/aws/route53.py +3 -1
- cartography/intel/aws/s3.py +104 -0
- cartography/intel/entra/__init__.py +41 -43
- cartography/intel/entra/applications.py +2 -1
- cartography/intel/entra/ou.py +1 -1
- cartography/intel/github/__init__.py +21 -25
- cartography/intel/github/repos.py +4 -36
- cartography/intel/kubernetes/__init__.py +4 -0
- cartography/intel/kubernetes/rbac.py +464 -0
- cartography/intel/kubernetes/util.py +17 -0
- cartography/models/aws/cognito/__init__.py +0 -0
- cartography/models/aws/cognito/identity_pool.py +70 -0
- cartography/models/aws/cognito/user_pool.py +47 -0
- cartography/models/aws/ec2/security_groups.py +1 -1
- cartography/models/aws/ecs/services.py +17 -0
- cartography/models/aws/ecs/tasks.py +1 -0
- cartography/models/aws/glue/job.py +69 -0
- cartography/models/aws/rds/event_subscription.py +146 -0
- cartography/models/aws/route53/dnsrecord.py +21 -0
- cartography/models/github/dependencies.py +1 -2
- cartography/models/kubernetes/clusterrolebindings.py +98 -0
- cartography/models/kubernetes/clusterroles.py +52 -0
- cartography/models/kubernetes/rolebindings.py +119 -0
- cartography/models/kubernetes/roles.py +76 -0
- cartography/models/kubernetes/serviceaccounts.py +77 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/METADATA +3 -3
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/RECORD +42 -31
- cartography/intel/entra/resources.py +0 -20
- /cartography/data/jobs/{analysis → scoped_analysis}/aws_s3acl_analysis.json +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/WHEEL +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/entry_points.txt +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.110.0rc1.dist-info → cartography-0.110.0rc2.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.110.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 110, 0, '
|
|
20
|
+
__version__ = version = '0.110.0rc2'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 110, 0, 'rc2')
|
cartography/cli.py
CHANGED
|
@@ -254,14 +254,6 @@ class CLI:
|
|
|
254
254
|
"The name of environment variable containing Entra Client Secret for Service Principal Authentication."
|
|
255
255
|
),
|
|
256
256
|
)
|
|
257
|
-
parser.add_argument(
|
|
258
|
-
"--entra-best-effort-mode",
|
|
259
|
-
action="store_true",
|
|
260
|
-
help=(
|
|
261
|
-
"Enable Entra ID sync best effort mode. This will allow cartography to continue "
|
|
262
|
-
"syncing other Entra ID entities and delay raising an exception until the end of the sync."
|
|
263
|
-
),
|
|
264
|
-
)
|
|
265
257
|
parser.add_argument(
|
|
266
258
|
"--aws-requested-syncs",
|
|
267
259
|
type=str,
|
cartography/config.py
CHANGED
|
@@ -51,9 +51,6 @@ class Config:
|
|
|
51
51
|
:param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
|
|
52
52
|
:type entra_client_secret: str
|
|
53
53
|
:param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
|
|
54
|
-
:type entra_best_effort_mode: bool
|
|
55
|
-
:param entra_best_effort_mode: If True, Entra ID sync will continue on errors and raise an aggregated
|
|
56
|
-
exception at the end of the sync. If False (default), exceptions will be raised immediately.
|
|
57
54
|
:type aws_requested_syncs: str
|
|
58
55
|
:param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
|
|
59
56
|
:type aws_guardduty_severity_threshold: str
|
|
@@ -167,8 +164,6 @@ class Config:
|
|
|
167
164
|
:param sentinelone_api_url: SentinelOne API URL. Optional.
|
|
168
165
|
:type sentinelone_api_token: string
|
|
169
166
|
:param sentinelone_api_token: SentinelOne API token for authentication. Optional.
|
|
170
|
-
:type sentinelone_api_token_env_var: string
|
|
171
|
-
:param sentinelone_api_token_env_var: The name of an environment variable containing the SentinelOne API token. Optional.
|
|
172
167
|
:type sentinelone_account_ids: list[str]
|
|
173
168
|
:param sentinelone_account_ids: List of SentinelOne account IDs to sync. Optional.
|
|
174
169
|
"""
|
|
@@ -194,7 +189,6 @@ class Config:
|
|
|
194
189
|
entra_tenant_id=None,
|
|
195
190
|
entra_client_id=None,
|
|
196
191
|
entra_client_secret=None,
|
|
197
|
-
entra_best_effort_mode=False,
|
|
198
192
|
aws_requested_syncs=None,
|
|
199
193
|
aws_guardduty_severity_threshold=None,
|
|
200
194
|
analysis_job_directory=None,
|
|
@@ -257,7 +251,6 @@ class Config:
|
|
|
257
251
|
scaleway_org=None,
|
|
258
252
|
sentinelone_api_url=None,
|
|
259
253
|
sentinelone_api_token=None,
|
|
260
|
-
sentinelone_api_token_env_var=None,
|
|
261
254
|
sentinelone_account_ids=None,
|
|
262
255
|
):
|
|
263
256
|
self.neo4j_uri = neo4j_uri
|
|
@@ -281,7 +274,6 @@ class Config:
|
|
|
281
274
|
self.entra_tenant_id = entra_tenant_id
|
|
282
275
|
self.entra_client_id = entra_client_id
|
|
283
276
|
self.entra_client_secret = entra_client_secret
|
|
284
|
-
self.entra_best_effort_mode = entra_best_effort_mode
|
|
285
277
|
self.aws_requested_syncs = aws_requested_syncs
|
|
286
278
|
self.aws_guardduty_severity_threshold = aws_guardduty_severity_threshold
|
|
287
279
|
self.analysis_job_directory = analysis_job_directory
|
|
@@ -344,5 +336,4 @@ class Config:
|
|
|
344
336
|
self.scaleway_org = scaleway_org
|
|
345
337
|
self.sentinelone_api_url = sentinelone_api_url
|
|
346
338
|
self.sentinelone_api_token = sentinelone_api_token
|
|
347
|
-
self.sentinelone_api_token_env_var = sentinelone_api_token_env_var
|
|
348
339
|
self.sentinelone_account_ids = sentinelone_account_ids
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"iterative": false
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
"__comment__": "Attach EC2KeyPairs with matching fingerprints to
|
|
26
|
-
"query": "MATCH (k1:EC2KeyPair)
|
|
25
|
+
"__comment__": "Attach EC2KeyPairs with matching fingerprints to each other and set duplicate_keyfingerprint = True. Use id(k1) < id(k2) to avoid Cartesian product warning and ensure O(1) comparison.",
|
|
26
|
+
"query": "MATCH (k1:EC2KeyPair) MATCH (k2:EC2KeyPair) WHERE id(k1) < id(k2) AND k1.keyfingerprint = k2.keyfingerprint SET k1.duplicate_keyfingerprint = True, k2.duplicate_keyfingerprint = True MERGE (k1)-[r:MATCHING_FINGERPRINT]-(k2) ON CREATE SET r.firstseen = $UPDATE_TAG SET r.lastupdated = $UPDATE_TAG RETURN COUNT(*) as TotalCompleted",
|
|
27
27
|
"iterative": false
|
|
28
28
|
}
|
|
29
29
|
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import neo4j
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.intel.aws.ec2.util import get_botocore_config
|
|
12
|
+
from cartography.models.aws.cognito.identity_pool import CognitoIdentityPoolSchema
|
|
13
|
+
from cartography.models.aws.cognito.user_pool import CognitoUserPoolSchema
|
|
14
|
+
from cartography.util import aws_handle_regions
|
|
15
|
+
from cartography.util import timeit
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@timeit
|
|
21
|
+
@aws_handle_regions
|
|
22
|
+
def get_identity_pools(
|
|
23
|
+
boto3_session: boto3.Session, region: str
|
|
24
|
+
) -> List[Dict[str, Any]]:
|
|
25
|
+
client = boto3_session.client(
|
|
26
|
+
"cognito-identity", region_name=region, config=get_botocore_config()
|
|
27
|
+
)
|
|
28
|
+
paginator = client.get_paginator("list_identity_pools")
|
|
29
|
+
|
|
30
|
+
all_identity_pools = []
|
|
31
|
+
|
|
32
|
+
for page in paginator.paginate(MaxResults=50):
|
|
33
|
+
identity_pools = page.get("IdentityPools", [])
|
|
34
|
+
all_identity_pools.extend(identity_pools)
|
|
35
|
+
return all_identity_pools
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@timeit
|
|
39
|
+
@aws_handle_regions
|
|
40
|
+
def get_identity_pool_roles(
|
|
41
|
+
boto3_session: boto3.Session, identity_pools: List[Dict[str, Any]], region: str
|
|
42
|
+
) -> List[Dict[str, Any]]:
|
|
43
|
+
client = boto3_session.client(
|
|
44
|
+
"cognito-identity", region_name=region, config=get_botocore_config()
|
|
45
|
+
)
|
|
46
|
+
all_identity_pool_details = []
|
|
47
|
+
for identity_pool in identity_pools:
|
|
48
|
+
response = client.get_identity_pool_roles(
|
|
49
|
+
IdentityPoolId=identity_pool["IdentityPoolId"]
|
|
50
|
+
)
|
|
51
|
+
all_identity_pool_details.append(response)
|
|
52
|
+
return all_identity_pool_details
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@timeit
|
|
56
|
+
@aws_handle_regions
|
|
57
|
+
def get_user_pools(boto3_session: boto3.Session, region: str) -> List[Dict[str, Any]]:
|
|
58
|
+
client = boto3_session.client(
|
|
59
|
+
"cognito-idp", region_name=region, config=get_botocore_config()
|
|
60
|
+
)
|
|
61
|
+
paginator = client.get_paginator("list_user_pools")
|
|
62
|
+
all_user_pools = []
|
|
63
|
+
|
|
64
|
+
for page in paginator.paginate(MaxResults=50):
|
|
65
|
+
user_pools = page.get("UserPools", [])
|
|
66
|
+
all_user_pools.extend(user_pools)
|
|
67
|
+
return all_user_pools
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def transform_identity_pools(
|
|
71
|
+
identity_pools: List[Dict[str, Any]], region: str
|
|
72
|
+
) -> List[Dict[str, Any]]:
|
|
73
|
+
transformed_identity_pools = []
|
|
74
|
+
for pool in identity_pools:
|
|
75
|
+
transformed_pool = {
|
|
76
|
+
"IdentityPoolId": pool["IdentityPoolId"],
|
|
77
|
+
"Region": region,
|
|
78
|
+
"Roles": list(pool.get("Roles", {}).values()),
|
|
79
|
+
}
|
|
80
|
+
transformed_identity_pools.append(transformed_pool)
|
|
81
|
+
return transformed_identity_pools
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def transform_user_pools(
|
|
85
|
+
user_pools: List[Dict[str, Any]], region: str
|
|
86
|
+
) -> List[Dict[str, Any]]:
|
|
87
|
+
transformed_user_pools = []
|
|
88
|
+
for pool in user_pools:
|
|
89
|
+
transformed_pool = {
|
|
90
|
+
"Id": pool["Id"],
|
|
91
|
+
"Region": region,
|
|
92
|
+
"Name": pool["Name"],
|
|
93
|
+
"Status": pool.get("Status"),
|
|
94
|
+
}
|
|
95
|
+
transformed_user_pools.append(transformed_pool)
|
|
96
|
+
return transformed_user_pools
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@timeit
|
|
100
|
+
def load_identity_pools(
|
|
101
|
+
neo4j_session: neo4j.Session,
|
|
102
|
+
data: List[Dict[str, Any]],
|
|
103
|
+
region: str,
|
|
104
|
+
current_aws_account_id: str,
|
|
105
|
+
aws_update_tag: int,
|
|
106
|
+
) -> None:
|
|
107
|
+
logger.info(
|
|
108
|
+
f"Loading Cognito Identity Pools {len(data)} for region '{region}' into graph.",
|
|
109
|
+
)
|
|
110
|
+
load(
|
|
111
|
+
neo4j_session,
|
|
112
|
+
CognitoIdentityPoolSchema(),
|
|
113
|
+
data,
|
|
114
|
+
lastupdated=aws_update_tag,
|
|
115
|
+
Region=region,
|
|
116
|
+
AWS_ID=current_aws_account_id,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@timeit
|
|
121
|
+
def load_user_pools(
|
|
122
|
+
neo4j_session: neo4j.Session,
|
|
123
|
+
data: List[Dict[str, Any]],
|
|
124
|
+
region: str,
|
|
125
|
+
current_aws_account_id: str,
|
|
126
|
+
aws_update_tag: int,
|
|
127
|
+
) -> None:
|
|
128
|
+
logger.info(
|
|
129
|
+
f"Loading Cognito User Pools {len(data)} for region '{region}' into graph.",
|
|
130
|
+
)
|
|
131
|
+
load(
|
|
132
|
+
neo4j_session,
|
|
133
|
+
CognitoUserPoolSchema(),
|
|
134
|
+
data,
|
|
135
|
+
lastupdated=aws_update_tag,
|
|
136
|
+
Region=region,
|
|
137
|
+
AWS_ID=current_aws_account_id,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@timeit
|
|
142
|
+
def cleanup(
|
|
143
|
+
neo4j_session: neo4j.Session,
|
|
144
|
+
common_job_parameters: Dict[str, Any],
|
|
145
|
+
) -> None:
|
|
146
|
+
logger.debug("Running Efs cleanup job.")
|
|
147
|
+
GraphJob.from_node_schema(CognitoIdentityPoolSchema(), common_job_parameters).run(
|
|
148
|
+
neo4j_session
|
|
149
|
+
)
|
|
150
|
+
GraphJob.from_node_schema(CognitoUserPoolSchema(), common_job_parameters).run(
|
|
151
|
+
neo4j_session
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@timeit
|
|
156
|
+
def sync(
|
|
157
|
+
neo4j_session: neo4j.Session,
|
|
158
|
+
boto3_session: boto3.session.Session,
|
|
159
|
+
regions: List[str],
|
|
160
|
+
current_aws_account_id: str,
|
|
161
|
+
update_tag: int,
|
|
162
|
+
common_job_parameters: Dict[str, Any],
|
|
163
|
+
) -> None:
|
|
164
|
+
for region in regions:
|
|
165
|
+
logger.info(
|
|
166
|
+
f"Syncing Cognito Identity Pools for region '{region}' in account '{current_aws_account_id}'.",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
identity_pools = get_identity_pools(boto3_session, region)
|
|
170
|
+
if not identity_pools:
|
|
171
|
+
logger.info(
|
|
172
|
+
f"No Cognito Identity Pools found in region '{region}'. Skipping sync."
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
identity_pool_roles = get_identity_pool_roles(
|
|
176
|
+
boto3_session, identity_pools, region
|
|
177
|
+
)
|
|
178
|
+
transformed_identity_pools = transform_identity_pools(
|
|
179
|
+
identity_pool_roles, region
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
load_identity_pools(
|
|
183
|
+
neo4j_session,
|
|
184
|
+
transformed_identity_pools,
|
|
185
|
+
region,
|
|
186
|
+
current_aws_account_id,
|
|
187
|
+
update_tag,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
user_pools = get_user_pools(boto3_session, region)
|
|
191
|
+
transformed_user_pools = transform_user_pools(user_pools, region)
|
|
192
|
+
|
|
193
|
+
load_user_pools(
|
|
194
|
+
neo4j_session,
|
|
195
|
+
transformed_user_pools,
|
|
196
|
+
region,
|
|
197
|
+
current_aws_account_id,
|
|
198
|
+
update_tag,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
cleanup(neo4j_session, common_job_parameters)
|
cartography/intel/aws/ecs.py
CHANGED
|
@@ -171,9 +171,15 @@ def _get_containers_from_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, An
|
|
|
171
171
|
|
|
172
172
|
def transform_ecs_tasks(tasks: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
173
173
|
"""
|
|
174
|
-
Extract network interface ID from task attachments.
|
|
174
|
+
Extract network interface ID from task attachments and service name from group.
|
|
175
175
|
"""
|
|
176
176
|
for task in tasks:
|
|
177
|
+
# Extract serviceName from group field
|
|
178
|
+
group = task.get("group")
|
|
179
|
+
if group and group.startswith("service:"):
|
|
180
|
+
task["serviceName"] = group.split("service:", 1)[1]
|
|
181
|
+
|
|
182
|
+
# Extract network interface ID from task attachments
|
|
177
183
|
for attachment in task.get("attachments", []):
|
|
178
184
|
if attachment.get("type") == "ElasticNetworkInterface":
|
|
179
185
|
details = attachment.get("details", [])
|
cartography/intel/aws/glue.py
CHANGED
|
@@ -10,6 +10,7 @@ from cartography.client.core.tx import load
|
|
|
10
10
|
from cartography.graph.job import GraphJob
|
|
11
11
|
from cartography.intel.aws.ec2.util import get_botocore_config
|
|
12
12
|
from cartography.models.aws.glue.connection import GlueConnectionSchema
|
|
13
|
+
from cartography.models.aws.glue.job import GlueJobSchema
|
|
13
14
|
from cartography.util import aws_handle_regions
|
|
14
15
|
from cartography.util import timeit
|
|
15
16
|
|
|
@@ -32,6 +33,37 @@ def get_glue_connections(
|
|
|
32
33
|
return connections
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
@timeit
|
|
37
|
+
@aws_handle_regions
|
|
38
|
+
def get_glue_jobs(boto3_session: boto3.Session, region: str) -> List[Dict[str, Any]]:
|
|
39
|
+
client = boto3_session.client(
|
|
40
|
+
"glue", region_name=region, config=get_botocore_config()
|
|
41
|
+
)
|
|
42
|
+
paginator = client.get_paginator("get_jobs")
|
|
43
|
+
jobs = []
|
|
44
|
+
for page in paginator.paginate():
|
|
45
|
+
jobs.extend(page.get("Jobs", []))
|
|
46
|
+
return jobs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transform_glue_job(jobs: List[Dict[str, Any]], region: str) -> List[Dict[str, Any]]:
|
|
50
|
+
"""
|
|
51
|
+
Transform Glue job data for ingestion
|
|
52
|
+
"""
|
|
53
|
+
transformed_jobs = []
|
|
54
|
+
for job in jobs:
|
|
55
|
+
transformed_job = {
|
|
56
|
+
"Name": job["Name"],
|
|
57
|
+
"ProfileName": job.get("ProfileName"),
|
|
58
|
+
"JobMode": job.get("JobMode"),
|
|
59
|
+
"Connections": job.get("Connections", {}).get("Connections"),
|
|
60
|
+
"Region": region,
|
|
61
|
+
"Description": job.get("Description"),
|
|
62
|
+
}
|
|
63
|
+
transformed_jobs.append(transformed_job)
|
|
64
|
+
return transformed_jobs
|
|
65
|
+
|
|
66
|
+
|
|
35
67
|
def transform_glue_connections(
|
|
36
68
|
connections: List[Dict[str, Any]], region: str
|
|
37
69
|
) -> List[Dict[str, Any]]:
|
|
@@ -79,6 +111,27 @@ def load_glue_connections(
|
|
|
79
111
|
)
|
|
80
112
|
|
|
81
113
|
|
|
114
|
+
@timeit
|
|
115
|
+
def load_glue_jobs(
|
|
116
|
+
neo4j_session: neo4j.Session,
|
|
117
|
+
data: List[Dict[str, Any]],
|
|
118
|
+
region: str,
|
|
119
|
+
current_aws_account_id: str,
|
|
120
|
+
aws_update_tag: int,
|
|
121
|
+
) -> None:
|
|
122
|
+
logger.info(
|
|
123
|
+
f"Loading Glue {len(data)} jobs for region '{region}' into graph.",
|
|
124
|
+
)
|
|
125
|
+
load(
|
|
126
|
+
neo4j_session,
|
|
127
|
+
GlueJobSchema(),
|
|
128
|
+
data,
|
|
129
|
+
lastupdated=aws_update_tag,
|
|
130
|
+
Region=region,
|
|
131
|
+
AWS_ID=current_aws_account_id,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
82
135
|
@timeit
|
|
83
136
|
def cleanup(
|
|
84
137
|
neo4j_session: neo4j.Session,
|
|
@@ -88,6 +141,7 @@ def cleanup(
|
|
|
88
141
|
GraphJob.from_node_schema(GlueConnectionSchema(), common_job_parameters).run(
|
|
89
142
|
neo4j_session
|
|
90
143
|
)
|
|
144
|
+
GraphJob.from_node_schema(GlueJobSchema(), common_job_parameters).run(neo4j_session)
|
|
91
145
|
|
|
92
146
|
|
|
93
147
|
@timeit
|
|
@@ -114,4 +168,14 @@ def sync(
|
|
|
114
168
|
update_tag,
|
|
115
169
|
)
|
|
116
170
|
|
|
171
|
+
jobs = get_glue_jobs(boto3_session, region)
|
|
172
|
+
transformed_jobs = transform_glue_job(jobs, region)
|
|
173
|
+
load_glue_jobs(
|
|
174
|
+
neo4j_session,
|
|
175
|
+
transformed_jobs,
|
|
176
|
+
region,
|
|
177
|
+
current_aws_account_id,
|
|
178
|
+
update_tag,
|
|
179
|
+
)
|
|
180
|
+
|
|
117
181
|
cleanup(neo4j_session, common_job_parameters)
|
cartography/intel/aws/kms.py
CHANGED
|
@@ -76,8 +76,8 @@ def get_policy(key: Dict, client: botocore.client.BaseClient) -> Any:
|
|
|
76
76
|
try:
|
|
77
77
|
policy = client.get_key_policy(KeyId=key["KeyId"], PolicyName="default")
|
|
78
78
|
except ClientError as e:
|
|
79
|
-
policy = None
|
|
80
79
|
if e.response["Error"]["Code"] == "AccessDeniedException":
|
|
80
|
+
policy = None
|
|
81
81
|
logger.warning(
|
|
82
82
|
f"kms:get_key_policy on key id {key['KeyId']} failed with AccessDeniedException; continuing sync.",
|
|
83
83
|
exc_info=True,
|
|
@@ -187,6 +187,18 @@ def transform_kms_key_policies(
|
|
|
187
187
|
policy_data = {}
|
|
188
188
|
|
|
189
189
|
for key_id, policy, *_ in policy_alias_grants_data:
|
|
190
|
+
# Handle keys with null policy (access denied)
|
|
191
|
+
if policy is None:
|
|
192
|
+
logger.info(
|
|
193
|
+
f"Skipping KMS key {key_id} policy due to AccessDenied; policy analysis properties will be null"
|
|
194
|
+
)
|
|
195
|
+
policy_data[key_id] = {
|
|
196
|
+
"kms_key": key_id,
|
|
197
|
+
"anonymous_access": None,
|
|
198
|
+
"anonymous_actions": None,
|
|
199
|
+
}
|
|
200
|
+
continue
|
|
201
|
+
|
|
190
202
|
parsed_policy = parse_policy(key_id, policy)
|
|
191
203
|
policy_data[key_id] = parsed_policy
|
|
192
204
|
|
cartography/intel/aws/rds.py
CHANGED
|
@@ -9,6 +9,7 @@ import neo4j
|
|
|
9
9
|
from cartography.client.core.tx import load
|
|
10
10
|
from cartography.graph.job import GraphJob
|
|
11
11
|
from cartography.models.aws.rds.cluster import RDSClusterSchema
|
|
12
|
+
from cartography.models.aws.rds.event_subscription import RDSEventSubscriptionSchema
|
|
12
13
|
from cartography.models.aws.rds.instance import RDSInstanceSchema
|
|
13
14
|
from cartography.models.aws.rds.snapshot import RDSSnapshotSchema
|
|
14
15
|
from cartography.models.aws.rds.subnet_group import DBSubnetGroupSchema
|
|
@@ -136,6 +137,38 @@ def load_rds_snapshots(
|
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
@timeit
|
|
141
|
+
@aws_handle_regions
|
|
142
|
+
def get_rds_event_subscription_data(
|
|
143
|
+
boto3_session: boto3.session.Session,
|
|
144
|
+
region: str,
|
|
145
|
+
) -> List[Dict[str, Any]]:
|
|
146
|
+
client = boto3_session.client("rds", region_name=region)
|
|
147
|
+
paginator = client.get_paginator("describe_event_subscriptions")
|
|
148
|
+
subscriptions = []
|
|
149
|
+
for page in paginator.paginate():
|
|
150
|
+
subscriptions.extend(page["EventSubscriptionsList"])
|
|
151
|
+
return subscriptions
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@timeit
|
|
155
|
+
def load_rds_event_subscriptions(
|
|
156
|
+
neo4j_session: neo4j.Session,
|
|
157
|
+
data: List[Dict],
|
|
158
|
+
region: str,
|
|
159
|
+
current_aws_account_id: str,
|
|
160
|
+
aws_update_tag: int,
|
|
161
|
+
) -> None:
|
|
162
|
+
load(
|
|
163
|
+
neo4j_session,
|
|
164
|
+
RDSEventSubscriptionSchema(),
|
|
165
|
+
data,
|
|
166
|
+
lastupdated=aws_update_tag,
|
|
167
|
+
Region=region,
|
|
168
|
+
AWS_ID=current_aws_account_id,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
139
172
|
def _validate_rds_endpoint(rds: Dict) -> Dict:
|
|
140
173
|
"""
|
|
141
174
|
Get Endpoint from RDS data structure. Log to debug if an Endpoint field does not exist.
|
|
@@ -292,6 +325,28 @@ def transform_rds_instances(
|
|
|
292
325
|
return instances
|
|
293
326
|
|
|
294
327
|
|
|
328
|
+
def transform_rds_event_subscriptions(data: List[Dict]) -> List[Dict]:
|
|
329
|
+
subscriptions = []
|
|
330
|
+
for subscription in data:
|
|
331
|
+
transformed = {
|
|
332
|
+
"CustSubscriptionId": subscription.get("CustSubscriptionId"),
|
|
333
|
+
"EventSubscriptionArn": subscription.get("EventSubscriptionArn"),
|
|
334
|
+
"CustomerAwsId": subscription.get("CustomerAwsId"),
|
|
335
|
+
"SnsTopicArn": subscription.get("SnsTopicArn"),
|
|
336
|
+
"SourceType": subscription.get("SourceType"),
|
|
337
|
+
"Status": subscription.get("Status"),
|
|
338
|
+
"Enabled": subscription.get("Enabled"),
|
|
339
|
+
"SubscriptionCreationTime": dict_value_to_str(
|
|
340
|
+
subscription, "SubscriptionCreationTime"
|
|
341
|
+
),
|
|
342
|
+
"event_categories": subscription.get("EventCategoriesList") or None,
|
|
343
|
+
"source_ids": subscription.get("SourceIdsList") or None,
|
|
344
|
+
"lastupdated": None, # This will be set by the loader
|
|
345
|
+
}
|
|
346
|
+
subscriptions.append(transformed)
|
|
347
|
+
return subscriptions
|
|
348
|
+
|
|
349
|
+
|
|
295
350
|
def transform_rds_subnet_groups(
|
|
296
351
|
data: List[Dict], region: str, current_aws_account_id: str
|
|
297
352
|
) -> List[Dict]:
|
|
@@ -412,6 +467,20 @@ def cleanup_rds_snapshots(
|
|
|
412
467
|
)
|
|
413
468
|
|
|
414
469
|
|
|
470
|
+
@timeit
|
|
471
|
+
def cleanup_rds_event_subscriptions(
|
|
472
|
+
neo4j_session: neo4j.Session,
|
|
473
|
+
common_job_parameters: Dict,
|
|
474
|
+
) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Remove RDS event subscriptions that weren't updated in this sync run
|
|
477
|
+
"""
|
|
478
|
+
logger.debug("Running RDS event subscriptions cleanup job")
|
|
479
|
+
GraphJob.from_node_schema(RDSEventSubscriptionSchema(), common_job_parameters).run(
|
|
480
|
+
neo4j_session
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
415
484
|
@timeit
|
|
416
485
|
def sync_rds_clusters(
|
|
417
486
|
neo4j_session: neo4j.Session,
|
|
@@ -498,6 +567,32 @@ def sync_rds_snapshots(
|
|
|
498
567
|
cleanup_rds_snapshots(neo4j_session, common_job_parameters)
|
|
499
568
|
|
|
500
569
|
|
|
570
|
+
@timeit
|
|
571
|
+
def sync_rds_event_subscriptions(
|
|
572
|
+
neo4j_session: neo4j.Session,
|
|
573
|
+
boto3_session: boto3.session.Session,
|
|
574
|
+
regions: List[str],
|
|
575
|
+
current_aws_account_id: str,
|
|
576
|
+
update_tag: int,
|
|
577
|
+
common_job_parameters: Dict,
|
|
578
|
+
) -> None:
|
|
579
|
+
"""
|
|
580
|
+
Grab RDS event subscription data from AWS, ingest to neo4j, and run the cleanup job.
|
|
581
|
+
"""
|
|
582
|
+
for region in regions:
|
|
583
|
+
logger.info(
|
|
584
|
+
"Syncing RDS event subscriptions for region '%s' in account '%s'.",
|
|
585
|
+
region,
|
|
586
|
+
current_aws_account_id,
|
|
587
|
+
)
|
|
588
|
+
data = get_rds_event_subscription_data(boto3_session, region)
|
|
589
|
+
transformed = transform_rds_event_subscriptions(data)
|
|
590
|
+
load_rds_event_subscriptions(
|
|
591
|
+
neo4j_session, transformed, region, current_aws_account_id, update_tag
|
|
592
|
+
)
|
|
593
|
+
cleanup_rds_event_subscriptions(neo4j_session, common_job_parameters)
|
|
594
|
+
|
|
595
|
+
|
|
501
596
|
@timeit
|
|
502
597
|
def sync(
|
|
503
598
|
neo4j_session: neo4j.Session,
|
|
@@ -531,6 +626,16 @@ def sync(
|
|
|
531
626
|
update_tag,
|
|
532
627
|
common_job_parameters,
|
|
533
628
|
)
|
|
629
|
+
|
|
630
|
+
sync_rds_event_subscriptions(
|
|
631
|
+
neo4j_session,
|
|
632
|
+
boto3_session,
|
|
633
|
+
regions,
|
|
634
|
+
current_aws_account_id,
|
|
635
|
+
update_tag,
|
|
636
|
+
common_job_parameters,
|
|
637
|
+
)
|
|
638
|
+
|
|
534
639
|
merge_module_sync_metadata(
|
|
535
640
|
neo4j_session,
|
|
536
641
|
group_type="AWSAccount",
|
|
@@ -9,6 +9,7 @@ from . import cloudtrail
|
|
|
9
9
|
from . import cloudtrail_management_events
|
|
10
10
|
from . import cloudwatch
|
|
11
11
|
from . import codebuild
|
|
12
|
+
from . import cognito
|
|
12
13
|
from . import config
|
|
13
14
|
from . import dynamodb
|
|
14
15
|
from . import ecr
|
|
@@ -116,6 +117,7 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
|
|
|
116
117
|
"efs": efs.sync,
|
|
117
118
|
"guardduty": guardduty.sync,
|
|
118
119
|
"codebuild": codebuild.sync,
|
|
120
|
+
"cognito": cognito.sync,
|
|
119
121
|
"eventbridge": eventbridge.sync,
|
|
120
122
|
"glue": glue.sync,
|
|
121
123
|
}
|
cartography/intel/aws/route53.py
CHANGED
|
@@ -398,7 +398,9 @@ def link_sub_zones(
|
|
|
398
398
|
MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(z:AWSDNSZone)
|
|
399
399
|
<-[:MEMBER_OF_DNS_ZONE]-(record:DNSRecord{type:"NS"})
|
|
400
400
|
-[:DNS_POINTS_TO]->(ns:NameServer)<-[:NAMESERVER]-(z2:AWSDNSZone)
|
|
401
|
-
|
|
401
|
+
WHERE record.name = z2.name AND
|
|
402
|
+
z2.name ENDS WITH '.' + z.name AND
|
|
403
|
+
NOT z = z2
|
|
402
404
|
RETURN z.id as zone_id, z2.id as subzone_id
|
|
403
405
|
"""
|
|
404
406
|
zone_to_subzone = neo4j_session.read_transaction(
|