aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.0__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 +4 -4
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
- aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
- aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
- aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
- aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
- aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
- aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
- aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
- aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
- aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
- aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
- aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
- aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
- aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
- aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
- aws_cis_assessment/controls/ig1/control_macie.py +165 -0
- aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
- aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
- aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
- aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
- aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
- aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
- aws_cis_assessment/controls/ig1/control_version_mgmt.py +329 -0
- aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
- aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
- aws_cis_assessment/core/models.py +20 -1
- aws_cis_assessment/core/scoring_engine.py +98 -1
- aws_cis_assessment/reporters/base_reporter.py +31 -1
- aws_cis_assessment/reporters/html_reporter.py +163 -0
- aws_cis_controls_assessment-1.2.0.dist-info/METADATA +320 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/RECORD +39 -15
- docs/developer-guide.md +204 -5
- docs/user-guide.md +137 -4
- aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
|
-
CIS Control
|
|
3
|
-
|
|
2
|
+
CIS Control 13.6 - Network Security
|
|
3
|
+
Ensures advanced network security controls are deployed.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from typing import List, Dict, Any
|
|
8
|
-
import
|
|
9
|
-
import json
|
|
10
|
-
from botocore.exceptions import ClientError, NoCredentialsError
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
11
9
|
|
|
12
10
|
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
13
11
|
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
@@ -16,657 +14,234 @@ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
|
16
14
|
logger = logging.getLogger(__name__)
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
class
|
|
17
|
+
class NetworkFirewallDeployedAssessment(BaseConfigRuleAssessment):
|
|
20
18
|
"""
|
|
21
|
-
CIS Control
|
|
22
|
-
AWS Config Rule:
|
|
19
|
+
CIS Control 13.6 - Deny Communications with Known Malicious IP Addresses
|
|
20
|
+
AWS Config Rule: network-firewall-deployed
|
|
23
21
|
|
|
24
|
-
Ensures
|
|
22
|
+
Ensures AWS Network Firewall is deployed for advanced network protection.
|
|
25
23
|
"""
|
|
26
24
|
|
|
27
25
|
def __init__(self):
|
|
28
26
|
super().__init__(
|
|
29
|
-
rule_name="
|
|
30
|
-
control_id="
|
|
31
|
-
resource_types=["AWS::
|
|
27
|
+
rule_name="network-firewall-deployed",
|
|
28
|
+
control_id="13.6",
|
|
29
|
+
resource_types=["AWS::EC2::VPC"]
|
|
32
30
|
)
|
|
33
31
|
|
|
34
32
|
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
35
|
-
"""Get
|
|
36
|
-
if resource_type != "AWS::
|
|
33
|
+
"""Get VPCs and check for Network Firewall deployment."""
|
|
34
|
+
if resource_type != "AWS::EC2::VPC":
|
|
37
35
|
return []
|
|
38
36
|
|
|
39
37
|
try:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
logger.debug(f"Found {len(instances)} DMS replication instances in {region}")
|
|
60
|
-
return instances
|
|
61
|
-
|
|
62
|
-
except ClientError as e:
|
|
63
|
-
logger.error(f"Error retrieving DMS replication instances in {region}: {e}")
|
|
64
|
-
raise
|
|
65
|
-
except Exception as e:
|
|
66
|
-
logger.error(f"Unexpected error retrieving DMS replication instances in {region}: {e}")
|
|
67
|
-
raise
|
|
68
|
-
|
|
69
|
-
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
70
|
-
"""Evaluate if DMS replication instance is publicly accessible."""
|
|
71
|
-
instance_id = resource.get('ReplicationInstanceIdentifier', 'unknown')
|
|
72
|
-
is_public = resource.get('PubliclyAccessible', False)
|
|
73
|
-
|
|
74
|
-
if is_public:
|
|
75
|
-
return ComplianceResult(
|
|
76
|
-
resource_id=instance_id,
|
|
77
|
-
resource_type="AWS::DMS::ReplicationInstance",
|
|
78
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
79
|
-
evaluation_reason="DMS replication instance is publicly accessible",
|
|
80
|
-
config_rule_name=self.rule_name,
|
|
81
|
-
region=region
|
|
82
|
-
)
|
|
83
|
-
else:
|
|
84
|
-
return ComplianceResult(
|
|
85
|
-
resource_id=instance_id,
|
|
86
|
-
resource_type="AWS::DMS::ReplicationInstance",
|
|
87
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
88
|
-
evaluation_reason="DMS replication instance is not publicly accessible",
|
|
89
|
-
config_rule_name=self.rule_name,
|
|
90
|
-
region=region
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
class ElasticsearchInVPCOnlyAssessment(BaseConfigRuleAssessment):
|
|
95
|
-
"""
|
|
96
|
-
CIS Control 3.3 - Configure Data Access Control Lists
|
|
97
|
-
AWS Config Rule: elasticsearch-in-vpc-only
|
|
98
|
-
|
|
99
|
-
Ensures Elasticsearch domains are deployed within VPC to prevent public access.
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
def __init__(self):
|
|
103
|
-
super().__init__(
|
|
104
|
-
rule_name="elasticsearch-in-vpc-only",
|
|
105
|
-
control_id="3.3",
|
|
106
|
-
resource_types=["AWS::Elasticsearch::Domain"]
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
110
|
-
"""Get all Elasticsearch domains in the region."""
|
|
111
|
-
if resource_type != "AWS::Elasticsearch::Domain":
|
|
112
|
-
return []
|
|
113
|
-
|
|
114
|
-
try:
|
|
115
|
-
es_client = aws_factory.get_client('es', region)
|
|
116
|
-
|
|
117
|
-
# Get all Elasticsearch domains
|
|
118
|
-
response = es_client.list_domain_names()
|
|
119
|
-
domains = []
|
|
120
|
-
|
|
121
|
-
for domain_info in response.get('DomainNames', []):
|
|
122
|
-
domain_name = domain_info['DomainName']
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
# Get detailed domain configuration
|
|
126
|
-
domain_response = es_client.describe_elasticsearch_domain(DomainName=domain_name)
|
|
127
|
-
domain = domain_response['DomainStatus']
|
|
128
|
-
|
|
129
|
-
vpc_options = domain.get('VPCOptions', {})
|
|
130
|
-
|
|
131
|
-
domains.append({
|
|
132
|
-
'DomainName': domain_name,
|
|
133
|
-
'DomainArn': domain.get('ARN', ''),
|
|
134
|
-
'ElasticsearchVersion': domain.get('ElasticsearchVersion', ''),
|
|
135
|
-
'VPCOptions': vpc_options,
|
|
136
|
-
'VPCId': vpc_options.get('VPCId', ''),
|
|
137
|
-
'SubnetIds': vpc_options.get('SubnetIds', []),
|
|
138
|
-
'SecurityGroupIds': vpc_options.get('SecurityGroupIds', []),
|
|
139
|
-
'Endpoint': domain.get('Endpoint', ''),
|
|
140
|
-
'Endpoints': domain.get('Endpoints', {})
|
|
141
|
-
})
|
|
38
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
39
|
+
network_firewall_client = aws_factory.get_client('network-firewall', region)
|
|
40
|
+
|
|
41
|
+
# Get all VPCs
|
|
42
|
+
vpcs_response = ec2_client.describe_vpcs()
|
|
43
|
+
vpcs = []
|
|
44
|
+
|
|
45
|
+
# Get all firewalls
|
|
46
|
+
try:
|
|
47
|
+
firewalls_response = network_firewall_client.list_firewalls()
|
|
48
|
+
firewalls = firewalls_response.get('Firewalls', [])
|
|
49
|
+
firewall_vpcs = {fw.get('VpcId') for fw in firewalls}
|
|
50
|
+
except ClientError:
|
|
51
|
+
firewall_vpcs = set()
|
|
52
|
+
|
|
53
|
+
for vpc in vpcs_response.get('Vpcs', []):
|
|
54
|
+
vpc_id = vpc.get('VpcId')
|
|
55
|
+
has_firewall = vpc_id in firewall_vpcs
|
|
142
56
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
57
|
+
vpcs.append({
|
|
58
|
+
'VpcId': vpc_id,
|
|
59
|
+
'HasFirewall': has_firewall,
|
|
60
|
+
'IsDefault': vpc.get('IsDefault', False)
|
|
61
|
+
})
|
|
146
62
|
|
|
147
|
-
|
|
148
|
-
return domains
|
|
63
|
+
return vpcs
|
|
149
64
|
|
|
150
65
|
except ClientError as e:
|
|
151
|
-
logger.error(f"Error retrieving
|
|
152
|
-
raise
|
|
153
|
-
except Exception as e:
|
|
154
|
-
logger.error(f"Unexpected error retrieving Elasticsearch domains in {region}: {e}")
|
|
155
|
-
raise
|
|
156
|
-
|
|
157
|
-
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
158
|
-
"""Evaluate if Elasticsearch domain is deployed within VPC."""
|
|
159
|
-
domain_name = resource.get('DomainName', 'unknown')
|
|
160
|
-
vpc_id = resource.get('VPCId', '')
|
|
161
|
-
|
|
162
|
-
if vpc_id:
|
|
163
|
-
return ComplianceResult(
|
|
164
|
-
resource_id=domain_name,
|
|
165
|
-
resource_type="AWS::Elasticsearch::Domain",
|
|
166
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
167
|
-
evaluation_reason=f"Elasticsearch domain is deployed within VPC {vpc_id}",
|
|
168
|
-
config_rule_name=self.rule_name,
|
|
169
|
-
region=region
|
|
170
|
-
)
|
|
171
|
-
else:
|
|
172
|
-
return ComplianceResult(
|
|
173
|
-
resource_id=domain_name,
|
|
174
|
-
resource_type="AWS::Elasticsearch::Domain",
|
|
175
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
176
|
-
evaluation_reason="Elasticsearch domain is not deployed within VPC",
|
|
177
|
-
config_rule_name=self.rule_name,
|
|
178
|
-
region=region
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class EC2InstancesInVPCAssessment(BaseConfigRuleAssessment):
|
|
183
|
-
"""
|
|
184
|
-
CIS Control 3.3 - Configure Data Access Control Lists
|
|
185
|
-
AWS Config Rule: ec2-instances-in-vpc
|
|
186
|
-
|
|
187
|
-
Ensures EC2 instances are deployed within VPC for network security.
|
|
188
|
-
"""
|
|
189
|
-
|
|
190
|
-
def __init__(self):
|
|
191
|
-
super().__init__(
|
|
192
|
-
rule_name="ec2-instances-in-vpc",
|
|
193
|
-
control_id="3.3",
|
|
194
|
-
resource_types=["AWS::EC2::Instance"]
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
198
|
-
"""Get all EC2 instances in the region."""
|
|
199
|
-
if resource_type != "AWS::EC2::Instance":
|
|
66
|
+
logger.error(f"Error retrieving VPCs in {region}: {e}")
|
|
200
67
|
return []
|
|
201
|
-
|
|
202
|
-
try:
|
|
203
|
-
ec2_client = aws_factory.get_client('ec2', region)
|
|
204
|
-
|
|
205
|
-
# Get all EC2 instances
|
|
206
|
-
paginator = ec2_client.get_paginator('describe_instances')
|
|
207
|
-
instances = []
|
|
208
|
-
|
|
209
|
-
for page in paginator.paginate():
|
|
210
|
-
for reservation in page['Reservations']:
|
|
211
|
-
for instance in reservation['Instances']:
|
|
212
|
-
instances.append({
|
|
213
|
-
'InstanceId': instance['InstanceId'],
|
|
214
|
-
'VpcId': instance.get('VpcId', ''),
|
|
215
|
-
'SubnetId': instance.get('SubnetId', ''),
|
|
216
|
-
'State': instance.get('State', {}).get('Name', ''),
|
|
217
|
-
'InstanceType': instance.get('InstanceType', ''),
|
|
218
|
-
'PublicIpAddress': instance.get('PublicIpAddress', ''),
|
|
219
|
-
'PrivateIpAddress': instance.get('PrivateIpAddress', '')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
logger.debug(f"Found {len(instances)} EC2 instances in {region}")
|
|
223
|
-
return instances
|
|
224
|
-
|
|
225
|
-
except ClientError as e:
|
|
226
|
-
logger.error(f"Error retrieving EC2 instances in {region}: {e}")
|
|
227
|
-
raise
|
|
228
|
-
except Exception as e:
|
|
229
|
-
logger.error(f"Unexpected error retrieving EC2 instances in {region}: {e}")
|
|
230
|
-
raise
|
|
231
68
|
|
|
232
|
-
def _evaluate_resource_compliance(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return ComplianceResult(
|
|
251
|
-
resource_id=instance_id,
|
|
252
|
-
resource_type="AWS::EC2::Instance",
|
|
253
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
254
|
-
evaluation_reason=f"EC2 instance is deployed within VPC {vpc_id}",
|
|
255
|
-
config_rule_name=self.rule_name,
|
|
256
|
-
region=region
|
|
257
|
-
)
|
|
69
|
+
def _evaluate_resource_compliance(
|
|
70
|
+
self,
|
|
71
|
+
resource: Dict[str, Any],
|
|
72
|
+
aws_factory: AWSClientFactory,
|
|
73
|
+
region: str
|
|
74
|
+
) -> ComplianceResult:
|
|
75
|
+
"""Evaluate if VPC has Network Firewall deployed."""
|
|
76
|
+
vpc_id = resource.get('VpcId', 'unknown')
|
|
77
|
+
has_firewall = resource.get('HasFirewall', False)
|
|
78
|
+
is_default = resource.get('IsDefault', False)
|
|
79
|
+
|
|
80
|
+
# Default VPCs are often not used for production
|
|
81
|
+
if is_default:
|
|
82
|
+
evaluation_reason = f"VPC {vpc_id} is default VPC (typically not used for production)"
|
|
83
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
84
|
+
elif has_firewall:
|
|
85
|
+
evaluation_reason = f"VPC {vpc_id} has AWS Network Firewall deployed"
|
|
86
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
258
87
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
class EMRMasterNoPublicIPAssessment(BaseConfigRuleAssessment):
|
|
270
|
-
"""
|
|
271
|
-
CIS Control 3.3 - Configure Data Access Control Lists
|
|
272
|
-
AWS Config Rule: emr-master-no-public-ip
|
|
273
|
-
|
|
274
|
-
Ensures EMR master nodes do not have public IP addresses.
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
def __init__(self):
|
|
278
|
-
super().__init__(
|
|
279
|
-
rule_name="emr-master-no-public-ip",
|
|
280
|
-
control_id="3.3",
|
|
281
|
-
resource_types=["AWS::EMR::Cluster"]
|
|
88
|
+
evaluation_reason = f"VPC {vpc_id} does not have AWS Network Firewall deployed"
|
|
89
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
90
|
+
|
|
91
|
+
return ComplianceResult(
|
|
92
|
+
resource_id=vpc_id,
|
|
93
|
+
resource_type="AWS::EC2::VPC",
|
|
94
|
+
compliance_status=compliance_status,
|
|
95
|
+
evaluation_reason=evaluation_reason,
|
|
96
|
+
config_rule_name=self.rule_name,
|
|
97
|
+
region=region
|
|
282
98
|
)
|
|
283
99
|
|
|
284
|
-
def
|
|
285
|
-
"""Get
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
for instance in instances_response['Instances']:
|
|
318
|
-
if instance.get('PublicIpAddress'):
|
|
319
|
-
master_public_ip = True
|
|
320
|
-
break
|
|
321
|
-
break
|
|
322
|
-
|
|
323
|
-
clusters.append({
|
|
324
|
-
'ClusterId': cluster_id,
|
|
325
|
-
'Name': cluster.get('Name', ''),
|
|
326
|
-
'State': cluster.get('Status', {}).get('State', ''),
|
|
327
|
-
'MasterPublicDnsName': cluster.get('MasterPublicDnsName', ''),
|
|
328
|
-
'Ec2InstanceAttributes': cluster.get('Ec2InstanceAttributes', {}),
|
|
329
|
-
'MasterHasPublicIP': master_public_ip
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
except ClientError as e:
|
|
333
|
-
logger.warning(f"Error getting details for EMR cluster {cluster_id}: {e}")
|
|
334
|
-
continue
|
|
335
|
-
|
|
336
|
-
logger.debug(f"Found {len(clusters)} EMR clusters in {region}")
|
|
337
|
-
return clusters
|
|
338
|
-
|
|
339
|
-
except ClientError as e:
|
|
340
|
-
logger.error(f"Error retrieving EMR clusters in {region}: {e}")
|
|
341
|
-
raise
|
|
342
|
-
except Exception as e:
|
|
343
|
-
logger.error(f"Unexpected error retrieving EMR clusters in {region}: {e}")
|
|
344
|
-
raise
|
|
345
|
-
|
|
346
|
-
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
347
|
-
"""Evaluate if EMR master node has public IP address."""
|
|
348
|
-
cluster_id = resource.get('ClusterId', 'unknown')
|
|
349
|
-
state = resource.get('State', '')
|
|
350
|
-
master_has_public_ip = resource.get('MasterHasPublicIP', False)
|
|
351
|
-
|
|
352
|
-
# Skip terminated clusters
|
|
353
|
-
if state in ['TERMINATED', 'TERMINATED_WITH_ERRORS']:
|
|
354
|
-
return ComplianceResult(
|
|
355
|
-
resource_id=cluster_id,
|
|
356
|
-
resource_type="AWS::EMR::Cluster",
|
|
357
|
-
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
358
|
-
evaluation_reason=f"EMR cluster {cluster_id} is terminated",
|
|
359
|
-
config_rule_name=self.rule_name,
|
|
360
|
-
region=region
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
if master_has_public_ip:
|
|
364
|
-
return ComplianceResult(
|
|
365
|
-
resource_id=cluster_id,
|
|
366
|
-
resource_type="AWS::EMR::Cluster",
|
|
367
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
368
|
-
evaluation_reason="EMR master node has public IP address",
|
|
369
|
-
config_rule_name=self.rule_name,
|
|
370
|
-
region=region
|
|
371
|
-
)
|
|
372
|
-
else:
|
|
373
|
-
return ComplianceResult(
|
|
374
|
-
resource_id=cluster_id,
|
|
375
|
-
resource_type="AWS::EMR::Cluster",
|
|
376
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
377
|
-
evaluation_reason="EMR master node does not have public IP address",
|
|
378
|
-
config_rule_name=self.rule_name,
|
|
379
|
-
region=region
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
class LambdaFunctionPublicAccessProhibitedAssessment(BaseConfigRuleAssessment):
|
|
100
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
101
|
+
"""Get remediation steps for deploying Network Firewall."""
|
|
102
|
+
return [
|
|
103
|
+
"1. Create Network Firewall policy:",
|
|
104
|
+
" aws network-firewall create-firewall-policy \\",
|
|
105
|
+
" --firewall-policy-name production-policy \\",
|
|
106
|
+
" --firewall-policy '{...}'",
|
|
107
|
+
"",
|
|
108
|
+
"2. Deploy Network Firewall:",
|
|
109
|
+
" aws network-firewall create-firewall \\",
|
|
110
|
+
" --firewall-name production-firewall \\",
|
|
111
|
+
" --firewall-policy-arn <policy-arn> \\",
|
|
112
|
+
" --vpc-id <vpc-id> \\",
|
|
113
|
+
" --subnet-mappings SubnetId=<subnet-id>",
|
|
114
|
+
"",
|
|
115
|
+
"3. Update route tables to route traffic through firewall",
|
|
116
|
+
"",
|
|
117
|
+
"4. Console method:",
|
|
118
|
+
" - Navigate to VPC > Network Firewall",
|
|
119
|
+
" - Click 'Create firewall'",
|
|
120
|
+
" - Configure firewall policy and rules",
|
|
121
|
+
" - Select VPC and subnets",
|
|
122
|
+
" - Update route tables",
|
|
123
|
+
"",
|
|
124
|
+
"Priority: MEDIUM - Enhanced network security",
|
|
125
|
+
"Effort: High - Requires network architecture changes",
|
|
126
|
+
"",
|
|
127
|
+
"AWS Documentation:",
|
|
128
|
+
"https://docs.aws.amazon.com/network-firewall/latest/developerguide/getting-started.html"
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Route53ResolverFirewallEnabledAssessment(BaseConfigRuleAssessment):
|
|
384
133
|
"""
|
|
385
|
-
CIS Control
|
|
386
|
-
AWS Config Rule:
|
|
134
|
+
CIS Control 13.6 - Deny Communications with Known Malicious IP Addresses
|
|
135
|
+
AWS Config Rule: route53-resolver-firewall-enabled
|
|
387
136
|
|
|
388
|
-
Ensures
|
|
137
|
+
Ensures Route 53 Resolver DNS Firewall is enabled for DNS-level protection.
|
|
389
138
|
"""
|
|
390
139
|
|
|
391
140
|
def __init__(self):
|
|
392
141
|
super().__init__(
|
|
393
|
-
rule_name="
|
|
394
|
-
control_id="
|
|
395
|
-
resource_types=["AWS::
|
|
142
|
+
rule_name="route53-resolver-firewall-enabled",
|
|
143
|
+
control_id="13.6",
|
|
144
|
+
resource_types=["AWS::EC2::VPC"]
|
|
396
145
|
)
|
|
397
146
|
|
|
398
147
|
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
399
|
-
"""Get
|
|
400
|
-
if resource_type != "AWS::
|
|
148
|
+
"""Get VPCs and check for Route 53 Resolver Firewall."""
|
|
149
|
+
if resource_type != "AWS::EC2::VPC":
|
|
401
150
|
return []
|
|
402
151
|
|
|
403
152
|
try:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if principal == '*' or (isinstance(principal, dict) and principal.get('AWS') == '*'):
|
|
429
|
-
has_public_access = True
|
|
430
|
-
public_statements.append(statement)
|
|
431
|
-
|
|
432
|
-
functions.append({
|
|
433
|
-
'FunctionName': function_name,
|
|
434
|
-
'FunctionArn': function['FunctionArn'],
|
|
435
|
-
'Runtime': function.get('Runtime', ''),
|
|
436
|
-
'VpcConfig': function.get('VpcConfig', {}),
|
|
437
|
-
'HasPublicAccess': has_public_access,
|
|
438
|
-
'PublicStatements': public_statements
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
except ClientError as e:
|
|
442
|
-
if e.response.get('Error', {}).get('Code') == 'ResourceNotFoundException':
|
|
443
|
-
# Function has no policy, so no public access
|
|
444
|
-
functions.append({
|
|
445
|
-
'FunctionName': function_name,
|
|
446
|
-
'FunctionArn': function['FunctionArn'],
|
|
447
|
-
'Runtime': function.get('Runtime', ''),
|
|
448
|
-
'VpcConfig': function.get('VpcConfig', {}),
|
|
449
|
-
'HasPublicAccess': False,
|
|
450
|
-
'PublicStatements': []
|
|
451
|
-
})
|
|
452
|
-
else:
|
|
453
|
-
logger.warning(f"Error getting policy for Lambda function {function_name}: {e}")
|
|
454
|
-
continue
|
|
153
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
154
|
+
route53resolver_client = aws_factory.get_client('route53resolver', region)
|
|
155
|
+
|
|
156
|
+
# Get all VPCs
|
|
157
|
+
vpcs_response = ec2_client.describe_vpcs()
|
|
158
|
+
vpcs = []
|
|
159
|
+
|
|
160
|
+
# Get firewall rule group associations
|
|
161
|
+
try:
|
|
162
|
+
associations_response = route53resolver_client.list_firewall_rule_group_associations()
|
|
163
|
+
associations = associations_response.get('FirewallRuleGroupAssociations', [])
|
|
164
|
+
protected_vpcs = {assoc.get('VpcId') for assoc in associations}
|
|
165
|
+
except ClientError:
|
|
166
|
+
protected_vpcs = set()
|
|
167
|
+
|
|
168
|
+
for vpc in vpcs_response.get('Vpcs', []):
|
|
169
|
+
vpc_id = vpc.get('VpcId')
|
|
170
|
+
has_dns_firewall = vpc_id in protected_vpcs
|
|
171
|
+
|
|
172
|
+
vpcs.append({
|
|
173
|
+
'VpcId': vpc_id,
|
|
174
|
+
'HasDNSFirewall': has_dns_firewall,
|
|
175
|
+
'IsDefault': vpc.get('IsDefault', False)
|
|
176
|
+
})
|
|
455
177
|
|
|
456
|
-
|
|
457
|
-
return functions
|
|
178
|
+
return vpcs
|
|
458
179
|
|
|
459
180
|
except ClientError as e:
|
|
460
|
-
logger.error(f"Error retrieving
|
|
461
|
-
raise
|
|
462
|
-
except Exception as e:
|
|
463
|
-
logger.error(f"Unexpected error retrieving Lambda functions in {region}: {e}")
|
|
464
|
-
raise
|
|
465
|
-
|
|
466
|
-
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
467
|
-
"""Evaluate if Lambda function has public access."""
|
|
468
|
-
function_name = resource.get('FunctionName', 'unknown')
|
|
469
|
-
has_public_access = resource.get('HasPublicAccess', False)
|
|
470
|
-
|
|
471
|
-
if has_public_access:
|
|
472
|
-
return ComplianceResult(
|
|
473
|
-
resource_id=function_name,
|
|
474
|
-
resource_type="AWS::Lambda::Function",
|
|
475
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
476
|
-
evaluation_reason="Lambda function allows public access",
|
|
477
|
-
config_rule_name=self.rule_name,
|
|
478
|
-
region=region
|
|
479
|
-
)
|
|
480
|
-
else:
|
|
481
|
-
return ComplianceResult(
|
|
482
|
-
resource_id=function_name,
|
|
483
|
-
resource_type="AWS::Lambda::Function",
|
|
484
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
485
|
-
evaluation_reason="Lambda function does not allow public access",
|
|
486
|
-
config_rule_name=self.rule_name,
|
|
487
|
-
region=region
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
class SageMakerNotebookNoDirectInternetAccessAssessment(BaseConfigRuleAssessment):
|
|
492
|
-
"""
|
|
493
|
-
CIS Control 3.3 - Configure Data Access Control Lists
|
|
494
|
-
AWS Config Rule: sagemaker-notebook-no-direct-internet-access
|
|
495
|
-
|
|
496
|
-
Ensures SageMaker notebooks do not have direct internet access.
|
|
497
|
-
"""
|
|
498
|
-
|
|
499
|
-
def __init__(self):
|
|
500
|
-
super().__init__(
|
|
501
|
-
rule_name="sagemaker-notebook-no-direct-internet-access",
|
|
502
|
-
control_id="3.3",
|
|
503
|
-
resource_types=["AWS::SageMaker::NotebookInstance"]
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
507
|
-
"""Get all SageMaker notebook instances in the region."""
|
|
508
|
-
if resource_type != "AWS::SageMaker::NotebookInstance":
|
|
181
|
+
logger.error(f"Error retrieving VPCs in {region}: {e}")
|
|
509
182
|
return []
|
|
510
|
-
|
|
511
|
-
try:
|
|
512
|
-
sagemaker_client = aws_factory.get_client('sagemaker', region)
|
|
513
|
-
|
|
514
|
-
# Get all SageMaker notebook instances
|
|
515
|
-
paginator = sagemaker_client.get_paginator('list_notebook_instances')
|
|
516
|
-
notebooks = []
|
|
517
|
-
|
|
518
|
-
for page in paginator.paginate():
|
|
519
|
-
for notebook_summary in page['NotebookInstances']:
|
|
520
|
-
notebook_name = notebook_summary['NotebookInstanceName']
|
|
521
|
-
|
|
522
|
-
try:
|
|
523
|
-
# Get detailed notebook instance information
|
|
524
|
-
notebook_response = sagemaker_client.describe_notebook_instance(
|
|
525
|
-
NotebookInstanceName=notebook_name
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
notebooks.append({
|
|
529
|
-
'NotebookInstanceName': notebook_name,
|
|
530
|
-
'NotebookInstanceArn': notebook_response['NotebookInstanceArn'],
|
|
531
|
-
'NotebookInstanceStatus': notebook_response['NotebookInstanceStatus'],
|
|
532
|
-
'InstanceType': notebook_response['InstanceType'],
|
|
533
|
-
'SubnetId': notebook_response.get('SubnetId', ''),
|
|
534
|
-
'SecurityGroups': notebook_response.get('SecurityGroups', []),
|
|
535
|
-
'DirectInternetAccess': notebook_response.get('DirectInternetAccess', 'Enabled')
|
|
536
|
-
})
|
|
537
|
-
|
|
538
|
-
except ClientError as e:
|
|
539
|
-
logger.warning(f"Error getting details for SageMaker notebook {notebook_name}: {e}")
|
|
540
|
-
continue
|
|
541
|
-
|
|
542
|
-
logger.debug(f"Found {len(notebooks)} SageMaker notebook instances in {region}")
|
|
543
|
-
return notebooks
|
|
544
|
-
|
|
545
|
-
except ClientError as e:
|
|
546
|
-
logger.error(f"Error retrieving SageMaker notebook instances in {region}: {e}")
|
|
547
|
-
raise
|
|
548
|
-
except Exception as e:
|
|
549
|
-
logger.error(f"Unexpected error retrieving SageMaker notebook instances in {region}: {e}")
|
|
550
|
-
raise
|
|
551
183
|
|
|
552
|
-
def _evaluate_resource_compliance(
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if direct_internet_access == 'Enabled':
|
|
570
|
-
return ComplianceResult(
|
|
571
|
-
resource_id=notebook_name,
|
|
572
|
-
resource_type="AWS::SageMaker::NotebookInstance",
|
|
573
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
574
|
-
evaluation_reason="SageMaker notebook has direct internet access enabled",
|
|
575
|
-
config_rule_name=self.rule_name,
|
|
576
|
-
region=region
|
|
577
|
-
)
|
|
184
|
+
def _evaluate_resource_compliance(
|
|
185
|
+
self,
|
|
186
|
+
resource: Dict[str, Any],
|
|
187
|
+
aws_factory: AWSClientFactory,
|
|
188
|
+
region: str
|
|
189
|
+
) -> ComplianceResult:
|
|
190
|
+
"""Evaluate if VPC has Route 53 Resolver DNS Firewall."""
|
|
191
|
+
vpc_id = resource.get('VpcId', 'unknown')
|
|
192
|
+
has_dns_firewall = resource.get('HasDNSFirewall', False)
|
|
193
|
+
is_default = resource.get('IsDefault', False)
|
|
194
|
+
|
|
195
|
+
if is_default:
|
|
196
|
+
evaluation_reason = f"VPC {vpc_id} is default VPC (typically not used for production)"
|
|
197
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
198
|
+
elif has_dns_firewall:
|
|
199
|
+
evaluation_reason = f"VPC {vpc_id} has Route 53 Resolver DNS Firewall enabled"
|
|
200
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
578
201
|
else:
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
class SubnetAutoAssignPublicIPDisabledAssessment(BaseConfigRuleAssessment):
|
|
590
|
-
"""
|
|
591
|
-
CIS Control 3.3 - Configure Data Access Control Lists
|
|
592
|
-
AWS Config Rule: subnet-auto-assign-public-ip-disabled
|
|
593
|
-
|
|
594
|
-
Ensures subnets do not automatically assign public IPs to prevent accidental exposure.
|
|
595
|
-
"""
|
|
596
|
-
|
|
597
|
-
def __init__(self):
|
|
598
|
-
super().__init__(
|
|
599
|
-
rule_name="subnet-auto-assign-public-ip-disabled",
|
|
600
|
-
control_id="3.3",
|
|
601
|
-
resource_types=["AWS::EC2::Subnet"]
|
|
202
|
+
evaluation_reason = f"VPC {vpc_id} does not have Route 53 Resolver DNS Firewall enabled"
|
|
203
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
204
|
+
|
|
205
|
+
return ComplianceResult(
|
|
206
|
+
resource_id=vpc_id,
|
|
207
|
+
resource_type="AWS::EC2::VPC",
|
|
208
|
+
compliance_status=compliance_status,
|
|
209
|
+
evaluation_reason=evaluation_reason,
|
|
210
|
+
config_rule_name=self.rule_name,
|
|
211
|
+
region=region
|
|
602
212
|
)
|
|
603
213
|
|
|
604
|
-
def
|
|
605
|
-
"""Get
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
639
|
-
"""Evaluate if subnet auto-assigns public IPs."""
|
|
640
|
-
subnet_id = resource.get('SubnetId', 'unknown')
|
|
641
|
-
map_public_ip = resource.get('MapPublicIpOnLaunch', False)
|
|
642
|
-
state = resource.get('State', '')
|
|
643
|
-
|
|
644
|
-
# Skip subnets that are not available
|
|
645
|
-
if state != 'available':
|
|
646
|
-
return ComplianceResult(
|
|
647
|
-
resource_id=subnet_id,
|
|
648
|
-
resource_type="AWS::EC2::Subnet",
|
|
649
|
-
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
650
|
-
evaluation_reason=f"Subnet {subnet_id} is in state '{state}'",
|
|
651
|
-
config_rule_name=self.rule_name,
|
|
652
|
-
region=region
|
|
653
|
-
)
|
|
654
|
-
|
|
655
|
-
if map_public_ip:
|
|
656
|
-
return ComplianceResult(
|
|
657
|
-
resource_id=subnet_id,
|
|
658
|
-
resource_type="AWS::EC2::Subnet",
|
|
659
|
-
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
660
|
-
evaluation_reason="Subnet automatically assigns public IP addresses",
|
|
661
|
-
config_rule_name=self.rule_name,
|
|
662
|
-
region=region
|
|
663
|
-
)
|
|
664
|
-
else:
|
|
665
|
-
return ComplianceResult(
|
|
666
|
-
resource_id=subnet_id,
|
|
667
|
-
resource_type="AWS::EC2::Subnet",
|
|
668
|
-
compliance_status=ComplianceStatus.COMPLIANT,
|
|
669
|
-
evaluation_reason="Subnet does not automatically assign public IP addresses",
|
|
670
|
-
config_rule_name=self.rule_name,
|
|
671
|
-
region=region
|
|
672
|
-
)
|
|
214
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
215
|
+
"""Get remediation steps for enabling DNS Firewall."""
|
|
216
|
+
return [
|
|
217
|
+
"1. Create DNS Firewall rule group:",
|
|
218
|
+
" aws route53resolver create-firewall-rule-group \\",
|
|
219
|
+
" --name block-malicious-domains \\",
|
|
220
|
+
" --creator-request-id $(uuidgen)",
|
|
221
|
+
"",
|
|
222
|
+
"2. Add rules to block malicious domains:",
|
|
223
|
+
" aws route53resolver create-firewall-rule \\",
|
|
224
|
+
" --firewall-rule-group-id <group-id> \\",
|
|
225
|
+
" --firewall-domain-list-id <list-id> \\",
|
|
226
|
+
" --priority 100 \\",
|
|
227
|
+
" --action BLOCK",
|
|
228
|
+
"",
|
|
229
|
+
"3. Associate rule group with VPC:",
|
|
230
|
+
" aws route53resolver associate-firewall-rule-group \\",
|
|
231
|
+
" --firewall-rule-group-id <group-id> \\",
|
|
232
|
+
" --vpc-id <vpc-id> \\",
|
|
233
|
+
" --priority 100 \\",
|
|
234
|
+
" --name vpc-protection",
|
|
235
|
+
"",
|
|
236
|
+
"4. Console method:",
|
|
237
|
+
" - Navigate to Route 53 > Resolver > DNS Firewall",
|
|
238
|
+
" - Create rule group",
|
|
239
|
+
" - Add domain lists and rules",
|
|
240
|
+
" - Associate with VPCs",
|
|
241
|
+
"",
|
|
242
|
+
"Priority: MEDIUM - DNS-level threat protection",
|
|
243
|
+
"Effort: Medium - Requires rule configuration",
|
|
244
|
+
"",
|
|
245
|
+
"AWS Documentation:",
|
|
246
|
+
"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall.html"
|
|
247
|
+
]
|