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,374 @@
1
+ """Wildcard resource check - detects Resource: '*' in IAM policies."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import ClassVar
6
+
7
+ from iam_validator.checks.utils.action_parser import get_action_case_insensitive, parse_action
8
+ from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
9
+ from iam_validator.core.aws_service import AWSServiceFetcher
10
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
11
+ from iam_validator.core.models import ActionDetail, ServiceDetail, Statement, ValidationIssue
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Module-level cache for action resource support lookups.
16
+ # Maps action name (e.g., "s3:GetObject") to whether it supports resource-level permissions.
17
+ # True = supports resources (should be flagged for wildcard)
18
+ # False = doesn't support resources (wildcard is appropriate)
19
+ # None = unknown (be conservative, assume it supports resources)
20
+ _action_resource_support_cache: dict[str, bool | None] = {}
21
+
22
+ # Module-level cache for action access level lookups.
23
+ # Maps action name (e.g., "s3:ListBuckets") to its access level.
24
+ # "list" = list-level action (safe with wildcards)
25
+ # Other values or None = unknown
26
+ _action_access_level_cache: dict[str, str | None] = {}
27
+
28
+
29
+ def _get_access_level(action_detail: ActionDetail) -> str:
30
+ """Derive access level from action annotations.
31
+
32
+ AWS API provides Properties dict with boolean flags instead of AccessLevel string.
33
+ We derive the access level from these flags.
34
+
35
+ Args:
36
+ action_detail: Action detail object with annotations
37
+
38
+ Returns:
39
+ Access level string: "permissions-management", "tagging", "write", "list", or "read"
40
+ """
41
+ if not action_detail.annotations:
42
+ return "unknown"
43
+
44
+ props = action_detail.annotations.get("Properties", {})
45
+ if not props:
46
+ return "unknown"
47
+
48
+ # Check flags in priority order
49
+ if props.get("IsPermissionManagement"):
50
+ return "permissions-management"
51
+ if props.get("IsTaggingOnly"):
52
+ return "tagging"
53
+ if props.get("IsWrite"):
54
+ return "write"
55
+ if props.get("IsList"):
56
+ return "list"
57
+
58
+ # Default to read if none of the above
59
+ return "read"
60
+
61
+
62
+ def clear_resource_support_cache() -> None:
63
+ """Clear the action resource support and access level caches.
64
+
65
+ This is primarily useful for testing to ensure a clean state between tests.
66
+ In production, the cache persists for the lifetime of the process, which is
67
+ beneficial as AWS action definitions don't change frequently.
68
+ """
69
+ _action_resource_support_cache.clear()
70
+ _action_access_level_cache.clear()
71
+
72
+
73
+ class WildcardResourceCheck(PolicyCheck):
74
+ """Checks for wildcard resources (Resource: '*') which grant access to all resources."""
75
+
76
+ check_id: ClassVar[str] = "wildcard_resource"
77
+ description: ClassVar[str] = "Checks for wildcard resources (*)"
78
+ default_severity: ClassVar[str] = "medium"
79
+
80
+ async def execute(
81
+ self,
82
+ statement: Statement,
83
+ statement_idx: int,
84
+ fetcher: AWSServiceFetcher,
85
+ config: CheckConfig,
86
+ ) -> list[ValidationIssue]:
87
+ """Execute wildcard resource check on a statement."""
88
+ issues = []
89
+
90
+ # Only check Allow statements
91
+ if statement.effect != "Allow":
92
+ return issues
93
+
94
+ actions = statement.get_actions()
95
+ resources = statement.get_resources()
96
+
97
+ # Check for wildcard resource (Resource: "*")
98
+ if "*" in resources:
99
+ # First, filter out actions that don't support resource-level permissions
100
+ # These actions legitimately require Resource: "*"
101
+ actions_requiring_specific_resources = await self._filter_actions_requiring_resources(
102
+ actions, fetcher
103
+ )
104
+
105
+ # If all actions don't support resources, wildcard is appropriate - no issue
106
+ if not actions_requiring_specific_resources:
107
+ return issues
108
+
109
+ # Use filtered actions for the rest of the check
110
+ actions = actions_requiring_specific_resources
111
+ # Check if all actions are in the allowed_wildcards list
112
+ # allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
113
+ # to all matching AWS actions using the AWS API, then checking if the policy's
114
+ # actions are in that expanded list. This ensures only validated AWS actions
115
+ # are allowed with Resource: "*".
116
+ allowed_wildcards_config = config.config.get("allowed_wildcards", [])
117
+ allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
118
+
119
+ # Check if ALL actions (excluding full wildcard "*") are in the expanded list
120
+ non_wildcard_actions = [a for a in actions if a != "*"]
121
+
122
+ if (allowed_wildcards_config or allowed_wildcards_expanded) and non_wildcard_actions:
123
+ # Strategy 1: Check literal pattern match (fast path)
124
+ # If policy action matches config pattern literally, allow it
125
+ # Example: Policy has "iam:Get*", config has "iam:Get*" -> match
126
+ all_actions_allowed_literal = all(
127
+ action in allowed_wildcards_config for action in non_wildcard_actions
128
+ )
129
+
130
+ if all_actions_allowed_literal:
131
+ # All actions match literally, Resource: "*" is acceptable
132
+ return issues
133
+
134
+ # Strategy 2: Check expanded pattern match (comprehensive path)
135
+ # Expand both policy actions and config patterns, then compare
136
+ # Example: Policy has "iam:Get*" -> ["iam:GetUser", ...],
137
+ # config has "iam:Get*" -> ["iam:GetUser", ...] -> all match
138
+ if allowed_wildcards_expanded:
139
+ expanded_statement_actions = await expand_wildcard_actions(
140
+ non_wildcard_actions, fetcher
141
+ )
142
+
143
+ # Check if all expanded actions are in the expanded allowed list (exact match)
144
+ all_actions_allowed_expanded = all(
145
+ action in allowed_wildcards_expanded
146
+ for action in expanded_statement_actions
147
+ )
148
+
149
+ # If all actions are in the expanded list, skip the wildcard resource warning
150
+ if all_actions_allowed_expanded:
151
+ # All actions are safe, Resource: "*" is acceptable
152
+ return issues
153
+
154
+ # Flag the issue if actions are not all allowed or no allowed_wildcards configured
155
+ # Build a helpful message showing which actions require specific resources
156
+ custom_message = config.config.get("message")
157
+ if custom_message:
158
+ message = custom_message
159
+ else:
160
+ # Build default message with action list
161
+ # Note: actions_requiring_specific_resources is guaranteed non-empty here
162
+ # because we return early above if it's empty
163
+ sorted_actions = sorted(actions_requiring_specific_resources)
164
+ if len(sorted_actions) <= 5:
165
+ action_list = ", ".join(f"`{a}`" for a in sorted_actions)
166
+ else:
167
+ action_list = ", ".join(f"`{a}`" for a in sorted_actions[:5])
168
+ action_list += f" (+{len(sorted_actions) - 5} more)"
169
+ message = (
170
+ f'Statement applies to all resources `"*"`. '
171
+ f"Actions that support resource-level permissions: {action_list}"
172
+ )
173
+
174
+ suggestion = config.config.get(
175
+ "suggestion", "Replace wildcard with specific resource ARNs"
176
+ )
177
+ example = config.config.get("example", "")
178
+
179
+ issues.append(
180
+ ValidationIssue(
181
+ severity=self.get_severity(config),
182
+ statement_sid=statement.sid,
183
+ statement_index=statement_idx,
184
+ issue_type="overly_permissive",
185
+ message=message,
186
+ suggestion=suggestion,
187
+ example=example if example else None,
188
+ line_number=statement.line_number,
189
+ field_name="resource",
190
+ )
191
+ )
192
+
193
+ return issues
194
+
195
+ async def _get_expanded_allowed_wildcards(
196
+ self, config: CheckConfig, fetcher: AWSServiceFetcher
197
+ ) -> frozenset[str]:
198
+ """Get and expand allowed_wildcards configuration.
199
+
200
+ This method retrieves wildcard patterns from the allowed_wildcards config
201
+ and expands them using the AWS API to get all matching actual AWS actions.
202
+
203
+ How it works:
204
+ 1. Retrieves patterns from config (e.g., ["ec2:Describe*", "s3:List*"])
205
+ 2. Expands each pattern using AWS API:
206
+ - "ec2:Describe*" → ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
207
+ - "s3:List*" → ["s3:ListBucket", "s3:ListObjects", ...]
208
+ 3. Returns a set of all expanded actions
209
+
210
+ This allows you to:
211
+ - Specify patterns like "ec2:Describe*" in config
212
+ - Have the validator allow specific actions like "ec2:DescribeInstances" with Resource: "*"
213
+ - Ensure only real AWS actions (validated via API) are allowed
214
+
215
+ Example:
216
+ Config: allowed_wildcards: ["ec2:Describe*"]
217
+ Expands to: ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
218
+ Policy: "Action": ["ec2:DescribeInstances"], "Resource": "*"
219
+ Result: ✅ Allowed (ec2:DescribeInstances is in expanded list)
220
+
221
+ Args:
222
+ config: The check configuration
223
+ fetcher: AWS service fetcher for expanding wildcards via AWS API
224
+
225
+ Returns:
226
+ A frozenset of all expanded action names from the configured patterns
227
+ """
228
+ patterns_to_expand = config.config.get("allowed_wildcards", [])
229
+
230
+ # If no patterns configured, return empty set
231
+ if not patterns_to_expand or not isinstance(patterns_to_expand, list):
232
+ return frozenset()
233
+
234
+ # Expand the wildcard patterns using the AWS API
235
+ # This converts patterns like "ec2:Describe*" to actual AWS actions
236
+ expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
237
+
238
+ return frozenset(expanded_actions)
239
+
240
+ async def _filter_actions_requiring_resources(
241
+ self, actions: list[str], fetcher: AWSServiceFetcher
242
+ ) -> list[str]:
243
+ """Filter actions to only those that should be flagged for wildcard resources.
244
+
245
+ This method filters out actions that legitimately use Resource: "*":
246
+ 1. Actions that don't support resource-level permissions (e.g., sts:GetCallerIdentity)
247
+ 2. List-level actions (e.g., s3:ListBuckets) - these only enumerate resources
248
+ and are not dangerous with wildcards
249
+
250
+ Examples of actions filtered out:
251
+ - iam:ListUsers (list-level, must use Resource: "*")
252
+ - sts:GetCallerIdentity (must use Resource: "*")
253
+ - ec2:DescribeInstances (must use Resource: "*")
254
+ - s3:ListAllMyBuckets (list-level)
255
+
256
+ This method uses a module-level cache to avoid repeated lookups and
257
+ fetches all required services in parallel for better performance.
258
+
259
+ Args:
260
+ actions: List of actions from the policy statement
261
+ fetcher: AWS service fetcher for looking up action definitions
262
+
263
+ Returns:
264
+ List of actions that should be flagged for wildcard resource usage
265
+ """
266
+ actions_requiring_resources = []
267
+ # Actions that need service lookup, grouped by service
268
+ service_actions: dict[str, list[tuple[str, str]]] = {} # service -> [(action, action_name)]
269
+
270
+ for action in actions:
271
+ # Full wildcard "*" - keep it (it's too broad to determine)
272
+ if action == "*":
273
+ actions_requiring_resources.append(action)
274
+ continue
275
+
276
+ # Parse action using the utility
277
+ parsed = parse_action(action)
278
+ if not parsed:
279
+ # Malformed action - keep it (be conservative)
280
+ actions_requiring_resources.append(action)
281
+ continue
282
+
283
+ # Wildcard in service or action name - keep it (can't determine resource support)
284
+ if parsed.has_wildcard:
285
+ actions_requiring_resources.append(action)
286
+ continue
287
+
288
+ service = parsed.service
289
+ action_name = parsed.action_name
290
+
291
+ # Check module-level caches first
292
+ if action in _action_resource_support_cache and action in _action_access_level_cache:
293
+ cached_resource_support = _action_resource_support_cache[action]
294
+ cached_access_level = _action_access_level_cache[action]
295
+
296
+ # Skip list-level actions - they're safe with wildcards
297
+ if cached_access_level == "list":
298
+ continue
299
+
300
+ if cached_resource_support is True or cached_resource_support is None:
301
+ # Supports resources or unknown - include it
302
+ actions_requiring_resources.append(action)
303
+ # If False, action doesn't support resources - skip it
304
+ continue
305
+
306
+ # Group actions by service for parallel fetching
307
+ if service not in service_actions:
308
+ service_actions[service] = []
309
+ service_actions[service].append((action, action_name))
310
+
311
+ # If no services to look up, return early
312
+ if not service_actions:
313
+ return actions_requiring_resources
314
+
315
+ # Fetch all services in parallel
316
+ services = list(service_actions.keys())
317
+ results = await asyncio.gather(
318
+ *[fetcher.fetch_service_by_name(s) for s in services],
319
+ return_exceptions=True,
320
+ )
321
+
322
+ # Build service cache from successful results
323
+ service_cache: dict[str, ServiceDetail | None] = {}
324
+ for service, result in zip(services, results):
325
+ if isinstance(result, BaseException):
326
+ logger.debug(f"Could not look up service {service}: {result}")
327
+ # Mark service as failed - will keep all its actions (conservative)
328
+ service_cache[service] = None
329
+ else:
330
+ # Result is ServiceDetail when not an exception
331
+ service_cache[service] = result
332
+
333
+ # Process actions using cached service data
334
+ for service, action_list in service_actions.items():
335
+ service_detail = service_cache.get(service)
336
+
337
+ if not service_detail:
338
+ # Unknown service - keep all its actions (be conservative)
339
+ for action, _ in action_list:
340
+ _action_resource_support_cache[action] = None # Cache as unknown
341
+ _action_access_level_cache[action] = None # Cache as unknown
342
+ actions_requiring_resources.append(action)
343
+ continue
344
+
345
+ for action, action_name in action_list:
346
+ # Use case-insensitive lookup since AWS actions are case-insensitive
347
+ action_detail = get_action_case_insensitive(service_detail.actions, action_name)
348
+ if not action_detail:
349
+ # Unknown action - keep it (be conservative)
350
+ _action_resource_support_cache[action] = None # Cache as unknown
351
+ _action_access_level_cache[action] = None # Cache as unknown
352
+ actions_requiring_resources.append(action)
353
+ continue
354
+
355
+ # Get action's access level and cache it
356
+ access_level = _get_access_level(action_detail)
357
+ _action_access_level_cache[action] = access_level
358
+
359
+ # Skip list-level actions - they only enumerate resources and are safe with wildcards
360
+ if access_level == "list":
361
+ _action_resource_support_cache[action] = False # Mark as not needing resources
362
+ continue
363
+
364
+ # Check if action supports resource-level permissions
365
+ # action_detail.resources is empty for actions that don't support resources
366
+ supports_resources = bool(action_detail.resources)
367
+ _action_resource_support_cache[action] = supports_resources # Cache result
368
+
369
+ if supports_resources:
370
+ # Action supports resources - should be flagged for wildcard
371
+ actions_requiring_resources.append(action)
372
+ # Else: action doesn't support resources, Resource: "*" is appropriate
373
+
374
+ return actions_requiring_resources
@@ -0,0 +1,31 @@
1
+ """CLI commands for IAM Policy Validator."""
2
+
3
+ from .analyze import AnalyzeCommand
4
+ from .cache import CacheCommand
5
+ from .completion import CompletionCommand
6
+ from .download_services import DownloadServicesCommand
7
+ from .post_to_pr import PostToPRCommand
8
+ from .query import QueryCommand
9
+ from .validate import ValidateCommand
10
+
11
+ # All available commands
12
+ ALL_COMMANDS = [
13
+ ValidateCommand(),
14
+ PostToPRCommand(),
15
+ AnalyzeCommand(),
16
+ CacheCommand(),
17
+ DownloadServicesCommand(),
18
+ QueryCommand(),
19
+ CompletionCommand(),
20
+ ]
21
+
22
+ __all__ = [
23
+ "ValidateCommand",
24
+ "PostToPRCommand",
25
+ "AnalyzeCommand",
26
+ "CacheCommand",
27
+ "DownloadServicesCommand",
28
+ "QueryCommand",
29
+ "CompletionCommand",
30
+ "ALL_COMMANDS",
31
+ ]