iam-policy-validator 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (83) hide show
  1. iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
  2. iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
  3. iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +7 -0
  9. iam_validator/checks/__init__.py +43 -0
  10. iam_validator/checks/action_condition_enforcement.py +884 -0
  11. iam_validator/checks/action_resource_matching.py +441 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +92 -0
  14. iam_validator/checks/condition_type_mismatch.py +259 -0
  15. iam_validator/checks/full_wildcard.py +71 -0
  16. iam_validator/checks/mfa_condition_check.py +112 -0
  17. iam_validator/checks/policy_size.py +147 -0
  18. iam_validator/checks/policy_type_validation.py +305 -0
  19. iam_validator/checks/principal_validation.py +776 -0
  20. iam_validator/checks/resource_validation.py +138 -0
  21. iam_validator/checks/sensitive_action.py +254 -0
  22. iam_validator/checks/service_wildcard.py +107 -0
  23. iam_validator/checks/set_operator_validation.py +157 -0
  24. iam_validator/checks/sid_uniqueness.py +170 -0
  25. iam_validator/checks/utils/__init__.py +1 -0
  26. iam_validator/checks/utils/policy_level_checks.py +143 -0
  27. iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
  28. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  29. iam_validator/checks/wildcard_action.py +67 -0
  30. iam_validator/checks/wildcard_resource.py +135 -0
  31. iam_validator/commands/__init__.py +25 -0
  32. iam_validator/commands/analyze.py +531 -0
  33. iam_validator/commands/base.py +48 -0
  34. iam_validator/commands/cache.py +392 -0
  35. iam_validator/commands/download_services.py +255 -0
  36. iam_validator/commands/post_to_pr.py +86 -0
  37. iam_validator/commands/validate.py +600 -0
  38. iam_validator/core/__init__.py +14 -0
  39. iam_validator/core/access_analyzer.py +671 -0
  40. iam_validator/core/access_analyzer_report.py +640 -0
  41. iam_validator/core/aws_fetcher.py +940 -0
  42. iam_validator/core/check_registry.py +607 -0
  43. iam_validator/core/cli.py +134 -0
  44. iam_validator/core/condition_validators.py +626 -0
  45. iam_validator/core/config/__init__.py +81 -0
  46. iam_validator/core/config/aws_api.py +35 -0
  47. iam_validator/core/config/aws_global_conditions.py +160 -0
  48. iam_validator/core/config/category_suggestions.py +104 -0
  49. iam_validator/core/config/condition_requirements.py +155 -0
  50. iam_validator/core/config/config_loader.py +472 -0
  51. iam_validator/core/config/defaults.py +523 -0
  52. iam_validator/core/config/principal_requirements.py +421 -0
  53. iam_validator/core/config/sensitive_actions.py +672 -0
  54. iam_validator/core/config/service_principals.py +95 -0
  55. iam_validator/core/config/wildcards.py +124 -0
  56. iam_validator/core/constants.py +74 -0
  57. iam_validator/core/formatters/__init__.py +27 -0
  58. iam_validator/core/formatters/base.py +147 -0
  59. iam_validator/core/formatters/console.py +59 -0
  60. iam_validator/core/formatters/csv.py +170 -0
  61. iam_validator/core/formatters/enhanced.py +440 -0
  62. iam_validator/core/formatters/html.py +672 -0
  63. iam_validator/core/formatters/json.py +33 -0
  64. iam_validator/core/formatters/markdown.py +63 -0
  65. iam_validator/core/formatters/sarif.py +251 -0
  66. iam_validator/core/models.py +327 -0
  67. iam_validator/core/policy_checks.py +656 -0
  68. iam_validator/core/policy_loader.py +396 -0
  69. iam_validator/core/pr_commenter.py +424 -0
  70. iam_validator/core/report.py +872 -0
  71. iam_validator/integrations/__init__.py +28 -0
  72. iam_validator/integrations/github_integration.py +815 -0
  73. iam_validator/integrations/ms_teams.py +442 -0
  74. iam_validator/sdk/__init__.py +187 -0
  75. iam_validator/sdk/arn_matching.py +382 -0
  76. iam_validator/sdk/context.py +222 -0
  77. iam_validator/sdk/exceptions.py +48 -0
  78. iam_validator/sdk/helpers.py +177 -0
  79. iam_validator/sdk/policy_utils.py +425 -0
  80. iam_validator/sdk/shortcuts.py +283 -0
  81. iam_validator/utils/__init__.py +31 -0
  82. iam_validator/utils/cache.py +105 -0
  83. iam_validator/utils/regex.py +206 -0
@@ -0,0 +1,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