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,573 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 3.3 - IAM Advanced Security Controls
|
|
3
|
+
Advanced IAM security rules for comprehensive identity and access management.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
import boto3
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone, timedelta
|
|
11
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
12
|
+
|
|
13
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
14
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
15
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IAMRootAccessKeyCheckAssessment(BaseConfigRuleAssessment):
|
|
21
|
+
"""
|
|
22
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
23
|
+
AWS Config Rule: iam-root-access-key-check
|
|
24
|
+
|
|
25
|
+
Ensures the root user does not have access keys attached to prevent unauthorized access.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__(
|
|
30
|
+
rule_name="iam-root-access-key-check",
|
|
31
|
+
control_id="3.3",
|
|
32
|
+
resource_types=["AWS::::Account"]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
36
|
+
"""Get account information for root access key check."""
|
|
37
|
+
if resource_type != "AWS::::Account":
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Get account ID
|
|
42
|
+
sts_client = aws_factory.get_client('sts', region)
|
|
43
|
+
identity = sts_client.get_caller_identity()
|
|
44
|
+
account_id = identity['Account']
|
|
45
|
+
|
|
46
|
+
return [{
|
|
47
|
+
'AccountId': account_id,
|
|
48
|
+
'ResourceType': 'Account'
|
|
49
|
+
}]
|
|
50
|
+
|
|
51
|
+
except ClientError as e:
|
|
52
|
+
logger.error(f"Error retrieving account information: {e}")
|
|
53
|
+
raise
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Unexpected error retrieving account information: {e}")
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
59
|
+
"""Evaluate if root user has access keys."""
|
|
60
|
+
account_id = resource.get('AccountId', 'unknown')
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
64
|
+
|
|
65
|
+
# Get account summary to check for root access keys
|
|
66
|
+
summary = iam_client.get_account_summary()
|
|
67
|
+
|
|
68
|
+
# Check for root access keys
|
|
69
|
+
root_access_keys = summary.get('SummaryMap', {}).get('AccountAccessKeysPresent', 0)
|
|
70
|
+
|
|
71
|
+
if root_access_keys > 0:
|
|
72
|
+
return ComplianceResult(
|
|
73
|
+
resource_id=account_id,
|
|
74
|
+
resource_type="AWS::::Account",
|
|
75
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
76
|
+
evaluation_reason=f"Root user has {root_access_keys} access key(s) attached",
|
|
77
|
+
config_rule_name=self.rule_name,
|
|
78
|
+
region=region
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
return ComplianceResult(
|
|
82
|
+
resource_id=account_id,
|
|
83
|
+
resource_type="AWS::::Account",
|
|
84
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
85
|
+
evaluation_reason="Root user has no access keys attached",
|
|
86
|
+
config_rule_name=self.rule_name,
|
|
87
|
+
region=region
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
except ClientError as e:
|
|
91
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
92
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
93
|
+
return ComplianceResult(
|
|
94
|
+
resource_id=account_id,
|
|
95
|
+
resource_type="AWS::::Account",
|
|
96
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
97
|
+
evaluation_reason=f"Insufficient permissions to check root access keys: {error_code}",
|
|
98
|
+
config_rule_name=self.rule_name,
|
|
99
|
+
region=region
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
return ComplianceResult(
|
|
103
|
+
resource_id=account_id,
|
|
104
|
+
resource_type="AWS::::Account",
|
|
105
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
106
|
+
evaluation_reason=f"Error checking root access keys: {str(e)}",
|
|
107
|
+
config_rule_name=self.rule_name,
|
|
108
|
+
region=region
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return ComplianceResult(
|
|
113
|
+
resource_id=account_id,
|
|
114
|
+
resource_type="AWS::::Account",
|
|
115
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
116
|
+
evaluation_reason=f"Unexpected error: {str(e)}",
|
|
117
|
+
config_rule_name=self.rule_name,
|
|
118
|
+
region=region
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class IAMUserUnusedCredentialsCheckAssessment(BaseConfigRuleAssessment):
|
|
123
|
+
"""
|
|
124
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
125
|
+
AWS Config Rule: iam-user-unused-credentials-check
|
|
126
|
+
|
|
127
|
+
Ensures IAM users don't have unused credentials that could pose security risks.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, max_credential_usage_age: int = 90):
|
|
131
|
+
super().__init__(
|
|
132
|
+
rule_name="iam-user-unused-credentials-check",
|
|
133
|
+
control_id="3.3",
|
|
134
|
+
resource_types=["AWS::IAM::User"],
|
|
135
|
+
parameters={"maxCredentialUsageAge": max_credential_usage_age}
|
|
136
|
+
)
|
|
137
|
+
self.max_credential_usage_age = max_credential_usage_age
|
|
138
|
+
|
|
139
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
140
|
+
"""Get all IAM users with their credential information."""
|
|
141
|
+
if resource_type != "AWS::IAM::User":
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
146
|
+
|
|
147
|
+
# Get all IAM users
|
|
148
|
+
paginator = iam_client.get_paginator('list_users')
|
|
149
|
+
users = []
|
|
150
|
+
|
|
151
|
+
for page in paginator.paginate():
|
|
152
|
+
for user in page['Users']:
|
|
153
|
+
user_name = user['UserName']
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Get access keys for the user
|
|
157
|
+
access_keys_response = iam_client.list_access_keys(UserName=user_name)
|
|
158
|
+
access_keys = access_keys_response.get('AccessKeyMetadata', [])
|
|
159
|
+
|
|
160
|
+
# Get login profile (console password)
|
|
161
|
+
has_login_profile = False
|
|
162
|
+
login_profile_last_used = None
|
|
163
|
+
try:
|
|
164
|
+
login_profile = iam_client.get_login_profile(UserName=user_name)
|
|
165
|
+
has_login_profile = True
|
|
166
|
+
# Get password last used
|
|
167
|
+
user_detail = iam_client.get_user(UserName=user_name)
|
|
168
|
+
login_profile_last_used = user_detail['User'].get('PasswordLastUsed')
|
|
169
|
+
except ClientError as e:
|
|
170
|
+
if e.response.get('Error', {}).get('Code') != 'NoSuchEntity':
|
|
171
|
+
logger.warning(f"Error getting login profile for user {user_name}: {e}")
|
|
172
|
+
|
|
173
|
+
users.append({
|
|
174
|
+
'UserName': user_name,
|
|
175
|
+
'UserId': user['UserId'],
|
|
176
|
+
'Arn': user['Arn'],
|
|
177
|
+
'CreateDate': user['CreateDate'],
|
|
178
|
+
'PasswordLastUsed': user.get('PasswordLastUsed'),
|
|
179
|
+
'HasLoginProfile': has_login_profile,
|
|
180
|
+
'LoginProfileLastUsed': login_profile_last_used,
|
|
181
|
+
'AccessKeys': access_keys
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
except ClientError as e:
|
|
185
|
+
logger.warning(f"Error getting credentials for IAM user {user_name}: {e}")
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
logger.debug(f"Found {len(users)} IAM users")
|
|
189
|
+
return users
|
|
190
|
+
|
|
191
|
+
except ClientError as e:
|
|
192
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
193
|
+
raise
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Unexpected error retrieving IAM users: {e}")
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
199
|
+
"""Evaluate if IAM user has unused credentials."""
|
|
200
|
+
user_name = resource.get('UserName', 'unknown')
|
|
201
|
+
password_last_used = resource.get('PasswordLastUsed')
|
|
202
|
+
has_login_profile = resource.get('HasLoginProfile', False)
|
|
203
|
+
access_keys = resource.get('AccessKeys', [])
|
|
204
|
+
|
|
205
|
+
now = datetime.now(timezone.utc)
|
|
206
|
+
cutoff_date = now - timedelta(days=self.max_credential_usage_age)
|
|
207
|
+
|
|
208
|
+
unused_credentials = []
|
|
209
|
+
|
|
210
|
+
# Check password usage
|
|
211
|
+
if has_login_profile:
|
|
212
|
+
if password_last_used is None:
|
|
213
|
+
unused_credentials.append("Console password (never used)")
|
|
214
|
+
elif password_last_used < cutoff_date:
|
|
215
|
+
days_unused = (now - password_last_used).days
|
|
216
|
+
unused_credentials.append(f"Console password (unused for {days_unused} days)")
|
|
217
|
+
|
|
218
|
+
# Check access keys usage
|
|
219
|
+
try:
|
|
220
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
221
|
+
|
|
222
|
+
for access_key in access_keys:
|
|
223
|
+
access_key_id = access_key['AccessKeyId']
|
|
224
|
+
|
|
225
|
+
# Get access key last used
|
|
226
|
+
try:
|
|
227
|
+
last_used_response = iam_client.get_access_key_last_used(AccessKeyId=access_key_id)
|
|
228
|
+
last_used_info = last_used_response.get('AccessKeyLastUsed', {})
|
|
229
|
+
last_used_date = last_used_info.get('LastUsedDate')
|
|
230
|
+
|
|
231
|
+
if last_used_date is None:
|
|
232
|
+
unused_credentials.append(f"Access key {access_key_id} (never used)")
|
|
233
|
+
elif last_used_date < cutoff_date:
|
|
234
|
+
days_unused = (now - last_used_date).days
|
|
235
|
+
unused_credentials.append(f"Access key {access_key_id} (unused for {days_unused} days)")
|
|
236
|
+
|
|
237
|
+
except ClientError as e:
|
|
238
|
+
logger.warning(f"Error getting last used info for access key {access_key_id}: {e}")
|
|
239
|
+
|
|
240
|
+
except ClientError as e:
|
|
241
|
+
logger.warning(f"Error checking access key usage for user {user_name}: {e}")
|
|
242
|
+
|
|
243
|
+
if unused_credentials:
|
|
244
|
+
return ComplianceResult(
|
|
245
|
+
resource_id=user_name,
|
|
246
|
+
resource_type="AWS::IAM::User",
|
|
247
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
248
|
+
evaluation_reason=f"IAM user has unused credentials: {'; '.join(unused_credentials)}",
|
|
249
|
+
config_rule_name=self.rule_name,
|
|
250
|
+
region=region
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
return ComplianceResult(
|
|
254
|
+
resource_id=user_name,
|
|
255
|
+
resource_type="AWS::IAM::User",
|
|
256
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
257
|
+
evaluation_reason=f"IAM user has no unused credentials (within {self.max_credential_usage_age} days)",
|
|
258
|
+
config_rule_name=self.rule_name,
|
|
259
|
+
region=region
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class IAMCustomerPolicyBlockedKMSActionsAssessment(BaseConfigRuleAssessment):
|
|
264
|
+
"""
|
|
265
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
266
|
+
AWS Config Rule: iam-customer-policy-blocked-kms-actions
|
|
267
|
+
|
|
268
|
+
Ensures customer-managed IAM policies don't contain blocked KMS actions.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, blocked_actions_patterns: List[str] = None):
|
|
272
|
+
super().__init__(
|
|
273
|
+
rule_name="iam-customer-policy-blocked-kms-actions",
|
|
274
|
+
control_id="3.3",
|
|
275
|
+
resource_types=["AWS::IAM::Policy"],
|
|
276
|
+
parameters={"blockedActionsPatterns": blocked_actions_patterns or ["kms:Decrypt", "kms:ReEncryptFrom"]}
|
|
277
|
+
)
|
|
278
|
+
self.blocked_actions_patterns = blocked_actions_patterns or ["kms:Decrypt", "kms:ReEncryptFrom"]
|
|
279
|
+
|
|
280
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
281
|
+
"""Get all customer-managed IAM policies."""
|
|
282
|
+
if resource_type != "AWS::IAM::Policy":
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
287
|
+
|
|
288
|
+
# Get all customer-managed policies (not AWS managed)
|
|
289
|
+
paginator = iam_client.get_paginator('list_policies')
|
|
290
|
+
policies = []
|
|
291
|
+
|
|
292
|
+
for page in paginator.paginate(Scope='Local'): # Only customer-managed policies
|
|
293
|
+
for policy in page['Policies']:
|
|
294
|
+
policy_arn = policy['Arn']
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
# Get the policy document
|
|
298
|
+
policy_response = iam_client.get_policy(PolicyArn=policy_arn)
|
|
299
|
+
policy_version_response = iam_client.get_policy_version(
|
|
300
|
+
PolicyArn=policy_arn,
|
|
301
|
+
VersionId=policy_response['Policy']['DefaultVersionId']
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
policy_document = policy_version_response['PolicyVersion']['Document']
|
|
305
|
+
|
|
306
|
+
# Analyze policy for blocked KMS actions
|
|
307
|
+
has_blocked_actions = False
|
|
308
|
+
blocked_statements = []
|
|
309
|
+
|
|
310
|
+
statements = policy_document.get('Statement', [])
|
|
311
|
+
if not isinstance(statements, list):
|
|
312
|
+
statements = [statements]
|
|
313
|
+
|
|
314
|
+
for statement in statements:
|
|
315
|
+
if isinstance(statement, dict):
|
|
316
|
+
effect = statement.get('Effect', '')
|
|
317
|
+
actions = statement.get('Action', [])
|
|
318
|
+
resources = statement.get('Resource', [])
|
|
319
|
+
|
|
320
|
+
if effect == 'Allow':
|
|
321
|
+
if isinstance(actions, str):
|
|
322
|
+
actions = [actions]
|
|
323
|
+
if isinstance(resources, str):
|
|
324
|
+
resources = [resources]
|
|
325
|
+
|
|
326
|
+
# Check for blocked KMS actions on all KMS keys
|
|
327
|
+
for action in actions:
|
|
328
|
+
for blocked_pattern in self.blocked_actions_patterns:
|
|
329
|
+
if (action == blocked_pattern or action == 'kms:*' or action == '*') and \
|
|
330
|
+
('*' in resources or any('arn:aws:kms:*' in res for res in resources)):
|
|
331
|
+
has_blocked_actions = True
|
|
332
|
+
blocked_statements.append({
|
|
333
|
+
'Action': action,
|
|
334
|
+
'Resource': resources,
|
|
335
|
+
'BlockedPattern': blocked_pattern
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
policies.append({
|
|
339
|
+
'PolicyName': policy['PolicyName'],
|
|
340
|
+
'PolicyArn': policy_arn,
|
|
341
|
+
'Path': policy['Path'],
|
|
342
|
+
'CreateDate': policy['CreateDate'],
|
|
343
|
+
'UpdateDate': policy['UpdateDate'],
|
|
344
|
+
'AttachmentCount': policy['AttachmentCount'],
|
|
345
|
+
'HasBlockedActions': has_blocked_actions,
|
|
346
|
+
'BlockedStatements': blocked_statements,
|
|
347
|
+
'PolicyDocument': policy_document
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
except ClientError as e:
|
|
351
|
+
logger.warning(f"Error getting policy document for {policy_arn}: {e}")
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
logger.debug(f"Found {len(policies)} customer-managed IAM policies")
|
|
355
|
+
return policies
|
|
356
|
+
|
|
357
|
+
except ClientError as e:
|
|
358
|
+
logger.error(f"Error retrieving IAM policies: {e}")
|
|
359
|
+
raise
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Unexpected error retrieving IAM policies: {e}")
|
|
362
|
+
raise
|
|
363
|
+
|
|
364
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
365
|
+
"""Evaluate if IAM policy contains blocked KMS actions."""
|
|
366
|
+
policy_name = resource.get('PolicyName', 'unknown')
|
|
367
|
+
policy_arn = resource.get('PolicyArn', 'unknown')
|
|
368
|
+
has_blocked_actions = resource.get('HasBlockedActions', False)
|
|
369
|
+
blocked_statements = resource.get('BlockedStatements', [])
|
|
370
|
+
|
|
371
|
+
if has_blocked_actions:
|
|
372
|
+
blocked_details = []
|
|
373
|
+
for stmt in blocked_statements:
|
|
374
|
+
blocked_details.append(f"Action: {stmt['Action']}, Pattern: {stmt['BlockedPattern']}")
|
|
375
|
+
|
|
376
|
+
return ComplianceResult(
|
|
377
|
+
resource_id=policy_arn,
|
|
378
|
+
resource_type="AWS::IAM::Policy",
|
|
379
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
380
|
+
evaluation_reason=f"IAM policy contains blocked KMS actions: {'; '.join(blocked_details)}",
|
|
381
|
+
config_rule_name=self.rule_name,
|
|
382
|
+
region=region
|
|
383
|
+
)
|
|
384
|
+
else:
|
|
385
|
+
return ComplianceResult(
|
|
386
|
+
resource_id=policy_arn,
|
|
387
|
+
resource_type="AWS::IAM::Policy",
|
|
388
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
389
|
+
evaluation_reason="IAM policy does not contain blocked KMS actions",
|
|
390
|
+
config_rule_name=self.rule_name,
|
|
391
|
+
region=region
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class IAMInlinePolicyBlockedKMSActionsAssessment(BaseConfigRuleAssessment):
|
|
396
|
+
"""
|
|
397
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
398
|
+
AWS Config Rule: iam-inline-policy-blocked-kms-actions
|
|
399
|
+
|
|
400
|
+
Ensures inline IAM policies don't contain blocked KMS actions.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
def __init__(self, blocked_actions_patterns: List[str] = None):
|
|
404
|
+
super().__init__(
|
|
405
|
+
rule_name="iam-inline-policy-blocked-kms-actions",
|
|
406
|
+
control_id="3.3",
|
|
407
|
+
resource_types=["AWS::IAM::User", "AWS::IAM::Role", "AWS::IAM::Group"],
|
|
408
|
+
parameters={"blockedActionsPatterns": blocked_actions_patterns or ["kms:Decrypt", "kms:ReEncryptFrom"]}
|
|
409
|
+
)
|
|
410
|
+
self.blocked_actions_patterns = blocked_actions_patterns or ["kms:Decrypt", "kms:ReEncryptFrom"]
|
|
411
|
+
|
|
412
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
413
|
+
"""Get all IAM entities with inline policies."""
|
|
414
|
+
resources = []
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
418
|
+
|
|
419
|
+
if resource_type == "AWS::IAM::User":
|
|
420
|
+
# Get all users with inline policies
|
|
421
|
+
paginator = iam_client.get_paginator('list_users')
|
|
422
|
+
for page in paginator.paginate():
|
|
423
|
+
for user in page['Users']:
|
|
424
|
+
user_name = user['UserName']
|
|
425
|
+
try:
|
|
426
|
+
inline_policies = iam_client.list_user_policies(UserName=user_name)
|
|
427
|
+
if inline_policies.get('PolicyNames'):
|
|
428
|
+
resources.append({
|
|
429
|
+
'EntityType': 'User',
|
|
430
|
+
'EntityName': user_name,
|
|
431
|
+
'EntityArn': user['Arn'],
|
|
432
|
+
'InlinePolicyNames': inline_policies['PolicyNames']
|
|
433
|
+
})
|
|
434
|
+
except ClientError as e:
|
|
435
|
+
logger.warning(f"Error getting inline policies for user {user_name}: {e}")
|
|
436
|
+
|
|
437
|
+
elif resource_type == "AWS::IAM::Role":
|
|
438
|
+
# Get all roles with inline policies
|
|
439
|
+
paginator = iam_client.get_paginator('list_roles')
|
|
440
|
+
for page in paginator.paginate():
|
|
441
|
+
for role in page['Roles']:
|
|
442
|
+
role_name = role['RoleName']
|
|
443
|
+
try:
|
|
444
|
+
inline_policies = iam_client.list_role_policies(RoleName=role_name)
|
|
445
|
+
if inline_policies.get('PolicyNames'):
|
|
446
|
+
resources.append({
|
|
447
|
+
'EntityType': 'Role',
|
|
448
|
+
'EntityName': role_name,
|
|
449
|
+
'EntityArn': role['Arn'],
|
|
450
|
+
'InlinePolicyNames': inline_policies['PolicyNames']
|
|
451
|
+
})
|
|
452
|
+
except ClientError as e:
|
|
453
|
+
logger.warning(f"Error getting inline policies for role {role_name}: {e}")
|
|
454
|
+
|
|
455
|
+
elif resource_type == "AWS::IAM::Group":
|
|
456
|
+
# Get all groups with inline policies
|
|
457
|
+
paginator = iam_client.get_paginator('list_groups')
|
|
458
|
+
for page in paginator.paginate():
|
|
459
|
+
for group in page['Groups']:
|
|
460
|
+
group_name = group['GroupName']
|
|
461
|
+
try:
|
|
462
|
+
inline_policies = iam_client.list_group_policies(GroupName=group_name)
|
|
463
|
+
if inline_policies.get('PolicyNames'):
|
|
464
|
+
resources.append({
|
|
465
|
+
'EntityType': 'Group',
|
|
466
|
+
'EntityName': group_name,
|
|
467
|
+
'EntityArn': group['Arn'],
|
|
468
|
+
'InlinePolicyNames': inline_policies['PolicyNames']
|
|
469
|
+
})
|
|
470
|
+
except ClientError as e:
|
|
471
|
+
logger.warning(f"Error getting inline policies for group {group_name}: {e}")
|
|
472
|
+
|
|
473
|
+
logger.debug(f"Found {len(resources)} IAM entities with inline policies of type {resource_type}")
|
|
474
|
+
return resources
|
|
475
|
+
|
|
476
|
+
except ClientError as e:
|
|
477
|
+
logger.error(f"Error retrieving IAM entities: {e}")
|
|
478
|
+
raise
|
|
479
|
+
except Exception as e:
|
|
480
|
+
logger.error(f"Unexpected error retrieving IAM entities: {e}")
|
|
481
|
+
raise
|
|
482
|
+
|
|
483
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
484
|
+
"""Evaluate if inline policies contain blocked KMS actions."""
|
|
485
|
+
entity_type = resource.get('EntityType', 'unknown')
|
|
486
|
+
entity_name = resource.get('EntityName', 'unknown')
|
|
487
|
+
entity_arn = resource.get('EntityArn', 'unknown')
|
|
488
|
+
inline_policy_names = resource.get('InlinePolicyNames', [])
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
492
|
+
|
|
493
|
+
blocked_policies = []
|
|
494
|
+
|
|
495
|
+
for policy_name in inline_policy_names:
|
|
496
|
+
try:
|
|
497
|
+
# Get the inline policy document
|
|
498
|
+
if entity_type == 'User':
|
|
499
|
+
policy_response = iam_client.get_user_policy(UserName=entity_name, PolicyName=policy_name)
|
|
500
|
+
elif entity_type == 'Role':
|
|
501
|
+
policy_response = iam_client.get_role_policy(RoleName=entity_name, PolicyName=policy_name)
|
|
502
|
+
elif entity_type == 'Group':
|
|
503
|
+
policy_response = iam_client.get_group_policy(GroupName=entity_name, PolicyName=policy_name)
|
|
504
|
+
else:
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
policy_document = policy_response['PolicyDocument']
|
|
508
|
+
|
|
509
|
+
# Check for blocked KMS actions
|
|
510
|
+
statements = policy_document.get('Statement', [])
|
|
511
|
+
if not isinstance(statements, list):
|
|
512
|
+
statements = [statements]
|
|
513
|
+
|
|
514
|
+
for statement in statements:
|
|
515
|
+
if isinstance(statement, dict):
|
|
516
|
+
effect = statement.get('Effect', '')
|
|
517
|
+
actions = statement.get('Action', [])
|
|
518
|
+
resources_list = statement.get('Resource', [])
|
|
519
|
+
|
|
520
|
+
if effect == 'Allow':
|
|
521
|
+
if isinstance(actions, str):
|
|
522
|
+
actions = [actions]
|
|
523
|
+
if isinstance(resources_list, str):
|
|
524
|
+
resources_list = [resources_list]
|
|
525
|
+
|
|
526
|
+
# Check for blocked KMS actions on all KMS keys
|
|
527
|
+
for action in actions:
|
|
528
|
+
for blocked_pattern in self.blocked_actions_patterns:
|
|
529
|
+
if (action == blocked_pattern or action == 'kms:*' or action == '*') and \
|
|
530
|
+
('*' in resources_list or any('arn:aws:kms:*' in res for res in resources_list)):
|
|
531
|
+
blocked_policies.append(f"{policy_name} (Action: {action})")
|
|
532
|
+
break
|
|
533
|
+
|
|
534
|
+
except ClientError as e:
|
|
535
|
+
logger.warning(f"Error getting inline policy {policy_name} for {entity_type} {entity_name}: {e}")
|
|
536
|
+
|
|
537
|
+
if blocked_policies:
|
|
538
|
+
return ComplianceResult(
|
|
539
|
+
resource_id=entity_arn,
|
|
540
|
+
resource_type=f"AWS::IAM::{entity_type}",
|
|
541
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
542
|
+
evaluation_reason=f"IAM {entity_type.lower()} has inline policies with blocked KMS actions: {'; '.join(blocked_policies)}",
|
|
543
|
+
config_rule_name=self.rule_name,
|
|
544
|
+
region=region
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
return ComplianceResult(
|
|
548
|
+
resource_id=entity_arn,
|
|
549
|
+
resource_type=f"AWS::IAM::{entity_type}",
|
|
550
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
551
|
+
evaluation_reason=f"IAM {entity_type.lower()} inline policies do not contain blocked KMS actions",
|
|
552
|
+
config_rule_name=self.rule_name,
|
|
553
|
+
region=region
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
except ClientError as e:
|
|
557
|
+
return ComplianceResult(
|
|
558
|
+
resource_id=entity_arn,
|
|
559
|
+
resource_type=f"AWS::IAM::{entity_type}",
|
|
560
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
561
|
+
evaluation_reason=f"Error checking inline policies: {str(e)}",
|
|
562
|
+
config_rule_name=self.rule_name,
|
|
563
|
+
region=region
|
|
564
|
+
)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
return ComplianceResult(
|
|
567
|
+
resource_id=entity_arn,
|
|
568
|
+
resource_type=f"AWS::IAM::{entity_type}",
|
|
569
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
570
|
+
evaluation_reason=f"Unexpected error: {str(e)}",
|
|
571
|
+
config_rule_name=self.rule_name,
|
|
572
|
+
region=region
|
|
573
|
+
)
|