iam-policy-validator 1.7.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.

Potentially problematic release.


This version of iam-policy-validator might be problematic. Click here for more details.

Files changed (83) hide show
  1. iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
  2. iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
  3. iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +7 -0
  9. iam_validator/checks/__init__.py +43 -0
  10. iam_validator/checks/action_condition_enforcement.py +884 -0
  11. iam_validator/checks/action_resource_matching.py +441 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +92 -0
  14. iam_validator/checks/condition_type_mismatch.py +259 -0
  15. iam_validator/checks/full_wildcard.py +71 -0
  16. iam_validator/checks/mfa_condition_check.py +112 -0
  17. iam_validator/checks/policy_size.py +147 -0
  18. iam_validator/checks/policy_type_validation.py +305 -0
  19. iam_validator/checks/principal_validation.py +776 -0
  20. iam_validator/checks/resource_validation.py +138 -0
  21. iam_validator/checks/sensitive_action.py +254 -0
  22. iam_validator/checks/service_wildcard.py +107 -0
  23. iam_validator/checks/set_operator_validation.py +157 -0
  24. iam_validator/checks/sid_uniqueness.py +170 -0
  25. iam_validator/checks/utils/__init__.py +1 -0
  26. iam_validator/checks/utils/policy_level_checks.py +143 -0
  27. iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
  28. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  29. iam_validator/checks/wildcard_action.py +67 -0
  30. iam_validator/checks/wildcard_resource.py +135 -0
  31. iam_validator/commands/__init__.py +25 -0
  32. iam_validator/commands/analyze.py +531 -0
  33. iam_validator/commands/base.py +48 -0
  34. iam_validator/commands/cache.py +392 -0
  35. iam_validator/commands/download_services.py +255 -0
  36. iam_validator/commands/post_to_pr.py +86 -0
  37. iam_validator/commands/validate.py +600 -0
  38. iam_validator/core/__init__.py +14 -0
  39. iam_validator/core/access_analyzer.py +671 -0
  40. iam_validator/core/access_analyzer_report.py +640 -0
  41. iam_validator/core/aws_fetcher.py +940 -0
  42. iam_validator/core/check_registry.py +607 -0
  43. iam_validator/core/cli.py +134 -0
  44. iam_validator/core/condition_validators.py +626 -0
  45. iam_validator/core/config/__init__.py +81 -0
  46. iam_validator/core/config/aws_api.py +35 -0
  47. iam_validator/core/config/aws_global_conditions.py +160 -0
  48. iam_validator/core/config/category_suggestions.py +104 -0
  49. iam_validator/core/config/condition_requirements.py +155 -0
  50. iam_validator/core/config/config_loader.py +472 -0
  51. iam_validator/core/config/defaults.py +523 -0
  52. iam_validator/core/config/principal_requirements.py +421 -0
  53. iam_validator/core/config/sensitive_actions.py +672 -0
  54. iam_validator/core/config/service_principals.py +95 -0
  55. iam_validator/core/config/wildcards.py +124 -0
  56. iam_validator/core/constants.py +74 -0
  57. iam_validator/core/formatters/__init__.py +27 -0
  58. iam_validator/core/formatters/base.py +147 -0
  59. iam_validator/core/formatters/console.py +59 -0
  60. iam_validator/core/formatters/csv.py +170 -0
  61. iam_validator/core/formatters/enhanced.py +440 -0
  62. iam_validator/core/formatters/html.py +672 -0
  63. iam_validator/core/formatters/json.py +33 -0
  64. iam_validator/core/formatters/markdown.py +63 -0
  65. iam_validator/core/formatters/sarif.py +251 -0
  66. iam_validator/core/models.py +327 -0
  67. iam_validator/core/policy_checks.py +656 -0
  68. iam_validator/core/policy_loader.py +396 -0
  69. iam_validator/core/pr_commenter.py +424 -0
  70. iam_validator/core/report.py +872 -0
  71. iam_validator/integrations/__init__.py +28 -0
  72. iam_validator/integrations/github_integration.py +815 -0
  73. iam_validator/integrations/ms_teams.py +442 -0
  74. iam_validator/sdk/__init__.py +187 -0
  75. iam_validator/sdk/arn_matching.py +382 -0
  76. iam_validator/sdk/context.py +222 -0
  77. iam_validator/sdk/exceptions.py +48 -0
  78. iam_validator/sdk/helpers.py +177 -0
  79. iam_validator/sdk/policy_utils.py +425 -0
  80. iam_validator/sdk/shortcuts.py +283 -0
  81. iam_validator/utils/__init__.py +31 -0
  82. iam_validator/utils/cache.py +105 -0
  83. iam_validator/utils/regex.py +206 -0
@@ -0,0 +1,138 @@
1
+ """Resource validation check - validates ARN formats."""
2
+
3
+ import re
4
+
5
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
6
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
+ from iam_validator.core.constants import DEFAULT_ARN_VALIDATION_PATTERN, MAX_ARN_LENGTH
8
+ from iam_validator.core.models import Statement, ValidationIssue
9
+ from iam_validator.sdk.arn_matching import (
10
+ has_template_variables,
11
+ normalize_template_variables,
12
+ )
13
+
14
+
15
+ class ResourceValidationCheck(PolicyCheck):
16
+ """Validates ARN format for resources."""
17
+
18
+ @property
19
+ def check_id(self) -> str:
20
+ return "resource_validation"
21
+
22
+ @property
23
+ def description(self) -> str:
24
+ return "Validates ARN format for resources"
25
+
26
+ @property
27
+ def default_severity(self) -> str:
28
+ return "error"
29
+
30
+ async def execute(
31
+ self,
32
+ statement: Statement,
33
+ statement_idx: int,
34
+ fetcher: AWSServiceFetcher,
35
+ config: CheckConfig,
36
+ ) -> list[ValidationIssue]:
37
+ """Execute resource ARN validation on a statement."""
38
+ issues = []
39
+
40
+ # Get resources from statement
41
+ resources = statement.get_resources()
42
+ statement_sid = statement.sid
43
+ line_number = statement.line_number
44
+
45
+ # Get ARN pattern from config, or use default
46
+ # Pattern allows wildcards (*) in region and account fields
47
+ arn_pattern_str = config.config.get("arn_pattern", DEFAULT_ARN_VALIDATION_PATTERN)
48
+
49
+ # Compile pattern
50
+ try:
51
+ arn_pattern = re.compile(arn_pattern_str)
52
+ except re.error:
53
+ # Fallback to default pattern if custom pattern is invalid
54
+ arn_pattern = re.compile(DEFAULT_ARN_VALIDATION_PATTERN)
55
+
56
+ # Check if template variable support is enabled (default: true)
57
+ # Try global settings first, then check-specific config
58
+ allow_template_variables = config.root_config.get("settings", {}).get(
59
+ "allow_template_variables",
60
+ config.config.get("allow_template_variables", True),
61
+ )
62
+
63
+ for resource in resources:
64
+ # Skip wildcard resources (handled by security checks)
65
+ if resource == "*":
66
+ continue
67
+
68
+ # Validate ARN length to prevent ReDoS attacks
69
+ if len(resource) > MAX_ARN_LENGTH:
70
+ issues.append(
71
+ ValidationIssue(
72
+ severity=self.get_severity(config),
73
+ statement_sid=statement_sid,
74
+ statement_index=statement_idx,
75
+ issue_type="invalid_resource",
76
+ message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
77
+ resource=resource[:100] + "...",
78
+ suggestion="ARN is too long and may be invalid",
79
+ line_number=line_number,
80
+ )
81
+ )
82
+ continue
83
+
84
+ # Check if resource contains template variables
85
+ has_templates = has_template_variables(resource)
86
+
87
+ # If template variables are found and allowed, normalize them for validation
88
+ validation_resource = resource
89
+ if has_templates and allow_template_variables:
90
+ validation_resource = normalize_template_variables(resource)
91
+
92
+ # Validate ARN format
93
+ try:
94
+ if not arn_pattern.match(validation_resource):
95
+ # If original resource had templates and normalization didn't help,
96
+ # provide a more informative message
97
+ if has_templates and allow_template_variables:
98
+ issues.append(
99
+ ValidationIssue(
100
+ severity=self.get_severity(config),
101
+ statement_sid=statement_sid,
102
+ statement_index=statement_idx,
103
+ issue_type="invalid_resource",
104
+ message=f"Invalid ARN format even after normalizing template variables: {resource}",
105
+ resource=resource,
106
+ suggestion="ARN should follow format: arn:partition:service:region:account-id:resource (template variables like ${aws_account_id} are supported)",
107
+ line_number=line_number,
108
+ )
109
+ )
110
+ else:
111
+ issues.append(
112
+ ValidationIssue(
113
+ severity=self.get_severity(config),
114
+ statement_sid=statement_sid,
115
+ statement_index=statement_idx,
116
+ issue_type="invalid_resource",
117
+ message=f"Invalid ARN format: {resource}",
118
+ resource=resource,
119
+ suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
120
+ line_number=line_number,
121
+ )
122
+ )
123
+ except Exception:
124
+ # If regex matching fails (shouldn't happen with length check), treat as invalid
125
+ issues.append(
126
+ ValidationIssue(
127
+ severity=self.get_severity(config),
128
+ statement_sid=statement_sid,
129
+ statement_index=statement_idx,
130
+ issue_type="invalid_resource",
131
+ message=f"Could not validate ARN format: {resource}",
132
+ resource=resource,
133
+ suggestion="ARN validation failed - may contain unexpected characters",
134
+ line_number=line_number,
135
+ )
136
+ )
137
+
138
+ return issues
@@ -0,0 +1,254 @@
1
+ """Sensitive action check - detects sensitive actions without IAM conditions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
6
+ from iam_validator.checks.utils.sensitive_action_matcher import (
7
+ DEFAULT_SENSITIVE_ACTIONS,
8
+ check_sensitive_actions,
9
+ )
10
+ from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
11
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
13
+ from iam_validator.core.config.sensitive_actions import get_category_for_action
14
+ from iam_validator.core.models import Statement, ValidationIssue
15
+
16
+ if TYPE_CHECKING:
17
+ from iam_validator.core.models import IAMPolicy
18
+
19
+
20
+ class SensitiveActionCheck(PolicyCheck):
21
+ """Checks for sensitive actions without IAM conditions to limit their use."""
22
+
23
+ @property
24
+ def check_id(self) -> str:
25
+ return "sensitive_action"
26
+
27
+ @property
28
+ def description(self) -> str:
29
+ return "Checks for sensitive actions without conditions"
30
+
31
+ @property
32
+ def default_severity(self) -> str:
33
+ return "medium"
34
+
35
+ def _get_severity_for_action(self, action: str, config: CheckConfig) -> str:
36
+ """
37
+ Get severity for a specific action, considering category-based overrides.
38
+
39
+ Args:
40
+ action: The AWS action to check
41
+ config: Check configuration
42
+
43
+ Returns:
44
+ Severity level for the action (considers category overrides)
45
+ """
46
+ # Check if category severities are configured
47
+ category_severities = config.config.get("category_severities", {})
48
+ if not category_severities:
49
+ return self.get_severity(config)
50
+
51
+ # Get the category for this action
52
+ category = get_category_for_action(action)
53
+ if category and category in category_severities:
54
+ return category_severities[category]
55
+
56
+ # Fall back to default severity
57
+ return self.get_severity(config)
58
+
59
+ def _get_category_specific_suggestion(
60
+ self, action: str, config: CheckConfig
61
+ ) -> tuple[str, str]:
62
+ """
63
+ Get category-specific suggestion and example for an action.
64
+
65
+ Args:
66
+ action: The AWS action to check
67
+ config: Check configuration
68
+
69
+ Returns:
70
+ Tuple of (suggestion_text, example_text) tailored to the action's category
71
+ """
72
+ category = get_category_for_action(action)
73
+
74
+ # Get category suggestions from config (ABAC-focused by default)
75
+ # See: iam_validator/core/config/category_suggestions.py
76
+ category_suggestions = config.config.get("category_suggestions", {})
77
+
78
+ # Get category-specific content or fall back to generic ABAC guidance
79
+ if category and category in category_suggestions:
80
+ return (
81
+ category_suggestions[category]["suggestion"],
82
+ category_suggestions[category]["example"],
83
+ )
84
+
85
+ # Generic ABAC fallback for uncategorized actions
86
+ return (
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/X = aws:ResourceTag/X)\n"
89
+ "• Require MFA (aws:MultiFactorAuthPresent = true)\n"
90
+ "• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
91
+ '"Condition": {\n'
92
+ ' "StringEquals": {\n'
93
+ ' "aws:PrincipalTag/owner": "${aws:ResourceTag/owner}"\n'
94
+ " }\n"
95
+ "}",
96
+ )
97
+
98
+ async def execute(
99
+ self,
100
+ statement: Statement,
101
+ statement_idx: int,
102
+ fetcher: AWSServiceFetcher,
103
+ config: CheckConfig,
104
+ ) -> list[ValidationIssue]:
105
+ """Execute sensitive action check on a statement."""
106
+ issues = []
107
+
108
+ # Only check Allow statements
109
+ if statement.effect != "Allow":
110
+ return issues
111
+
112
+ actions = statement.get_actions()
113
+ has_conditions = statement.condition is not None and len(statement.condition) > 0
114
+
115
+ # Expand wildcards to actual actions using AWS API
116
+ expanded_actions = await expand_wildcard_actions(actions, fetcher)
117
+
118
+ # Check if sensitive actions match using any_of/all_of logic
119
+ is_sensitive, matched_actions = check_sensitive_actions(
120
+ expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
121
+ )
122
+
123
+ if is_sensitive and not has_conditions:
124
+ # Create appropriate message based on matched actions using configurable templates
125
+ if len(matched_actions) == 1:
126
+ message_template = config.config.get(
127
+ "message_single",
128
+ "Sensitive action '{action}' should have conditions to limit when it can be used",
129
+ )
130
+ message = message_template.format(action=matched_actions[0])
131
+ else:
132
+ action_list = "', '".join(matched_actions)
133
+ message_template = config.config.get(
134
+ "message_multiple",
135
+ "Sensitive actions '{actions}' should have conditions to limit when they can be used",
136
+ )
137
+ message = message_template.format(actions=action_list)
138
+
139
+ # Get category-specific suggestion and example (or use config defaults)
140
+ # Use the first matched action to determine the category
141
+ suggestion_text, example = self._get_category_specific_suggestion(
142
+ matched_actions[0], config
143
+ )
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
+ # Determine severity based on the highest severity action in the list
153
+ # If single action, use its category severity
154
+ # If multiple actions, use the highest severity among them
155
+ severity = self.get_severity(config) # Default
156
+ if matched_actions:
157
+ # Get severity for first action (or highest if we want to be more sophisticated)
158
+ severity = self._get_severity_for_action(matched_actions[0], config)
159
+
160
+ issues.append(
161
+ ValidationIssue(
162
+ severity=severity,
163
+ statement_sid=statement.sid,
164
+ statement_index=statement_idx,
165
+ issue_type="missing_condition",
166
+ message=message,
167
+ action=(matched_actions[0] if len(matched_actions) == 1 else None),
168
+ suggestion=suggestion,
169
+ line_number=statement.line_number,
170
+ )
171
+ )
172
+
173
+ return issues
174
+
175
+ async def execute_policy(
176
+ self,
177
+ policy: "IAMPolicy",
178
+ policy_file: str,
179
+ fetcher: AWSServiceFetcher,
180
+ config: CheckConfig,
181
+ **kwargs,
182
+ ) -> list[ValidationIssue]:
183
+ """
184
+ Execute policy-level sensitive action checks.
185
+
186
+ This method examines the entire policy to detect privilege escalation patterns
187
+ and other security issues that span multiple statements.
188
+
189
+ Args:
190
+ policy: The complete IAM policy to check
191
+ policy_file: Path to the policy file (for context/reporting)
192
+ fetcher: AWS service fetcher for validation against AWS APIs
193
+ config: Configuration for this check instance
194
+
195
+ Returns:
196
+ List of ValidationIssue objects found by this check
197
+ """
198
+ del policy_file, fetcher # Not used in current implementation
199
+ issues = []
200
+
201
+ # Collect all actions from all Allow statements across the entire policy
202
+ all_actions: set[str] = set()
203
+ statement_map: dict[
204
+ str, list[tuple[int, str | None]]
205
+ ] = {} # action -> [(stmt_idx, sid), ...]
206
+
207
+ for idx, statement in enumerate(policy.statement):
208
+ if statement.effect == "Allow":
209
+ actions = statement.get_actions()
210
+ # Filter out wildcards for privilege escalation detection
211
+ filtered_actions = [a for a in actions if a != "*"]
212
+
213
+ for action in filtered_actions:
214
+ all_actions.add(action)
215
+ if action not in statement_map:
216
+ statement_map[action] = []
217
+ statement_map[action].append((idx, statement.sid))
218
+
219
+ # Get configuration for sensitive actions
220
+ sensitive_actions_config = config.config.get("sensitive_actions")
221
+ sensitive_patterns_config = config.config.get("sensitive_action_patterns")
222
+
223
+ # Check for privilege escalation patterns using all_of logic
224
+ # We need to check both exact actions and patterns
225
+ policy_issues = []
226
+
227
+ # Check sensitive_actions configuration
228
+ if sensitive_actions_config:
229
+ policy_issues.extend(
230
+ check_policy_level_actions(
231
+ list(all_actions),
232
+ statement_map,
233
+ sensitive_actions_config,
234
+ config,
235
+ "actions",
236
+ self.get_severity,
237
+ )
238
+ )
239
+
240
+ # Check sensitive_action_patterns configuration
241
+ if sensitive_patterns_config:
242
+ policy_issues.extend(
243
+ check_policy_level_actions(
244
+ list(all_actions),
245
+ statement_map,
246
+ sensitive_patterns_config,
247
+ config,
248
+ "patterns",
249
+ self.get_severity,
250
+ )
251
+ )
252
+
253
+ issues.extend(policy_issues)
254
+ return issues
@@ -0,0 +1,107 @@
1
+ """Service wildcard check - detects service-level wildcards like 'iam:*', 's3:*'."""
2
+
3
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
4
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
5
+ from iam_validator.core.models import Statement, ValidationIssue
6
+
7
+
8
+ class ServiceWildcardCheck(PolicyCheck):
9
+ """Checks for service-level wildcards (e.g., 'iam:*', 's3:*') which grant all permissions for a service."""
10
+
11
+ @property
12
+ def check_id(self) -> str:
13
+ return "service_wildcard"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Checks for service-level wildcards (e.g., 'iam:*', 's3:*')"
18
+
19
+ @property
20
+ def default_severity(self) -> str:
21
+ return "high"
22
+
23
+ async def execute(
24
+ self,
25
+ statement: Statement,
26
+ statement_idx: int,
27
+ fetcher: AWSServiceFetcher,
28
+ config: CheckConfig,
29
+ ) -> list[ValidationIssue]:
30
+ """Execute service wildcard check on a statement."""
31
+ issues = []
32
+
33
+ # Only check Allow statements
34
+ if statement.effect != "Allow":
35
+ return issues
36
+
37
+ actions = statement.get_actions()
38
+ allowed_services = self._get_allowed_service_wildcards(config)
39
+
40
+ for action in actions:
41
+ # Skip full wildcard (covered by wildcard_action check)
42
+ if action == "*":
43
+ continue
44
+
45
+ # Check if it's a service-level wildcard (e.g., "iam:*", "s3:*")
46
+ if ":" in action and action.endswith(":*"):
47
+ service = action.split(":")[0]
48
+
49
+ # Check if this service is in the allowed list
50
+ if service not in allowed_services:
51
+ # Get message template and replace placeholders
52
+ message_template = config.config.get(
53
+ "message",
54
+ "Service-level wildcard '{action}' grants all permissions for {service} service",
55
+ )
56
+ suggestion_template = config.config.get(
57
+ "suggestion",
58
+ "Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
59
+ )
60
+ example_template = config.config.get("example", "")
61
+
62
+ message = message_template.format(action=action, service=service)
63
+ suggestion_text = suggestion_template.format(action=action, service=service)
64
+ example = (
65
+ example_template.format(action=action, service=service)
66
+ if example_template
67
+ else ""
68
+ )
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
+ issues.append(
78
+ ValidationIssue(
79
+ severity=self.get_severity(config),
80
+ statement_sid=statement.sid,
81
+ statement_index=statement_idx,
82
+ issue_type="overly_permissive",
83
+ message=message,
84
+ action=action,
85
+ suggestion=suggestion,
86
+ line_number=statement.line_number,
87
+ )
88
+ )
89
+
90
+ return issues
91
+
92
+ def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
93
+ """
94
+ Get list of services that are allowed to use service-level wildcards.
95
+
96
+ This allows configuration like:
97
+ service_wildcard:
98
+ allowed_services:
99
+ - "logs" # Allow "logs:*"
100
+ - "cloudwatch" # Allow "cloudwatch:*"
101
+
102
+ Returns empty set if no exceptions are configured.
103
+ """
104
+ allowed = config.config.get("allowed_services", [])
105
+ if allowed and isinstance(allowed, list):
106
+ return set(allowed)
107
+ return set()
@@ -0,0 +1,157 @@
1
+ """Set Operator Validation Check.
2
+
3
+ Validates proper usage of ForAllValues and ForAnyValue set operators in IAM policies.
4
+
5
+ Based on AWS IAM best practices:
6
+ https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html
7
+ """
8
+
9
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
10
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
11
+ from iam_validator.core.condition_validators import (
12
+ is_multivalued_context_key,
13
+ normalize_operator,
14
+ )
15
+ from iam_validator.core.models import Statement, ValidationIssue
16
+
17
+
18
+ class SetOperatorValidationCheck(PolicyCheck):
19
+ """Check for proper usage of ForAllValues and ForAnyValue set operators."""
20
+
21
+ @property
22
+ def check_id(self) -> str:
23
+ """Unique identifier for this check."""
24
+ return "set_operator_validation"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ """Description of what this check does."""
29
+ return "Validates proper usage of ForAllValues and ForAnyValue set operators"
30
+
31
+ @property
32
+ def default_severity(self) -> str:
33
+ """Default severity level for issues found by this check."""
34
+ return "error"
35
+
36
+ async def execute(
37
+ self,
38
+ statement: Statement,
39
+ statement_idx: int,
40
+ fetcher: AWSServiceFetcher,
41
+ config: CheckConfig,
42
+ ) -> list[ValidationIssue]:
43
+ """
44
+ Execute the set operator validation check.
45
+
46
+ Validates:
47
+ 1. ForAllValues/ForAnyValue not used with single-valued context keys (anti-pattern)
48
+ 2. ForAllValues with Allow effect includes Null condition check (security)
49
+ 3. ForAnyValue with Deny effect includes Null condition check (predictability)
50
+
51
+ Args:
52
+ statement: The IAM statement to check
53
+ statement_idx: Index of this statement in the policy
54
+ fetcher: AWS service fetcher (unused but required by interface)
55
+ config: Check configuration
56
+
57
+ Returns:
58
+ List of validation issues found
59
+ """
60
+ issues = []
61
+
62
+ # Only check statements with conditions
63
+ if not statement.condition:
64
+ return issues
65
+
66
+ statement_sid = statement.sid
67
+ line_number = statement.line_number
68
+ effect = statement.effect
69
+
70
+ # Track which condition keys have set operators and Null checks
71
+ set_operator_keys: dict[str, str] = {} # key -> operator prefix
72
+ null_checked_keys: set[str] = set()
73
+
74
+ # First pass: Identify set operators and Null checks
75
+ for operator, conditions in statement.condition.items():
76
+ base_operator, operator_type, set_prefix = normalize_operator(operator)
77
+
78
+ # Track Null checks
79
+ if base_operator == "Null":
80
+ for condition_key in conditions.keys():
81
+ null_checked_keys.add(condition_key)
82
+
83
+ # Track set operators
84
+ if set_prefix in ["ForAllValues", "ForAnyValue"]:
85
+ for condition_key in conditions.keys():
86
+ set_operator_keys[condition_key] = set_prefix
87
+
88
+ # Second pass: Validate set operator usage
89
+ for operator, conditions in statement.condition.items():
90
+ base_operator, operator_type, set_prefix = normalize_operator(operator)
91
+
92
+ if not set_prefix:
93
+ continue
94
+
95
+ # Check each condition key used with a set operator
96
+ for condition_key, condition_values in conditions.items():
97
+ # Issue 1: Set operator used with single-valued context key (anti-pattern)
98
+ if not is_multivalued_context_key(condition_key):
99
+ issues.append(
100
+ ValidationIssue(
101
+ severity=self.get_severity(config),
102
+ message=(
103
+ f"Set operator '{set_prefix}' should not be used with single-valued "
104
+ f"condition key '{condition_key}'. This can lead to overly permissive policies. "
105
+ f"Set operators are designed for multivalued context keys like 'aws:TagKeys'. "
106
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
107
+ ),
108
+ statement_sid=statement_sid,
109
+ statement_index=statement_idx,
110
+ issue_type="set_operator_on_single_valued_key",
111
+ condition_key=condition_key,
112
+ line_number=line_number,
113
+ )
114
+ )
115
+
116
+ # Issue 2: ForAllValues with Allow effect without Null check (security risk)
117
+ if set_prefix == "ForAllValues" and effect == "Allow":
118
+ if condition_key not in null_checked_keys:
119
+ issues.append(
120
+ ValidationIssue(
121
+ severity="warning",
122
+ message=(
123
+ f"Security risk: ForAllValues with Allow effect on '{condition_key}' "
124
+ f"should include a Null condition check. Without it, requests with missing "
125
+ f'\'{condition_key}\' will be granted access. Add: \'"Null": {{"{condition_key}": "false"}}\'. '
126
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
127
+ ),
128
+ statement_sid=statement_sid,
129
+ statement_index=statement_idx,
130
+ issue_type="forallvalues_allow_without_null_check",
131
+ condition_key=condition_key,
132
+ line_number=line_number,
133
+ )
134
+ )
135
+
136
+ # Issue 3: ForAnyValue with Deny effect without Null check (unpredictable)
137
+ if set_prefix == "ForAnyValue" and effect == "Deny":
138
+ if condition_key not in null_checked_keys:
139
+ issues.append(
140
+ ValidationIssue(
141
+ severity="warning",
142
+ message=(
143
+ f"Unpredictable behavior: ForAnyValue with Deny effect on '{condition_key}' "
144
+ f"should include a Null condition check. Without it, requests with missing "
145
+ f"'{condition_key}' will evaluate to 'No match' instead of denying access. "
146
+ f'Add: \'"Null": {{"{condition_key}": "false"}}\'. '
147
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
148
+ ),
149
+ statement_sid=statement_sid,
150
+ statement_index=statement_idx,
151
+ issue_type="foranyvalue_deny_without_null_check",
152
+ condition_key=condition_key,
153
+ line_number=line_number,
154
+ )
155
+ )
156
+
157
+ return issues