iam-policy-validator 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
  2. iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +20 -4
  5. iam_validator/checks/action_condition_enforcement.py +165 -8
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +24 -2
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/full_wildcard.py +67 -0
  10. iam_validator/checks/mfa_condition_check.py +112 -0
  11. iam_validator/checks/principal_validation.py +497 -3
  12. iam_validator/checks/sensitive_action.py +250 -0
  13. iam_validator/checks/service_wildcard.py +105 -0
  14. iam_validator/checks/set_operator_validation.py +157 -0
  15. iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
  16. iam_validator/checks/wildcard_action.py +62 -0
  17. iam_validator/checks/wildcard_resource.py +131 -0
  18. iam_validator/commands/cache.py +1 -1
  19. iam_validator/commands/download_services.py +3 -8
  20. iam_validator/commands/validate.py +72 -13
  21. iam_validator/core/aws_fetcher.py +114 -64
  22. iam_validator/core/check_registry.py +167 -29
  23. iam_validator/core/condition_validators.py +626 -0
  24. iam_validator/core/config/__init__.py +81 -0
  25. iam_validator/core/config/aws_api.py +35 -0
  26. iam_validator/core/config/aws_global_conditions.py +160 -0
  27. iam_validator/core/config/category_suggestions.py +104 -0
  28. iam_validator/core/config/condition_requirements.py +155 -0
  29. iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
  30. iam_validator/core/config/defaults.py +523 -0
  31. iam_validator/core/config/principal_requirements.py +421 -0
  32. iam_validator/core/config/sensitive_actions.py +672 -0
  33. iam_validator/core/config/service_principals.py +95 -0
  34. iam_validator/core/config/wildcards.py +124 -0
  35. iam_validator/core/formatters/enhanced.py +11 -5
  36. iam_validator/core/formatters/sarif.py +78 -14
  37. iam_validator/core/models.py +14 -1
  38. iam_validator/core/policy_checks.py +4 -4
  39. iam_validator/core/pr_commenter.py +1 -1
  40. iam_validator/sdk/__init__.py +187 -0
  41. iam_validator/sdk/arn_matching.py +274 -0
  42. iam_validator/sdk/context.py +222 -0
  43. iam_validator/sdk/exceptions.py +48 -0
  44. iam_validator/sdk/helpers.py +177 -0
  45. iam_validator/sdk/policy_utils.py +425 -0
  46. iam_validator/sdk/shortcuts.py +283 -0
  47. iam_validator/utils/__init__.py +31 -0
  48. iam_validator/utils/cache.py +105 -0
  49. iam_validator/utils/regex.py +206 -0
  50. iam_policy_validator-1.4.0.dist-info/RECORD +0 -56
  51. iam_validator/checks/action_resource_constraint.py +0 -151
  52. iam_validator/checks/security_best_practices.py +0 -536
  53. iam_validator/core/aws_global_conditions.py +0 -137
  54. iam_validator/core/defaults.py +0 -393
  55. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  56. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  57. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,7 @@ This built-in check ensures that specific actions have required conditions.
5
5
  Supports ALL types of conditions: MFA, IP, VPC, time, tags, encryption, etc.
6
6
 
7
7
  Supports advanced "all_of" and "any_of" logic for both actions and conditions.
8
+ Supports both STATEMENT-LEVEL and POLICY-LEVEL enforcement.
8
9
 
9
10
  Common use cases:
10
11
  - iam:PassRole must have iam:PassedToService condition
@@ -12,6 +13,7 @@ Common use cases:
12
13
  - Actions must have source IP restrictions
13
14
  - Resources must have required tags
14
15
  - Combine multiple conditions (MFA + IP + Tags)
16
+ - Policy-level: Ensure ALL statements granting certain actions have MFA
15
17
 
16
18
  Configuration in iam-validator.yaml:
17
19
 
@@ -21,6 +23,7 @@ Configuration in iam-validator.yaml:
21
23
  severity: error
22
24
  description: "Enforce specific conditions for specific actions"
23
25
 
26
+ # STATEMENT-LEVEL: Check individual statements (default)
24
27
  action_condition_requirements:
25
28
  # BASIC: Simple action with required condition
26
29
  - actions:
@@ -90,15 +93,56 @@ Configuration in iam-validator.yaml:
90
93
  - "iam:*"
91
94
  - "s3:DeleteBucket"
92
95
  description: "These dangerous actions should never be used"
96
+
97
+ # POLICY-LEVEL: Scan entire policy and enforce conditions across all matching statements
98
+ policy_level_requirements:
99
+ # Example: If ANY statement grants privilege escalation actions,
100
+ # then ALL such statements must have MFA
101
+ - actions:
102
+ any_of:
103
+ - "iam:CreateUser"
104
+ - "iam:AttachUserPolicy"
105
+ - "iam:PutUserPolicy"
106
+ scope: "policy"
107
+ required_conditions:
108
+ - condition_key: "aws:MultiFactorAuthPresent"
109
+ expected_value: true
110
+ description: "Privilege escalation actions require MFA across entire policy"
111
+ severity: "critical"
112
+
113
+ # Example: All admin actions across the policy must have MFA
114
+ - actions:
115
+ any_of:
116
+ - "iam:*"
117
+ - "s3:*"
118
+ scope: "policy"
119
+ required_conditions:
120
+ all_of:
121
+ - condition_key: "aws:MultiFactorAuthPresent"
122
+ expected_value: true
123
+ - condition_key: "aws:SourceIp"
124
+ apply_to: "all_matching_statements"
125
+
126
+ # Example: Ensure no statement in the policy allows dangerous combinations
127
+ - actions:
128
+ all_of:
129
+ - "iam:CreateAccessKey"
130
+ - "iam:UpdateAccessKey"
131
+ scope: "policy"
132
+ severity: "critical"
133
+ description: "Dangerous combination of actions detected in policy"
93
134
  """
94
135
 
95
136
  import re
96
- from typing import Any
137
+ from typing import TYPE_CHECKING, Any
97
138
 
98
139
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
99
140
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
100
141
  from iam_validator.core.models import Statement, ValidationIssue
101
142
 
143
+ if TYPE_CHECKING:
144
+ from iam_validator.core.models import IAMPolicy
145
+
102
146
 
103
147
  class ActionConditionEnforcementCheck(PolicyCheck):
104
148
  """Enforces specific condition requirements for specific actions with all_of/any_of support."""
@@ -122,7 +166,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
122
166
  fetcher: AWSServiceFetcher,
123
167
  config: CheckConfig,
124
168
  ) -> list[ValidationIssue]:
125
- """Execute condition enforcement check."""
169
+ """Execute statement-level condition enforcement check."""
126
170
  issues = []
127
171
 
128
172
  # Only check Allow statements
@@ -186,6 +230,124 @@ class ActionConditionEnforcementCheck(PolicyCheck):
186
230
 
187
231
  return issues
188
232
 
233
+ async def execute_policy(
234
+ self,
235
+ policy: "IAMPolicy",
236
+ policy_file: str,
237
+ fetcher: AWSServiceFetcher,
238
+ config: CheckConfig,
239
+ **kwargs,
240
+ ) -> list[ValidationIssue]:
241
+ """
242
+ Execute policy-level condition enforcement check.
243
+
244
+ This method scans the entire policy and enforces that ALL statements granting
245
+ certain actions must have specific conditions. This is useful for ensuring
246
+ consistent security controls across the entire policy.
247
+
248
+ Example use case:
249
+ - "If ANY statement in the policy grants iam:CreateUser, iam:AttachUserPolicy,
250
+ or iam:PutUserPolicy, then ALL such statements must have MFA condition."
251
+
252
+ Args:
253
+ policy: The complete IAM policy to check
254
+ policy_file: Path to the policy file (for context/reporting)
255
+ fetcher: AWS service fetcher for validation against AWS APIs
256
+ config: Configuration for this check instance
257
+ **kwargs: Additional context (policy_type, etc.)
258
+
259
+ Returns:
260
+ List of ValidationIssue objects found by this check
261
+ """
262
+ del policy_file, kwargs # Not used in current implementation
263
+ issues = []
264
+
265
+ # Get policy-level requirements from config
266
+ policy_level_requirements = config.config.get("policy_level_requirements", [])
267
+ if not policy_level_requirements:
268
+ return issues
269
+
270
+ # Process each policy-level requirement
271
+ for requirement in policy_level_requirements:
272
+ # Collect all statements that match the action criteria
273
+ matching_statements: list[tuple[int, Statement, list[str]]] = []
274
+
275
+ for idx, statement in enumerate(policy.statement):
276
+ # Only check Allow statements
277
+ if statement.effect != "Allow":
278
+ continue
279
+
280
+ statement_actions = statement.get_actions()
281
+
282
+ # Check if this statement matches the action requirement
283
+ actions_match, matching_actions = await self._check_action_match(
284
+ statement_actions, requirement, fetcher
285
+ )
286
+
287
+ if actions_match and matching_actions:
288
+ matching_statements.append((idx, statement, matching_actions))
289
+
290
+ # If no statements match, skip this requirement
291
+ if not matching_statements:
292
+ continue
293
+
294
+ # Now validate that ALL matching statements have the required conditions
295
+ required_conditions_config = requirement.get("required_conditions", [])
296
+ if not required_conditions_config:
297
+ # No conditions specified, just report that actions were found
298
+ description = requirement.get("description", "")
299
+ severity = requirement.get("severity", self.get_severity(config))
300
+
301
+ # Create a summary issue for all matching statements
302
+ all_actions = set()
303
+ statement_refs = []
304
+ for idx, stmt, actions in matching_statements:
305
+ all_actions.update(actions)
306
+ sid_info = f" (SID: {stmt.sid})" if stmt.sid else ""
307
+ statement_refs.append(f"Statement #{idx + 1}{sid_info}")
308
+
309
+ # Use the first matching statement's index for the issue
310
+ first_idx, first_stmt, _ = matching_statements[0]
311
+
312
+ issues.append(
313
+ ValidationIssue(
314
+ severity=severity,
315
+ statement_sid=first_stmt.sid,
316
+ statement_index=first_idx,
317
+ issue_type="policy_level_action_detected",
318
+ message=f"POLICY-LEVEL: Actions {sorted(all_actions)} found in {len(matching_statements)} statement(s). {description}",
319
+ action=", ".join(sorted(all_actions)),
320
+ suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
321
+ line_number=first_stmt.line_number,
322
+ )
323
+ )
324
+ continue
325
+
326
+ # Validate conditions for each matching statement
327
+ for idx, statement, matching_actions in matching_statements:
328
+ condition_issues = self._validate_conditions(
329
+ statement,
330
+ idx,
331
+ required_conditions_config,
332
+ matching_actions,
333
+ config,
334
+ requirement,
335
+ )
336
+
337
+ # Add policy-level context to each issue
338
+ for issue in condition_issues:
339
+ # Modify the message to indicate this is part of policy-level enforcement
340
+ issue.message = f"POLICY-LEVEL: {issue.message}"
341
+ issue.suggestion = (
342
+ f"{issue.suggestion}\n\n"
343
+ f"Note: This is enforced at the policy level. "
344
+ f"Found {len(matching_statements)} statement(s) with these actions in the policy."
345
+ )
346
+
347
+ issues.extend(condition_issues)
348
+
349
+ return issues
350
+
189
351
  async def _check_action_match(
190
352
  self, statement_actions: list[str], requirement: dict[str, Any], fetcher: AWSServiceFetcher
191
353
  ) -> tuple[bool, list[str]]:
@@ -607,10 +769,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
607
769
  statement_sid=statement.sid,
608
770
  statement_index=statement_idx,
609
771
  issue_type="missing_required_condition",
610
- message=(
611
- f"{message_prefix} Action(s) {matching_actions} require condition '{condition_key}'. "
612
- f"{description}"
613
- ),
772
+ message=f"{message_prefix} Action(s) {matching_actions} require condition '{condition_key}'",
614
773
  action=", ".join(matching_actions),
615
774
  condition_key=condition_key,
616
775
  suggestion=self._build_suggestion(
@@ -707,8 +866,6 @@ class ActionConditionEnforcementCheck(PolicyCheck):
707
866
  )
708
867
  if expected_value is not None:
709
868
  message += f" with value '{expected_value}'"
710
- if description:
711
- message += f". {description}"
712
869
 
713
870
  suggestion = f"Remove the '{condition_key}' condition from the statement"
714
871
  if description:
@@ -0,0 +1,424 @@
1
+ """
2
+ Resource-Action Matching check.
3
+
4
+ This check validates that resources in a policy statement match the required
5
+ resource types for the actions. This catches common mistakes like:
6
+
7
+ - s3:GetObject with bucket ARN (needs object ARN: arn:aws:s3:::bucket/*)
8
+ - s3:ListBucket with object ARN (needs bucket ARN: arn:aws:s3:::bucket)
9
+ - iam:ListUsers with user ARN (needs wildcard: *)
10
+
11
+ This is inspired by Parliament's RESOURCE_MISMATCH check.
12
+
13
+ Example:
14
+ Policy with mismatch:
15
+ {
16
+ "Effect": "Allow",
17
+ "Action": "s3:GetObject",
18
+ "Resource": "arn:aws:s3:::mybucket" # Missing /* for object path!
19
+ }
20
+
21
+ This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
22
+ """
23
+
24
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
25
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
26
+ from iam_validator.core.models import Statement, ValidationIssue
27
+ from iam_validator.sdk.arn_matching import (
28
+ arn_strictly_valid,
29
+ convert_aws_pattern_to_wildcard,
30
+ )
31
+
32
+
33
+ class ActionResourceMatchingCheck(PolicyCheck):
34
+ """
35
+ Validates that resources match the required types for actions.
36
+
37
+ This check helps identify policies that are syntactically valid but won't
38
+ work as intended because the resource ARNs don't match what the action requires.
39
+ """
40
+
41
+ @property
42
+ def check_id(self) -> str:
43
+ return "action_resource_matching"
44
+
45
+ @property
46
+ def description(self) -> str:
47
+ return "Validates that resources match required types for actions"
48
+
49
+ @property
50
+ def default_severity(self) -> str:
51
+ return "medium" # Security issue, not IAM validity error
52
+
53
+ async def execute(
54
+ self,
55
+ statement: Statement,
56
+ statement_idx: int,
57
+ fetcher: AWSServiceFetcher,
58
+ config: CheckConfig,
59
+ ) -> list[ValidationIssue]:
60
+ """
61
+ Execute resource-action matching validation on a statement.
62
+
63
+ Args:
64
+ statement: The IAM policy statement to check
65
+ statement_idx: Index of the statement in the policy
66
+ fetcher: AWS service fetcher for action definitions
67
+ config: Configuration for this check
68
+
69
+ Returns:
70
+ List of ValidationIssue objects for resource mismatches
71
+ """
72
+ issues = []
73
+
74
+ # Get actions and resources
75
+ actions = statement.get_actions()
76
+ resources = statement.get_resources()
77
+ statement_sid = statement.sid
78
+ line_number = statement.line_number
79
+
80
+ # Skip if we have a wildcard resource (handled by other checks)
81
+ if "*" in resources:
82
+ return issues
83
+
84
+ # Check each action
85
+ for action in actions:
86
+ # Skip wildcard actions
87
+ if action == "*" or ":" not in action:
88
+ continue
89
+
90
+ # Parse service and action name
91
+ try:
92
+ service, action_name = action.split(":", 1)
93
+ except ValueError:
94
+ continue # Invalid action format, handled by action_validation
95
+
96
+ # Skip wildcard actions
97
+ if "*" in service or "*" in action_name:
98
+ continue
99
+
100
+ # Get service definition
101
+ service_detail = await fetcher.fetch_service_by_name(service)
102
+ if not service_detail:
103
+ continue # Unknown service, handled by action_validation
104
+
105
+ # Get action definition
106
+ action_detail = service_detail.actions.get(action_name)
107
+ if not action_detail:
108
+ continue # Unknown action, handled by action_validation
109
+
110
+ # Get required resource types for this action
111
+ required_resources = action_detail.resources or []
112
+
113
+ # If action requires no specific resources, it needs Resource: "*"
114
+ if not required_resources:
115
+ # Check if all resources are "*"
116
+ if not all(r == "*" for r in resources):
117
+ issues.append(
118
+ self._create_mismatch_issue(
119
+ action=action,
120
+ required_format="*",
121
+ required_type="*",
122
+ provided_resources=resources,
123
+ statement_idx=statement_idx,
124
+ statement_sid=statement_sid,
125
+ line_number=line_number,
126
+ config=config,
127
+ reason=f'Action {action} can only use Resource: "*"',
128
+ )
129
+ )
130
+ continue
131
+
132
+ # Check if ANY policy resource matches ANY required resource type
133
+ match_found = False
134
+
135
+ for req_resource in required_resources:
136
+ # Get the resource type name from the action's required resources
137
+ resource_name = req_resource.get("Name", "")
138
+ if not resource_name:
139
+ continue
140
+
141
+ # Look up the full resource type definition in the service's resources
142
+ # The action's Resources field only has names like {"Name": "object"}
143
+ # The service's Resources field has full definitions with ARN formats
144
+ resource_type = service_detail.resources.get(resource_name)
145
+ if not resource_type:
146
+ continue
147
+
148
+ # Get the ARN pattern (first format from ARNFormats array)
149
+ arn_pattern = resource_type.arn_pattern
150
+ if not arn_pattern:
151
+ continue
152
+
153
+ # Convert AWS pattern format (${Partition}, ${BucketName}) to wildcards (*)
154
+ # AWS provides patterns like: arn:${Partition}:s3:::${BucketName}/${ObjectName}
155
+ # We need wildcards like: arn:*:s3:::*/*
156
+ wildcard_pattern = convert_aws_pattern_to_wildcard(arn_pattern)
157
+
158
+ # Check if any policy resource matches this ARN pattern
159
+ for resource in resources:
160
+ if arn_strictly_valid(wildcard_pattern, resource, resource_name):
161
+ match_found = True
162
+ break
163
+
164
+ if match_found:
165
+ break
166
+
167
+ # If no match found, create an issue
168
+ if not match_found and required_resources:
169
+ # Build helpful error message with required formats
170
+ # Look up each resource type in the service to get ARN patterns
171
+ required_formats = []
172
+ for req_res in required_resources:
173
+ res_name = req_res.get("Name", "")
174
+ if not res_name:
175
+ continue
176
+ res_type = service_detail.resources.get(res_name)
177
+ if res_type and res_type.arn_pattern:
178
+ required_formats.append(
179
+ {
180
+ "type": res_name,
181
+ "format": res_type.arn_pattern,
182
+ }
183
+ )
184
+
185
+ issues.append(
186
+ self._create_mismatch_issue(
187
+ action=action,
188
+ required_format=required_formats[0]["format"] if required_formats else "",
189
+ required_type=required_formats[0]["type"] if required_formats else "",
190
+ provided_resources=resources,
191
+ statement_idx=statement_idx,
192
+ statement_sid=statement_sid,
193
+ line_number=line_number,
194
+ config=config,
195
+ all_required_formats=required_formats,
196
+ )
197
+ )
198
+
199
+ return issues
200
+
201
+ def _create_mismatch_issue(
202
+ self,
203
+ action: str,
204
+ required_format: str,
205
+ required_type: str,
206
+ provided_resources: list[str],
207
+ statement_idx: int,
208
+ statement_sid: str | None,
209
+ line_number: int | None,
210
+ config: CheckConfig,
211
+ all_required_formats: list[dict] | None = None,
212
+ reason: str | None = None,
213
+ ) -> ValidationIssue:
214
+ """Create a validation issue for resource mismatch."""
215
+ # Build helpful message
216
+ if reason:
217
+ message = reason
218
+ elif all_required_formats and len(all_required_formats) > 1:
219
+ types = ", ".join(f["type"] for f in all_required_formats)
220
+ message = (
221
+ f"No resources match for action '{action}'. This action requires one of: {types}"
222
+ )
223
+ else:
224
+ message = (
225
+ f"No resources match for action '{action}'. "
226
+ f"This action requires resource type: {required_type}"
227
+ )
228
+
229
+ # Build suggestion with examples
230
+ suggestion = self._get_suggestion(action, required_format, provided_resources)
231
+
232
+ return ValidationIssue(
233
+ severity=self.get_severity(config),
234
+ statement_sid=statement_sid,
235
+ statement_index=statement_idx,
236
+ issue_type="resource_mismatch",
237
+ message=message,
238
+ action=action,
239
+ resource=", ".join(provided_resources)
240
+ if len(provided_resources) <= 3
241
+ else f"{provided_resources[0]}...",
242
+ suggestion=suggestion,
243
+ line_number=line_number,
244
+ )
245
+
246
+ def _get_suggestion(
247
+ self,
248
+ action: str,
249
+ required_format: str,
250
+ provided_resources: list[str],
251
+ ) -> str:
252
+ """
253
+ Generate helpful suggestion for fixing the mismatch.
254
+
255
+ This function is service-agnostic and extracts resource type information
256
+ from the ARN pattern to provide contextual examples.
257
+ """
258
+ if not required_format:
259
+ return "Check AWS documentation for required resource types for this action"
260
+
261
+ # Extract action name for contextual hints (e.g., "GetObject" from "s3:GetObject")
262
+ action_name = action.split(":")[1] if ":" in action else action
263
+
264
+ # Special case: Wildcard resource
265
+ if required_format == "*":
266
+ return (
267
+ f'Action {action} can only use Resource: "*" (wildcard).\n'
268
+ f" This action does not support resource-level permissions.\n"
269
+ f' Example: "Resource": "*"'
270
+ )
271
+
272
+ # Extract resource type from ARN pattern
273
+ # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
274
+ # Examples:
275
+ # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
276
+ # arn:${Partition}:iam::${Account}:user/${UserName} -> user
277
+ resource_type = self._extract_resource_type_from_pattern(required_format)
278
+
279
+ # Build service-specific suggestion
280
+ suggestion_parts = []
281
+
282
+ # Add action description
283
+ suggestion_parts.append(f"Action {action} requires {resource_type} resource type.")
284
+
285
+ # Add expected format
286
+ suggestion_parts.append(f" Expected format: {required_format}")
287
+
288
+ # Add practical example based on the pattern
289
+ example = self._generate_example_arn(required_format)
290
+ if example:
291
+ suggestion_parts.append(f" Example: {example}")
292
+
293
+ # Add helpful context for common patterns
294
+ context = self._get_resource_context(action_name, resource_type, required_format)
295
+ if context:
296
+ suggestion_parts.append(f" {context}")
297
+
298
+ suggestion = "\n".join(suggestion_parts)
299
+
300
+ # Add current resources to help user understand the mismatch
301
+ if provided_resources and len(provided_resources) <= 3:
302
+ current = ", ".join(provided_resources)
303
+ suggestion += f"\n Current resources: {current}"
304
+
305
+ return suggestion
306
+
307
+ def _extract_resource_type_from_pattern(self, pattern: str) -> str:
308
+ """
309
+ Extract the resource type from an ARN pattern.
310
+
311
+ Examples:
312
+ arn:${Partition}:s3:::${BucketName}/${ObjectName} -> "object"
313
+ arn:${Partition}:iam::${Account}:user/${UserName} -> "user"
314
+ arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId} -> "instance"
315
+ """
316
+ # Split ARN by colons to get resource part
317
+ parts = pattern.split(":")
318
+ if len(parts) < 6:
319
+ return "resource"
320
+
321
+ # Resource part is everything after the 5th colon
322
+ resource_part = ":".join(parts[5:])
323
+
324
+ # Extract resource type (part before / or entire string)
325
+ if "/" in resource_part:
326
+ resource_type = resource_part.split("/")[0]
327
+ elif ":" in resource_part:
328
+ resource_type = resource_part.split(":")[0]
329
+ else:
330
+ resource_type = resource_part
331
+
332
+ # Remove template variables like ${...}
333
+ import re
334
+
335
+ resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
336
+
337
+ return resource_type.strip() or "resource"
338
+
339
+ def _generate_example_arn(self, pattern: str) -> str:
340
+ """
341
+ Generate a practical example ARN based on the pattern.
342
+
343
+ Converts AWS template variables to realistic examples.
344
+ """
345
+ import re
346
+
347
+ example = pattern
348
+
349
+ # Common substitutions
350
+ substitutions = {
351
+ r"\$\{Partition\}": "aws",
352
+ r"\$\{Region\}": "us-east-1",
353
+ r"\$\{Account\}": "123456789012",
354
+ r"\$\{BucketName\}": "my-bucket",
355
+ r"\$\{ObjectName\}": "*",
356
+ r"\$\{UserName\}": "my-user",
357
+ r"\$\{UserNameWithPath\}": "my-user",
358
+ r"\$\{RoleName\}": "my-role",
359
+ r"\$\{RoleNameWithPath\}": "my-role",
360
+ r"\$\{GroupName\}": "my-group",
361
+ r"\$\{PolicyName\}": "my-policy",
362
+ r"\$\{FunctionName\}": "my-function",
363
+ r"\$\{TableName\}": "MyTable",
364
+ r"\$\{QueueName\}": "MyQueue",
365
+ r"\$\{TopicName\}": "MyTopic",
366
+ r"\$\{InstanceId\}": "i-1234567890abcdef0",
367
+ r"\$\{VolumeId\}": "vol-1234567890abcdef0",
368
+ r"\$\{SnapshotId\}": "snap-1234567890abcdef0",
369
+ r"\$\{KeyId\}": "my-key",
370
+ r"\$\{StreamName\}": "MyStream",
371
+ r"\$\{LayerName\}": "my-layer",
372
+ r"\$\{Token\}": "*",
373
+ r"\$\{[^}]+\}": "*", # Catch-all for any remaining variables
374
+ }
375
+
376
+ for pattern_var, replacement in substitutions.items():
377
+ example = re.sub(pattern_var, replacement, example)
378
+
379
+ return example
380
+
381
+ def _get_resource_context(self, action_name: str, resource_type: str, pattern: str) -> str:
382
+ """
383
+ Provide helpful context about resource requirements.
384
+
385
+ Analyzes the ARN pattern structure and action type to provide
386
+ generic, service-agnostic guidance that works for any AWS service.
387
+ """
388
+ contexts = []
389
+
390
+ # Detect path separator patterns (e.g., bucket/object, layer:version)
391
+ if "/" in pattern:
392
+ # Pattern has path separator - resource needs it too
393
+ parts = pattern.split("/")
394
+ if len(parts) > 1 and "${" in parts[-1]:
395
+ # Last part is a variable like ${ObjectName}, ${InstanceId}
396
+ contexts.append("ARN must include path separator (/) with resource identifier")
397
+
398
+ # Detect colon-separated resource identifiers
399
+ resource_part = ":".join(pattern.split(":")[5:]) if pattern.count(":") >= 5 else ""
400
+ if resource_part.count(":") > 0 and "${" in resource_part:
401
+ # Resource section uses colons, like function:version or layer:version
402
+ contexts.append("ARN uses colon (:) separators in resource section")
403
+
404
+ # Detect List/Describe actions (often need wildcards)
405
+ if (
406
+ action_name.startswith("List")
407
+ or action_name.startswith("Describe")
408
+ or action_name.startswith("Get")
409
+ ):
410
+ # Some Get/List actions require specific resources, others need "*"
411
+ # Only suggest wildcard if pattern is actually "*"
412
+ if pattern == "*":
413
+ contexts.append("This action does not support resource-level permissions")
414
+
415
+ # Generic resource type matching hint
416
+ if resource_type and resource_type != "resource":
417
+ # Avoid redundant message if resource type is obvious
418
+ if not any(
419
+ word in resource_type
420
+ for word in ["object", "bucket", "function", "instance", "user", "role"]
421
+ ):
422
+ contexts.append(f"Resource ARN must be of type '{resource_type}'")
423
+
424
+ return "Note: " + " | ".join(contexts) if contexts else ""