iam-policy-validator 1.7.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.

Potentially problematic release.


This version of iam-policy-validator might be problematic. Click here for more details.

Files changed (83) hide show
  1. iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
  2. iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
  3. iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.7.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 +7 -0
  9. iam_validator/checks/__init__.py +43 -0
  10. iam_validator/checks/action_condition_enforcement.py +884 -0
  11. iam_validator/checks/action_resource_matching.py +441 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +92 -0
  14. iam_validator/checks/condition_type_mismatch.py +259 -0
  15. iam_validator/checks/full_wildcard.py +71 -0
  16. iam_validator/checks/mfa_condition_check.py +112 -0
  17. iam_validator/checks/policy_size.py +147 -0
  18. iam_validator/checks/policy_type_validation.py +305 -0
  19. iam_validator/checks/principal_validation.py +776 -0
  20. iam_validator/checks/resource_validation.py +138 -0
  21. iam_validator/checks/sensitive_action.py +254 -0
  22. iam_validator/checks/service_wildcard.py +107 -0
  23. iam_validator/checks/set_operator_validation.py +157 -0
  24. iam_validator/checks/sid_uniqueness.py +170 -0
  25. iam_validator/checks/utils/__init__.py +1 -0
  26. iam_validator/checks/utils/policy_level_checks.py +143 -0
  27. iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
  28. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  29. iam_validator/checks/wildcard_action.py +67 -0
  30. iam_validator/checks/wildcard_resource.py +135 -0
  31. iam_validator/commands/__init__.py +25 -0
  32. iam_validator/commands/analyze.py +531 -0
  33. iam_validator/commands/base.py +48 -0
  34. iam_validator/commands/cache.py +392 -0
  35. iam_validator/commands/download_services.py +255 -0
  36. iam_validator/commands/post_to_pr.py +86 -0
  37. iam_validator/commands/validate.py +600 -0
  38. iam_validator/core/__init__.py +14 -0
  39. iam_validator/core/access_analyzer.py +671 -0
  40. iam_validator/core/access_analyzer_report.py +640 -0
  41. iam_validator/core/aws_fetcher.py +940 -0
  42. iam_validator/core/check_registry.py +607 -0
  43. iam_validator/core/cli.py +134 -0
  44. iam_validator/core/condition_validators.py +626 -0
  45. iam_validator/core/config/__init__.py +81 -0
  46. iam_validator/core/config/aws_api.py +35 -0
  47. iam_validator/core/config/aws_global_conditions.py +160 -0
  48. iam_validator/core/config/category_suggestions.py +104 -0
  49. iam_validator/core/config/condition_requirements.py +155 -0
  50. iam_validator/core/config/config_loader.py +472 -0
  51. iam_validator/core/config/defaults.py +523 -0
  52. iam_validator/core/config/principal_requirements.py +421 -0
  53. iam_validator/core/config/sensitive_actions.py +672 -0
  54. iam_validator/core/config/service_principals.py +95 -0
  55. iam_validator/core/config/wildcards.py +124 -0
  56. iam_validator/core/constants.py +74 -0
  57. iam_validator/core/formatters/__init__.py +27 -0
  58. iam_validator/core/formatters/base.py +147 -0
  59. iam_validator/core/formatters/console.py +59 -0
  60. iam_validator/core/formatters/csv.py +170 -0
  61. iam_validator/core/formatters/enhanced.py +440 -0
  62. iam_validator/core/formatters/html.py +672 -0
  63. iam_validator/core/formatters/json.py +33 -0
  64. iam_validator/core/formatters/markdown.py +63 -0
  65. iam_validator/core/formatters/sarif.py +251 -0
  66. iam_validator/core/models.py +327 -0
  67. iam_validator/core/policy_checks.py +656 -0
  68. iam_validator/core/policy_loader.py +396 -0
  69. iam_validator/core/pr_commenter.py +424 -0
  70. iam_validator/core/report.py +872 -0
  71. iam_validator/integrations/__init__.py +28 -0
  72. iam_validator/integrations/github_integration.py +815 -0
  73. iam_validator/integrations/ms_teams.py +442 -0
  74. iam_validator/sdk/__init__.py +187 -0
  75. iam_validator/sdk/arn_matching.py +382 -0
  76. iam_validator/sdk/context.py +222 -0
  77. iam_validator/sdk/exceptions.py +48 -0
  78. iam_validator/sdk/helpers.py +177 -0
  79. iam_validator/sdk/policy_utils.py +425 -0
  80. iam_validator/sdk/shortcuts.py +283 -0
  81. iam_validator/utils/__init__.py +31 -0
  82. iam_validator/utils/cache.py +105 -0
  83. iam_validator/utils/regex.py +206 -0
@@ -0,0 +1,396 @@
1
+ """IAM Policy Loader Module.
2
+
3
+ This module provides functionality to load and parse IAM policy documents
4
+ from various file formats (JSON, YAML) and directories.
5
+
6
+ The loader supports both eager loading (load all at once) and streaming
7
+ (process one file at a time) to optimize memory usage.
8
+
9
+ Example usage:
10
+ loader = PolicyLoader()
11
+
12
+ # Eager loading (loads all files into memory)
13
+ policy = loader.load_from_file("policy.json")
14
+ policies = loader.load_from_directory("./policies/", recursive=True)
15
+ policies = loader.load_from_path("./policies/", recursive=False)
16
+
17
+ # Streaming (memory-efficient, processes one file at a time)
18
+ for file_path, policy in loader.stream_from_path("./policies/"):
19
+ # Process each policy immediately
20
+ validate_and_report(file_path, policy)
21
+
22
+ # Batch processing (configurable batch size)
23
+ for batch in loader.batch_from_paths(["./policies/"], batch_size=10):
24
+ # Process batch of up to 10 policies
25
+ validate_batch(batch)
26
+ """
27
+
28
+ import json
29
+ import logging
30
+ from collections.abc import Generator
31
+ from pathlib import Path
32
+
33
+ import yaml
34
+
35
+ from iam_validator.core.models import IAMPolicy
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class PolicyLoader:
41
+ """Loads and parses IAM policy documents from files.
42
+
43
+ Supports both eager loading and streaming for memory efficiency.
44
+ """
45
+
46
+ SUPPORTED_EXTENSIONS = {".json", ".yaml", ".yml"}
47
+
48
+ def __init__(self, max_file_size_mb: int = 100) -> None:
49
+ """Initialize the policy loader.
50
+
51
+ Args:
52
+ max_file_size_mb: Maximum file size in MB to load (default: 100MB)
53
+ """
54
+ self.loaded_policies: list[tuple[str, IAMPolicy]] = []
55
+ self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
56
+
57
+ @staticmethod
58
+ def _find_statement_line_numbers(file_content: str) -> list[int]:
59
+ """Find line numbers for each statement in a JSON policy file.
60
+
61
+ Args:
62
+ file_content: Raw content of the policy file
63
+
64
+ Returns:
65
+ List of line numbers (1-indexed) for each statement's Sid or opening brace
66
+ """
67
+ lines = file_content.split("\n")
68
+ statement_lines = []
69
+ in_statement_array = False
70
+ brace_depth = 0
71
+ statement_start_line = None
72
+ current_statement_first_field = None
73
+
74
+ for line_num, line in enumerate(lines, start=1):
75
+ # Look for "Statement" array
76
+ if '"Statement"' in line or "'Statement'" in line:
77
+ in_statement_array = True
78
+ continue
79
+
80
+ if not in_statement_array:
81
+ continue
82
+
83
+ # Track opening braces for statement objects
84
+ for char in line:
85
+ if char == "{":
86
+ if brace_depth == 0 and statement_start_line is None:
87
+ # Found the start of a statement object
88
+ statement_start_line = line_num
89
+ current_statement_first_field = None
90
+ brace_depth += 1
91
+ elif char == "}":
92
+ brace_depth -= 1
93
+ if brace_depth == 0 and statement_start_line is not None:
94
+ # Completed a statement object
95
+ # Use first field line if found, otherwise use opening brace
96
+ statement_lines.append(
97
+ current_statement_first_field or statement_start_line
98
+ )
99
+ statement_start_line = None
100
+ current_statement_first_field = None
101
+ elif char == "]" and brace_depth == 0:
102
+ # End of Statement array
103
+ in_statement_array = False
104
+ break
105
+
106
+ # Track first field in statement (usually Sid, Effect, or Action)
107
+ if (
108
+ in_statement_array
109
+ and brace_depth == 1
110
+ and current_statement_first_field is None
111
+ and statement_start_line is not None
112
+ ):
113
+ stripped = line.strip()
114
+ # Look for first JSON field (e.g., "Sid":, "Effect":, "Action":)
115
+ if (
116
+ stripped
117
+ and stripped[0] == '"'
118
+ and ":" in stripped
119
+ and not stripped.startswith('"{')
120
+ ):
121
+ current_statement_first_field = line_num
122
+
123
+ return statement_lines
124
+
125
+ def _check_file_size(self, path: Path) -> bool:
126
+ """Check if file size is within limits.
127
+
128
+ Args:
129
+ path: Path to the file
130
+
131
+ Returns:
132
+ True if file size is acceptable, False otherwise
133
+ """
134
+ try:
135
+ file_size = path.stat().st_size
136
+ if file_size > self.max_file_size_bytes:
137
+ logger.warning(
138
+ f"File {path} exceeds maximum size "
139
+ f"({file_size / 1024 / 1024:.2f}MB > "
140
+ f"{self.max_file_size_bytes / 1024 / 1024:.2f}MB). Skipping."
141
+ )
142
+ return False
143
+ return True
144
+ except OSError as e:
145
+ logger.error(f"Failed to check file size for {path}: {e}")
146
+ return False
147
+
148
+ def load_from_file(self, file_path: str) -> IAMPolicy | None:
149
+ """Load a single IAM policy from a file.
150
+
151
+ Args:
152
+ file_path: Path to the policy file
153
+
154
+ Returns:
155
+ Parsed IAMPolicy or None if loading fails
156
+ """
157
+ path = Path(file_path)
158
+
159
+ if not path.exists():
160
+ logger.error(f"File not found: {file_path}")
161
+ return None
162
+
163
+ if not path.is_file():
164
+ logger.error(f"Not a file: {file_path}")
165
+ return None
166
+
167
+ if path.suffix.lower() not in self.SUPPORTED_EXTENSIONS:
168
+ logger.warning(
169
+ f"Unsupported file extension: {path.suffix}. "
170
+ f"Supported: {', '.join(self.SUPPORTED_EXTENSIONS)}"
171
+ )
172
+ return None
173
+
174
+ # Check file size before loading
175
+ if not self._check_file_size(path):
176
+ return None
177
+
178
+ try:
179
+ with open(path, encoding="utf-8") as f:
180
+ file_content = f.read()
181
+
182
+ # Parse line numbers for JSON files
183
+ statement_line_numbers = []
184
+ if path.suffix.lower() == ".json":
185
+ statement_line_numbers = self._find_statement_line_numbers(file_content)
186
+ data = json.loads(file_content)
187
+ else: # .yaml or .yml
188
+ data = yaml.safe_load(file_content)
189
+ # TODO: Add YAML line number tracking if needed
190
+
191
+ # Validate and parse the policy
192
+ policy = IAMPolicy.model_validate(data)
193
+
194
+ # Attach line numbers to statements
195
+ if statement_line_numbers:
196
+ for idx, statement in enumerate(policy.statement):
197
+ if idx < len(statement_line_numbers):
198
+ statement.line_number = statement_line_numbers[idx]
199
+
200
+ logger.info(f"Successfully loaded policy from {file_path}")
201
+ return policy
202
+
203
+ except json.JSONDecodeError as e:
204
+ logger.error(f"Invalid JSON in {file_path}: {e}")
205
+ return None
206
+ except yaml.YAMLError as e:
207
+ logger.error(f"Invalid YAML in {file_path}: {e}")
208
+ return None
209
+ except Exception as e:
210
+ logger.error(f"Failed to load policy from {file_path}: {e}")
211
+ return None
212
+
213
+ def load_from_directory(
214
+ self, directory_path: str, recursive: bool = True
215
+ ) -> list[tuple[str, IAMPolicy]]:
216
+ """Load all IAM policies from a directory.
217
+
218
+ Args:
219
+ directory_path: Path to the directory
220
+ recursive: Whether to search subdirectories
221
+
222
+ Returns:
223
+ List of tuples (file_path, policy)
224
+ """
225
+ path = Path(directory_path)
226
+
227
+ if not path.exists():
228
+ logger.error(f"Directory not found: {directory_path}")
229
+ return []
230
+
231
+ if not path.is_dir():
232
+ logger.error(f"Not a directory: {directory_path}")
233
+ return []
234
+
235
+ policies: list[tuple[str, IAMPolicy]] = []
236
+ pattern = "**/*" if recursive else "*"
237
+
238
+ for file_path in path.glob(pattern):
239
+ if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
240
+ policy = self.load_from_file(str(file_path))
241
+ if policy:
242
+ policies.append((str(file_path), policy))
243
+
244
+ logger.info(f"Loaded {len(policies)} policies from {directory_path}")
245
+ return policies
246
+
247
+ def load_from_path(self, path: str, recursive: bool = True) -> list[tuple[str, IAMPolicy]]:
248
+ """Load IAM policies from a file or directory.
249
+
250
+ Args:
251
+ path: Path to file or directory
252
+ recursive: Whether to search subdirectories (only applies to directories)
253
+
254
+ Returns:
255
+ List of tuples (file_path, policy)
256
+ """
257
+ path_obj = Path(path)
258
+
259
+ if path_obj.is_file():
260
+ policy = self.load_from_file(path)
261
+ return [(path, policy)] if policy else []
262
+ elif path_obj.is_dir():
263
+ return self.load_from_directory(path, recursive)
264
+ else:
265
+ logger.error(f"Path not found: {path}")
266
+ return []
267
+
268
+ def load_from_paths(
269
+ self, paths: list[str], recursive: bool = True
270
+ ) -> list[tuple[str, IAMPolicy]]:
271
+ """Load IAM policies from multiple files or directories.
272
+
273
+ Args:
274
+ paths: List of paths to files or directories
275
+ recursive: Whether to search subdirectories (only applies to directories)
276
+
277
+ Returns:
278
+ List of tuples (file_path, policy) from all paths combined
279
+ """
280
+ all_policies: list[tuple[str, IAMPolicy]] = []
281
+
282
+ for path in paths:
283
+ policies = self.load_from_path(path.strip(), recursive)
284
+ all_policies.extend(policies)
285
+
286
+ logger.info(f"Loaded {len(all_policies)} total policies from {len(paths)} path(s)")
287
+ return all_policies
288
+
289
+ def _get_policy_files(self, path: str, recursive: bool = True) -> Generator[Path, None, None]:
290
+ """Get all policy files from a path (file or directory).
291
+
292
+ This is a generator that yields file paths without loading them,
293
+ enabling memory-efficient iteration.
294
+
295
+ Args:
296
+ path: Path to file or directory
297
+ recursive: Whether to search subdirectories
298
+
299
+ Yields:
300
+ Path objects for policy files
301
+ """
302
+ path_obj = Path(path)
303
+
304
+ if path_obj.is_file():
305
+ if path_obj.suffix.lower() in self.SUPPORTED_EXTENSIONS:
306
+ yield path_obj
307
+ elif path_obj.is_dir():
308
+ pattern = "**/*" if recursive else "*"
309
+ for file_path in path_obj.glob(pattern):
310
+ if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_EXTENSIONS:
311
+ yield file_path
312
+ else:
313
+ logger.error(f"Path not found: {path}")
314
+
315
+ def stream_from_path(
316
+ self, path: str, recursive: bool = True
317
+ ) -> Generator[tuple[str, IAMPolicy], None, None]:
318
+ """Stream IAM policies from a file or directory one at a time.
319
+
320
+ This is a memory-efficient alternative to load_from_path that yields
321
+ policies one at a time instead of loading all into memory.
322
+
323
+ Args:
324
+ path: Path to file or directory
325
+ recursive: Whether to search subdirectories
326
+
327
+ Yields:
328
+ Tuples of (file_path, policy) for each successfully loaded policy
329
+ """
330
+ for file_path in self._get_policy_files(path, recursive):
331
+ policy = self.load_from_file(str(file_path))
332
+ if policy:
333
+ yield (str(file_path), policy)
334
+
335
+ def stream_from_paths(
336
+ self, paths: list[str], recursive: bool = True
337
+ ) -> Generator[tuple[str, IAMPolicy], None, None]:
338
+ """Stream IAM policies from multiple paths one at a time.
339
+
340
+ This is a memory-efficient alternative to load_from_paths that yields
341
+ policies one at a time instead of loading all into memory.
342
+
343
+ Args:
344
+ paths: List of paths to files or directories
345
+ recursive: Whether to search subdirectories
346
+
347
+ Yields:
348
+ Tuples of (file_path, policy) for each successfully loaded policy
349
+ """
350
+ for path in paths:
351
+ yield from self.stream_from_path(path.strip(), recursive)
352
+
353
+ def batch_from_paths(
354
+ self, paths: list[str], batch_size: int = 10, recursive: bool = True
355
+ ) -> Generator[list[tuple[str, IAMPolicy]], None, None]:
356
+ """Load policies in batches for balanced memory usage and performance.
357
+
358
+ Args:
359
+ paths: List of paths to files or directories
360
+ batch_size: Number of policies per batch (default: 10)
361
+ recursive: Whether to search subdirectories
362
+
363
+ Yields:
364
+ Lists of (file_path, policy) tuples, up to batch_size per list
365
+ """
366
+ batch: list[tuple[str, IAMPolicy]] = []
367
+
368
+ for file_path, policy in self.stream_from_paths(paths, recursive):
369
+ batch.append((file_path, policy))
370
+
371
+ if len(batch) >= batch_size:
372
+ yield batch
373
+ batch = []
374
+
375
+ # Yield remaining policies
376
+ if batch:
377
+ yield batch
378
+
379
+ @staticmethod
380
+ def parse_policy_string(policy_json: str) -> IAMPolicy | None:
381
+ """Parse an IAM policy from a JSON string.
382
+
383
+ Args:
384
+ policy_json: JSON string containing the policy
385
+
386
+ Returns:
387
+ Parsed IAMPolicy or None if parsing fails
388
+ """
389
+ try:
390
+ data = json.loads(policy_json)
391
+ policy = IAMPolicy.model_validate(data)
392
+ logger.info("Successfully parsed policy from string")
393
+ return policy
394
+ except Exception as e:
395
+ logger.error(f"Failed to parse policy string: {e}")
396
+ return None