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.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. 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
+ ]