iam-policy-validator 1.5.0__py3-none-any.whl → 1.6.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.
Files changed (42) hide show
  1. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
  2. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +9 -3
  5. iam_validator/checks/action_condition_enforcement.py +164 -2
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +3 -1
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/mfa_condition_check.py +112 -0
  10. iam_validator/checks/sensitive_action.py +78 -6
  11. iam_validator/checks/set_operator_validation.py +157 -0
  12. iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
  13. iam_validator/commands/cache.py +1 -1
  14. iam_validator/commands/validate.py +44 -11
  15. iam_validator/core/aws_fetcher.py +89 -52
  16. iam_validator/core/check_registry.py +165 -21
  17. iam_validator/core/condition_validators.py +626 -0
  18. iam_validator/core/config/__init__.py +13 -15
  19. iam_validator/core/config/aws_global_conditions.py +160 -0
  20. iam_validator/core/config/category_suggestions.py +104 -0
  21. iam_validator/core/config/condition_requirements.py +5 -385
  22. iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
  23. iam_validator/core/config/defaults.py +187 -54
  24. iam_validator/core/config/sensitive_actions.py +620 -81
  25. iam_validator/core/models.py +14 -1
  26. iam_validator/core/policy_checks.py +4 -4
  27. iam_validator/core/pr_commenter.py +1 -1
  28. iam_validator/sdk/__init__.py +187 -0
  29. iam_validator/sdk/arn_matching.py +274 -0
  30. iam_validator/sdk/context.py +222 -0
  31. iam_validator/sdk/exceptions.py +48 -0
  32. iam_validator/sdk/helpers.py +177 -0
  33. iam_validator/sdk/policy_utils.py +425 -0
  34. iam_validator/sdk/shortcuts.py +283 -0
  35. iam_validator/utils/__init__.py +31 -0
  36. iam_validator/utils/cache.py +105 -0
  37. iam_validator/utils/regex.py +206 -0
  38. iam_validator/checks/action_resource_constraint.py +0 -151
  39. iam_validator/core/aws_global_conditions.py +0 -137
  40. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  41. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  42. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,259 @@
1
+ """Condition Type Mismatch Check.
2
+
3
+ Validates that condition operators match the expected types for condition keys and values.
4
+ """
5
+
6
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
7
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
8
+ from iam_validator.core.condition_validators import (
9
+ normalize_operator,
10
+ translate_type,
11
+ validate_value_for_type,
12
+ )
13
+ from iam_validator.core.models import Statement, ValidationIssue
14
+
15
+
16
+ class ConditionTypeMismatchCheck(PolicyCheck):
17
+ """Check for type mismatches between operators, keys, and values."""
18
+
19
+ @property
20
+ def check_id(self) -> str:
21
+ """Unique identifier for this check."""
22
+ return "condition_type_mismatch"
23
+
24
+ @property
25
+ def description(self) -> str:
26
+ """Description of what this check does."""
27
+ return "Validates condition operator types match key types and value formats"
28
+
29
+ @property
30
+ def default_severity(self) -> str:
31
+ """Default severity level for issues found by this check."""
32
+ return "error"
33
+
34
+ async def execute(
35
+ self,
36
+ statement: Statement,
37
+ statement_idx: int,
38
+ fetcher: AWSServiceFetcher,
39
+ config: CheckConfig,
40
+ ) -> list[ValidationIssue]:
41
+ """
42
+ Execute the condition type mismatch check.
43
+
44
+ Validates:
45
+ 1. Operator type matches condition key type
46
+ 2. Condition values match the expected type format
47
+
48
+ Args:
49
+ statement: The IAM statement to check
50
+ statement_idx: Index of this statement in the policy
51
+ fetcher: AWS service fetcher for looking up condition key types
52
+ config: Check configuration
53
+
54
+ Returns:
55
+ List of validation issues found
56
+ """
57
+ issues = []
58
+
59
+ # Only check statements with conditions
60
+ if not statement.condition:
61
+ return issues
62
+
63
+ # Skip Null operator - it's special and doesn't need type validation
64
+ # (Null just checks if a key exists or doesn't exist)
65
+ skip_operators = {"Null"}
66
+
67
+ statement_sid = statement.sid
68
+ line_number = statement.line_number
69
+ actions = statement.get_actions()
70
+ resources = statement.get_resources()
71
+
72
+ # Check each condition operator and its keys/values
73
+ for operator, conditions in statement.condition.items():
74
+ # Normalize the operator and get its expected type
75
+ base_operator, operator_type, set_prefix = normalize_operator(operator)
76
+
77
+ if operator_type is None:
78
+ # Unknown operator - this will be caught by another check
79
+ continue
80
+
81
+ if base_operator in skip_operators:
82
+ continue
83
+
84
+ # Check each condition key
85
+ for condition_key, condition_values in conditions.items():
86
+ # Normalize values to a list
87
+ values = (
88
+ condition_values if isinstance(condition_values, list) else [condition_values]
89
+ )
90
+
91
+ # Get the expected type for this condition key
92
+ key_type = await self._get_condition_key_type(
93
+ fetcher, condition_key, actions, resources
94
+ )
95
+
96
+ if key_type is None:
97
+ # Unknown condition key - will be caught by condition_key_validation check
98
+ continue
99
+
100
+ # Normalize the key type
101
+ key_type = translate_type(key_type)
102
+ operator_type = translate_type(operator_type)
103
+
104
+ # Special case: String operators with ARN types (usable but not recommended)
105
+ if operator_type == "String" and key_type == "ARN":
106
+ issues.append(
107
+ ValidationIssue(
108
+ severity="warning",
109
+ message=(
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
+ f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
113
+ ),
114
+ statement_sid=statement_sid,
115
+ statement_index=statement_idx,
116
+ issue_type="type_mismatch_usable",
117
+ line_number=line_number,
118
+ )
119
+ )
120
+ # Check if operator type matches key type
121
+ elif not self._types_compatible(operator_type, key_type):
122
+ issues.append(
123
+ ValidationIssue(
124
+ severity=self.get_severity(config),
125
+ message=(
126
+ f"Type mismatch: Operator '{operator}' expects {operator_type} values, "
127
+ f"but condition key '{condition_key}' is type {key_type}."
128
+ ),
129
+ statement_sid=statement_sid,
130
+ statement_index=statement_idx,
131
+ issue_type="type_mismatch",
132
+ condition_key=condition_key,
133
+ line_number=line_number,
134
+ )
135
+ )
136
+
137
+ # Validate that the values match the expected type format
138
+ is_valid, error_msg = validate_value_for_type(key_type, values)
139
+ if not is_valid:
140
+ issues.append(
141
+ ValidationIssue(
142
+ severity=self.get_severity(config),
143
+ message=(
144
+ f"Invalid value format for condition key '{condition_key}': {error_msg}"
145
+ ),
146
+ statement_sid=statement_sid,
147
+ statement_index=statement_idx,
148
+ issue_type="invalid_value_format",
149
+ condition_key=condition_key,
150
+ line_number=line_number,
151
+ )
152
+ )
153
+
154
+ return issues
155
+
156
+ async def _get_condition_key_type(
157
+ self,
158
+ fetcher: AWSServiceFetcher,
159
+ condition_key: str,
160
+ actions: list[str],
161
+ resources: list[str],
162
+ ) -> str | None:
163
+ """
164
+ Get the expected type for a condition key by checking global keys and service definitions.
165
+
166
+ Args:
167
+ fetcher: AWS service fetcher
168
+ condition_key: The condition key to look up
169
+ actions: List of actions from the statement
170
+ resources: List of resources from the statement
171
+
172
+ Returns:
173
+ Type string or None if not found
174
+ """
175
+ from iam_validator.core.config.aws_global_conditions import get_global_conditions
176
+
177
+ # Check if it's a global condition key
178
+ global_conditions = get_global_conditions()
179
+ key_type = global_conditions.get_key_type(condition_key)
180
+ if key_type:
181
+ return key_type
182
+
183
+ # Check service-specific and action-specific condition keys
184
+ for action in actions:
185
+ if action == "*":
186
+ continue
187
+
188
+ try:
189
+ service_prefix, action_name = fetcher.parse_action(action)
190
+ service_detail = await fetcher.fetch_service_by_name(service_prefix)
191
+
192
+ # Check service-level condition keys
193
+ if condition_key in service_detail.condition_keys:
194
+ condition_key_obj = service_detail.condition_keys[condition_key]
195
+ if condition_key_obj.types:
196
+ return condition_key_obj.types[0]
197
+
198
+ # Check action-level condition keys
199
+ if action_name in service_detail.actions:
200
+ action_detail = service_detail.actions[action_name]
201
+
202
+ # For action-specific keys, we need to check the service condition keys list
203
+ if (
204
+ action_detail.action_condition_keys
205
+ and condition_key in action_detail.action_condition_keys
206
+ ):
207
+ if condition_key in service_detail.condition_keys:
208
+ condition_key_obj = service_detail.condition_keys[condition_key]
209
+ if condition_key_obj.types:
210
+ return condition_key_obj.types[0]
211
+
212
+ # Check resource-specific condition keys
213
+ if resources and action_detail.resources:
214
+ for res_req in action_detail.resources:
215
+ resource_name = res_req.get("Name", "")
216
+ if not resource_name:
217
+ continue
218
+
219
+ resource_type = service_detail.resources.get(resource_name)
220
+ if resource_type and resource_type.condition_keys:
221
+ if condition_key in resource_type.condition_keys:
222
+ # Resource condition keys reference service condition keys
223
+ if condition_key in service_detail.condition_keys:
224
+ condition_key_obj = service_detail.condition_keys[
225
+ condition_key
226
+ ]
227
+ if condition_key_obj.types:
228
+ return condition_key_obj.types[0]
229
+
230
+ except Exception:
231
+ # If we can't look up the action, skip it
232
+ continue
233
+
234
+ return None
235
+
236
+ def _types_compatible(self, operator_type: str, key_type: str) -> bool:
237
+ """
238
+ Check if an operator type is compatible with a key type.
239
+
240
+ Note: String/ARN compatibility is handled separately with a warning,
241
+ so this method returns False for that combination.
242
+
243
+ Args:
244
+ operator_type: Type expected by the operator
245
+ key_type: Type of the condition key
246
+
247
+ Returns:
248
+ True if compatible
249
+ """
250
+ # Exact match
251
+ if operator_type == key_type:
252
+ return True
253
+
254
+ # EpochTime can accept both Date and Numeric
255
+ # (this is a special case mentioned in Parliament)
256
+ if key_type == "Date" and operator_type == "Numeric":
257
+ return True
258
+
259
+ return False
@@ -0,0 +1,112 @@
1
+ """MFA Condition Anti-Pattern Check.
2
+
3
+ Detects dangerous MFA-related condition patterns that may not enforce MFA as intended.
4
+ """
5
+
6
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
+ from iam_validator.core.models import Statement, ValidationIssue
8
+
9
+
10
+ class MFAConditionCheck(PolicyCheck):
11
+ """Check for MFA condition anti-patterns."""
12
+
13
+ @property
14
+ def check_id(self) -> str:
15
+ """Unique identifier for this check."""
16
+ return "mfa_condition_antipattern"
17
+
18
+ @property
19
+ def description(self) -> str:
20
+ """Description of what this check does."""
21
+ return "Detects dangerous MFA-related condition patterns"
22
+
23
+ @property
24
+ def default_severity(self) -> str:
25
+ """Default severity level for issues found by this check."""
26
+ return "warning"
27
+
28
+ async def execute(
29
+ self, statement: Statement, statement_idx: int, fetcher, config: CheckConfig
30
+ ) -> list[ValidationIssue]:
31
+ """
32
+ Execute the MFA condition anti-pattern check.
33
+
34
+ Common anti-patterns:
35
+ 1. Using Bool with aws:MultiFactorAuthPresent = false
36
+ Problem: The key may not exist in the request, so condition doesn't enforce anything
37
+
38
+ 2. Using Null with aws:MultiFactorAuthPresent = false
39
+ Problem: This only checks if the key exists, not if MFA was used
40
+
41
+ Args:
42
+ statement: The IAM statement to check
43
+ statement_idx: Index of this statement in the policy
44
+ fetcher: AWS service fetcher (not used in this check)
45
+ config: Check configuration
46
+
47
+ Returns:
48
+ List of validation issues found
49
+ """
50
+ issues = []
51
+
52
+ # Only check statements with conditions
53
+ if not statement.condition:
54
+ return issues
55
+
56
+ statement_sid = statement.sid
57
+ line_number = statement.line_number
58
+
59
+ # Check for anti-pattern #1: Bool with aws:MultiFactorAuthPresent = false
60
+ bool_conditions = statement.condition.get("Bool", {})
61
+ for key, value in bool_conditions.items():
62
+ if key.lower() == "aws:multifactorauthpresent":
63
+ # Normalize value to list
64
+ values = value if isinstance(value, list) else [value]
65
+ # Convert to lowercase strings for comparison
66
+ values_lower = [str(v).lower() for v in values]
67
+
68
+ if "false" in values_lower or False in values:
69
+ issues.append(
70
+ ValidationIssue(
71
+ severity=self.get_severity(config),
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."
78
+ ),
79
+ statement_sid=statement_sid,
80
+ statement_index=statement_idx,
81
+ issue_type="mfa_antipattern_bool_false",
82
+ line_number=line_number,
83
+ )
84
+ )
85
+
86
+ # Check for anti-pattern #2: Null with aws:MultiFactorAuthPresent = false
87
+ null_conditions = statement.condition.get("Null", {})
88
+ for key, value in null_conditions.items():
89
+ if key.lower() == "aws:multifactorauthpresent":
90
+ # Normalize value to list
91
+ values = value if isinstance(value, list) else [value]
92
+ # Convert to lowercase strings for comparison
93
+ values_lower = [str(v).lower() for v in values]
94
+
95
+ if "false" in values_lower or False in values:
96
+ issues.append(
97
+ ValidationIssue(
98
+ severity=self.get_severity(config),
99
+ message=(
100
+ "Dangerous MFA condition pattern detected. "
101
+ 'Using {"Null": {"aws:MultiFactorAuthPresent": "false"}} only checks if the key exists, '
102
+ "not whether MFA was actually used. This does not enforce MFA. "
103
+ 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement instead.'
104
+ ),
105
+ statement_sid=statement_sid,
106
+ statement_index=statement_idx,
107
+ issue_type="mfa_antipattern_null_false",
108
+ line_number=line_number,
109
+ )
110
+ )
111
+
112
+ return issues
@@ -10,6 +10,7 @@ from iam_validator.checks.utils.sensitive_action_matcher import (
10
10
  from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
11
11
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
12
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
13
+ from iam_validator.core.config.sensitive_actions import get_category_for_action
13
14
  from iam_validator.core.models import Statement, ValidationIssue
14
15
 
15
16
  if TYPE_CHECKING:
@@ -31,6 +32,69 @@ class SensitiveActionCheck(PolicyCheck):
31
32
  def default_severity(self) -> str:
32
33
  return "medium"
33
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
+
34
98
  async def execute(
35
99
  self,
36
100
  statement: Statement,
@@ -72,18 +136,26 @@ class SensitiveActionCheck(PolicyCheck):
72
136
  )
73
137
  message = message_template.format(actions=action_list)
74
138
 
75
- suggestion_text = config.config.get(
76
- "suggestion",
77
- "Add IAM conditions to limit when this action can be used. Consider: ABAC (ResourceTag OR RequestTag matching ${aws:PrincipalTag}), IP restrictions (aws:SourceIp), MFA requirements (aws:MultiFactorAuthPresent), or time-based restrictions (aws:CurrentTime)",
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
78
143
  )
79
- example = config.config.get("example", "")
80
144
 
81
145
  # Combine suggestion + example
82
- suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
146
+ suggestion = f"{suggestion_text}\n\nExample:\n{example}" if example else suggestion_text
147
+
148
+ # Determine severity based on the highest severity action in the list
149
+ # If single action, use its category severity
150
+ # If multiple actions, use the highest severity among them
151
+ severity = self.get_severity(config) # Default
152
+ if matched_actions:
153
+ # Get severity for first action (or highest if we want to be more sophisticated)
154
+ severity = self._get_severity_for_action(matched_actions[0], config)
83
155
 
84
156
  issues.append(
85
157
  ValidationIssue(
86
- severity=self.get_severity(config),
158
+ severity=severity,
87
159
  statement_sid=statement.sid,
88
160
  statement_index=statement_idx,
89
161
  issue_type="missing_condition",
@@ -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