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
@@ -0,0 +1,512 @@
1
+ """Trust Policy Validation Check.
2
+
3
+ Validates trust policies (role assumption policies) for security best practices.
4
+ This check ensures that assume role actions have appropriate principals and conditions.
5
+
6
+ Trust policies are resource-based policies attached to IAM roles that control
7
+ who can assume the role and under what conditions.
8
+
9
+ Key Validations:
10
+ 1. Action-Principal Type Matching
11
+ - sts:AssumeRole → AWS or Service principals
12
+ - sts:AssumeRoleWithSAML → Federated (SAML provider) principals
13
+ - sts:AssumeRoleWithWebIdentity → Federated (OIDC provider) principals
14
+
15
+ 2. Provider ARN Validation
16
+ - SAML providers must match: arn:aws:iam::account:saml-provider/name
17
+ - OIDC providers must match: arn:aws:iam::account:oidc-provider/domain
18
+
19
+ 3. Required Conditions
20
+ - SAML: Requires SAML:aud condition
21
+ - OIDC: Requires provider-specific audience/subject conditions
22
+ - Cross-account: Should have ExternalId or PrincipalOrgID
23
+
24
+ Complements existing checks:
25
+ - principal_validation: Validates which principals are allowed/blocked
26
+ - action_condition_enforcement: Validates required conditions for actions
27
+ - trust_policy_validation: Validates action-principal coupling and trust-specific rules
28
+
29
+ This check is DISABLED by default. Enable it for trust policy validation:
30
+
31
+ trust_policy_validation:
32
+ enabled: true
33
+ severity: high
34
+ """
35
+
36
+ import re
37
+ from typing import TYPE_CHECKING, Any
38
+
39
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
40
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
41
+ from iam_validator.core.models import Statement, ValidationIssue
42
+
43
+ if TYPE_CHECKING:
44
+ pass
45
+
46
+
47
+ class TrustPolicyValidationCheck(PolicyCheck):
48
+ """Validates trust policies for role assumption security."""
49
+
50
+ # Default validation rules for assume actions
51
+ DEFAULT_RULES = {
52
+ "sts:AssumeRole": {
53
+ "allowed_principal_types": ["AWS", "Service"],
54
+ "description": "Standard role assumption",
55
+ },
56
+ "sts:AssumeRoleWithSAML": {
57
+ "allowed_principal_types": ["Federated"],
58
+ "provider_pattern": r"^arn:aws:iam::\d{12}:saml-provider/[\w+=,.@-]+$",
59
+ "required_conditions": ["SAML:aud"],
60
+ "description": "SAML-based federated role assumption",
61
+ },
62
+ "sts:AssumeRoleWithWebIdentity": {
63
+ "allowed_principal_types": ["Federated"],
64
+ "provider_pattern": r"^arn:aws:iam::\d{12}:oidc-provider/[\w./-]+$",
65
+ "required_conditions": ["*:aud"], # Require audience condition (provider-specific key)
66
+ "description": "OIDC-based federated role assumption",
67
+ },
68
+ "sts:TagSession": {
69
+ "allowed_principal_types": ["AWS", "Service", "Federated"],
70
+ "description": "Session tagging during role assumption (can be combined with any assume action)",
71
+ },
72
+ "sts:SetSourceIdentity": {
73
+ "allowed_principal_types": ["AWS", "Service", "Federated"],
74
+ "description": "Set source identity during role assumption (tracks original identity through role chains)",
75
+ },
76
+ "sts:SetContext": {
77
+ "allowed_principal_types": ["AWS", "Service", "Federated"],
78
+ "description": "Set session context during role assumption",
79
+ },
80
+ }
81
+
82
+ @property
83
+ def check_id(self) -> str:
84
+ return "trust_policy_validation"
85
+
86
+ @property
87
+ def description(self) -> str:
88
+ return "Validates trust policies for role assumption security and action-principal coupling"
89
+
90
+ @property
91
+ def default_severity(self) -> str:
92
+ return "high"
93
+
94
+ async def execute(
95
+ self,
96
+ statement: Statement,
97
+ statement_idx: int,
98
+ fetcher: AWSServiceFetcher,
99
+ config: CheckConfig,
100
+ ) -> list[ValidationIssue]:
101
+ """Execute trust policy validation on a single statement.
102
+
103
+ Args:
104
+ statement: The statement to validate
105
+ statement_idx: Index of the statement in the policy
106
+ fetcher: AWS service fetcher instance
107
+ config: Configuration for this check
108
+
109
+ Returns:
110
+ List of validation issues
111
+ """
112
+ issues = []
113
+
114
+ # Skip if no principal (trust policies must have principals)
115
+ if statement.principal is None and statement.not_principal is None:
116
+ return issues
117
+
118
+ # Get actions from statement
119
+ actions = self._get_actions(statement)
120
+ if not actions:
121
+ return issues
122
+
123
+ # Get validation rules (use custom rules if provided, otherwise defaults)
124
+ validation_rules = config.config.get("validation_rules", self.DEFAULT_RULES)
125
+
126
+ # Check each assume action
127
+ for action in actions:
128
+ # Skip wildcard actions (too broad to validate specifically)
129
+ if action == "*" or action == "sts:*":
130
+ continue
131
+
132
+ # Find matching rule (exact matches for assume actions)
133
+ rule = self._find_matching_rule(action, validation_rules)
134
+ if not rule:
135
+ continue # Not an assume action we validate
136
+
137
+ # Validate principal type for this action
138
+ principal_issues = self._validate_principal_type(
139
+ statement, action, rule, statement_idx, config
140
+ )
141
+ issues.extend(principal_issues)
142
+
143
+ # Validate provider ARN format if required
144
+ if "provider_pattern" in rule:
145
+ provider_issues = self._validate_provider_format(
146
+ statement, action, rule, statement_idx, config
147
+ )
148
+ issues.extend(provider_issues)
149
+
150
+ # Validate required conditions
151
+ if "required_conditions" in rule:
152
+ condition_issues = self._validate_required_conditions(
153
+ statement, action, rule, statement_idx, config
154
+ )
155
+ issues.extend(condition_issues)
156
+
157
+ return issues
158
+
159
+ def _get_actions(self, statement: Statement) -> list[str]:
160
+ """Extract actions from statement.
161
+
162
+ Args:
163
+ statement: IAM policy statement
164
+
165
+ Returns:
166
+ List of action strings
167
+ """
168
+ if statement.action is None:
169
+ return []
170
+ return [statement.action] if isinstance(statement.action, str) else statement.action
171
+
172
+ def _find_matching_rule(self, action: str, rules: dict[str, Any]) -> dict[str, Any] | None:
173
+ """Find validation rule matching the action.
174
+
175
+ Supports wildcards in action names.
176
+
177
+ Args:
178
+ action: Action to find rule for (e.g., "sts:AssumeRole")
179
+ rules: Validation rules dict
180
+
181
+ Returns:
182
+ Matching rule dict or None
183
+ """
184
+ # Exact match first (performance optimization)
185
+ if action in rules:
186
+ return rules[action]
187
+
188
+ # Check for wildcard patterns in action
189
+ for rule_action, rule_config in rules.items():
190
+ # Support wildcards in the action being validated
191
+ if "*" in action:
192
+ pattern = action.replace("*", ".*")
193
+ if re.match(f"^{pattern}$", rule_action):
194
+ return rule_config
195
+
196
+ return None
197
+
198
+ def _extract_principal_types(self, statement: Statement) -> dict[str, list[str]]:
199
+ """Extract principals grouped by type (AWS, Service, Federated, etc.).
200
+
201
+ Args:
202
+ statement: IAM policy statement
203
+
204
+ Returns:
205
+ Dict mapping principal type to list of principal values
206
+ """
207
+ principal_types: dict[str, list[str]] = {}
208
+
209
+ if statement.principal:
210
+ if isinstance(statement.principal, str):
211
+ # Simple string principal like "*"
212
+ principal_types["AWS"] = [statement.principal]
213
+ elif isinstance(statement.principal, dict):
214
+ for key, value in statement.principal.items():
215
+ if isinstance(value, str):
216
+ principal_types[key] = [value]
217
+ elif isinstance(value, list):
218
+ principal_types[key] = value
219
+
220
+ return principal_types
221
+
222
+ def _validate_principal_type(
223
+ self,
224
+ statement: Statement,
225
+ action: str,
226
+ rule: dict[str, Any],
227
+ statement_idx: int,
228
+ config: CheckConfig,
229
+ ) -> list[ValidationIssue]:
230
+ """Validate that principal type matches the assume action.
231
+
232
+ Args:
233
+ statement: IAM policy statement
234
+ action: Assume action being validated
235
+ rule: Validation rule for this action
236
+ statement_idx: Statement index
237
+
238
+ Returns:
239
+ List of validation issues
240
+ """
241
+ issues = []
242
+
243
+ allowed_types = rule.get("allowed_principal_types", [])
244
+ if not allowed_types:
245
+ return issues
246
+
247
+ principal_types = self._extract_principal_types(statement)
248
+
249
+ # Check if any principal type is not allowed
250
+ for principal_type, principals in principal_types.items():
251
+ if principal_type not in allowed_types:
252
+ principals_list = ", ".join(f"`{p}`" for p in principals)
253
+ allowed_list = ", ".join(f"`{t}`" for t in allowed_types)
254
+
255
+ issues.append(
256
+ ValidationIssue(
257
+ severity=self.get_severity(config),
258
+ issue_type="invalid_principal_type_for_assume_action",
259
+ message=f"Action `{action}` should not use Principal type `{principal_type}`. "
260
+ f"Expected principal types: {allowed_list}",
261
+ statement_index=statement_idx,
262
+ statement_sid=statement.sid,
263
+ line_number=statement.line_number,
264
+ action=action,
265
+ suggestion=f"For `{action}`, use {allowed_list} principal type instead of `{principal_type}`. "
266
+ f"\n\nFound principals: `{principals_list}`\n\n"
267
+ f"{rule.get('description', '')}",
268
+ example=self._get_example_for_action(
269
+ action, allowed_types[0] if allowed_types else "AWS"
270
+ ),
271
+ )
272
+ )
273
+
274
+ return issues
275
+
276
+ def _validate_provider_format(
277
+ self,
278
+ statement: Statement,
279
+ action: str,
280
+ rule: dict[str, Any],
281
+ statement_idx: int,
282
+ config: CheckConfig,
283
+ ) -> list[ValidationIssue]:
284
+ """Validate that federated provider ARN matches expected format.
285
+
286
+ Args:
287
+ statement: IAM policy statement
288
+ action: Assume action being validated
289
+ rule: Validation rule for this action
290
+ statement_idx: Statement index
291
+
292
+ Returns:
293
+ List of validation issues
294
+ """
295
+ issues = []
296
+
297
+ provider_pattern = rule.get("provider_pattern")
298
+ if not provider_pattern:
299
+ return issues
300
+
301
+ principal_types = self._extract_principal_types(statement)
302
+ federated_principals = principal_types.get("Federated", [])
303
+
304
+ for principal in federated_principals:
305
+ if not re.match(provider_pattern, principal):
306
+ provider_type = "SAML" if "saml-provider" in provider_pattern else "OIDC"
307
+
308
+ issues.append(
309
+ ValidationIssue(
310
+ severity=self.get_severity(config),
311
+ issue_type="invalid_provider_format",
312
+ message=f"Federated principal `{principal}` does not match expected `{provider_type}` provider format for `{action}`",
313
+ statement_index=statement_idx,
314
+ statement_sid=statement.sid,
315
+ line_number=statement.line_number,
316
+ action=action,
317
+ suggestion=f"For `{action}`, use a valid `{provider_type}` provider ARN.\n\n"
318
+ f"Expected pattern: `{provider_pattern}`\n"
319
+ f"Found: `{principal}`",
320
+ example=self._get_provider_example(provider_type),
321
+ )
322
+ )
323
+
324
+ return issues
325
+
326
+ def _validate_required_conditions(
327
+ self,
328
+ statement: Statement,
329
+ action: str,
330
+ rule: dict[str, Any],
331
+ statement_idx: int,
332
+ config: CheckConfig,
333
+ ) -> list[ValidationIssue]:
334
+ """Validate that required conditions are present.
335
+
336
+ Args:
337
+ statement: IAM policy statement
338
+ action: Assume action being validated
339
+ rule: Validation rule for this action
340
+ statement_idx: Statement index
341
+
342
+ Returns:
343
+ List of validation issues
344
+ """
345
+ issues = []
346
+
347
+ required_conditions = rule.get("required_conditions", [])
348
+ if not required_conditions:
349
+ return issues
350
+
351
+ # Get all condition keys from statement
352
+ condition_keys = set()
353
+ if statement.condition:
354
+ for _operator, keys_dict in statement.condition.items():
355
+ if isinstance(keys_dict, dict):
356
+ condition_keys.update(keys_dict.keys())
357
+
358
+ # Check for missing required conditions (supports wildcards like *:aud)
359
+ missing_conditions = []
360
+ for required_cond in required_conditions:
361
+ if "*:" in required_cond:
362
+ # Wildcard pattern - check if any key ends with the suffix
363
+ suffix = required_cond.split("*:")[1]
364
+ if not any(key.endswith(f":{suffix}") for key in condition_keys):
365
+ missing_conditions.append(required_cond)
366
+ else:
367
+ # Exact match
368
+ if required_cond not in condition_keys:
369
+ missing_conditions.append(required_cond)
370
+
371
+ if missing_conditions:
372
+ missing_list = ", ".join(f"`{c}`" for c in missing_conditions)
373
+
374
+ issues.append(
375
+ ValidationIssue(
376
+ severity=self.get_severity(config),
377
+ issue_type="missing_required_condition_for_assume_action",
378
+ message=f"Action `{action}` is missing required conditions: `{missing_list}`",
379
+ statement_index=statement_idx,
380
+ statement_sid=statement.sid,
381
+ line_number=statement.line_number,
382
+ action=action,
383
+ suggestion=f"Add required condition(s) to restrict when `{action}` can be performed. "
384
+ f"Missing: `{missing_list}`\n\n"
385
+ f"{rule.get('description', '')}",
386
+ example=self._get_condition_example(action, required_conditions[0]),
387
+ )
388
+ )
389
+
390
+ return issues
391
+
392
+ def _get_example_for_action(self, action: str, principal_type: str) -> str:
393
+ """Generate example JSON for an assume action.
394
+
395
+ Args:
396
+ action: Assume action
397
+ principal_type: Expected principal type
398
+
399
+ Returns:
400
+ JSON example string
401
+ """
402
+ examples = {
403
+ ("sts:AssumeRole", "AWS"): """{
404
+ "Effect": "Allow",
405
+ "Principal": {
406
+ "AWS": "arn:aws:iam::123456789012:root"
407
+ },
408
+ "Action": "sts:AssumeRole",
409
+ "Condition": {
410
+ "StringEquals": {
411
+ "sts:ExternalId": "unique-external-id"
412
+ }
413
+ }
414
+ }""",
415
+ ("sts:AssumeRole", "Service"): """{
416
+ "Effect": "Allow",
417
+ "Principal": {
418
+ "Service": "lambda.amazonaws.com"
419
+ },
420
+ "Action": "sts:AssumeRole"
421
+ }""",
422
+ ("sts:AssumeRoleWithSAML", "Federated"): """{
423
+ "Effect": "Allow",
424
+ "Principal": {
425
+ "Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
426
+ },
427
+ "Action": "sts:AssumeRoleWithSAML",
428
+ "Condition": {
429
+ "StringEquals": {
430
+ "SAML:aud": "https://signin.aws.amazon.com/saml"
431
+ }
432
+ }
433
+ }""",
434
+ ("sts:AssumeRoleWithWebIdentity", "Federated"): """{
435
+ "Effect": "Allow",
436
+ "Principal": {
437
+ "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
438
+ },
439
+ "Action": "sts:AssumeRoleWithWebIdentity",
440
+ "Condition": {
441
+ "StringEquals": {
442
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
443
+ },
444
+ "StringLike": {
445
+ "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
446
+ }
447
+ }
448
+ }""",
449
+ }
450
+
451
+ return examples.get((action, principal_type), "")
452
+
453
+ def _get_provider_example(self, provider_type: str) -> str:
454
+ """Get example provider ARN.
455
+
456
+ Args:
457
+ provider_type: Type of provider (SAML or OIDC)
458
+
459
+ Returns:
460
+ Example ARN string
461
+ """
462
+ if provider_type == "SAML":
463
+ return """{
464
+ "Principal": {
465
+ "Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
466
+ }
467
+ }"""
468
+ else: # OIDC
469
+ return """{
470
+ "Principal": {
471
+ "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
472
+ }
473
+ }"""
474
+
475
+ def _get_condition_example(self, action: str, condition_key: str) -> str:
476
+ """Get example condition for an action.
477
+
478
+ Args:
479
+ action: Assume action
480
+ condition_key: Required condition key
481
+
482
+ Returns:
483
+ JSON example string
484
+ """
485
+ examples = {
486
+ "SAML:aud": """{
487
+ "Condition": {
488
+ "StringEquals": {
489
+ "SAML:aud": "https://signin.aws.amazon.com/saml"
490
+ }
491
+ }
492
+ }""",
493
+ "sts:ExternalId": """{
494
+ "Condition": {
495
+ "StringEquals": {
496
+ "sts:ExternalId": "unique-external-id-shared-with-trusted-party"
497
+ }
498
+ }
499
+ }""",
500
+ "aws:PrincipalOrgID": """{
501
+ "Condition": {
502
+ "StringEquals": {
503
+ "aws:PrincipalOrgID": "o-123456789"
504
+ }
505
+ }
506
+ }""",
507
+ }
508
+
509
+ return examples.get(
510
+ condition_key,
511
+ f'{{\n "Condition": {{\n "StringEquals": {{\n "{condition_key}": "value"\n }}\n }}\n}}',
512
+ )
@@ -5,15 +5,13 @@ configurations, supporting exact matches, regex patterns, and any_of/all_of logi
5
5
 
6
6
  Performance optimizations:
7
7
  - Uses frozenset for O(1) lookups
8
- - LRU cache for compiled regex patterns
8
+ - Centralized LRU cache for compiled regex patterns (from ignore_patterns module)
9
9
  - Lazy loading of default actions from modular data structure
10
10
  """
11
11
 
12
- import re
13
- from functools import lru_cache
14
-
15
12
  from iam_validator.core.check_registry import CheckConfig
16
13
  from iam_validator.core.config.sensitive_actions import get_sensitive_actions
14
+ from iam_validator.core.ignore_patterns import compile_pattern
17
15
 
18
16
  # Lazy-loaded default set of sensitive actions
19
17
  # This will be loaded only when first accessed
@@ -37,7 +35,9 @@ def _get_default_sensitive_actions() -> frozenset[str]:
37
35
  return _DEFAULT_SENSITIVE_ACTIONS_CACHE
38
36
 
39
37
 
40
- def get_sensitive_actions_by_categories(categories: list[str] | None = None) -> frozenset[str]:
38
+ def get_sensitive_actions_by_categories(
39
+ categories: list[str] | None = None,
40
+ ) -> frozenset[str]:
41
41
  """
42
42
  Get sensitive actions filtered by categories.
43
43
 
@@ -66,25 +66,10 @@ def get_sensitive_actions_by_categories(categories: list[str] | None = None) ->
66
66
  DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
67
67
 
68
68
 
69
- # Global regex pattern cache for performance
70
- @lru_cache(maxsize=256)
71
- def compile_pattern(pattern: str) -> re.Pattern[str] | None:
72
- """Compile and cache regex patterns.
73
-
74
- Args:
75
- pattern: Regex pattern string
76
-
77
- Returns:
78
- Compiled pattern or None if invalid
79
- """
80
- try:
81
- return re.compile(pattern)
82
- except re.error:
83
- return None
84
-
85
-
86
69
  def check_sensitive_actions(
87
- actions: list[str], config: CheckConfig, default_actions: frozenset[str] | None = None
70
+ actions: list[str],
71
+ config: CheckConfig,
72
+ default_actions: frozenset[str] | None = None,
88
73
  ) -> tuple[bool, list[str]]:
89
74
  """
90
75
  Check if actions match sensitive action criteria with any_of/all_of support.
@@ -115,6 +100,10 @@ def check_sensitive_actions(
115
100
  # Use all categories if no specific categories configured
116
101
  default_actions = _get_default_sensitive_actions()
117
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
+
118
107
  # Filter out wildcards
119
108
  filtered_actions = [a for a in actions if a != "*"]
120
109
  if not filtered_actions:
@@ -141,6 +130,17 @@ def check_sensitive_actions(
141
130
  matched_set = set(actions_matched) | set(patterns_matched)
142
131
  matched_actions = list(matched_set)
143
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
144
  return is_sensitive, matched_actions
145
145
 
146
146
 
@@ -243,7 +243,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
243
243
  # Each item can be a string pattern, or a dict with any_of/all_of
244
244
  if isinstance(item, str):
245
245
  # Simple string pattern - check if any action matches
246
- # Use cached compiled pattern
246
+ # Use cached compiled pattern from centralized ignore_patterns module
247
247
  compiled = compile_pattern(item)
248
248
  if compiled:
249
249
  for action in actions:
@@ -262,7 +262,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
262
262
  # any_of: at least one action must match at least one pattern
263
263
  if "any_of" in config:
264
264
  matched = set()
265
- # Pre-compile all patterns
265
+ # Pre-compile all patterns using centralized cache
266
266
  compiled_patterns = [compile_pattern(p) for p in config["any_of"]]
267
267
 
268
268
  for action in actions:
@@ -274,7 +274,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
274
274
 
275
275
  # all_of: at least one action must match ALL patterns
276
276
  if "all_of" in config:
277
- # Pre-compile all patterns
277
+ # Pre-compile all patterns using centralized cache
278
278
  compiled_patterns = [compile_pattern(p) for p in config["all_of"]]
279
279
  # Filter out invalid patterns
280
280
  compiled_patterns = [p for p in compiled_patterns if p]
@@ -72,7 +72,7 @@ async def expand_wildcard_actions(actions: list[str], fetcher: AWSServiceFetcher
72
72
  available_actions = list(service_detail.actions.keys())
73
73
 
74
74
  # Match wildcard pattern against available actions
75
- _, matched_actions = fetcher._match_wildcard_action(action_name, available_actions)
75
+ _, matched_actions = fetcher.match_wildcard_action(action_name, available_actions)
76
76
 
77
77
  # Add expanded actions with service prefix
78
78
  for matched_action in matched_actions:
@@ -38,20 +38,15 @@ class WildcardActionCheck(PolicyCheck):
38
38
 
39
39
  # Check for wildcard action (Action: "*")
40
40
  if "*" in actions:
41
- message = config.config.get("message", "Statement allows all actions (*)")
42
- suggestion_text = config.config.get(
41
+ message = config.config.get(
42
+ "message", 'Statement allows all actions `"*"` (wildcard action).'
43
+ )
44
+ suggestion = config.config.get(
43
45
  "suggestion",
44
46
  "Replace wildcard with specific actions needed for your use case",
45
47
  )
46
48
  example = config.config.get("example", "")
47
49
 
48
- # Combine suggestion + example
49
- suggestion = (
50
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
51
- if example
52
- else suggestion_text
53
- )
54
-
55
50
  issues.append(
56
51
  ValidationIssue(
57
52
  severity=self.get_severity(config),
@@ -60,6 +55,7 @@ class WildcardActionCheck(PolicyCheck):
60
55
  issue_type="overly_permissive",
61
56
  message=message,
62
57
  suggestion=suggestion,
58
+ example=example if example else None,
63
59
  line_number=statement.line_number,
64
60
  )
65
61
  )