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

Files changed (34) hide show
  1. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +88 -10
  2. iam_policy_validator-1.1.1.dist-info/RECORD +53 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +2 -0
  5. iam_validator/checks/action_condition_enforcement.py +112 -28
  6. iam_validator/checks/action_resource_constraint.py +151 -0
  7. iam_validator/checks/action_validation.py +18 -138
  8. iam_validator/checks/security_best_practices.py +241 -400
  9. iam_validator/checks/utils/__init__.py +1 -0
  10. iam_validator/checks/utils/policy_level_checks.py +143 -0
  11. iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
  12. iam_validator/checks/utils/wildcard_expansion.py +89 -0
  13. iam_validator/commands/__init__.py +3 -1
  14. iam_validator/commands/cache.py +402 -0
  15. iam_validator/commands/validate.py +7 -5
  16. iam_validator/core/access_analyzer_report.py +2 -1
  17. iam_validator/core/aws_fetcher.py +79 -19
  18. iam_validator/core/check_registry.py +3 -0
  19. iam_validator/core/cli.py +1 -1
  20. iam_validator/core/config_loader.py +40 -3
  21. iam_validator/core/defaults.py +334 -0
  22. iam_validator/core/formatters/__init__.py +2 -0
  23. iam_validator/core/formatters/console.py +44 -7
  24. iam_validator/core/formatters/csv.py +7 -2
  25. iam_validator/core/formatters/enhanced.py +433 -0
  26. iam_validator/core/formatters/html.py +127 -37
  27. iam_validator/core/formatters/markdown.py +10 -2
  28. iam_validator/core/models.py +30 -6
  29. iam_validator/core/policy_checks.py +21 -2
  30. iam_validator/core/report.py +112 -26
  31. iam_policy_validator-1.0.4.dist-info/RECORD +0 -45
  32. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
  33. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
  34. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -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"
@@ -91,14 +57,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
91
57
  if self._is_sub_check_enabled(config, "wildcard_action_check"):
92
58
  if "*" in actions:
93
59
  severity = self._get_sub_check_severity(config, "wildcard_action_check", "warning")
60
+ sub_check_config = config.config.get("wildcard_action_check", {})
61
+
62
+ message = sub_check_config.get("message", "Statement allows all actions (*)")
63
+ suggestion_text = sub_check_config.get(
64
+ "suggestion", "Consider limiting to specific actions needed"
65
+ )
66
+ example = sub_check_config.get("example", "")
67
+
68
+ # Combine suggestion + example like action_condition_enforcement does
69
+ suggestion = (
70
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
71
+ )
72
+
94
73
  issues.append(
95
74
  ValidationIssue(
96
75
  severity=severity,
97
76
  statement_sid=statement_sid,
98
77
  statement_index=statement_idx,
99
78
  issue_type="overly_permissive",
100
- message="Statement allows all actions (*)",
101
- suggestion="Consider limiting to specific actions needed",
79
+ message=message,
80
+ suggestion=suggestion,
102
81
  line_number=line_number,
103
82
  )
104
83
  )
@@ -106,33 +85,69 @@ class SecurityBestPracticesCheck(PolicyCheck):
106
85
  # Check 2: Wildcard resource check
107
86
  if self._is_sub_check_enabled(config, "wildcard_resource_check"):
108
87
  if "*" in resources:
109
- severity = self._get_sub_check_severity(
110
- config, "wildcard_resource_check", "warning"
111
- )
112
- issues.append(
113
- ValidationIssue(
114
- severity=severity,
115
- statement_sid=statement_sid,
116
- statement_index=statement_idx,
117
- issue_type="overly_permissive",
118
- message="Statement applies to all resources (*)",
119
- suggestion="Consider limiting to specific resources",
120
- line_number=line_number,
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
+ )
101
+
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
121
120
  )
122
- )
123
121
 
124
122
  # Check 3: Critical - both wildcards together
125
123
  if self._is_sub_check_enabled(config, "full_wildcard_check"):
126
124
  if "*" in actions and "*" in resources:
127
125
  severity = self._get_sub_check_severity(config, "full_wildcard_check", "error")
126
+ sub_check_config = config.config.get("full_wildcard_check", {})
127
+
128
+ message = sub_check_config.get(
129
+ "message",
130
+ "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
131
+ )
132
+ suggestion_text = sub_check_config.get(
133
+ "suggestion",
134
+ "This grants full administrative access. Restrict to specific actions and resources.",
135
+ )
136
+ example = sub_check_config.get("example", "")
137
+
138
+ # Combine suggestion + example
139
+ suggestion = (
140
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
141
+ )
142
+
128
143
  issues.append(
129
144
  ValidationIssue(
130
145
  severity=severity,
131
146
  statement_sid=statement_sid,
132
147
  statement_index=statement_idx,
133
148
  issue_type="security_risk",
134
- message="Statement allows all actions on all resources - CRITICAL SECURITY RISK",
135
- suggestion="This grants full administrative access. Restrict to specific actions and resources.",
149
+ message=message,
150
+ suggestion=suggestion,
136
151
  line_number=line_number,
137
152
  )
138
153
  )
@@ -155,15 +170,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
155
170
  severity = self._get_sub_check_severity(
156
171
  config, "service_wildcard_check", "warning"
157
172
  )
173
+ sub_check_config = config.config.get("service_wildcard_check", {})
174
+
175
+ # Get message template and replace placeholders
176
+ message_template = sub_check_config.get(
177
+ "message",
178
+ "Service-level wildcard '{action}' grants all permissions for {service} service",
179
+ )
180
+ suggestion_template = sub_check_config.get(
181
+ "suggestion",
182
+ "Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
183
+ )
184
+ example_template = sub_check_config.get("example", "")
185
+
186
+ message = message_template.format(action=action, service=service)
187
+ suggestion_text = suggestion_template.format(action=action, service=service)
188
+ example = (
189
+ example_template.format(action=action, service=service)
190
+ if example_template
191
+ else ""
192
+ )
193
+
194
+ # Combine suggestion + example
195
+ suggestion = (
196
+ f"{suggestion_text}\nExample:\n{example}"
197
+ if example
198
+ else suggestion_text
199
+ )
200
+
158
201
  issues.append(
159
202
  ValidationIssue(
160
203
  severity=severity,
161
204
  statement_sid=statement_sid,
162
205
  statement_index=statement_idx,
163
206
  issue_type="overly_permissive",
164
- message=f"Service-level wildcard '{action}' grants all permissions for {service} service",
207
+ message=message,
165
208
  action=action,
166
- suggestion=f"Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
209
+ suggestion=suggestion,
167
210
  line_number=line_number,
168
211
  )
169
212
  )
@@ -172,18 +215,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
172
215
  if self._is_sub_check_enabled(config, "sensitive_action_check"):
173
216
  has_conditions = statement.condition is not None and len(statement.condition) > 0
174
217
 
218
+ # Expand wildcards to actual actions using AWS API
219
+ expanded_actions = await expand_wildcard_actions(actions, fetcher)
220
+
175
221
  # Check if sensitive actions match using any_of/all_of logic
176
- 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
+ )
177
225
 
178
226
  if is_sensitive and not has_conditions:
179
227
  severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
228
+ sub_check_config = config.config.get("sensitive_action_check", {})
180
229
 
181
- # Create appropriate message based on matched actions
230
+ # Create appropriate message based on matched actions using configurable templates
182
231
  if len(matched_actions) == 1:
183
- message = f"Sensitive action '{matched_actions[0]}' should have conditions to limit when it can be used"
232
+ message_template = sub_check_config.get(
233
+ "message_single",
234
+ "Sensitive action '{action}' should have conditions to limit when it can be used",
235
+ )
236
+ message = message_template.format(action=matched_actions[0])
184
237
  else:
185
238
  action_list = "', '".join(matched_actions)
186
- message = f"Sensitive actions '{action_list}' should have conditions to limit when they can be used"
239
+ message_template = sub_check_config.get(
240
+ "message_multiple",
241
+ "Sensitive actions '{actions}' should have conditions to limit when they can be used",
242
+ )
243
+ message = message_template.format(actions=action_list)
244
+
245
+ suggestion_text = sub_check_config.get(
246
+ "suggestion",
247
+ "Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
248
+ )
249
+ example = sub_check_config.get("example", "")
250
+
251
+ # Combine suggestion + example
252
+ suggestion = (
253
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
254
+ )
187
255
 
188
256
  issues.append(
189
257
  ValidationIssue(
@@ -193,7 +261,7 @@ class SecurityBestPracticesCheck(PolicyCheck):
193
261
  issue_type="missing_condition",
194
262
  message=message,
195
263
  action=(matched_actions[0] if len(matched_actions) == 1 else None),
196
- suggestion="Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
264
+ suggestion=suggestion,
197
265
  line_number=line_number,
198
266
  )
199
267
  )
@@ -262,164 +330,32 @@ class SecurityBestPracticesCheck(PolicyCheck):
262
330
  # Check sensitive_actions configuration
263
331
  if sensitive_actions_config:
264
332
  policy_issues.extend(
265
- self._check_policy_level_actions(
333
+ check_policy_level_actions(
266
334
  list(all_actions),
267
335
  statement_map,
268
336
  sensitive_actions_config,
269
337
  config,
270
338
  "actions",
339
+ self._get_sub_check_severity,
271
340
  )
272
341
  )
273
342
 
274
343
  # Check sensitive_action_patterns configuration
275
344
  if sensitive_patterns_config:
276
345
  policy_issues.extend(
277
- self._check_policy_level_actions(
346
+ check_policy_level_actions(
278
347
  list(all_actions),
279
348
  statement_map,
280
349
  sensitive_patterns_config,
281
350
  config,
282
351
  "patterns",
352
+ self._get_sub_check_severity,
283
353
  )
284
354
  )
285
355
 
286
356
  issues.extend(policy_issues)
287
357
  return issues
288
358
 
289
- def _check_policy_level_actions(
290
- self,
291
- all_actions: list[str],
292
- statement_map: dict[str, list[tuple[int, str | None]]],
293
- config,
294
- check_config: CheckConfig,
295
- check_type: str,
296
- ) -> list[ValidationIssue]:
297
- """
298
- Check for policy-level privilege escalation patterns.
299
-
300
- Args:
301
- all_actions: All actions across the entire policy
302
- statement_map: Mapping of action -> [(statement_idx, sid), ...]
303
- config: The sensitive_actions or sensitive_action_patterns configuration
304
- check_config: Full check configuration
305
- check_type: Either "actions" (exact match) or "patterns" (regex match)
306
-
307
- Returns:
308
- List of ValidationIssue objects
309
- """
310
- import re
311
-
312
- issues = []
313
-
314
- if not config:
315
- return issues
316
-
317
- # Handle list of items (could be simple strings or dicts with all_of/any_of)
318
- if isinstance(config, list):
319
- for item in config:
320
- if isinstance(item, dict) and "all_of" in item:
321
- # This is a privilege escalation pattern - all actions must be present
322
- required_actions = item["all_of"]
323
- matched_actions = []
324
-
325
- if check_type == "actions":
326
- # Exact matching
327
- matched_actions = [a for a in all_actions if a in required_actions]
328
- else:
329
- # Pattern matching - for each pattern, find actions that match
330
- for pattern in required_actions:
331
- for action in all_actions:
332
- try:
333
- if re.match(pattern, action):
334
- matched_actions.append(action)
335
- break # Found at least one match for this pattern
336
- except re.error:
337
- continue
338
-
339
- # Check if ALL required actions/patterns are present
340
- if len(matched_actions) >= len(required_actions):
341
- # Privilege escalation detected!
342
- severity = self._get_sub_check_severity(
343
- check_config, "sensitive_action_check", "error"
344
- )
345
-
346
- # Collect which statements these actions appear in
347
- statement_refs = []
348
- for action in matched_actions:
349
- if action in statement_map:
350
- for stmt_idx, sid in statement_map[action]:
351
- sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
352
- statement_refs.append(f"Statement {sid_str}: {action}")
353
-
354
- action_list = "', '".join(matched_actions)
355
- stmt_details = "\n - ".join(statement_refs)
356
-
357
- issues.append(
358
- ValidationIssue(
359
- severity=severity,
360
- statement_sid=None, # Policy-level issue
361
- statement_index=-1, # -1 indicates policy-level issue
362
- issue_type="privilege_escalation",
363
- message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
364
- suggestion=f"These actions combined allow privilege escalation. Consider:\n"
365
- f" 1. Splitting into separate policies for different users/roles\n"
366
- f" 2. Adding strict conditions to limit when these actions can be used together\n"
367
- f" 3. Reviewing if all these permissions are truly necessary\n\n"
368
- f"Actions found in:\n - {stmt_details}",
369
- line_number=None,
370
- )
371
- )
372
-
373
- # Handle dict with all_of at the top level
374
- elif isinstance(config, dict) and "all_of" in config:
375
- required_actions = config["all_of"]
376
- matched_actions = []
377
-
378
- if check_type == "actions":
379
- matched_actions = [a for a in all_actions if a in required_actions]
380
- else:
381
- for pattern in required_actions:
382
- for action in all_actions:
383
- try:
384
- if re.match(pattern, action):
385
- matched_actions.append(action)
386
- break
387
- except re.error:
388
- continue
389
-
390
- if len(matched_actions) >= len(required_actions):
391
- severity = self._get_sub_check_severity(
392
- check_config, "sensitive_action_check", "error"
393
- )
394
-
395
- statement_refs = []
396
- for action in matched_actions:
397
- if action in statement_map:
398
- for stmt_idx, sid in statement_map[action]:
399
- sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
400
- statement_refs.append(f"Statement {sid_str}: {action}")
401
-
402
- action_list = "', '".join(matched_actions)
403
- stmt_details = "\n - ".join(statement_refs)
404
-
405
- issues.append(
406
- ValidationIssue(
407
- severity=severity,
408
- statement_sid=None,
409
- statement_index=-1, # -1 indicates policy-level issue
410
- issue_type="privilege_escalation",
411
- message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
412
- suggestion=f"These actions combined allow privilege escalation. Consider:\n"
413
- f" 1. Splitting into separate policies for different users/roles\n"
414
- f" 2. Adding strict conditions to limit when these actions can be used together\n"
415
- f" 3. Reviewing if all these permissions are truly necessary\n\n"
416
- f"Actions found in:\n - {stmt_details}",
417
- line_number=None,
418
- )
419
- )
420
-
421
- return issues
422
-
423
359
  def _is_sub_check_enabled(self, config: CheckConfig, sub_check_name: str) -> bool:
424
360
  """Check if a sub-check is enabled in the configuration."""
425
361
  if sub_check_name not in config.config:
@@ -442,6 +378,50 @@ class SecurityBestPracticesCheck(PolicyCheck):
442
378
  return sub_check_config.get("severity", default)
443
379
  return default
444
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
+
445
425
  def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
446
426
  """
447
427
  Get list of services that are allowed to use service-level wildcards.
@@ -463,212 +443,73 @@ class SecurityBestPracticesCheck(PolicyCheck):
463
443
 
464
444
  return set()
465
445
 
466
- def _check_sensitive_actions(
467
- self, actions: list[str], config: CheckConfig
468
- ) -> tuple[bool, list[str]]:
469
- """
470
- Check if actions match sensitive action criteria with any_of/all_of support.
471
-
472
- Returns:
473
- tuple[bool, list[str]]: (is_sensitive, matched_actions)
474
- - is_sensitive: True if the actions match the sensitive criteria
475
- - matched_actions: List of actions that matched the criteria
476
- """
477
- # Filter out wildcards
478
- filtered_actions = [a for a in actions if a != "*"]
479
- if not filtered_actions:
480
- return False, []
481
-
482
- # Get configuration for both sensitive_actions and sensitive_action_patterns
483
- sub_check_config = config.config.get("sensitive_action_check", {})
484
- if not isinstance(sub_check_config, dict):
485
- return False, []
486
-
487
- sensitive_actions_config = sub_check_config.get("sensitive_actions")
488
- sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
489
-
490
- # Check sensitive_actions (exact matches)
491
- actions_match, actions_matched = self._check_actions_config(
492
- filtered_actions, sensitive_actions_config
493
- )
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.
494
450
 
495
- # Check sensitive_action_patterns (regex patterns)
496
- patterns_match, patterns_matched = self._check_patterns_config(
497
- filtered_actions, sensitive_patterns_config
498
- )
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.
499
454
 
500
- # Combine results - if either matched, we consider it sensitive
501
- is_sensitive = actions_match or patterns_match
502
- # Use set operations for efficient deduplication
503
- matched_set = set(actions_matched) | set(patterns_matched)
504
- 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
505
458
 
506
- return is_sensitive, matched_actions
459
+ Returns:
460
+ True if the action matches any pattern in the allowlist
507
461
 
508
- 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.
509
465
  """
510
- Check actions against sensitive_actions configuration.
511
-
512
- Supports:
513
- - Simple list: ["action1", "action2"] (backward compatible, any_of logic)
514
- - any_of: {"any_of": ["action1", "action2"]}
515
- - all_of: {"all_of": ["action1", "action2"]}
516
- - 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
517
477
 
518
- Returns:
519
- tuple[bool, list[str]]: (matches, matched_actions)
520
- """
521
- if not config:
522
- # If no config, fall back to defaults with any_of logic
523
- # DEFAULT_SENSITIVE_ACTIONS is already a frozenset for O(1) lookups
524
- matched = [a for a in actions if a in self.DEFAULT_SENSITIVE_ACTIONS]
525
- return len(matched) > 0, matched
526
-
527
- # Handle simple list with potential mixed items
528
- if isinstance(config, list):
529
- # Use set for O(1) membership checks
530
- all_matched = set()
531
- actions_set = set(actions) # Convert once for O(1) lookups
532
-
533
- for item in config:
534
- # Each item can be a string, or a dict with any_of/all_of
535
- if isinstance(item, str):
536
- # Simple string - check if action matches (O(1) lookup)
537
- if item in actions_set:
538
- all_matched.add(item)
539
- elif isinstance(item, dict):
540
- # Recurse for dict items
541
- matches, matched = self._check_actions_config(actions, item)
542
- if matches:
543
- all_matched.update(matched)
544
-
545
- return len(all_matched) > 0, list(all_matched)
546
-
547
- # Handle dict with any_of/all_of
548
- if isinstance(config, dict):
549
- # any_of: at least one action must match
550
- if "any_of" in config:
551
- # Convert once for O(1) intersection
552
- any_of_set = set(config["any_of"])
553
- actions_set = set(actions)
554
- matched = list(any_of_set & actions_set)
555
- return len(matched) > 0, matched
556
-
557
- # all_of: all specified actions must be present in the statement
558
- if "all_of" in config:
559
- all_of_set = set(config["all_of"])
560
- actions_set = set(actions)
561
- matched = list(all_of_set & actions_set)
562
- # All required actions must be present
563
- return all_of_set.issubset(actions_set), matched
564
-
565
- return False, []
566
-
567
- def _check_patterns_config(self, actions: list[str], config) -> tuple[bool, list[str]]:
568
- """
569
- 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
570
482
 
571
- Supports:
572
- - Simple list: ["^pattern1.*", "^pattern2.*"] (backward compatible, any_of logic)
573
- - any_of: {"any_of": ["^pattern1.*", "^pattern2.*"]}
574
- - all_of: {"all_of": ["^pattern1.*", "^pattern2.*"]}
575
- - Multiple groups: [{"all_of": [...]}, {"any_of": [...]}, "^pattern.*"]
483
+ return False
576
484
 
577
- Returns:
578
- 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.
579
487
 
580
- Performance:
581
- Uses cached compiled regex patterns for 10-50x speedup
582
- """
583
- if not config:
584
- return False, []
585
-
586
- # Handle simple list with potential mixed items
587
- if isinstance(config, list):
588
- # Use set for O(1) membership checks instead of list
589
- all_matched = set()
590
-
591
- for item in config:
592
- # Each item can be a string pattern, or a dict with any_of/all_of
593
- if isinstance(item, str):
594
- # Simple string pattern - check if any action matches
595
- # Use cached compiled pattern
596
- compiled = _compile_pattern(item)
597
- if compiled:
598
- for action in actions:
599
- if compiled.match(action):
600
- all_matched.add(action)
601
- elif isinstance(item, dict):
602
- # Recurse for dict items
603
- matches, matched = self._check_patterns_config(actions, item)
604
- if matches:
605
- all_matched.update(matched)
606
-
607
- return len(all_matched) > 0, list(all_matched)
608
-
609
- # Handle dict with any_of/all_of
610
- if isinstance(config, dict):
611
- # any_of: at least one action must match at least one pattern
612
- if "any_of" in config:
613
- matched = set()
614
- # Pre-compile all patterns
615
- compiled_patterns = [_compile_pattern(p) for p in config["any_of"]]
616
-
617
- for action in actions:
618
- for compiled in compiled_patterns:
619
- if compiled and compiled.match(action):
620
- matched.add(action)
621
- break
622
- return len(matched) > 0, list(matched)
623
-
624
- # all_of: at least one action must match ALL patterns
625
- if "all_of" in config:
626
- # Pre-compile all patterns
627
- compiled_patterns = [_compile_pattern(p) for p in config["all_of"]]
628
- # Filter out invalid patterns
629
- compiled_patterns = [p for p in compiled_patterns if p]
630
-
631
- if not compiled_patterns:
632
- return False, []
633
-
634
- matched = set()
635
- for action in actions:
636
- # Check if this action matches ALL patterns
637
- if all(compiled.match(action) for compiled in compiled_patterns):
638
- matched.add(action)
639
-
640
- return len(matched) > 0, list(matched)
641
-
642
- return False, []
643
-
644
- def _matches_sensitive_pattern(self, action: str, config: CheckConfig) -> bool:
645
- """
646
- 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.
647
490
 
648
- Check if action matches any sensitive action pattern (supports regex).
491
+ Args:
492
+ config: The check configuration
649
493
 
650
- This allows configuration like:
651
- sensitive_action_patterns:
652
- - "^iam:.*" # All IAM actions
653
- - ".*:Delete.*" # Any delete action
654
- - "s3:PutBucket.*" # S3 bucket modification actions
494
+ Returns:
495
+ A frozenset of allowed wildcard patterns
655
496
  """
656
- import re
657
-
658
- sub_check_config = config.config.get("sensitive_action_check", {})
659
- if not isinstance(sub_check_config, dict):
660
- return False
661
-
662
- patterns = sub_check_config.get("sensitive_action_patterns", [])
663
- if not patterns:
664
- return False
665
-
666
- for pattern in patterns:
667
- try:
668
- if re.match(pattern, action):
669
- return True
670
- except re.error:
671
- # Invalid regex pattern, skip it
672
- continue
673
-
674
- 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()