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,395 @@
|
|
|
1
|
+
"""Control 12.8: Establish and Maintain Dedicated Computing Resources for All Administrative Work 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 APIGatewayAssociatedWithWAFAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for api-gw-associated-with-waf Config rule."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize API Gateway associated with WAF assessment."""
|
|
20
|
+
super().__init__(
|
|
21
|
+
rule_name="api-gw-associated-with-waf",
|
|
22
|
+
control_id="12.8",
|
|
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 this 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
|
+
'deploymentId': stage.get('deploymentId'),
|
|
56
|
+
'webAclArn': stage.get('webAclArn'),
|
|
57
|
+
'createdDate': stage.get('createdDate'),
|
|
58
|
+
'lastUpdatedDate': stage.get('lastUpdatedDate'),
|
|
59
|
+
'tags': stage.get('tags', {})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
except ClientError as e:
|
|
63
|
+
logger.warning(f"Could not get stages for API {api_id}: {e}")
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
logger.debug(f"Found {len(stages)} API Gateway stages in region {region}")
|
|
67
|
+
return stages
|
|
68
|
+
|
|
69
|
+
except ClientError as e:
|
|
70
|
+
logger.error(f"Error retrieving API Gateway stages in region {region}: {e}")
|
|
71
|
+
raise
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Unexpected error retrieving API Gateway stages in region {region}: {e}")
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
77
|
+
"""Evaluate if API Gateway stage is associated with WAF."""
|
|
78
|
+
api_id = resource.get('restApiId', 'unknown')
|
|
79
|
+
stage_name = resource.get('stageName', 'unknown')
|
|
80
|
+
api_name = resource.get('apiName', 'unknown')
|
|
81
|
+
resource_id = f"{api_id}/{stage_name}"
|
|
82
|
+
web_acl_arn = resource.get('webAclArn')
|
|
83
|
+
|
|
84
|
+
# Check if stage is associated with a WAF Web ACL
|
|
85
|
+
if web_acl_arn:
|
|
86
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
87
|
+
evaluation_reason = f"API Gateway stage {stage_name} in API {api_name} is associated with WAF Web ACL: {web_acl_arn}"
|
|
88
|
+
else:
|
|
89
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
90
|
+
evaluation_reason = f"API Gateway stage {stage_name} in API {api_name} is not associated with any WAF Web ACL"
|
|
91
|
+
|
|
92
|
+
return ComplianceResult(
|
|
93
|
+
resource_id=resource_id,
|
|
94
|
+
resource_type="AWS::ApiGateway::Stage",
|
|
95
|
+
compliance_status=compliance_status,
|
|
96
|
+
evaluation_reason=evaluation_reason,
|
|
97
|
+
config_rule_name=self.rule_name,
|
|
98
|
+
region=region
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
102
|
+
"""Get specific remediation steps for API Gateway WAF association."""
|
|
103
|
+
return [
|
|
104
|
+
"Associate API Gateway stages with AWS WAF Web ACLs for protection",
|
|
105
|
+
"For each non-compliant stage:",
|
|
106
|
+
" 1. Create or identify an appropriate WAF Web ACL",
|
|
107
|
+
" 2. Associate the Web ACL with the API Gateway stage",
|
|
108
|
+
" 3. Configure WAF rules for protection against common attacks",
|
|
109
|
+
" 4. Test the WAF configuration to ensure it doesn't block legitimate traffic",
|
|
110
|
+
"Create a WAF Web ACL:",
|
|
111
|
+
"aws wafv2 create-web-acl --name <web-acl-name> --scope REGIONAL --default-action Allow={} --rules file://waf-rules.json",
|
|
112
|
+
"Associate Web ACL with API Gateway stage:",
|
|
113
|
+
"aws apigateway update-stage --rest-api-id <api-id> --stage-name <stage-name> --patch-ops op=replace,path=/webAclArn,value=<web-acl-arn>",
|
|
114
|
+
"Configure common WAF rules:",
|
|
115
|
+
" - AWS Managed Rules for Core Rule Set",
|
|
116
|
+
" - AWS Managed Rules for Known Bad Inputs",
|
|
117
|
+
" - Rate limiting rules to prevent abuse",
|
|
118
|
+
" - IP reputation rules to block known malicious IPs",
|
|
119
|
+
"Monitor WAF metrics and blocked requests",
|
|
120
|
+
"Regularly review and update WAF rules based on threat intelligence",
|
|
121
|
+
"Set up CloudWatch alarms for WAF blocked requests"
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class VPCSecurityGroupOpenOnlyToAuthorizedPortsAssessment(BaseConfigRuleAssessment):
|
|
126
|
+
"""Assessment for vpc-sg-open-only-to-authorized-ports Config rule."""
|
|
127
|
+
|
|
128
|
+
def __init__(self):
|
|
129
|
+
"""Initialize VPC security group authorized ports assessment."""
|
|
130
|
+
super().__init__(
|
|
131
|
+
rule_name="vpc-sg-open-only-to-authorized-ports",
|
|
132
|
+
control_id="12.8",
|
|
133
|
+
resource_types=["AWS::EC2::SecurityGroup"],
|
|
134
|
+
parameters={"authorizedTcpPorts": "443,22"}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
138
|
+
"""Get all security groups in the region."""
|
|
139
|
+
if resource_type != "AWS::EC2::SecurityGroup":
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
144
|
+
|
|
145
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
146
|
+
lambda: ec2_client.describe_security_groups()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
security_groups = []
|
|
150
|
+
for sg in response.get('SecurityGroups', []):
|
|
151
|
+
security_groups.append({
|
|
152
|
+
'GroupId': sg.get('GroupId'),
|
|
153
|
+
'GroupName': sg.get('GroupName'),
|
|
154
|
+
'Description': sg.get('Description'),
|
|
155
|
+
'VpcId': sg.get('VpcId'),
|
|
156
|
+
'IpPermissions': sg.get('IpPermissions', []),
|
|
157
|
+
'IpPermissionsEgress': sg.get('IpPermissionsEgress', []),
|
|
158
|
+
'Tags': sg.get('Tags', [])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
logger.debug(f"Found {len(security_groups)} security groups in region {region}")
|
|
162
|
+
return security_groups
|
|
163
|
+
|
|
164
|
+
except ClientError as e:
|
|
165
|
+
logger.error(f"Error retrieving security groups in region {region}: {e}")
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Unexpected error retrieving security groups in region {region}: {e}")
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
172
|
+
"""Evaluate if security group is open only to authorized ports."""
|
|
173
|
+
group_id = resource.get('GroupId', 'unknown')
|
|
174
|
+
group_name = resource.get('GroupName', 'unknown')
|
|
175
|
+
ip_permissions = resource.get('IpPermissions', [])
|
|
176
|
+
|
|
177
|
+
# Get authorized ports from parameters
|
|
178
|
+
authorized_ports_param = self.parameters.get('authorizedTcpPorts', '443,22')
|
|
179
|
+
authorized_ports = set()
|
|
180
|
+
if authorized_ports_param:
|
|
181
|
+
try:
|
|
182
|
+
authorized_ports = set(int(port.strip()) for port in authorized_ports_param.split(',') if port.strip())
|
|
183
|
+
except ValueError:
|
|
184
|
+
logger.warning(f"Invalid authorized ports parameter: {authorized_ports_param}")
|
|
185
|
+
authorized_ports = {443, 22} # Default to HTTPS and SSH
|
|
186
|
+
|
|
187
|
+
# Check for unauthorized open ports
|
|
188
|
+
unauthorized_rules = []
|
|
189
|
+
|
|
190
|
+
for rule in ip_permissions:
|
|
191
|
+
ip_protocol = rule.get('IpProtocol', '')
|
|
192
|
+
from_port = rule.get('FromPort')
|
|
193
|
+
to_port = rule.get('ToPort')
|
|
194
|
+
ip_ranges = rule.get('IpRanges', [])
|
|
195
|
+
ipv6_ranges = rule.get('Ipv6Ranges', [])
|
|
196
|
+
|
|
197
|
+
# Check if rule allows access from anywhere (0.0.0.0/0 or ::/0)
|
|
198
|
+
allows_public_access = False
|
|
199
|
+
for ip_range in ip_ranges:
|
|
200
|
+
if ip_range.get('CidrIp') == '0.0.0.0/0':
|
|
201
|
+
allows_public_access = True
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
if not allows_public_access:
|
|
205
|
+
for ipv6_range in ipv6_ranges:
|
|
206
|
+
if ipv6_range.get('CidrIpv6') == '::/0':
|
|
207
|
+
allows_public_access = True
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# If rule allows public access, check if ports are authorized
|
|
211
|
+
if allows_public_access:
|
|
212
|
+
if ip_protocol == '-1': # All protocols
|
|
213
|
+
unauthorized_rules.append({
|
|
214
|
+
'protocol': 'All',
|
|
215
|
+
'port': 'All',
|
|
216
|
+
'from_port': 'All',
|
|
217
|
+
'to_port': 'All'
|
|
218
|
+
})
|
|
219
|
+
elif ip_protocol.lower() == 'tcp' and from_port is not None and to_port is not None:
|
|
220
|
+
# Check each port in the range
|
|
221
|
+
for port in range(from_port, to_port + 1):
|
|
222
|
+
if port not in authorized_ports:
|
|
223
|
+
unauthorized_rules.append({
|
|
224
|
+
'protocol': ip_protocol,
|
|
225
|
+
'port': port,
|
|
226
|
+
'from_port': from_port,
|
|
227
|
+
'to_port': to_port
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
if unauthorized_rules:
|
|
231
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
232
|
+
unauthorized_ports = set(rule['port'] for rule in unauthorized_rules if rule['port'] != 'All')
|
|
233
|
+
if unauthorized_ports:
|
|
234
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) allows public access to unauthorized TCP ports: {sorted(unauthorized_ports)}"
|
|
235
|
+
else:
|
|
236
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) allows unrestricted public access to all ports"
|
|
237
|
+
else:
|
|
238
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
239
|
+
evaluation_reason = f"Security group {group_name} ({group_id}) only allows public access to authorized ports: {sorted(authorized_ports)}"
|
|
240
|
+
|
|
241
|
+
return ComplianceResult(
|
|
242
|
+
resource_id=group_id,
|
|
243
|
+
resource_type="AWS::EC2::SecurityGroup",
|
|
244
|
+
compliance_status=compliance_status,
|
|
245
|
+
evaluation_reason=evaluation_reason,
|
|
246
|
+
config_rule_name=self.rule_name,
|
|
247
|
+
region=region
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
251
|
+
"""Get specific remediation steps for security group port restrictions."""
|
|
252
|
+
return [
|
|
253
|
+
"Restrict security group rules to authorized ports only",
|
|
254
|
+
"For each non-compliant security group:",
|
|
255
|
+
" 1. Review all inbound rules allowing public access (0.0.0.0/0)",
|
|
256
|
+
" 2. Remove or modify rules that allow access to unauthorized ports",
|
|
257
|
+
" 3. Ensure only necessary ports (like 443 for HTTPS, 22 for SSH) are open",
|
|
258
|
+
" 4. Consider using more restrictive CIDR blocks instead of 0.0.0.0/0",
|
|
259
|
+
"Remove unauthorized inbound rules:",
|
|
260
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port <port> --cidr 0.0.0.0/0",
|
|
261
|
+
"Add authorized rules with specific CIDR blocks:",
|
|
262
|
+
"aws ec2 authorize-security-group-ingress --group-id <sg-id> --protocol tcp --port 443 --cidr <specific-cidr>",
|
|
263
|
+
"For SSH access, use bastion hosts or VPN instead of public access:",
|
|
264
|
+
"aws ec2 revoke-security-group-ingress --group-id <sg-id> --protocol tcp --port 22 --cidr 0.0.0.0/0",
|
|
265
|
+
"Consider using AWS Systems Manager Session Manager for secure access",
|
|
266
|
+
"Implement security group rules with least privilege principle",
|
|
267
|
+
"Use security group references instead of CIDR blocks where possible",
|
|
268
|
+
"Regularly audit security group rules for compliance",
|
|
269
|
+
"Set up CloudWatch alarms for security group changes",
|
|
270
|
+
"Use AWS Config rules to monitor security group compliance"
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class NoUnrestrictedRouteToIGWAssessment(BaseConfigRuleAssessment):
|
|
275
|
+
"""Assessment for no-unrestricted-route-to-igw Config rule."""
|
|
276
|
+
|
|
277
|
+
def __init__(self):
|
|
278
|
+
"""Initialize no unrestricted route to IGW assessment."""
|
|
279
|
+
super().__init__(
|
|
280
|
+
rule_name="no-unrestricted-route-to-igw",
|
|
281
|
+
control_id="12.8",
|
|
282
|
+
resource_types=["AWS::EC2::RouteTable"]
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
286
|
+
"""Get all route tables in the region."""
|
|
287
|
+
if resource_type != "AWS::EC2::RouteTable":
|
|
288
|
+
return []
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
292
|
+
|
|
293
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
294
|
+
lambda: ec2_client.describe_route_tables()
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
route_tables = []
|
|
298
|
+
for rt in response.get('RouteTables', []):
|
|
299
|
+
route_tables.append({
|
|
300
|
+
'RouteTableId': rt.get('RouteTableId'),
|
|
301
|
+
'VpcId': rt.get('VpcId'),
|
|
302
|
+
'Routes': rt.get('Routes', []),
|
|
303
|
+
'Associations': rt.get('Associations', []),
|
|
304
|
+
'Tags': rt.get('Tags', [])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
logger.debug(f"Found {len(route_tables)} route tables in region {region}")
|
|
308
|
+
return route_tables
|
|
309
|
+
|
|
310
|
+
except ClientError as e:
|
|
311
|
+
logger.error(f"Error retrieving route tables in region {region}: {e}")
|
|
312
|
+
raise
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Unexpected error retrieving route tables 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 route table has unrestricted routes to internet gateway."""
|
|
319
|
+
route_table_id = resource.get('RouteTableId', 'unknown')
|
|
320
|
+
vpc_id = resource.get('VpcId', 'unknown')
|
|
321
|
+
routes = resource.get('Routes', [])
|
|
322
|
+
associations = resource.get('Associations', [])
|
|
323
|
+
|
|
324
|
+
# Check if this route table is associated with subnets (not just main route table)
|
|
325
|
+
has_subnet_associations = any(assoc.get('SubnetId') for assoc in associations)
|
|
326
|
+
|
|
327
|
+
# Look for routes to internet gateway with unrestricted destination
|
|
328
|
+
unrestricted_igw_routes = []
|
|
329
|
+
|
|
330
|
+
for route in routes:
|
|
331
|
+
destination_cidr = route.get('DestinationCidrBlock', '')
|
|
332
|
+
destination_ipv6_cidr = route.get('DestinationIpv6CidrBlock', '')
|
|
333
|
+
gateway_id = route.get('GatewayId', '')
|
|
334
|
+
|
|
335
|
+
# Check if route goes to an internet gateway
|
|
336
|
+
is_igw_route = gateway_id.startswith('igw-')
|
|
337
|
+
|
|
338
|
+
# Check if destination is unrestricted (0.0.0.0/0 or ::/0)
|
|
339
|
+
is_unrestricted = (destination_cidr == '0.0.0.0/0' or destination_ipv6_cidr == '::/0')
|
|
340
|
+
|
|
341
|
+
if is_igw_route and is_unrestricted:
|
|
342
|
+
unrestricted_igw_routes.append({
|
|
343
|
+
'destination': destination_cidr or destination_ipv6_cidr,
|
|
344
|
+
'gateway_id': gateway_id
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
if unrestricted_igw_routes:
|
|
348
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
349
|
+
destinations = [route['destination'] for route in unrestricted_igw_routes]
|
|
350
|
+
gateways = [route['gateway_id'] for route in unrestricted_igw_routes]
|
|
351
|
+
|
|
352
|
+
if has_subnet_associations:
|
|
353
|
+
evaluation_reason = f"Route table {route_table_id} in VPC {vpc_id} has unrestricted routes to internet gateway(s) {', '.join(set(gateways))} for destinations {', '.join(destinations)} and is associated with subnets"
|
|
354
|
+
else:
|
|
355
|
+
evaluation_reason = f"Route table {route_table_id} in VPC {vpc_id} has unrestricted routes to internet gateway(s) {', '.join(set(gateways))} for destinations {', '.join(destinations)}"
|
|
356
|
+
else:
|
|
357
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
358
|
+
evaluation_reason = f"Route table {route_table_id} in VPC {vpc_id} does not have unrestricted routes to internet gateway"
|
|
359
|
+
|
|
360
|
+
return ComplianceResult(
|
|
361
|
+
resource_id=route_table_id,
|
|
362
|
+
resource_type="AWS::EC2::RouteTable",
|
|
363
|
+
compliance_status=compliance_status,
|
|
364
|
+
evaluation_reason=evaluation_reason,
|
|
365
|
+
config_rule_name=self.rule_name,
|
|
366
|
+
region=region
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
370
|
+
"""Get specific remediation steps for removing unrestricted IGW routes."""
|
|
371
|
+
return [
|
|
372
|
+
"Remove unrestricted routes to internet gateway from route tables",
|
|
373
|
+
"For each non-compliant route table:",
|
|
374
|
+
" 1. Review routes with destination 0.0.0.0/0 or ::/0 pointing to IGW",
|
|
375
|
+
" 2. Determine if unrestricted internet access is necessary",
|
|
376
|
+
" 3. Replace with more specific routes if possible",
|
|
377
|
+
" 4. Consider using NAT Gateway for outbound-only internet access",
|
|
378
|
+
"Remove unrestricted route to IGW:",
|
|
379
|
+
"aws ec2 delete-route --route-table-id <route-table-id> --destination-cidr-block 0.0.0.0/0",
|
|
380
|
+
"Create NAT Gateway for outbound internet access:",
|
|
381
|
+
"aws ec2 create-nat-gateway --subnet-id <public-subnet-id> --allocation-id <eip-allocation-id>",
|
|
382
|
+
"Add route to NAT Gateway in private subnet route table:",
|
|
383
|
+
"aws ec2 create-route --route-table-id <private-route-table-id> --destination-cidr-block 0.0.0.0/0 --nat-gateway-id <nat-gateway-id>",
|
|
384
|
+
"For public subnets that need internet access:",
|
|
385
|
+
" 1. Ensure they are in a separate route table from private subnets",
|
|
386
|
+
" 2. Use security groups and NACLs to control access",
|
|
387
|
+
" 3. Consider using Application Load Balancer for web traffic",
|
|
388
|
+
"Implement network segmentation best practices:",
|
|
389
|
+
" - Separate public and private subnets",
|
|
390
|
+
" - Use different route tables for different subnet types",
|
|
391
|
+
" - Implement least privilege routing",
|
|
392
|
+
"Monitor route table changes with CloudTrail",
|
|
393
|
+
"Set up Config rules to detect unauthorized route changes",
|
|
394
|
+
"Regularly audit network architecture for security compliance"
|
|
395
|
+
]
|