iam-policy-validator 1.8.0__py3-none-any.whl → 1.10.0__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.
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/METADATA +106 -1
- iam_policy_validator-1.10.0.dist-info/RECORD +96 -0
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +504 -190
- iam_validator/checks/action_resource_matching.py +8 -15
- iam_validator/checks/action_validation.py +6 -12
- iam_validator/checks/condition_key_validation.py +6 -12
- iam_validator/checks/condition_type_mismatch.py +9 -16
- iam_validator/checks/full_wildcard.py +9 -13
- iam_validator/checks/mfa_condition_check.py +8 -17
- iam_validator/checks/policy_size.py +6 -39
- iam_validator/checks/policy_structure.py +10 -40
- iam_validator/checks/policy_type_validation.py +18 -19
- iam_validator/checks/principal_validation.py +11 -20
- iam_validator/checks/resource_validation.py +5 -12
- iam_validator/checks/sensitive_action.py +8 -15
- iam_validator/checks/service_wildcard.py +6 -12
- iam_validator/checks/set_operator_validation.py +11 -18
- iam_validator/checks/sid_uniqueness.py +8 -38
- iam_validator/checks/trust_policy_validation.py +8 -14
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +6 -12
- iam_validator/checks/wildcard_resource.py +6 -12
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +26 -4
- iam_validator/core/__init__.py +1 -1
- iam_validator/core/aws_fetcher.py +24 -1030
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +612 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +379 -0
- iam_validator/core/check_registry.py +82 -14
- iam_validator/core/config/defaults.py +10 -0
- iam_validator/core/constants.py +17 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/policy_checks.py +7 -3
- iam_validator/core/pr_commenter.py +34 -7
- iam_validator/sdk/__init__.py +1 -1
- iam_validator/sdk/context.py +1 -1
- iam_validator/sdk/helpers.py +1 -1
- iam_policy_validator-1.8.0.dist-info/RECORD +0 -87
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Validation logic for AWS actions, condition keys, and resources.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive validation for IAM policy elements
|
|
4
|
+
including actions, condition keys, and ARN formats.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from iam_validator.core.aws_service.parsers import ServiceParser
|
|
12
|
+
from iam_validator.core.models import ServiceDetail
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConditionKeyValidationResult:
|
|
19
|
+
"""Result of condition key validation.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
is_valid: True if the condition key is valid for the action
|
|
23
|
+
error_message: Short error message if invalid (shown prominently)
|
|
24
|
+
warning_message: Warning message if valid but not recommended
|
|
25
|
+
suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
is_valid: bool
|
|
29
|
+
error_message: str | None = None
|
|
30
|
+
warning_message: str | None = None
|
|
31
|
+
suggestion: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServiceValidator:
|
|
35
|
+
"""Validates AWS actions, condition keys, and resources.
|
|
36
|
+
|
|
37
|
+
This class provides validation logic for IAM policy elements,
|
|
38
|
+
working with AWS service definitions to ensure correctness.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, parser: ServiceParser | None = None) -> None:
|
|
42
|
+
"""Initialize validator with parser.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
parser: Optional ServiceParser instance (creates new one if not provided)
|
|
46
|
+
"""
|
|
47
|
+
self._parser = parser or ServiceParser()
|
|
48
|
+
|
|
49
|
+
async def validate_action(
|
|
50
|
+
self,
|
|
51
|
+
action: str,
|
|
52
|
+
service_detail: ServiceDetail,
|
|
53
|
+
allow_wildcards: bool = True,
|
|
54
|
+
) -> tuple[bool, str | None, bool]:
|
|
55
|
+
"""Validate IAM action against service definition.
|
|
56
|
+
|
|
57
|
+
Supports:
|
|
58
|
+
- Exact actions: s3:GetObject
|
|
59
|
+
- Full wildcards: s3:*
|
|
60
|
+
- Partial wildcards: s3:Get*, s3:*Object, s3:*Get*
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
action: Full action string (e.g., "s3:GetObject")
|
|
64
|
+
service_detail: Service definition to validate against
|
|
65
|
+
allow_wildcards: Whether to allow wildcard actions
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (is_valid, error_message, is_wildcard)
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> validator = ServiceValidator()
|
|
72
|
+
>>> service = await fetcher.fetch_service_by_name("s3")
|
|
73
|
+
>>> is_valid, error, is_wildcard = await validator.validate_action(
|
|
74
|
+
... "s3:GetObject", service
|
|
75
|
+
... )
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
service_prefix, action_name = self._parser.parse_action(action)
|
|
79
|
+
|
|
80
|
+
# Quick wildcard check
|
|
81
|
+
is_wildcard = self._parser.is_wildcard_action(action_name)
|
|
82
|
+
|
|
83
|
+
# Handle full wildcard
|
|
84
|
+
if action_name == "*":
|
|
85
|
+
if allow_wildcards:
|
|
86
|
+
return True, None, True
|
|
87
|
+
return False, "Wildcard actions are not allowed", True
|
|
88
|
+
|
|
89
|
+
# Get available actions from service
|
|
90
|
+
available_actions = list(service_detail.actions.keys())
|
|
91
|
+
|
|
92
|
+
# Handle partial wildcards (e.g., Get*, *Object, Describe*)
|
|
93
|
+
if is_wildcard:
|
|
94
|
+
if not allow_wildcards:
|
|
95
|
+
return False, "Wildcard actions are not allowed", True
|
|
96
|
+
|
|
97
|
+
has_matches, matched_actions = self._parser.match_wildcard_action(
|
|
98
|
+
action_name, available_actions
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if has_matches:
|
|
102
|
+
# Wildcard is valid and matches at least one action
|
|
103
|
+
return True, None, True
|
|
104
|
+
|
|
105
|
+
# Wildcard doesn't match any actions
|
|
106
|
+
return (
|
|
107
|
+
False,
|
|
108
|
+
f"Action pattern `{action_name}` does not match any actions in service `{service_prefix}`",
|
|
109
|
+
True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Check if exact action exists (case-insensitive)
|
|
113
|
+
action_exists = any(a.lower() == action_name.lower() for a in available_actions)
|
|
114
|
+
|
|
115
|
+
if action_exists:
|
|
116
|
+
return True, None, False
|
|
117
|
+
|
|
118
|
+
# Suggest similar actions
|
|
119
|
+
similar = [f"`{a}`" for a in available_actions if action_name.lower() in a.lower()][:3]
|
|
120
|
+
|
|
121
|
+
suggestion = f" Did you mean: {', '.join(similar)}?" if similar else ""
|
|
122
|
+
return (
|
|
123
|
+
False,
|
|
124
|
+
f"Action `{action_name}` not found in service `{service_prefix}`.{suggestion}",
|
|
125
|
+
False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
return False, str(e), False
|
|
130
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
131
|
+
logger.error(f"Error validating action {action}: {e}")
|
|
132
|
+
return False, f"Failed to validate action: {e!s}", False
|
|
133
|
+
|
|
134
|
+
async def validate_condition_key(
|
|
135
|
+
self,
|
|
136
|
+
action: str,
|
|
137
|
+
condition_key: str,
|
|
138
|
+
service_detail: ServiceDetail,
|
|
139
|
+
resources: list[str] | None = None,
|
|
140
|
+
) -> ConditionKeyValidationResult:
|
|
141
|
+
"""Validate condition key against action and optionally resource types.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
action: IAM action (e.g., "s3:GetObject")
|
|
145
|
+
condition_key: Condition key to validate (e.g., "s3:prefix")
|
|
146
|
+
service_detail: Service definition containing actions and resources
|
|
147
|
+
resources: Optional list of resource ARNs to validate against
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
ConditionKeyValidationResult with validation details
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> validator = ServiceValidator()
|
|
154
|
+
>>> service = await fetcher.fetch_service_by_name("s3")
|
|
155
|
+
>>> result = await validator.validate_condition_key(
|
|
156
|
+
... "s3:GetObject", "s3:prefix", service
|
|
157
|
+
... )
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
|
|
161
|
+
get_global_conditions,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
service_prefix, action_name = self._parser.parse_action(action)
|
|
165
|
+
|
|
166
|
+
# Check if it's a global condition key
|
|
167
|
+
is_global_key = False
|
|
168
|
+
if condition_key.startswith("aws:"):
|
|
169
|
+
global_conditions = get_global_conditions()
|
|
170
|
+
if global_conditions.is_valid_global_key(condition_key):
|
|
171
|
+
is_global_key = True
|
|
172
|
+
else:
|
|
173
|
+
return ConditionKeyValidationResult(
|
|
174
|
+
is_valid=False,
|
|
175
|
+
error_message=f"Invalid AWS global condition key: `{condition_key}`.",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Check service-specific condition keys
|
|
179
|
+
if condition_key in service_detail.condition_keys:
|
|
180
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
181
|
+
|
|
182
|
+
# Check action-specific condition keys
|
|
183
|
+
if action_name in service_detail.actions:
|
|
184
|
+
action_detail = service_detail.actions[action_name]
|
|
185
|
+
if (
|
|
186
|
+
action_detail.action_condition_keys
|
|
187
|
+
and condition_key in action_detail.action_condition_keys
|
|
188
|
+
):
|
|
189
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
190
|
+
|
|
191
|
+
# Check resource-specific condition keys
|
|
192
|
+
# Get resource types required by this action
|
|
193
|
+
if resources and action_detail.resources:
|
|
194
|
+
for res_req in action_detail.resources:
|
|
195
|
+
resource_name = res_req.get("Name", "")
|
|
196
|
+
if not resource_name:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Look up resource type definition
|
|
200
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
201
|
+
if resource_type and resource_type.condition_keys:
|
|
202
|
+
if condition_key in resource_type.condition_keys:
|
|
203
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
204
|
+
|
|
205
|
+
# If it's a global key but the action has specific condition keys defined,
|
|
206
|
+
# AWS allows it but the key may not be available in every request context
|
|
207
|
+
if is_global_key and action_detail.action_condition_keys is not None:
|
|
208
|
+
warning_msg = (
|
|
209
|
+
f"Global condition key `{condition_key}` is used with action `{action}`. "
|
|
210
|
+
f"While global condition keys can be used across all AWS services, "
|
|
211
|
+
f"the key may not be available in every request context. "
|
|
212
|
+
f"Verify that `{condition_key}` is available for this specific action's request context. "
|
|
213
|
+
f"Consider using `*IfExists` operators (e.g., `StringEqualsIfExists`) if the key might be missing."
|
|
214
|
+
)
|
|
215
|
+
return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
|
|
216
|
+
|
|
217
|
+
# If it's a global key and action doesn't define specific keys, allow it
|
|
218
|
+
if is_global_key:
|
|
219
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
220
|
+
|
|
221
|
+
# Short error message
|
|
222
|
+
error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
|
|
223
|
+
|
|
224
|
+
# Collect valid condition keys for this action
|
|
225
|
+
valid_keys: set[str] = set()
|
|
226
|
+
|
|
227
|
+
# Add service-level condition keys
|
|
228
|
+
if service_detail.condition_keys:
|
|
229
|
+
if isinstance(service_detail.condition_keys, dict):
|
|
230
|
+
valid_keys.update(service_detail.condition_keys.keys())
|
|
231
|
+
elif isinstance(service_detail.condition_keys, list):
|
|
232
|
+
valid_keys.update(service_detail.condition_keys)
|
|
233
|
+
|
|
234
|
+
# Add action-specific condition keys
|
|
235
|
+
if action_name in service_detail.actions:
|
|
236
|
+
action_detail = service_detail.actions[action_name]
|
|
237
|
+
if action_detail.action_condition_keys:
|
|
238
|
+
if isinstance(action_detail.action_condition_keys, dict):
|
|
239
|
+
valid_keys.update(action_detail.action_condition_keys.keys())
|
|
240
|
+
elif isinstance(action_detail.action_condition_keys, list):
|
|
241
|
+
valid_keys.update(action_detail.action_condition_keys)
|
|
242
|
+
|
|
243
|
+
# Add resource-specific condition keys
|
|
244
|
+
if action_detail.resources:
|
|
245
|
+
for res_req in action_detail.resources:
|
|
246
|
+
resource_name = res_req.get("Name", "")
|
|
247
|
+
if resource_name:
|
|
248
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
249
|
+
if resource_type and resource_type.condition_keys:
|
|
250
|
+
if isinstance(resource_type.condition_keys, dict):
|
|
251
|
+
valid_keys.update(resource_type.condition_keys.keys())
|
|
252
|
+
elif isinstance(resource_type.condition_keys, list):
|
|
253
|
+
valid_keys.update(resource_type.condition_keys)
|
|
254
|
+
|
|
255
|
+
# Build detailed suggestion with valid keys (goes in collapsible section)
|
|
256
|
+
suggestion_parts = []
|
|
257
|
+
|
|
258
|
+
if valid_keys:
|
|
259
|
+
# Sort and limit to first 10 keys for readability
|
|
260
|
+
sorted_keys = sorted(valid_keys)
|
|
261
|
+
suggestion_parts.append("**Valid condition keys for this action:**")
|
|
262
|
+
if len(sorted_keys) <= 10:
|
|
263
|
+
for key in sorted_keys:
|
|
264
|
+
suggestion_parts.append(f"- `{key}`")
|
|
265
|
+
else:
|
|
266
|
+
for key in sorted_keys[:10]:
|
|
267
|
+
suggestion_parts.append(f"- `{key}`")
|
|
268
|
+
suggestion_parts.append(f"- ... and {len(sorted_keys) - 10} more")
|
|
269
|
+
|
|
270
|
+
suggestion_parts.append("")
|
|
271
|
+
suggestion_parts.append(
|
|
272
|
+
"**Global condition keys** (e.g., `aws:ResourceOrgID`, `aws:RequestedRegion`, `aws:SourceIp`, `aws:SourceVpce`) "
|
|
273
|
+
"can also be used with any AWS action"
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
# No action-specific keys - mention global keys
|
|
277
|
+
suggestion_parts.append(
|
|
278
|
+
"This action does not have specific condition keys defined.\n\n"
|
|
279
|
+
"However, you can use **global condition keys** such as:\n"
|
|
280
|
+
"- `aws:RequestedRegion`\n"
|
|
281
|
+
"- `aws:SourceIp`\n"
|
|
282
|
+
"- `aws:SourceVpce`\n"
|
|
283
|
+
"- `aws:UserAgent`\n"
|
|
284
|
+
"- `aws:CurrentTime`\n"
|
|
285
|
+
"- `aws:SecureTransport`\n"
|
|
286
|
+
"- `aws:PrincipalArn`\n"
|
|
287
|
+
"- And many others"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
suggestion = "\n".join(suggestion_parts)
|
|
291
|
+
|
|
292
|
+
return ConditionKeyValidationResult(
|
|
293
|
+
is_valid=False,
|
|
294
|
+
error_message=error_msg,
|
|
295
|
+
suggestion=suggestion,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
299
|
+
logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
|
|
300
|
+
return ConditionKeyValidationResult(
|
|
301
|
+
is_valid=False,
|
|
302
|
+
error_message=f"Failed to validate condition key: {e!s}",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def get_resources_for_action(
|
|
306
|
+
self, action: str, service_detail: ServiceDetail
|
|
307
|
+
) -> list[dict[str, Any]]:
|
|
308
|
+
"""Get resource types required for a specific action.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
|
|
312
|
+
service_detail: Service definition containing action details
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of resource dictionaries from AWS API, or empty list if action not found
|
|
316
|
+
|
|
317
|
+
Example:
|
|
318
|
+
>>> validator = ServiceValidator()
|
|
319
|
+
>>> service = await fetcher.fetch_service_by_name("s3")
|
|
320
|
+
>>> resources = validator.get_resources_for_action("s3:GetObject", service)
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
_, action_name = self._parser.parse_action(action)
|
|
324
|
+
|
|
325
|
+
# Find the action (case-insensitive)
|
|
326
|
+
action_detail = service_detail.actions.get(action_name)
|
|
327
|
+
if action_detail and action_detail.resources:
|
|
328
|
+
return action_detail.resources
|
|
329
|
+
return []
|
|
330
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
331
|
+
logger.error(f"Error getting resources for action {action}: {e}")
|
|
332
|
+
return []
|
|
333
|
+
|
|
334
|
+
def get_arn_formats_for_action(self, action: str, service_detail: ServiceDetail) -> list[str]:
|
|
335
|
+
"""Get ARN formats/patterns for resources used by a specific action.
|
|
336
|
+
|
|
337
|
+
This method extracts the ARN format patterns from the resource types
|
|
338
|
+
that an action can operate on. Useful for validating Resource elements
|
|
339
|
+
in IAM policies.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
|
|
343
|
+
service_detail: Service definition containing action and resource details
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
List of ARN format strings, or empty list if action not found or has no resources
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> validator = ServiceValidator()
|
|
350
|
+
>>> service = await fetcher.fetch_service_by_name("s3")
|
|
351
|
+
>>> arns = validator.get_arn_formats_for_action("s3:GetObject", service)
|
|
352
|
+
>>> # Returns: ["arn:${Partition}:s3:::${BucketName}/${ObjectName}"]
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
_, action_name = self._parser.parse_action(action)
|
|
356
|
+
|
|
357
|
+
# Find the action
|
|
358
|
+
action_detail = service_detail.actions.get(action_name)
|
|
359
|
+
if not action_detail or not action_detail.resources:
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
# Extract ARN formats from resource types
|
|
363
|
+
arn_formats = []
|
|
364
|
+
for resource_ref in action_detail.resources:
|
|
365
|
+
# resource_ref is a dict with "Name" key pointing to resource type name
|
|
366
|
+
resource_name = resource_ref.get("Name", "")
|
|
367
|
+
if not resource_name:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
# Look up the resource type in service definition
|
|
371
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
372
|
+
if resource_type and resource_type.arn_formats:
|
|
373
|
+
arn_formats.extend(resource_type.arn_formats)
|
|
374
|
+
|
|
375
|
+
return arn_formats
|
|
376
|
+
|
|
377
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
378
|
+
logger.error(f"Error getting ARN formats for action {action}: {e}")
|
|
379
|
+
return []
|
|
@@ -10,11 +10,11 @@ This module provides a pluggable check system that allows:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
from abc import ABC
|
|
13
|
+
from abc import ABC
|
|
14
14
|
from dataclasses import dataclass, field
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
|
-
from iam_validator.core.
|
|
17
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
18
18
|
from iam_validator.core.ignore_patterns import IgnorePatternMatcher
|
|
19
19
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
20
20
|
|
|
@@ -103,38 +103,101 @@ class PolicyCheck(ABC):
|
|
|
103
103
|
|
|
104
104
|
To create a custom check:
|
|
105
105
|
1. Inherit from this class
|
|
106
|
-
2. Implement check_id
|
|
107
|
-
3.
|
|
106
|
+
2. Implement check_id and description (required)
|
|
107
|
+
3. Implement either execute() OR execute_policy() (or both)
|
|
108
|
+
4. Register with CheckRegistry
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
Two ways to define check_id and description:
|
|
111
|
+
|
|
112
|
+
Option 1 - Class attributes (simpler, recommended for static values):
|
|
113
|
+
from typing import ClassVar
|
|
114
|
+
|
|
115
|
+
class MyCheck(PolicyCheck):
|
|
116
|
+
check_id: ClassVar[str] = "my_check"
|
|
117
|
+
description: ClassVar[str] = "My check description"
|
|
118
|
+
|
|
119
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
Note: ClassVar annotation is required for Pylance type checker compatibility.
|
|
123
|
+
|
|
124
|
+
Option 2 - Property decorators (more flexible, supports dynamic values):
|
|
125
|
+
class MyCheck(PolicyCheck):
|
|
126
|
+
@property
|
|
127
|
+
def check_id(self) -> str:
|
|
128
|
+
return "my_check"
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def description(self) -> str:
|
|
132
|
+
return "My check description"
|
|
113
133
|
|
|
114
134
|
async def execute(self, statement, statement_idx, fetcher, config):
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
Statement-level check example:
|
|
138
|
+
from typing import ClassVar
|
|
139
|
+
|
|
140
|
+
class MyStatementCheck(PolicyCheck):
|
|
141
|
+
check_id: ClassVar[str] = "my_statement_check"
|
|
142
|
+
description: ClassVar[str] = "Validates individual statements"
|
|
143
|
+
|
|
144
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
145
|
+
issues = []
|
|
146
|
+
# Your validation logic here
|
|
147
|
+
return issues
|
|
148
|
+
|
|
149
|
+
Policy-level check example:
|
|
150
|
+
from typing import ClassVar
|
|
151
|
+
|
|
152
|
+
class MyPolicyCheck(PolicyCheck):
|
|
153
|
+
check_id: ClassVar[str] = "my_policy_check"
|
|
154
|
+
description: ClassVar[str] = "Validates entire policy"
|
|
155
|
+
|
|
156
|
+
async def execute_policy(self, policy, policy_file, fetcher, config, **kwargs):
|
|
115
157
|
issues = []
|
|
116
158
|
# Your validation logic here
|
|
117
159
|
return issues
|
|
118
160
|
"""
|
|
119
161
|
|
|
120
162
|
@property
|
|
121
|
-
@abstractmethod
|
|
122
163
|
def check_id(self) -> str:
|
|
123
164
|
"""Unique identifier for this check (e.g., 'action_validation')."""
|
|
124
|
-
|
|
165
|
+
raise NotImplementedError("Subclasses must define check_id")
|
|
125
166
|
|
|
126
167
|
@property
|
|
127
|
-
@abstractmethod
|
|
128
168
|
def description(self) -> str:
|
|
129
169
|
"""Human-readable description of what this check does."""
|
|
130
|
-
|
|
170
|
+
raise NotImplementedError("Subclasses must define description")
|
|
131
171
|
|
|
132
172
|
@property
|
|
133
173
|
def default_severity(self) -> str:
|
|
134
174
|
"""Default severity level for issues found by this check."""
|
|
135
175
|
return "warning"
|
|
136
176
|
|
|
137
|
-
|
|
177
|
+
def __init_subclass__(cls, **kwargs):
|
|
178
|
+
"""
|
|
179
|
+
Validate that subclasses override at least one execution method.
|
|
180
|
+
|
|
181
|
+
This ensures checks implement either execute() OR execute_policy() (or both).
|
|
182
|
+
If neither is overridden, the check would never produce any results.
|
|
183
|
+
"""
|
|
184
|
+
super().__init_subclass__(**kwargs)
|
|
185
|
+
|
|
186
|
+
# Skip validation for abstract classes
|
|
187
|
+
if ABC in cls.__bases__:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Check if at least one method is overridden
|
|
191
|
+
has_execute = cls.execute is not PolicyCheck.execute
|
|
192
|
+
has_execute_policy = cls.execute_policy is not PolicyCheck.execute_policy
|
|
193
|
+
|
|
194
|
+
if not has_execute and not has_execute_policy:
|
|
195
|
+
raise TypeError(
|
|
196
|
+
f"Check '{cls.__name__}' must override at least one of: "
|
|
197
|
+
"execute() for statement-level checks, or "
|
|
198
|
+
"execute_policy() for policy-level checks"
|
|
199
|
+
)
|
|
200
|
+
|
|
138
201
|
async def execute(
|
|
139
202
|
self,
|
|
140
203
|
statement: Statement,
|
|
@@ -145,6 +208,10 @@ class PolicyCheck(ABC):
|
|
|
145
208
|
"""
|
|
146
209
|
Execute the check on a policy statement.
|
|
147
210
|
|
|
211
|
+
This method is called for statement-level checks. If your check only needs
|
|
212
|
+
to examine the entire policy (not individual statements), you can leave this
|
|
213
|
+
as the default implementation and override execute_policy() instead.
|
|
214
|
+
|
|
148
215
|
Args:
|
|
149
216
|
statement: The IAM policy statement to check
|
|
150
217
|
statement_idx: Index of the statement in the policy
|
|
@@ -154,7 +221,8 @@ class PolicyCheck(ABC):
|
|
|
154
221
|
Returns:
|
|
155
222
|
List of ValidationIssue objects found by this check
|
|
156
223
|
"""
|
|
157
|
-
|
|
224
|
+
del statement, statement_idx, fetcher, config # Unused in default implementation
|
|
225
|
+
return []
|
|
158
226
|
|
|
159
227
|
async def execute_policy(
|
|
160
228
|
self,
|
|
@@ -75,6 +75,16 @@ DEFAULT_CONFIG = {
|
|
|
75
75
|
# IAM Validity: error, warning, info
|
|
76
76
|
# Security: critical, high, medium, low
|
|
77
77
|
"fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
|
|
78
|
+
# GitHub PR label mapping based on severity findings
|
|
79
|
+
# When issues with these severities are found, apply the corresponding labels
|
|
80
|
+
# If no issues with these severities exist, remove the labels if present
|
|
81
|
+
# Supports both single labels and lists of labels per severity
|
|
82
|
+
# Examples:
|
|
83
|
+
# Single label per severity: {"error": "iam-validity-error", "critical": "security-critical"}
|
|
84
|
+
# Multiple labels per severity: {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-review"]}
|
|
85
|
+
# Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
|
|
86
|
+
# Default: {} (disabled)
|
|
87
|
+
"severity_labels": {},
|
|
78
88
|
},
|
|
79
89
|
# ========================================================================
|
|
80
90
|
# AWS IAM Validation Checks (17 checks total)
|
iam_validator/core/constants.py
CHANGED
|
@@ -123,6 +123,23 @@ DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
|
123
123
|
# Time conversion constants
|
|
124
124
|
SECONDS_PER_HOUR = 3600
|
|
125
125
|
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# Policy Type Restrictions
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
# AWS services that support Resource Control Policies (RCP)
|
|
131
|
+
# These services can have wildcard actions in RCP policy statements
|
|
132
|
+
# Reference: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_rcps.html
|
|
133
|
+
RCP_SUPPORTED_SERVICES = frozenset(
|
|
134
|
+
{
|
|
135
|
+
"s3",
|
|
136
|
+
"sts",
|
|
137
|
+
"sqs",
|
|
138
|
+
"secretsmanager",
|
|
139
|
+
"kms",
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
126
143
|
# ============================================================================
|
|
127
144
|
# AWS Documentation URLs
|
|
128
145
|
# ============================================================================
|