iam-policy-validator 1.7.0__py3-none-any.whl → 1.7.2__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.2.dist-info/METADATA +428 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +37 -36
- iam_validator/__version__.py +4 -2
- iam_validator/checks/action_condition_enforcement.py +22 -13
- iam_validator/checks/action_resource_matching.py +70 -36
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +8 -6
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/principal_validation.py +24 -20
- iam_validator/checks/sensitive_action.py +3 -9
- iam_validator/checks/service_wildcard.py +2 -8
- iam_validator/checks/sid_uniqueness.py +1 -1
- iam_validator/checks/utils/sensitive_action_matcher.py +1 -2
- iam_validator/checks/utils/wildcard_expansion.py +1 -2
- iam_validator/checks/wildcard_action.py +2 -8
- iam_validator/checks/wildcard_resource.py +2 -8
- iam_validator/commands/validate.py +2 -2
- iam_validator/core/aws_fetcher.py +115 -22
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +16 -7
- 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/models.py +22 -7
- iam_validator/core/policy_checks.py +5 -4
- iam_validator/core/policy_loader.py +71 -14
- 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/regex.py +7 -8
- iam_validator/utils/terminal.py +22 -0
- iam_policy_validator-1.7.0.dist-info/METADATA +0 -1057
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.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
|
|
@@ -231,18 +233,23 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
231
233
|
if reason:
|
|
232
234
|
message = reason
|
|
233
235
|
elif all_required_formats and len(all_required_formats) > 1:
|
|
234
|
-
types = ", ".join(f[
|
|
236
|
+
types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
|
|
235
237
|
message = (
|
|
236
|
-
f"No resources match for action
|
|
238
|
+
f"No resources match for action `{action}`. This action requires one of: {types}"
|
|
237
239
|
)
|
|
238
240
|
else:
|
|
239
241
|
message = (
|
|
240
|
-
f"No resources match for action
|
|
241
|
-
f"This action requires resource type: {required_type}"
|
|
242
|
+
f"No resources match for action `{action}`. "
|
|
243
|
+
f"This action requires resource type: `{required_type}`"
|
|
242
244
|
)
|
|
243
245
|
|
|
244
246
|
# Build suggestion with examples
|
|
245
|
-
suggestion = self._get_suggestion(
|
|
247
|
+
suggestion = self._get_suggestion(
|
|
248
|
+
action=action,
|
|
249
|
+
required_format=required_format,
|
|
250
|
+
provided_resources=provided_resources,
|
|
251
|
+
all_required_formats=all_required_formats,
|
|
252
|
+
)
|
|
246
253
|
|
|
247
254
|
return ValidationIssue(
|
|
248
255
|
severity=self.get_severity(config),
|
|
@@ -265,6 +272,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
265
272
|
action: str,
|
|
266
273
|
required_format: str,
|
|
267
274
|
provided_resources: list[str],
|
|
275
|
+
all_required_formats: list[dict] | None = None,
|
|
268
276
|
) -> str:
|
|
269
277
|
"""
|
|
270
278
|
Generate helpful suggestion for fixing the mismatch.
|
|
@@ -281,44 +289,75 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
281
289
|
# Special case: Wildcard resource
|
|
282
290
|
if required_format == "*":
|
|
283
291
|
return (
|
|
284
|
-
f'Action {action} can only use Resource: "*" (wildcard).\n'
|
|
292
|
+
f'Action `{action}` can only use Resource: "*" (wildcard).\n'
|
|
285
293
|
f" This action does not support resource-level permissions.\n"
|
|
286
294
|
f' Example: "Resource": "*"'
|
|
287
295
|
)
|
|
288
296
|
|
|
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
|
|
297
|
+
# Build service-specific suggestion with proper markdown formatting
|
|
297
298
|
suggestion_parts = []
|
|
298
299
|
|
|
299
|
-
#
|
|
300
|
-
|
|
300
|
+
# If multiple resource types are valid, show all of them
|
|
301
|
+
if all_required_formats and len(all_required_formats) > 1:
|
|
302
|
+
resource_types = [fmt["type"] for fmt in all_required_formats]
|
|
303
|
+
suggestion_parts.append(
|
|
304
|
+
f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
|
|
305
|
+
)
|
|
306
|
+
suggestion_parts.append("")
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
# Show format and example for each resource type
|
|
309
|
+
for fmt in all_required_formats:
|
|
310
|
+
resource_type = fmt["type"]
|
|
311
|
+
arn_format = fmt["format"]
|
|
304
312
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
313
|
+
suggestion_parts.append(
|
|
314
|
+
f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
|
|
315
|
+
)
|
|
316
|
+
suggestion_parts.append("```")
|
|
317
|
+
suggestion_parts.append(arn_format)
|
|
318
|
+
suggestion_parts.append("```")
|
|
309
319
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
320
|
+
# Add practical example
|
|
321
|
+
example = self._generate_example_arn(arn_format)
|
|
322
|
+
if example:
|
|
323
|
+
suggestion_parts.append(f"Example: `{example}`")
|
|
314
324
|
|
|
315
|
-
|
|
325
|
+
suggestion_parts.append("")
|
|
326
|
+
else:
|
|
327
|
+
# Single resource type - show detailed info
|
|
328
|
+
# Extract resource type from ARN pattern
|
|
329
|
+
# Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
|
|
330
|
+
# Examples:
|
|
331
|
+
# arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
|
|
332
|
+
# arn:${Partition}:iam::${Account}:user/${UserName} -> user
|
|
333
|
+
resource_type = self._extract_resource_type_from_pattern(required_format)
|
|
334
|
+
|
|
335
|
+
# Add action description
|
|
336
|
+
suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
|
|
337
|
+
suggestion_parts.append("")
|
|
338
|
+
|
|
339
|
+
# Add expected format in code block
|
|
340
|
+
suggestion_parts.append("**Expected format:**")
|
|
341
|
+
suggestion_parts.append(f"```\n{required_format}\n```")
|
|
342
|
+
|
|
343
|
+
# Add practical example based on the pattern
|
|
344
|
+
example = self._generate_example_arn(required_format)
|
|
345
|
+
if example:
|
|
346
|
+
suggestion_parts.append("**Example:**")
|
|
347
|
+
suggestion_parts.append(f"```\n{example}\n```")
|
|
348
|
+
|
|
349
|
+
# Add helpful context for common patterns
|
|
350
|
+
context = self._get_resource_context(action_name, resource_type, required_format)
|
|
351
|
+
if context:
|
|
352
|
+
suggestion_parts.append(f"**Note:** {context}")
|
|
316
353
|
|
|
317
354
|
# Add current resources to help user understand the mismatch
|
|
318
355
|
if provided_resources and len(provided_resources) <= 3:
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
suggestion_parts.append("**Current resources:**")
|
|
357
|
+
for resource in provided_resources:
|
|
358
|
+
suggestion_parts.append(f"- `{resource}`")
|
|
321
359
|
|
|
360
|
+
suggestion = "\n".join(suggestion_parts)
|
|
322
361
|
return suggestion
|
|
323
362
|
|
|
324
363
|
def _extract_resource_type_from_pattern(self, pattern: str) -> str:
|
|
@@ -340,17 +379,14 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
340
379
|
|
|
341
380
|
# Extract resource type (part before / or entire string)
|
|
342
381
|
if "/" in resource_part:
|
|
343
|
-
resource_type = resource_part.split("/")[0]
|
|
382
|
+
resource_type = resource_part.split("/", maxsplit=1)[0]
|
|
344
383
|
elif ":" in resource_part:
|
|
345
|
-
resource_type = resource_part.split(":")[0]
|
|
384
|
+
resource_type = resource_part.split(":", maxsplit=1)[0]
|
|
346
385
|
else:
|
|
347
386
|
resource_type = resource_part
|
|
348
387
|
|
|
349
388
|
# Remove template variables like ${...}
|
|
350
|
-
import re
|
|
351
|
-
|
|
352
389
|
resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
|
|
353
|
-
|
|
354
390
|
return resource_type.strip() or "resource"
|
|
355
391
|
|
|
356
392
|
def _generate_example_arn(self, pattern: str) -> str:
|
|
@@ -359,8 +395,6 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
359
395
|
|
|
360
396
|
Converts AWS template variables to realistic examples.
|
|
361
397
|
"""
|
|
362
|
-
import re
|
|
363
|
-
|
|
364
398
|
example = pattern
|
|
365
399
|
|
|
366
400
|
# Common substitutions
|
|
@@ -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,
|
|
@@ -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 (
|
|
176
|
+
get_global_conditions,
|
|
177
|
+
)
|
|
176
178
|
|
|
177
179
|
# Check if it's a global condition key
|
|
178
180
|
global_conditions = get_global_conditions()
|
|
@@ -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,
|
|
@@ -112,12 +112,12 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
112
112
|
ValidationIssue(
|
|
113
113
|
severity=self.get_severity(config),
|
|
114
114
|
issue_type="blocked_principal",
|
|
115
|
-
message=f"Blocked principal detected: {principal}
|
|
115
|
+
message=f"Blocked principal detected: `{principal}`. "
|
|
116
116
|
f"This principal is explicitly blocked by your security policy.",
|
|
117
117
|
statement_index=statement_idx,
|
|
118
118
|
statement_sid=statement.sid,
|
|
119
119
|
line_number=statement.line_number,
|
|
120
|
-
suggestion=f"Remove the principal
|
|
120
|
+
suggestion=f"Remove the principal `{principal}` or add appropriate conditions to restrict access. "
|
|
121
121
|
"Consider using more specific principals instead of wildcards.",
|
|
122
122
|
)
|
|
123
123
|
)
|
|
@@ -131,8 +131,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
131
131
|
ValidationIssue(
|
|
132
132
|
severity=self.get_severity(config),
|
|
133
133
|
issue_type="unauthorized_principal",
|
|
134
|
-
message=f"Principal not in allowed list: {principal}
|
|
135
|
-
f"Only principals in the allowed_principals whitelist are permitted.",
|
|
134
|
+
message=f"Principal not in allowed list: `{principal}`. "
|
|
135
|
+
f"Only principals in the `allowed_principals` whitelist are permitted.",
|
|
136
136
|
statement_index=statement_idx,
|
|
137
137
|
statement_sid=statement.sid,
|
|
138
138
|
line_number=statement.line_number,
|
|
@@ -151,7 +151,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
151
151
|
ValidationIssue(
|
|
152
152
|
severity=self.get_severity(config),
|
|
153
153
|
issue_type="missing_principal_conditions",
|
|
154
|
-
message=f"Principal
|
|
154
|
+
message=f"Principal `{principal}` requires conditions: {', '.join(f'`{c}`' for c in missing_conditions)}. "
|
|
155
155
|
f"This principal must have these condition keys to restrict access.",
|
|
156
156
|
statement_index=statement_idx,
|
|
157
157
|
statement_sid=statement.sid,
|
|
@@ -169,7 +169,11 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
169
169
|
# Check advanced format: principal_condition_requirements
|
|
170
170
|
if principal_condition_requirements:
|
|
171
171
|
condition_issues = self._validate_principal_condition_requirements(
|
|
172
|
-
statement,
|
|
172
|
+
statement,
|
|
173
|
+
statement_idx,
|
|
174
|
+
principals,
|
|
175
|
+
principal_condition_requirements,
|
|
176
|
+
config,
|
|
173
177
|
)
|
|
174
178
|
issues.extend(condition_issues)
|
|
175
179
|
|
|
@@ -629,15 +633,18 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
629
633
|
or self.get_severity(config)
|
|
630
634
|
)
|
|
631
635
|
|
|
636
|
+
suggestion_text, example_code = self._build_condition_suggestion(
|
|
637
|
+
condition_key, description, example, expected_value, operator
|
|
638
|
+
)
|
|
639
|
+
|
|
632
640
|
return ValidationIssue(
|
|
633
641
|
severity=severity,
|
|
634
642
|
statement_sid=statement.sid,
|
|
635
643
|
statement_index=statement_idx,
|
|
636
644
|
issue_type="missing_principal_condition",
|
|
637
645
|
message=f"{message_prefix} Principal(s) {matching_principals} require condition '{condition_key}'",
|
|
638
|
-
suggestion=
|
|
639
|
-
|
|
640
|
-
),
|
|
646
|
+
suggestion=suggestion_text,
|
|
647
|
+
example=example_code,
|
|
641
648
|
line_number=statement.line_number,
|
|
642
649
|
)
|
|
643
650
|
|
|
@@ -648,8 +655,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
648
655
|
example: str,
|
|
649
656
|
expected_value: Any = None,
|
|
650
657
|
operator: str = "StringEquals",
|
|
651
|
-
) -> str:
|
|
652
|
-
"""Build
|
|
658
|
+
) -> tuple[str, str]:
|
|
659
|
+
"""Build suggestion and example for adding the missing condition.
|
|
653
660
|
|
|
654
661
|
Args:
|
|
655
662
|
condition_key: The condition key
|
|
@@ -659,19 +666,16 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
659
666
|
operator: Condition operator
|
|
660
667
|
|
|
661
668
|
Returns:
|
|
662
|
-
|
|
669
|
+
Tuple of (suggestion_text, example_code)
|
|
663
670
|
"""
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if description:
|
|
667
|
-
parts.append(description)
|
|
671
|
+
suggestion = description if description else f"Add condition: {condition_key}"
|
|
668
672
|
|
|
669
673
|
# Build example based on condition key type
|
|
670
674
|
if example:
|
|
671
|
-
|
|
675
|
+
example_code = example
|
|
672
676
|
else:
|
|
673
677
|
# Auto-generate example
|
|
674
|
-
example_lines = [
|
|
678
|
+
example_lines = [f' "{operator}": {{']
|
|
675
679
|
|
|
676
680
|
if isinstance(expected_value, list):
|
|
677
681
|
value_str = (
|
|
@@ -698,9 +702,9 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
698
702
|
example_lines.append(f' "{condition_key}": {value_str}')
|
|
699
703
|
example_lines.append(" }")
|
|
700
704
|
|
|
701
|
-
|
|
705
|
+
example_code = "\n".join(example_lines)
|
|
702
706
|
|
|
703
|
-
return
|
|
707
|
+
return suggestion, example_code
|
|
704
708
|
|
|
705
709
|
def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
|
|
706
710
|
"""Build suggestion for any_of conditions.
|
|
@@ -85,7 +85,7 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
85
85
|
# Generic ABAC fallback for uncategorized actions
|
|
86
86
|
return (
|
|
87
87
|
"Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
|
|
88
|
-
"• Match principal tags to resource tags (aws:PrincipalTag
|
|
88
|
+
"• Match principal tags to resource tags (aws:PrincipalTag/<tag-name> = aws:ResourceTag/<tag-name>)\n"
|
|
89
89
|
"• Require MFA (aws:MultiFactorAuthPresent = true)\n"
|
|
90
90
|
"• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
|
|
91
91
|
'"Condition": {\n'
|
|
@@ -142,13 +142,6 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
142
142
|
matched_actions[0], config
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
# Combine suggestion + example
|
|
146
|
-
suggestion = (
|
|
147
|
-
f"{suggestion_text}\n\nExample:\n```json\n{example}\n```"
|
|
148
|
-
if example
|
|
149
|
-
else suggestion_text
|
|
150
|
-
)
|
|
151
|
-
|
|
152
145
|
# Determine severity based on the highest severity action in the list
|
|
153
146
|
# If single action, use its category severity
|
|
154
147
|
# If multiple actions, use the highest severity among them
|
|
@@ -165,7 +158,8 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
165
158
|
issue_type="missing_condition",
|
|
166
159
|
message=message,
|
|
167
160
|
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
168
|
-
suggestion=
|
|
161
|
+
suggestion=suggestion_text,
|
|
162
|
+
example=example if example else None,
|
|
169
163
|
line_number=statement.line_number,
|
|
170
164
|
)
|
|
171
165
|
)
|
|
@@ -60,20 +60,13 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
60
60
|
example_template = config.config.get("example", "")
|
|
61
61
|
|
|
62
62
|
message = message_template.format(action=action, service=service)
|
|
63
|
-
|
|
63
|
+
suggestion = suggestion_template.format(action=action, service=service)
|
|
64
64
|
example = (
|
|
65
65
|
example_template.format(action=action, service=service)
|
|
66
66
|
if example_template
|
|
67
67
|
else ""
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
# Combine suggestion + example
|
|
71
|
-
suggestion = (
|
|
72
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
73
|
-
if example
|
|
74
|
-
else suggestion_text
|
|
75
|
-
)
|
|
76
|
-
|
|
77
70
|
issues.append(
|
|
78
71
|
ValidationIssue(
|
|
79
72
|
severity=self.get_severity(config),
|
|
@@ -83,6 +76,7 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
83
76
|
message=message,
|
|
84
77
|
action=action,
|
|
85
78
|
suggestion=suggestion,
|
|
79
|
+
example=example if example else None,
|
|
86
80
|
line_number=statement.line_number,
|
|
87
81
|
)
|
|
88
82
|
)
|
|
@@ -91,7 +91,7 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
|
|
|
91
91
|
statement_sid=duplicate_sid,
|
|
92
92
|
statement_index=idx,
|
|
93
93
|
issue_type="duplicate_sid",
|
|
94
|
-
message=f"Statement ID
|
|
94
|
+
message=f"Statement ID `{duplicate_sid}` is used **{count} times** in this policy (found in statements {statement_numbers})",
|
|
95
95
|
suggestion="Change this SID to a unique value. Statement IDs help identify and reference specific statements, so duplicates can cause confusion.",
|
|
96
96
|
line_number=statement.line_number,
|
|
97
97
|
)
|
|
@@ -11,7 +11,6 @@ Performance optimizations:
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
from functools import lru_cache
|
|
14
|
-
from re import Pattern
|
|
15
14
|
|
|
16
15
|
from iam_validator.core.check_registry import CheckConfig
|
|
17
16
|
from iam_validator.core.config.sensitive_actions import get_sensitive_actions
|
|
@@ -69,7 +68,7 @@ DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
|
69
68
|
|
|
70
69
|
# Global regex pattern cache for performance
|
|
71
70
|
@lru_cache(maxsize=256)
|
|
72
|
-
def compile_pattern(pattern: str) -> Pattern[str] | None:
|
|
71
|
+
def compile_pattern(pattern: str) -> re.Pattern[str] | None:
|
|
73
72
|
"""Compile and cache regex patterns.
|
|
74
73
|
|
|
75
74
|
Args:
|
|
@@ -6,7 +6,6 @@ to their actual action names using the AWS Service Reference API.
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
from functools import lru_cache
|
|
9
|
-
from re import Pattern
|
|
10
9
|
|
|
11
10
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
12
11
|
|
|
@@ -14,7 +13,7 @@ from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
|
14
13
|
# Global cache for compiled wildcard patterns (shared across checks)
|
|
15
14
|
# Using lru_cache for O(1) pattern reuse and 20-30x performance improvement
|
|
16
15
|
@lru_cache(maxsize=512)
|
|
17
|
-
def compile_wildcard_pattern(pattern: str) -> Pattern[str]:
|
|
16
|
+
def compile_wildcard_pattern(pattern: str) -> re.Pattern[str]:
|
|
18
17
|
"""Compile and cache wildcard patterns for O(1) reuse.
|
|
19
18
|
|
|
20
19
|
Args:
|
|
@@ -39,19 +39,12 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
39
39
|
# Check for wildcard action (Action: "*")
|
|
40
40
|
if "*" in actions:
|
|
41
41
|
message = config.config.get("message", "Statement allows all actions (*)")
|
|
42
|
-
|
|
42
|
+
suggestion = config.config.get(
|
|
43
43
|
"suggestion",
|
|
44
44
|
"Replace wildcard with specific actions needed for your use case",
|
|
45
45
|
)
|
|
46
46
|
example = config.config.get("example", "")
|
|
47
47
|
|
|
48
|
-
# Combine suggestion + example
|
|
49
|
-
suggestion = (
|
|
50
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
51
|
-
if example
|
|
52
|
-
else suggestion_text
|
|
53
|
-
)
|
|
54
|
-
|
|
55
48
|
issues.append(
|
|
56
49
|
ValidationIssue(
|
|
57
50
|
severity=self.get_severity(config),
|
|
@@ -60,6 +53,7 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
60
53
|
issue_type="overly_permissive",
|
|
61
54
|
message=message,
|
|
62
55
|
suggestion=suggestion,
|
|
56
|
+
example=example if example else None,
|
|
63
57
|
line_number=statement.line_number,
|
|
64
58
|
)
|
|
65
59
|
)
|
|
@@ -63,18 +63,11 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
63
63
|
|
|
64
64
|
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
65
65
|
message = config.config.get("message", "Statement applies to all resources (*)")
|
|
66
|
-
|
|
66
|
+
suggestion = config.config.get(
|
|
67
67
|
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
68
68
|
)
|
|
69
69
|
example = config.config.get("example", "")
|
|
70
70
|
|
|
71
|
-
# Combine suggestion + example
|
|
72
|
-
suggestion = (
|
|
73
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
74
|
-
if example
|
|
75
|
-
else suggestion_text
|
|
76
|
-
)
|
|
77
|
-
|
|
78
71
|
issues.append(
|
|
79
72
|
ValidationIssue(
|
|
80
73
|
severity=self.get_severity(config),
|
|
@@ -83,6 +76,7 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
83
76
|
issue_type="overly_permissive",
|
|
84
77
|
message=message,
|
|
85
78
|
suggestion=suggestion,
|
|
79
|
+
example=example if example else None,
|
|
86
80
|
line_number=statement.line_number,
|
|
87
81
|
)
|
|
88
82
|
)
|
|
@@ -254,9 +254,9 @@ Examples:
|
|
|
254
254
|
policy_type=policy_type,
|
|
255
255
|
)
|
|
256
256
|
|
|
257
|
-
# Generate report
|
|
257
|
+
# Generate report (include parsing errors if any)
|
|
258
258
|
generator = ReportGenerator()
|
|
259
|
-
report = generator.generate_report(results)
|
|
259
|
+
report = generator.generate_report(results, parsing_errors=loader.parsing_errors)
|
|
260
260
|
|
|
261
261
|
# Output results
|
|
262
262
|
if args.format is None:
|