iam-policy-validator 1.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -0
@@ -0,0 +1,190 @@
1
+ """Policy-level privilege escalation detection for IAM policy checks.
2
+
3
+ This module provides functionality to detect privilege escalation patterns
4
+ that span multiple statements in a policy.
5
+ """
6
+
7
+ import re
8
+
9
+ from iam_validator.core.check_registry import CheckConfig
10
+ from iam_validator.core.models import ValidationIssue
11
+
12
+
13
+ def check_policy_level_actions(
14
+ all_actions: list[str],
15
+ statement_map: dict[str, list[tuple[int, str | None]]],
16
+ config,
17
+ check_config: CheckConfig,
18
+ check_type: str,
19
+ get_severity_func,
20
+ ) -> list[ValidationIssue]:
21
+ """
22
+ Check for policy-level privilege escalation patterns.
23
+
24
+ This function detects when a policy grants a dangerous combination of
25
+ permissions across multiple statements (e.g., iam:CreateUser + iam:AttachUserPolicy).
26
+
27
+ Args:
28
+ all_actions: All actions across the entire policy
29
+ statement_map: Mapping of action -> [(statement_idx, sid), ...]
30
+ config: The sensitive_actions or sensitive_action_patterns configuration
31
+ check_config: Full check configuration
32
+ check_type: Either "actions" (exact match) or "patterns" (regex match)
33
+ get_severity_func: Function to get severity for the check
34
+
35
+ Returns:
36
+ List of ValidationIssue objects
37
+ """
38
+ issues = []
39
+
40
+ if not config:
41
+ return issues
42
+
43
+ # Handle list of items (could be simple strings or dicts with all_of/any_of)
44
+ if isinstance(config, list):
45
+ for item in config:
46
+ if isinstance(item, dict) and "all_of" in item:
47
+ # This is a privilege escalation pattern - all actions must be present
48
+ issue = _check_all_of_pattern(
49
+ all_actions,
50
+ statement_map,
51
+ item["all_of"],
52
+ item, # Pass the entire item config (includes severity, message, suggestion)
53
+ check_config,
54
+ check_type,
55
+ get_severity_func,
56
+ )
57
+ if issue:
58
+ issues.append(issue)
59
+
60
+ # Handle dict with all_of at the top level
61
+ elif isinstance(config, dict) and "all_of" in config:
62
+ issue = _check_all_of_pattern(
63
+ all_actions,
64
+ statement_map,
65
+ config["all_of"],
66
+ config, # Pass the entire config dict (includes severity, message, suggestion)
67
+ check_config,
68
+ check_type,
69
+ get_severity_func,
70
+ )
71
+ if issue:
72
+ issues.append(issue)
73
+
74
+ return issues
75
+
76
+
77
+ def _check_all_of_pattern(
78
+ all_actions: list[str],
79
+ statement_map: dict[str, list[tuple[int, str | None]]],
80
+ required_actions: list[str],
81
+ item_config: dict,
82
+ check_config: CheckConfig,
83
+ check_type: str,
84
+ get_severity_func,
85
+ ) -> ValidationIssue | None:
86
+ """
87
+ Check if all required actions/patterns are present in the policy.
88
+
89
+ Args:
90
+ all_actions: All actions across the entire policy
91
+ statement_map: Mapping of action -> [(statement_idx, sid), ...]
92
+ required_actions: List of required actions or patterns
93
+ item_config: Configuration for this specific pattern (includes severity, message, suggestion)
94
+ check_config: Full check configuration
95
+ check_type: Either "actions" (exact match) or "patterns" (regex match)
96
+ get_severity_func: Function to get severity for the check
97
+
98
+ Returns:
99
+ ValidationIssue if privilege escalation detected, None otherwise
100
+ """
101
+ # Filter out actions that match ignore_patterns BEFORE checking for privilege escalation
102
+ # This allows users to exclude specific actions from privilege escalation detection
103
+ # by adding them to ignore_patterns in sensitive_action config
104
+ filtered_actions = check_config.filter_actions(frozenset(all_actions))
105
+ all_actions_filtered = list(filtered_actions)
106
+
107
+ matched_actions = []
108
+
109
+ if check_type == "actions":
110
+ # Exact matching
111
+ matched_actions = [a for a in all_actions_filtered if a in required_actions]
112
+ else:
113
+ # Pattern matching - for each pattern, find actions that match
114
+ for pattern in required_actions:
115
+ for action in all_actions_filtered:
116
+ try:
117
+ if re.match(pattern, action):
118
+ matched_actions.append(action)
119
+ break # Found at least one match for this pattern
120
+ except re.error:
121
+ continue
122
+
123
+ # Check if ALL required actions/patterns are present
124
+ if len(matched_actions) >= len(required_actions):
125
+ # Privilege escalation detected!
126
+ # Use severity from item_config if available, otherwise use default from check
127
+ severity = item_config.get("severity") or get_severity_func(check_config)
128
+
129
+ # Collect which statements these actions appear in
130
+ statement_refs = []
131
+ action_to_statements = {} # Map action -> list of statement references
132
+
133
+ for action in matched_actions:
134
+ action_to_statements[action] = []
135
+ if action in statement_map:
136
+ for stmt_idx, sid in statement_map[action]:
137
+ # Use index notation instead of # to avoid GitHub PR link interpretation
138
+ sid_str = f"'{sid}'" if sid else f"[{stmt_idx}]"
139
+ statement_refs.append(f"Statement {sid_str}: {action}")
140
+ action_to_statements[action].append(f"Statement {sid_str}")
141
+
142
+ # Format actions with backticks and statement references
143
+ action_list = "`, `".join(matched_actions)
144
+ stmt_details = "\n - ".join(statement_refs)
145
+
146
+ # Build a compact statement summary for the message
147
+ action_stmt_summary = []
148
+ for action in matched_actions:
149
+ stmts = action_to_statements.get(action, [])
150
+ if stmts:
151
+ action_stmt_summary.append(f"`{action}` in {', '.join(stmts)}")
152
+
153
+ stmt_summary = "; ".join(action_stmt_summary)
154
+
155
+ # Use custom message if provided in item_config, otherwise use default
156
+ # Support {actions} and {statements} placeholders in custom messages
157
+ message_template = item_config.get(
158
+ "message",
159
+ f"Policy grants [`{action_list}`] across statements - enables privilege escalation. Found: {stmt_summary}",
160
+ )
161
+ # Replace placeholders if present in custom message
162
+ message = message_template.replace("{actions}", f"`{action_list}`").replace(
163
+ "{statements}", stmt_summary
164
+ )
165
+
166
+ # Use custom suggestion if provided in item_config, otherwise use default
167
+ suggestion = item_config.get(
168
+ "suggestion",
169
+ f"These actions combined allow privilege escalation. Consider:\n"
170
+ f" 1. Splitting into separate policies for different users/roles\n"
171
+ f" 2. Adding strict conditions to limit when these actions can be used together\n"
172
+ f" 3. Reviewing if all these permissions are truly necessary\n\n"
173
+ f"Actions found in:\n - {stmt_details}",
174
+ )
175
+
176
+ # Use custom example if provided in item_config
177
+ example = item_config.get("example")
178
+
179
+ return ValidationIssue(
180
+ severity=severity,
181
+ statement_sid=None, # Policy-level issue
182
+ statement_index=-1, # -1 indicates policy-level issue
183
+ issue_type="privilege_escalation",
184
+ message=message,
185
+ suggestion=suggestion,
186
+ example=example,
187
+ line_number=1, # Policy-level issues point to line 1 (top of policy)
188
+ )
189
+
190
+ return None
@@ -0,0 +1,293 @@
1
+ """Sensitive action matching utilities for IAM policy checks.
2
+
3
+ This module provides functionality to match actions against sensitive action
4
+ configurations, supporting exact matches, regex patterns, and any_of/all_of logic.
5
+
6
+ Performance optimizations:
7
+ - Uses frozenset for O(1) lookups
8
+ - Centralized LRU cache for compiled regex patterns (from ignore_patterns module)
9
+ - Lazy loading of default actions from modular data structure
10
+ """
11
+
12
+ from iam_validator.core.check_registry import CheckConfig
13
+ from iam_validator.core.config.sensitive_actions import get_sensitive_actions
14
+ from iam_validator.core.ignore_patterns import compile_pattern
15
+
16
+ # Lazy-loaded default set of sensitive actions
17
+ # This will be loaded only when first accessed
18
+ _DEFAULT_SENSITIVE_ACTIONS_CACHE: frozenset[str] | None = None
19
+
20
+
21
+ def _get_default_sensitive_actions() -> frozenset[str]:
22
+ """
23
+ Get default sensitive actions with lazy loading and caching.
24
+
25
+ Returns:
26
+ Frozenset of all default sensitive actions
27
+
28
+ Performance:
29
+ - First call: Loads from sensitive actions list
30
+ - Subsequent calls: O(1) cached lookup
31
+ """
32
+ global _DEFAULT_SENSITIVE_ACTIONS_CACHE
33
+ if _DEFAULT_SENSITIVE_ACTIONS_CACHE is None:
34
+ _DEFAULT_SENSITIVE_ACTIONS_CACHE = get_sensitive_actions()
35
+ return _DEFAULT_SENSITIVE_ACTIONS_CACHE
36
+
37
+
38
+ def get_sensitive_actions_by_categories(
39
+ categories: list[str] | None = None,
40
+ ) -> frozenset[str]:
41
+ """
42
+ Get sensitive actions filtered by categories.
43
+
44
+ Args:
45
+ categories: List of category IDs to include. If None, returns all actions.
46
+ Valid categories: 'credential_exposure', 'data_access',
47
+ 'priv_esc', 'resource_exposure'
48
+
49
+ Returns:
50
+ Frozenset of sensitive actions matching the specified categories
51
+
52
+ Examples:
53
+ >>> # Get all sensitive actions (default behavior)
54
+ >>> all_actions = get_sensitive_actions_by_categories()
55
+
56
+ >>> # Get only privilege escalation actions
57
+ >>> priv_esc = get_sensitive_actions_by_categories(['priv_esc'])
58
+
59
+ >>> # Get credential exposure and data access actions
60
+ >>> sensitive = get_sensitive_actions_by_categories(['credential_exposure', 'data_access'])
61
+ """
62
+ return get_sensitive_actions(categories)
63
+
64
+
65
+ # Export for backward compatibility
66
+ DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
67
+
68
+
69
+ def check_sensitive_actions(
70
+ actions: list[str],
71
+ config: CheckConfig,
72
+ default_actions: frozenset[str] | None = None,
73
+ ) -> tuple[bool, list[str]]:
74
+ """
75
+ Check if actions match sensitive action criteria with any_of/all_of support.
76
+
77
+ Args:
78
+ actions: List of actions to check
79
+ config: Check configuration
80
+ default_actions: Default sensitive actions to use if no config (lazy-loaded)
81
+
82
+ Returns:
83
+ tuple[bool, list[str]]: (is_sensitive, matched_actions)
84
+ - is_sensitive: True if the actions match the sensitive criteria
85
+ - matched_actions: List of actions that matched the criteria
86
+
87
+ Performance:
88
+ - Uses lazy-loaded defaults (only loaded on first use)
89
+ - O(1) frozenset lookups for action matching
90
+ """
91
+ # Check if categories are specified in config
92
+ categories = config.config.get("categories")
93
+ if categories is not None:
94
+ # If categories is an empty list, disable the check
95
+ if len(categories) == 0:
96
+ return False, []
97
+ # Get sensitive actions filtered by categories
98
+ default_actions = get_sensitive_actions_by_categories(categories)
99
+ elif default_actions is None:
100
+ # Use all categories if no specific categories configured
101
+ default_actions = _get_default_sensitive_actions()
102
+
103
+ # Apply ignore_patterns to filter out default actions
104
+ # This allows users to exclude specific actions from the default 490 actions
105
+ default_actions = config.filter_actions(default_actions)
106
+
107
+ # Filter out wildcards
108
+ filtered_actions = [a for a in actions if a != "*"]
109
+ if not filtered_actions:
110
+ return False, []
111
+
112
+ # Get configuration for both sensitive_actions and sensitive_action_patterns
113
+ # Config is now flat (no longer nested under sensitive_action_check)
114
+ sensitive_actions_config = config.config.get("sensitive_actions")
115
+ sensitive_patterns_config = config.config.get("sensitive_action_patterns")
116
+
117
+ # Check sensitive_actions (exact matches)
118
+ actions_match, actions_matched = check_actions_config(
119
+ filtered_actions, sensitive_actions_config, default_actions
120
+ )
121
+
122
+ # Check sensitive_action_patterns (regex patterns)
123
+ patterns_match, patterns_matched = check_patterns_config(
124
+ filtered_actions, sensitive_patterns_config
125
+ )
126
+
127
+ # Combine results - if either matched, we consider it sensitive
128
+ is_sensitive = actions_match or patterns_match
129
+ # Use set operations for efficient deduplication
130
+ matched_set = set(actions_matched) | set(patterns_matched)
131
+ matched_actions = list(matched_set)
132
+
133
+ # Apply ignore_patterns to filter the final matched actions
134
+ # This ensures ignore_patterns work for:
135
+ # 1. Default actions (490 actions from Python modules)
136
+ # 2. Custom sensitive_actions configuration
137
+ # 3. Custom sensitive_action_patterns configuration
138
+ if matched_actions and config.ignore_patterns:
139
+ filtered_matched = config.filter_actions(frozenset(matched_actions))
140
+ matched_actions = list(filtered_matched)
141
+ # Update is_sensitive based on filtered results
142
+ is_sensitive = len(matched_actions) > 0
143
+
144
+ return is_sensitive, matched_actions
145
+
146
+
147
+ def check_actions_config(
148
+ actions: list[str], config, default_actions: frozenset[str]
149
+ ) -> tuple[bool, list[str]]:
150
+ """
151
+ Check actions against sensitive_actions configuration.
152
+
153
+ Supports:
154
+ - Simple list: ["action1", "action2"] (backward compatible, any_of logic)
155
+ - any_of: {"any_of": ["action1", "action2"]}
156
+ - all_of: {"all_of": ["action1", "action2"]}
157
+ - Multiple groups: [{"all_of": [...]}, {"all_of": [...]}, "action3"]
158
+
159
+ Args:
160
+ actions: List of actions to check
161
+ config: Sensitive actions configuration
162
+ default_actions: Default sensitive actions to use if no config
163
+
164
+ Returns:
165
+ tuple[bool, list[str]]: (matches, matched_actions)
166
+ """
167
+ if not config:
168
+ # If no config, fall back to defaults with any_of logic
169
+ # default_actions is already a frozenset for O(1) lookups
170
+ matched = [a for a in actions if a in default_actions]
171
+ return len(matched) > 0, matched
172
+
173
+ # Handle simple list with potential mixed items
174
+ if isinstance(config, list):
175
+ # Use set for O(1) membership checks
176
+ all_matched = set()
177
+ actions_set = set(actions) # Convert once for O(1) lookups
178
+
179
+ for item in config:
180
+ # Each item can be a string, or a dict with any_of/all_of
181
+ if isinstance(item, str):
182
+ # Simple string - check if action matches (O(1) lookup)
183
+ if item in actions_set:
184
+ all_matched.add(item)
185
+ elif isinstance(item, dict):
186
+ # Recurse for dict items
187
+ matches, matched = check_actions_config(actions, item, default_actions)
188
+ if matches:
189
+ all_matched.update(matched)
190
+
191
+ return len(all_matched) > 0, list(all_matched)
192
+
193
+ # Handle dict with any_of/all_of
194
+ if isinstance(config, dict):
195
+ # any_of: at least one action must match
196
+ if "any_of" in config:
197
+ # Convert once for O(1) intersection
198
+ any_of_set = set(config["any_of"])
199
+ actions_set = set(actions)
200
+ matched = list(any_of_set & actions_set)
201
+ return len(matched) > 0, matched
202
+
203
+ # all_of: all specified actions must be present in the statement
204
+ if "all_of" in config:
205
+ all_of_set = set(config["all_of"])
206
+ actions_set = set(actions)
207
+ matched = list(all_of_set & actions_set)
208
+ # All required actions must be present
209
+ return all_of_set.issubset(actions_set), matched
210
+
211
+ return False, []
212
+
213
+
214
+ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
215
+ """
216
+ Check actions against sensitive_action_patterns configuration.
217
+
218
+ Supports:
219
+ - Simple list: ["^pattern1.*", "^pattern2.*"] (backward compatible, any_of logic)
220
+ - any_of: {"any_of": ["^pattern1.*", "^pattern2.*"]}
221
+ - all_of: {"all_of": ["^pattern1.*", "^pattern2.*"]}
222
+ - Multiple groups: [{"all_of": [...]}, {"any_of": [...]}, "^pattern.*"]
223
+
224
+ Args:
225
+ actions: List of actions to check
226
+ config: Sensitive action patterns configuration
227
+
228
+ Returns:
229
+ tuple[bool, list[str]]: (matches, matched_actions)
230
+
231
+ Performance:
232
+ Uses cached compiled regex patterns for 10-50x speedup
233
+ """
234
+ if not config:
235
+ return False, []
236
+
237
+ # Handle simple list with potential mixed items
238
+ if isinstance(config, list):
239
+ # Use set for O(1) membership checks instead of list
240
+ all_matched = set()
241
+
242
+ for item in config:
243
+ # Each item can be a string pattern, or a dict with any_of/all_of
244
+ if isinstance(item, str):
245
+ # Simple string pattern - check if any action matches
246
+ # Use cached compiled pattern from centralized ignore_patterns module
247
+ compiled = compile_pattern(item)
248
+ if compiled:
249
+ for action in actions:
250
+ if compiled.match(action):
251
+ all_matched.add(action)
252
+ elif isinstance(item, dict):
253
+ # Recurse for dict items
254
+ matches, matched = check_patterns_config(actions, item)
255
+ if matches:
256
+ all_matched.update(matched)
257
+
258
+ return len(all_matched) > 0, list(all_matched)
259
+
260
+ # Handle dict with any_of/all_of
261
+ if isinstance(config, dict):
262
+ # any_of: at least one action must match at least one pattern
263
+ if "any_of" in config:
264
+ matched = set()
265
+ # Pre-compile all patterns using centralized cache
266
+ compiled_patterns = [compile_pattern(p) for p in config["any_of"]]
267
+
268
+ for action in actions:
269
+ for compiled in compiled_patterns:
270
+ if compiled and compiled.match(action):
271
+ matched.add(action)
272
+ break
273
+ return len(matched) > 0, list(matched)
274
+
275
+ # all_of: at least one action must match ALL patterns
276
+ if "all_of" in config:
277
+ # Pre-compile all patterns using centralized cache
278
+ compiled_patterns = [compile_pattern(p) for p in config["all_of"]]
279
+ # Filter out invalid patterns
280
+ compiled_patterns = [p for p in compiled_patterns if p]
281
+
282
+ if not compiled_patterns:
283
+ return False, []
284
+
285
+ matched = set()
286
+ for action in actions:
287
+ # Check if this action matches ALL patterns
288
+ if all(compiled.match(action) for compiled in compiled_patterns):
289
+ matched.add(action)
290
+
291
+ return len(matched) > 0, list(matched)
292
+
293
+ return False, []
@@ -0,0 +1,86 @@
1
+ """Wildcard action expansion utilities for IAM policy checks.
2
+
3
+ This module provides functionality to expand wildcard actions (like ec2:*, iam:Delete*)
4
+ to their actual action names using the AWS Service Reference API.
5
+ """
6
+
7
+ import re
8
+ from functools import lru_cache
9
+
10
+ from iam_validator.core.aws_service import AWSServiceFetcher
11
+
12
+
13
+ # Global cache for compiled wildcard patterns (shared across checks)
14
+ # Using lru_cache for O(1) pattern reuse and 20-30x performance improvement
15
+ @lru_cache(maxsize=512)
16
+ def compile_wildcard_pattern(pattern: str) -> re.Pattern[str]:
17
+ """Compile and cache wildcard patterns for O(1) reuse.
18
+
19
+ Args:
20
+ pattern: Wildcard pattern (e.g., "s3:Get*")
21
+
22
+ Returns:
23
+ Compiled regex pattern
24
+
25
+ Performance:
26
+ 20-30x speedup by avoiding repeated pattern compilation
27
+ """
28
+ regex_pattern = "^" + re.escape(pattern).replace(r"\*", ".*") + "$"
29
+ return re.compile(regex_pattern, re.IGNORECASE)
30
+
31
+
32
+ async def expand_wildcard_actions(actions: list[str], fetcher: AWSServiceFetcher) -> list[str]:
33
+ """
34
+ Expand wildcard actions to their actual action names using AWS API.
35
+
36
+ This function expands wildcard patterns like "s3:*", "ec2:Delete*", "iam:*User*"
37
+ to the actual action names they grant. This is crucial for sensitive action
38
+ detection to catch wildcards that include sensitive actions.
39
+
40
+ Examples:
41
+ ["s3:GetObject", "ec2:*"] -> ["s3:GetObject", "ec2:DeleteVolume", "ec2:TerminateInstances", ...]
42
+ ["iam:Delete*"] -> ["iam:DeleteUser", "iam:DeleteRole", "iam:DeleteAccessKey", ...]
43
+
44
+ Args:
45
+ actions: List of action patterns (may include wildcards)
46
+ fetcher: AWS service fetcher for API lookups
47
+
48
+ Returns:
49
+ List of expanded action names (wildcards replaced with actual actions)
50
+ """
51
+ expanded = []
52
+
53
+ for action in actions:
54
+ # Skip full wildcard "*" - it's too broad to expand
55
+ if action == "*":
56
+ expanded.append(action)
57
+ continue
58
+
59
+ # Check if action contains wildcards
60
+ if "*" not in action:
61
+ # No wildcard, keep as-is
62
+ expanded.append(action)
63
+ continue
64
+
65
+ # Action has wildcard - expand it using AWS API
66
+ try:
67
+ # Parse action to get service and action name
68
+ service_prefix, action_name = fetcher.parse_action(action)
69
+
70
+ # Fetch service detail to get all available actions
71
+ service_detail = await fetcher.fetch_service_by_name(service_prefix)
72
+ available_actions = list(service_detail.actions.keys())
73
+
74
+ # Match wildcard pattern against available actions
75
+ _, matched_actions = fetcher.match_wildcard_action(action_name, available_actions)
76
+
77
+ # Add expanded actions with service prefix
78
+ for matched_action in matched_actions:
79
+ expanded.append(f"{service_prefix}:{matched_action}")
80
+
81
+ except Exception:
82
+ # If expansion fails (invalid service, etc.), keep original action
83
+ # This ensures we don't lose actions due to API errors
84
+ expanded.append(action)
85
+
86
+ return expanded
@@ -0,0 +1,58 @@
1
+ """Wildcard action check - detects Action: '*' in IAM policies."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from iam_validator.core.aws_service import AWSServiceFetcher
6
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
+ from iam_validator.core.models import Statement, ValidationIssue
8
+
9
+
10
+ class WildcardActionCheck(PolicyCheck):
11
+ """Checks for wildcard actions (Action: '*') which grant all permissions."""
12
+
13
+ check_id: ClassVar[str] = "wildcard_action"
14
+ description: ClassVar[str] = "Checks for wildcard actions (*)"
15
+ default_severity: ClassVar[str] = "medium"
16
+
17
+ async def execute(
18
+ self,
19
+ statement: Statement,
20
+ statement_idx: int,
21
+ fetcher: AWSServiceFetcher,
22
+ config: CheckConfig,
23
+ ) -> list[ValidationIssue]:
24
+ """Execute wildcard action check on a statement."""
25
+ issues = []
26
+
27
+ # Only check Allow statements
28
+ if statement.effect != "Allow":
29
+ return issues
30
+
31
+ actions = statement.get_actions()
32
+
33
+ # Check for wildcard action (Action: "*")
34
+ if "*" in actions:
35
+ message = config.config.get(
36
+ "message", 'Statement allows all actions `"*"` (wildcard action).'
37
+ )
38
+ suggestion = config.config.get(
39
+ "suggestion",
40
+ "Replace wildcard with specific actions needed for your use case",
41
+ )
42
+ example = config.config.get("example", "")
43
+
44
+ issues.append(
45
+ ValidationIssue(
46
+ severity=self.get_severity(config),
47
+ statement_sid=statement.sid,
48
+ statement_index=statement_idx,
49
+ issue_type="overly_permissive",
50
+ message=message,
51
+ suggestion=suggestion,
52
+ example=example if example else None,
53
+ line_number=statement.line_number,
54
+ field_name="action",
55
+ )
56
+ )
57
+
58
+ return issues