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,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
+ )