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,383 @@
|
|
|
1
|
+
"""IAM Policy Management assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
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 IAMPolicyNoStatementsWithAdminAccessAssessment(BaseConfigRuleAssessment):
|
|
16
|
+
"""Assessment for iam-policy-no-statements-with-admin-access Config rule."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize IAM policy admin access assessment."""
|
|
20
|
+
super().__init__(
|
|
21
|
+
rule_name="iam-policy-no-statements-with-admin-access",
|
|
22
|
+
control_id="3.3",
|
|
23
|
+
resource_types=["AWS::IAM::Policy"]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
27
|
+
"""Get all customer-managed IAM policies."""
|
|
28
|
+
if resource_type != "AWS::IAM::Policy":
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
33
|
+
|
|
34
|
+
# Get customer-managed policies only
|
|
35
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
36
|
+
lambda: iam_client.list_policies(Scope='Local')
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
policies = []
|
|
40
|
+
for policy in response.get('Policies', []):
|
|
41
|
+
policies.append({
|
|
42
|
+
'PolicyName': policy.get('PolicyName'),
|
|
43
|
+
'PolicyId': policy.get('PolicyId'),
|
|
44
|
+
'Arn': policy.get('Arn'),
|
|
45
|
+
'DefaultVersionId': policy.get('DefaultVersionId'),
|
|
46
|
+
'CreateDate': policy.get('CreateDate')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
logger.debug(f"Found {len(policies)} customer-managed IAM policies")
|
|
50
|
+
return policies
|
|
51
|
+
|
|
52
|
+
except ClientError as e:
|
|
53
|
+
logger.error(f"Error retrieving IAM policies: {e}")
|
|
54
|
+
raise
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Unexpected error retrieving IAM policies: {e}")
|
|
57
|
+
raise
|
|
58
|
+
|
|
59
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
60
|
+
"""Evaluate if IAM policy contains admin access statements."""
|
|
61
|
+
policy_arn = resource.get('Arn', 'unknown')
|
|
62
|
+
policy_name = resource.get('PolicyName', 'unknown')
|
|
63
|
+
version_id = resource.get('DefaultVersionId', 'v1')
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
67
|
+
|
|
68
|
+
# Get policy document
|
|
69
|
+
policy_response = aws_factory.aws_api_call_with_retry(
|
|
70
|
+
lambda: iam_client.get_policy_version(
|
|
71
|
+
PolicyArn=policy_arn,
|
|
72
|
+
VersionId=version_id
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
policy_document = policy_response.get('PolicyVersion', {}).get('Document', {})
|
|
77
|
+
|
|
78
|
+
# Check for admin access patterns
|
|
79
|
+
admin_statements = []
|
|
80
|
+
statements = policy_document.get('Statement', [])
|
|
81
|
+
|
|
82
|
+
if not isinstance(statements, list):
|
|
83
|
+
statements = [statements]
|
|
84
|
+
|
|
85
|
+
for i, statement in enumerate(statements):
|
|
86
|
+
if statement.get('Effect') == 'Allow':
|
|
87
|
+
actions = statement.get('Action', [])
|
|
88
|
+
resources = statement.get('Resource', [])
|
|
89
|
+
|
|
90
|
+
if not isinstance(actions, list):
|
|
91
|
+
actions = [actions]
|
|
92
|
+
if not isinstance(resources, list):
|
|
93
|
+
resources = [resources]
|
|
94
|
+
|
|
95
|
+
# Check for admin access pattern: Action: "*" and Resource: "*"
|
|
96
|
+
if '*' in actions and '*' in resources:
|
|
97
|
+
admin_statements.append(f"Statement {i+1}")
|
|
98
|
+
|
|
99
|
+
if admin_statements:
|
|
100
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
101
|
+
evaluation_reason = f"Policy {policy_name} contains admin access statements: {', '.join(admin_statements)}"
|
|
102
|
+
else:
|
|
103
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
104
|
+
evaluation_reason = f"Policy {policy_name} does not contain admin access statements"
|
|
105
|
+
|
|
106
|
+
except ClientError as e:
|
|
107
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
108
|
+
if error_code in ['AccessDenied', 'NoSuchEntity']:
|
|
109
|
+
compliance_status = ComplianceStatus.ERROR
|
|
110
|
+
evaluation_reason = f"Cannot access policy {policy_name}: {error_code}"
|
|
111
|
+
else:
|
|
112
|
+
compliance_status = ComplianceStatus.ERROR
|
|
113
|
+
evaluation_reason = f"Error checking policy {policy_name}: {str(e)}"
|
|
114
|
+
except Exception as e:
|
|
115
|
+
compliance_status = ComplianceStatus.ERROR
|
|
116
|
+
evaluation_reason = f"Unexpected error checking policy {policy_name}: {str(e)}"
|
|
117
|
+
|
|
118
|
+
return ComplianceResult(
|
|
119
|
+
resource_id=policy_arn,
|
|
120
|
+
resource_type="AWS::IAM::Policy",
|
|
121
|
+
compliance_status=compliance_status,
|
|
122
|
+
evaluation_reason=evaluation_reason,
|
|
123
|
+
config_rule_name=self.rule_name,
|
|
124
|
+
region=region
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
128
|
+
"""Get specific remediation steps for admin access policies."""
|
|
129
|
+
return [
|
|
130
|
+
"Identify IAM policies with admin access statements (Action: '*', Resource: '*')",
|
|
131
|
+
"For each policy with admin access:",
|
|
132
|
+
" 1. Review the policy's purpose and usage",
|
|
133
|
+
" 2. Identify specific permissions actually needed",
|
|
134
|
+
" 3. Create a new policy version with least privilege permissions",
|
|
135
|
+
" 4. Test the new policy with affected users/roles",
|
|
136
|
+
" 5. Set the new version as default",
|
|
137
|
+
" 6. Monitor for any access issues",
|
|
138
|
+
"Use AWS CLI: aws iam create-policy-version --policy-arn <arn> --policy-document file://policy.json",
|
|
139
|
+
"Consider using AWS managed policies for common use cases",
|
|
140
|
+
"Implement regular policy reviews and access audits"
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class IAMNoInlinePolicyCheckAssessment(BaseConfigRuleAssessment):
|
|
145
|
+
"""Assessment for iam-no-inline-policy-check Config rule."""
|
|
146
|
+
|
|
147
|
+
def __init__(self):
|
|
148
|
+
"""Initialize IAM inline policy assessment."""
|
|
149
|
+
super().__init__(
|
|
150
|
+
rule_name="iam-no-inline-policy-check",
|
|
151
|
+
control_id="3.3",
|
|
152
|
+
resource_types=["AWS::IAM::User", "AWS::IAM::Role", "AWS::IAM::Group"]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
156
|
+
"""Get all IAM users, roles, and groups."""
|
|
157
|
+
resources = []
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
161
|
+
|
|
162
|
+
if resource_type == "AWS::IAM::User":
|
|
163
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
164
|
+
lambda: iam_client.list_users()
|
|
165
|
+
)
|
|
166
|
+
for user in response.get('Users', []):
|
|
167
|
+
resources.append({
|
|
168
|
+
'Type': 'User',
|
|
169
|
+
'Name': user.get('UserName'),
|
|
170
|
+
'Arn': user.get('Arn'),
|
|
171
|
+
'CreateDate': user.get('CreateDate')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
elif resource_type == "AWS::IAM::Role":
|
|
175
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
176
|
+
lambda: iam_client.list_roles()
|
|
177
|
+
)
|
|
178
|
+
for role in response.get('Roles', []):
|
|
179
|
+
resources.append({
|
|
180
|
+
'Type': 'Role',
|
|
181
|
+
'Name': role.get('RoleName'),
|
|
182
|
+
'Arn': role.get('Arn'),
|
|
183
|
+
'CreateDate': role.get('CreateDate')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
elif resource_type == "AWS::IAM::Group":
|
|
187
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
188
|
+
lambda: iam_client.list_groups()
|
|
189
|
+
)
|
|
190
|
+
for group in response.get('Groups', []):
|
|
191
|
+
resources.append({
|
|
192
|
+
'Type': 'Group',
|
|
193
|
+
'Name': group.get('GroupName'),
|
|
194
|
+
'Arn': group.get('Arn'),
|
|
195
|
+
'CreateDate': group.get('CreateDate')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
logger.debug(f"Found {len(resources)} IAM {resource_type.split('::')[-1].lower()}s")
|
|
199
|
+
return resources
|
|
200
|
+
|
|
201
|
+
except ClientError as e:
|
|
202
|
+
logger.error(f"Error retrieving IAM {resource_type}: {e}")
|
|
203
|
+
raise
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Unexpected error retrieving IAM {resource_type}: {e}")
|
|
206
|
+
raise
|
|
207
|
+
|
|
208
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
209
|
+
"""Evaluate if IAM entity has inline policies."""
|
|
210
|
+
entity_type = resource.get('Type', 'unknown')
|
|
211
|
+
entity_name = resource.get('Name', 'unknown')
|
|
212
|
+
entity_arn = resource.get('Arn', 'unknown')
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
216
|
+
|
|
217
|
+
inline_policies = []
|
|
218
|
+
|
|
219
|
+
if entity_type == 'User':
|
|
220
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
221
|
+
lambda: iam_client.list_user_policies(UserName=entity_name)
|
|
222
|
+
)
|
|
223
|
+
inline_policies = response.get('PolicyNames', [])
|
|
224
|
+
|
|
225
|
+
elif entity_type == 'Role':
|
|
226
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
227
|
+
lambda: iam_client.list_role_policies(RoleName=entity_name)
|
|
228
|
+
)
|
|
229
|
+
inline_policies = response.get('PolicyNames', [])
|
|
230
|
+
|
|
231
|
+
elif entity_type == 'Group':
|
|
232
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
233
|
+
lambda: iam_client.list_group_policies(GroupName=entity_name)
|
|
234
|
+
)
|
|
235
|
+
inline_policies = response.get('PolicyNames', [])
|
|
236
|
+
|
|
237
|
+
if inline_policies:
|
|
238
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
239
|
+
evaluation_reason = f"{entity_type} {entity_name} has {len(inline_policies)} inline policy(ies): {', '.join(inline_policies)}"
|
|
240
|
+
else:
|
|
241
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
242
|
+
evaluation_reason = f"{entity_type} {entity_name} has no inline policies"
|
|
243
|
+
|
|
244
|
+
except ClientError as e:
|
|
245
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
246
|
+
if error_code in ['AccessDenied', 'NoSuchEntity']:
|
|
247
|
+
compliance_status = ComplianceStatus.ERROR
|
|
248
|
+
evaluation_reason = f"Cannot access {entity_type.lower()} {entity_name}: {error_code}"
|
|
249
|
+
else:
|
|
250
|
+
compliance_status = ComplianceStatus.ERROR
|
|
251
|
+
evaluation_reason = f"Error checking {entity_type.lower()} {entity_name}: {str(e)}"
|
|
252
|
+
except Exception as e:
|
|
253
|
+
compliance_status = ComplianceStatus.ERROR
|
|
254
|
+
evaluation_reason = f"Unexpected error checking {entity_type.lower()} {entity_name}: {str(e)}"
|
|
255
|
+
|
|
256
|
+
return ComplianceResult(
|
|
257
|
+
resource_id=entity_arn,
|
|
258
|
+
resource_type=f"AWS::IAM::{entity_type}",
|
|
259
|
+
compliance_status=compliance_status,
|
|
260
|
+
evaluation_reason=evaluation_reason,
|
|
261
|
+
config_rule_name=self.rule_name,
|
|
262
|
+
region=region
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
266
|
+
"""Get specific remediation steps for inline policies."""
|
|
267
|
+
return [
|
|
268
|
+
"Identify IAM users, roles, and groups with inline policies",
|
|
269
|
+
"For each entity with inline policies:",
|
|
270
|
+
" 1. Review the inline policy permissions",
|
|
271
|
+
" 2. Create equivalent managed policies",
|
|
272
|
+
" 3. Attach the managed policies to the entity",
|
|
273
|
+
" 4. Test that permissions work correctly",
|
|
274
|
+
" 5. Remove the inline policies",
|
|
275
|
+
"Use AWS CLI: aws iam create-policy --policy-name <name> --policy-document file://policy.json",
|
|
276
|
+
"Use AWS CLI: aws iam attach-user-policy --user-name <user> --policy-arn <arn>",
|
|
277
|
+
"Use AWS CLI: aws iam delete-user-policy --user-name <user> --policy-name <policy>",
|
|
278
|
+
"Prefer AWS managed policies when available",
|
|
279
|
+
"Implement governance to prevent future inline policy creation"
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class IAMUserGroupMembershipCheckAssessment(BaseConfigRuleAssessment):
|
|
284
|
+
"""Assessment for iam-user-group-membership-check Config rule."""
|
|
285
|
+
|
|
286
|
+
def __init__(self):
|
|
287
|
+
"""Initialize IAM user group membership assessment."""
|
|
288
|
+
super().__init__(
|
|
289
|
+
rule_name="iam-user-group-membership-check",
|
|
290
|
+
control_id="3.3",
|
|
291
|
+
resource_types=["AWS::IAM::User"]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
295
|
+
"""Get all IAM users."""
|
|
296
|
+
if resource_type != "AWS::IAM::User":
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
301
|
+
|
|
302
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
303
|
+
lambda: iam_client.list_users()
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
users = []
|
|
307
|
+
for user in response.get('Users', []):
|
|
308
|
+
users.append({
|
|
309
|
+
'UserName': user.get('UserName'),
|
|
310
|
+
'UserId': user.get('UserId'),
|
|
311
|
+
'Arn': user.get('Arn'),
|
|
312
|
+
'CreateDate': user.get('CreateDate')
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
logger.debug(f"Found {len(users)} IAM users")
|
|
316
|
+
return users
|
|
317
|
+
|
|
318
|
+
except ClientError as e:
|
|
319
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
320
|
+
raise
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Unexpected error retrieving IAM users: {e}")
|
|
323
|
+
raise
|
|
324
|
+
|
|
325
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
326
|
+
"""Evaluate if IAM user is member of at least one group."""
|
|
327
|
+
user_name = resource.get('UserName', 'unknown')
|
|
328
|
+
user_arn = resource.get('Arn', 'unknown')
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
332
|
+
|
|
333
|
+
# Get groups for user
|
|
334
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
335
|
+
lambda: iam_client.get_groups_for_user(UserName=user_name)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
groups = response.get('Groups', [])
|
|
339
|
+
|
|
340
|
+
if groups:
|
|
341
|
+
group_names = [group.get('GroupName') for group in groups]
|
|
342
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
343
|
+
evaluation_reason = f"User {user_name} is member of {len(groups)} group(s): {', '.join(group_names)}"
|
|
344
|
+
else:
|
|
345
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
346
|
+
evaluation_reason = f"User {user_name} is not a member of any groups"
|
|
347
|
+
|
|
348
|
+
except ClientError as e:
|
|
349
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
350
|
+
if error_code in ['AccessDenied', 'NoSuchEntity']:
|
|
351
|
+
compliance_status = ComplianceStatus.ERROR
|
|
352
|
+
evaluation_reason = f"Cannot access user {user_name}: {error_code}"
|
|
353
|
+
else:
|
|
354
|
+
compliance_status = ComplianceStatus.ERROR
|
|
355
|
+
evaluation_reason = f"Error checking user {user_name}: {str(e)}"
|
|
356
|
+
except Exception as e:
|
|
357
|
+
compliance_status = ComplianceStatus.ERROR
|
|
358
|
+
evaluation_reason = f"Unexpected error checking user {user_name}: {str(e)}"
|
|
359
|
+
|
|
360
|
+
return ComplianceResult(
|
|
361
|
+
resource_id=user_arn,
|
|
362
|
+
resource_type="AWS::IAM::User",
|
|
363
|
+
compliance_status=compliance_status,
|
|
364
|
+
evaluation_reason=evaluation_reason,
|
|
365
|
+
config_rule_name=self.rule_name,
|
|
366
|
+
region=region
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
370
|
+
"""Get specific remediation steps for user group membership."""
|
|
371
|
+
return [
|
|
372
|
+
"Identify IAM users who are not members of any groups",
|
|
373
|
+
"For each user without group membership:",
|
|
374
|
+
" 1. Determine appropriate groups based on user's role/responsibilities",
|
|
375
|
+
" 2. Create groups if they don't exist",
|
|
376
|
+
" 3. Add the user to appropriate groups",
|
|
377
|
+
" 4. Remove any direct policy attachments from the user",
|
|
378
|
+
" 5. Verify the user has necessary permissions through group membership",
|
|
379
|
+
"Use AWS CLI: aws iam add-user-to-group --user-name <user> --group-name <group>",
|
|
380
|
+
"Use AWS CLI: aws iam detach-user-policy --user-name <user> --policy-arn <arn>",
|
|
381
|
+
"Follow principle of least privilege when assigning group memberships",
|
|
382
|
+
"Implement regular reviews of user group memberships"
|
|
383
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Control 11.2: Perform Automated Backups - Instance optimization."""
|
|
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 EBSOptimizedInstanceAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for ebs-optimized-instance AWS Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__(
|
|
19
|
+
rule_name="ebs-optimized-instance",
|
|
20
|
+
control_id="11.2",
|
|
21
|
+
resource_types=["AWS::EC2::Instance"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Get EC2 instances."""
|
|
26
|
+
if resource_type != "AWS::EC2::Instance":
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
ec2_client = aws_factory.get_client('ec2', region)
|
|
31
|
+
|
|
32
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
33
|
+
lambda: ec2_client.describe_instances()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
instances = []
|
|
37
|
+
for reservation in response.get('Reservations', []):
|
|
38
|
+
for instance in reservation.get('Instances', []):
|
|
39
|
+
instances.append({
|
|
40
|
+
'InstanceId': instance.get('InstanceId'),
|
|
41
|
+
'InstanceType': instance.get('InstanceType'),
|
|
42
|
+
'EbsOptimized': instance.get('EbsOptimized', False),
|
|
43
|
+
'State': instance.get('State', {})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return instances
|
|
47
|
+
|
|
48
|
+
except ClientError as e:
|
|
49
|
+
logger.error(f"Error retrieving EC2 instances in region {region}: {e}")
|
|
50
|
+
raise
|
|
51
|
+
|
|
52
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
53
|
+
"""Evaluate if EC2 instance is EBS optimized."""
|
|
54
|
+
instance_id = resource.get('InstanceId', 'unknown')
|
|
55
|
+
instance_type = resource.get('InstanceType', 'unknown')
|
|
56
|
+
ebs_optimized = resource.get('EbsOptimized', False)
|
|
57
|
+
state = resource.get('State', {})
|
|
58
|
+
state_name = state.get('Name', 'unknown')
|
|
59
|
+
|
|
60
|
+
# Only evaluate running instances
|
|
61
|
+
if state_name not in ['running', 'stopped']:
|
|
62
|
+
return ComplianceResult(
|
|
63
|
+
resource_id=instance_id,
|
|
64
|
+
resource_type="AWS::EC2::Instance",
|
|
65
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
66
|
+
evaluation_reason=f"Instance {instance_id} is in state '{state_name}', not applicable for EBS optimization check",
|
|
67
|
+
config_rule_name=self.rule_name,
|
|
68
|
+
region=region
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Some instance types are EBS optimized by default
|
|
72
|
+
ebs_optimized_by_default_types = [
|
|
73
|
+
'c4.', 'c5.', 'c5d.', 'c5n.', 'c6i.', 'c6id.', 'c6in.',
|
|
74
|
+
'm4.', 'm5.', 'm5d.', 'm5n.', 'm5dn.', 'm6i.', 'm6id.', 'm6in.',
|
|
75
|
+
'r4.', 'r5.', 'r5d.', 'r5n.', 'r5dn.', 'r6i.', 'r6id.', 'r6in.',
|
|
76
|
+
't3.', 't3a.', 't4g.',
|
|
77
|
+
'x1.', 'x1e.', 'x2iezn.', 'x2idn.', 'x2iedn.',
|
|
78
|
+
'z1d.'
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
is_optimized_by_default = any(instance_type.startswith(prefix) for prefix in ebs_optimized_by_default_types)
|
|
82
|
+
|
|
83
|
+
if ebs_optimized or is_optimized_by_default:
|
|
84
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
85
|
+
if is_optimized_by_default:
|
|
86
|
+
evaluation_reason = f"Instance {instance_id} ({instance_type}) is EBS optimized by default"
|
|
87
|
+
else:
|
|
88
|
+
evaluation_reason = f"Instance {instance_id} ({instance_type}) has EBS optimization enabled"
|
|
89
|
+
else:
|
|
90
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
91
|
+
evaluation_reason = f"Instance {instance_id} ({instance_type}) does not have EBS optimization enabled"
|
|
92
|
+
|
|
93
|
+
return ComplianceResult(
|
|
94
|
+
resource_id=instance_id,
|
|
95
|
+
resource_type="AWS::EC2::Instance",
|
|
96
|
+
compliance_status=compliance_status,
|
|
97
|
+
evaluation_reason=evaluation_reason,
|
|
98
|
+
config_rule_name=self.rule_name,
|
|
99
|
+
region=region
|
|
100
|
+
)
|