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,656 @@
1
+ """IAM Policy Validation Module.
2
+
3
+ This module provides comprehensive validation of IAM policies including:
4
+ - Action validation against AWS Service Reference API
5
+ - Condition key validation
6
+ - Resource ARN format validation
7
+ - Security best practices checks
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ import re
13
+ from pathlib import Path
14
+
15
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
16
+ from iam_validator.core.check_registry import CheckRegistry
17
+ from iam_validator.core.models import (
18
+ IAMPolicy,
19
+ PolicyType,
20
+ PolicyValidationResult,
21
+ Statement,
22
+ ValidationIssue,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _should_fail_on_issue(
29
+ issue: ValidationIssue, fail_on_severities: list[str] | None = None
30
+ ) -> bool:
31
+ """Determine if an issue should cause validation to fail.
32
+
33
+ Args:
34
+ issue: Validation issue to check
35
+ fail_on_severities: List of severity levels that should cause failure
36
+ Defaults to ["error"] if not specified
37
+
38
+ Returns:
39
+ True if the issue should cause validation to fail
40
+ """
41
+ if not fail_on_severities:
42
+ fail_on_severities = ["error"] # Default: only fail on errors
43
+
44
+ # Check if issue severity is in the fail list
45
+ return issue.severity in fail_on_severities
46
+
47
+
48
+ class PolicyValidator:
49
+ """Validates IAM policies for correctness and security."""
50
+
51
+ def __init__(self, fetcher: AWSServiceFetcher):
52
+ """Initialize the validator.
53
+
54
+ Args:
55
+ fetcher: AWS service fetcher instance
56
+ """
57
+ self.fetcher = fetcher
58
+ self._file_cache: dict[str, list[str]] = {}
59
+
60
+ def _find_field_line(
61
+ self, policy_file: str, statement_line: int, search_term: str
62
+ ) -> int | None:
63
+ """Find the specific line number for a field within a statement.
64
+
65
+ Args:
66
+ policy_file: Path to the policy file
67
+ statement_line: Line number where the statement starts (Sid/first field line)
68
+ search_term: The term to search for (e.g., action name, resource ARN)
69
+
70
+ Returns:
71
+ Line number where the field is found, or None
72
+ """
73
+ try:
74
+ # Cache file contents
75
+ if policy_file not in self._file_cache:
76
+ with open(policy_file, encoding="utf-8") as f:
77
+ self._file_cache[policy_file] = f.readlines()
78
+
79
+ lines = self._file_cache[policy_file]
80
+
81
+ # Need to go back to find the opening brace of the statement
82
+ # Look backwards from statement_line to find the opening {
83
+ statement_start = statement_line
84
+ for i in range(statement_line - 1, max(0, statement_line - 10), -1):
85
+ if "{" in lines[i]:
86
+ statement_start = i + 1 # Convert to 1-indexed
87
+ break
88
+
89
+ # Now search from the statement opening brace
90
+ brace_depth = 0
91
+ in_statement = False
92
+
93
+ for i, line in enumerate(lines[statement_start - 1 :], start=statement_start):
94
+ # Track braces to stay within statement bounds
95
+ for char in line:
96
+ if char == "{":
97
+ brace_depth += 1
98
+ in_statement = True
99
+ elif char == "}":
100
+ brace_depth -= 1
101
+
102
+ # Search for the term in this line
103
+ if in_statement and search_term in line:
104
+ return i
105
+
106
+ # Exit if we've left the statement
107
+ if in_statement and brace_depth == 0:
108
+ break
109
+
110
+ return None
111
+
112
+ except Exception as e:
113
+ logger.debug(f"Could not find field line in {policy_file}: {e}")
114
+ return None
115
+
116
+ async def validate_policy(
117
+ self, policy: IAMPolicy, policy_file: str, policy_type: PolicyType = "IDENTITY_POLICY"
118
+ ) -> PolicyValidationResult:
119
+ """Validate a complete IAM policy.
120
+
121
+ Args:
122
+ policy: IAM policy to validate
123
+ policy_file: Path to the policy file
124
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
125
+
126
+ Returns:
127
+ PolicyValidationResult with all findings
128
+ """
129
+ result = PolicyValidationResult(
130
+ policy_file=policy_file, is_valid=True, policy_type=policy_type
131
+ )
132
+
133
+ # Apply automatic policy-type validation (not configurable - always runs)
134
+ from iam_validator.checks import policy_type_validation
135
+
136
+ policy_type_issues = await policy_type_validation.execute_policy(
137
+ policy, policy_file, policy_type=policy_type
138
+ )
139
+ result.issues.extend(policy_type_issues)
140
+
141
+ for idx, statement in enumerate(policy.statement):
142
+ # Get line number for this statement
143
+ statement_line = statement.line_number
144
+
145
+ # Validate actions
146
+ # Optimization: Batch actions by service and cache line lookups
147
+ actions = statement.get_actions()
148
+ non_wildcard_actions = [a for a in actions if a != "*"]
149
+
150
+ # Group actions by service prefix for batch validation
151
+ from collections import defaultdict
152
+
153
+ actions_by_service = defaultdict(list)
154
+ for action in non_wildcard_actions:
155
+ if ":" in action:
156
+ service_prefix = action.split(":")[0]
157
+ actions_by_service[service_prefix].append(action)
158
+ else:
159
+ # Invalid action format, validate individually
160
+ actions_by_service["_invalid"].append(action)
161
+
162
+ # Pre-fetch all required services in parallel
163
+ if actions_by_service:
164
+ service_prefixes = [s for s in actions_by_service.keys() if s != "_invalid"]
165
+ # Batch fetch services to warm up cache
166
+ fetch_results = await asyncio.gather(
167
+ *[self.fetcher.fetch_service_by_name(s) for s in service_prefixes],
168
+ return_exceptions=True, # Don't fail if a service doesn't exist
169
+ )
170
+
171
+ # Log any service fetch failures for debugging
172
+ # Note: Individual action validation will still work and report proper errors
173
+ for i, fetch_result in enumerate(fetch_results):
174
+ if isinstance(fetch_result, Exception):
175
+ service_name = service_prefixes[i]
176
+ logger.debug(
177
+ f"Pre-fetch failed for service '{service_name}': {fetch_result}. "
178
+ "Will validate actions individually."
179
+ )
180
+
181
+ # Cache action line lookups to avoid repeated file searches
182
+ action_line_cache = {}
183
+
184
+ for action in non_wildcard_actions:
185
+ # Look up line number once per action (cached)
186
+ if action not in action_line_cache:
187
+ action_line = None
188
+ if statement_line:
189
+ # Search for the full action string in quotes to avoid partial matches
190
+ # Try full action first (e.g., "s3:GetObject")
191
+ action_line = self._find_field_line(
192
+ policy_file, statement_line, f'"{action}"'
193
+ )
194
+ # If not found, try just the action part after colon
195
+ if not action_line and ":" in action:
196
+ action_name = action.split(":")[-1]
197
+ action_line = self._find_field_line(
198
+ policy_file, statement_line, f'"{action_name}"'
199
+ )
200
+ action_line_cache[action] = action_line or statement_line
201
+
202
+ await self._validate_action(
203
+ action,
204
+ idx,
205
+ statement.sid,
206
+ action_line_cache[action],
207
+ result,
208
+ )
209
+
210
+ # Validate condition keys if present
211
+ # Optimization: Cache condition line lookups and batch validations
212
+ if statement.condition:
213
+ # Pre-filter non-wildcard actions once
214
+ non_wildcard_actions = [a for a in actions if a != "*"]
215
+
216
+ # Cache condition key line numbers to avoid repeated file searches
217
+ condition_line_cache = {}
218
+
219
+ for operator, conditions in statement.condition.items():
220
+ for condition_key in conditions.keys():
221
+ # Look up line number once per condition key
222
+ if condition_key not in condition_line_cache:
223
+ condition_line = None
224
+ if statement_line:
225
+ condition_line = self._find_field_line(
226
+ policy_file, statement_line, condition_key
227
+ )
228
+ condition_line_cache[condition_key] = condition_line or statement_line
229
+
230
+ # Validate condition key against all non-wildcard actions
231
+ for action in non_wildcard_actions:
232
+ await self._validate_condition_key(
233
+ action,
234
+ condition_key,
235
+ idx,
236
+ statement.sid,
237
+ condition_line_cache[condition_key],
238
+ result,
239
+ )
240
+
241
+ # Validate resources
242
+ resources = statement.get_resources()
243
+ for resource in resources:
244
+ if resource != "*": # Skip wildcard resources
245
+ # Try to find specific resource line
246
+ resource_line = None
247
+ if statement_line:
248
+ resource_line = self._find_field_line(policy_file, statement_line, resource)
249
+ self._validate_resource(
250
+ resource,
251
+ idx,
252
+ statement.sid,
253
+ resource_line or statement_line,
254
+ result,
255
+ )
256
+
257
+ # Security best practice checks
258
+ self._check_security_best_practices(statement, idx, statement_line, result, policy_file)
259
+
260
+ # Update final validation status
261
+ # Default to failing only on "error" severity for legacy validator
262
+ result.is_valid = len([i for i in result.issues if _should_fail_on_issue(i)]) == 0
263
+
264
+ return result
265
+
266
+ async def _validate_action(
267
+ self,
268
+ action: str,
269
+ statement_idx: int,
270
+ statement_sid: str | None,
271
+ line_number: int | None,
272
+ result: PolicyValidationResult,
273
+ ) -> None:
274
+ """Validate a single action."""
275
+ result.actions_checked += 1
276
+
277
+ # Handle wildcard patterns like "s3:Get*"
278
+ if "*" in action and action != "*":
279
+ # Validate the service prefix exists
280
+ try:
281
+ service_prefix = action.split(":")[0]
282
+ await self.fetcher.fetch_service_by_name(service_prefix)
283
+ # For now, accept wildcard actions if service exists
284
+ logger.debug(f"Wildcard action validated: {action}")
285
+ return
286
+ except Exception:
287
+ result.issues.append(
288
+ ValidationIssue(
289
+ severity="warning",
290
+ statement_sid=statement_sid,
291
+ statement_index=statement_idx,
292
+ issue_type="wildcard_action",
293
+ message=f"Wildcard action '{action}' uses unverified service",
294
+ action=action,
295
+ suggestion="Consider being more specific with action permissions",
296
+ line_number=line_number,
297
+ )
298
+ )
299
+ return
300
+
301
+ is_valid, error_msg, is_wildcard = await self.fetcher.validate_action(action)
302
+
303
+ if not is_valid:
304
+ result.issues.append(
305
+ ValidationIssue(
306
+ severity="error",
307
+ statement_sid=statement_sid,
308
+ statement_index=statement_idx,
309
+ issue_type="invalid_action",
310
+ message=error_msg or f"Invalid action: {action}",
311
+ action=action,
312
+ line_number=line_number,
313
+ )
314
+ )
315
+
316
+ async def _validate_condition_key(
317
+ self,
318
+ action: str,
319
+ condition_key: str,
320
+ statement_idx: int,
321
+ statement_sid: str | None,
322
+ line_number: int | None,
323
+ result: PolicyValidationResult,
324
+ ) -> None:
325
+ """Validate a condition key against an action."""
326
+ result.condition_keys_checked += 1
327
+
328
+ is_valid, error_msg = await self.fetcher.validate_condition_key(action, condition_key)
329
+
330
+ if not is_valid:
331
+ result.issues.append(
332
+ ValidationIssue(
333
+ severity="warning",
334
+ statement_sid=statement_sid,
335
+ statement_index=statement_idx,
336
+ issue_type="invalid_condition_key",
337
+ message=error_msg or f"Invalid condition key: {condition_key}",
338
+ action=action,
339
+ condition_key=condition_key,
340
+ line_number=line_number,
341
+ )
342
+ )
343
+
344
+ def _validate_resource(
345
+ self,
346
+ resource: str,
347
+ statement_idx: int,
348
+ statement_sid: str | None,
349
+ line_number: int | None,
350
+ result: PolicyValidationResult,
351
+ ) -> None:
352
+ """Validate resource ARN format."""
353
+ result.resources_checked += 1
354
+
355
+ # Basic ARN format: arn:partition:service:region:account-id:resource-type/resource-id
356
+ arn_pattern = r"^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\-]+:[a-z0-9\-]*:[0-9]*:.+$"
357
+
358
+ if not re.match(arn_pattern, resource, re.IGNORECASE):
359
+ result.issues.append(
360
+ ValidationIssue(
361
+ severity="error",
362
+ statement_sid=statement_sid,
363
+ statement_index=statement_idx,
364
+ issue_type="invalid_resource",
365
+ message=f"Invalid ARN format: {resource}",
366
+ resource=resource,
367
+ suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
368
+ line_number=line_number,
369
+ )
370
+ )
371
+
372
+ def _check_security_best_practices(
373
+ self,
374
+ statement: Statement,
375
+ statement_idx: int,
376
+ line_number: int | None,
377
+ result: PolicyValidationResult,
378
+ policy_file: str,
379
+ ) -> None:
380
+ """Check for security best practices."""
381
+
382
+ # Check for overly permissive wildcards
383
+ actions = statement.get_actions()
384
+ resources = statement.get_resources()
385
+
386
+ if statement.effect == "Allow":
387
+ # Check for "*" in actions
388
+ if "*" in actions:
389
+ # Try to find "Action" field line
390
+ action_field_line = None
391
+ if line_number:
392
+ action_field_line = self._find_field_line(policy_file, line_number, '"Action"')
393
+ result.issues.append(
394
+ ValidationIssue(
395
+ severity="warning",
396
+ statement_sid=statement.sid,
397
+ statement_index=statement_idx,
398
+ issue_type="overly_permissive",
399
+ message="Statement allows all actions (*)",
400
+ suggestion="Consider limiting to specific actions needed",
401
+ line_number=action_field_line or line_number,
402
+ )
403
+ )
404
+
405
+ # Check for "*" in resources
406
+ if "*" in resources:
407
+ # Try to find "Resource" field line
408
+ resource_field_line = None
409
+ if line_number:
410
+ resource_field_line = self._find_field_line(
411
+ policy_file, line_number, '"Resource"'
412
+ )
413
+ result.issues.append(
414
+ ValidationIssue(
415
+ severity="warning",
416
+ statement_sid=statement.sid,
417
+ statement_index=statement_idx,
418
+ issue_type="overly_permissive",
419
+ message="Statement applies to all resources (*)",
420
+ suggestion="Consider limiting to specific resources",
421
+ line_number=resource_field_line or line_number,
422
+ )
423
+ )
424
+
425
+ # Check for both wildcards
426
+ if "*" in actions and "*" in resources:
427
+ result.issues.append(
428
+ ValidationIssue(
429
+ severity="error",
430
+ statement_sid=statement.sid,
431
+ statement_index=statement_idx,
432
+ issue_type="security_risk",
433
+ message="Statement allows all actions on all resources - CRITICAL SECURITY RISK",
434
+ suggestion="This grants full administrative access. Restrict to specific actions and resources.",
435
+ line_number=line_number,
436
+ )
437
+ )
438
+
439
+ # Check for missing conditions on sensitive actions
440
+ sensitive_actions = [
441
+ "iam:PassRole",
442
+ "iam:CreateUser",
443
+ "iam:CreateRole",
444
+ "iam:PutUserPolicy",
445
+ "iam:PutRolePolicy",
446
+ "s3:DeleteBucket",
447
+ "s3:PutBucketPolicy",
448
+ "ec2:TerminateInstances",
449
+ ]
450
+
451
+ for action in actions:
452
+ if action in sensitive_actions and not statement.condition:
453
+ # Try to find specific action line
454
+ action_line = None
455
+ if line_number:
456
+ action_name = action.split(":")[-1] if ":" in action else action
457
+ action_line = self._find_field_line(policy_file, line_number, action_name)
458
+ result.issues.append(
459
+ ValidationIssue(
460
+ severity="warning",
461
+ statement_sid=statement.sid,
462
+ statement_index=statement_idx,
463
+ issue_type="missing_condition",
464
+ message=f"Sensitive action '{action}' has no conditions",
465
+ action=action,
466
+ suggestion="Consider adding conditions to restrict when this action can be performed",
467
+ line_number=action_line or line_number,
468
+ )
469
+ )
470
+
471
+
472
+ async def validate_policies(
473
+ policies: list[tuple[str, IAMPolicy]],
474
+ config_path: str | None = None,
475
+ use_registry: bool = True,
476
+ custom_checks_dir: str | None = None,
477
+ policy_type: PolicyType = "IDENTITY_POLICY",
478
+ ) -> list[PolicyValidationResult]:
479
+ """Validate multiple policies concurrently.
480
+
481
+ Args:
482
+ policies: List of (file_path, policy) tuples
483
+ config_path: Optional path to configuration file
484
+ use_registry: If True, use CheckRegistry system; if False, use legacy validator
485
+ custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
486
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
487
+
488
+ Returns:
489
+ List of validation results
490
+ """
491
+ if not use_registry:
492
+ # Legacy path - use old PolicyValidator
493
+ # Load config for cache settings even in legacy mode
494
+ from iam_validator.core.config.config_loader import ConfigLoader
495
+
496
+ config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
497
+ cache_enabled = config.get_setting("cache_enabled", True)
498
+ cache_ttl_hours = config.get_setting("cache_ttl_hours", 168)
499
+ cache_directory = config.get_setting("cache_directory", None)
500
+ aws_services_dir = config.get_setting("aws_services_dir", None)
501
+ cache_ttl_seconds = cache_ttl_hours * 3600
502
+
503
+ async with AWSServiceFetcher(
504
+ enable_cache=cache_enabled,
505
+ cache_ttl=cache_ttl_seconds,
506
+ cache_dir=cache_directory,
507
+ aws_services_dir=aws_services_dir,
508
+ ) as fetcher:
509
+ validator = PolicyValidator(fetcher)
510
+
511
+ tasks = [
512
+ validator.validate_policy(policy, file_path, policy_type)
513
+ for file_path, policy in policies
514
+ ]
515
+
516
+ results = await asyncio.gather(*tasks)
517
+
518
+ return list(results)
519
+
520
+ # New path - use CheckRegistry system
521
+ from iam_validator.core.check_registry import create_default_registry
522
+ from iam_validator.core.config.config_loader import ConfigLoader
523
+
524
+ # Load configuration
525
+ config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
526
+
527
+ # Create registry with or without built-in checks based on configuration
528
+ enable_parallel = config.get_setting("parallel_execution", True)
529
+ enable_builtin_checks = config.get_setting("enable_builtin_checks", True)
530
+
531
+ registry = create_default_registry(
532
+ enable_parallel=enable_parallel, include_builtin_checks=enable_builtin_checks
533
+ )
534
+
535
+ if not enable_builtin_checks:
536
+ logger.info("Built-in checks disabled - using only custom checks")
537
+
538
+ # Apply configuration to built-in checks (if they were registered)
539
+ if enable_builtin_checks:
540
+ ConfigLoader.apply_config_to_registry(config, registry)
541
+
542
+ # Load custom checks from explicit module paths (old method)
543
+ custom_checks = ConfigLoader.load_custom_checks(config, registry)
544
+ if custom_checks:
545
+ logger.info(
546
+ f"Loaded {len(custom_checks)} custom checks from modules: {', '.join(custom_checks)}"
547
+ )
548
+
549
+ # Auto-discover custom checks from directory (new method)
550
+ # Priority: CLI arg > config file > default None
551
+ checks_dir = custom_checks_dir or config.custom_checks_dir
552
+ if checks_dir:
553
+ checks_dir_path = Path(checks_dir).resolve()
554
+ discovered_checks = ConfigLoader.discover_checks_in_directory(checks_dir_path, registry)
555
+ if discovered_checks:
556
+ logger.info(
557
+ f"Auto-discovered {len(discovered_checks)} custom checks from {checks_dir_path}"
558
+ )
559
+
560
+ # Apply configuration again to include custom checks
561
+ # This allows configuring auto-discovered checks via the config file
562
+ ConfigLoader.apply_config_to_registry(config, registry)
563
+
564
+ # Get fail_on_severity setting from config
565
+ fail_on_severities = config.get_setting("fail_on_severity", ["error"])
566
+
567
+ # Get cache settings from config
568
+ cache_enabled = config.get_setting("cache_enabled", True)
569
+ cache_ttl_hours = config.get_setting("cache_ttl_hours", 168) # 7 days default
570
+ cache_directory = config.get_setting("cache_directory", None)
571
+ aws_services_dir = config.get_setting("aws_services_dir", None)
572
+ cache_ttl_seconds = cache_ttl_hours * 3600
573
+
574
+ # Validate policies using registry
575
+ async with AWSServiceFetcher(
576
+ enable_cache=cache_enabled,
577
+ cache_ttl=cache_ttl_seconds,
578
+ cache_dir=cache_directory,
579
+ aws_services_dir=aws_services_dir,
580
+ ) as fetcher:
581
+ tasks = [
582
+ _validate_policy_with_registry(
583
+ policy, file_path, registry, fetcher, fail_on_severities, policy_type
584
+ )
585
+ for file_path, policy in policies
586
+ ]
587
+
588
+ results = await asyncio.gather(*tasks)
589
+
590
+ return list(results)
591
+
592
+
593
+ async def _validate_policy_with_registry(
594
+ policy: IAMPolicy,
595
+ policy_file: str,
596
+ registry: CheckRegistry,
597
+ fetcher: AWSServiceFetcher,
598
+ fail_on_severities: list[str] | None = None,
599
+ policy_type: PolicyType = "IDENTITY_POLICY",
600
+ ) -> PolicyValidationResult:
601
+ """Validate a single policy using the CheckRegistry system.
602
+
603
+ Args:
604
+ policy: IAM policy to validate
605
+ policy_file: Path to the policy file
606
+ registry: CheckRegistry instance with configured checks
607
+ fetcher: AWS service fetcher instance
608
+ fail_on_severities: List of severity levels that should cause validation to fail
609
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
610
+
611
+ Returns:
612
+ PolicyValidationResult with all findings
613
+ """
614
+ result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
615
+
616
+ # Apply automatic policy-type validation (not configurable - always runs)
617
+ from iam_validator.checks import policy_type_validation
618
+
619
+ policy_type_issues = await policy_type_validation.execute_policy(
620
+ policy, policy_file, policy_type=policy_type
621
+ )
622
+ result.issues.extend(policy_type_issues)
623
+
624
+ # Run policy-level checks first (checks that need to see the entire policy)
625
+ # These checks examine relationships between statements, not individual statements
626
+ policy_level_issues = await registry.execute_policy_checks(
627
+ policy, policy_file, fetcher, policy_type
628
+ )
629
+ result.issues.extend(policy_level_issues)
630
+
631
+ # Execute all statement-level checks for each statement
632
+ for idx, statement in enumerate(policy.statement):
633
+ # Execute all registered checks in parallel (with ignore_patterns filtering)
634
+ issues = await registry.execute_checks_parallel(statement, idx, fetcher, policy_file)
635
+
636
+ # Add issues to result
637
+ result.issues.extend(issues)
638
+
639
+ # Update counters (approximate based on what was checked)
640
+ actions = statement.get_actions()
641
+ resources = statement.get_resources()
642
+
643
+ result.actions_checked += len([a for a in actions if a != "*"])
644
+ result.resources_checked += len([r for r in resources if r != "*"])
645
+
646
+ # Count condition keys if present
647
+ if statement.condition:
648
+ for conditions in statement.condition.values():
649
+ result.condition_keys_checked += len(conditions)
650
+
651
+ # Update final validation status based on fail_on_severities configuration
652
+ result.is_valid = (
653
+ len([i for i in result.issues if _should_fail_on_issue(i, fail_on_severities)]) == 0
654
+ )
655
+
656
+ return result