iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.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 (51) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
  2. iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +81 -36
  6. iam_validator/checks/action_resource_matching.py +75 -37
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +7 -7
  9. iam_validator/checks/condition_type_mismatch.py +10 -8
  10. iam_validator/checks/full_wildcard.py +2 -8
  11. iam_validator/checks/mfa_condition_check.py +8 -8
  12. iam_validator/checks/policy_structure.py +577 -0
  13. iam_validator/checks/policy_type_validation.py +48 -32
  14. iam_validator/checks/principal_validation.py +86 -150
  15. iam_validator/checks/resource_validation.py +8 -8
  16. iam_validator/checks/sensitive_action.py +9 -11
  17. iam_validator/checks/service_wildcard.py +4 -10
  18. iam_validator/checks/set_operator_validation.py +11 -11
  19. iam_validator/checks/sid_uniqueness.py +8 -4
  20. iam_validator/checks/trust_policy_validation.py +512 -0
  21. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +5 -9
  24. iam_validator/checks/wildcard_resource.py +5 -9
  25. iam_validator/commands/validate.py +8 -14
  26. iam_validator/core/__init__.py +1 -2
  27. iam_validator/core/access_analyzer.py +1 -1
  28. iam_validator/core/access_analyzer_report.py +2 -2
  29. iam_validator/core/aws_fetcher.py +159 -64
  30. iam_validator/core/check_registry.py +83 -79
  31. iam_validator/core/config/condition_requirements.py +69 -17
  32. iam_validator/core/config/config_loader.py +1 -2
  33. iam_validator/core/config/defaults.py +74 -59
  34. iam_validator/core/config/service_principals.py +40 -3
  35. iam_validator/core/constants.py +57 -0
  36. iam_validator/core/formatters/console.py +10 -1
  37. iam_validator/core/formatters/csv.py +2 -1
  38. iam_validator/core/formatters/enhanced.py +42 -8
  39. iam_validator/core/formatters/markdown.py +2 -1
  40. iam_validator/core/ignore_patterns.py +297 -0
  41. iam_validator/core/models.py +35 -10
  42. iam_validator/core/policy_checks.py +34 -474
  43. iam_validator/core/policy_loader.py +98 -18
  44. iam_validator/core/report.py +65 -24
  45. iam_validator/integrations/github_integration.py +4 -5
  46. iam_validator/utils/__init__.py +4 -0
  47. iam_validator/utils/terminal.py +22 -0
  48. iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
  49. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  50. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  51. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,18 +9,19 @@ This module provides comprehensive validation of IAM policies including:
9
9
 
10
10
  import asyncio
11
11
  import logging
12
- import re
13
12
  from pathlib import Path
14
13
 
14
+ from iam_validator.core import constants
15
15
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
16
- from iam_validator.core.check_registry import CheckRegistry
16
+ from iam_validator.core.check_registry import CheckRegistry, create_default_registry
17
+ from iam_validator.core.config.config_loader import ConfigLoader
17
18
  from iam_validator.core.models import (
18
19
  IAMPolicy,
19
20
  PolicyType,
20
21
  PolicyValidationResult,
21
- Statement,
22
22
  ValidationIssue,
23
23
  )
24
+ from iam_validator.core.policy_loader import PolicyLoader
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -45,482 +46,23 @@ def _should_fail_on_issue(
45
46
  return issue.severity in fail_on_severities
46
47
 
47
48
 
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
49
  async def validate_policies(
473
- policies: list[tuple[str, IAMPolicy]],
50
+ policies: list[tuple[str, IAMPolicy]] | list[tuple[str, IAMPolicy, dict]],
474
51
  config_path: str | None = None,
475
- use_registry: bool = True,
476
52
  custom_checks_dir: str | None = None,
477
53
  policy_type: PolicyType = "IDENTITY_POLICY",
478
54
  ) -> list[PolicyValidationResult]:
479
55
  """Validate multiple policies concurrently.
480
56
 
481
57
  Args:
482
- policies: List of (file_path, policy) tuples
58
+ policies: List of (file_path, policy) or (file_path, policy, raw_dict) tuples
483
59
  config_path: Optional path to configuration file
484
- use_registry: If True, use CheckRegistry system; if False, use legacy validator
485
60
  custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
486
61
  policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
487
62
 
488
63
  Returns:
489
64
  List of validation results
490
65
  """
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
66
  # Load configuration
525
67
  config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
526
68
 
@@ -566,10 +108,10 @@ async def validate_policies(
566
108
 
567
109
  # Get cache settings from config
568
110
  cache_enabled = config.get_setting("cache_enabled", True)
569
- cache_ttl_hours = config.get_setting("cache_ttl_hours", 168) # 7 days default
111
+ cache_ttl_hours = config.get_setting("cache_ttl_hours", constants.DEFAULT_CACHE_TTL_HOURS)
570
112
  cache_directory = config.get_setting("cache_directory", None)
571
113
  aws_services_dir = config.get_setting("aws_services_dir", None)
572
- cache_ttl_seconds = cache_ttl_hours * 3600
114
+ cache_ttl_seconds = cache_ttl_hours * constants.SECONDS_PER_HOUR
573
115
 
574
116
  # Validate policies using registry
575
117
  async with AWSServiceFetcher(
@@ -580,9 +122,15 @@ async def validate_policies(
580
122
  ) as fetcher:
581
123
  tasks = [
582
124
  _validate_policy_with_registry(
583
- policy, file_path, registry, fetcher, fail_on_severities, policy_type
125
+ item[1], # policy
126
+ item[0], # file_path
127
+ registry,
128
+ fetcher,
129
+ fail_on_severities,
130
+ policy_type,
131
+ item[2] if len(item) == 3 else None, # raw_dict (optional)
584
132
  )
585
- for file_path, policy in policies
133
+ for item in policies
586
134
  ]
587
135
 
588
136
  results = await asyncio.gather(*tasks)
@@ -597,6 +145,7 @@ async def _validate_policy_with_registry(
597
145
  fetcher: AWSServiceFetcher,
598
146
  fail_on_severities: list[str] | None = None,
599
147
  policy_type: PolicyType = "IDENTITY_POLICY",
148
+ raw_policy_dict: dict | None = None,
600
149
  ) -> PolicyValidationResult:
601
150
  """Validate a single policy using the CheckRegistry system.
602
151
 
@@ -607,34 +156,45 @@ async def _validate_policy_with_registry(
607
156
  fetcher: AWS service fetcher instance
608
157
  fail_on_severities: List of severity levels that should cause validation to fail
609
158
  policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
159
+ raw_policy_dict: Raw policy dictionary for structural validation (optional, will be loaded if not provided)
610
160
 
611
161
  Returns:
612
162
  PolicyValidationResult with all findings
613
163
  """
614
164
  result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
615
165
 
166
+ # Load raw dict if not provided (for structural validation)
167
+ if raw_policy_dict is None:
168
+ loader = PolicyLoader()
169
+ loaded_result = loader.load_from_file(policy_file, return_raw_dict=True)
170
+ if loaded_result and isinstance(loaded_result, tuple):
171
+ raw_policy_dict = loaded_result[1]
172
+
616
173
  # Apply automatic policy-type validation (not configurable - always runs)
617
- from iam_validator.checks import policy_type_validation
174
+ # Note: Import here to avoid circular import (policy_checks -> checks -> sdk -> policy_checks)
175
+ from iam_validator.checks import ( # pylint: disable=import-outside-toplevel
176
+ policy_type_validation,
177
+ )
618
178
 
619
179
  policy_type_issues = await policy_type_validation.execute_policy(
620
180
  policy, policy_file, policy_type=policy_type
621
181
  )
622
- result.issues.extend(policy_type_issues)
182
+ result.issues.extend(policy_type_issues) # pylint: disable=no-member
623
183
 
624
184
  # Run policy-level checks first (checks that need to see the entire policy)
625
185
  # These checks examine relationships between statements, not individual statements
626
186
  policy_level_issues = await registry.execute_policy_checks(
627
- policy, policy_file, fetcher, policy_type
187
+ policy, policy_file, fetcher, policy_type, raw_policy_dict=raw_policy_dict
628
188
  )
629
- result.issues.extend(policy_level_issues)
189
+ result.issues.extend(policy_level_issues) # pylint: disable=no-member
630
190
 
631
191
  # Execute all statement-level checks for each statement
632
- for idx, statement in enumerate(policy.statement):
192
+ for idx, statement in enumerate(policy.statement or []):
633
193
  # Execute all registered checks in parallel (with ignore_patterns filtering)
634
194
  issues = await registry.execute_checks_parallel(statement, idx, fetcher, policy_file)
635
195
 
636
196
  # Add issues to result
637
- result.issues.extend(issues)
197
+ result.issues.extend(issues) # pylint: disable=no-member
638
198
 
639
199
  # Update counters (approximate based on what was checked)
640
200
  actions = statement.get_actions()