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,382 @@
|
|
|
1
|
+
"""Control 3.10: Encrypt Sensitive Data in Transit assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
8
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
9
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ELBACMCertificateRequiredAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for elb-acm-certificate-required Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize ELB ACM certificate assessment."""
|
|
19
|
+
super().__init__(
|
|
20
|
+
rule_name="elb-acm-certificate-required",
|
|
21
|
+
control_id="3.10",
|
|
22
|
+
resource_types=["AWS::ElasticLoadBalancing::LoadBalancer"]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
26
|
+
"""Get all Classic Load Balancers in the region."""
|
|
27
|
+
if resource_type != "AWS::ElasticLoadBalancing::LoadBalancer":
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
elb_client = aws_factory.get_client('elb', region)
|
|
32
|
+
|
|
33
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
34
|
+
lambda: elb_client.describe_load_balancers()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
load_balancers = []
|
|
38
|
+
for lb in response.get('LoadBalancerDescriptions', []):
|
|
39
|
+
load_balancers.append({
|
|
40
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
41
|
+
'DNSName': lb.get('DNSName'),
|
|
42
|
+
'Scheme': lb.get('Scheme'),
|
|
43
|
+
'ListenerDescriptions': lb.get('ListenerDescriptions', []),
|
|
44
|
+
'VPCId': lb.get('VPCId')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
logger.debug(f"Found {len(load_balancers)} Classic Load Balancers in region {region}")
|
|
48
|
+
return load_balancers
|
|
49
|
+
|
|
50
|
+
except ClientError as e:
|
|
51
|
+
logger.error(f"Error retrieving Classic Load Balancers in region {region}: {e}")
|
|
52
|
+
raise
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Unexpected error retrieving Classic Load Balancers in region {region}: {e}")
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
58
|
+
"""Evaluate if Classic Load Balancer uses ACM certificates for HTTPS/SSL listeners."""
|
|
59
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
60
|
+
listener_descriptions = resource.get('ListenerDescriptions', [])
|
|
61
|
+
|
|
62
|
+
https_listeners = []
|
|
63
|
+
non_acm_listeners = []
|
|
64
|
+
|
|
65
|
+
for listener_desc in listener_descriptions:
|
|
66
|
+
listener = listener_desc.get('Listener', {})
|
|
67
|
+
protocol = listener.get('Protocol', '')
|
|
68
|
+
ssl_certificate_id = listener.get('SSLCertificateId', '')
|
|
69
|
+
|
|
70
|
+
if protocol in ['HTTPS', 'SSL']:
|
|
71
|
+
https_listeners.append({
|
|
72
|
+
'Protocol': protocol,
|
|
73
|
+
'LoadBalancerPort': listener.get('LoadBalancerPort'),
|
|
74
|
+
'SSLCertificateId': ssl_certificate_id
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
# Check if certificate is from ACM (ACM ARNs contain 'acm')
|
|
78
|
+
if ssl_certificate_id and 'acm' not in ssl_certificate_id.lower():
|
|
79
|
+
non_acm_listeners.append({
|
|
80
|
+
'Protocol': protocol,
|
|
81
|
+
'LoadBalancerPort': listener.get('LoadBalancerPort'),
|
|
82
|
+
'SSLCertificateId': ssl_certificate_id
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if not https_listeners:
|
|
86
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
87
|
+
evaluation_reason = f"Load Balancer {lb_name} has no HTTPS/SSL listeners"
|
|
88
|
+
elif non_acm_listeners:
|
|
89
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
90
|
+
listener_details = [f"{l['Protocol']}:{l['LoadBalancerPort']}" for l in non_acm_listeners]
|
|
91
|
+
evaluation_reason = f"Load Balancer {lb_name} has {len(non_acm_listeners)} HTTPS/SSL listener(s) not using ACM certificates: {', '.join(listener_details)}"
|
|
92
|
+
else:
|
|
93
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
94
|
+
evaluation_reason = f"Load Balancer {lb_name} has {len(https_listeners)} HTTPS/SSL listener(s) using ACM certificates"
|
|
95
|
+
|
|
96
|
+
return ComplianceResult(
|
|
97
|
+
resource_id=lb_name,
|
|
98
|
+
resource_type="AWS::ElasticLoadBalancing::LoadBalancer",
|
|
99
|
+
compliance_status=compliance_status,
|
|
100
|
+
evaluation_reason=evaluation_reason,
|
|
101
|
+
config_rule_name=self.rule_name,
|
|
102
|
+
region=region
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
106
|
+
"""Get specific remediation steps for ELB ACM certificates."""
|
|
107
|
+
return [
|
|
108
|
+
"Identify Classic Load Balancers with HTTPS/SSL listeners not using ACM certificates",
|
|
109
|
+
"For each non-compliant load balancer:",
|
|
110
|
+
" 1. Request or import a certificate in AWS Certificate Manager",
|
|
111
|
+
" 2. Update the load balancer listener to use the ACM certificate",
|
|
112
|
+
" 3. Test the SSL/TLS configuration",
|
|
113
|
+
" 4. Remove the old certificate if it's no longer needed",
|
|
114
|
+
"Use AWS CLI: aws acm request-certificate --domain-name <domain>",
|
|
115
|
+
"Use AWS CLI: aws elb set-load-balancer-listener-ssl-certificate --load-balancer-name <name> --load-balancer-port <port> --ssl-certificate-id <acm-arn>",
|
|
116
|
+
"Enable automatic certificate renewal for ACM certificates",
|
|
117
|
+
"Monitor certificate expiration dates and renewal status"
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ELBv2ACMCertificateRequiredAssessment(BaseConfigRuleAssessment):
|
|
122
|
+
"""Assessment for elbv2-acm-certificate-required Config rule."""
|
|
123
|
+
|
|
124
|
+
def __init__(self):
|
|
125
|
+
"""Initialize ELBv2 ACM certificate assessment."""
|
|
126
|
+
super().__init__(
|
|
127
|
+
rule_name="elbv2-acm-certificate-required",
|
|
128
|
+
control_id="3.10",
|
|
129
|
+
resource_types=["AWS::ElasticLoadBalancingV2::LoadBalancer"]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
133
|
+
"""Get all Application/Network Load Balancers in the region."""
|
|
134
|
+
if resource_type != "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
139
|
+
|
|
140
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
141
|
+
lambda: elbv2_client.describe_load_balancers()
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
load_balancers = []
|
|
145
|
+
for lb in response.get('LoadBalancers', []):
|
|
146
|
+
load_balancers.append({
|
|
147
|
+
'LoadBalancerArn': lb.get('LoadBalancerArn'),
|
|
148
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
149
|
+
'DNSName': lb.get('DNSName'),
|
|
150
|
+
'Scheme': lb.get('Scheme'),
|
|
151
|
+
'Type': lb.get('Type'),
|
|
152
|
+
'VpcId': lb.get('VpcId')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
logger.debug(f"Found {len(load_balancers)} Application/Network Load Balancers in region {region}")
|
|
156
|
+
return load_balancers
|
|
157
|
+
|
|
158
|
+
except ClientError as e:
|
|
159
|
+
logger.error(f"Error retrieving Application/Network Load Balancers in region {region}: {e}")
|
|
160
|
+
raise
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Unexpected error retrieving Application/Network Load Balancers in region {region}: {e}")
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
166
|
+
"""Evaluate if ALB/NLB uses ACM certificates for HTTPS/TLS listeners."""
|
|
167
|
+
lb_arn = resource.get('LoadBalancerArn', 'unknown')
|
|
168
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
169
|
+
lb_type = resource.get('Type', 'unknown')
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
173
|
+
|
|
174
|
+
# Get listeners for the load balancer
|
|
175
|
+
listeners_response = aws_factory.aws_api_call_with_retry(
|
|
176
|
+
lambda: elbv2_client.describe_listeners(LoadBalancerArn=lb_arn)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
https_listeners = []
|
|
180
|
+
non_acm_listeners = []
|
|
181
|
+
|
|
182
|
+
for listener in listeners_response.get('Listeners', []):
|
|
183
|
+
protocol = listener.get('Protocol', '')
|
|
184
|
+
port = listener.get('Port')
|
|
185
|
+
certificates = listener.get('Certificates', [])
|
|
186
|
+
|
|
187
|
+
if protocol in ['HTTPS', 'TLS']:
|
|
188
|
+
https_listeners.append({
|
|
189
|
+
'Protocol': protocol,
|
|
190
|
+
'Port': port,
|
|
191
|
+
'Certificates': certificates
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
# Check if any certificate is not from ACM
|
|
195
|
+
for cert in certificates:
|
|
196
|
+
cert_arn = cert.get('CertificateArn', '')
|
|
197
|
+
if cert_arn and 'acm' not in cert_arn.lower():
|
|
198
|
+
non_acm_listeners.append({
|
|
199
|
+
'Protocol': protocol,
|
|
200
|
+
'Port': port,
|
|
201
|
+
'CertificateArn': cert_arn
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
if not https_listeners:
|
|
205
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
206
|
+
evaluation_reason = f"Load Balancer {lb_name} has no HTTPS/TLS listeners"
|
|
207
|
+
elif non_acm_listeners:
|
|
208
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
209
|
+
listener_details = [f"{l['Protocol']}:{l['Port']}" for l in non_acm_listeners]
|
|
210
|
+
evaluation_reason = f"Load Balancer {lb_name} has {len(non_acm_listeners)} HTTPS/TLS listener(s) not using ACM certificates: {', '.join(listener_details)}"
|
|
211
|
+
else:
|
|
212
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
213
|
+
evaluation_reason = f"Load Balancer {lb_name} has {len(https_listeners)} HTTPS/TLS listener(s) using ACM certificates"
|
|
214
|
+
|
|
215
|
+
except ClientError as e:
|
|
216
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
217
|
+
if error_code in ['AccessDenied', 'LoadBalancerNotFound']:
|
|
218
|
+
compliance_status = ComplianceStatus.ERROR
|
|
219
|
+
evaluation_reason = f"Cannot access load balancer {lb_name}: {error_code}"
|
|
220
|
+
else:
|
|
221
|
+
compliance_status = ComplianceStatus.ERROR
|
|
222
|
+
evaluation_reason = f"Error checking load balancer {lb_name}: {str(e)}"
|
|
223
|
+
except Exception as e:
|
|
224
|
+
compliance_status = ComplianceStatus.ERROR
|
|
225
|
+
evaluation_reason = f"Unexpected error checking load balancer {lb_name}: {str(e)}"
|
|
226
|
+
|
|
227
|
+
return ComplianceResult(
|
|
228
|
+
resource_id=lb_arn,
|
|
229
|
+
resource_type="AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
230
|
+
compliance_status=compliance_status,
|
|
231
|
+
evaluation_reason=evaluation_reason,
|
|
232
|
+
config_rule_name=self.rule_name,
|
|
233
|
+
region=region
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
237
|
+
"""Get specific remediation steps for ELBv2 ACM certificates."""
|
|
238
|
+
return [
|
|
239
|
+
"Identify Application/Network Load Balancers with HTTPS/TLS listeners not using ACM certificates",
|
|
240
|
+
"For each non-compliant load balancer:",
|
|
241
|
+
" 1. Request or import a certificate in AWS Certificate Manager",
|
|
242
|
+
" 2. Update the listener to use the ACM certificate",
|
|
243
|
+
" 3. Test the SSL/TLS configuration",
|
|
244
|
+
" 4. Remove the old certificate if it's no longer needed",
|
|
245
|
+
"Use AWS CLI: aws acm request-certificate --domain-name <domain>",
|
|
246
|
+
"Use AWS CLI: aws elbv2 modify-listener --listener-arn <arn> --certificates CertificateArn=<acm-arn>",
|
|
247
|
+
"Enable automatic certificate renewal for ACM certificates",
|
|
248
|
+
"Monitor certificate expiration dates and renewal status"
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class OpenSearchHTTPSRequiredAssessment(BaseConfigRuleAssessment):
|
|
253
|
+
"""Assessment for opensearch-https-required Config rule."""
|
|
254
|
+
|
|
255
|
+
def __init__(self):
|
|
256
|
+
"""Initialize OpenSearch HTTPS assessment."""
|
|
257
|
+
super().__init__(
|
|
258
|
+
rule_name="opensearch-https-required",
|
|
259
|
+
control_id="3.10",
|
|
260
|
+
resource_types=["AWS::OpenSearch::Domain"]
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
264
|
+
"""Get all OpenSearch domains in the region."""
|
|
265
|
+
if resource_type != "AWS::OpenSearch::Domain":
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
opensearch_client = aws_factory.get_client('opensearch', region)
|
|
270
|
+
|
|
271
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
272
|
+
lambda: opensearch_client.list_domain_names()
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
domains = []
|
|
276
|
+
for domain in response.get('DomainNames', []):
|
|
277
|
+
domain_name = domain.get('DomainName')
|
|
278
|
+
if domain_name:
|
|
279
|
+
domains.append({
|
|
280
|
+
'DomainName': domain_name,
|
|
281
|
+
'EngineType': domain.get('EngineType', 'OpenSearch')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
logger.debug(f"Found {len(domains)} OpenSearch domains in region {region}")
|
|
285
|
+
return domains
|
|
286
|
+
|
|
287
|
+
except ClientError as e:
|
|
288
|
+
# Try Elasticsearch service if OpenSearch is not available
|
|
289
|
+
if e.response.get('Error', {}).get('Code') in ['UnknownOperationException', 'InvalidAction']:
|
|
290
|
+
try:
|
|
291
|
+
es_client = aws_factory.get_client('es', region)
|
|
292
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
293
|
+
lambda: es_client.list_domain_names()
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
domains = []
|
|
297
|
+
for domain in response.get('DomainNames', []):
|
|
298
|
+
domain_name = domain.get('DomainName')
|
|
299
|
+
if domain_name:
|
|
300
|
+
domains.append({
|
|
301
|
+
'DomainName': domain_name,
|
|
302
|
+
'EngineType': 'Elasticsearch'
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
logger.debug(f"Found {len(domains)} Elasticsearch domains in region {region}")
|
|
306
|
+
return domains
|
|
307
|
+
except ClientError as es_error:
|
|
308
|
+
logger.error(f"Error retrieving Elasticsearch domains in region {region}: {es_error}")
|
|
309
|
+
raise es_error
|
|
310
|
+
else:
|
|
311
|
+
logger.error(f"Error retrieving OpenSearch domains in region {region}: {e}")
|
|
312
|
+
raise
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Unexpected error retrieving OpenSearch domains in region {region}: {e}")
|
|
315
|
+
raise
|
|
316
|
+
|
|
317
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
318
|
+
"""Evaluate if OpenSearch domain requires HTTPS."""
|
|
319
|
+
domain_name = resource.get('DomainName', 'unknown')
|
|
320
|
+
engine_type = resource.get('EngineType', 'OpenSearch')
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
if engine_type == 'Elasticsearch':
|
|
324
|
+
client = aws_factory.get_client('es', region)
|
|
325
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
326
|
+
lambda: client.describe_elasticsearch_domain(DomainName=domain_name)
|
|
327
|
+
)
|
|
328
|
+
domain_config = response.get('DomainStatus', {})
|
|
329
|
+
else:
|
|
330
|
+
client = aws_factory.get_client('opensearch', region)
|
|
331
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
332
|
+
lambda: client.describe_domain(DomainName=domain_name)
|
|
333
|
+
)
|
|
334
|
+
domain_config = response.get('DomainStatus', {})
|
|
335
|
+
|
|
336
|
+
# Check domain endpoint options
|
|
337
|
+
domain_endpoint_options = domain_config.get('DomainEndpointOptions', {})
|
|
338
|
+
enforce_https = domain_endpoint_options.get('EnforceHTTPS', False)
|
|
339
|
+
|
|
340
|
+
if enforce_https:
|
|
341
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
342
|
+
evaluation_reason = f"{engine_type} domain {domain_name} enforces HTTPS"
|
|
343
|
+
else:
|
|
344
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
345
|
+
evaluation_reason = f"{engine_type} domain {domain_name} does not enforce HTTPS"
|
|
346
|
+
|
|
347
|
+
except ClientError as e:
|
|
348
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
349
|
+
if error_code in ['AccessDenied', 'ResourceNotFoundException']:
|
|
350
|
+
compliance_status = ComplianceStatus.ERROR
|
|
351
|
+
evaluation_reason = f"Cannot access {engine_type} domain {domain_name}: {error_code}"
|
|
352
|
+
else:
|
|
353
|
+
compliance_status = ComplianceStatus.ERROR
|
|
354
|
+
evaluation_reason = f"Error checking {engine_type} domain {domain_name}: {str(e)}"
|
|
355
|
+
except Exception as e:
|
|
356
|
+
compliance_status = ComplianceStatus.ERROR
|
|
357
|
+
evaluation_reason = f"Unexpected error checking {engine_type} domain {domain_name}: {str(e)}"
|
|
358
|
+
|
|
359
|
+
return ComplianceResult(
|
|
360
|
+
resource_id=domain_name,
|
|
361
|
+
resource_type="AWS::OpenSearch::Domain",
|
|
362
|
+
compliance_status=compliance_status,
|
|
363
|
+
evaluation_reason=evaluation_reason,
|
|
364
|
+
config_rule_name=self.rule_name,
|
|
365
|
+
region=region
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
369
|
+
"""Get specific remediation steps for OpenSearch HTTPS enforcement."""
|
|
370
|
+
return [
|
|
371
|
+
"Identify OpenSearch/Elasticsearch domains that do not enforce HTTPS",
|
|
372
|
+
"For each non-compliant domain:",
|
|
373
|
+
" 1. Plan for a maintenance window as this requires domain update",
|
|
374
|
+
" 2. Update domain configuration to enforce HTTPS",
|
|
375
|
+
" 3. Update applications to use HTTPS endpoints only",
|
|
376
|
+
" 4. Test connectivity and functionality",
|
|
377
|
+
" 5. Monitor domain health after the change",
|
|
378
|
+
"Use AWS CLI: aws opensearch update-domain-config --domain-name <name> --domain-endpoint-options EnforceHTTPS=true",
|
|
379
|
+
"Use AWS CLI: aws es update-elasticsearch-domain-config --domain-name <name> --domain-endpoint-options EnforceHTTPS=true",
|
|
380
|
+
"Consider enabling node-to-node encryption for additional security",
|
|
381
|
+
"Update security groups and NACLs to allow only HTTPS traffic"
|
|
382
|
+
]
|