aws-cis-controls-assessment 1.0.3__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.
- aws_cis_assessment/__init__.py +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- docs/user-guide.md +487 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""Control 3.10: Encrypt Sensitive Data in Transit assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
9
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
10
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APIGatewaySSLEnabledAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for api-gw-ssl-enabled Config rule - ensures API Gateway stages have SSL certificates."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize API Gateway SSL enabled assessment."""
|
|
20
|
+
super().__init__(
|
|
21
|
+
rule_name="api-gw-ssl-enabled",
|
|
22
|
+
control_id="3.10",
|
|
23
|
+
resource_types=["AWS::ApiGateway::Stage"]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
27
|
+
"""Get all API Gateway stages in the region."""
|
|
28
|
+
if resource_type != "AWS::ApiGateway::Stage":
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
apigateway_client = aws_factory.get_client('apigateway', region)
|
|
33
|
+
|
|
34
|
+
# First get all REST APIs
|
|
35
|
+
apis_response = aws_factory.aws_api_call_with_retry(
|
|
36
|
+
lambda: apigateway_client.get_rest_apis()
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
stages = []
|
|
40
|
+
for api in apis_response.get('items', []):
|
|
41
|
+
api_id = api.get('id')
|
|
42
|
+
api_name = api.get('name', 'Unknown')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Get stages for each API
|
|
46
|
+
stages_response = aws_factory.aws_api_call_with_retry(
|
|
47
|
+
lambda: apigateway_client.get_stages(restApiId=api_id)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for stage in stages_response.get('item', []):
|
|
51
|
+
stages.append({
|
|
52
|
+
'restApiId': api_id,
|
|
53
|
+
'apiName': api_name,
|
|
54
|
+
'stageName': stage.get('stageName'),
|
|
55
|
+
'clientCertificateId': stage.get('clientCertificateId'),
|
|
56
|
+
'methodSettings': stage.get('methodSettings', {}),
|
|
57
|
+
'tags': stage.get('tags', {})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
except ClientError as e:
|
|
61
|
+
logger.warning(f"Could not get stages for API {api_id}: {e}")
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
logger.debug(f"Found {len(stages)} API Gateway stages in region {region}")
|
|
65
|
+
return stages
|
|
66
|
+
|
|
67
|
+
except ClientError as e:
|
|
68
|
+
logger.error(f"Error retrieving API Gateway stages in region {region}: {e}")
|
|
69
|
+
raise
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Unexpected error retrieving API Gateway stages in region {region}: {e}")
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
75
|
+
"""Evaluate if API Gateway stage has SSL certificate configured."""
|
|
76
|
+
api_id = resource.get('restApiId', 'unknown')
|
|
77
|
+
stage_name = resource.get('stageName', 'unknown')
|
|
78
|
+
api_name = resource.get('apiName', 'Unknown')
|
|
79
|
+
client_cert_id = resource.get('clientCertificateId')
|
|
80
|
+
method_settings = resource.get('methodSettings', {})
|
|
81
|
+
|
|
82
|
+
resource_id = f"{api_id}/{stage_name}"
|
|
83
|
+
|
|
84
|
+
# Check if client certificate is configured
|
|
85
|
+
has_client_cert = bool(client_cert_id)
|
|
86
|
+
|
|
87
|
+
# Check if HTTPS is required in method settings
|
|
88
|
+
https_required = False
|
|
89
|
+
for method_key, settings in method_settings.items():
|
|
90
|
+
if settings.get('requireAuthorizationForCacheControl', False):
|
|
91
|
+
https_required = True
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
# For this rule, we primarily check for client certificate
|
|
95
|
+
if has_client_cert:
|
|
96
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
97
|
+
evaluation_reason = f"API Gateway stage {stage_name} (API: {api_name}) has client certificate configured: {client_cert_id}"
|
|
98
|
+
else:
|
|
99
|
+
# Check if this is a public API that might not need client certificates
|
|
100
|
+
# For now, we'll mark as non-compliant if no client certificate
|
|
101
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
102
|
+
evaluation_reason = f"API Gateway stage {stage_name} (API: {api_name}) does not have a client certificate configured"
|
|
103
|
+
|
|
104
|
+
return ComplianceResult(
|
|
105
|
+
resource_id=resource_id,
|
|
106
|
+
resource_type="AWS::ApiGateway::Stage",
|
|
107
|
+
compliance_status=compliance_status,
|
|
108
|
+
evaluation_reason=evaluation_reason,
|
|
109
|
+
config_rule_name=self.rule_name,
|
|
110
|
+
region=region
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
114
|
+
"""Get specific remediation steps for API Gateway SSL configuration."""
|
|
115
|
+
return [
|
|
116
|
+
"Identify API Gateway stages without SSL certificates configured",
|
|
117
|
+
"For each non-compliant stage:",
|
|
118
|
+
" 1. Generate or import a client certificate in API Gateway",
|
|
119
|
+
" 2. Associate the client certificate with the stage",
|
|
120
|
+
" 3. Configure method settings to require HTTPS",
|
|
121
|
+
"Use AWS CLI: aws apigateway generate-client-certificate --description 'Client cert for API'",
|
|
122
|
+
"Associate certificate: aws apigateway update-stage --rest-api-id <api-id> --stage-name <stage> --patch-ops op=replace,path=/clientCertificateId,value=<cert-id>",
|
|
123
|
+
"Configure custom domain with SSL certificate for production APIs",
|
|
124
|
+
"Enable CloudTrail logging for API Gateway to monitor SSL usage",
|
|
125
|
+
"Review API Gateway access logs to ensure HTTPS usage"
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ALBHTTPToHTTPSRedirectionAssessment(BaseConfigRuleAssessment):
|
|
130
|
+
"""Assessment for alb-http-to-https-redirection-check Config rule."""
|
|
131
|
+
|
|
132
|
+
def __init__(self):
|
|
133
|
+
"""Initialize ALB HTTP to HTTPS redirection assessment."""
|
|
134
|
+
super().__init__(
|
|
135
|
+
rule_name="alb-http-to-https-redirection-check",
|
|
136
|
+
control_id="3.10",
|
|
137
|
+
resource_types=["AWS::ElasticLoadBalancingV2::LoadBalancer"]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
141
|
+
"""Get all Application Load Balancers in the region."""
|
|
142
|
+
if resource_type != "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
147
|
+
|
|
148
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
149
|
+
lambda: elbv2_client.describe_load_balancers()
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
albs = []
|
|
153
|
+
for lb in response.get('LoadBalancers', []):
|
|
154
|
+
# Only include Application Load Balancers
|
|
155
|
+
if lb.get('Type') == 'application':
|
|
156
|
+
albs.append({
|
|
157
|
+
'LoadBalancerArn': lb.get('LoadBalancerArn'),
|
|
158
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
159
|
+
'DNSName': lb.get('DNSName'),
|
|
160
|
+
'Scheme': lb.get('Scheme'),
|
|
161
|
+
'State': lb.get('State', {}),
|
|
162
|
+
'Type': lb.get('Type')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
logger.debug(f"Found {len(albs)} Application Load Balancers in region {region}")
|
|
166
|
+
return albs
|
|
167
|
+
|
|
168
|
+
except ClientError as e:
|
|
169
|
+
logger.error(f"Error retrieving ALBs in region {region}: {e}")
|
|
170
|
+
raise
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Unexpected error retrieving ALBs in region {region}: {e}")
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
176
|
+
"""Evaluate if ALB has HTTP to HTTPS redirection configured."""
|
|
177
|
+
lb_arn = resource.get('LoadBalancerArn', 'unknown')
|
|
178
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
182
|
+
|
|
183
|
+
# Get listeners for this load balancer
|
|
184
|
+
listeners_response = aws_factory.aws_api_call_with_retry(
|
|
185
|
+
lambda: elbv2_client.describe_listeners(LoadBalancerArn=lb_arn)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
listeners = listeners_response.get('Listeners', [])
|
|
189
|
+
http_listeners = []
|
|
190
|
+
https_listeners = []
|
|
191
|
+
http_redirect_listeners = []
|
|
192
|
+
|
|
193
|
+
for listener in listeners:
|
|
194
|
+
protocol = listener.get('Protocol', '')
|
|
195
|
+
port = listener.get('Port', 0)
|
|
196
|
+
|
|
197
|
+
if protocol == 'HTTP':
|
|
198
|
+
http_listeners.append(listener)
|
|
199
|
+
|
|
200
|
+
# Check if this HTTP listener has redirect actions
|
|
201
|
+
default_actions = listener.get('DefaultActions', [])
|
|
202
|
+
for action in default_actions:
|
|
203
|
+
if action.get('Type') == 'redirect':
|
|
204
|
+
redirect_config = action.get('RedirectConfig', {})
|
|
205
|
+
if redirect_config.get('Protocol') == 'HTTPS':
|
|
206
|
+
http_redirect_listeners.append(listener)
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
elif protocol == 'HTTPS':
|
|
210
|
+
https_listeners.append(listener)
|
|
211
|
+
|
|
212
|
+
# Evaluate compliance
|
|
213
|
+
if not http_listeners:
|
|
214
|
+
# No HTTP listeners, so no redirection needed
|
|
215
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
216
|
+
evaluation_reason = f"ALB {lb_name} has no HTTP listeners"
|
|
217
|
+
elif len(http_redirect_listeners) == len(http_listeners):
|
|
218
|
+
# All HTTP listeners have HTTPS redirection
|
|
219
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
220
|
+
evaluation_reason = f"ALB {lb_name} has HTTP to HTTPS redirection configured for all HTTP listeners"
|
|
221
|
+
else:
|
|
222
|
+
# Some HTTP listeners don't have HTTPS redirection
|
|
223
|
+
non_redirect_count = len(http_listeners) - len(http_redirect_listeners)
|
|
224
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
225
|
+
evaluation_reason = f"ALB {lb_name} has {non_redirect_count} HTTP listener(s) without HTTPS redirection"
|
|
226
|
+
|
|
227
|
+
except ClientError as e:
|
|
228
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
229
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
230
|
+
compliance_status = ComplianceStatus.ERROR
|
|
231
|
+
evaluation_reason = f"Insufficient permissions to check listeners for ALB {lb_name}"
|
|
232
|
+
else:
|
|
233
|
+
compliance_status = ComplianceStatus.ERROR
|
|
234
|
+
evaluation_reason = f"Error checking listeners for ALB {lb_name}: {str(e)}"
|
|
235
|
+
except Exception as e:
|
|
236
|
+
compliance_status = ComplianceStatus.ERROR
|
|
237
|
+
evaluation_reason = f"Unexpected error checking listeners for ALB {lb_name}: {str(e)}"
|
|
238
|
+
|
|
239
|
+
return ComplianceResult(
|
|
240
|
+
resource_id=lb_arn,
|
|
241
|
+
resource_type="AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
242
|
+
compliance_status=compliance_status,
|
|
243
|
+
evaluation_reason=evaluation_reason,
|
|
244
|
+
config_rule_name=self.rule_name,
|
|
245
|
+
region=region
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
249
|
+
"""Get specific remediation steps for ALB HTTP to HTTPS redirection."""
|
|
250
|
+
return [
|
|
251
|
+
"Identify Application Load Balancers with HTTP listeners that don't redirect to HTTPS",
|
|
252
|
+
"For each non-compliant ALB:",
|
|
253
|
+
" 1. Modify HTTP listeners to add redirect actions",
|
|
254
|
+
" 2. Configure redirect to HTTPS with appropriate status code (301 or 302)",
|
|
255
|
+
" 3. Ensure HTTPS listeners are configured with valid SSL certificates",
|
|
256
|
+
"Use AWS CLI: aws elbv2 modify-listener --listener-arn <listener-arn> --default-actions Type=redirect,RedirectConfig='{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'",
|
|
257
|
+
"Create HTTPS listener if not exists: aws elbv2 create-listener --load-balancer-arn <lb-arn> --protocol HTTPS --port 443 --certificates CertificateArn=<cert-arn>",
|
|
258
|
+
"Test redirection by accessing HTTP URLs and verifying HTTPS redirect",
|
|
259
|
+
"Update security groups to allow HTTPS traffic (port 443)",
|
|
260
|
+
"Consider removing HTTP listeners entirely if HTTPS-only is acceptable"
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ELBTLSHTTPSListenersOnlyAssessment(BaseConfigRuleAssessment):
|
|
265
|
+
"""Assessment for elb-tls-https-listeners-only Config rule."""
|
|
266
|
+
|
|
267
|
+
def __init__(self):
|
|
268
|
+
"""Initialize ELB TLS/HTTPS listeners only assessment."""
|
|
269
|
+
super().__init__(
|
|
270
|
+
rule_name="elb-tls-https-listeners-only",
|
|
271
|
+
control_id="3.10",
|
|
272
|
+
resource_types=["AWS::ElasticLoadBalancing::LoadBalancer"]
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
276
|
+
"""Get all Classic Load Balancers in the region."""
|
|
277
|
+
if resource_type != "AWS::ElasticLoadBalancing::LoadBalancer":
|
|
278
|
+
return []
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
elb_client = aws_factory.get_client('elb', region)
|
|
282
|
+
|
|
283
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
284
|
+
lambda: elb_client.describe_load_balancers()
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
elbs = []
|
|
288
|
+
for lb in response.get('LoadBalancerDescriptions', []):
|
|
289
|
+
elbs.append({
|
|
290
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
291
|
+
'DNSName': lb.get('DNSName'),
|
|
292
|
+
'Scheme': lb.get('Scheme'),
|
|
293
|
+
'ListenerDescriptions': lb.get('ListenerDescriptions', []),
|
|
294
|
+
'AvailabilityZones': lb.get('AvailabilityZones', [])
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
logger.debug(f"Found {len(elbs)} Classic Load Balancers in region {region}")
|
|
298
|
+
return elbs
|
|
299
|
+
|
|
300
|
+
except ClientError as e:
|
|
301
|
+
logger.error(f"Error retrieving Classic ELBs in region {region}: {e}")
|
|
302
|
+
raise
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Unexpected error retrieving Classic ELBs in region {region}: {e}")
|
|
305
|
+
raise
|
|
306
|
+
|
|
307
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
308
|
+
"""Evaluate if ELB uses only TLS/HTTPS listeners."""
|
|
309
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
310
|
+
listener_descriptions = resource.get('ListenerDescriptions', [])
|
|
311
|
+
|
|
312
|
+
secure_protocols = ['HTTPS', 'SSL', 'TLS']
|
|
313
|
+
insecure_protocols = ['HTTP', 'TCP']
|
|
314
|
+
|
|
315
|
+
secure_listeners = []
|
|
316
|
+
insecure_listeners = []
|
|
317
|
+
|
|
318
|
+
for listener_desc in listener_descriptions:
|
|
319
|
+
listener = listener_desc.get('Listener', {})
|
|
320
|
+
protocol = listener.get('Protocol', '')
|
|
321
|
+
load_balancer_port = listener.get('LoadBalancerPort', 0)
|
|
322
|
+
|
|
323
|
+
if protocol in secure_protocols:
|
|
324
|
+
secure_listeners.append({
|
|
325
|
+
'protocol': protocol,
|
|
326
|
+
'port': load_balancer_port
|
|
327
|
+
})
|
|
328
|
+
elif protocol in insecure_protocols:
|
|
329
|
+
# For TCP, we need to check the port to determine if it's likely secure
|
|
330
|
+
if protocol == 'TCP' and load_balancer_port in [443, 8443, 9443]:
|
|
331
|
+
# TCP on HTTPS ports is likely secure
|
|
332
|
+
secure_listeners.append({
|
|
333
|
+
'protocol': protocol,
|
|
334
|
+
'port': load_balancer_port
|
|
335
|
+
})
|
|
336
|
+
else:
|
|
337
|
+
insecure_listeners.append({
|
|
338
|
+
'protocol': protocol,
|
|
339
|
+
'port': load_balancer_port
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
# Evaluate compliance
|
|
343
|
+
if not listener_descriptions:
|
|
344
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
345
|
+
evaluation_reason = f"ELB {lb_name} has no listeners configured"
|
|
346
|
+
elif not insecure_listeners:
|
|
347
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
348
|
+
secure_count = len(secure_listeners)
|
|
349
|
+
evaluation_reason = f"ELB {lb_name} uses only secure protocols ({secure_count} secure listener(s))"
|
|
350
|
+
else:
|
|
351
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
352
|
+
insecure_count = len(insecure_listeners)
|
|
353
|
+
insecure_details = [f"{l['protocol']}:{l['port']}" for l in insecure_listeners]
|
|
354
|
+
evaluation_reason = f"ELB {lb_name} has {insecure_count} insecure listener(s): {', '.join(insecure_details)}"
|
|
355
|
+
|
|
356
|
+
return ComplianceResult(
|
|
357
|
+
resource_id=lb_name,
|
|
358
|
+
resource_type="AWS::ElasticLoadBalancing::LoadBalancer",
|
|
359
|
+
compliance_status=compliance_status,
|
|
360
|
+
evaluation_reason=evaluation_reason,
|
|
361
|
+
config_rule_name=self.rule_name,
|
|
362
|
+
region=region
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
366
|
+
"""Get specific remediation steps for ELB secure listeners."""
|
|
367
|
+
return [
|
|
368
|
+
"Identify Classic Load Balancers with insecure listeners (HTTP, TCP on non-secure ports)",
|
|
369
|
+
"For each non-compliant ELB:",
|
|
370
|
+
" 1. Create HTTPS/SSL listeners to replace HTTP/insecure TCP listeners",
|
|
371
|
+
" 2. Upload or import SSL certificates for HTTPS/SSL listeners",
|
|
372
|
+
" 3. Update security groups to allow HTTPS/SSL traffic",
|
|
373
|
+
" 4. Remove or modify insecure listeners",
|
|
374
|
+
"Use AWS CLI: aws elb create-load-balancer-listeners --load-balancer-name <lb-name> --listeners Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=<cert-arn>",
|
|
375
|
+
"Delete insecure listeners: aws elb delete-load-balancer-listeners --load-balancer-name <lb-name> --load-balancer-ports 80",
|
|
376
|
+
"Test connectivity after changes to ensure applications work correctly",
|
|
377
|
+
"Consider migrating to Application Load Balancer (ALB) for better SSL/TLS features",
|
|
378
|
+
"Update DNS records and application configurations to use HTTPS URLs"
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class S3BucketSSLRequestsOnlyAssessment(BaseConfigRuleAssessment):
|
|
383
|
+
"""Assessment for s3-bucket-ssl-requests-only Config rule."""
|
|
384
|
+
|
|
385
|
+
def __init__(self):
|
|
386
|
+
"""Initialize S3 bucket SSL requests only assessment."""
|
|
387
|
+
super().__init__(
|
|
388
|
+
rule_name="s3-bucket-ssl-requests-only",
|
|
389
|
+
control_id="3.10",
|
|
390
|
+
resource_types=["AWS::S3::Bucket"]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
394
|
+
"""Get all S3 buckets (S3 is global but we'll check from this region)."""
|
|
395
|
+
if resource_type != "AWS::S3::Bucket":
|
|
396
|
+
return []
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
400
|
+
|
|
401
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
402
|
+
lambda: s3_client.list_buckets()
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
buckets = []
|
|
406
|
+
for bucket in response.get('Buckets', []):
|
|
407
|
+
bucket_name = bucket.get('Name')
|
|
408
|
+
|
|
409
|
+
# Get bucket location to determine if we should evaluate it from this region
|
|
410
|
+
try:
|
|
411
|
+
location_response = aws_factory.aws_api_call_with_retry(
|
|
412
|
+
lambda: s3_client.get_bucket_location(Bucket=bucket_name)
|
|
413
|
+
)
|
|
414
|
+
bucket_region = location_response.get('LocationConstraint')
|
|
415
|
+
|
|
416
|
+
# Handle special case where us-east-1 returns None
|
|
417
|
+
if bucket_region is None:
|
|
418
|
+
bucket_region = 'us-east-1'
|
|
419
|
+
|
|
420
|
+
# Only include buckets in this region or if we're in us-east-1 (global)
|
|
421
|
+
if bucket_region == region or region == 'us-east-1':
|
|
422
|
+
buckets.append({
|
|
423
|
+
'Name': bucket_name,
|
|
424
|
+
'CreationDate': bucket.get('CreationDate'),
|
|
425
|
+
'Region': bucket_region
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
except ClientError as e:
|
|
429
|
+
# If we can't get bucket location, skip this bucket
|
|
430
|
+
logger.warning(f"Could not get location for bucket {bucket_name}: {e}")
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
logger.debug(f"Found {len(buckets)} S3 buckets accessible from region {region}")
|
|
434
|
+
return buckets
|
|
435
|
+
|
|
436
|
+
except ClientError as e:
|
|
437
|
+
logger.error(f"Error retrieving S3 buckets from region {region}: {e}")
|
|
438
|
+
raise
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.error(f"Unexpected error retrieving S3 buckets from region {region}: {e}")
|
|
441
|
+
raise
|
|
442
|
+
|
|
443
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
444
|
+
"""Evaluate if S3 bucket has policy requiring SSL requests."""
|
|
445
|
+
bucket_name = resource.get('Name', 'unknown')
|
|
446
|
+
bucket_region = resource.get('Region', region)
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
# Use the bucket's region for API calls
|
|
450
|
+
s3_client = aws_factory.get_client('s3', bucket_region)
|
|
451
|
+
|
|
452
|
+
# Get bucket policy
|
|
453
|
+
try:
|
|
454
|
+
policy_response = aws_factory.aws_api_call_with_retry(
|
|
455
|
+
lambda: s3_client.get_bucket_policy(Bucket=bucket_name)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
policy_document = policy_response.get('Policy', '{}')
|
|
459
|
+
policy = json.loads(policy_document)
|
|
460
|
+
|
|
461
|
+
# Check if policy denies non-SSL requests
|
|
462
|
+
has_ssl_deny_policy = self._check_ssl_deny_policy(policy)
|
|
463
|
+
|
|
464
|
+
if has_ssl_deny_policy:
|
|
465
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
466
|
+
evaluation_reason = f"S3 bucket {bucket_name} has policy requiring SSL requests"
|
|
467
|
+
else:
|
|
468
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
469
|
+
evaluation_reason = f"S3 bucket {bucket_name} does not have policy requiring SSL requests"
|
|
470
|
+
|
|
471
|
+
except ClientError as e:
|
|
472
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
473
|
+
if error_code == 'NoSuchBucketPolicy':
|
|
474
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
475
|
+
evaluation_reason = f"S3 bucket {bucket_name} has no bucket policy (SSL not required)"
|
|
476
|
+
elif error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
477
|
+
compliance_status = ComplianceStatus.ERROR
|
|
478
|
+
evaluation_reason = f"Insufficient permissions to check bucket policy for {bucket_name}"
|
|
479
|
+
else:
|
|
480
|
+
compliance_status = ComplianceStatus.ERROR
|
|
481
|
+
evaluation_reason = f"Error checking bucket policy for {bucket_name}: {str(e)}"
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
compliance_status = ComplianceStatus.ERROR
|
|
485
|
+
evaluation_reason = f"Unexpected error checking SSL policy for bucket {bucket_name}: {str(e)}"
|
|
486
|
+
|
|
487
|
+
return ComplianceResult(
|
|
488
|
+
resource_id=bucket_name,
|
|
489
|
+
resource_type="AWS::S3::Bucket",
|
|
490
|
+
compliance_status=compliance_status,
|
|
491
|
+
evaluation_reason=evaluation_reason,
|
|
492
|
+
config_rule_name=self.rule_name,
|
|
493
|
+
region=bucket_region
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def _check_ssl_deny_policy(self, policy: Dict[str, Any]) -> bool:
|
|
497
|
+
"""Check if bucket policy denies non-SSL requests."""
|
|
498
|
+
statements = policy.get('Statement', [])
|
|
499
|
+
|
|
500
|
+
for statement in statements:
|
|
501
|
+
effect = statement.get('Effect', '')
|
|
502
|
+
condition = statement.get('Condition', {})
|
|
503
|
+
|
|
504
|
+
# Look for Deny statements with SSL condition
|
|
505
|
+
if effect == 'Deny':
|
|
506
|
+
# Check for aws:SecureTransport condition
|
|
507
|
+
bool_conditions = condition.get('Bool', {})
|
|
508
|
+
if 'aws:SecureTransport' in bool_conditions:
|
|
509
|
+
secure_transport = bool_conditions['aws:SecureTransport']
|
|
510
|
+
# Policy should deny when SecureTransport is false
|
|
511
|
+
if secure_transport == 'false' or secure_transport is False:
|
|
512
|
+
return True
|
|
513
|
+
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
517
|
+
"""Get specific remediation steps for S3 SSL-only policy."""
|
|
518
|
+
return [
|
|
519
|
+
"Identify S3 buckets without SSL-only access policies",
|
|
520
|
+
"For each non-compliant bucket:",
|
|
521
|
+
" 1. Create or update bucket policy to deny non-SSL requests",
|
|
522
|
+
" 2. Add condition 'aws:SecureTransport': 'false' with Effect: Deny",
|
|
523
|
+
" 3. Test policy to ensure it works correctly",
|
|
524
|
+
"Example bucket policy to deny non-SSL requests:",
|
|
525
|
+
'''{
|
|
526
|
+
"Version": "2012-10-17",
|
|
527
|
+
"Statement": [
|
|
528
|
+
{
|
|
529
|
+
"Sid": "DenyInsecureConnections",
|
|
530
|
+
"Effect": "Deny",
|
|
531
|
+
"Principal": "*",
|
|
532
|
+
"Action": "s3:*",
|
|
533
|
+
"Resource": [
|
|
534
|
+
"arn:aws:s3:::BUCKET_NAME",
|
|
535
|
+
"arn:aws:s3:::BUCKET_NAME/*"
|
|
536
|
+
],
|
|
537
|
+
"Condition": {
|
|
538
|
+
"Bool": {
|
|
539
|
+
"aws:SecureTransport": "false"
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
]
|
|
544
|
+
}''',
|
|
545
|
+
"Use AWS CLI: aws s3api put-bucket-policy --bucket <bucket-name> --policy file://ssl-only-policy.json",
|
|
546
|
+
"Test with HTTP request to verify denial",
|
|
547
|
+
"Update applications to use HTTPS for S3 access"
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class RedshiftRequireTLSSSLAssessment(BaseConfigRuleAssessment):
|
|
552
|
+
"""Assessment for redshift-require-tls-ssl Config rule."""
|
|
553
|
+
|
|
554
|
+
def __init__(self):
|
|
555
|
+
"""Initialize Redshift require TLS/SSL assessment."""
|
|
556
|
+
super().__init__(
|
|
557
|
+
rule_name="redshift-require-tls-ssl",
|
|
558
|
+
control_id="3.10",
|
|
559
|
+
resource_types=["AWS::Redshift::Cluster"]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
563
|
+
"""Get all Redshift clusters in the region."""
|
|
564
|
+
if resource_type != "AWS::Redshift::Cluster":
|
|
565
|
+
return []
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
redshift_client = aws_factory.get_client('redshift', region)
|
|
569
|
+
|
|
570
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
571
|
+
lambda: redshift_client.describe_clusters()
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
clusters = []
|
|
575
|
+
for cluster in response.get('Clusters', []):
|
|
576
|
+
clusters.append({
|
|
577
|
+
'ClusterIdentifier': cluster.get('ClusterIdentifier'),
|
|
578
|
+
'ClusterStatus': cluster.get('ClusterStatus'),
|
|
579
|
+
'NodeType': cluster.get('NodeType'),
|
|
580
|
+
'NumberOfNodes': cluster.get('NumberOfNodes'),
|
|
581
|
+
'Endpoint': cluster.get('Endpoint', {}),
|
|
582
|
+
'ClusterParameterGroups': cluster.get('ClusterParameterGroups', []),
|
|
583
|
+
'Tags': cluster.get('Tags', [])
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
logger.debug(f"Found {len(clusters)} Redshift clusters in region {region}")
|
|
587
|
+
return clusters
|
|
588
|
+
|
|
589
|
+
except ClientError as e:
|
|
590
|
+
logger.error(f"Error retrieving Redshift clusters in region {region}: {e}")
|
|
591
|
+
raise
|
|
592
|
+
except Exception as e:
|
|
593
|
+
logger.error(f"Unexpected error retrieving Redshift clusters in region {region}: {e}")
|
|
594
|
+
raise
|
|
595
|
+
|
|
596
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
597
|
+
"""Evaluate if Redshift cluster requires TLS/SSL connections."""
|
|
598
|
+
cluster_id = resource.get('ClusterIdentifier', 'unknown')
|
|
599
|
+
cluster_status = resource.get('ClusterStatus', 'unknown')
|
|
600
|
+
parameter_groups = resource.get('ClusterParameterGroups', [])
|
|
601
|
+
|
|
602
|
+
# Skip clusters that are not available
|
|
603
|
+
if cluster_status not in ['available']:
|
|
604
|
+
return ComplianceResult(
|
|
605
|
+
resource_id=cluster_id,
|
|
606
|
+
resource_type="AWS::Redshift::Cluster",
|
|
607
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
608
|
+
evaluation_reason=f"Redshift cluster {cluster_id} is in status '{cluster_status}', not available",
|
|
609
|
+
config_rule_name=self.rule_name,
|
|
610
|
+
region=region
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
redshift_client = aws_factory.get_client('redshift', region)
|
|
615
|
+
|
|
616
|
+
# Check parameter groups for require_ssl setting
|
|
617
|
+
ssl_required = False
|
|
618
|
+
checked_groups = []
|
|
619
|
+
|
|
620
|
+
for param_group in parameter_groups:
|
|
621
|
+
group_name = param_group.get('ParameterGroupName')
|
|
622
|
+
if group_name:
|
|
623
|
+
checked_groups.append(group_name)
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
# Get parameters for this group
|
|
627
|
+
params_response = aws_factory.aws_api_call_with_retry(
|
|
628
|
+
lambda: redshift_client.describe_cluster_parameters(
|
|
629
|
+
ParameterGroupName=group_name,
|
|
630
|
+
Source='user'
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
parameters = params_response.get('Parameters', [])
|
|
635
|
+
|
|
636
|
+
# Look for require_ssl parameter
|
|
637
|
+
for param in parameters:
|
|
638
|
+
if param.get('ParameterName') == 'require_ssl':
|
|
639
|
+
param_value = param.get('ParameterValue', 'false')
|
|
640
|
+
if param_value.lower() == 'true':
|
|
641
|
+
ssl_required = True
|
|
642
|
+
break
|
|
643
|
+
|
|
644
|
+
if ssl_required:
|
|
645
|
+
break
|
|
646
|
+
|
|
647
|
+
except ClientError as e:
|
|
648
|
+
logger.warning(f"Could not get parameters for group {group_name}: {e}")
|
|
649
|
+
continue
|
|
650
|
+
|
|
651
|
+
# Evaluate compliance
|
|
652
|
+
if ssl_required:
|
|
653
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
654
|
+
evaluation_reason = f"Redshift cluster {cluster_id} has require_ssl parameter set to true"
|
|
655
|
+
else:
|
|
656
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
657
|
+
if checked_groups:
|
|
658
|
+
evaluation_reason = f"Redshift cluster {cluster_id} does not have require_ssl parameter set to true (checked groups: {', '.join(checked_groups)})"
|
|
659
|
+
else:
|
|
660
|
+
evaluation_reason = f"Redshift cluster {cluster_id} has no parameter groups to check for SSL requirement"
|
|
661
|
+
|
|
662
|
+
except ClientError as e:
|
|
663
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
664
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
665
|
+
compliance_status = ComplianceStatus.ERROR
|
|
666
|
+
evaluation_reason = f"Insufficient permissions to check parameters for Redshift cluster {cluster_id}"
|
|
667
|
+
else:
|
|
668
|
+
compliance_status = ComplianceStatus.ERROR
|
|
669
|
+
evaluation_reason = f"Error checking SSL parameters for Redshift cluster {cluster_id}: {str(e)}"
|
|
670
|
+
except Exception as e:
|
|
671
|
+
compliance_status = ComplianceStatus.ERROR
|
|
672
|
+
evaluation_reason = f"Unexpected error checking SSL parameters for Redshift cluster {cluster_id}: {str(e)}"
|
|
673
|
+
|
|
674
|
+
return ComplianceResult(
|
|
675
|
+
resource_id=cluster_id,
|
|
676
|
+
resource_type="AWS::Redshift::Cluster",
|
|
677
|
+
compliance_status=compliance_status,
|
|
678
|
+
evaluation_reason=evaluation_reason,
|
|
679
|
+
config_rule_name=self.rule_name,
|
|
680
|
+
region=region
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
684
|
+
"""Get specific remediation steps for Redshift SSL requirement."""
|
|
685
|
+
return [
|
|
686
|
+
"Identify Redshift clusters without SSL requirement enabled",
|
|
687
|
+
"For each non-compliant cluster:",
|
|
688
|
+
" 1. Create or modify cluster parameter group to set require_ssl=true",
|
|
689
|
+
" 2. Apply the parameter group to the cluster",
|
|
690
|
+
" 3. Reboot the cluster to apply the parameter change",
|
|
691
|
+
"Use AWS CLI: aws redshift create-cluster-parameter-group --parameter-group-name ssl-required-group --parameter-group-family redshift-1.0 --description 'Parameter group requiring SSL'",
|
|
692
|
+
"Set parameter: aws redshift modify-cluster-parameter-group --parameter-group-name ssl-required-group --parameters ParameterName=require_ssl,ParameterValue=true",
|
|
693
|
+
"Apply to cluster: aws redshift modify-cluster --cluster-identifier <cluster-id> --cluster-parameter-group-name ssl-required-group",
|
|
694
|
+
"Reboot cluster: aws redshift reboot-cluster --cluster-identifier <cluster-id>",
|
|
695
|
+
"Update client applications to use SSL connections",
|
|
696
|
+
"Test connectivity with SSL to ensure applications work correctly",
|
|
697
|
+
"Monitor cluster performance after enabling SSL requirement"
|
|
698
|
+
]
|