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.
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +9 -3
- iam_validator/checks/action_condition_enforcement.py +164 -2
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +3 -1
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/sensitive_action.py +78 -6
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/validate.py +44 -11
- iam_validator/core/aws_fetcher.py +89 -52
- iam_validator/core/check_registry.py +165 -21
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +13 -15
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +5 -385
- iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
- iam_validator/core/config/defaults.py +187 -54
- iam_validator/core/config/sensitive_actions.py +620 -81
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/core/aws_global_conditions.py +0 -137
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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=
|
|
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
|