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,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
+ )