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,382 @@
1
+ """
2
+ ARN pattern matching utilities for IAM policy validation.
3
+
4
+ This module provides functions for matching ARN patterns with glob support.
5
+ Portions of this code are derived from or inspired by Parliament's ARN matching
6
+ implementation.
7
+
8
+ Original work Copyright 2019 Duo Security (BSD 3-Clause License)
9
+ Modifications and additions Copyright 2024 (MIT License)
10
+
11
+ Parliament: https://github.com/duo-labs/parliament
12
+ License: https://github.com/duo-labs/parliament/blob/master/LICENSE
13
+
14
+ The is_glob_match() function is adapted from Parliament's implementation.
15
+ See: https://github.com/duo-labs/parliament/issues/36#issuecomment-574001764
16
+ """
17
+
18
+ import re
19
+
20
+
21
+ def arn_matches(
22
+ arn_pattern: str,
23
+ arn: str,
24
+ resource_type: str | None = None,
25
+ ) -> bool:
26
+ """
27
+ Check if an ARN matches a pattern with glob support.
28
+
29
+ Both the pattern and ARN can contain wildcards (*). This is useful for
30
+ validating that policy resources match the required format for actions.
31
+
32
+ Args:
33
+ arn_pattern: ARN pattern (e.g., from AWS docs), can have wildcards
34
+ arn: ARN from policy, can have wildcards
35
+ resource_type: Optional resource type (e.g., "bucket", "object") for special handling
36
+
37
+ Returns:
38
+ True if ARN could match the pattern
39
+
40
+ Examples:
41
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::bucket/key")
42
+ True
43
+
44
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::bucket")
45
+ False
46
+
47
+ >>> # Both can have wildcards
48
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::*personalize*")
49
+ True # Could match "arn:aws:s3:::personalize/file"
50
+
51
+ >>> # Special case: S3 buckets can't have /
52
+ >>> arn_matches("arn:*:s3:::*", "arn:aws:s3:::bucket/key", resource_type="bucket")
53
+ False
54
+ """
55
+ # Wildcard shortcuts
56
+ if arn_pattern == "*" or arn == "*":
57
+ return True
58
+
59
+ # Special case for S3 buckets - no "/" allowed
60
+ if resource_type and "bucket" in resource_type.lower():
61
+ # Strip variables like ${aws:username} before checking
62
+ arn_without_vars = _strip_variables_from_arn(arn)
63
+ if "/" in arn_without_vars:
64
+ return False
65
+
66
+ # Parse ARN into parts
67
+ pattern_parts = arn_pattern.split(":")
68
+ arn_parts = arn.split(":")
69
+
70
+ # ARN must have at least 6 parts: arn:partition:service:region:account:resource
71
+ if len(pattern_parts) < 6 or len(arn_parts) < 6:
72
+ # Invalid ARN format
73
+ return False
74
+
75
+ # Match first 5 parts (arn:partition:service:region:account)
76
+ for i in range(5):
77
+ pattern_part = pattern_parts[i]
78
+ arn_part = arn_parts[i]
79
+
80
+ # Pattern wildcard matches any non-empty value
81
+ if pattern_part == "*" and arn_part != "":
82
+ continue
83
+ # ARN wildcard matches anything
84
+ elif arn_part == "*":
85
+ continue
86
+ # Exact match
87
+ elif pattern_part == arn_part:
88
+ continue
89
+ else:
90
+ # No match
91
+ return False
92
+
93
+ # Match resource ID (everything after 5th colon)
94
+ pattern_id = ":".join(pattern_parts[5:])
95
+ arn_id = ":".join(arn_parts[5:])
96
+
97
+ # Replace variables like [key] with wildcard
98
+ arn_id = re.sub(r"\[.+?\]", "*", arn_id)
99
+
100
+ return is_glob_match(pattern_id, arn_id)
101
+
102
+
103
+ def arn_strictly_valid(
104
+ arn_pattern: str,
105
+ arn: str,
106
+ resource_type: str | None = None,
107
+ ) -> bool:
108
+ """
109
+ Strictly validate ARN against pattern with resource type checking.
110
+
111
+ This is stricter than arn_matches() and enforces:
112
+ - Resource type must be present and match
113
+ - No wildcards in resource type portion
114
+ - No extra colons in resource ID
115
+
116
+ Args:
117
+ arn_pattern: ARN pattern from AWS service definition
118
+ arn: ARN from policy
119
+ resource_type: Optional resource type for additional validation
120
+
121
+ Returns:
122
+ True if ARN strictly matches the pattern
123
+
124
+ Examples:
125
+ >>> # Valid: has resource type "user"
126
+ >>> arn_strictly_valid("arn:*:iam::*:user/*", "arn:aws:iam::123456789012:user/alice")
127
+ True
128
+
129
+ >>> # Invalid: missing resource type
130
+ >>> arn_strictly_valid("arn:*:iam::*:user/*", "arn:aws:iam::123456789012:u*")
131
+ False
132
+ """
133
+ # First check basic match
134
+ if not arn_matches(arn_pattern, arn, resource_type):
135
+ return False
136
+
137
+ # Parse ARNs
138
+ pattern_parts = arn_pattern.split(":")
139
+ arn_parts = arn.split(":")
140
+
141
+ pattern_id = ":".join(pattern_parts[5:])
142
+ arn_id = ":".join(arn_parts[5:])
143
+
144
+ # Check if pattern has a resource type component
145
+ # Example: "user/alice" has resource type "user"
146
+ # Regex: resource type word followed by : or / (excluding patterns starting with *)
147
+ resource_type_match = re.match(r"(^[^\*][\w-]+)[\/\:](.+)", pattern_id)
148
+
149
+ if resource_type_match and arn_id != "*":
150
+ expected_resource_type = resource_type_match.group(1)
151
+
152
+ # ARN must start with the same resource type
153
+ # Invalid: arn:aws:iam::123456789012:u* (wildcards not allowed in resource type)
154
+ if not arn_id.startswith(expected_resource_type):
155
+ return False
156
+
157
+ # Check for invalid colons in resource ID
158
+ # Strip variables first
159
+ arn_id_without_vars = _strip_variables_from_arn(arn_id)
160
+
161
+ # If ARN has colons but pattern doesn't, it's invalid
162
+ if ":" in arn_id_without_vars and ":" not in pattern_id:
163
+ return False
164
+
165
+ return True
166
+
167
+
168
+ def is_glob_match(s1: str, s2: str) -> bool:
169
+ """
170
+ Recursive glob pattern matching for two strings.
171
+
172
+ Both strings can contain wildcards (*). This implements a recursive
173
+ algorithm that handles all combinations of wildcard positions.
174
+
175
+ Args:
176
+ s1: First string (can contain *)
177
+ s2: Second string (can contain *)
178
+
179
+ Returns:
180
+ True if strings could match
181
+
182
+ Examples:
183
+ >>> is_glob_match("*/*", "*personalize*")
184
+ True
185
+
186
+ >>> is_glob_match("*/*", "mybucket")
187
+ False
188
+
189
+ >>> is_glob_match("*mybucket", "*myotherthing")
190
+ False
191
+
192
+ >>> is_glob_match("test*", "test123")
193
+ True
194
+
195
+ Note:
196
+ This is adapted from Parliament's implementation:
197
+ https://github.com/duo-labs/parliament/issues/36#issuecomment-574001764
198
+ """
199
+ # If strings are equal, TRUE
200
+ if s1 == s2:
201
+ return True
202
+
203
+ # If either string is all wildcards, TRUE
204
+ if (s1 and all(c == "*" for c in s1)) or (s2 and all(c == "*" for c in s2)):
205
+ return True
206
+
207
+ # If either string is empty, FALSE (already handled both empty above)
208
+ if not s1 or not s2:
209
+ return False
210
+
211
+ # At this point, both strings are non-empty
212
+ # If both start with *, TRUE if match first with remainder of second
213
+ # or second with remainder of first
214
+ if s1[0] == s2[0] == "*":
215
+ return is_glob_match(s1[1:], s2) or is_glob_match(s1, s2[1:])
216
+
217
+ # If s1 starts with *, TRUE if remainder of s1 matches any suffix of s2
218
+ if s1[0] == "*":
219
+ return any(is_glob_match(s1[1:], s2[i:]) for i in range(len(s2) + 1))
220
+
221
+ # If s2 starts with *, TRUE if remainder of s2 matches any suffix of s1
222
+ if s2[0] == "*":
223
+ return any(is_glob_match(s1[i:], s2[1:]) for i in range(len(s1) + 1))
224
+
225
+ # TRUE if both have same first character and remainders match
226
+ return s1[0] == s2[0] and is_glob_match(s1[1:], s2[1:])
227
+
228
+
229
+ def _strip_variables_from_arn(arn: str, replace_with: str = "") -> str:
230
+ """
231
+ Strip AWS policy variables from ARN.
232
+
233
+ Examples:
234
+ ${aws:username} → ""
235
+ bucket-${aws:username} → "bucket-"
236
+
237
+ Args:
238
+ arn: ARN string that may contain variables
239
+ replace_with: What to replace variables with (default: empty string)
240
+
241
+ Returns:
242
+ ARN with variables replaced
243
+ """
244
+ # Match ${aws.whatever} or ${aws:whatever}
245
+ return re.sub(r"\$\{aws[\.:][\w\/]+\}", replace_with, arn)
246
+
247
+
248
+ def normalize_template_variables(arn: str) -> str:
249
+ """
250
+ Normalize template variables in ARN to valid placeholders for validation.
251
+
252
+ This function is POSITION-AWARE and handles ANY variable name by determining
253
+ the appropriate replacement based on where the variable appears in the ARN structure.
254
+ It correctly handles variables with colons inside them (e.g., ${AWS::AccountId}).
255
+
256
+ Supports template variables from:
257
+ - Terraform/Terragrunt: ${var.name}, ${local.value}, ${data.source.attr}, etc.
258
+ - CloudFormation: ${AWS::AccountId}, ${AWS::Region}, ${MyParameter}, etc.
259
+ - AWS policy variables: ${aws:username}, ${aws:PrincipalTag/tag-key}, etc.
260
+
261
+ Args:
262
+ arn: ARN string that may contain template variables
263
+
264
+ Returns:
265
+ ARN with template variables replaced with valid placeholders based on position
266
+
267
+ Examples:
268
+ >>> normalize_template_variables("arn:aws:iam::${my_account}:role/name")
269
+ 'arn:aws:iam::123456789012:role/name'
270
+
271
+ >>> normalize_template_variables("arn:aws:iam::${AWS::AccountId}:role/name")
272
+ 'arn:aws:iam::123456789012:role/name'
273
+
274
+ >>> normalize_template_variables("arn:${var.partition}:s3:::${var.bucket}/*")
275
+ 'arn:aws:s3:::placeholder/*'
276
+ """
277
+ # Strategy: Use a simpler, more robust approach
278
+ # First protect template variables by temporarily replacing them with markers,
279
+ # then split the ARN, then replace based on position
280
+
281
+ # Step 1: Find all template variables and temporarily replace them with position markers
282
+ # This handles variables with colons inside them (like ${AWS::AccountId})
283
+ variables = []
284
+
285
+ def save_variable(match):
286
+ variables.append(match.group(0))
287
+ return f"__VAR{len(variables) - 1}__"
288
+
289
+ # Save all template variables (including those with colons, dots, slashes, etc.)
290
+ temp_arn = re.sub(r"\$\{[^}]+\}", save_variable, arn)
291
+
292
+ # Step 2: Now we can safely split by colons
293
+ parts = temp_arn.split(":", 5)
294
+
295
+ if len(parts) < 6:
296
+ # Not a valid ARN format, restore variables with generic placeholder
297
+ result = arn
298
+ for var in variables:
299
+ if re.match(r"\$\{aws[\.:]", var, re.IGNORECASE):
300
+ result = result.replace(var, "placeholder", 1)
301
+ else:
302
+ result = result.replace(var, "placeholder", 1)
303
+ return result
304
+
305
+ # Step 3: Restore variables based on their position in the ARN
306
+ # ARN format: arn:partition:service:region:account:resource
307
+ replacements = {
308
+ 1: "aws", # partition
309
+ 2: "s3", # service (generic placeholder)
310
+ 3: "us-east-1", # region
311
+ 4: "123456789012", # account
312
+ 5: "placeholder", # resource
313
+ }
314
+
315
+ for i, part in enumerate(parts):
316
+ if "__VAR" in part:
317
+ # Find all variable markers in this part
318
+ for j, var in enumerate(variables):
319
+ marker = f"__VAR{j}__"
320
+ if marker in part:
321
+ # Determine replacement based on position
322
+ if i in replacements:
323
+ parts[i] = parts[i].replace(marker, replacements[i])
324
+ else:
325
+ parts[i] = parts[i].replace(marker, "placeholder")
326
+
327
+ # Reconstruct ARN
328
+ return ":".join(parts)
329
+
330
+
331
+ def has_template_variables(arn: str) -> bool:
332
+ """
333
+ Check if an ARN contains template variables.
334
+
335
+ Detects template variables from:
336
+ - Terraform/Terragrunt: ${var_name}
337
+ - CloudFormation: ${AWS::AccountId}
338
+ - AWS policy variables: ${aws:username}
339
+
340
+ Args:
341
+ arn: ARN string to check
342
+
343
+ Returns:
344
+ True if ARN contains template variables, False otherwise
345
+
346
+ Examples:
347
+ >>> has_template_variables("arn:aws:iam::${aws_account_id}:role/name")
348
+ True
349
+
350
+ >>> has_template_variables("arn:aws:iam::123456789012:role/name")
351
+ False
352
+ """
353
+ return bool(re.search(r"\$\{[\w\-\.\_:\/]+\}", arn))
354
+
355
+
356
+ def convert_aws_pattern_to_wildcard(pattern: str) -> str:
357
+ """
358
+ Convert AWS ARN pattern format to wildcard pattern for matching.
359
+
360
+ AWS provides ARN patterns with placeholders like ${Partition}, ${BucketName},
361
+ etc. This function converts them to wildcard (*) patterns that can be used
362
+ with arn_matches() and arn_strictly_valid().
363
+
364
+ Args:
365
+ pattern: ARN pattern from AWS service definition
366
+
367
+ Returns:
368
+ ARN pattern with placeholders replaced by wildcards
369
+
370
+ Examples:
371
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:s3:::${BucketName}/${ObjectName}")
372
+ "arn:*:s3:::*/*"
373
+
374
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:iam::${Account}:user/${UserNameWithPath}")
375
+ "arn:*:iam::*:user/*"
376
+
377
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId}")
378
+ "arn:*:ec2:*:*:instance/*"
379
+ """
380
+ # Replace all ${...} placeholders with *
381
+ # Matches ${Partition}, ${BucketName}, ${Account}, etc.
382
+ return re.sub(r"\$\{[^}]+\}", "*", pattern)
@@ -0,0 +1,222 @@
1
+ """
2
+ Context managers for common validation workflows.
3
+
4
+ This module provides context managers that handle resource lifecycle
5
+ and make the validation API more convenient to use.
6
+ """
7
+
8
+ from contextlib import asynccontextmanager
9
+ from pathlib import Path
10
+
11
+ from iam_validator.core.aws_service import AWSServiceFetcher
12
+ from iam_validator.core.models import PolicyValidationResult
13
+ from iam_validator.core.policy_checks import validate_policies
14
+ from iam_validator.core.policy_loader import PolicyLoader
15
+ from iam_validator.core.report import ReportGenerator
16
+
17
+
18
+ class ValidationContext:
19
+ """
20
+ Validation context that provides convenience methods with shared resources.
21
+
22
+ This class maintains a shared AWSServiceFetcher and configuration
23
+ across multiple validation operations, improving performance.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ fetcher: AWSServiceFetcher,
29
+ config_path: str | None = None,
30
+ ):
31
+ """
32
+ Initialize validation context.
33
+
34
+ Args:
35
+ fetcher: AWS service fetcher instance
36
+ config_path: Optional path to configuration file
37
+ """
38
+ self.fetcher = fetcher
39
+ self.config_path = config_path
40
+ self.loader = PolicyLoader()
41
+
42
+ async def validate_file(self, file_path: str | Path) -> PolicyValidationResult:
43
+ """
44
+ Validate a single IAM policy file.
45
+
46
+ Args:
47
+ file_path: Path to the policy file
48
+
49
+ Returns:
50
+ PolicyValidationResult for the policy
51
+ """
52
+ policies = self.loader.load_from_path(str(file_path))
53
+
54
+ if not policies:
55
+ raise ValueError(f"No IAM policies found in {file_path}")
56
+
57
+ results = await validate_policies(
58
+ policies,
59
+ config_path=self.config_path,
60
+ )
61
+
62
+ return (
63
+ results[0]
64
+ if results
65
+ else PolicyValidationResult(
66
+ policy_file=str(file_path),
67
+ is_valid=False,
68
+ issues=[],
69
+ )
70
+ )
71
+
72
+ async def validate_directory(self, dir_path: str | Path) -> list[PolicyValidationResult]:
73
+ """
74
+ Validate all IAM policies in a directory.
75
+
76
+ Args:
77
+ dir_path: Path to directory containing policy files
78
+
79
+ Returns:
80
+ List of PolicyValidationResults for all policies found
81
+ """
82
+ policies = self.loader.load_from_path(str(dir_path))
83
+
84
+ if not policies:
85
+ raise ValueError(f"No IAM policies found in {dir_path}")
86
+
87
+ return await validate_policies(
88
+ policies,
89
+ config_path=self.config_path,
90
+ )
91
+
92
+ async def validate_json(
93
+ self, policy_json: dict, policy_name: str = "inline-policy"
94
+ ) -> PolicyValidationResult:
95
+ """
96
+ Validate an IAM policy from a Python dictionary.
97
+
98
+ Args:
99
+ policy_json: IAM policy as a Python dict
100
+ policy_name: Name to identify this policy in results
101
+
102
+ Returns:
103
+ PolicyValidationResult for the policy
104
+ """
105
+ from iam_validator.core.models import IAMPolicy
106
+
107
+ # Parse the dict into an IAMPolicy
108
+ policy = IAMPolicy(**policy_json)
109
+
110
+ results = await validate_policies(
111
+ [(policy_name, policy)],
112
+ config_path=self.config_path,
113
+ )
114
+
115
+ return (
116
+ results[0]
117
+ if results
118
+ else PolicyValidationResult(
119
+ policy_file=policy_name,
120
+ is_valid=False,
121
+ issues=[],
122
+ )
123
+ )
124
+
125
+ def generate_report(
126
+ self, results: list[PolicyValidationResult], format: str = "console"
127
+ ) -> str:
128
+ """
129
+ Generate a report from validation results.
130
+
131
+ Args:
132
+ results: List of validation results
133
+ format: Output format (console, json, html, csv, markdown, sarif)
134
+
135
+ Returns:
136
+ Formatted report as string
137
+ """
138
+ generator = ReportGenerator()
139
+ report = generator.generate_report(results)
140
+
141
+ if format == "console":
142
+ # Return empty string for console (it prints directly)
143
+ generator.print_console_report(report)
144
+ return ""
145
+ elif format == "json":
146
+ from iam_validator.core.formatters.json import JSONFormatter
147
+
148
+ return JSONFormatter().format(report)
149
+ elif format == "html":
150
+ from iam_validator.core.formatters.html import HTMLFormatter
151
+
152
+ return HTMLFormatter().format(report)
153
+ elif format == "csv":
154
+ from iam_validator.core.formatters.csv import CSVFormatter
155
+
156
+ return CSVFormatter().format(report)
157
+ elif format == "markdown":
158
+ from iam_validator.core.formatters.markdown import MarkdownFormatter
159
+
160
+ return MarkdownFormatter().format(report)
161
+ elif format == "sarif":
162
+ from iam_validator.core.formatters.sarif import SARIFFormatter
163
+
164
+ return SARIFFormatter().format(report)
165
+ else:
166
+ raise ValueError(f"Unknown format: {format}")
167
+
168
+
169
+ @asynccontextmanager
170
+ async def validator(
171
+ config_path: str | None = None,
172
+ ):
173
+ """
174
+ Context manager that handles AWS fetcher lifecycle.
175
+
176
+ This context manager creates an AWS service fetcher, provides a validation
177
+ context for performing multiple validations efficiently, and ensures proper
178
+ cleanup when done.
179
+
180
+ Args:
181
+ config_path: Optional path to configuration file
182
+
183
+ Yields:
184
+ ValidationContext for performing validations
185
+
186
+ Example:
187
+ >>> async with validator() as v:
188
+ ... result = await v.validate_file("policy.json")
189
+ ... report = v.generate_report([result], format="json")
190
+ ...
191
+ ... # Can do multiple validations with same context
192
+ ... result2 = await v.validate_directory("./policies")
193
+
194
+ Example with configuration:
195
+ >>> async with validator(config_path="./iam-validator.yaml") as v:
196
+ ... results = await v.validate_directory("./policies")
197
+ ... v.generate_report(results, format="console")
198
+ """
199
+ fetcher = AWSServiceFetcher()
200
+ yield ValidationContext(fetcher, config_path)
201
+
202
+
203
+ @asynccontextmanager
204
+ async def validator_from_config(config_path: str):
205
+ """
206
+ Context manager that loads configuration and creates a validator.
207
+
208
+ Convenience wrapper around validator() that loads config from a file.
209
+
210
+ Args:
211
+ config_path: Path to configuration file
212
+
213
+ Yields:
214
+ ValidationContext configured from the config file
215
+
216
+ Example:
217
+ >>> async with validator_from_config("./iam-validator.yaml") as v:
218
+ ... results = await v.validate_directory("./policies")
219
+ ... v.generate_report(results)
220
+ """
221
+ fetcher = AWSServiceFetcher()
222
+ yield ValidationContext(fetcher, config_path=config_path)
@@ -0,0 +1,48 @@
1
+ """
2
+ Public exception types for the IAM Policy Validator SDK.
3
+
4
+ This module defines user-facing exceptions that library users might want to catch
5
+ and handle in their code.
6
+ """
7
+
8
+
9
+ class IAMValidatorError(Exception):
10
+ """Base exception for all IAM Validator errors."""
11
+
12
+ pass
13
+
14
+
15
+ class PolicyLoadError(IAMValidatorError):
16
+ """Raised when a policy file cannot be loaded or parsed."""
17
+
18
+ pass
19
+
20
+
21
+ class PolicyValidationError(IAMValidatorError):
22
+ """Raised when policy validation fails critically."""
23
+
24
+ pass
25
+
26
+
27
+ class ConfigurationError(IAMValidatorError):
28
+ """Raised when configuration is invalid or cannot be loaded."""
29
+
30
+ pass
31
+
32
+
33
+ class AWSServiceError(IAMValidatorError):
34
+ """Raised when AWS service data cannot be fetched."""
35
+
36
+ pass
37
+
38
+
39
+ class InvalidPolicyFormatError(PolicyLoadError):
40
+ """Raised when policy format is invalid (not valid JSON/YAML or missing required fields)."""
41
+
42
+ pass
43
+
44
+
45
+ class UnsupportedPolicyTypeError(PolicyLoadError):
46
+ """Raised when policy type is not supported."""
47
+
48
+ pass