iam-policy-validator 1.4.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.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
- iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +20 -4
- iam_validator/checks/action_condition_enforcement.py +165 -8
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +24 -2
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/principal_validation.py +497 -3
- iam_validator/checks/sensitive_action.py +250 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/validate.py +72 -13
- iam_validator/core/aws_fetcher.py +114 -64
- iam_validator/core/check_registry.py +167 -29
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- 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 +155 -0
- iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
- iam_validator/core/config/defaults.py +523 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- 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_policy_validator-1.4.0.dist-info/RECORD +0 -56
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/checks/security_best_practices.py +0 -536
- iam_validator/core/aws_global_conditions.py +0 -137
- iam_validator/core/defaults.py +0 -393
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -34,9 +34,13 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
34
34
|
if not statement.condition:
|
|
35
35
|
return issues
|
|
36
36
|
|
|
37
|
+
# Check if global condition key warnings are enabled (default: True)
|
|
38
|
+
warn_on_global_keys = config.config.get("warn_on_global_condition_keys", True)
|
|
39
|
+
|
|
37
40
|
statement_sid = statement.sid
|
|
38
41
|
line_number = statement.line_number
|
|
39
42
|
actions = statement.get_actions()
|
|
43
|
+
resources = statement.get_resources()
|
|
40
44
|
|
|
41
45
|
# Extract all condition keys from all condition operators
|
|
42
46
|
for operator, conditions in statement.condition.items():
|
|
@@ -47,8 +51,9 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
47
51
|
if action == "*":
|
|
48
52
|
continue
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
# Validate against action and resource types
|
|
55
|
+
is_valid, error_msg, warning_msg = await fetcher.validate_condition_key(
|
|
56
|
+
action, condition_key, resources
|
|
52
57
|
)
|
|
53
58
|
|
|
54
59
|
if not is_valid:
|
|
@@ -66,5 +71,22 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
66
71
|
)
|
|
67
72
|
# Only report once per condition key (not per action)
|
|
68
73
|
break
|
|
74
|
+
elif warning_msg and warn_on_global_keys:
|
|
75
|
+
# Add warning for global condition keys with action-specific keys
|
|
76
|
+
# Only if warn_on_global_condition_keys is enabled
|
|
77
|
+
issues.append(
|
|
78
|
+
ValidationIssue(
|
|
79
|
+
severity="warning",
|
|
80
|
+
statement_sid=statement_sid,
|
|
81
|
+
statement_index=statement_idx,
|
|
82
|
+
issue_type="global_condition_key_with_action_specific",
|
|
83
|
+
message=warning_msg,
|
|
84
|
+
action=action,
|
|
85
|
+
condition_key=condition_key,
|
|
86
|
+
line_number=line_number,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
# Only report once per condition key (not per action)
|
|
90
|
+
break
|
|
69
91
|
|
|
70
92
|
return issues
|
|
@@ -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,67 @@
|
|
|
1
|
+
"""Full wildcard check - detects Action: '*' AND Resource: '*' together (critical security risk)."""
|
|
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 FullWildcardCheck(PolicyCheck):
|
|
9
|
+
"""Checks for both Action: '*' AND Resource: '*' which grants full administrative access."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def check_id(self) -> str:
|
|
13
|
+
return "full_wildcard"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def description(self) -> str:
|
|
17
|
+
return "Checks for both action and resource wildcards together (critical risk)"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def default_severity(self) -> str:
|
|
21
|
+
return "critical"
|
|
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 full 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
|
+
resources = statement.get_resources()
|
|
39
|
+
|
|
40
|
+
# Check for both wildcards together (CRITICAL)
|
|
41
|
+
if "*" in actions and "*" in resources:
|
|
42
|
+
message = config.config.get(
|
|
43
|
+
"message",
|
|
44
|
+
"Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
45
|
+
)
|
|
46
|
+
suggestion_text = config.config.get(
|
|
47
|
+
"suggestion",
|
|
48
|
+
"This grants full administrative access. Replace both wildcards with specific actions and resources to follow least-privilege principle",
|
|
49
|
+
)
|
|
50
|
+
example = config.config.get("example", "")
|
|
51
|
+
|
|
52
|
+
# Combine suggestion + example
|
|
53
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
54
|
+
|
|
55
|
+
issues.append(
|
|
56
|
+
ValidationIssue(
|
|
57
|
+
severity=self.get_severity(config),
|
|
58
|
+
statement_sid=statement.sid,
|
|
59
|
+
statement_index=statement_idx,
|
|
60
|
+
issue_type="security_risk",
|
|
61
|
+
message=message,
|
|
62
|
+
suggestion=suggestion,
|
|
63
|
+
line_number=statement.line_number,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return issues
|
|
@@ -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
|