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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.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 +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,380 @@
1
+ """Validation logic for AWS actions, condition keys, and resources.
2
+
3
+ This module provides comprehensive validation for IAM policy elements
4
+ including actions, condition keys, and ARN formats.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ from iam_validator.core.aws_service.parsers import ServiceParser
12
+ from iam_validator.core.models import ServiceDetail
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class ConditionKeyValidationResult:
19
+ """Result of condition key validation.
20
+
21
+ Attributes:
22
+ is_valid: True if the condition key is valid for the action
23
+ error_message: Short error message if invalid (shown prominently)
24
+ warning_message: Warning message if valid but not recommended
25
+ suggestion: Detailed suggestion with valid keys (shown in collapsible section)
26
+ """
27
+
28
+ is_valid: bool
29
+ error_message: str | None = None
30
+ warning_message: str | None = None
31
+ suggestion: str | None = None
32
+
33
+
34
+ class ServiceValidator:
35
+ """Validates AWS actions, condition keys, and resources.
36
+
37
+ This class provides validation logic for IAM policy elements,
38
+ working with AWS service definitions to ensure correctness.
39
+ """
40
+
41
+ def __init__(self, parser: ServiceParser | None = None) -> None:
42
+ """Initialize validator with parser.
43
+
44
+ Args:
45
+ parser: Optional ServiceParser instance (creates new one if not provided)
46
+ """
47
+ self._parser = parser or ServiceParser()
48
+
49
+ async def validate_action(
50
+ self,
51
+ action: str,
52
+ service_detail: ServiceDetail,
53
+ allow_wildcards: bool = True,
54
+ ) -> tuple[bool, str | None, bool]:
55
+ """Validate IAM action against service definition.
56
+
57
+ Supports:
58
+ - Exact actions: s3:GetObject
59
+ - Full wildcards: s3:*
60
+ - Partial wildcards: s3:Get*, s3:*Object, s3:*Get*
61
+
62
+ Args:
63
+ action: Full action string (e.g., "s3:GetObject")
64
+ service_detail: Service definition to validate against
65
+ allow_wildcards: Whether to allow wildcard actions
66
+
67
+ Returns:
68
+ Tuple of (is_valid, error_message, is_wildcard)
69
+
70
+ Example:
71
+ >>> validator = ServiceValidator()
72
+ >>> service = await fetcher.fetch_service_by_name("s3")
73
+ >>> is_valid, error, is_wildcard = await validator.validate_action(
74
+ ... "s3:GetObject", service
75
+ ... )
76
+ """
77
+ try:
78
+ service_prefix, action_name = self._parser.parse_action(action)
79
+
80
+ # Quick wildcard check
81
+ is_wildcard = self._parser.is_wildcard_action(action_name)
82
+
83
+ # Handle full wildcard
84
+ if action_name == "*":
85
+ if allow_wildcards:
86
+ return True, None, True
87
+ return False, "Wildcard actions are not allowed", True
88
+
89
+ # Get available actions from service
90
+ available_actions = list(service_detail.actions.keys())
91
+
92
+ # Handle partial wildcards (e.g., Get*, *Object, Describe*)
93
+ if is_wildcard:
94
+ if not allow_wildcards:
95
+ return False, "Wildcard actions are not allowed", True
96
+
97
+ has_matches, _ = self._parser.match_wildcard_action(action_name, available_actions)
98
+
99
+ if has_matches:
100
+ # Wildcard is valid and matches at least one action
101
+ return True, None, True
102
+
103
+ # Wildcard doesn't match any actions
104
+ return (
105
+ False,
106
+ f"Action pattern `{action_name}` does not match any actions in service `{service_prefix}`",
107
+ True,
108
+ )
109
+
110
+ # Check if exact action exists (case-insensitive)
111
+ action_exists = any(a.lower() == action_name.lower() for a in available_actions)
112
+
113
+ if action_exists:
114
+ return True, None, False
115
+
116
+ # Suggest similar actions
117
+ similar = [f"`{a}`" for a in available_actions if action_name.lower() in a.lower()][:3]
118
+
119
+ suggestion = f" Did you mean: {', '.join(similar)}?" if similar else ""
120
+ return (
121
+ False,
122
+ f"Action `{action_name}` not found in service `{service_prefix}`.{suggestion}",
123
+ False,
124
+ )
125
+
126
+ except ValueError as e:
127
+ return False, str(e), False
128
+ except Exception as e: # pylint: disable=broad-exception-caught
129
+ logger.error(f"Error validating action {action}: {e}")
130
+ return False, f"Failed to validate action: {e!s}", False
131
+
132
+ async def validate_condition_key(
133
+ self,
134
+ action: str,
135
+ condition_key: str,
136
+ service_detail: ServiceDetail,
137
+ resources: list[str] | None = None,
138
+ ) -> ConditionKeyValidationResult:
139
+ """Validate condition key against action and optionally resource types.
140
+
141
+ Args:
142
+ action: IAM action (e.g., "s3:GetObject")
143
+ condition_key: Condition key to validate (e.g., "s3:prefix")
144
+ service_detail: Service definition containing actions and resources
145
+ resources: Optional list of resource ARNs to validate against
146
+
147
+ Returns:
148
+ ConditionKeyValidationResult with validation details
149
+
150
+ Example:
151
+ >>> validator = ServiceValidator()
152
+ >>> service = await fetcher.fetch_service_by_name("s3")
153
+ >>> result = await validator.validate_condition_key(
154
+ ... "s3:GetObject", "s3:prefix", service
155
+ ... )
156
+ """
157
+ try:
158
+ from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
159
+ get_global_conditions,
160
+ )
161
+
162
+ _, action_name = self._parser.parse_action(action)
163
+
164
+ # Check if it's a global condition key
165
+ is_global_key = False
166
+ if condition_key.startswith("aws:"):
167
+ global_conditions = get_global_conditions()
168
+ if global_conditions.is_valid_global_key(condition_key):
169
+ is_global_key = True
170
+ else:
171
+ return ConditionKeyValidationResult(
172
+ is_valid=False,
173
+ error_message=f"Invalid AWS global condition key: `{condition_key}`.",
174
+ )
175
+
176
+ # Check service-specific condition keys
177
+ if condition_key in service_detail.condition_keys:
178
+ return ConditionKeyValidationResult(is_valid=True)
179
+
180
+ # Check action-specific condition keys
181
+ if action_name in service_detail.actions:
182
+ action_detail = service_detail.actions[action_name]
183
+ if (
184
+ action_detail.action_condition_keys
185
+ and condition_key in action_detail.action_condition_keys
186
+ ):
187
+ return ConditionKeyValidationResult(is_valid=True)
188
+
189
+ # Check resource-specific condition keys
190
+ # Get resource types required by this action
191
+ if resources and action_detail.resources:
192
+ for res_req in action_detail.resources:
193
+ resource_name = res_req.get("Name", "")
194
+ if not resource_name:
195
+ continue
196
+
197
+ # Look up resource type definition
198
+ resource_type = service_detail.resources.get(resource_name)
199
+ if resource_type and resource_type.condition_keys:
200
+ if condition_key in resource_type.condition_keys:
201
+ return ConditionKeyValidationResult(is_valid=True)
202
+
203
+ # If it's a global key but the action has specific condition keys defined,
204
+ # AWS allows it but the key may not be available in every request context
205
+ if is_global_key and action_detail.action_condition_keys is not None:
206
+ warning_msg = (
207
+ f"Global condition key `{condition_key}` is used with action `{action}`. "
208
+ f"While global condition keys can be used across all AWS services, "
209
+ f"the key may not be available in every request context. "
210
+ f"Verify that `{condition_key}` is available for this specific action's request context. "
211
+ f"Consider using `*IfExists` operators (e.g., `StringEqualsIfExists`) if the key might be missing."
212
+ )
213
+ return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
214
+
215
+ # If it's a global key and action doesn't define specific keys, allow it
216
+ if is_global_key:
217
+ return ConditionKeyValidationResult(is_valid=True)
218
+
219
+ # Short error message
220
+ error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
221
+
222
+ # Collect valid condition keys for this action
223
+ valid_keys: set[str] = set()
224
+
225
+ # Add service-level condition keys
226
+ if service_detail.condition_keys:
227
+ if isinstance(service_detail.condition_keys, dict):
228
+ valid_keys.update(service_detail.condition_keys.keys())
229
+ elif isinstance(service_detail.condition_keys, list):
230
+ valid_keys.update(service_detail.condition_keys)
231
+
232
+ # Add action-specific condition keys
233
+ if action_name in service_detail.actions:
234
+ action_detail = service_detail.actions[action_name]
235
+ if action_detail.action_condition_keys:
236
+ if isinstance(action_detail.action_condition_keys, dict):
237
+ valid_keys.update(action_detail.action_condition_keys.keys())
238
+ elif isinstance(action_detail.action_condition_keys, list):
239
+ valid_keys.update(action_detail.action_condition_keys)
240
+
241
+ # Add resource-specific condition keys
242
+ if action_detail.resources:
243
+ for res_req in action_detail.resources:
244
+ resource_name = res_req.get("Name", "")
245
+ if resource_name:
246
+ resource_type = service_detail.resources.get(resource_name)
247
+ if resource_type and resource_type.condition_keys:
248
+ if isinstance(resource_type.condition_keys, dict):
249
+ valid_keys.update(resource_type.condition_keys.keys())
250
+ elif isinstance(resource_type.condition_keys, list):
251
+ valid_keys.update(resource_type.condition_keys)
252
+
253
+ # Build detailed suggestion with valid keys (goes in collapsible section)
254
+ suggestion_parts = []
255
+
256
+ if valid_keys:
257
+ # Sort and limit to first 10 keys for readability
258
+ sorted_keys = sorted(valid_keys)
259
+ suggestion_parts.append("**Valid condition keys for this action:**")
260
+ if len(sorted_keys) <= 10:
261
+ for key in sorted_keys:
262
+ suggestion_parts.append(f"- `{key}`")
263
+ else:
264
+ for key in sorted_keys[:10]:
265
+ suggestion_parts.append(f"- `{key}`")
266
+ suggestion_parts.append(f"- ... and {len(sorted_keys) - 10} more")
267
+
268
+ suggestion_parts.append("")
269
+ suggestion_parts.append(
270
+ "**Global condition keys** (e.g., `aws:ResourceOrgID`, `aws:RequestedRegion`, `aws:SourceIp`, `aws:SourceVpce`) "
271
+ "can also be used with any AWS action"
272
+ )
273
+ else:
274
+ # No action-specific keys - mention global keys
275
+ suggestion_parts.append(
276
+ "This action does not have specific condition keys defined.\n\n"
277
+ "However, you can use **global condition keys** such as:\n"
278
+ "- `aws:RequestedRegion`\n"
279
+ "- `aws:SourceIp`\n"
280
+ "- `aws:SourceVpce`\n"
281
+ "- `aws:ResourceOrgID`\n"
282
+ "- `aws:PrincipalOrgID`\n"
283
+ "- `aws:SourceAccount`\n"
284
+ "- `aws:PrincipalAccount`\n"
285
+ "- `aws:CurrentTime`\n"
286
+ "- `aws:ResourceAccount`\n"
287
+ "- `aws:PrincipalArn`\n"
288
+ "- And many others"
289
+ )
290
+
291
+ suggestion = "\n".join(suggestion_parts)
292
+
293
+ return ConditionKeyValidationResult(
294
+ is_valid=False,
295
+ error_message=error_msg,
296
+ suggestion=suggestion,
297
+ )
298
+
299
+ except Exception as e: # pylint: disable=broad-exception-caught
300
+ logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
301
+ return ConditionKeyValidationResult(
302
+ is_valid=False,
303
+ error_message=f"Failed to validate condition key: {e!s}",
304
+ )
305
+
306
+ def get_resources_for_action(
307
+ self, action: str, service_detail: ServiceDetail
308
+ ) -> list[dict[str, Any]]:
309
+ """Get resource types required for a specific action.
310
+
311
+ Args:
312
+ action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
313
+ service_detail: Service definition containing action details
314
+
315
+ Returns:
316
+ List of resource dictionaries from AWS API, or empty list if action not found
317
+
318
+ Example:
319
+ >>> validator = ServiceValidator()
320
+ >>> service = await fetcher.fetch_service_by_name("s3")
321
+ >>> resources = validator.get_resources_for_action("s3:GetObject", service)
322
+ """
323
+ try:
324
+ _, action_name = self._parser.parse_action(action) # pylint: disable=unused-variable
325
+
326
+ # Find the action (case-insensitive)
327
+ action_detail = service_detail.actions.get(action_name)
328
+ if action_detail and action_detail.resources:
329
+ return action_detail.resources
330
+ return []
331
+ except Exception as e: # pylint: disable=broad-exception-caught
332
+ logger.error(f"Error getting resources for action {action}: {e}")
333
+ return []
334
+
335
+ def get_arn_formats_for_action(self, action: str, service_detail: ServiceDetail) -> list[str]:
336
+ """Get ARN formats/patterns for resources used by a specific action.
337
+
338
+ This method extracts the ARN format patterns from the resource types
339
+ that an action can operate on. Useful for validating Resource elements
340
+ in IAM policies.
341
+
342
+ Args:
343
+ action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
344
+ service_detail: Service definition containing action and resource details
345
+
346
+ Returns:
347
+ List of ARN format strings, or empty list if action not found or has no resources
348
+
349
+ Example:
350
+ >>> validator = ServiceValidator()
351
+ >>> service = await fetcher.fetch_service_by_name("s3")
352
+ >>> arns = validator.get_arn_formats_for_action("s3:GetObject", service)
353
+ >>> # Returns: ["arn:${Partition}:s3:::${BucketName}/${ObjectName}"]
354
+ """
355
+ try:
356
+ _, action_name = self._parser.parse_action(action)
357
+
358
+ # Find the action
359
+ action_detail = service_detail.actions.get(action_name)
360
+ if not action_detail or not action_detail.resources:
361
+ return []
362
+
363
+ # Extract ARN formats from resource types
364
+ arn_formats = []
365
+ for resource_ref in action_detail.resources:
366
+ # resource_ref is a dict with "Name" key pointing to resource type name
367
+ resource_name = resource_ref.get("Name", "")
368
+ if not resource_name:
369
+ continue
370
+
371
+ # Look up the resource type in service definition
372
+ resource_type = service_detail.resources.get(resource_name)
373
+ if resource_type and resource_type.arn_formats:
374
+ arn_formats.extend(resource_type.arn_formats)
375
+
376
+ return arn_formats
377
+
378
+ except Exception as e: # pylint: disable=broad-exception-caught
379
+ logger.error(f"Error getting ARN formats for action {action}: {e}")
380
+ return []