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.
Files changed (40) hide show
  1. aws_cis_assessment/__init__.py +4 -4
  2. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
  3. aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
  4. aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
  5. aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
  6. aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
  7. aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
  8. aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
  9. aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
  10. aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
  11. aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
  12. aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
  13. aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
  14. aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
  15. aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
  16. aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
  17. aws_cis_assessment/controls/ig1/control_macie.py +165 -0
  18. aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
  19. aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
  20. aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
  21. aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
  22. aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
  23. aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
  24. aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
  25. aws_cis_assessment/controls/ig1/control_version_mgmt.py +329 -0
  26. aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
  27. aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
  28. aws_cis_assessment/core/models.py +20 -1
  29. aws_cis_assessment/core/scoring_engine.py +98 -1
  30. aws_cis_assessment/reporters/base_reporter.py +31 -1
  31. aws_cis_assessment/reporters/html_reporter.py +163 -0
  32. aws_cis_controls_assessment-1.2.0.dist-info/METADATA +320 -0
  33. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/RECORD +39 -15
  34. docs/developer-guide.md +204 -5
  35. docs/user-guide.md +137 -4
  36. aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
  37. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/WHEEL +0 -0
  38. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/entry_points.txt +0 -0
  39. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/licenses/LICENSE +0 -0
  40. {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.3 - Network Security Controls
3
- Critical network security rules to prevent public exposure and ensure proper network isolation.
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, Optional
8
- import boto3
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 DMSReplicationNotPublicAssessment(BaseConfigRuleAssessment):
17
+ class NetworkFirewallDeployedAssessment(BaseConfigRuleAssessment):
20
18
  """
21
- CIS Control 3.3 - Configure Data Access Control Lists
22
- AWS Config Rule: dms-replication-not-public
19
+ CIS Control 13.6 - Deny Communications with Known Malicious IP Addresses
20
+ AWS Config Rule: network-firewall-deployed
23
21
 
24
- Ensures DMS replication instances are not publicly accessible to prevent data exposure.
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="dms-replication-not-public",
30
- control_id="3.3",
31
- resource_types=["AWS::DMS::ReplicationInstance"]
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 all DMS replication instances in the region."""
36
- if resource_type != "AWS::DMS::ReplicationInstance":
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
- dms_client = aws_factory.get_client('dms', region)
41
-
42
- # Get all DMS replication instances
43
- paginator = dms_client.get_paginator('describe_replication_instances')
44
- instances = []
45
-
46
- for page in paginator.paginate():
47
- for instance in page['ReplicationInstances']:
48
- instances.append({
49
- 'ReplicationInstanceIdentifier': instance['ReplicationInstanceIdentifier'],
50
- 'ReplicationInstanceArn': instance['ReplicationInstanceArn'],
51
- 'ReplicationInstanceClass': instance.get('ReplicationInstanceClass', ''),
52
- 'PubliclyAccessible': instance.get('PubliclyAccessible', False),
53
- 'VpcSecurityGroups': [sg['VpcSecurityGroupId'] for sg in instance.get('VpcSecurityGroups', [])],
54
- 'ReplicationSubnetGroup': instance.get('ReplicationSubnetGroup', {}).get('ReplicationSubnetGroupIdentifier', ''),
55
- 'AvailabilityZone': instance.get('AvailabilityZone', ''),
56
- 'MultiAZ': instance.get('MultiAZ', False)
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
- except ClientError as e:
144
- logger.warning(f"Error getting details for Elasticsearch domain {domain_name}: {e}")
145
- continue
57
+ vpcs.append({
58
+ 'VpcId': vpc_id,
59
+ 'HasFirewall': has_firewall,
60
+ 'IsDefault': vpc.get('IsDefault', False)
61
+ })
146
62
 
147
- logger.debug(f"Found {len(domains)} Elasticsearch domains in {region}")
148
- return domains
63
+ return vpcs
149
64
 
150
65
  except ClientError as e:
151
- logger.error(f"Error retrieving Elasticsearch domains in {region}: {e}")
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(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
233
- """Evaluate if EC2 instance is deployed within VPC."""
234
- instance_id = resource.get('InstanceId', 'unknown')
235
- vpc_id = resource.get('VpcId', '')
236
- state = resource.get('State', '')
237
-
238
- # Skip terminated instances
239
- if state == 'terminated':
240
- return ComplianceResult(
241
- resource_id=instance_id,
242
- resource_type="AWS::EC2::Instance",
243
- compliance_status=ComplianceStatus.NOT_APPLICABLE,
244
- evaluation_reason=f"Instance {instance_id} is terminated",
245
- config_rule_name=self.rule_name,
246
- region=region
247
- )
248
-
249
- if vpc_id:
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
- return ComplianceResult(
260
- resource_id=instance_id,
261
- resource_type="AWS::EC2::Instance",
262
- compliance_status=ComplianceStatus.NON_COMPLIANT,
263
- evaluation_reason="EC2 instance is not deployed within VPC (EC2-Classic)",
264
- config_rule_name=self.rule_name,
265
- region=region
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 _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
285
- """Get all EMR clusters in the region."""
286
- if resource_type != "AWS::EMR::Cluster":
287
- return []
288
-
289
- try:
290
- emr_client = aws_factory.get_client('emr', region)
291
-
292
- # Get all EMR clusters
293
- paginator = emr_client.get_paginator('list_clusters')
294
- clusters = []
295
-
296
- for page in paginator.paginate():
297
- for cluster_summary in page['Clusters']:
298
- cluster_id = cluster_summary['Id']
299
-
300
- try:
301
- # Get detailed cluster information
302
- cluster_response = emr_client.describe_cluster(ClusterId=cluster_id)
303
- cluster = cluster_response['Cluster']
304
-
305
- # Get instance groups to check master node configuration
306
- instance_groups_response = emr_client.list_instance_groups(ClusterId=cluster_id)
307
-
308
- master_public_ip = False
309
- for instance_group in instance_groups_response['InstanceGroups']:
310
- if instance_group['InstanceGroupType'] == 'MASTER':
311
- # Check if master instances have public IPs
312
- instances_response = emr_client.list_instances(
313
- ClusterId=cluster_id,
314
- InstanceGroupTypes=['MASTER']
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 3.3 - Configure Data Access Control Lists
386
- AWS Config Rule: lambda-function-public-access-prohibited
134
+ CIS Control 13.6 - Deny Communications with Known Malicious IP Addresses
135
+ AWS Config Rule: route53-resolver-firewall-enabled
387
136
 
388
- Ensures Lambda functions cannot be publicly accessed.
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="lambda-function-public-access-prohibited",
394
- control_id="3.3",
395
- resource_types=["AWS::Lambda::Function"]
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 all Lambda functions in the region."""
400
- if resource_type != "AWS::Lambda::Function":
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
- lambda_client = aws_factory.get_client('lambda', region)
405
-
406
- # Get all Lambda functions
407
- paginator = lambda_client.get_paginator('list_functions')
408
- functions = []
409
-
410
- for page in paginator.paginate():
411
- for function in page['Functions']:
412
- function_name = function['FunctionName']
413
-
414
- try:
415
- # Get function policy to check for public access
416
- policy_response = lambda_client.get_policy(FunctionName=function_name)
417
- policy_doc = json.loads(policy_response['Policy'])
418
-
419
- has_public_access = False
420
- public_statements = []
421
-
422
- for statement in policy_doc.get('Statement', []):
423
- if isinstance(statement, dict):
424
- effect = statement.get('Effect', '')
425
- principal = statement.get('Principal', {})
426
-
427
- if effect == 'Allow':
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
- logger.debug(f"Found {len(functions)} Lambda functions in {region}")
457
- return functions
178
+ return vpcs
458
179
 
459
180
  except ClientError as e:
460
- logger.error(f"Error retrieving Lambda functions in {region}: {e}")
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(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
553
- """Evaluate if SageMaker notebook has direct internet access."""
554
- notebook_name = resource.get('NotebookInstanceName', 'unknown')
555
- status = resource.get('NotebookInstanceStatus', '')
556
- direct_internet_access = resource.get('DirectInternetAccess', 'Enabled')
557
-
558
- # Skip deleted notebooks
559
- if status == 'Deleting':
560
- return ComplianceResult(
561
- resource_id=notebook_name,
562
- resource_type="AWS::SageMaker::NotebookInstance",
563
- compliance_status=ComplianceStatus.NOT_APPLICABLE,
564
- evaluation_reason=f"SageMaker notebook {notebook_name} is being deleted",
565
- config_rule_name=self.rule_name,
566
- region=region
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
- return ComplianceResult(
580
- resource_id=notebook_name,
581
- resource_type="AWS::SageMaker::NotebookInstance",
582
- compliance_status=ComplianceStatus.COMPLIANT,
583
- evaluation_reason="SageMaker notebook does not have direct internet access",
584
- config_rule_name=self.rule_name,
585
- region=region
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 _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
605
- """Get all subnets in the region."""
606
- if resource_type != "AWS::EC2::Subnet":
607
- return []
608
-
609
- try:
610
- ec2_client = aws_factory.get_client('ec2', region)
611
-
612
- # Get all subnets
613
- paginator = ec2_client.get_paginator('describe_subnets')
614
- subnets = []
615
-
616
- for page in paginator.paginate():
617
- for subnet in page['Subnets']:
618
- subnets.append({
619
- 'SubnetId': subnet['SubnetId'],
620
- 'VpcId': subnet['VpcId'],
621
- 'AvailabilityZone': subnet['AvailabilityZone'],
622
- 'CidrBlock': subnet['CidrBlock'],
623
- 'MapPublicIpOnLaunch': subnet.get('MapPublicIpOnLaunch', False),
624
- 'State': subnet.get('State', ''),
625
- 'Tags': subnet.get('Tags', [])
626
- })
627
-
628
- logger.debug(f"Found {len(subnets)} subnets in {region}")
629
- return subnets
630
-
631
- except ClientError as e:
632
- logger.error(f"Error retrieving subnets in {region}: {e}")
633
- raise
634
- except Exception as e:
635
- logger.error(f"Unexpected error retrieving subnets in {region}: {e}")
636
- raise
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
+ ]