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,310 @@
|
|
|
1
|
+
"""Access Keys Rotation and Management assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone, timedelta
|
|
6
|
+
from botocore.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
9
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
10
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AccessKeysRotatedAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for access-keys-rotated Config rule."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, max_access_key_age: int = 90):
|
|
19
|
+
"""Initialize access keys rotation assessment."""
|
|
20
|
+
super().__init__(
|
|
21
|
+
rule_name="access-keys-rotated",
|
|
22
|
+
control_id="4.1",
|
|
23
|
+
resource_types=["AWS::IAM::User"],
|
|
24
|
+
parameters={"maxAccessKeyAge": max_access_key_age}
|
|
25
|
+
)
|
|
26
|
+
self.max_access_key_age = max_access_key_age
|
|
27
|
+
|
|
28
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
29
|
+
"""Get all IAM users with access keys."""
|
|
30
|
+
if resource_type != "AWS::IAM::User":
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
35
|
+
|
|
36
|
+
# Get all users
|
|
37
|
+
users_response = aws_factory.aws_api_call_with_retry(
|
|
38
|
+
lambda: iam_client.list_users()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
users_with_keys = []
|
|
42
|
+
for user in users_response.get('Users', []):
|
|
43
|
+
user_name = user.get('UserName')
|
|
44
|
+
|
|
45
|
+
# Get access keys for each user
|
|
46
|
+
try:
|
|
47
|
+
keys_response = aws_factory.aws_api_call_with_retry(
|
|
48
|
+
lambda: iam_client.list_access_keys(UserName=user_name)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
access_keys = keys_response.get('AccessKeyMetadata', [])
|
|
52
|
+
if access_keys:
|
|
53
|
+
users_with_keys.append({
|
|
54
|
+
'UserName': user_name,
|
|
55
|
+
'UserId': user.get('UserId'),
|
|
56
|
+
'CreateDate': user.get('CreateDate'),
|
|
57
|
+
'AccessKeys': access_keys
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
except ClientError as e:
|
|
61
|
+
logger.debug(f"Cannot access keys for user {user_name}: {e}")
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
logger.debug(f"Found {len(users_with_keys)} IAM users with access keys")
|
|
65
|
+
return users_with_keys
|
|
66
|
+
|
|
67
|
+
except ClientError as e:
|
|
68
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
69
|
+
raise
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Unexpected error retrieving IAM users: {e}")
|
|
72
|
+
raise
|
|
73
|
+
|
|
74
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
75
|
+
"""Evaluate if IAM user's access keys are rotated within the specified timeframe."""
|
|
76
|
+
user_name = resource.get('UserName', 'unknown')
|
|
77
|
+
access_keys = resource.get('AccessKeys', [])
|
|
78
|
+
|
|
79
|
+
now = datetime.now(timezone.utc)
|
|
80
|
+
old_keys = []
|
|
81
|
+
compliant_keys = []
|
|
82
|
+
|
|
83
|
+
for key in access_keys:
|
|
84
|
+
if key.get('Status') == 'Active':
|
|
85
|
+
create_date = key.get('CreateDate')
|
|
86
|
+
if isinstance(create_date, datetime):
|
|
87
|
+
if create_date.tzinfo is None:
|
|
88
|
+
create_date = create_date.replace(tzinfo=timezone.utc)
|
|
89
|
+
|
|
90
|
+
age_days = (now - create_date).days
|
|
91
|
+
|
|
92
|
+
if age_days > self.max_access_key_age:
|
|
93
|
+
old_keys.append({
|
|
94
|
+
'AccessKeyId': key.get('AccessKeyId'),
|
|
95
|
+
'Age': age_days
|
|
96
|
+
})
|
|
97
|
+
else:
|
|
98
|
+
compliant_keys.append({
|
|
99
|
+
'AccessKeyId': key.get('AccessKeyId'),
|
|
100
|
+
'Age': age_days
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if old_keys:
|
|
104
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
105
|
+
key_details = [f"{key['AccessKeyId']} ({key['Age']} days old)" for key in old_keys]
|
|
106
|
+
evaluation_reason = f"User {user_name} has {len(old_keys)} access key(s) older than {self.max_access_key_age} days: {', '.join(key_details)}"
|
|
107
|
+
elif compliant_keys:
|
|
108
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
109
|
+
evaluation_reason = f"User {user_name} has {len(compliant_keys)} access key(s) within rotation period"
|
|
110
|
+
else:
|
|
111
|
+
compliance_status = ComplianceStatus.NOT_APPLICABLE
|
|
112
|
+
evaluation_reason = f"User {user_name} has no active access keys"
|
|
113
|
+
|
|
114
|
+
return ComplianceResult(
|
|
115
|
+
resource_id=user_name,
|
|
116
|
+
resource_type="AWS::IAM::User",
|
|
117
|
+
compliance_status=compliance_status,
|
|
118
|
+
evaluation_reason=evaluation_reason,
|
|
119
|
+
config_rule_name=self.rule_name,
|
|
120
|
+
region=region
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
124
|
+
"""Get specific remediation steps for access key rotation."""
|
|
125
|
+
return [
|
|
126
|
+
f"Identify IAM users with access keys older than {self.max_access_key_age} days",
|
|
127
|
+
"For each user with old access keys:",
|
|
128
|
+
" 1. Create a new access key for the user",
|
|
129
|
+
" 2. Update applications/services to use the new access key",
|
|
130
|
+
" 3. Test that applications work with the new key",
|
|
131
|
+
" 4. Deactivate the old access key",
|
|
132
|
+
" 5. Monitor for any issues, then delete the old key",
|
|
133
|
+
"Use AWS CLI: aws iam create-access-key --user-name <username>",
|
|
134
|
+
"Use AWS CLI: aws iam delete-access-key --user-name <username> --access-key-id <old-key-id>",
|
|
135
|
+
"Set up automated access key rotation using AWS Secrets Manager or custom solutions",
|
|
136
|
+
"Implement monitoring and alerting for access key age"
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class EC2IMDSv2CheckAssessment(BaseConfigRuleAssessment):
|
|
141
|
+
"""Assessment for ec2-imdsv2-check Config rule."""
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
"""Initialize EC2 IMDSv2 assessment."""
|
|
145
|
+
super().__init__(
|
|
146
|
+
rule_name="ec2-imdsv2-check",
|
|
147
|
+
control_id="3.3",
|
|
148
|
+
resource_types=["AWS::EC2::Instance"]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
152
|
+
"""Get all EC2 instances in the region."""
|
|
153
|
+
if resource_type != "AWS::EC2::Instance":
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
158
|
+
|
|
159
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
160
|
+
lambda: ec2_client.describe_instances()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
instances = []
|
|
164
|
+
for reservation in response.get('Reservations', []):
|
|
165
|
+
for instance in reservation.get('Instances', []):
|
|
166
|
+
if instance.get('State', {}).get('Name') in ['running', 'stopped']:
|
|
167
|
+
instances.append({
|
|
168
|
+
'InstanceId': instance.get('InstanceId'),
|
|
169
|
+
'State': instance.get('State', {}),
|
|
170
|
+
'MetadataOptions': instance.get('MetadataOptions', {}),
|
|
171
|
+
'InstanceType': instance.get('InstanceType'),
|
|
172
|
+
'Tags': instance.get('Tags', [])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
logger.debug(f"Found {len(instances)} EC2 instances in region {region}")
|
|
176
|
+
return instances
|
|
177
|
+
|
|
178
|
+
except ClientError as e:
|
|
179
|
+
logger.error(f"Error retrieving EC2 instances in region {region}: {e}")
|
|
180
|
+
raise
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Unexpected error retrieving EC2 instances in region {region}: {e}")
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
186
|
+
"""Evaluate if EC2 instance requires IMDSv2."""
|
|
187
|
+
instance_id = resource.get('InstanceId', 'unknown')
|
|
188
|
+
metadata_options = resource.get('MetadataOptions', {})
|
|
189
|
+
|
|
190
|
+
# Check if IMDSv2 is required
|
|
191
|
+
http_tokens = metadata_options.get('HttpTokens', 'optional')
|
|
192
|
+
http_endpoint = metadata_options.get('HttpEndpoint', 'enabled')
|
|
193
|
+
|
|
194
|
+
if http_endpoint == 'disabled':
|
|
195
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
196
|
+
evaluation_reason = f"Instance {instance_id} has metadata service disabled"
|
|
197
|
+
elif http_tokens == 'required':
|
|
198
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
199
|
+
evaluation_reason = f"Instance {instance_id} requires IMDSv2 (HttpTokens: required)"
|
|
200
|
+
else:
|
|
201
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
202
|
+
evaluation_reason = f"Instance {instance_id} allows IMDSv1 (HttpTokens: {http_tokens})"
|
|
203
|
+
|
|
204
|
+
return ComplianceResult(
|
|
205
|
+
resource_id=instance_id,
|
|
206
|
+
resource_type="AWS::EC2::Instance",
|
|
207
|
+
compliance_status=compliance_status,
|
|
208
|
+
evaluation_reason=evaluation_reason,
|
|
209
|
+
config_rule_name=self.rule_name,
|
|
210
|
+
region=region
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
214
|
+
"""Get specific remediation steps for IMDSv2 enforcement."""
|
|
215
|
+
return [
|
|
216
|
+
"Identify EC2 instances that allow IMDSv1 access",
|
|
217
|
+
"For each instance, enforce IMDSv2:",
|
|
218
|
+
" 1. Test applications to ensure IMDSv2 compatibility",
|
|
219
|
+
" 2. Stop the instance (if required for modification)",
|
|
220
|
+
" 3. Modify instance metadata options to require IMDSv2",
|
|
221
|
+
" 4. Start the instance and verify functionality",
|
|
222
|
+
"Use AWS CLI: aws ec2 modify-instance-metadata-options --instance-id <id> --http-tokens required",
|
|
223
|
+
"Update launch templates and Auto Scaling groups to enforce IMDSv2 by default",
|
|
224
|
+
"Monitor applications for any IMDSv1 dependencies",
|
|
225
|
+
"Consider setting HttpPutResponseHopLimit to 1 for additional security"
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class EC2InstanceProfileAttachedAssessment(BaseConfigRuleAssessment):
|
|
230
|
+
"""Assessment for ec2-instance-profile-attached Config rule."""
|
|
231
|
+
|
|
232
|
+
def __init__(self):
|
|
233
|
+
"""Initialize EC2 instance profile assessment."""
|
|
234
|
+
super().__init__(
|
|
235
|
+
rule_name="ec2-instance-profile-attached",
|
|
236
|
+
control_id="3.3",
|
|
237
|
+
resource_types=["AWS::EC2::Instance"]
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
241
|
+
"""Get all EC2 instances in the region."""
|
|
242
|
+
if resource_type != "AWS::EC2::Instance":
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
247
|
+
|
|
248
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
249
|
+
lambda: ec2_client.describe_instances()
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
instances = []
|
|
253
|
+
for reservation in response.get('Reservations', []):
|
|
254
|
+
for instance in reservation.get('Instances', []):
|
|
255
|
+
if instance.get('State', {}).get('Name') in ['running', 'stopped']:
|
|
256
|
+
instances.append({
|
|
257
|
+
'InstanceId': instance.get('InstanceId'),
|
|
258
|
+
'State': instance.get('State', {}),
|
|
259
|
+
'IamInstanceProfile': instance.get('IamInstanceProfile'),
|
|
260
|
+
'InstanceType': instance.get('InstanceType'),
|
|
261
|
+
'Tags': instance.get('Tags', [])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
logger.debug(f"Found {len(instances)} EC2 instances in region {region}")
|
|
265
|
+
return instances
|
|
266
|
+
|
|
267
|
+
except ClientError as e:
|
|
268
|
+
logger.error(f"Error retrieving EC2 instances in region {region}: {e}")
|
|
269
|
+
raise
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Unexpected error retrieving EC2 instances in region {region}: {e}")
|
|
272
|
+
raise
|
|
273
|
+
|
|
274
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
275
|
+
"""Evaluate if EC2 instance has an IAM instance profile attached."""
|
|
276
|
+
instance_id = resource.get('InstanceId', 'unknown')
|
|
277
|
+
iam_instance_profile = resource.get('IamInstanceProfile')
|
|
278
|
+
|
|
279
|
+
if iam_instance_profile:
|
|
280
|
+
profile_arn = iam_instance_profile.get('Arn', 'unknown')
|
|
281
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
282
|
+
evaluation_reason = f"Instance {instance_id} has IAM instance profile attached: {profile_arn}"
|
|
283
|
+
else:
|
|
284
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
285
|
+
evaluation_reason = f"Instance {instance_id} does not have an IAM instance profile attached"
|
|
286
|
+
|
|
287
|
+
return ComplianceResult(
|
|
288
|
+
resource_id=instance_id,
|
|
289
|
+
resource_type="AWS::EC2::Instance",
|
|
290
|
+
compliance_status=compliance_status,
|
|
291
|
+
evaluation_reason=evaluation_reason,
|
|
292
|
+
config_rule_name=self.rule_name,
|
|
293
|
+
region=region
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
297
|
+
"""Get specific remediation steps for instance profile attachment."""
|
|
298
|
+
return [
|
|
299
|
+
"Identify EC2 instances without IAM instance profiles",
|
|
300
|
+
"For each instance, attach an appropriate IAM instance profile:",
|
|
301
|
+
" 1. Create an IAM role with necessary permissions",
|
|
302
|
+
" 2. Create an instance profile and add the role to it",
|
|
303
|
+
" 3. Stop the instance (if required)",
|
|
304
|
+
" 4. Associate the instance profile with the instance",
|
|
305
|
+
" 5. Start the instance and verify functionality",
|
|
306
|
+
"Use AWS CLI: aws ec2 associate-iam-instance-profile --instance-id <id> --iam-instance-profile Name=<profile>",
|
|
307
|
+
"Follow principle of least privilege when creating IAM roles",
|
|
308
|
+
"Update launch templates to include instance profiles by default",
|
|
309
|
+
"Monitor and audit instance profile usage regularly"
|
|
310
|
+
]
|