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,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 3.3 - Identity and Access Management Controls
|
|
3
|
+
Critical IAM governance and access control rules to ensure proper access management.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
import boto3
|
|
9
|
+
import json
|
|
10
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
11
|
+
|
|
12
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
13
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
14
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IAMGroupHasUsersCheckAssessment(BaseConfigRuleAssessment):
|
|
20
|
+
"""
|
|
21
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
22
|
+
AWS Config Rule: iam-group-has-users-check
|
|
23
|
+
|
|
24
|
+
Ensures IAM groups have at least one user for proper access management.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__(
|
|
29
|
+
rule_name="iam-group-has-users-check",
|
|
30
|
+
control_id="3.3",
|
|
31
|
+
resource_types=["AWS::IAM::Group"]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
35
|
+
"""Get all IAM groups."""
|
|
36
|
+
if resource_type != "AWS::IAM::Group":
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
41
|
+
|
|
42
|
+
# Get all IAM groups
|
|
43
|
+
paginator = iam_client.get_paginator('list_groups')
|
|
44
|
+
groups = []
|
|
45
|
+
|
|
46
|
+
for page in paginator.paginate():
|
|
47
|
+
for group in page['Groups']:
|
|
48
|
+
group_name = group['GroupName']
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Get users in the group
|
|
52
|
+
users_response = iam_client.get_group(GroupName=group_name)
|
|
53
|
+
users = users_response.get('Users', [])
|
|
54
|
+
|
|
55
|
+
groups.append({
|
|
56
|
+
'GroupName': group_name,
|
|
57
|
+
'GroupId': group['GroupId'],
|
|
58
|
+
'Arn': group['Arn'],
|
|
59
|
+
'Path': group['Path'],
|
|
60
|
+
'CreateDate': group['CreateDate'],
|
|
61
|
+
'UserCount': len(users),
|
|
62
|
+
'Users': [user['UserName'] for user in users]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
except ClientError as e:
|
|
66
|
+
logger.warning(f"Error getting users for IAM group {group_name}: {e}")
|
|
67
|
+
# Add group with unknown user count
|
|
68
|
+
groups.append({
|
|
69
|
+
'GroupName': group_name,
|
|
70
|
+
'GroupId': group['GroupId'],
|
|
71
|
+
'Arn': group['Arn'],
|
|
72
|
+
'Path': group['Path'],
|
|
73
|
+
'CreateDate': group['CreateDate'],
|
|
74
|
+
'UserCount': -1, # Unknown
|
|
75
|
+
'Users': []
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
logger.debug(f"Found {len(groups)} IAM groups")
|
|
79
|
+
return groups
|
|
80
|
+
|
|
81
|
+
except ClientError as e:
|
|
82
|
+
logger.error(f"Error retrieving IAM groups: {e}")
|
|
83
|
+
raise
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Unexpected error retrieving IAM groups: {e}")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
89
|
+
"""Evaluate if IAM group has at least one user."""
|
|
90
|
+
group_name = resource.get('GroupName', 'unknown')
|
|
91
|
+
user_count = resource.get('UserCount', 0)
|
|
92
|
+
|
|
93
|
+
if user_count == -1:
|
|
94
|
+
return ComplianceResult(
|
|
95
|
+
resource_id=group_name,
|
|
96
|
+
resource_type="AWS::IAM::Group",
|
|
97
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
98
|
+
evaluation_reason="Unable to determine user count for IAM group",
|
|
99
|
+
config_rule_name=self.rule_name,
|
|
100
|
+
region=region
|
|
101
|
+
)
|
|
102
|
+
elif user_count > 0:
|
|
103
|
+
return ComplianceResult(
|
|
104
|
+
resource_id=group_name,
|
|
105
|
+
resource_type="AWS::IAM::Group",
|
|
106
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
107
|
+
evaluation_reason=f"IAM group has {user_count} user(s)",
|
|
108
|
+
config_rule_name=self.rule_name,
|
|
109
|
+
region=region
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
return ComplianceResult(
|
|
113
|
+
resource_id=group_name,
|
|
114
|
+
resource_type="AWS::IAM::Group",
|
|
115
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
116
|
+
evaluation_reason="IAM group has no users",
|
|
117
|
+
config_rule_name=self.rule_name,
|
|
118
|
+
region=region
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class IAMPolicyNoStatementsWithFullAccessAssessment(BaseConfigRuleAssessment):
|
|
123
|
+
"""
|
|
124
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
125
|
+
AWS Config Rule: iam-policy-no-statements-with-full-access
|
|
126
|
+
|
|
127
|
+
Prevents IAM policies with overly broad permissions to prevent privilege escalation.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self):
|
|
131
|
+
super().__init__(
|
|
132
|
+
rule_name="iam-policy-no-statements-with-full-access",
|
|
133
|
+
control_id="3.3",
|
|
134
|
+
resource_types=["AWS::IAM::Policy"]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
138
|
+
"""Get all customer-managed IAM policies."""
|
|
139
|
+
if resource_type != "AWS::IAM::Policy":
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
144
|
+
|
|
145
|
+
# Get all customer-managed policies (not AWS managed)
|
|
146
|
+
paginator = iam_client.get_paginator('list_policies')
|
|
147
|
+
policies = []
|
|
148
|
+
|
|
149
|
+
for page in paginator.paginate(Scope='Local'): # Only customer-managed policies
|
|
150
|
+
for policy in page['Policies']:
|
|
151
|
+
policy_arn = policy['Arn']
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Get the policy document
|
|
155
|
+
policy_response = iam_client.get_policy(PolicyArn=policy_arn)
|
|
156
|
+
policy_version_response = iam_client.get_policy_version(
|
|
157
|
+
PolicyArn=policy_arn,
|
|
158
|
+
VersionId=policy_response['Policy']['DefaultVersionId']
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
policy_document = policy_version_response['PolicyVersion']['Document']
|
|
162
|
+
|
|
163
|
+
# Analyze policy for full access statements
|
|
164
|
+
has_full_access = False
|
|
165
|
+
full_access_statements = []
|
|
166
|
+
|
|
167
|
+
statements = policy_document.get('Statement', [])
|
|
168
|
+
if not isinstance(statements, list):
|
|
169
|
+
statements = [statements]
|
|
170
|
+
|
|
171
|
+
for statement in statements:
|
|
172
|
+
if isinstance(statement, dict):
|
|
173
|
+
effect = statement.get('Effect', '')
|
|
174
|
+
action = statement.get('Action', [])
|
|
175
|
+
resource = statement.get('Resource', [])
|
|
176
|
+
|
|
177
|
+
if effect == 'Allow':
|
|
178
|
+
# Check for wildcard actions and resources
|
|
179
|
+
if isinstance(action, str):
|
|
180
|
+
action = [action]
|
|
181
|
+
if isinstance(resource, str):
|
|
182
|
+
resource = [resource]
|
|
183
|
+
|
|
184
|
+
# Check for full access patterns
|
|
185
|
+
has_wildcard_action = '*' in action
|
|
186
|
+
has_wildcard_resource = '*' in resource
|
|
187
|
+
|
|
188
|
+
if has_wildcard_action and has_wildcard_resource:
|
|
189
|
+
has_full_access = True
|
|
190
|
+
full_access_statements.append(statement)
|
|
191
|
+
|
|
192
|
+
policies.append({
|
|
193
|
+
'PolicyName': policy['PolicyName'],
|
|
194
|
+
'PolicyArn': policy_arn,
|
|
195
|
+
'Path': policy['Path'],
|
|
196
|
+
'CreateDate': policy['CreateDate'],
|
|
197
|
+
'UpdateDate': policy['UpdateDate'],
|
|
198
|
+
'AttachmentCount': policy['AttachmentCount'],
|
|
199
|
+
'HasFullAccess': has_full_access,
|
|
200
|
+
'FullAccessStatements': full_access_statements,
|
|
201
|
+
'PolicyDocument': policy_document
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
except ClientError as e:
|
|
205
|
+
logger.warning(f"Error getting policy document for {policy_arn}: {e}")
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
logger.debug(f"Found {len(policies)} customer-managed IAM policies")
|
|
209
|
+
return policies
|
|
210
|
+
|
|
211
|
+
except ClientError as e:
|
|
212
|
+
logger.error(f"Error retrieving IAM policies: {e}")
|
|
213
|
+
raise
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.error(f"Unexpected error retrieving IAM policies: {e}")
|
|
216
|
+
raise
|
|
217
|
+
|
|
218
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
219
|
+
"""Evaluate if IAM policy has statements with full access."""
|
|
220
|
+
policy_name = resource.get('PolicyName', 'unknown')
|
|
221
|
+
policy_arn = resource.get('PolicyArn', 'unknown')
|
|
222
|
+
has_full_access = resource.get('HasFullAccess', False)
|
|
223
|
+
|
|
224
|
+
if has_full_access:
|
|
225
|
+
return ComplianceResult(
|
|
226
|
+
resource_id=policy_arn,
|
|
227
|
+
resource_type="AWS::IAM::Policy",
|
|
228
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
229
|
+
evaluation_reason="IAM policy contains statements with full access (Action: *, Resource: *)",
|
|
230
|
+
config_rule_name=self.rule_name,
|
|
231
|
+
region=region
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
return ComplianceResult(
|
|
235
|
+
resource_id=policy_arn,
|
|
236
|
+
resource_type="AWS::IAM::Policy",
|
|
237
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
238
|
+
evaluation_reason="IAM policy does not contain statements with full access",
|
|
239
|
+
config_rule_name=self.rule_name,
|
|
240
|
+
region=region
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class IAMUserNoPoliciesCheckAssessment(BaseConfigRuleAssessment):
|
|
245
|
+
"""
|
|
246
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
247
|
+
AWS Config Rule: iam-user-no-policies-check
|
|
248
|
+
|
|
249
|
+
Ensures IAM policies are attached to groups/roles, not users directly for proper access management.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self):
|
|
253
|
+
super().__init__(
|
|
254
|
+
rule_name="iam-user-no-policies-check",
|
|
255
|
+
control_id="3.3",
|
|
256
|
+
resource_types=["AWS::IAM::User"]
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
260
|
+
"""Get all IAM users."""
|
|
261
|
+
if resource_type != "AWS::IAM::User":
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
iam_client = aws_factory.get_client('iam', region)
|
|
266
|
+
|
|
267
|
+
# Get all IAM users
|
|
268
|
+
paginator = iam_client.get_paginator('list_users')
|
|
269
|
+
users = []
|
|
270
|
+
|
|
271
|
+
for page in paginator.paginate():
|
|
272
|
+
for user in page['Users']:
|
|
273
|
+
user_name = user['UserName']
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Get attached managed policies
|
|
277
|
+
attached_policies_response = iam_client.list_attached_user_policies(UserName=user_name)
|
|
278
|
+
attached_policies = attached_policies_response.get('AttachedPolicies', [])
|
|
279
|
+
|
|
280
|
+
# Get inline policies
|
|
281
|
+
inline_policies_response = iam_client.list_user_policies(UserName=user_name)
|
|
282
|
+
inline_policies = inline_policies_response.get('PolicyNames', [])
|
|
283
|
+
|
|
284
|
+
users.append({
|
|
285
|
+
'UserName': user_name,
|
|
286
|
+
'UserId': user['UserId'],
|
|
287
|
+
'Arn': user['Arn'],
|
|
288
|
+
'Path': user['Path'],
|
|
289
|
+
'CreateDate': user['CreateDate'],
|
|
290
|
+
'AttachedPolicies': attached_policies,
|
|
291
|
+
'InlinePolicies': inline_policies,
|
|
292
|
+
'HasDirectPolicies': len(attached_policies) > 0 or len(inline_policies) > 0
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
except ClientError as e:
|
|
296
|
+
logger.warning(f"Error getting policies for IAM user {user_name}: {e}")
|
|
297
|
+
# Add user with unknown policy status
|
|
298
|
+
users.append({
|
|
299
|
+
'UserName': user_name,
|
|
300
|
+
'UserId': user['UserId'],
|
|
301
|
+
'Arn': user['Arn'],
|
|
302
|
+
'Path': user['Path'],
|
|
303
|
+
'CreateDate': user['CreateDate'],
|
|
304
|
+
'AttachedPolicies': [],
|
|
305
|
+
'InlinePolicies': [],
|
|
306
|
+
'HasDirectPolicies': None # Unknown
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
logger.debug(f"Found {len(users)} IAM users")
|
|
310
|
+
return users
|
|
311
|
+
|
|
312
|
+
except ClientError as e:
|
|
313
|
+
logger.error(f"Error retrieving IAM users: {e}")
|
|
314
|
+
raise
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.error(f"Unexpected error retrieving IAM users: {e}")
|
|
317
|
+
raise
|
|
318
|
+
|
|
319
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
320
|
+
"""Evaluate if IAM user has policies attached directly."""
|
|
321
|
+
user_name = resource.get('UserName', 'unknown')
|
|
322
|
+
has_direct_policies = resource.get('HasDirectPolicies', None)
|
|
323
|
+
attached_policies = resource.get('AttachedPolicies', [])
|
|
324
|
+
inline_policies = resource.get('InlinePolicies', [])
|
|
325
|
+
|
|
326
|
+
if has_direct_policies is None:
|
|
327
|
+
return ComplianceResult(
|
|
328
|
+
resource_id=user_name,
|
|
329
|
+
resource_type="AWS::IAM::User",
|
|
330
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
331
|
+
evaluation_reason="Unable to determine policy attachments for IAM user",
|
|
332
|
+
config_rule_name=self.rule_name,
|
|
333
|
+
region=region
|
|
334
|
+
)
|
|
335
|
+
elif has_direct_policies:
|
|
336
|
+
policy_details = []
|
|
337
|
+
if attached_policies:
|
|
338
|
+
policy_details.append(f"{len(attached_policies)} managed policies")
|
|
339
|
+
if inline_policies:
|
|
340
|
+
policy_details.append(f"{len(inline_policies)} inline policies")
|
|
341
|
+
|
|
342
|
+
return ComplianceResult(
|
|
343
|
+
resource_id=user_name,
|
|
344
|
+
resource_type="AWS::IAM::User",
|
|
345
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
346
|
+
evaluation_reason=f"IAM user has policies attached directly: {', '.join(policy_details)}",
|
|
347
|
+
config_rule_name=self.rule_name,
|
|
348
|
+
region=region
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
return ComplianceResult(
|
|
352
|
+
resource_id=user_name,
|
|
353
|
+
resource_type="AWS::IAM::User",
|
|
354
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
355
|
+
evaluation_reason="IAM user has no policies attached directly",
|
|
356
|
+
config_rule_name=self.rule_name,
|
|
357
|
+
region=region
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class SSMDocumentNotPublicAssessment(BaseConfigRuleAssessment):
|
|
362
|
+
"""
|
|
363
|
+
CIS Control 3.3 - Configure Data Access Control Lists
|
|
364
|
+
AWS Config Rule: ssm-document-not-public
|
|
365
|
+
|
|
366
|
+
Ensures SSM documents are not publicly accessible to prevent exposure of automation scripts.
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
def __init__(self):
|
|
370
|
+
super().__init__(
|
|
371
|
+
rule_name="ssm-document-not-public",
|
|
372
|
+
control_id="3.3",
|
|
373
|
+
resource_types=["AWS::SSM::Document"]
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
377
|
+
"""Get all SSM documents owned by the account."""
|
|
378
|
+
if resource_type != "AWS::SSM::Document":
|
|
379
|
+
return []
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
ssm_client = aws_factory.get_client('ssm', region)
|
|
383
|
+
|
|
384
|
+
# Get all SSM documents owned by the account
|
|
385
|
+
paginator = ssm_client.get_paginator('list_documents')
|
|
386
|
+
documents = []
|
|
387
|
+
|
|
388
|
+
for page in paginator.paginate(
|
|
389
|
+
Filters=[
|
|
390
|
+
{
|
|
391
|
+
'Key': 'Owner',
|
|
392
|
+
'Values': ['Self']
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
):
|
|
396
|
+
for document in page['DocumentIdentifiers']:
|
|
397
|
+
document_name = document['Name']
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
# Get document permissions
|
|
401
|
+
permissions_response = ssm_client.describe_document_permission(
|
|
402
|
+
Name=document_name,
|
|
403
|
+
PermissionType='Share'
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
account_ids = permissions_response.get('AccountIds', [])
|
|
407
|
+
is_public = 'all' in account_ids
|
|
408
|
+
|
|
409
|
+
documents.append({
|
|
410
|
+
'DocumentName': document_name,
|
|
411
|
+
'DocumentType': document.get('DocumentType', ''),
|
|
412
|
+
'DocumentFormat': document.get('DocumentFormat', ''),
|
|
413
|
+
'DocumentVersion': document.get('DocumentVersion', ''),
|
|
414
|
+
'Owner': document.get('Owner', ''),
|
|
415
|
+
'CreatedDate': document.get('CreatedDate'),
|
|
416
|
+
'Status': document.get('Status', ''),
|
|
417
|
+
'IsPublic': is_public,
|
|
418
|
+
'SharedAccountIds': account_ids
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
except ClientError as e:
|
|
422
|
+
if e.response.get('Error', {}).get('Code') == 'InvalidDocument':
|
|
423
|
+
# Document might not exist anymore
|
|
424
|
+
continue
|
|
425
|
+
else:
|
|
426
|
+
logger.warning(f"Error getting permissions for SSM document {document_name}: {e}")
|
|
427
|
+
# Add document with unknown public status
|
|
428
|
+
documents.append({
|
|
429
|
+
'DocumentName': document_name,
|
|
430
|
+
'DocumentType': document.get('DocumentType', ''),
|
|
431
|
+
'DocumentFormat': document.get('DocumentFormat', ''),
|
|
432
|
+
'DocumentVersion': document.get('DocumentVersion', ''),
|
|
433
|
+
'Owner': document.get('Owner', ''),
|
|
434
|
+
'CreatedDate': document.get('CreatedDate'),
|
|
435
|
+
'Status': document.get('Status', ''),
|
|
436
|
+
'IsPublic': None, # Unknown
|
|
437
|
+
'SharedAccountIds': []
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
logger.debug(f"Found {len(documents)} SSM documents")
|
|
441
|
+
return documents
|
|
442
|
+
|
|
443
|
+
except ClientError as e:
|
|
444
|
+
logger.error(f"Error retrieving SSM documents in {region}: {e}")
|
|
445
|
+
raise
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.error(f"Unexpected error retrieving SSM documents in {region}: {e}")
|
|
448
|
+
raise
|
|
449
|
+
|
|
450
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
451
|
+
"""Evaluate if SSM document is publicly accessible."""
|
|
452
|
+
document_name = resource.get('DocumentName', 'unknown')
|
|
453
|
+
is_public = resource.get('IsPublic', None)
|
|
454
|
+
status = resource.get('Status', '')
|
|
455
|
+
|
|
456
|
+
# Skip documents that are not active
|
|
457
|
+
if status != 'Active':
|
|
458
|
+
return ComplianceResult(
|
|
459
|
+
resource_id=document_name,
|
|
460
|
+
resource_type="AWS::SSM::Document",
|
|
461
|
+
compliance_status=ComplianceStatus.NOT_APPLICABLE,
|
|
462
|
+
evaluation_reason=f"SSM document is in status '{status}'",
|
|
463
|
+
config_rule_name=self.rule_name,
|
|
464
|
+
region=region
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if is_public is None:
|
|
468
|
+
return ComplianceResult(
|
|
469
|
+
resource_id=document_name,
|
|
470
|
+
resource_type="AWS::SSM::Document",
|
|
471
|
+
compliance_status=ComplianceStatus.ERROR,
|
|
472
|
+
evaluation_reason="Unable to determine public access status for SSM document",
|
|
473
|
+
config_rule_name=self.rule_name,
|
|
474
|
+
region=region
|
|
475
|
+
)
|
|
476
|
+
elif is_public:
|
|
477
|
+
return ComplianceResult(
|
|
478
|
+
resource_id=document_name,
|
|
479
|
+
resource_type="AWS::SSM::Document",
|
|
480
|
+
compliance_status=ComplianceStatus.NON_COMPLIANT,
|
|
481
|
+
evaluation_reason="SSM document is publicly accessible",
|
|
482
|
+
config_rule_name=self.rule_name,
|
|
483
|
+
region=region
|
|
484
|
+
)
|
|
485
|
+
else:
|
|
486
|
+
return ComplianceResult(
|
|
487
|
+
resource_id=document_name,
|
|
488
|
+
resource_type="AWS::SSM::Document",
|
|
489
|
+
compliance_status=ComplianceStatus.COMPLIANT,
|
|
490
|
+
evaluation_reason="SSM document is not publicly accessible",
|
|
491
|
+
config_rule_name=self.rule_name,
|
|
492
|
+
region=region
|
|
493
|
+
)
|