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.
- iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -0
- 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/config_loader.py +472 -0
- 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/constants.py +74 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +440 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +382 -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
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Condition Key and Value Validators.
|
|
3
|
+
|
|
4
|
+
This module provides validators for IAM policy condition operators, keys, and values.
|
|
5
|
+
Based on AWS IAM Policy Elements Reference:
|
|
6
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
7
|
+
|
|
8
|
+
Supports:
|
|
9
|
+
- All standard IAM condition operators (String, Numeric, Date, Bool, Binary, IP, ARN)
|
|
10
|
+
- IfExists variants for all operators (e.g., StringEqualsIfExists)
|
|
11
|
+
- Set operators (ForAllValues, ForAnyValue) for multivalued keys
|
|
12
|
+
- Null operator for key existence checking
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ipaddress
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
# IAM Condition Operators mapped to their expected value types
|
|
20
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
21
|
+
CONDITION_OPERATORS = {
|
|
22
|
+
# String operators - Case-sensitive and case-insensitive matching with wildcard support
|
|
23
|
+
"StringEquals": "String",
|
|
24
|
+
"StringNotEquals": "String",
|
|
25
|
+
"StringEqualsIgnoreCase": "String",
|
|
26
|
+
"StringNotEqualsIgnoreCase": "String",
|
|
27
|
+
"StringLike": "String", # Supports * and ? wildcards
|
|
28
|
+
"StringNotLike": "String", # Supports * and ? wildcards
|
|
29
|
+
# Numeric operators - Integer and decimal comparisons
|
|
30
|
+
"NumericEquals": "Numeric",
|
|
31
|
+
"NumericNotEquals": "Numeric",
|
|
32
|
+
"NumericLessThan": "Numeric",
|
|
33
|
+
"NumericLessThanEquals": "Numeric",
|
|
34
|
+
"NumericGreaterThan": "Numeric",
|
|
35
|
+
"NumericGreaterThanEquals": "Numeric",
|
|
36
|
+
# Date operators - W3C ISO 8601 and UNIX epoch time formats
|
|
37
|
+
"DateEquals": "Date",
|
|
38
|
+
"DateNotEquals": "Date",
|
|
39
|
+
"DateLessThan": "Date",
|
|
40
|
+
"DateLessThanEquals": "Date",
|
|
41
|
+
"DateGreaterThan": "Date",
|
|
42
|
+
"DateGreaterThanEquals": "Date",
|
|
43
|
+
# Boolean operators
|
|
44
|
+
"Bool": "Bool",
|
|
45
|
+
# Binary operators - Base-64 encoded byte-for-byte comparison
|
|
46
|
+
"BinaryEquals": "Binary",
|
|
47
|
+
# IP address operators - IPv4 and IPv6 CIDR notation
|
|
48
|
+
"IpAddress": "IPAddress",
|
|
49
|
+
"NotIpAddress": "IPAddress",
|
|
50
|
+
# ARN operators - Amazon Resource Name matching with wildcard support
|
|
51
|
+
"ArnEquals": "ARN",
|
|
52
|
+
"ArnLike": "ARN",
|
|
53
|
+
"ArnNotEquals": "ARN",
|
|
54
|
+
"ArnNotLike": "ARN",
|
|
55
|
+
# Null check operator - Key existence validation
|
|
56
|
+
"Null": "Bool",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Set operator prefixes for multivalued condition keys
|
|
60
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_multi-value-conditions.html
|
|
61
|
+
SET_OPERATOR_PREFIXES = ["ForAllValues", "ForAnyValue"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def normalize_operator(operator: str) -> tuple[str, str | None, str | None]:
|
|
65
|
+
"""
|
|
66
|
+
Normalize condition operator, handling IfExists and ForAllValues/ForAnyValue prefixes.
|
|
67
|
+
|
|
68
|
+
AWS IAM supports several operator modifiers:
|
|
69
|
+
- IfExists suffix: Allows condition to pass if key is missing
|
|
70
|
+
- ForAllValues/ForAnyValue prefixes: Set operators for multivalued keys
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
operator: Raw operator from policy (e.g., "StringEqualsIfExists", "ForAllValues:StringLike")
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Tuple of (base_operator, expected_type, set_prefix) where:
|
|
77
|
+
- base_operator: Normalized operator name (e.g., "StringEquals")
|
|
78
|
+
- expected_type: Expected value type (e.g., "String") or None if unknown
|
|
79
|
+
- set_prefix: Set operator prefix ("ForAllValues"/"ForAnyValue") or None
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> normalize_operator("StringEquals")
|
|
83
|
+
("StringEquals", "String", None)
|
|
84
|
+
>>> normalize_operator("StringEqualsIfExists")
|
|
85
|
+
("StringEquals", "String", None)
|
|
86
|
+
>>> normalize_operator("ForAllValues:StringLike")
|
|
87
|
+
("StringLike", "String", "ForAllValues")
|
|
88
|
+
>>> normalize_operator("ForAnyValue:NumericLessThanIfExists")
|
|
89
|
+
("NumericLessThan", "Numeric", "ForAnyValue")
|
|
90
|
+
|
|
91
|
+
Reference:
|
|
92
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
93
|
+
"""
|
|
94
|
+
set_prefix = None
|
|
95
|
+
cleaned = operator
|
|
96
|
+
|
|
97
|
+
# Remove ForAllValues/ForAnyValue prefix
|
|
98
|
+
if ":" in operator:
|
|
99
|
+
parts = operator.split(":", 1)
|
|
100
|
+
if parts[0] in SET_OPERATOR_PREFIXES:
|
|
101
|
+
set_prefix = parts[0]
|
|
102
|
+
cleaned = parts[1]
|
|
103
|
+
|
|
104
|
+
# Remove IfExists suffix
|
|
105
|
+
if cleaned.endswith("IfExists"):
|
|
106
|
+
cleaned = cleaned[:-8] # Remove "IfExists"
|
|
107
|
+
|
|
108
|
+
# Look up the base operator (case-insensitive)
|
|
109
|
+
for base_op, op_type in CONDITION_OPERATORS.items():
|
|
110
|
+
if cleaned.lower() == base_op.lower():
|
|
111
|
+
return base_op, op_type, set_prefix
|
|
112
|
+
|
|
113
|
+
return operator, None, set_prefix
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def translate_type(doc_type: str) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Translate documentation type names to normalized types.
|
|
119
|
+
|
|
120
|
+
AWS documentation uses various type names across different services.
|
|
121
|
+
This function normalizes them to standard IAM condition types.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
doc_type: Type from AWS docs (e.g., "Long", "ARN", "Boolean", "ArrayOfString")
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Normalized type string (String, Numeric, Bool, Date, ARN, IPAddress, Binary)
|
|
128
|
+
|
|
129
|
+
Examples:
|
|
130
|
+
>>> translate_type("Long")
|
|
131
|
+
"Numeric"
|
|
132
|
+
>>> translate_type("Boolean")
|
|
133
|
+
"Bool"
|
|
134
|
+
>>> translate_type("ArrayOfString")
|
|
135
|
+
"String"
|
|
136
|
+
>>> translate_type("Arn")
|
|
137
|
+
"ARN"
|
|
138
|
+
|
|
139
|
+
Reference:
|
|
140
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
141
|
+
"""
|
|
142
|
+
type_map = {
|
|
143
|
+
# ARN types
|
|
144
|
+
"ARN": "ARN",
|
|
145
|
+
"Arn": "ARN",
|
|
146
|
+
# Boolean types
|
|
147
|
+
"Bool": "Bool",
|
|
148
|
+
"Boolean": "Bool",
|
|
149
|
+
# Date types
|
|
150
|
+
"Date": "Date",
|
|
151
|
+
# Numeric types
|
|
152
|
+
"Long": "Numeric",
|
|
153
|
+
"Numeric": "Numeric",
|
|
154
|
+
"Number": "Numeric",
|
|
155
|
+
# String types
|
|
156
|
+
"String": "String",
|
|
157
|
+
"string": "String",
|
|
158
|
+
"ArrayOfString": "String",
|
|
159
|
+
# IP Address types
|
|
160
|
+
"IPAddress": "IPAddress",
|
|
161
|
+
"Ip": "IPAddress",
|
|
162
|
+
# Binary types
|
|
163
|
+
"Binary": "Binary",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return type_map.get(doc_type, doc_type)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_value_for_type(value_type: str, values: list[Any]) -> tuple[bool, str | None]:
|
|
170
|
+
"""
|
|
171
|
+
Validate that condition values match the expected type.
|
|
172
|
+
|
|
173
|
+
Supports all AWS IAM condition value types with comprehensive validation:
|
|
174
|
+
- String: Any text value
|
|
175
|
+
- Numeric: Integers and decimals
|
|
176
|
+
- Date: W3C ISO 8601 format or UNIX epoch timestamps
|
|
177
|
+
- Bool: true/false (case-insensitive)
|
|
178
|
+
- Binary: Base-64 encoded strings
|
|
179
|
+
- IPAddress: IPv4/IPv6 with optional CIDR notation
|
|
180
|
+
- ARN: Amazon Resource Names
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
value_type: Expected type (String, ARN, Bool, Date, IPAddress, Numeric, Binary)
|
|
184
|
+
values: List of values from the condition
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (is_valid, error_message)
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
>>> validate_value_for_type("Date", ["2019-07-16T12:00:00Z"])
|
|
191
|
+
(True, None)
|
|
192
|
+
>>> validate_value_for_type("Date", ["1563278400"]) # UNIX epoch
|
|
193
|
+
(True, None)
|
|
194
|
+
>>> validate_value_for_type("Numeric", ["123.45"])
|
|
195
|
+
(True, None)
|
|
196
|
+
>>> validate_value_for_type("IPAddress", ["2001:DB8::/32"])
|
|
197
|
+
(True, None)
|
|
198
|
+
>>> validate_value_for_type("Bool", ["invalid"])
|
|
199
|
+
(False, "Expected Bool value (true/false) but got: invalid")
|
|
200
|
+
|
|
201
|
+
Reference:
|
|
202
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
203
|
+
"""
|
|
204
|
+
# Normalize the type
|
|
205
|
+
value_type = translate_type(value_type)
|
|
206
|
+
|
|
207
|
+
if value_type not in ["ARN", "Binary", "Bool", "Date", "IPAddress", "Numeric", "String"]:
|
|
208
|
+
return False, f"Unknown type: {value_type}"
|
|
209
|
+
|
|
210
|
+
for value in values:
|
|
211
|
+
# Convert booleans to lowercase strings
|
|
212
|
+
if isinstance(value, bool):
|
|
213
|
+
value = str(value).lower()
|
|
214
|
+
|
|
215
|
+
# Convert to string
|
|
216
|
+
value_str = str(value)
|
|
217
|
+
|
|
218
|
+
# Type-specific validation
|
|
219
|
+
is_valid, error = _validate_single_value(value_type, value_str)
|
|
220
|
+
if not is_valid:
|
|
221
|
+
return False, error
|
|
222
|
+
|
|
223
|
+
return True, None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _validate_single_value(value_type: str, value_str: str) -> tuple[bool, str | None]:
|
|
227
|
+
"""
|
|
228
|
+
Validate a single value against its expected type.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
value_type: Expected type (normalized)
|
|
232
|
+
value_str: String representation of value
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Tuple of (is_valid, error_message)
|
|
236
|
+
"""
|
|
237
|
+
if value_type == "ARN":
|
|
238
|
+
# ARN format: arn:partition:service:region:account-id:resource
|
|
239
|
+
# Wildcards are allowed in ARN values
|
|
240
|
+
arn_pattern = r"^arn:[^:]*:[^:]*:[^:]*:[^:]*:.+$"
|
|
241
|
+
if not re.match(arn_pattern, value_str):
|
|
242
|
+
return (
|
|
243
|
+
False,
|
|
244
|
+
f"Expected ARN value (arn:aws:service:region:account:resource) but got: {value_str}",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
elif value_type == "Binary":
|
|
248
|
+
# Base-64 encoded string validation
|
|
249
|
+
binary_pattern = r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"
|
|
250
|
+
if not re.match(binary_pattern, value_str):
|
|
251
|
+
return False, f"Expected Binary value (base64-encoded string) but got: {value_str}"
|
|
252
|
+
|
|
253
|
+
elif value_type == "Bool":
|
|
254
|
+
# Boolean: true or false (case-insensitive)
|
|
255
|
+
if value_str.lower() not in ["true", "false"]:
|
|
256
|
+
return False, f"Expected Bool value (true/false) but got: {value_str}"
|
|
257
|
+
|
|
258
|
+
elif value_type == "Date":
|
|
259
|
+
# Date: W3C ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ) or UNIX epoch timestamp
|
|
260
|
+
iso_pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"
|
|
261
|
+
epoch_pattern = r"^\d+$"
|
|
262
|
+
|
|
263
|
+
if not (re.match(iso_pattern, value_str) or re.match(epoch_pattern, value_str)):
|
|
264
|
+
return (
|
|
265
|
+
False,
|
|
266
|
+
f"Expected Date value (2019-07-16T12:00:00Z or UNIX epoch timestamp) but got: {value_str}",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
elif value_type == "IPAddress":
|
|
270
|
+
# IP Address: IPv4 or IPv6 with optional CIDR notation
|
|
271
|
+
# Use Python's ipaddress module for robust validation
|
|
272
|
+
try:
|
|
273
|
+
# Try parsing as network (with CIDR) or address
|
|
274
|
+
try:
|
|
275
|
+
ipaddress.ip_network(value_str, strict=False)
|
|
276
|
+
except ValueError:
|
|
277
|
+
ipaddress.ip_address(value_str)
|
|
278
|
+
except ValueError:
|
|
279
|
+
return (
|
|
280
|
+
False,
|
|
281
|
+
f"Expected IPAddress value (203.0.113.0/24 or 2001:DB8::/32) but got: {value_str}",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
elif value_type == "Numeric":
|
|
285
|
+
# Numeric: Integers and decimals (positive and negative)
|
|
286
|
+
numeric_pattern = r"^-?\d+(\.\d+)?$"
|
|
287
|
+
if not re.match(numeric_pattern, value_str):
|
|
288
|
+
return False, f"Expected Numeric value (integer or decimal) but got: {value_str}"
|
|
289
|
+
|
|
290
|
+
elif value_type == "String":
|
|
291
|
+
# Strings can be any value
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
return True, None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def is_condition_key_match(documented_key: str, policy_key: str) -> bool:
|
|
298
|
+
"""
|
|
299
|
+
Determine if a documented condition key matches a policy condition key.
|
|
300
|
+
|
|
301
|
+
Handles various wildcard and placeholder patterns used in AWS documentation:
|
|
302
|
+
- ${...} patterns (e.g., ${TagKey})
|
|
303
|
+
- <...> patterns (e.g., <key>)
|
|
304
|
+
- Literal tag-key placeholders
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
documented_key: Key from AWS documentation (may contain placeholders)
|
|
308
|
+
policy_key: Key from the actual policy
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True if they match
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
>>> is_condition_key_match("s3:prefix", "s3:prefix")
|
|
315
|
+
True
|
|
316
|
+
>>> is_condition_key_match("s3:ExistingObjectTag/<key>", "s3:ExistingObjectTag/backup")
|
|
317
|
+
True
|
|
318
|
+
>>> is_condition_key_match("license-manager:ResourceTag/${TagKey}", "license-manager:ResourceTag/Environment")
|
|
319
|
+
True
|
|
320
|
+
>>> is_condition_key_match("secretsmanager:ResourceTag/tag-key", "secretsmanager:ResourceTag/Production")
|
|
321
|
+
True
|
|
322
|
+
|
|
323
|
+
Reference:
|
|
324
|
+
AWS service authorization documentation for condition key patterns
|
|
325
|
+
"""
|
|
326
|
+
# Normalize both to lowercase for comparison
|
|
327
|
+
doc_key_lower = documented_key.lower()
|
|
328
|
+
policy_key_lower = policy_key.lower()
|
|
329
|
+
|
|
330
|
+
# Exact match
|
|
331
|
+
if doc_key_lower == policy_key_lower:
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
# Check for ${...} pattern (e.g., license-manager:ResourceTag/${TagKey})
|
|
335
|
+
if "${" in doc_key_lower:
|
|
336
|
+
prefix = doc_key_lower.split("${")[0]
|
|
337
|
+
if policy_key_lower.startswith(prefix):
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
# Check for <...> pattern (e.g., s3:ExistingObjectTag/<key>)
|
|
341
|
+
if "<" in doc_key_lower:
|
|
342
|
+
prefix = doc_key_lower.split("<")[0]
|
|
343
|
+
if policy_key_lower.startswith(prefix):
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
# Check for tag-key literal (e.g., secretsmanager:ResourceTag/tag-key)
|
|
347
|
+
if "tag-key" in doc_key_lower:
|
|
348
|
+
prefix = doc_key_lower.split("tag-key")[0]
|
|
349
|
+
if policy_key_lower.startswith(prefix):
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def is_negated_operator(operator: str) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Determine if a condition operator is negated (NotEquals, NotLike, etc.).
|
|
358
|
+
|
|
359
|
+
Negated operators have special behavior with missing keys:
|
|
360
|
+
- Standard operators: Missing keys evaluate to false (condition fails)
|
|
361
|
+
- Negated operators: Missing keys evaluate to true (condition passes)
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
operator: Condition operator to check
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
True if the operator is negated
|
|
368
|
+
|
|
369
|
+
Examples:
|
|
370
|
+
>>> is_negated_operator("StringNotEquals")
|
|
371
|
+
True
|
|
372
|
+
>>> is_negated_operator("StringEquals")
|
|
373
|
+
False
|
|
374
|
+
>>> is_negated_operator("ForAllValues:StringNotLike")
|
|
375
|
+
True
|
|
376
|
+
>>> is_negated_operator("ArnNotEqualsIfExists")
|
|
377
|
+
True
|
|
378
|
+
>>> is_negated_operator("NotIpAddress")
|
|
379
|
+
True
|
|
380
|
+
|
|
381
|
+
Reference:
|
|
382
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
383
|
+
"""
|
|
384
|
+
# Remove set operator prefix if present
|
|
385
|
+
cleaned = operator
|
|
386
|
+
if ":" in operator:
|
|
387
|
+
parts = operator.split(":", 1)
|
|
388
|
+
if parts[0] in SET_OPERATOR_PREFIXES:
|
|
389
|
+
cleaned = parts[1]
|
|
390
|
+
|
|
391
|
+
# Remove IfExists suffix
|
|
392
|
+
if cleaned.endswith("IfExists"):
|
|
393
|
+
cleaned = cleaned[:-8]
|
|
394
|
+
|
|
395
|
+
# Check if operator contains "Not" (case-insensitive)
|
|
396
|
+
return "not" in cleaned.lower()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def is_operator_supports_policy_variables(operator: str) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Determine if a condition operator supports policy variables.
|
|
402
|
+
|
|
403
|
+
Policy variables allow dynamic values in conditions (e.g., ${aws:username}).
|
|
404
|
+
|
|
405
|
+
Operators supporting policy variables:
|
|
406
|
+
- String operators (all variants)
|
|
407
|
+
- ARN operators (all variants)
|
|
408
|
+
- Bool operator
|
|
409
|
+
|
|
410
|
+
Operators NOT supporting policy variables:
|
|
411
|
+
- Numeric operators
|
|
412
|
+
- Date operators
|
|
413
|
+
- Binary operators
|
|
414
|
+
- IP address operators
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
operator: Condition operator to check
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
True if the operator supports policy variables
|
|
421
|
+
|
|
422
|
+
Examples:
|
|
423
|
+
>>> is_operator_supports_policy_variables("StringEquals")
|
|
424
|
+
True
|
|
425
|
+
>>> is_operator_supports_policy_variables("NumericEquals")
|
|
426
|
+
False
|
|
427
|
+
>>> is_operator_supports_policy_variables("ArnLike")
|
|
428
|
+
True
|
|
429
|
+
>>> is_operator_supports_policy_variables("Bool")
|
|
430
|
+
True
|
|
431
|
+
>>> is_operator_supports_policy_variables("DateLessThan")
|
|
432
|
+
False
|
|
433
|
+
|
|
434
|
+
Reference:
|
|
435
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html
|
|
436
|
+
"""
|
|
437
|
+
base_op, _, _ = normalize_operator(operator)
|
|
438
|
+
|
|
439
|
+
# Operators that support policy variables
|
|
440
|
+
policy_var_operators = {
|
|
441
|
+
# String operators
|
|
442
|
+
"StringEquals",
|
|
443
|
+
"StringNotEquals",
|
|
444
|
+
"StringEqualsIgnoreCase",
|
|
445
|
+
"StringNotEqualsIgnoreCase",
|
|
446
|
+
"StringLike",
|
|
447
|
+
"StringNotLike",
|
|
448
|
+
# ARN operators
|
|
449
|
+
"ArnEquals",
|
|
450
|
+
"ArnLike",
|
|
451
|
+
"ArnNotEquals",
|
|
452
|
+
"ArnNotLike",
|
|
453
|
+
# Bool operator
|
|
454
|
+
"Bool",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return base_op in policy_var_operators
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def is_operator_supports_wildcards(operator: str) -> bool:
|
|
461
|
+
"""
|
|
462
|
+
Determine if a condition operator supports wildcard characters (* and ?).
|
|
463
|
+
|
|
464
|
+
Wildcard support:
|
|
465
|
+
- StringLike/StringNotLike: Yes (* for multi-char, ? for single-char)
|
|
466
|
+
- ArnLike/ArnNotLike/ArnEquals/ArnNotEquals: Yes
|
|
467
|
+
- All other operators: No
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
operator: Condition operator to check
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
True if the operator supports wildcards
|
|
474
|
+
|
|
475
|
+
Examples:
|
|
476
|
+
>>> is_operator_supports_wildcards("StringLike")
|
|
477
|
+
True
|
|
478
|
+
>>> is_operator_supports_wildcards("StringEquals")
|
|
479
|
+
False
|
|
480
|
+
>>> is_operator_supports_wildcards("ArnLike")
|
|
481
|
+
True
|
|
482
|
+
>>> is_operator_supports_wildcards("NumericEquals")
|
|
483
|
+
False
|
|
484
|
+
|
|
485
|
+
Reference:
|
|
486
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
487
|
+
"""
|
|
488
|
+
base_op, _, _ = normalize_operator(operator)
|
|
489
|
+
|
|
490
|
+
# Operators that support wildcards
|
|
491
|
+
wildcard_operators = {
|
|
492
|
+
"StringLike",
|
|
493
|
+
"StringNotLike",
|
|
494
|
+
"ArnEquals",
|
|
495
|
+
"ArnLike",
|
|
496
|
+
"ArnNotEquals",
|
|
497
|
+
"ArnNotLike",
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return base_op in wildcard_operators
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def get_operator_description(operator: str) -> str:
|
|
504
|
+
"""
|
|
505
|
+
Get a human-readable description of what a condition operator does.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
operator: Condition operator
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Description of the operator's behavior
|
|
512
|
+
|
|
513
|
+
Examples:
|
|
514
|
+
>>> get_operator_description("StringEquals")
|
|
515
|
+
"Case-sensitive string matching"
|
|
516
|
+
>>> get_operator_description("NumericLessThan")
|
|
517
|
+
"Numeric less-than comparison"
|
|
518
|
+
>>> get_operator_description("ForAllValues:StringLike")
|
|
519
|
+
"All values must match pattern (case-sensitive with wildcards)"
|
|
520
|
+
|
|
521
|
+
Reference:
|
|
522
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
523
|
+
"""
|
|
524
|
+
base_op, _, set_prefix = normalize_operator(operator)
|
|
525
|
+
|
|
526
|
+
descriptions = {
|
|
527
|
+
"StringEquals": "Case-sensitive string matching",
|
|
528
|
+
"StringNotEquals": "Case-sensitive string non-matching",
|
|
529
|
+
"StringEqualsIgnoreCase": "Case-insensitive string matching",
|
|
530
|
+
"StringNotEqualsIgnoreCase": "Case-insensitive string non-matching",
|
|
531
|
+
"StringLike": "Case-sensitive pattern matching (supports * and ? wildcards)",
|
|
532
|
+
"StringNotLike": "Case-sensitive pattern non-matching (supports * and ? wildcards)",
|
|
533
|
+
"NumericEquals": "Numeric equality comparison",
|
|
534
|
+
"NumericNotEquals": "Numeric inequality comparison",
|
|
535
|
+
"NumericLessThan": "Numeric less-than comparison",
|
|
536
|
+
"NumericLessThanEquals": "Numeric less-than-or-equal comparison",
|
|
537
|
+
"NumericGreaterThan": "Numeric greater-than comparison",
|
|
538
|
+
"NumericGreaterThanEquals": "Numeric greater-than-or-equal comparison",
|
|
539
|
+
"DateEquals": "Date/time equality comparison",
|
|
540
|
+
"DateNotEquals": "Date/time inequality comparison",
|
|
541
|
+
"DateLessThan": "Date/time before comparison",
|
|
542
|
+
"DateLessThanEquals": "Date/time at-or-before comparison",
|
|
543
|
+
"DateGreaterThan": "Date/time after comparison",
|
|
544
|
+
"DateGreaterThanEquals": "Date/time at-or-after comparison",
|
|
545
|
+
"Bool": "Boolean value matching",
|
|
546
|
+
"BinaryEquals": "Binary data byte-for-byte comparison",
|
|
547
|
+
"IpAddress": "IP address within CIDR range",
|
|
548
|
+
"NotIpAddress": "IP address outside CIDR range",
|
|
549
|
+
"ArnEquals": "ARN matching (supports wildcards)",
|
|
550
|
+
"ArnLike": "ARN pattern matching (supports wildcards)",
|
|
551
|
+
"ArnNotEquals": "ARN non-matching (supports wildcards)",
|
|
552
|
+
"ArnNotLike": "ARN pattern non-matching (supports wildcards)",
|
|
553
|
+
"Null": "Key existence check",
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
desc = descriptions.get(base_op, "Unknown operator")
|
|
557
|
+
|
|
558
|
+
# Add set operator prefix description
|
|
559
|
+
if set_prefix == "ForAllValues":
|
|
560
|
+
desc = f"All values must match: {desc}"
|
|
561
|
+
elif set_prefix == "ForAnyValue":
|
|
562
|
+
desc = f"At least one value must match: {desc}"
|
|
563
|
+
|
|
564
|
+
return desc
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def is_multivalued_context_key(condition_key: str) -> bool:
|
|
568
|
+
"""
|
|
569
|
+
Determine if a condition key is multivalued (can have multiple values in request context).
|
|
570
|
+
|
|
571
|
+
Multivalued context keys include:
|
|
572
|
+
- aws:TagKeys (list of tag keys being applied)
|
|
573
|
+
- aws:PrincipalOrgPaths (organization paths)
|
|
574
|
+
- Service-specific multivalued keys (e.g., s3:x-amz-grant-*)
|
|
575
|
+
|
|
576
|
+
Single-valued context keys include:
|
|
577
|
+
- aws:SourceIp (single IP address)
|
|
578
|
+
- aws:userid (single user ID)
|
|
579
|
+
- aws:username (single username)
|
|
580
|
+
- Most global condition keys
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
condition_key: The condition key to check
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
True if the key is multivalued
|
|
587
|
+
|
|
588
|
+
Examples:
|
|
589
|
+
>>> is_multivalued_context_key("aws:TagKeys")
|
|
590
|
+
True
|
|
591
|
+
>>> is_multivalued_context_key("aws:SourceIp")
|
|
592
|
+
False
|
|
593
|
+
>>> is_multivalued_context_key("aws:PrincipalTag/Department")
|
|
594
|
+
False
|
|
595
|
+
|
|
596
|
+
Reference:
|
|
597
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html
|
|
598
|
+
|
|
599
|
+
Note:
|
|
600
|
+
This function identifies commonly known multivalued keys. For complete validation,
|
|
601
|
+
consult AWS service documentation or use the AWSServiceFetcher to look up key metadata.
|
|
602
|
+
"""
|
|
603
|
+
# Normalize to lowercase for comparison
|
|
604
|
+
key_lower = condition_key.lower()
|
|
605
|
+
|
|
606
|
+
# Known multivalued global condition keys
|
|
607
|
+
multivalued_keys = {
|
|
608
|
+
"aws:tagkeys", # List of tag keys being applied/modified
|
|
609
|
+
"aws:principalorgpaths", # Organization paths for the principal
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# Check exact matches
|
|
613
|
+
if key_lower in multivalued_keys:
|
|
614
|
+
return True
|
|
615
|
+
|
|
616
|
+
# Service-specific multivalued patterns
|
|
617
|
+
# S3 grant headers are multivalued
|
|
618
|
+
if key_lower.startswith("s3:x-amz-grant-"):
|
|
619
|
+
return True
|
|
620
|
+
|
|
621
|
+
# EC2 resource tags in requests can be multivalued
|
|
622
|
+
if "ec2:" in key_lower and ":resourcetag/" in key_lower:
|
|
623
|
+
return True
|
|
624
|
+
|
|
625
|
+
# Most condition keys are single-valued by default
|
|
626
|
+
return False
|