iam-policy-validator 1.1.0__py3-none-any.whl → 1.1.1__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.

@@ -1,10 +1,16 @@
1
1
  """Security best practices check - validates security anti-patterns."""
2
2
 
3
- import re
4
- from functools import lru_cache
5
- from re import Pattern
6
3
  from typing import TYPE_CHECKING
7
4
 
5
+ from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
6
+ from iam_validator.checks.utils.sensitive_action_matcher import (
7
+ DEFAULT_SENSITIVE_ACTIONS,
8
+ check_sensitive_actions,
9
+ )
10
+ from iam_validator.checks.utils.wildcard_expansion import (
11
+ compile_wildcard_pattern,
12
+ expand_wildcard_actions,
13
+ )
8
14
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
9
15
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
10
16
  from iam_validator.core.models import Statement, ValidationIssue
@@ -13,49 +19,9 @@ if TYPE_CHECKING:
13
19
  from iam_validator.core.models import IAMPolicy
14
20
 
15
21
 
16
- # Global regex pattern cache for performance
17
- @lru_cache(maxsize=256)
18
- def _compile_pattern(pattern: str) -> Pattern[str] | None:
19
- """Compile and cache regex patterns.
20
-
21
- Args:
22
- pattern: Regex pattern string
23
-
24
- Returns:
25
- Compiled pattern or None if invalid
26
- """
27
- try:
28
- return re.compile(pattern)
29
- except re.error:
30
- return None
31
-
32
-
33
22
  class SecurityBestPracticesCheck(PolicyCheck):
34
23
  """Checks for common security anti-patterns and best practices violations."""
35
24
 
36
- # Default set of sensitive actions that should have conditions
37
- # Using frozenset for O(1) lookups and immutability
38
- DEFAULT_SENSITIVE_ACTIONS = frozenset(
39
- {
40
- "iam:CreateUser",
41
- "iam:CreateRole",
42
- "iam:PutUserPolicy",
43
- "iam:PutRolePolicy",
44
- "iam:AttachUserPolicy",
45
- "iam:AttachRolePolicy",
46
- "iam:CreateAccessKey",
47
- "iam:DeleteUser",
48
- "iam:DeleteRole",
49
- "s3:DeleteBucket",
50
- "s3:PutBucketPolicy",
51
- "s3:DeleteBucketPolicy",
52
- "ec2:TerminateInstances",
53
- "ec2:DeleteVolume",
54
- "rds:DeleteDBInstance",
55
- "lambda:DeleteFunction",
56
- }
57
- )
58
-
59
25
  @property
60
26
  def check_id(self) -> str:
61
27
  return "security_best_practices"
@@ -119,33 +85,39 @@ class SecurityBestPracticesCheck(PolicyCheck):
119
85
  # Check 2: Wildcard resource check
120
86
  if self._is_sub_check_enabled(config, "wildcard_resource_check"):
121
87
  if "*" in resources:
122
- severity = self._get_sub_check_severity(
123
- config, "wildcard_resource_check", "warning"
124
- )
125
- sub_check_config = config.config.get("wildcard_resource_check", {})
126
-
127
- message = sub_check_config.get("message", "Statement applies to all resources (*)")
128
- suggestion_text = sub_check_config.get(
129
- "suggestion", "Consider limiting to specific resources"
130
- )
131
- example = sub_check_config.get("example", "")
132
-
133
- # Combine suggestion + example
134
- suggestion = (
135
- f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
136
- )
88
+ # Check if all actions are in the allowed_wildcards list
89
+ # This allows Resource: "*" when only safe read-only wildcard actions are used
90
+ allowed_wildcards = self._get_allowed_wildcards_for_resources(config)
91
+
92
+ # Check if ALL actions (excluding full wildcard "*") match allowed patterns
93
+ non_wildcard_actions = [a for a in actions if a != "*"]
94
+
95
+ if allowed_wildcards and non_wildcard_actions:
96
+ # Check if all actions are allowed wildcards
97
+ all_actions_allowed = all(
98
+ self._is_action_allowed_wildcard(action, allowed_wildcards)
99
+ for action in non_wildcard_actions
100
+ )
137
101
 
138
- issues.append(
139
- ValidationIssue(
140
- severity=severity,
141
- statement_sid=statement_sid,
142
- statement_index=statement_idx,
143
- issue_type="overly_permissive",
144
- message=message,
145
- suggestion=suggestion,
146
- line_number=line_number,
102
+ # If all actions are in the allowed list, skip the wildcard resource warning
103
+ if all_actions_allowed:
104
+ # All actions are safe wildcards, Resource: "*" is acceptable
105
+ pass
106
+ else:
107
+ # Some actions are not in allowed list, flag the issue
108
+ self._add_wildcard_resource_issue(
109
+ issues,
110
+ config,
111
+ statement_sid,
112
+ statement_idx,
113
+ line_number,
114
+ )
115
+ else:
116
+ # No allowed_wildcards configured OR only has "*" action
117
+ # Always flag wildcard resources in these cases
118
+ self._add_wildcard_resource_issue(
119
+ issues, config, statement_sid, statement_idx, line_number
147
120
  )
148
- )
149
121
 
150
122
  # Check 3: Critical - both wildcards together
151
123
  if self._is_sub_check_enabled(config, "full_wildcard_check"):
@@ -243,8 +215,13 @@ class SecurityBestPracticesCheck(PolicyCheck):
243
215
  if self._is_sub_check_enabled(config, "sensitive_action_check"):
244
216
  has_conditions = statement.condition is not None and len(statement.condition) > 0
245
217
 
218
+ # Expand wildcards to actual actions using AWS API
219
+ expanded_actions = await expand_wildcard_actions(actions, fetcher)
220
+
246
221
  # Check if sensitive actions match using any_of/all_of logic
247
- is_sensitive, matched_actions = self._check_sensitive_actions(actions, config)
222
+ is_sensitive, matched_actions = check_sensitive_actions(
223
+ expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
224
+ )
248
225
 
249
226
  if is_sensitive and not has_conditions:
250
227
  severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
@@ -353,164 +330,32 @@ class SecurityBestPracticesCheck(PolicyCheck):
353
330
  # Check sensitive_actions configuration
354
331
  if sensitive_actions_config:
355
332
  policy_issues.extend(
356
- self._check_policy_level_actions(
333
+ check_policy_level_actions(
357
334
  list(all_actions),
358
335
  statement_map,
359
336
  sensitive_actions_config,
360
337
  config,
361
338
  "actions",
339
+ self._get_sub_check_severity,
362
340
  )
363
341
  )
364
342
 
365
343
  # Check sensitive_action_patterns configuration
366
344
  if sensitive_patterns_config:
367
345
  policy_issues.extend(
368
- self._check_policy_level_actions(
346
+ check_policy_level_actions(
369
347
  list(all_actions),
370
348
  statement_map,
371
349
  sensitive_patterns_config,
372
350
  config,
373
351
  "patterns",
352
+ self._get_sub_check_severity,
374
353
  )
375
354
  )
376
355
 
377
356
  issues.extend(policy_issues)
378
357
  return issues
379
358
 
380
- def _check_policy_level_actions(
381
- self,
382
- all_actions: list[str],
383
- statement_map: dict[str, list[tuple[int, str | None]]],
384
- config,
385
- check_config: CheckConfig,
386
- check_type: str,
387
- ) -> list[ValidationIssue]:
388
- """
389
- Check for policy-level privilege escalation patterns.
390
-
391
- Args:
392
- all_actions: All actions across the entire policy
393
- statement_map: Mapping of action -> [(statement_idx, sid), ...]
394
- config: The sensitive_actions or sensitive_action_patterns configuration
395
- check_config: Full check configuration
396
- check_type: Either "actions" (exact match) or "patterns" (regex match)
397
-
398
- Returns:
399
- List of ValidationIssue objects
400
- """
401
- import re
402
-
403
- issues = []
404
-
405
- if not config:
406
- return issues
407
-
408
- # Handle list of items (could be simple strings or dicts with all_of/any_of)
409
- if isinstance(config, list):
410
- for item in config:
411
- if isinstance(item, dict) and "all_of" in item:
412
- # This is a privilege escalation pattern - all actions must be present
413
- required_actions = item["all_of"]
414
- matched_actions = []
415
-
416
- if check_type == "actions":
417
- # Exact matching
418
- matched_actions = [a for a in all_actions if a in required_actions]
419
- else:
420
- # Pattern matching - for each pattern, find actions that match
421
- for pattern in required_actions:
422
- for action in all_actions:
423
- try:
424
- if re.match(pattern, action):
425
- matched_actions.append(action)
426
- break # Found at least one match for this pattern
427
- except re.error:
428
- continue
429
-
430
- # Check if ALL required actions/patterns are present
431
- if len(matched_actions) >= len(required_actions):
432
- # Privilege escalation detected!
433
- severity = self._get_sub_check_severity(
434
- check_config, "sensitive_action_check", "error"
435
- )
436
-
437
- # Collect which statements these actions appear in
438
- statement_refs = []
439
- for action in matched_actions:
440
- if action in statement_map:
441
- for stmt_idx, sid in statement_map[action]:
442
- sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
443
- statement_refs.append(f"Statement {sid_str}: {action}")
444
-
445
- action_list = "', '".join(matched_actions)
446
- stmt_details = "\n - ".join(statement_refs)
447
-
448
- issues.append(
449
- ValidationIssue(
450
- severity=severity,
451
- statement_sid=None, # Policy-level issue
452
- statement_index=-1, # -1 indicates policy-level issue
453
- issue_type="privilege_escalation",
454
- message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
455
- suggestion=f"These actions combined allow privilege escalation. Consider:\n"
456
- f" 1. Splitting into separate policies for different users/roles\n"
457
- f" 2. Adding strict conditions to limit when these actions can be used together\n"
458
- f" 3. Reviewing if all these permissions are truly necessary\n\n"
459
- f"Actions found in:\n - {stmt_details}",
460
- line_number=None,
461
- )
462
- )
463
-
464
- # Handle dict with all_of at the top level
465
- elif isinstance(config, dict) and "all_of" in config:
466
- required_actions = config["all_of"]
467
- matched_actions = []
468
-
469
- if check_type == "actions":
470
- matched_actions = [a for a in all_actions if a in required_actions]
471
- else:
472
- for pattern in required_actions:
473
- for action in all_actions:
474
- try:
475
- if re.match(pattern, action):
476
- matched_actions.append(action)
477
- break
478
- except re.error:
479
- continue
480
-
481
- if len(matched_actions) >= len(required_actions):
482
- severity = self._get_sub_check_severity(
483
- check_config, "sensitive_action_check", "error"
484
- )
485
-
486
- statement_refs = []
487
- for action in matched_actions:
488
- if action in statement_map:
489
- for stmt_idx, sid in statement_map[action]:
490
- sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
491
- statement_refs.append(f"Statement {sid_str}: {action}")
492
-
493
- action_list = "', '".join(matched_actions)
494
- stmt_details = "\n - ".join(statement_refs)
495
-
496
- issues.append(
497
- ValidationIssue(
498
- severity=severity,
499
- statement_sid=None,
500
- statement_index=-1, # -1 indicates policy-level issue
501
- issue_type="privilege_escalation",
502
- message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
503
- suggestion=f"These actions combined allow privilege escalation. Consider:\n"
504
- f" 1. Splitting into separate policies for different users/roles\n"
505
- f" 2. Adding strict conditions to limit when these actions can be used together\n"
506
- f" 3. Reviewing if all these permissions are truly necessary\n\n"
507
- f"Actions found in:\n - {stmt_details}",
508
- line_number=None,
509
- )
510
- )
511
-
512
- return issues
513
-
514
359
  def _is_sub_check_enabled(self, config: CheckConfig, sub_check_name: str) -> bool:
515
360
  """Check if a sub-check is enabled in the configuration."""
516
361
  if sub_check_name not in config.config:
@@ -533,6 +378,50 @@ class SecurityBestPracticesCheck(PolicyCheck):
533
378
  return sub_check_config.get("severity", default)
534
379
  return default
535
380
 
381
+ def _add_wildcard_resource_issue(
382
+ self,
383
+ issues: list[ValidationIssue],
384
+ config: CheckConfig,
385
+ statement_sid: str | None,
386
+ statement_idx: int,
387
+ line_number: int | None,
388
+ ) -> None:
389
+ """Add a wildcard resource issue to the issues list.
390
+
391
+ This is a helper method to avoid code duplication when adding
392
+ wildcard resource warnings.
393
+
394
+ Args:
395
+ issues: List to append the issue to
396
+ config: Check configuration
397
+ statement_sid: Statement ID
398
+ statement_idx: Statement index
399
+ line_number: Line number in the policy file
400
+ """
401
+ severity = self._get_sub_check_severity(config, "wildcard_resource_check", "warning")
402
+ sub_check_config = config.config.get("wildcard_resource_check", {})
403
+
404
+ message = sub_check_config.get("message", "Statement applies to all resources (*)")
405
+ suggestion_text = sub_check_config.get(
406
+ "suggestion", "Consider limiting to specific resources"
407
+ )
408
+ example = sub_check_config.get("example", "")
409
+
410
+ # Combine suggestion + example
411
+ suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
412
+
413
+ issues.append(
414
+ ValidationIssue(
415
+ severity=severity,
416
+ statement_sid=statement_sid,
417
+ statement_index=statement_idx,
418
+ issue_type="overly_permissive",
419
+ message=message,
420
+ suggestion=suggestion,
421
+ line_number=line_number,
422
+ )
423
+ )
424
+
536
425
  def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
537
426
  """
538
427
  Get list of services that are allowed to use service-level wildcards.
@@ -554,212 +443,73 @@ class SecurityBestPracticesCheck(PolicyCheck):
554
443
 
555
444
  return set()
556
445
 
557
- def _check_sensitive_actions(
558
- self, actions: list[str], config: CheckConfig
559
- ) -> tuple[bool, list[str]]:
560
- """
561
- Check if actions match sensitive action criteria with any_of/all_of support.
562
-
563
- Returns:
564
- tuple[bool, list[str]]: (is_sensitive, matched_actions)
565
- - is_sensitive: True if the actions match the sensitive criteria
566
- - matched_actions: List of actions that matched the criteria
567
- """
568
- # Filter out wildcards
569
- filtered_actions = [a for a in actions if a != "*"]
570
- if not filtered_actions:
571
- return False, []
572
-
573
- # Get configuration for both sensitive_actions and sensitive_action_patterns
574
- sub_check_config = config.config.get("sensitive_action_check", {})
575
- if not isinstance(sub_check_config, dict):
576
- return False, []
577
-
578
- sensitive_actions_config = sub_check_config.get("sensitive_actions")
579
- sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
580
-
581
- # Check sensitive_actions (exact matches)
582
- actions_match, actions_matched = self._check_actions_config(
583
- filtered_actions, sensitive_actions_config
584
- )
446
+ def _is_action_allowed_wildcard(
447
+ self, action: str, allowed_wildcards: frozenset[str] | list[str] | set[str]
448
+ ) -> bool:
449
+ """Check if an action matches the allowed_wildcards list.
585
450
 
586
- # Check sensitive_action_patterns (regex patterns)
587
- patterns_match, patterns_matched = self._check_patterns_config(
588
- filtered_actions, sensitive_patterns_config
589
- )
451
+ This method checks if a given action is in the allowed_wildcards configuration
452
+ from action_validation_check. This is used to determine if wildcard resources
453
+ are acceptable when only safe wildcard actions are used.
590
454
 
591
- # Combine results - if either matched, we consider it sensitive
592
- is_sensitive = actions_match or patterns_match
593
- # Use set operations for efficient deduplication
594
- matched_set = set(actions_matched) | set(patterns_matched)
595
- matched_actions = list(matched_set)
455
+ Args:
456
+ action: The action to check (e.g., "s3:List*", "ec2:DescribeInstances")
457
+ allowed_wildcards: Set or list of allowed wildcard patterns
596
458
 
597
- return is_sensitive, matched_actions
459
+ Returns:
460
+ True if the action matches any pattern in the allowlist
598
461
 
599
- def _check_actions_config(self, actions: list[str], config) -> tuple[bool, list[str]]:
462
+ Note:
463
+ Exact matches use O(1) set lookup for performance.
464
+ Pattern matches (wildcards in allowlist) require O(n) iteration.
600
465
  """
601
- Check actions against sensitive_actions configuration.
602
-
603
- Supports:
604
- - Simple list: ["action1", "action2"] (backward compatible, any_of logic)
605
- - any_of: {"any_of": ["action1", "action2"]}
606
- - all_of: {"all_of": ["action1", "action2"]}
607
- - Multiple groups: [{"all_of": [...]}, {"all_of": [...]}, "action3"]
466
+ # Fast O(1) exact match using set membership
467
+ if action in allowed_wildcards:
468
+ return True
469
+
470
+ # Pattern match - check if action matches any pattern in allowlist
471
+ # This is needed when allowlist contains wildcards like "s3:*"
472
+ # Uses cached compiled patterns for 20-30x speedup
473
+ for pattern in allowed_wildcards:
474
+ # Skip exact matches (already checked above)
475
+ if "*" not in pattern:
476
+ continue
608
477
 
609
- Returns:
610
- tuple[bool, list[str]]: (matches, matched_actions)
611
- """
612
- if not config:
613
- # If no config, fall back to defaults with any_of logic
614
- # DEFAULT_SENSITIVE_ACTIONS is already a frozenset for O(1) lookups
615
- matched = [a for a in actions if a in self.DEFAULT_SENSITIVE_ACTIONS]
616
- return len(matched) > 0, matched
617
-
618
- # Handle simple list with potential mixed items
619
- if isinstance(config, list):
620
- # Use set for O(1) membership checks
621
- all_matched = set()
622
- actions_set = set(actions) # Convert once for O(1) lookups
623
-
624
- for item in config:
625
- # Each item can be a string, or a dict with any_of/all_of
626
- if isinstance(item, str):
627
- # Simple string - check if action matches (O(1) lookup)
628
- if item in actions_set:
629
- all_matched.add(item)
630
- elif isinstance(item, dict):
631
- # Recurse for dict items
632
- matches, matched = self._check_actions_config(actions, item)
633
- if matches:
634
- all_matched.update(matched)
635
-
636
- return len(all_matched) > 0, list(all_matched)
637
-
638
- # Handle dict with any_of/all_of
639
- if isinstance(config, dict):
640
- # any_of: at least one action must match
641
- if "any_of" in config:
642
- # Convert once for O(1) intersection
643
- any_of_set = set(config["any_of"])
644
- actions_set = set(actions)
645
- matched = list(any_of_set & actions_set)
646
- return len(matched) > 0, matched
647
-
648
- # all_of: all specified actions must be present in the statement
649
- if "all_of" in config:
650
- all_of_set = set(config["all_of"])
651
- actions_set = set(actions)
652
- matched = list(all_of_set & actions_set)
653
- # All required actions must be present
654
- return all_of_set.issubset(actions_set), matched
655
-
656
- return False, []
657
-
658
- def _check_patterns_config(self, actions: list[str], config) -> tuple[bool, list[str]]:
659
- """
660
- Check actions against sensitive_action_patterns configuration.
478
+ # Use cached compiled pattern
479
+ compiled = compile_wildcard_pattern(pattern)
480
+ if compiled.match(action):
481
+ return True
661
482
 
662
- Supports:
663
- - Simple list: ["^pattern1.*", "^pattern2.*"] (backward compatible, any_of logic)
664
- - any_of: {"any_of": ["^pattern1.*", "^pattern2.*"]}
665
- - all_of: {"all_of": ["^pattern1.*", "^pattern2.*"]}
666
- - Multiple groups: [{"all_of": [...]}, {"any_of": [...]}, "^pattern.*"]
483
+ return False
667
484
 
668
- Returns:
669
- tuple[bool, list[str]]: (matches, matched_actions)
485
+ def _get_allowed_wildcards_for_resources(self, config: CheckConfig) -> frozenset[str]:
486
+ """Get allowed_wildcards for resource check configuration.
670
487
 
671
- Performance:
672
- Uses cached compiled regex patterns for 10-50x speedup
673
- """
674
- if not config:
675
- return False, []
676
-
677
- # Handle simple list with potential mixed items
678
- if isinstance(config, list):
679
- # Use set for O(1) membership checks instead of list
680
- all_matched = set()
681
-
682
- for item in config:
683
- # Each item can be a string pattern, or a dict with any_of/all_of
684
- if isinstance(item, str):
685
- # Simple string pattern - check if any action matches
686
- # Use cached compiled pattern
687
- compiled = _compile_pattern(item)
688
- if compiled:
689
- for action in actions:
690
- if compiled.match(action):
691
- all_matched.add(action)
692
- elif isinstance(item, dict):
693
- # Recurse for dict items
694
- matches, matched = self._check_patterns_config(actions, item)
695
- if matches:
696
- all_matched.update(matched)
697
-
698
- return len(all_matched) > 0, list(all_matched)
699
-
700
- # Handle dict with any_of/all_of
701
- if isinstance(config, dict):
702
- # any_of: at least one action must match at least one pattern
703
- if "any_of" in config:
704
- matched = set()
705
- # Pre-compile all patterns
706
- compiled_patterns = [_compile_pattern(p) for p in config["any_of"]]
707
-
708
- for action in actions:
709
- for compiled in compiled_patterns:
710
- if compiled and compiled.match(action):
711
- matched.add(action)
712
- break
713
- return len(matched) > 0, list(matched)
714
-
715
- # all_of: at least one action must match ALL patterns
716
- if "all_of" in config:
717
- # Pre-compile all patterns
718
- compiled_patterns = [_compile_pattern(p) for p in config["all_of"]]
719
- # Filter out invalid patterns
720
- compiled_patterns = [p for p in compiled_patterns if p]
721
-
722
- if not compiled_patterns:
723
- return False, []
724
-
725
- matched = set()
726
- for action in actions:
727
- # Check if this action matches ALL patterns
728
- if all(compiled.match(action) for compiled in compiled_patterns):
729
- matched.add(action)
730
-
731
- return len(matched) > 0, list(matched)
732
-
733
- return False, []
734
-
735
- def _matches_sensitive_pattern(self, action: str, config: CheckConfig) -> bool:
736
- """
737
- DEPRECATED: Use _check_sensitive_actions instead.
488
+ This checks for explicit allowed_wildcards configuration in wildcard_resource_check.
489
+ If not configured, it falls back to the parent security_best_practices_check's allowed_wildcards.
738
490
 
739
- Check if action matches any sensitive action pattern (supports regex).
491
+ Args:
492
+ config: The check configuration
740
493
 
741
- This allows configuration like:
742
- sensitive_action_patterns:
743
- - "^iam:.*" # All IAM actions
744
- - ".*:Delete.*" # Any delete action
745
- - "s3:PutBucket.*" # S3 bucket modification actions
494
+ Returns:
495
+ A frozenset of allowed wildcard patterns
746
496
  """
747
- import re
748
-
749
- sub_check_config = config.config.get("sensitive_action_check", {})
750
- if not isinstance(sub_check_config, dict):
751
- return False
752
-
753
- patterns = sub_check_config.get("sensitive_action_patterns", [])
754
- if not patterns:
755
- return False
756
-
757
- for pattern in patterns:
758
- try:
759
- if re.match(pattern, action):
760
- return True
761
- except re.error:
762
- # Invalid regex pattern, skip it
763
- continue
764
-
765
- return False
497
+ sub_check_config = config.config.get("wildcard_resource_check", {})
498
+ if isinstance(sub_check_config, dict) and "allowed_wildcards" in sub_check_config:
499
+ # Explicitly configured in wildcard_resource_check (override)
500
+ allowed_wildcards = sub_check_config.get("allowed_wildcards", [])
501
+ if isinstance(allowed_wildcards, list):
502
+ return frozenset(allowed_wildcards)
503
+ elif isinstance(allowed_wildcards, set | frozenset):
504
+ return frozenset(allowed_wildcards)
505
+ return frozenset()
506
+
507
+ # Fall back to parent security_best_practices_check's allowed_wildcards
508
+ parent_allowed_wildcards = config.config.get("allowed_wildcards", [])
509
+ if isinstance(parent_allowed_wildcards, list):
510
+ return frozenset(parent_allowed_wildcards)
511
+ elif isinstance(parent_allowed_wildcards, set | frozenset):
512
+ return frozenset(parent_allowed_wildcards)
513
+
514
+ # No configuration found, return empty set (flag all Resource: "*")
515
+ return frozenset()
@@ -0,0 +1 @@
1
+ """Utility modules for IAM policy checks."""