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,472 @@
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
+ import re
25
+ from typing import ClassVar
26
+
27
+ from iam_validator.checks.utils.action_parser import get_action_case_insensitive, parse_action
28
+ from iam_validator.core.aws_service import AWSServiceFetcher
29
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
30
+ from iam_validator.core.models import Statement, ValidationIssue
31
+ from iam_validator.sdk.arn_matching import (
32
+ arn_strictly_valid,
33
+ convert_aws_pattern_to_wildcard,
34
+ has_template_variables,
35
+ normalize_template_variables,
36
+ )
37
+
38
+
39
+ class ActionResourceMatchingCheck(PolicyCheck):
40
+ """
41
+ Validates that resources match the required types for actions.
42
+
43
+ This check helps identify policies that are syntactically valid but won't
44
+ work as intended because the resource ARNs don't match what the action requires.
45
+ """
46
+
47
+ check_id: ClassVar[str] = "action_resource_matching"
48
+ description: ClassVar[str] = "Validates that resources match required types for actions"
49
+ default_severity: ClassVar[str] = "medium" # Security issue, not IAM validity error
50
+
51
+ async def execute(
52
+ self,
53
+ statement: Statement,
54
+ statement_idx: int,
55
+ fetcher: AWSServiceFetcher,
56
+ config: CheckConfig,
57
+ ) -> list[ValidationIssue]:
58
+ """
59
+ Execute resource-action matching validation on a statement.
60
+
61
+ Args:
62
+ statement: The IAM policy statement to check
63
+ statement_idx: Index of the statement in the policy
64
+ fetcher: AWS service fetcher for action definitions
65
+ config: Configuration for this check
66
+
67
+ Returns:
68
+ List of ValidationIssue objects for resource mismatches
69
+ """
70
+ issues = []
71
+
72
+ # Check if template variable support is enabled (default: true)
73
+ # Try global settings first, then check-specific config
74
+ allow_template_variables = config.root_config.get("settings", {}).get(
75
+ "allow_template_variables",
76
+ config.config.get("allow_template_variables", True),
77
+ )
78
+
79
+ # Get actions and resources
80
+ actions = statement.get_actions()
81
+ resources = statement.get_resources()
82
+ statement_sid = statement.sid
83
+ line_number = statement.line_number
84
+
85
+ # Skip if no resources to validate (e.g., trust policies don't have Resource field)
86
+ if not resources:
87
+ return issues
88
+
89
+ # Skip if we have a wildcard resource (handled by other checks)
90
+ if "*" in resources:
91
+ return issues
92
+
93
+ # Check each action
94
+ for action in actions:
95
+ # Parse and validate action
96
+ parsed = parse_action(action)
97
+ if not parsed:
98
+ continue # Invalid action format (or "*"), handled by action_validation
99
+
100
+ # Skip wildcard actions
101
+ if parsed.has_wildcard:
102
+ continue
103
+
104
+ service = parsed.service
105
+ action_name = parsed.action_name
106
+
107
+ # Get service definition
108
+ service_detail = await fetcher.fetch_service_by_name(service)
109
+ if not service_detail:
110
+ continue # Unknown service, handled by action_validation
111
+
112
+ # Get action definition (case-insensitive since AWS actions are case-insensitive)
113
+ action_detail = get_action_case_insensitive(service_detail.actions, action_name)
114
+ if not action_detail:
115
+ continue # Unknown action, handled by action_validation
116
+
117
+ # Get required resource types for this action
118
+ required_resources = action_detail.resources or []
119
+
120
+ # If action requires no specific resources, it needs Resource: "*"
121
+ if not required_resources:
122
+ # Check if all resources are "*"
123
+ if not all(r == "*" for r in resources):
124
+ issues.append(
125
+ self._create_mismatch_issue(
126
+ action=action,
127
+ required_format="*",
128
+ required_type="*",
129
+ provided_resources=resources,
130
+ statement_idx=statement_idx,
131
+ statement_sid=statement_sid,
132
+ line_number=line_number,
133
+ config=config,
134
+ reason=f'Action `{action}` can only use `Resource: "*"`',
135
+ )
136
+ )
137
+ continue
138
+
139
+ # Check if ANY policy resource matches ANY required resource type
140
+ match_found = False
141
+
142
+ for req_resource in required_resources:
143
+ # Get the resource type name from the action's required resources
144
+ resource_name = req_resource.get("Name", "")
145
+ if not resource_name:
146
+ continue
147
+
148
+ # Look up the full resource type definition in the service's resources
149
+ # The action's Resources field only has names like {"Name": "object"}
150
+ # The service's Resources field has full definitions with ARN formats
151
+ resource_type = service_detail.resources.get(resource_name)
152
+ if not resource_type:
153
+ continue
154
+
155
+ # Get the ARN pattern (first format from ARNFormats array)
156
+ arn_pattern = resource_type.arn_pattern
157
+ if not arn_pattern:
158
+ continue
159
+
160
+ # Convert AWS pattern format (${Partition}, ${BucketName}) to wildcards (*)
161
+ # AWS provides patterns like: arn:${Partition}:s3:::${BucketName}/${ObjectName}
162
+ # We need wildcards like: arn:*:s3:::*/*
163
+ wildcard_pattern = convert_aws_pattern_to_wildcard(arn_pattern)
164
+
165
+ # Check if any policy resource matches this ARN pattern
166
+ for resource in resources:
167
+ # Normalize template variables (Terraform/CloudFormation) before matching
168
+ # This allows policies with ${aws_account_id}, ${AWS::AccountId}, etc.
169
+ validation_resource = resource
170
+ if allow_template_variables and has_template_variables(resource):
171
+ validation_resource = normalize_template_variables(resource)
172
+
173
+ if arn_strictly_valid(wildcard_pattern, validation_resource, resource_name):
174
+ match_found = True
175
+ break
176
+
177
+ if match_found:
178
+ break
179
+
180
+ # If no match found, create an issue
181
+ if not match_found and required_resources:
182
+ # Build helpful error message with required formats
183
+ # Look up each resource type in the service to get ARN patterns
184
+ required_formats = []
185
+ for req_res in required_resources:
186
+ res_name = req_res.get("Name", "")
187
+ if not res_name:
188
+ continue
189
+ res_type = service_detail.resources.get(res_name)
190
+ if res_type and res_type.arn_pattern:
191
+ required_formats.append(
192
+ {
193
+ "type": res_name,
194
+ "format": res_type.arn_pattern,
195
+ }
196
+ )
197
+
198
+ issues.append(
199
+ self._create_mismatch_issue(
200
+ action=action,
201
+ required_format=(required_formats[0]["format"] if required_formats else ""),
202
+ required_type=(required_formats[0]["type"] if required_formats else ""),
203
+ provided_resources=resources,
204
+ statement_idx=statement_idx,
205
+ statement_sid=statement_sid,
206
+ line_number=line_number,
207
+ config=config,
208
+ all_required_formats=required_formats,
209
+ )
210
+ )
211
+
212
+ return issues
213
+
214
+ def _create_mismatch_issue(
215
+ self,
216
+ action: str,
217
+ required_format: str,
218
+ required_type: str,
219
+ provided_resources: list[str],
220
+ statement_idx: int,
221
+ statement_sid: str | None,
222
+ line_number: int | None,
223
+ config: CheckConfig,
224
+ all_required_formats: list[dict] | None = None,
225
+ reason: str | None = None,
226
+ ) -> ValidationIssue:
227
+ """Create a validation issue for resource mismatch."""
228
+ # Build helpful message
229
+ if reason:
230
+ message = reason
231
+ elif all_required_formats and len(all_required_formats) > 1:
232
+ types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
233
+ message = (
234
+ f"No resources match for action `{action}`. This action requires one of: {types}"
235
+ )
236
+ else:
237
+ message = (
238
+ f"No resources match for action `{action}`. "
239
+ f"This action requires resource type: `{required_type}`"
240
+ )
241
+
242
+ # Build suggestion with examples
243
+ suggestion = self._get_suggestion(
244
+ action=action,
245
+ required_format=required_format,
246
+ provided_resources=provided_resources,
247
+ all_required_formats=all_required_formats,
248
+ )
249
+
250
+ return ValidationIssue(
251
+ severity=self.get_severity(config),
252
+ statement_sid=statement_sid,
253
+ statement_index=statement_idx,
254
+ issue_type="resource_mismatch",
255
+ message=message,
256
+ action=action,
257
+ resource=(
258
+ ", ".join(provided_resources)
259
+ if len(provided_resources) <= 3
260
+ else f"{provided_resources[0]}..."
261
+ ),
262
+ suggestion=suggestion,
263
+ line_number=line_number,
264
+ field_name="resource",
265
+ )
266
+
267
+ def _get_suggestion(
268
+ self,
269
+ action: str,
270
+ required_format: str,
271
+ provided_resources: list[str],
272
+ all_required_formats: list[dict] | None = None,
273
+ ) -> str:
274
+ """
275
+ Generate helpful suggestion for fixing the mismatch.
276
+
277
+ This function is service-agnostic and extracts resource type information
278
+ from the ARN pattern to provide contextual examples.
279
+ """
280
+ if not required_format:
281
+ return "Check AWS documentation for required resource types for this action"
282
+
283
+ # Extract action name for contextual hints (e.g., "GetObject" from "s3:GetObject")
284
+ action_name = action.split(":")[1] if ":" in action else action
285
+
286
+ # Special case: Wildcard resource
287
+ if required_format == "*":
288
+ return (
289
+ f'Action `{action}` can only use `Resource: "*"` (wildcard).\n'
290
+ f" This action does not support resource-level permissions.\n"
291
+ f' Example: `"Resource": "*"`'
292
+ )
293
+
294
+ # Build service-specific suggestion with proper markdown formatting
295
+ suggestion_parts = []
296
+
297
+ # If multiple resource types are valid, show all of them
298
+ if all_required_formats and len(all_required_formats) > 1:
299
+ resource_types = [fmt["type"] for fmt in all_required_formats]
300
+ suggestion_parts.append(
301
+ f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
302
+ )
303
+ suggestion_parts.append("")
304
+
305
+ # Show format and example for each resource type
306
+ for fmt in all_required_formats:
307
+ resource_type = fmt["type"]
308
+ arn_format = fmt["format"]
309
+
310
+ suggestion_parts.append(
311
+ f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
312
+ )
313
+ suggestion_parts.append("```")
314
+ suggestion_parts.append(arn_format)
315
+ suggestion_parts.append("```")
316
+
317
+ # Add practical example
318
+ example = self._generate_example_arn(arn_format)
319
+ if example:
320
+ suggestion_parts.append(f"Example: `{example}`")
321
+
322
+ suggestion_parts.append("")
323
+ else:
324
+ # Single resource type - show detailed info
325
+ # Extract resource type from ARN pattern
326
+ # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
327
+ # Examples:
328
+ # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
329
+ # arn:${Partition}:iam::${Account}:user/${UserName} -> user
330
+ resource_type = self._extract_resource_type_from_pattern(required_format)
331
+
332
+ # Add action description
333
+ suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
334
+ suggestion_parts.append("")
335
+
336
+ # Add expected format in code block
337
+ suggestion_parts.append("**Expected format:**")
338
+ suggestion_parts.append(f"```\n{required_format}\n```")
339
+
340
+ # Add practical example based on the pattern
341
+ example = self._generate_example_arn(required_format)
342
+ if example:
343
+ suggestion_parts.append("**Example:**")
344
+ suggestion_parts.append(f"```\n{example}\n```")
345
+
346
+ # Add helpful context for common patterns
347
+ context = self._get_resource_context(action_name, resource_type, required_format)
348
+ if context:
349
+ suggestion_parts.append(f"**Note:** {context}")
350
+
351
+ # Add current resources to help user understand the mismatch
352
+ if provided_resources and len(provided_resources) <= 3:
353
+ suggestion_parts.append("**Current resources:**")
354
+ for resource in provided_resources:
355
+ suggestion_parts.append(f"- `{resource}`")
356
+
357
+ suggestion = "\n".join(suggestion_parts)
358
+ return suggestion
359
+
360
+ def _extract_resource_type_from_pattern(self, pattern: str) -> str:
361
+ """
362
+ Extract the resource type from an ARN pattern.
363
+
364
+ Examples:
365
+ arn:${Partition}:s3:::${BucketName}/${ObjectName} -> "object"
366
+ arn:${Partition}:iam::${Account}:user/${UserName} -> "user"
367
+ arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId} -> "instance"
368
+ """
369
+ # Split ARN by colons to get resource part
370
+ parts = pattern.split(":")
371
+ if len(parts) < 6:
372
+ return "resource"
373
+
374
+ # Resource part is everything after the 5th colon
375
+ resource_part = ":".join(parts[5:])
376
+
377
+ # Extract resource type (part before / or entire string)
378
+ if "/" in resource_part:
379
+ resource_type = resource_part.split("/", maxsplit=1)[0]
380
+ elif ":" in resource_part:
381
+ resource_type = resource_part.split(":", maxsplit=1)[0]
382
+ else:
383
+ resource_type = resource_part
384
+
385
+ # Remove template variables like ${...}
386
+ resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
387
+ return resource_type.strip() or "resource"
388
+
389
+ def _generate_example_arn(self, pattern: str) -> str:
390
+ """
391
+ Generate a practical example ARN based on the pattern.
392
+
393
+ Converts AWS template variables to realistic examples.
394
+ """
395
+ example = pattern
396
+
397
+ # Common substitutions
398
+ substitutions = {
399
+ r"\$\{Partition\}": "aws",
400
+ r"\$\{Region\}": "us-east-1",
401
+ r"\$\{Account\}": "123456789012",
402
+ r"\$\{BucketName\}": "my-bucket",
403
+ r"\$\{ObjectName\}": "*",
404
+ r"\$\{UserName\}": "my-user",
405
+ r"\$\{UserNameWithPath\}": "my-user",
406
+ r"\$\{RoleName\}": "my-role",
407
+ r"\$\{RoleNameWithPath\}": "my-role",
408
+ r"\$\{GroupName\}": "my-group",
409
+ r"\$\{PolicyName\}": "my-policy",
410
+ r"\$\{FunctionName\}": "my-function",
411
+ r"\$\{TableName\}": "MyTable",
412
+ r"\$\{QueueName\}": "MyQueue",
413
+ r"\$\{TopicName\}": "MyTopic",
414
+ r"\$\{InstanceId\}": "i-1234567890abcdef0",
415
+ r"\$\{VolumeId\}": "vol-1234567890abcdef0",
416
+ r"\$\{SnapshotId\}": "snap-1234567890abcdef0",
417
+ r"\$\{KeyId\}": "my-key",
418
+ r"\$\{StreamName\}": "MyStream",
419
+ r"\$\{LayerName\}": "my-layer",
420
+ r"\$\{Token\}": "*",
421
+ r"\$\{[^}]+\}": "*", # Catch-all for any remaining variables
422
+ }
423
+
424
+ for pattern_var, replacement in substitutions.items():
425
+ example = re.sub(pattern_var, replacement, example)
426
+
427
+ return example
428
+
429
+ def _get_resource_context(self, action_name: str, resource_type: str, pattern: str) -> str:
430
+ """
431
+ Provide helpful context about resource requirements.
432
+
433
+ Analyzes the ARN pattern structure and action type to provide
434
+ generic, service-agnostic guidance that works for any AWS service.
435
+ """
436
+ contexts = []
437
+
438
+ # Detect path separator patterns (e.g., bucket/object, layer:version)
439
+ if "/" in pattern:
440
+ # Pattern has path separator - resource needs it too
441
+ parts = pattern.split("/")
442
+ if len(parts) > 1 and "${" in parts[-1]:
443
+ # Last part is a variable like ${ObjectName}, ${InstanceId}
444
+ contexts.append("ARN must include path separator (/) with resource identifier")
445
+
446
+ # Detect colon-separated resource identifiers
447
+ resource_part = ":".join(pattern.split(":")[5:]) if pattern.count(":") >= 5 else ""
448
+ if resource_part.count(":") > 0 and "${" in resource_part:
449
+ # Resource section uses colons, like function:version or layer:version
450
+ contexts.append("ARN uses colon (:) separators in resource section")
451
+
452
+ # Detect List/Describe actions (often need wildcards)
453
+ if (
454
+ action_name.startswith("List")
455
+ or action_name.startswith("Describe")
456
+ or action_name.startswith("Get")
457
+ ):
458
+ # Some Get/List actions require specific resources, others need "*"
459
+ # Only suggest wildcard if pattern is actually "*"
460
+ if pattern == "*":
461
+ contexts.append("This action does not support resource-level permissions")
462
+
463
+ # Generic resource type matching hint
464
+ if resource_type and resource_type != "resource":
465
+ # Avoid redundant message if resource type is obvious
466
+ if not any(
467
+ word in resource_type
468
+ for word in ["object", "bucket", "function", "instance", "user", "role"]
469
+ ):
470
+ contexts.append(f"Resource ARN must be of type '{resource_type}'")
471
+
472
+ return "Note: " + " | ".join(contexts) if contexts else ""
@@ -0,0 +1,67 @@
1
+ """Action validation check - validates IAM actions against AWS service definitions.
2
+
3
+ This check ensures that all actions specified in IAM policies are valid actions
4
+ defined by AWS services. It helps identify typos or deprecated actions that may
5
+ lead to unintended access permissions.
6
+
7
+ This check is not necessary when using Access Analyzer, as it performs similar
8
+ validations. However, it can be useful in environments where Access Analyzer is
9
+ not available or for pre-deployment policy validation to catch errors early.
10
+ """
11
+
12
+ from typing import ClassVar
13
+
14
+ from iam_validator.core.aws_service import AWSServiceFetcher
15
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
16
+ from iam_validator.core.models import Statement, ValidationIssue
17
+
18
+
19
+ class ActionValidationCheck(PolicyCheck):
20
+ """Validates that IAM actions exist in AWS services."""
21
+
22
+ check_id: ClassVar[str] = "action_validation"
23
+ description: ClassVar[str] = "Validates that actions exist in AWS service definitions"
24
+ default_severity: ClassVar[str] = "error"
25
+
26
+ async def execute(
27
+ self,
28
+ statement: Statement,
29
+ statement_idx: int,
30
+ fetcher: AWSServiceFetcher,
31
+ config: CheckConfig,
32
+ ) -> list[ValidationIssue]:
33
+ """Execute action validation on a statement.
34
+
35
+ This check ONLY validates that actions exist in AWS service definitions.
36
+ Wildcard security checks are handled by security_best_practices_check.
37
+ """
38
+ issues = []
39
+
40
+ # Get actions from statement
41
+ actions = statement.get_actions()
42
+ statement_sid = statement.sid
43
+ line_number = statement.line_number
44
+
45
+ for action in actions:
46
+ # Skip wildcard actions - they're handled by security_best_practices_check
47
+ if action == "*" or "*" in action:
48
+ continue
49
+
50
+ # Validate the action exists in AWS
51
+ is_valid, error_msg, _is_wildcard = await fetcher.validate_action(action)
52
+
53
+ if not is_valid:
54
+ issues.append(
55
+ ValidationIssue(
56
+ severity=self.get_severity(config),
57
+ statement_sid=statement_sid,
58
+ statement_index=statement_idx,
59
+ issue_type="invalid_action",
60
+ message=error_msg or f"Invalid action: `{action}`",
61
+ action=action,
62
+ line_number=line_number,
63
+ field_name="action",
64
+ )
65
+ )
66
+
67
+ return issues
@@ -0,0 +1,88 @@
1
+ """Condition key validation check - validates condition keys against AWS definitions."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from iam_validator.core.aws_service import AWSServiceFetcher
6
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
+ from iam_validator.core.models import Statement, ValidationIssue
8
+
9
+
10
+ class ConditionKeyValidationCheck(PolicyCheck):
11
+ """Validates condition keys against AWS service definitions and global keys."""
12
+
13
+ check_id: ClassVar[str] = "condition_key_validation"
14
+ description: ClassVar[str] = "Validates condition keys against AWS service definitions"
15
+ default_severity: ClassVar[str] = "error" # Invalid condition keys are IAM policy errors
16
+
17
+ async def execute(
18
+ self,
19
+ statement: Statement,
20
+ statement_idx: int,
21
+ fetcher: AWSServiceFetcher,
22
+ config: CheckConfig,
23
+ ) -> list[ValidationIssue]:
24
+ """Execute condition key validation on a statement."""
25
+ issues = []
26
+
27
+ # Get conditions from statement
28
+ if not statement.condition:
29
+ return issues
30
+
31
+ # Check if global condition key warnings are enabled (default: True)
32
+ warn_on_global_keys = config.config.get("warn_on_global_condition_keys", True)
33
+
34
+ statement_sid = statement.sid
35
+ line_number = statement.line_number
36
+ actions = statement.get_actions()
37
+ resources = statement.get_resources()
38
+
39
+ # Extract all condition keys from all condition operators
40
+ for operator, conditions in statement.condition.items():
41
+ for condition_key in conditions.keys():
42
+ # Validate this condition key against each action in the statement
43
+ for action in actions:
44
+ # Skip wildcard actions
45
+ if action == "*":
46
+ continue
47
+
48
+ # Validate against action and resource types
49
+ result = await fetcher.validate_condition_key(action, condition_key, resources)
50
+
51
+ if not result.is_valid:
52
+ issues.append(
53
+ ValidationIssue(
54
+ severity=self.get_severity(config),
55
+ statement_sid=statement_sid,
56
+ statement_index=statement_idx,
57
+ issue_type="invalid_condition_key",
58
+ message=result.error_message
59
+ or f"Invalid condition key: `{condition_key}`",
60
+ action=action,
61
+ condition_key=condition_key,
62
+ line_number=line_number,
63
+ suggestion=result.suggestion,
64
+ field_name="condition",
65
+ )
66
+ )
67
+ # Only report once per condition key (not per action)
68
+ break
69
+ elif result.warning_message and warn_on_global_keys:
70
+ # Add warning for global condition keys with action-specific keys
71
+ # Only if warn_on_global_condition_keys is enabled
72
+ issues.append(
73
+ ValidationIssue(
74
+ severity="warning",
75
+ statement_sid=statement_sid,
76
+ statement_index=statement_idx,
77
+ issue_type="global_condition_key_with_action_specific",
78
+ message=result.warning_message,
79
+ action=action,
80
+ condition_key=condition_key,
81
+ line_number=line_number,
82
+ field_name="condition",
83
+ )
84
+ )
85
+ # Only report once per condition key (not per action)
86
+ break
87
+
88
+ return issues