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,235 @@
|
|
|
1
|
+
"""Control 3.4: Enforce Data Retention assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
8
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
9
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class S3VersionLifecyclePolicyAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for s3-version-lifecycle-policy-check Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize S3 version lifecycle policy assessment."""
|
|
19
|
+
super().__init__(
|
|
20
|
+
rule_name="s3-version-lifecycle-policy-check",
|
|
21
|
+
control_id="3.4",
|
|
22
|
+
resource_types=["AWS::S3::Bucket"]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
26
|
+
"""Get all S3 buckets."""
|
|
27
|
+
if resource_type != "AWS::S3::Bucket":
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
32
|
+
|
|
33
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
34
|
+
lambda: s3_client.list_buckets()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
buckets = []
|
|
38
|
+
for bucket in response.get('Buckets', []):
|
|
39
|
+
bucket_name = bucket.get('Name')
|
|
40
|
+
|
|
41
|
+
# Check if bucket is in the current region
|
|
42
|
+
try:
|
|
43
|
+
bucket_location = aws_factory.aws_api_call_with_retry(
|
|
44
|
+
lambda: s3_client.get_bucket_location(Bucket=bucket_name)
|
|
45
|
+
)
|
|
46
|
+
bucket_region = bucket_location.get('LocationConstraint') or 'us-east-1'
|
|
47
|
+
|
|
48
|
+
if bucket_region == region or (region == 'us-east-1' and bucket_region is None):
|
|
49
|
+
buckets.append({
|
|
50
|
+
'BucketName': bucket_name,
|
|
51
|
+
'CreationDate': bucket.get('CreationDate')
|
|
52
|
+
})
|
|
53
|
+
except ClientError as e:
|
|
54
|
+
# Skip buckets we can't access
|
|
55
|
+
logger.debug(f"Cannot access bucket {bucket_name}: {e}")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
logger.debug(f"Found {len(buckets)} S3 buckets in region {region}")
|
|
59
|
+
return buckets
|
|
60
|
+
|
|
61
|
+
except ClientError as e:
|
|
62
|
+
logger.error(f"Error retrieving S3 buckets: {e}")
|
|
63
|
+
raise
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"Unexpected error retrieving S3 buckets: {e}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
69
|
+
"""Evaluate if S3 bucket has lifecycle policy configured."""
|
|
70
|
+
bucket_name = resource.get('BucketName', 'unknown')
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
74
|
+
|
|
75
|
+
# Check if bucket has lifecycle configuration
|
|
76
|
+
try:
|
|
77
|
+
lifecycle_response = aws_factory.aws_api_call_with_retry(
|
|
78
|
+
lambda: s3_client.get_bucket_lifecycle_configuration(Bucket=bucket_name)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
rules = lifecycle_response.get('Rules', [])
|
|
82
|
+
|
|
83
|
+
# Check if there are any lifecycle rules
|
|
84
|
+
if rules:
|
|
85
|
+
# Check for versioning-related rules
|
|
86
|
+
has_version_rules = False
|
|
87
|
+
rule_details = []
|
|
88
|
+
|
|
89
|
+
for rule in rules:
|
|
90
|
+
if rule.get('Status') == 'Enabled':
|
|
91
|
+
rule_id = rule.get('Id', 'unnamed')
|
|
92
|
+
rule_details.append(rule_id)
|
|
93
|
+
|
|
94
|
+
# Check for noncurrent version transitions or expiration
|
|
95
|
+
if (rule.get('NoncurrentVersionTransitions') or
|
|
96
|
+
rule.get('NoncurrentVersionExpiration')):
|
|
97
|
+
has_version_rules = True
|
|
98
|
+
|
|
99
|
+
if has_version_rules:
|
|
100
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
101
|
+
evaluation_reason = f"Bucket {bucket_name} has lifecycle policy with version management rules: {', '.join(rule_details)}"
|
|
102
|
+
else:
|
|
103
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
104
|
+
evaluation_reason = f"Bucket {bucket_name} has lifecycle policy but no version management rules"
|
|
105
|
+
else:
|
|
106
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
107
|
+
evaluation_reason = f"Bucket {bucket_name} has no enabled lifecycle rules"
|
|
108
|
+
|
|
109
|
+
except ClientError as lifecycle_error:
|
|
110
|
+
if lifecycle_error.response.get('Error', {}).get('Code') == 'NoSuchLifecycleConfiguration':
|
|
111
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
112
|
+
evaluation_reason = f"Bucket {bucket_name} has no lifecycle configuration"
|
|
113
|
+
else:
|
|
114
|
+
raise lifecycle_error
|
|
115
|
+
|
|
116
|
+
except ClientError as e:
|
|
117
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
118
|
+
if error_code in ['AccessDenied', 'NoSuchBucket']:
|
|
119
|
+
compliance_status = ComplianceStatus.ERROR
|
|
120
|
+
evaluation_reason = f"Cannot access bucket {bucket_name}: {error_code}"
|
|
121
|
+
else:
|
|
122
|
+
compliance_status = ComplianceStatus.ERROR
|
|
123
|
+
evaluation_reason = f"Error checking lifecycle policy for bucket {bucket_name}: {str(e)}"
|
|
124
|
+
except Exception as e:
|
|
125
|
+
compliance_status = ComplianceStatus.ERROR
|
|
126
|
+
evaluation_reason = f"Unexpected error checking lifecycle policy for bucket {bucket_name}: {str(e)}"
|
|
127
|
+
|
|
128
|
+
return ComplianceResult(
|
|
129
|
+
resource_id=bucket_name,
|
|
130
|
+
resource_type="AWS::S3::Bucket",
|
|
131
|
+
compliance_status=compliance_status,
|
|
132
|
+
evaluation_reason=evaluation_reason,
|
|
133
|
+
config_rule_name=self.rule_name,
|
|
134
|
+
region=region
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
138
|
+
"""Get specific remediation steps for S3 lifecycle policies."""
|
|
139
|
+
return [
|
|
140
|
+
"Identify S3 buckets without lifecycle policies or version management rules",
|
|
141
|
+
"For each bucket, configure appropriate lifecycle policies:",
|
|
142
|
+
" 1. Go to the S3 console",
|
|
143
|
+
" 2. Select the bucket",
|
|
144
|
+
" 3. Go to Management > Lifecycle rules",
|
|
145
|
+
" 4. Create a new lifecycle rule",
|
|
146
|
+
" 5. Configure transitions for current and noncurrent versions",
|
|
147
|
+
" 6. Set expiration policies for old versions",
|
|
148
|
+
"Use AWS CLI: aws s3api put-bucket-lifecycle-configuration",
|
|
149
|
+
"Consider cost optimization by transitioning old versions to cheaper storage classes",
|
|
150
|
+
"Set up monitoring for lifecycle rule effectiveness"
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CloudWatchLogGroupRetentionAssessment(BaseConfigRuleAssessment):
|
|
155
|
+
"""Assessment for cw-loggroup-retention-period-check Config rule."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, min_retention_days: int = 30):
|
|
158
|
+
"""Initialize CloudWatch log group retention assessment."""
|
|
159
|
+
super().__init__(
|
|
160
|
+
rule_name="cw-loggroup-retention-period-check",
|
|
161
|
+
control_id="3.4",
|
|
162
|
+
resource_types=["AWS::Logs::LogGroup"],
|
|
163
|
+
parameters={"minRetentionTime": min_retention_days}
|
|
164
|
+
)
|
|
165
|
+
self.min_retention_days = min_retention_days
|
|
166
|
+
|
|
167
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
168
|
+
"""Get all CloudWatch log groups in the region."""
|
|
169
|
+
if resource_type != "AWS::Logs::LogGroup":
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
logs_client = aws_factory.get_client('logs', region)
|
|
174
|
+
|
|
175
|
+
log_groups = []
|
|
176
|
+
paginator = logs_client.get_paginator('describe_log_groups')
|
|
177
|
+
|
|
178
|
+
for page in paginator.paginate():
|
|
179
|
+
for log_group in page.get('logGroups', []):
|
|
180
|
+
log_groups.append({
|
|
181
|
+
'LogGroupName': log_group.get('logGroupName'),
|
|
182
|
+
'RetentionInDays': log_group.get('retentionInDays'),
|
|
183
|
+
'CreationTime': log_group.get('creationTime'),
|
|
184
|
+
'StoredBytes': log_group.get('storedBytes', 0)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
logger.debug(f"Found {len(log_groups)} CloudWatch log groups in region {region}")
|
|
188
|
+
return log_groups
|
|
189
|
+
|
|
190
|
+
except ClientError as e:
|
|
191
|
+
logger.error(f"Error retrieving CloudWatch log groups in region {region}: {e}")
|
|
192
|
+
raise
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Unexpected error retrieving CloudWatch log groups in region {region}: {e}")
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
198
|
+
"""Evaluate if CloudWatch log group has appropriate retention period."""
|
|
199
|
+
log_group_name = resource.get('LogGroupName', 'unknown')
|
|
200
|
+
retention_days = resource.get('RetentionInDays')
|
|
201
|
+
|
|
202
|
+
if retention_days is None:
|
|
203
|
+
# No retention policy means logs are kept indefinitely
|
|
204
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
205
|
+
evaluation_reason = f"Log group {log_group_name} has no retention policy (logs kept indefinitely)"
|
|
206
|
+
elif retention_days >= self.min_retention_days:
|
|
207
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
208
|
+
evaluation_reason = f"Log group {log_group_name} has {retention_days} days retention (meets minimum {self.min_retention_days} days)"
|
|
209
|
+
else:
|
|
210
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
211
|
+
evaluation_reason = f"Log group {log_group_name} has {retention_days} days retention (below minimum {self.min_retention_days} days)"
|
|
212
|
+
|
|
213
|
+
return ComplianceResult(
|
|
214
|
+
resource_id=log_group_name,
|
|
215
|
+
resource_type="AWS::Logs::LogGroup",
|
|
216
|
+
compliance_status=compliance_status,
|
|
217
|
+
evaluation_reason=evaluation_reason,
|
|
218
|
+
config_rule_name=self.rule_name,
|
|
219
|
+
region=region
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
223
|
+
"""Get specific remediation steps for CloudWatch log group retention."""
|
|
224
|
+
return [
|
|
225
|
+
f"Identify CloudWatch log groups without retention policies or with retention below {self.min_retention_days} days",
|
|
226
|
+
"For each log group, set appropriate retention period:",
|
|
227
|
+
" 1. Go to the CloudWatch console",
|
|
228
|
+
" 2. Navigate to Logs > Log groups",
|
|
229
|
+
" 3. Select the log group",
|
|
230
|
+
" 4. Go to Actions > Edit retention setting",
|
|
231
|
+
" 5. Set retention period based on compliance requirements",
|
|
232
|
+
f"Use AWS CLI: aws logs put-retention-policy --log-group-name <name> --retention-in-days {self.min_retention_days}",
|
|
233
|
+
"Consider cost implications of longer retention periods",
|
|
234
|
+
"Set up automated retention policy management for new log groups"
|
|
235
|
+
]
|