aws-cis-controls-assessment 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. docs/user-guide.md +487 -0
@@ -0,0 +1,363 @@
1
+ """Remaining AWS Config Rules - Final implementation."""
2
+
3
+ from typing import Dict, List, Any
4
+ import logging
5
+ from botocore.exceptions import ClientError
6
+
7
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
8
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
9
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ACMCertificateExpirationCheckAssessment(BaseConfigRuleAssessment):
15
+ """Assessment for acm-certificate-expiration-check AWS Config rule."""
16
+
17
+ def __init__(self):
18
+ super().__init__(
19
+ rule_name="acm-certificate-expiration-check",
20
+ control_id="4.1",
21
+ resource_types=["AWS::ACM::Certificate"]
22
+ )
23
+
24
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
25
+ """Get ACM certificates."""
26
+ if resource_type != "AWS::ACM::Certificate":
27
+ return []
28
+
29
+ try:
30
+ acm_client = aws_factory.get_client('acm', region)
31
+
32
+ response = aws_factory.aws_api_call_with_retry(
33
+ lambda: acm_client.list_certificates()
34
+ )
35
+
36
+ certificates = []
37
+ for cert in response.get('CertificateSummaryList', []):
38
+ certificates.append({
39
+ 'CertificateArn': cert.get('CertificateArn'),
40
+ 'DomainName': cert.get('DomainName')
41
+ })
42
+
43
+ return certificates
44
+
45
+ except ClientError as e:
46
+ logger.error(f"Error retrieving ACM certificates in region {region}: {e}")
47
+ raise
48
+
49
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
50
+ """Evaluate if ACM certificate is not expired or expiring soon."""
51
+ cert_arn = resource.get('CertificateArn', 'unknown')
52
+ domain_name = resource.get('DomainName', 'unknown')
53
+
54
+ try:
55
+ acm_client = aws_factory.get_client('acm', region)
56
+
57
+ response = aws_factory.aws_api_call_with_retry(
58
+ lambda: acm_client.describe_certificate(CertificateArn=cert_arn)
59
+ )
60
+
61
+ certificate = response.get('Certificate', {})
62
+ not_after = certificate.get('NotAfter')
63
+ status = certificate.get('Status')
64
+
65
+ if status == 'EXPIRED':
66
+ compliance_status = ComplianceStatus.NON_COMPLIANT
67
+ evaluation_reason = f"ACM certificate {domain_name} is expired"
68
+ elif not_after:
69
+ from datetime import datetime, timezone, timedelta
70
+
71
+ # Check if certificate expires within 30 days
72
+ expiry_date = not_after
73
+ if expiry_date.tzinfo is None:
74
+ expiry_date = expiry_date.replace(tzinfo=timezone.utc)
75
+
76
+ now = datetime.now(timezone.utc)
77
+ days_until_expiry = (expiry_date - now).days
78
+
79
+ if days_until_expiry <= 30:
80
+ compliance_status = ComplianceStatus.NON_COMPLIANT
81
+ evaluation_reason = f"ACM certificate {domain_name} expires in {days_until_expiry} days"
82
+ else:
83
+ compliance_status = ComplianceStatus.COMPLIANT
84
+ evaluation_reason = f"ACM certificate {domain_name} expires in {days_until_expiry} days"
85
+ else:
86
+ compliance_status = ComplianceStatus.ERROR
87
+ evaluation_reason = f"Could not determine expiry date for certificate {domain_name}"
88
+
89
+ except ClientError as e:
90
+ compliance_status = ComplianceStatus.ERROR
91
+ evaluation_reason = f"Error checking certificate expiration for {domain_name}: {str(e)}"
92
+
93
+ return ComplianceResult(
94
+ resource_id=domain_name,
95
+ resource_type="AWS::ACM::Certificate",
96
+ compliance_status=compliance_status,
97
+ evaluation_reason=evaluation_reason,
98
+ config_rule_name=self.rule_name,
99
+ region=region
100
+ )
101
+
102
+
103
+ class DynamoDBAutoScalingEnabledAssessment(BaseConfigRuleAssessment):
104
+ """Assessment for dynamodb-autoscaling-enabled AWS Config rule."""
105
+
106
+ def __init__(self):
107
+ super().__init__(
108
+ rule_name="dynamodb-autoscaling-enabled",
109
+ control_id="12.2",
110
+ resource_types=["AWS::DynamoDB::Table"]
111
+ )
112
+
113
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
114
+ """Get DynamoDB tables."""
115
+ if resource_type != "AWS::DynamoDB::Table":
116
+ return []
117
+
118
+ try:
119
+ dynamodb_client = aws_factory.get_client('dynamodb', region)
120
+
121
+ response = aws_factory.aws_api_call_with_retry(
122
+ lambda: dynamodb_client.list_tables()
123
+ )
124
+
125
+ tables = []
126
+ for table_name in response.get('TableNames', []):
127
+ tables.append({
128
+ 'TableName': table_name
129
+ })
130
+
131
+ return tables
132
+
133
+ except ClientError as e:
134
+ logger.error(f"Error retrieving DynamoDB tables in region {region}: {e}")
135
+ raise
136
+
137
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
138
+ """Evaluate if DynamoDB table has auto scaling enabled."""
139
+ table_name = resource.get('TableName', 'unknown')
140
+
141
+ try:
142
+ # Check if table has auto scaling targets
143
+ autoscaling_client = aws_factory.get_client('application-autoscaling', region)
144
+
145
+ response = aws_factory.aws_api_call_with_retry(
146
+ lambda: autoscaling_client.describe_scalable_targets(
147
+ ServiceNamespace='dynamodb',
148
+ ResourceIds=[f'table/{table_name}']
149
+ )
150
+ )
151
+
152
+ scalable_targets = response.get('ScalableTargets', [])
153
+
154
+ if scalable_targets:
155
+ compliance_status = ComplianceStatus.COMPLIANT
156
+ evaluation_reason = f"DynamoDB table {table_name} has auto scaling enabled"
157
+ else:
158
+ compliance_status = ComplianceStatus.NON_COMPLIANT
159
+ evaluation_reason = f"DynamoDB table {table_name} does not have auto scaling enabled"
160
+
161
+ except ClientError as e:
162
+ compliance_status = ComplianceStatus.ERROR
163
+ evaluation_reason = f"Error checking auto scaling for table {table_name}: {str(e)}"
164
+
165
+ return ComplianceResult(
166
+ resource_id=table_name,
167
+ resource_type="AWS::DynamoDB::Table",
168
+ compliance_status=compliance_status,
169
+ evaluation_reason=evaluation_reason,
170
+ config_rule_name=self.rule_name,
171
+ region=region
172
+ )
173
+
174
+
175
+ class RedshiftEnhancedVPCRoutingEnabledAssessment(BaseConfigRuleAssessment):
176
+ """Assessment for redshift-enhanced-vpc-routing-enabled AWS Config rule."""
177
+
178
+ def __init__(self):
179
+ super().__init__(
180
+ rule_name="redshift-enhanced-vpc-routing-enabled",
181
+ control_id="3.3",
182
+ resource_types=["AWS::Redshift::Cluster"]
183
+ )
184
+
185
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
186
+ """Get Redshift clusters."""
187
+ if resource_type != "AWS::Redshift::Cluster":
188
+ return []
189
+
190
+ try:
191
+ redshift_client = aws_factory.get_client('redshift', region)
192
+
193
+ response = aws_factory.aws_api_call_with_retry(
194
+ lambda: redshift_client.describe_clusters()
195
+ )
196
+
197
+ clusters = []
198
+ for cluster in response.get('Clusters', []):
199
+ clusters.append({
200
+ 'ClusterIdentifier': cluster.get('ClusterIdentifier'),
201
+ 'EnhancedVpcRouting': cluster.get('EnhancedVpcRouting', False)
202
+ })
203
+
204
+ return clusters
205
+
206
+ except ClientError as e:
207
+ logger.error(f"Error retrieving Redshift clusters in region {region}: {e}")
208
+ raise
209
+
210
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
211
+ """Evaluate if Redshift cluster has enhanced VPC routing enabled."""
212
+ cluster_id = resource.get('ClusterIdentifier', 'unknown')
213
+ enhanced_vpc_routing = resource.get('EnhancedVpcRouting', False)
214
+
215
+ if enhanced_vpc_routing:
216
+ compliance_status = ComplianceStatus.COMPLIANT
217
+ evaluation_reason = f"Redshift cluster {cluster_id} has enhanced VPC routing enabled"
218
+ else:
219
+ compliance_status = ComplianceStatus.NON_COMPLIANT
220
+ evaluation_reason = f"Redshift cluster {cluster_id} does not have enhanced VPC routing enabled"
221
+
222
+ return ComplianceResult(
223
+ resource_id=cluster_id,
224
+ resource_type="AWS::Redshift::Cluster",
225
+ compliance_status=compliance_status,
226
+ evaluation_reason=evaluation_reason,
227
+ config_rule_name=self.rule_name,
228
+ region=region
229
+ )
230
+
231
+
232
+ class RestrictedCommonPortsAssessment(BaseConfigRuleAssessment):
233
+ """Assessment for restricted-common-ports AWS Config rule."""
234
+
235
+ def __init__(self):
236
+ super().__init__(
237
+ rule_name="restricted-common-ports",
238
+ control_id="3.3",
239
+ resource_types=["AWS::EC2::SecurityGroup"]
240
+ )
241
+
242
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
243
+ """Get Security Groups."""
244
+ if resource_type != "AWS::EC2::SecurityGroup":
245
+ return []
246
+
247
+ try:
248
+ ec2_client = aws_factory.get_client('ec2', region)
249
+
250
+ response = aws_factory.aws_api_call_with_retry(
251
+ lambda: ec2_client.describe_security_groups()
252
+ )
253
+
254
+ security_groups = []
255
+ for sg in response.get('SecurityGroups', []):
256
+ security_groups.append({
257
+ 'GroupId': sg.get('GroupId'),
258
+ 'GroupName': sg.get('GroupName'),
259
+ 'IpPermissions': sg.get('IpPermissions', [])
260
+ })
261
+
262
+ return security_groups
263
+
264
+ except ClientError as e:
265
+ logger.error(f"Error retrieving Security Groups in region {region}: {e}")
266
+ raise
267
+
268
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
269
+ """Evaluate if Security Group restricts access to common ports."""
270
+ group_id = resource.get('GroupId', 'unknown')
271
+ group_name = resource.get('GroupName', 'unknown')
272
+ ip_permissions = resource.get('IpPermissions', [])
273
+
274
+ # Common ports that should be restricted
275
+ restricted_ports = [20, 21, 22, 23, 25, 53, 80, 110, 135, 143, 443, 993, 995, 1433, 1521, 3306, 3389, 5432, 5984, 6379, 8020, 8086, 8888, 9042, 9160, 9200, 9300, 11211, 27017, 27018, 27019]
276
+
277
+ violations = []
278
+
279
+ for permission in ip_permissions:
280
+ from_port = permission.get('FromPort')
281
+ to_port = permission.get('ToPort')
282
+ ip_ranges = permission.get('IpRanges', [])
283
+
284
+ # Check if any restricted ports are open to 0.0.0.0/0
285
+ for ip_range in ip_ranges:
286
+ cidr = ip_range.get('CidrIp', '')
287
+ if cidr == '0.0.0.0/0':
288
+ # Check if any restricted ports are in the range
289
+ if from_port is not None and to_port is not None:
290
+ for port in restricted_ports:
291
+ if from_port <= port <= to_port:
292
+ violations.append(f"Port {port} open to 0.0.0.0/0")
293
+
294
+ if not violations:
295
+ compliance_status = ComplianceStatus.COMPLIANT
296
+ evaluation_reason = f"Security Group {group_name} does not expose restricted ports to 0.0.0.0/0"
297
+ else:
298
+ compliance_status = ComplianceStatus.NON_COMPLIANT
299
+ evaluation_reason = f"Security Group {group_name} has violations: {'; '.join(violations[:3])}" # Limit to first 3
300
+
301
+ return ComplianceResult(
302
+ resource_id=group_id,
303
+ resource_type="AWS::EC2::SecurityGroup",
304
+ compliance_status=compliance_status,
305
+ evaluation_reason=evaluation_reason,
306
+ config_rule_name=self.rule_name,
307
+ region=region
308
+ )
309
+
310
+
311
+ class AuditLogPolicyExistsAssessment(BaseConfigRuleAssessment):
312
+ """Assessment for audit-log-policy-exists AWS Config rule (Process check)."""
313
+
314
+ def __init__(self):
315
+ super().__init__(
316
+ rule_name="audit-log-policy-exists (Process check)",
317
+ control_id="8.1",
318
+ resource_types=["AWS::::Account"]
319
+ )
320
+
321
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
322
+ """Get account-level resource for audit log policy check."""
323
+ if resource_type != "AWS::::Account":
324
+ return []
325
+
326
+ # Return a single account resource
327
+ return [{'AccountId': aws_factory.account_id}]
328
+
329
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
330
+ """Evaluate if account has audit log management policy (process check)."""
331
+ account_id = resource.get('AccountId', 'unknown')
332
+
333
+ # This is a process check - we can only verify if CloudTrail is configured
334
+ # as a proxy for audit log policy existence
335
+ try:
336
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
337
+
338
+ response = aws_factory.aws_api_call_with_retry(
339
+ lambda: cloudtrail_client.describe_trails()
340
+ )
341
+
342
+ trails = response.get('trailList', [])
343
+ active_trails = [trail for trail in trails if trail.get('IsLogging', False)]
344
+
345
+ if active_trails:
346
+ compliance_status = ComplianceStatus.COMPLIANT
347
+ evaluation_reason = f"Account {account_id} has {len(active_trails)} active CloudTrail(s) indicating audit log policy implementation"
348
+ else:
349
+ compliance_status = ComplianceStatus.NON_COMPLIANT
350
+ evaluation_reason = f"Account {account_id} has no active CloudTrail, indicating lack of audit log policy"
351
+
352
+ except ClientError as e:
353
+ compliance_status = ComplianceStatus.ERROR
354
+ evaluation_reason = f"Error checking audit log policy for account {account_id}: {str(e)}"
355
+
356
+ return ComplianceResult(
357
+ resource_id=account_id,
358
+ resource_type="AWS::::Account",
359
+ compliance_status=compliance_status,
360
+ evaluation_reason=evaluation_reason,
361
+ config_rule_name=self.rule_name,
362
+ region=region
363
+ )