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,467 @@
|
|
|
1
|
+
"""Network and High Availability Rules - AWS Config rule 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 ELBCrossZoneLoadBalancingEnabledAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for elb-cross-zone-load-balancing-enabled AWS Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__(
|
|
19
|
+
rule_name="elb-cross-zone-load-balancing-enabled",
|
|
20
|
+
control_id="12.2",
|
|
21
|
+
resource_types=["AWS::ElasticLoadBalancing::LoadBalancer"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Get Classic Load Balancers."""
|
|
26
|
+
if resource_type != "AWS::ElasticLoadBalancing::LoadBalancer":
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
elb_client = aws_factory.get_client('elb', region)
|
|
31
|
+
|
|
32
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
33
|
+
lambda: elb_client.describe_load_balancers()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
load_balancers = []
|
|
37
|
+
for lb in response.get('LoadBalancerDescriptions', []):
|
|
38
|
+
load_balancers.append({
|
|
39
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
40
|
+
'DNSName': lb.get('DNSName')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return load_balancers
|
|
44
|
+
|
|
45
|
+
except ClientError as e:
|
|
46
|
+
logger.error(f"Error retrieving Classic Load Balancers in region {region}: {e}")
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
50
|
+
"""Evaluate if Classic Load Balancer has cross-zone load balancing enabled."""
|
|
51
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
elb_client = aws_factory.get_client('elb', region)
|
|
55
|
+
|
|
56
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
57
|
+
lambda: elb_client.describe_load_balancer_attributes(LoadBalancerName=lb_name)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
attributes = response.get('LoadBalancerAttributes', {})
|
|
61
|
+
cross_zone_enabled = attributes.get('CrossZoneLoadBalancing', {}).get('Enabled', False)
|
|
62
|
+
|
|
63
|
+
if cross_zone_enabled:
|
|
64
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
65
|
+
evaluation_reason = f"Classic Load Balancer {lb_name} has cross-zone load balancing enabled"
|
|
66
|
+
else:
|
|
67
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
68
|
+
evaluation_reason = f"Classic Load Balancer {lb_name} does not have cross-zone load balancing enabled"
|
|
69
|
+
|
|
70
|
+
except ClientError as e:
|
|
71
|
+
compliance_status = ComplianceStatus.ERROR
|
|
72
|
+
evaluation_reason = f"Error checking cross-zone load balancing for {lb_name}: {str(e)}"
|
|
73
|
+
|
|
74
|
+
return ComplianceResult(
|
|
75
|
+
resource_id=lb_name,
|
|
76
|
+
resource_type="AWS::ElasticLoadBalancing::LoadBalancer",
|
|
77
|
+
compliance_status=compliance_status,
|
|
78
|
+
evaluation_reason=evaluation_reason,
|
|
79
|
+
config_rule_name=self.rule_name,
|
|
80
|
+
region=region
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ELBDeletionProtectionEnabledAssessment(BaseConfigRuleAssessment):
|
|
85
|
+
"""Assessment for elb-deletion-protection-enabled AWS Config rule."""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__(
|
|
89
|
+
rule_name="elb-deletion-protection-enabled",
|
|
90
|
+
control_id="11.4",
|
|
91
|
+
resource_types=["AWS::ElasticLoadBalancingV2::LoadBalancer"]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
95
|
+
"""Get Application/Network Load Balancers."""
|
|
96
|
+
if resource_type != "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
101
|
+
|
|
102
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
103
|
+
lambda: elbv2_client.describe_load_balancers()
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
load_balancers = []
|
|
107
|
+
for lb in response.get('LoadBalancers', []):
|
|
108
|
+
load_balancers.append({
|
|
109
|
+
'LoadBalancerArn': lb.get('LoadBalancerArn'),
|
|
110
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
111
|
+
'Type': lb.get('Type')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return load_balancers
|
|
115
|
+
|
|
116
|
+
except ClientError as e:
|
|
117
|
+
logger.error(f"Error retrieving ALB/NLB Load Balancers in region {region}: {e}")
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
121
|
+
"""Evaluate if ALB/NLB has deletion protection enabled."""
|
|
122
|
+
lb_arn = resource.get('LoadBalancerArn', 'unknown')
|
|
123
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
127
|
+
|
|
128
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
129
|
+
lambda: elbv2_client.describe_load_balancer_attributes(LoadBalancerArn=lb_arn)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
attributes = response.get('Attributes', [])
|
|
133
|
+
deletion_protection_enabled = False
|
|
134
|
+
|
|
135
|
+
for attr in attributes:
|
|
136
|
+
if attr.get('Key') == 'deletion_protection.enabled':
|
|
137
|
+
deletion_protection_enabled = attr.get('Value', 'false').lower() == 'true'
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
if deletion_protection_enabled:
|
|
141
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
142
|
+
evaluation_reason = f"Load Balancer {lb_name} has deletion protection enabled"
|
|
143
|
+
else:
|
|
144
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
145
|
+
evaluation_reason = f"Load Balancer {lb_name} does not have deletion protection enabled"
|
|
146
|
+
|
|
147
|
+
except ClientError as e:
|
|
148
|
+
compliance_status = ComplianceStatus.ERROR
|
|
149
|
+
evaluation_reason = f"Error checking deletion protection for {lb_name}: {str(e)}"
|
|
150
|
+
|
|
151
|
+
return ComplianceResult(
|
|
152
|
+
resource_id=lb_name,
|
|
153
|
+
resource_type="AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
154
|
+
compliance_status=compliance_status,
|
|
155
|
+
evaluation_reason=evaluation_reason,
|
|
156
|
+
config_rule_name=self.rule_name,
|
|
157
|
+
region=region
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ELBv2MultipleAZAssessment(BaseConfigRuleAssessment):
|
|
162
|
+
"""Assessment for elbv2-multiple-az AWS Config rule."""
|
|
163
|
+
|
|
164
|
+
def __init__(self):
|
|
165
|
+
super().__init__(
|
|
166
|
+
rule_name="elbv2-multiple-az",
|
|
167
|
+
control_id="12.2",
|
|
168
|
+
resource_types=["AWS::ElasticLoadBalancingV2::LoadBalancer"]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
172
|
+
"""Get Application/Network Load Balancers."""
|
|
173
|
+
if resource_type != "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
elbv2_client = aws_factory.get_client('elbv2', region)
|
|
178
|
+
|
|
179
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
180
|
+
lambda: elbv2_client.describe_load_balancers()
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
load_balancers = []
|
|
184
|
+
for lb in response.get('LoadBalancers', []):
|
|
185
|
+
load_balancers.append({
|
|
186
|
+
'LoadBalancerArn': lb.get('LoadBalancerArn'),
|
|
187
|
+
'LoadBalancerName': lb.get('LoadBalancerName'),
|
|
188
|
+
'AvailabilityZones': lb.get('AvailabilityZones', [])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return load_balancers
|
|
192
|
+
|
|
193
|
+
except ClientError as e:
|
|
194
|
+
logger.error(f"Error retrieving ALB/NLB Load Balancers in region {region}: {e}")
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
198
|
+
"""Evaluate if ALB/NLB spans multiple availability zones."""
|
|
199
|
+
lb_name = resource.get('LoadBalancerName', 'unknown')
|
|
200
|
+
availability_zones = resource.get('AvailabilityZones', [])
|
|
201
|
+
|
|
202
|
+
az_count = len(availability_zones)
|
|
203
|
+
|
|
204
|
+
if az_count >= 2:
|
|
205
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
206
|
+
evaluation_reason = f"Load Balancer {lb_name} spans {az_count} availability zones"
|
|
207
|
+
else:
|
|
208
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
209
|
+
evaluation_reason = f"Load Balancer {lb_name} only spans {az_count} availability zone(s), minimum 2 required"
|
|
210
|
+
|
|
211
|
+
return ComplianceResult(
|
|
212
|
+
resource_id=lb_name,
|
|
213
|
+
resource_type="AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
214
|
+
compliance_status=compliance_status,
|
|
215
|
+
evaluation_reason=evaluation_reason,
|
|
216
|
+
config_rule_name=self.rule_name,
|
|
217
|
+
region=region
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class RDSClusterMultiAZEnabledAssessment(BaseConfigRuleAssessment):
|
|
222
|
+
"""Assessment for rds-cluster-multi-az-enabled AWS Config rule."""
|
|
223
|
+
|
|
224
|
+
def __init__(self):
|
|
225
|
+
super().__init__(
|
|
226
|
+
rule_name="rds-cluster-multi-az-enabled",
|
|
227
|
+
control_id="12.2",
|
|
228
|
+
resource_types=["AWS::RDS::DBCluster"]
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
232
|
+
"""Get RDS clusters."""
|
|
233
|
+
if resource_type != "AWS::RDS::DBCluster":
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
238
|
+
|
|
239
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
240
|
+
lambda: rds_client.describe_db_clusters()
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
clusters = []
|
|
244
|
+
for cluster in response.get('DBClusters', []):
|
|
245
|
+
clusters.append({
|
|
246
|
+
'DBClusterIdentifier': cluster.get('DBClusterIdentifier'),
|
|
247
|
+
'MultiAZ': cluster.get('MultiAZ', False),
|
|
248
|
+
'AvailabilityZones': cluster.get('AvailabilityZones', [])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return clusters
|
|
252
|
+
|
|
253
|
+
except ClientError as e:
|
|
254
|
+
logger.error(f"Error retrieving RDS clusters in region {region}: {e}")
|
|
255
|
+
raise
|
|
256
|
+
|
|
257
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
258
|
+
"""Evaluate if RDS cluster has Multi-AZ enabled."""
|
|
259
|
+
cluster_id = resource.get('DBClusterIdentifier', 'unknown')
|
|
260
|
+
multi_az = resource.get('MultiAZ', False)
|
|
261
|
+
availability_zones = resource.get('AvailabilityZones', [])
|
|
262
|
+
|
|
263
|
+
if multi_az or len(availability_zones) > 1:
|
|
264
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
265
|
+
evaluation_reason = f"RDS cluster {cluster_id} has Multi-AZ enabled or spans multiple AZs"
|
|
266
|
+
else:
|
|
267
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
268
|
+
evaluation_reason = f"RDS cluster {cluster_id} does not have Multi-AZ enabled"
|
|
269
|
+
|
|
270
|
+
return ComplianceResult(
|
|
271
|
+
resource_id=cluster_id,
|
|
272
|
+
resource_type="AWS::RDS::DBCluster",
|
|
273
|
+
compliance_status=compliance_status,
|
|
274
|
+
evaluation_reason=evaluation_reason,
|
|
275
|
+
config_rule_name=self.rule_name,
|
|
276
|
+
region=region
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class RDSInstanceDeletionProtectionEnabledAssessment(BaseConfigRuleAssessment):
|
|
281
|
+
"""Assessment for rds-instance-deletion-protection-enabled AWS Config rule."""
|
|
282
|
+
|
|
283
|
+
def __init__(self):
|
|
284
|
+
super().__init__(
|
|
285
|
+
rule_name="rds-instance-deletion-protection-enabled",
|
|
286
|
+
control_id="11.4",
|
|
287
|
+
resource_types=["AWS::RDS::DBInstance"]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
291
|
+
"""Get RDS instances."""
|
|
292
|
+
if resource_type != "AWS::RDS::DBInstance":
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
297
|
+
|
|
298
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
299
|
+
lambda: rds_client.describe_db_instances()
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
instances = []
|
|
303
|
+
for instance in response.get('DBInstances', []):
|
|
304
|
+
instances.append({
|
|
305
|
+
'DBInstanceIdentifier': instance.get('DBInstanceIdentifier'),
|
|
306
|
+
'DeletionProtection': instance.get('DeletionProtection', False)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
return instances
|
|
310
|
+
|
|
311
|
+
except ClientError as e:
|
|
312
|
+
logger.error(f"Error retrieving RDS instances in region {region}: {e}")
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
316
|
+
"""Evaluate if RDS instance has deletion protection enabled."""
|
|
317
|
+
instance_id = resource.get('DBInstanceIdentifier', 'unknown')
|
|
318
|
+
deletion_protection = resource.get('DeletionProtection', False)
|
|
319
|
+
|
|
320
|
+
if deletion_protection:
|
|
321
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
322
|
+
evaluation_reason = f"RDS instance {instance_id} has deletion protection enabled"
|
|
323
|
+
else:
|
|
324
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
325
|
+
evaluation_reason = f"RDS instance {instance_id} does not have deletion protection enabled"
|
|
326
|
+
|
|
327
|
+
return ComplianceResult(
|
|
328
|
+
resource_id=instance_id,
|
|
329
|
+
resource_type="AWS::RDS::DBInstance",
|
|
330
|
+
compliance_status=compliance_status,
|
|
331
|
+
evaluation_reason=evaluation_reason,
|
|
332
|
+
config_rule_name=self.rule_name,
|
|
333
|
+
region=region
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class RDSMultiAZSupportAssessment(BaseConfigRuleAssessment):
|
|
338
|
+
"""Assessment for rds-multi-az-support AWS Config rule."""
|
|
339
|
+
|
|
340
|
+
def __init__(self):
|
|
341
|
+
super().__init__(
|
|
342
|
+
rule_name="rds-multi-az-support",
|
|
343
|
+
control_id="12.2",
|
|
344
|
+
resource_types=["AWS::RDS::DBInstance"]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
348
|
+
"""Get RDS instances."""
|
|
349
|
+
if resource_type != "AWS::RDS::DBInstance":
|
|
350
|
+
return []
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
354
|
+
|
|
355
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
356
|
+
lambda: rds_client.describe_db_instances()
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
instances = []
|
|
360
|
+
for instance in response.get('DBInstances', []):
|
|
361
|
+
instances.append({
|
|
362
|
+
'DBInstanceIdentifier': instance.get('DBInstanceIdentifier'),
|
|
363
|
+
'MultiAZ': instance.get('MultiAZ', False),
|
|
364
|
+
'Engine': instance.get('Engine')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return instances
|
|
368
|
+
|
|
369
|
+
except ClientError as e:
|
|
370
|
+
logger.error(f"Error retrieving RDS instances in region {region}: {e}")
|
|
371
|
+
raise
|
|
372
|
+
|
|
373
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
374
|
+
"""Evaluate if RDS instance has Multi-AZ support enabled."""
|
|
375
|
+
instance_id = resource.get('DBInstanceIdentifier', 'unknown')
|
|
376
|
+
multi_az = resource.get('MultiAZ', False)
|
|
377
|
+
engine = resource.get('Engine', 'unknown')
|
|
378
|
+
|
|
379
|
+
if multi_az:
|
|
380
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
381
|
+
evaluation_reason = f"RDS instance {instance_id} ({engine}) has Multi-AZ enabled"
|
|
382
|
+
else:
|
|
383
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
384
|
+
evaluation_reason = f"RDS instance {instance_id} ({engine}) does not have Multi-AZ enabled"
|
|
385
|
+
|
|
386
|
+
return ComplianceResult(
|
|
387
|
+
resource_id=instance_id,
|
|
388
|
+
resource_type="AWS::RDS::DBInstance",
|
|
389
|
+
compliance_status=compliance_status,
|
|
390
|
+
evaluation_reason=evaluation_reason,
|
|
391
|
+
config_rule_name=self.rule_name,
|
|
392
|
+
region=region
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class VPCVPNTwoTunnelsUpAssessment(BaseConfigRuleAssessment):
|
|
397
|
+
"""Assessment for vpc-vpn-2-tunnels-up AWS Config rule."""
|
|
398
|
+
|
|
399
|
+
def __init__(self):
|
|
400
|
+
super().__init__(
|
|
401
|
+
rule_name="vpc-vpn-2-tunnels-up",
|
|
402
|
+
control_id="12.2",
|
|
403
|
+
resource_types=["AWS::EC2::VPNConnection"]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
407
|
+
"""Get VPN connections."""
|
|
408
|
+
if resource_type != "AWS::EC2::VPNConnection":
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
413
|
+
|
|
414
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
415
|
+
lambda: ec2_client.describe_vpn_connections()
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
vpn_connections = []
|
|
419
|
+
for vpn in response.get('VpnConnections', []):
|
|
420
|
+
vpn_connections.append({
|
|
421
|
+
'VpnConnectionId': vpn.get('VpnConnectionId'),
|
|
422
|
+
'State': vpn.get('State'),
|
|
423
|
+
'VgwTelemetry': vpn.get('VgwTelemetry', [])
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
return vpn_connections
|
|
427
|
+
|
|
428
|
+
except ClientError as e:
|
|
429
|
+
logger.error(f"Error retrieving VPN connections in region {region}: {e}")
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
433
|
+
"""Evaluate if VPN connection has both tunnels up."""
|
|
434
|
+
vpn_id = resource.get('VpnConnectionId', 'unknown')
|
|
435
|
+
state = resource.get('State', 'unknown')
|
|
436
|
+
telemetry = resource.get('VgwTelemetry', [])
|
|
437
|
+
|
|
438
|
+
if state != 'available':
|
|
439
|
+
return ComplianceResult(
|
|
440
|
+
resource_id=vpn_id,
|
|
441
|
+
resource_type="AWS::EC2::VPNConnection",
|
|
442
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
443
|
+
evaluation_reason=f"VPN connection {vpn_id} is in state '{state}', not available",
|
|
444
|
+
config_rule_name=self.rule_name,
|
|
445
|
+
region=region
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
up_tunnels = 0
|
|
449
|
+
for tunnel in telemetry:
|
|
450
|
+
if tunnel.get('Status') == 'UP':
|
|
451
|
+
up_tunnels += 1
|
|
452
|
+
|
|
453
|
+
if up_tunnels >= 2:
|
|
454
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
455
|
+
evaluation_reason = f"VPN connection {vpn_id} has {up_tunnels} tunnels up"
|
|
456
|
+
else:
|
|
457
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
458
|
+
evaluation_reason = f"VPN connection {vpn_id} has only {up_tunnels} tunnel(s) up, 2 required"
|
|
459
|
+
|
|
460
|
+
return ComplianceResult(
|
|
461
|
+
resource_id=vpn_id,
|
|
462
|
+
resource_type="AWS::EC2::VPNConnection",
|
|
463
|
+
compliance_status=compliance_status,
|
|
464
|
+
evaluation_reason=evaluation_reason,
|
|
465
|
+
config_rule_name=self.rule_name,
|
|
466
|
+
region=region
|
|
467
|
+
)
|