iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.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.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
- iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
- iam_validator/__version__.py +4 -2
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +81 -36
- iam_validator/checks/action_resource_matching.py +75 -37
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +10 -8
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +86 -150
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +9 -11
- iam_validator/checks/service_wildcard.py +4 -10
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +5 -9
- iam_validator/checks/wildcard_resource.py +5 -9
- iam_validator/commands/validate.py +8 -14
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +159 -64
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +74 -59
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +35 -10
- iam_validator/core/policy_checks.py +34 -474
- iam_validator/core/policy_loader.py +98 -18
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/terminal.py +22 -0
- iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,6 +21,8 @@ Example:
|
|
|
21
21
|
This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import re
|
|
25
|
+
|
|
24
26
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
25
27
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
26
28
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
@@ -86,6 +88,10 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
86
88
|
statement_sid = statement.sid
|
|
87
89
|
line_number = statement.line_number
|
|
88
90
|
|
|
91
|
+
# Skip if no resources to validate (e.g., trust policies don't have Resource field)
|
|
92
|
+
if not resources:
|
|
93
|
+
return issues
|
|
94
|
+
|
|
89
95
|
# Skip if we have a wildcard resource (handled by other checks)
|
|
90
96
|
if "*" in resources:
|
|
91
97
|
return issues
|
|
@@ -231,18 +237,23 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
231
237
|
if reason:
|
|
232
238
|
message = reason
|
|
233
239
|
elif all_required_formats and len(all_required_formats) > 1:
|
|
234
|
-
types = ", ".join(f[
|
|
240
|
+
types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
|
|
235
241
|
message = (
|
|
236
|
-
f"No resources match for action
|
|
242
|
+
f"No resources match for action `{action}`. This action requires one of: {types}"
|
|
237
243
|
)
|
|
238
244
|
else:
|
|
239
245
|
message = (
|
|
240
|
-
f"No resources match for action
|
|
241
|
-
f"This action requires resource type: {required_type}"
|
|
246
|
+
f"No resources match for action `{action}`. "
|
|
247
|
+
f"This action requires resource type: `{required_type}`"
|
|
242
248
|
)
|
|
243
249
|
|
|
244
250
|
# Build suggestion with examples
|
|
245
|
-
suggestion = self._get_suggestion(
|
|
251
|
+
suggestion = self._get_suggestion(
|
|
252
|
+
action=action,
|
|
253
|
+
required_format=required_format,
|
|
254
|
+
provided_resources=provided_resources,
|
|
255
|
+
all_required_formats=all_required_formats,
|
|
256
|
+
)
|
|
246
257
|
|
|
247
258
|
return ValidationIssue(
|
|
248
259
|
severity=self.get_severity(config),
|
|
@@ -265,6 +276,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
265
276
|
action: str,
|
|
266
277
|
required_format: str,
|
|
267
278
|
provided_resources: list[str],
|
|
279
|
+
all_required_formats: list[dict] | None = None,
|
|
268
280
|
) -> str:
|
|
269
281
|
"""
|
|
270
282
|
Generate helpful suggestion for fixing the mismatch.
|
|
@@ -281,44 +293,75 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
281
293
|
# Special case: Wildcard resource
|
|
282
294
|
if required_format == "*":
|
|
283
295
|
return (
|
|
284
|
-
f'Action {action} can only use Resource: "*" (wildcard).\n'
|
|
296
|
+
f'Action `{action}` can only use Resource: `"*"` (wildcard).\n'
|
|
285
297
|
f" This action does not support resource-level permissions.\n"
|
|
286
|
-
f' Example: "Resource": "*"'
|
|
298
|
+
f' Example: "Resource": `"*"`'
|
|
287
299
|
)
|
|
288
300
|
|
|
289
|
-
#
|
|
290
|
-
# Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
|
|
291
|
-
# Examples:
|
|
292
|
-
# arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
|
|
293
|
-
# arn:${Partition}:iam::${Account}:user/${UserName} -> user
|
|
294
|
-
resource_type = self._extract_resource_type_from_pattern(required_format)
|
|
295
|
-
|
|
296
|
-
# Build service-specific suggestion
|
|
301
|
+
# Build service-specific suggestion with proper markdown formatting
|
|
297
302
|
suggestion_parts = []
|
|
298
303
|
|
|
299
|
-
#
|
|
300
|
-
|
|
304
|
+
# If multiple resource types are valid, show all of them
|
|
305
|
+
if all_required_formats and len(all_required_formats) > 1:
|
|
306
|
+
resource_types = [fmt["type"] for fmt in all_required_formats]
|
|
307
|
+
suggestion_parts.append(
|
|
308
|
+
f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
|
|
309
|
+
)
|
|
310
|
+
suggestion_parts.append("")
|
|
301
311
|
|
|
302
|
-
|
|
303
|
-
|
|
312
|
+
# Show format and example for each resource type
|
|
313
|
+
for fmt in all_required_formats:
|
|
314
|
+
resource_type = fmt["type"]
|
|
315
|
+
arn_format = fmt["format"]
|
|
304
316
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
317
|
+
suggestion_parts.append(
|
|
318
|
+
f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
|
|
319
|
+
)
|
|
320
|
+
suggestion_parts.append("```")
|
|
321
|
+
suggestion_parts.append(arn_format)
|
|
322
|
+
suggestion_parts.append("```")
|
|
309
323
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
324
|
+
# Add practical example
|
|
325
|
+
example = self._generate_example_arn(arn_format)
|
|
326
|
+
if example:
|
|
327
|
+
suggestion_parts.append(f"Example: `{example}`")
|
|
314
328
|
|
|
315
|
-
|
|
329
|
+
suggestion_parts.append("")
|
|
330
|
+
else:
|
|
331
|
+
# Single resource type - show detailed info
|
|
332
|
+
# Extract resource type from ARN pattern
|
|
333
|
+
# Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
|
|
334
|
+
# Examples:
|
|
335
|
+
# arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
|
|
336
|
+
# arn:${Partition}:iam::${Account}:user/${UserName} -> user
|
|
337
|
+
resource_type = self._extract_resource_type_from_pattern(required_format)
|
|
338
|
+
|
|
339
|
+
# Add action description
|
|
340
|
+
suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
|
|
341
|
+
suggestion_parts.append("")
|
|
342
|
+
|
|
343
|
+
# Add expected format in code block
|
|
344
|
+
suggestion_parts.append("**Expected format:**")
|
|
345
|
+
suggestion_parts.append(f"```\n{required_format}\n```")
|
|
346
|
+
|
|
347
|
+
# Add practical example based on the pattern
|
|
348
|
+
example = self._generate_example_arn(required_format)
|
|
349
|
+
if example:
|
|
350
|
+
suggestion_parts.append("**Example:**")
|
|
351
|
+
suggestion_parts.append(f"```\n{example}\n```")
|
|
352
|
+
|
|
353
|
+
# Add helpful context for common patterns
|
|
354
|
+
context = self._get_resource_context(action_name, resource_type, required_format)
|
|
355
|
+
if context:
|
|
356
|
+
suggestion_parts.append(f"**Note:** {context}")
|
|
316
357
|
|
|
317
358
|
# Add current resources to help user understand the mismatch
|
|
318
359
|
if provided_resources and len(provided_resources) <= 3:
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
suggestion_parts.append("**Current resources:**")
|
|
361
|
+
for resource in provided_resources:
|
|
362
|
+
suggestion_parts.append(f"- `{resource}`")
|
|
321
363
|
|
|
364
|
+
suggestion = "\n".join(suggestion_parts)
|
|
322
365
|
return suggestion
|
|
323
366
|
|
|
324
367
|
def _extract_resource_type_from_pattern(self, pattern: str) -> str:
|
|
@@ -340,17 +383,14 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
340
383
|
|
|
341
384
|
# Extract resource type (part before / or entire string)
|
|
342
385
|
if "/" in resource_part:
|
|
343
|
-
resource_type = resource_part.split("/")[0]
|
|
386
|
+
resource_type = resource_part.split("/", maxsplit=1)[0]
|
|
344
387
|
elif ":" in resource_part:
|
|
345
|
-
resource_type = resource_part.split(":")[0]
|
|
388
|
+
resource_type = resource_part.split(":", maxsplit=1)[0]
|
|
346
389
|
else:
|
|
347
390
|
resource_type = resource_part
|
|
348
391
|
|
|
349
392
|
# Remove template variables like ${...}
|
|
350
|
-
import re
|
|
351
|
-
|
|
352
393
|
resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
|
|
353
|
-
|
|
354
394
|
return resource_type.strip() or "resource"
|
|
355
395
|
|
|
356
396
|
def _generate_example_arn(self, pattern: str) -> str:
|
|
@@ -359,8 +399,6 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
359
399
|
|
|
360
400
|
Converts AWS template variables to realistic examples.
|
|
361
401
|
"""
|
|
362
|
-
import re
|
|
363
|
-
|
|
364
402
|
example = pattern
|
|
365
403
|
|
|
366
404
|
# Common substitutions
|
|
@@ -63,7 +63,7 @@ class ActionValidationCheck(PolicyCheck):
|
|
|
63
63
|
statement_sid=statement_sid,
|
|
64
64
|
statement_index=statement_idx,
|
|
65
65
|
issue_type="invalid_action",
|
|
66
|
-
message=error_msg or f"Invalid action: {action}",
|
|
66
|
+
message=error_msg or f"Invalid action: `{action}`",
|
|
67
67
|
action=action,
|
|
68
68
|
line_number=line_number,
|
|
69
69
|
)
|
|
@@ -52,26 +52,26 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
52
52
|
continue
|
|
53
53
|
|
|
54
54
|
# Validate against action and resource types
|
|
55
|
-
|
|
56
|
-
action, condition_key, resources
|
|
57
|
-
)
|
|
55
|
+
result = await fetcher.validate_condition_key(action, condition_key, resources)
|
|
58
56
|
|
|
59
|
-
if not is_valid:
|
|
57
|
+
if not result.is_valid:
|
|
60
58
|
issues.append(
|
|
61
59
|
ValidationIssue(
|
|
62
60
|
severity=self.get_severity(config),
|
|
63
61
|
statement_sid=statement_sid,
|
|
64
62
|
statement_index=statement_idx,
|
|
65
63
|
issue_type="invalid_condition_key",
|
|
66
|
-
message=
|
|
64
|
+
message=result.error_message
|
|
65
|
+
or f"Invalid condition key: `{condition_key}`",
|
|
67
66
|
action=action,
|
|
68
67
|
condition_key=condition_key,
|
|
69
68
|
line_number=line_number,
|
|
69
|
+
suggestion=result.suggestion,
|
|
70
70
|
)
|
|
71
71
|
)
|
|
72
72
|
# Only report once per condition key (not per action)
|
|
73
73
|
break
|
|
74
|
-
elif
|
|
74
|
+
elif result.warning_message and warn_on_global_keys:
|
|
75
75
|
# Add warning for global condition keys with action-specific keys
|
|
76
76
|
# Only if warn_on_global_condition_keys is enabled
|
|
77
77
|
issues.append(
|
|
@@ -80,7 +80,7 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
80
80
|
statement_sid=statement_sid,
|
|
81
81
|
statement_index=statement_idx,
|
|
82
82
|
issue_type="global_condition_key_with_action_specific",
|
|
83
|
-
message=
|
|
83
|
+
message=result.warning_message,
|
|
84
84
|
action=action,
|
|
85
85
|
condition_key=condition_key,
|
|
86
86
|
line_number=line_number,
|
|
@@ -72,7 +72,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
72
72
|
# Check each condition operator and its keys/values
|
|
73
73
|
for operator, conditions in statement.condition.items():
|
|
74
74
|
# Normalize the operator and get its expected type
|
|
75
|
-
base_operator, operator_type,
|
|
75
|
+
base_operator, operator_type, _set_prefix = normalize_operator(operator)
|
|
76
76
|
|
|
77
77
|
if operator_type is None:
|
|
78
78
|
# Unknown operator - this will be caught by another check
|
|
@@ -107,8 +107,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
107
107
|
ValidationIssue(
|
|
108
108
|
severity="warning",
|
|
109
109
|
message=(
|
|
110
|
-
f"Type mismatch (usable but not recommended): Operator
|
|
111
|
-
f"{operator_type} values, but condition key
|
|
110
|
+
f"Type mismatch (usable but not recommended): Operator `{operator}` expects "
|
|
111
|
+
f"`{operator_type}` values, but condition key `{condition_key}` is type `{key_type}`. "
|
|
112
112
|
f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
|
|
113
113
|
),
|
|
114
114
|
statement_sid=statement_sid,
|
|
@@ -123,8 +123,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
123
123
|
ValidationIssue(
|
|
124
124
|
severity=self.get_severity(config),
|
|
125
125
|
message=(
|
|
126
|
-
f"Type mismatch: Operator
|
|
127
|
-
f"but condition key
|
|
126
|
+
f"Type mismatch: Operator `{operator}` expects `{operator_type}` values, "
|
|
127
|
+
f"but condition key `{condition_key}` is type `{key_type}`."
|
|
128
128
|
),
|
|
129
129
|
statement_sid=statement_sid,
|
|
130
130
|
statement_index=statement_idx,
|
|
@@ -141,7 +141,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
141
141
|
ValidationIssue(
|
|
142
142
|
severity=self.get_severity(config),
|
|
143
143
|
message=(
|
|
144
|
-
f"Invalid value format for condition key
|
|
144
|
+
f"Invalid value format for condition key `{condition_key}`: {error_msg}"
|
|
145
145
|
),
|
|
146
146
|
statement_sid=statement_sid,
|
|
147
147
|
statement_index=statement_idx,
|
|
@@ -172,7 +172,9 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
172
172
|
Returns:
|
|
173
173
|
Type string or None if not found
|
|
174
174
|
"""
|
|
175
|
-
from iam_validator.core.config.aws_global_conditions import
|
|
175
|
+
from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
|
|
176
|
+
get_global_conditions,
|
|
177
|
+
)
|
|
176
178
|
|
|
177
179
|
# Check if it's a global condition key
|
|
178
180
|
global_conditions = get_global_conditions()
|
|
@@ -227,7 +229,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
227
229
|
if condition_key_obj.types:
|
|
228
230
|
return condition_key_obj.types[0]
|
|
229
231
|
|
|
230
|
-
except Exception:
|
|
232
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
231
233
|
# If we can't look up the action, skip it
|
|
232
234
|
continue
|
|
233
235
|
|
|
@@ -43,19 +43,12 @@ class FullWildcardCheck(PolicyCheck):
|
|
|
43
43
|
"message",
|
|
44
44
|
"Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
45
45
|
)
|
|
46
|
-
|
|
46
|
+
suggestion = config.config.get(
|
|
47
47
|
"suggestion",
|
|
48
48
|
"This grants full administrative access. Replace both wildcards with specific actions and resources to follow least-privilege principle",
|
|
49
49
|
)
|
|
50
50
|
example = config.config.get("example", "")
|
|
51
51
|
|
|
52
|
-
# Combine suggestion + example
|
|
53
|
-
suggestion = (
|
|
54
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
55
|
-
if example
|
|
56
|
-
else suggestion_text
|
|
57
|
-
)
|
|
58
|
-
|
|
59
52
|
issues.append(
|
|
60
53
|
ValidationIssue(
|
|
61
54
|
severity=self.get_severity(config),
|
|
@@ -64,6 +57,7 @@ class FullWildcardCheck(PolicyCheck):
|
|
|
64
57
|
issue_type="security_risk",
|
|
65
58
|
message=message,
|
|
66
59
|
suggestion=suggestion,
|
|
60
|
+
example=example if example else None,
|
|
67
61
|
line_number=statement.line_number,
|
|
68
62
|
)
|
|
69
63
|
)
|
|
@@ -70,11 +70,11 @@ class MFAConditionCheck(PolicyCheck):
|
|
|
70
70
|
ValidationIssue(
|
|
71
71
|
severity=self.get_severity(config),
|
|
72
72
|
message=(
|
|
73
|
-
"Dangerous MFA condition pattern detected
|
|
74
|
-
'Using {"Bool": {"aws:MultiFactorAuthPresent": "false"}} does not enforce MFA '
|
|
75
|
-
"because aws:MultiFactorAuthPresent may not exist in the request context. "
|
|
76
|
-
'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement, '
|
|
77
|
-
"or use BoolIfExists in a Deny statement."
|
|
73
|
+
"**Dangerous MFA condition pattern detected.** "
|
|
74
|
+
'Using `{"Bool": {"aws:MultiFactorAuthPresent": "false"}}` does not enforce MFA '
|
|
75
|
+
"because `aws:MultiFactorAuthPresent` may not exist in the request context. "
|
|
76
|
+
'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement, '
|
|
77
|
+
"or use `BoolIfExists` in a Deny statement."
|
|
78
78
|
),
|
|
79
79
|
statement_sid=statement_sid,
|
|
80
80
|
statement_index=statement_idx,
|
|
@@ -97,10 +97,10 @@ class MFAConditionCheck(PolicyCheck):
|
|
|
97
97
|
ValidationIssue(
|
|
98
98
|
severity=self.get_severity(config),
|
|
99
99
|
message=(
|
|
100
|
-
"Dangerous MFA condition pattern detected
|
|
101
|
-
'Using {"Null": {"aws:MultiFactorAuthPresent": "false"}} only checks if the key exists, '
|
|
100
|
+
"**Dangerous MFA condition pattern detected.** "
|
|
101
|
+
'Using `{"Null": {"aws:MultiFactorAuthPresent": "false"}}` only checks if the key exists, '
|
|
102
102
|
"not whether MFA was actually used. This does not enforce MFA. "
|
|
103
|
-
'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement instead.'
|
|
103
|
+
'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement instead.'
|
|
104
104
|
),
|
|
105
105
|
statement_sid=statement_sid,
|
|
106
106
|
statement_index=statement_idx,
|