iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.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 (56) hide show
  1. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +127 -6
  2. iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +5 -3
  6. iam_validator/checks/action_condition_enforcement.py +559 -207
  7. iam_validator/checks/action_resource_matching.py +12 -15
  8. iam_validator/checks/action_validation.py +7 -13
  9. iam_validator/checks/condition_key_validation.py +7 -13
  10. iam_validator/checks/condition_type_mismatch.py +15 -22
  11. iam_validator/checks/full_wildcard.py +9 -13
  12. iam_validator/checks/mfa_condition_check.py +8 -17
  13. iam_validator/checks/policy_size.py +6 -39
  14. iam_validator/checks/policy_structure.py +547 -0
  15. iam_validator/checks/policy_type_validation.py +61 -46
  16. iam_validator/checks/principal_validation.py +71 -148
  17. iam_validator/checks/resource_validation.py +13 -20
  18. iam_validator/checks/sensitive_action.py +15 -18
  19. iam_validator/checks/service_wildcard.py +8 -14
  20. iam_validator/checks/set_operator_validation.py +21 -28
  21. iam_validator/checks/sid_uniqueness.py +16 -42
  22. iam_validator/checks/trust_policy_validation.py +506 -0
  23. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  24. iam_validator/checks/utils/wildcard_expansion.py +2 -2
  25. iam_validator/checks/wildcard_action.py +9 -13
  26. iam_validator/checks/wildcard_resource.py +9 -13
  27. iam_validator/commands/cache.py +4 -3
  28. iam_validator/commands/validate.py +15 -9
  29. iam_validator/core/__init__.py +2 -3
  30. iam_validator/core/access_analyzer.py +1 -1
  31. iam_validator/core/access_analyzer_report.py +2 -2
  32. iam_validator/core/aws_fetcher.py +24 -1028
  33. iam_validator/core/aws_service/__init__.py +21 -0
  34. iam_validator/core/aws_service/cache.py +108 -0
  35. iam_validator/core/aws_service/client.py +205 -0
  36. iam_validator/core/aws_service/fetcher.py +612 -0
  37. iam_validator/core/aws_service/parsers.py +149 -0
  38. iam_validator/core/aws_service/patterns.py +51 -0
  39. iam_validator/core/aws_service/storage.py +291 -0
  40. iam_validator/core/aws_service/validators.py +379 -0
  41. iam_validator/core/check_registry.py +165 -93
  42. iam_validator/core/config/condition_requirements.py +69 -17
  43. iam_validator/core/config/defaults.py +58 -52
  44. iam_validator/core/config/service_principals.py +40 -3
  45. iam_validator/core/constants.py +17 -0
  46. iam_validator/core/ignore_patterns.py +297 -0
  47. iam_validator/core/models.py +15 -5
  48. iam_validator/core/policy_checks.py +38 -475
  49. iam_validator/core/policy_loader.py +27 -4
  50. iam_validator/sdk/__init__.py +1 -1
  51. iam_validator/sdk/context.py +1 -1
  52. iam_validator/sdk/helpers.py +1 -1
  53. iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
  54. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
  55. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
  56. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -22,7 +22,6 @@ from iam_validator.core.config.condition_requirements import CONDITION_REQUIREME
22
22
  from iam_validator.core.config.principal_requirements import (
23
23
  get_default_principal_requirements,
24
24
  )
25
- from iam_validator.core.config.service_principals import DEFAULT_SERVICE_PRINCIPALS
26
25
  from iam_validator.core.config.wildcards import (
27
26
  DEFAULT_ALLOWED_WILDCARDS,
28
27
  DEFAULT_SERVICE_WILDCARDS,
@@ -195,61 +194,68 @@ DEFAULT_CONFIG = {
195
194
  # 9. PRINCIPAL VALIDATION
196
195
  # ========================================================================
197
196
  # Validates Principal elements in resource-based policies
198
- # (S3 buckets, SNS topics, SQS queues, etc.)
199
- # Only runs when --policy-type RESOURCE_POLICY is specified
197
+ # Applies to: S3 buckets, SNS topics, SQS queues, Lambda functions, etc.
198
+ # Only runs when: --policy-type RESOURCE_POLICY
200
199
  #
201
- # See: iam_validator/core/config/service_principals.py for defaults
200
+ # Three control mechanisms:
201
+ # 1. blocked_principals - Block specific principals (deny list)
202
+ # 2. allowed_principals - Allow only specific principals (whitelist mode)
203
+ # 3. principal_condition_requirements - Require conditions for principals
204
+ # 4. allowed_service_principals - Always allow AWS service principals
202
205
  "principal_validation": {
203
206
  "enabled": True,
204
207
  "severity": "high", # Security issue, not IAM validity error
205
208
  "description": "Validates Principal elements in resource policies for security best practices",
206
- # blocked_principals: Principals that should NEVER be allowed (deny list)
207
- # Default: ["*"] blocks public access to everyone
208
- # Examples:
209
- # ["*"] - Block public access
210
- # ["*", "arn:aws:iam::*:root"] - Block public + all AWS accounts
209
+ # blocked_principals: Deny list - these principals are never allowed
210
+ # Default: ["*"] blocks public access
211
211
  "blocked_principals": ["*"],
212
- # allowed_principals: When set, ONLY these principals are allowed (whitelist mode)
213
- # Leave empty to allow all except blocked principals
214
- # Examples:
215
- # [] - Allow all (except blocked)
216
- # ["arn:aws:iam::123456789012:root"] - Only allow specific account
217
- # ["arn:aws:iam::*:role/OrgAccessRole"] - Allow specific role in any account
212
+ # allowed_principals: Whitelist mode - when set, ONLY these are allowed
213
+ # Default: [] allows all (except blocked)
218
214
  "allowed_principals": [],
219
- # require_conditions_for: Principals that MUST have specific IAM conditions
220
- # Format: {principal_pattern: [required_condition_keys]}
221
- # Default: Public access (*) must specify source to limit scope
222
- # Examples:
223
- # "*": ["aws:SourceArn"] - Public access must specify source ARN
224
- # "arn:aws:iam::*:root": ["aws:PrincipalOrgID"] - Cross-account must be from org
225
- "require_conditions_for": {
226
- "*": [
227
- "aws:SourceArn",
228
- "aws:SourceAccount",
229
- "aws:SourceVpce",
230
- "aws:SourceIp",
231
- "aws:SourceOrgID",
232
- "aws:SourceOrgPaths",
233
- ],
234
- },
235
- # principal_condition_requirements: Advanced condition requirements for principals
236
- # Similar to action_condition_enforcement but for principals
237
- # Supports all_of/any_of/none_of logic with rich metadata
238
- # Default: 2 critical requirements enabled (public_access, prevent_insecure_transport)
215
+ # principal_condition_requirements: Require conditions for specific principals
216
+ # Supports all_of/any_of/none_of logic like action_condition_enforcement
217
+ # Default: 2 enabled (public_access, prevent_insecure_transport)
239
218
  # See: iam_validator/core/config/principal_requirements.py
240
- # To customize requirements, use Python API:
241
- # from iam_validator.core.config import get_principal_requirements_by_names
242
- # requirements = get_principal_requirements_by_names(['public_access', 'cross_account_org'])
243
- # To disable: set to empty list []
244
219
  "principal_condition_requirements": get_default_principal_requirements(),
245
- # allowed_service_principals: AWS service principals that are always allowed
246
- # Default: 16 common AWS services (cloudfront, s3, lambda, logs, etc.)
247
- # These are typically safe as AWS services need access to resources
248
- # See: iam_validator/core/config/service_principals.py
249
- "allowed_service_principals": list(DEFAULT_SERVICE_PRINCIPALS),
220
+ # allowed_service_principals: AWS service principals (*.amazonaws.com)
221
+ # Default: ["aws:*"] allows ALL AWS service principals
222
+ # Note: "aws:*" is different from "*" (public access)
223
+ "allowed_service_principals": ["aws:*"],
224
+ },
225
+ # ========================================================================
226
+ # 10. TRUST POLICY VALIDATION
227
+ # ========================================================================
228
+ # Validate trust policies (role assumption policies) for security best practices
229
+ # Ensures assume role actions have appropriate principals and conditions
230
+ #
231
+ # Key validations:
232
+ # - Action-Principal type matching (e.g., AssumeRoleWithSAML needs Federated)
233
+ # - Provider ARN format validation (SAML vs OIDC provider patterns)
234
+ # - Required conditions per assume method
235
+ #
236
+ # Complements principal_validation check (which validates principal allowlists/blocklists)
237
+ # This check focuses on action-principal coupling specific to trust policies
238
+ #
239
+ # Auto-detection: Only runs on statements with assume role actions
240
+ "trust_policy_validation": {
241
+ "enabled": True, # Enabled by default (auto-detects trust policies)
242
+ "severity": "high", # Security issue
243
+ "description": "Validates trust policies for role assumption security and action-principal coupling",
244
+ # validation_rules: Custom rules override defaults
245
+ # Default rules validate:
246
+ # - sts:AssumeRole → AWS or Service principals
247
+ # - sts:AssumeRoleWithSAML → Federated (SAML provider) with SAML:aud
248
+ # - sts:AssumeRoleWithWebIdentity → Federated (OIDC provider)
249
+ # Example custom rules:
250
+ # "validation_rules": {
251
+ # "sts:AssumeRole": {
252
+ # "allowed_principal_types": ["AWS"], # Only AWS, not Service
253
+ # "required_conditions": ["sts:ExternalId"], # Always require ExternalId
254
+ # }
255
+ # }
250
256
  },
251
257
  # ========================================================================
252
- # 10. POLICY TYPE VALIDATION
258
+ # 11. POLICY TYPE VALIDATION
253
259
  # ========================================================================
254
260
  # Validate policy type requirements (new in v1.3.0)
255
261
  # Ensures policies conform to the declared type (IDENTITY vs RESOURCE_POLICY)
@@ -264,7 +270,7 @@ DEFAULT_CONFIG = {
264
270
  "description": "Validates policies match declared type and enforces RCP requirements",
265
271
  },
266
272
  # ========================================================================
267
- # 11. ACTION-RESOURCE MATCHING
273
+ # 12. ACTION-RESOURCE MATCHING
268
274
  # ========================================================================
269
275
  # Validate action-resource matching
270
276
  # Ensures resources match the required resource types for actions
@@ -304,7 +310,7 @@ DEFAULT_CONFIG = {
304
310
  # See: iam_validator/core/config/sensitive_actions.py for sensitive actions
305
311
  # ========================================================================
306
312
  # ========================================================================
307
- # 12. WILDCARD ACTION
313
+ # 13. WILDCARD ACTION
308
314
  # ========================================================================
309
315
  # Check for wildcard actions (Action: "*")
310
316
  # Flags statements that allow all actions
@@ -323,7 +329,7 @@ DEFAULT_CONFIG = {
323
329
  ),
324
330
  },
325
331
  # ========================================================================
326
- # 13. WILDCARD RESOURCE
332
+ # 14. WILDCARD RESOURCE
327
333
  # ========================================================================
328
334
  # Check for wildcard resources (Resource: "*")
329
335
  # Flags statements that apply to all resources
@@ -350,7 +356,7 @@ DEFAULT_CONFIG = {
350
356
  ),
351
357
  },
352
358
  # ========================================================================
353
- # 14. FULL WILDCARD (CRITICAL)
359
+ # 15. FULL WILDCARD (CRITICAL)
354
360
  # ========================================================================
355
361
  # Check for BOTH Action: "*" AND Resource: "*" (CRITICAL)
356
362
  # This grants full administrative access (AdministratorAccess equivalent)
@@ -374,7 +380,7 @@ DEFAULT_CONFIG = {
374
380
  ),
375
381
  },
376
382
  # ========================================================================
377
- # 15. SERVICE WILDCARD
383
+ # 16. SERVICE WILDCARD
378
384
  # ========================================================================
379
385
  # Check for service-level wildcards (e.g., "iam:*", "s3:*", "ec2:*")
380
386
  # These grant ALL permissions for a service (often too permissive)
@@ -404,7 +410,7 @@ DEFAULT_CONFIG = {
404
410
  ),
405
411
  },
406
412
  # ========================================================================
407
- # 16. SENSITIVE ACTION
413
+ # 17. SENSITIVE ACTION
408
414
  # ========================================================================
409
415
  # Check for sensitive actions without IAM conditions
410
416
  # Sensitive actions: IAM changes, secrets access, destructive operations
@@ -479,7 +485,7 @@ DEFAULT_CONFIG = {
479
485
  ],
480
486
  },
481
487
  # ========================================================================
482
- # 17. ACTION CONDITION ENFORCEMENT
488
+ # 18. ACTION CONDITION ENFORCEMENT
483
489
  # ========================================================================
484
490
  # Enforce specific IAM condition requirements for actions
485
491
  # Examples: iam:PassRole must specify iam:PassedToService,
@@ -1,8 +1,15 @@
1
1
  """
2
- Default service principals for resource policy validation.
2
+ Service principals utilities for resource policy validation.
3
3
 
4
- These are AWS service principals that are commonly used and considered safe
5
- in resource-based policies (S3 bucket policies, SNS topic policies, etc.).
4
+ This module provides:
5
+ - Default list of common AWS service principals
6
+ - Utility to check if a principal is any AWS service principal
7
+ - Functions to categorize service principals by type
8
+
9
+ Configuration:
10
+ - Use "*" in allowed_service_principals to allow ALL AWS service principals
11
+ - Use explicit list to restrict to specific services only
12
+ - AWS service principals end with .amazonaws.com or .amazonaws.com.cn
6
13
  """
7
14
 
8
15
  from typing import Final
@@ -58,6 +65,36 @@ def is_allowed_service_principal(principal: str) -> bool:
58
65
  return principal in DEFAULT_SERVICE_PRINCIPALS
59
66
 
60
67
 
68
+ def is_aws_service_principal(principal: str) -> bool:
69
+ """
70
+ Check if a principal is an AWS service principal (any AWS service).
71
+
72
+ This checks if the principal matches the AWS service principal pattern.
73
+ AWS service principals typically end with ".amazonaws.com" or ".amazonaws.com.cn"
74
+
75
+ Args:
76
+ principal: Principal to check (e.g., "lambda.amazonaws.com", "s3.amazonaws.com.cn")
77
+
78
+ Returns:
79
+ True if principal matches AWS service principal pattern
80
+
81
+ Examples:
82
+ >>> is_aws_service_principal("lambda.amazonaws.com")
83
+ True
84
+ >>> is_aws_service_principal("s3.amazonaws.com.cn")
85
+ True
86
+ >>> is_aws_service_principal("arn:aws:iam::123456789012:root")
87
+ False
88
+ >>> is_aws_service_principal("*")
89
+ False
90
+ """
91
+ if not isinstance(principal, str):
92
+ return False
93
+
94
+ # AWS service principals end with .amazonaws.com or .amazonaws.com.cn
95
+ return principal.endswith(".amazonaws.com") or principal.endswith(".amazonaws.com.cn")
96
+
97
+
61
98
  def get_service_principals_by_category() -> dict[str, tuple[str, ...]]:
62
99
  """
63
100
  Get service principals organized by service category.
@@ -123,6 +123,23 @@ DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
123
123
  # Time conversion constants
124
124
  SECONDS_PER_HOUR = 3600
125
125
 
126
+ # ============================================================================
127
+ # Policy Type Restrictions
128
+ # ============================================================================
129
+
130
+ # AWS services that support Resource Control Policies (RCP)
131
+ # These services can have wildcard actions in RCP policy statements
132
+ # Reference: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_rcps.html
133
+ RCP_SUPPORTED_SERVICES = frozenset(
134
+ {
135
+ "s3",
136
+ "sts",
137
+ "sqs",
138
+ "secretsmanager",
139
+ "kms",
140
+ }
141
+ )
142
+
126
143
  # ============================================================================
127
144
  # AWS Documentation URLs
128
145
  # ============================================================================
@@ -0,0 +1,297 @@
1
+ """
2
+ Centralized ignore patterns utility with caching and performance optimization.
3
+
4
+ This module provides high-performance pattern matching for ignore_patterns across
5
+ all checks. Uses LRU caching and compiled regex patterns for optimal performance.
6
+ """
7
+
8
+ import re
9
+ from functools import lru_cache
10
+ from typing import Any
11
+
12
+ from iam_validator.core.models import ValidationIssue
13
+
14
+
15
+ # Global regex pattern cache (shared across all checks for maximum efficiency)
16
+ @lru_cache(maxsize=512)
17
+ def compile_pattern(pattern: str) -> re.Pattern[str] | None:
18
+ """
19
+ Compile and cache regex patterns.
20
+
21
+ Uses LRU cache to avoid recompiling the same patterns across multiple calls.
22
+ This is critical for performance when the same patterns are used repeatedly.
23
+
24
+ This is a public API function used across multiple modules for consistent
25
+ regex caching.
26
+
27
+ Args:
28
+ pattern: Regex pattern string
29
+
30
+ Returns:
31
+ Compiled pattern or None if invalid
32
+
33
+ Performance:
34
+ - First call: O(n) compile time
35
+ - Cached calls: O(1) lookup
36
+ """
37
+ try:
38
+ return re.compile(str(pattern), re.IGNORECASE)
39
+ except re.error:
40
+ return None
41
+
42
+
43
+ class IgnorePatternMatcher:
44
+ """
45
+ High-performance pattern matcher for ignore_patterns.
46
+
47
+ Features:
48
+ - Cached compiled regex patterns (LRU cache)
49
+ - Support for new (simple) and old (verbose) field names
50
+ - Efficient filtering with early exit optimization
51
+ - Field-specific validation logic
52
+
53
+ Thread-safe: Yes (regex compilation is cached globally)
54
+ """
55
+
56
+ # Supported field name mappings (new -> old for backward compatibility)
57
+ FIELD_ALIASES = {
58
+ "filepath": "filepath_regex",
59
+ "action": "action_matches",
60
+ "resource": "resource_matches",
61
+ "sid": "statement_sid",
62
+ "condition_key": "condition_key_matches",
63
+ }
64
+
65
+ @staticmethod
66
+ def should_ignore_issue(
67
+ issue: ValidationIssue,
68
+ filepath: str,
69
+ ignore_patterns: list[dict[str, Any]],
70
+ ) -> bool:
71
+ """
72
+ Check if a ValidationIssue should be ignored based on patterns.
73
+
74
+ Pattern Matching Logic:
75
+ - Multiple fields in ONE pattern = AND logic (all must match)
76
+ - Multiple patterns = OR logic (any pattern matches → ignore)
77
+
78
+ Args:
79
+ issue: The validation issue to check
80
+ filepath: Path to the policy file
81
+ ignore_patterns: List of pattern dictionaries
82
+
83
+ Returns:
84
+ True if the issue should be ignored
85
+
86
+ Performance:
87
+ - Early exit on first match (OR logic)
88
+ - Cached regex compilation
89
+ - O(p * f) where p=patterns, f=fields per pattern
90
+ """
91
+ if not ignore_patterns:
92
+ return False
93
+
94
+ for pattern in ignore_patterns:
95
+ if IgnorePatternMatcher._matches_pattern(pattern, issue, filepath):
96
+ return True # Early exit on first match
97
+
98
+ return False
99
+
100
+ @staticmethod
101
+ def filter_actions(
102
+ actions: frozenset[str],
103
+ ignore_patterns: list[dict[str, Any]],
104
+ ) -> frozenset[str]:
105
+ """
106
+ Filter actions based on action ignore patterns.
107
+
108
+ Only considers patterns that contain an "action" or "action_matches" field.
109
+ This is optimized for the sensitive_action check which needs to filter
110
+ actions before creating ValidationIssues.
111
+
112
+ Supports both single action patterns and lists:
113
+ - action: "s3:.*" # Single regex pattern
114
+ - action: ["s3:GetObject", "s3:PutObject"] # List of patterns
115
+
116
+ Args:
117
+ actions: Set of actions to filter
118
+ ignore_patterns: List of pattern dictionaries
119
+
120
+ Returns:
121
+ Filtered set of actions (actions matching patterns removed)
122
+
123
+ Performance:
124
+ - Extracts action patterns once: O(p) where p=patterns
125
+ - Filters with cached regex: O(a * p) where a=actions, p=patterns
126
+ - Early exit per action when match found
127
+ """
128
+ if not ignore_patterns:
129
+ return actions
130
+
131
+ # Extract action patterns once (cache-friendly)
132
+ action_patterns = []
133
+ for pattern in ignore_patterns:
134
+ # Support both new and old field names
135
+ action_regex = pattern.get("action") or pattern.get("action_matches")
136
+ if action_regex:
137
+ # Support both single string and list of strings
138
+ if isinstance(action_regex, list):
139
+ action_patterns.extend(action_regex)
140
+ else:
141
+ action_patterns.append(action_regex)
142
+
143
+ if not action_patterns:
144
+ return actions
145
+
146
+ # Filter actions with compiled patterns (cached)
147
+ filtered = set()
148
+ for action in actions:
149
+ should_keep = True
150
+ for pattern_str in action_patterns:
151
+ compiled = compile_pattern(pattern_str)
152
+ if compiled and compiled.search(str(action)):
153
+ should_keep = False
154
+ break # Early exit on first match
155
+
156
+ if should_keep:
157
+ filtered.add(action)
158
+
159
+ return frozenset(filtered)
160
+
161
+ @staticmethod
162
+ def _matches_pattern(
163
+ pattern: dict[str, Any],
164
+ issue: ValidationIssue,
165
+ filepath: str,
166
+ ) -> bool:
167
+ """
168
+ Check if issue matches a single ignore pattern.
169
+
170
+ All fields in pattern must match (AND logic).
171
+ For list-based fields (like action), ANY match from the list counts (OR logic).
172
+
173
+ Args:
174
+ pattern: Pattern dict with optional fields
175
+ issue: ValidationIssue to check
176
+ filepath: Path to policy file
177
+
178
+ Returns:
179
+ True if all fields in pattern match the issue
180
+
181
+ Performance:
182
+ - Early exit on first non-match (AND logic)
183
+ - Uses cached compiled patterns
184
+ """
185
+ for field_name, regex_pattern in pattern.items():
186
+ # Get actual value from issue based on field name
187
+ actual_value = IgnorePatternMatcher._get_field_value(field_name, issue, filepath)
188
+
189
+ # Handle special case: SID with exact match (no regex)
190
+ if field_name in ("sid", "statement_sid"):
191
+ # Support both single string and list of strings
192
+ if isinstance(regex_pattern, list):
193
+ # List of SIDs - exact match or regex
194
+ matched = False
195
+ for single_sid in regex_pattern:
196
+ if isinstance(single_sid, str) and "*" not in single_sid:
197
+ # Exact match
198
+ if issue.statement_sid == single_sid:
199
+ matched = True
200
+ break
201
+ else:
202
+ # Regex match
203
+ compiled = compile_pattern(str(single_sid))
204
+ if compiled and compiled.search(str(issue.statement_sid or "")):
205
+ matched = True
206
+ break
207
+ if not matched:
208
+ return False
209
+ continue
210
+ elif isinstance(regex_pattern, str) and "*" not in regex_pattern:
211
+ # Single SID - exact match (not a regex)
212
+ if issue.statement_sid != regex_pattern:
213
+ return False # Early exit on non-match
214
+ continue
215
+
216
+ # Regex match for all other cases
217
+ if actual_value is None:
218
+ return False # Early exit on missing value
219
+
220
+ # Support list of patterns (OR logic - any match succeeds)
221
+ if isinstance(regex_pattern, list):
222
+ matched = False
223
+ for single_pattern in regex_pattern:
224
+ compiled = compile_pattern(str(single_pattern))
225
+ if compiled and compiled.search(str(actual_value)):
226
+ matched = True
227
+ break # Found a match in the list
228
+ if not matched:
229
+ return False # None of the patterns matched
230
+ else:
231
+ # Single pattern
232
+ compiled = compile_pattern(str(regex_pattern))
233
+ if not compiled or not compiled.search(str(actual_value)):
234
+ return False # Early exit on non-match
235
+
236
+ return True # All fields matched
237
+
238
+ @staticmethod
239
+ def _get_field_value(
240
+ field_name: str,
241
+ issue: ValidationIssue,
242
+ filepath: str,
243
+ ) -> str | None:
244
+ """
245
+ Extract field value from issue or filepath.
246
+
247
+ Supports both new (simple) and old (verbose) field names for
248
+ backward compatibility.
249
+
250
+ Args:
251
+ field_name: Name of the field to extract
252
+ issue: ValidationIssue to extract from
253
+ filepath: Policy file path
254
+
255
+ Returns:
256
+ Field value as string, or None if field not recognized
257
+ """
258
+ # Normalize field name (support both old and new names)
259
+ if field_name in ("filepath", "filepath_regex"):
260
+ return filepath
261
+ elif field_name in ("action", "action_matches"):
262
+ return issue.action or ""
263
+ elif field_name in ("resource", "resource_matches"):
264
+ return issue.resource or ""
265
+ elif field_name in ("sid", "statement_sid"):
266
+ return issue.statement_sid or ""
267
+ elif field_name in ("condition_key", "condition_key_matches"):
268
+ return issue.condition_key or ""
269
+ else:
270
+ # Unknown field - skip (don't fail)
271
+ return None
272
+
273
+
274
+ # Convenience functions for backward compatibility
275
+ def should_ignore_issue(
276
+ issue: ValidationIssue,
277
+ filepath: str,
278
+ ignore_patterns: list[dict[str, Any]],
279
+ ) -> bool:
280
+ """
281
+ Convenience function for checking if an issue should be ignored.
282
+
283
+ See IgnorePatternMatcher.should_ignore_issue() for details.
284
+ """
285
+ return IgnorePatternMatcher.should_ignore_issue(issue, filepath, ignore_patterns)
286
+
287
+
288
+ def filter_actions(
289
+ actions: frozenset[str],
290
+ ignore_patterns: list[dict[str, Any]],
291
+ ) -> frozenset[str]:
292
+ """
293
+ Convenience function for filtering actions.
294
+
295
+ See IgnorePatternMatcher.filter_actions() for details.
296
+ """
297
+ return IgnorePatternMatcher.filter_actions(actions, ignore_patterns)
@@ -14,6 +14,7 @@ from iam_validator.core import constants
14
14
  PolicyType = Literal[
15
15
  "IDENTITY_POLICY",
16
16
  "RESOURCE_POLICY",
17
+ "TRUST_POLICY", # Trust policies (role assumption policies - subtype of resource policies)
17
18
  "SERVICE_CONTROL_POLICY",
18
19
  "RESOURCE_CONTROL_POLICY",
19
20
  ]
@@ -105,10 +106,10 @@ class ServiceDetail(BaseModel):
105
106
  class Statement(BaseModel):
106
107
  """IAM policy statement."""
107
108
 
108
- model_config = ConfigDict(populate_by_name=True, extra="forbid")
109
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
109
110
 
110
111
  sid: str | None = Field(default=None, alias="Sid")
111
- effect: str = Field(alias="Effect")
112
+ effect: str | None = Field(default=None, alias="Effect")
112
113
  action: list[str] | str | None = Field(default=None, alias="Action")
113
114
  not_action: list[str] | str | None = Field(default=None, alias="NotAction")
114
115
  resource: list[str] | str | None = Field(default=None, alias="Resource")
@@ -135,10 +136,10 @@ class Statement(BaseModel):
135
136
  class IAMPolicy(BaseModel):
136
137
  """IAM policy document."""
137
138
 
138
- model_config = ConfigDict(populate_by_name=True, extra="forbid")
139
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
139
140
 
140
- version: str = Field(alias="Version")
141
- statement: list[Statement] = Field(alias="Statement")
141
+ version: str | None = Field(default=None, alias="Version")
142
+ statement: list[Statement] | None = Field(default=None, alias="Statement")
142
143
  id: str | None = Field(default=None, alias="Id")
143
144
 
144
145
 
@@ -164,6 +165,9 @@ class ValidationIssue(BaseModel):
164
165
  suggestion: str | None = None
165
166
  example: str | None = None # Code example (JSON/YAML) - formatted separately for GitHub
166
167
  line_number: int | None = None # Line number in the policy file (if available)
168
+ check_id: str | None = (
169
+ None # Check that triggered this issue (e.g., "policy_size", "sensitive_action")
170
+ )
167
171
 
168
172
  # Severity level constants (ClassVar to avoid Pydantic treating them as fields)
169
173
  VALID_SEVERITIES: ClassVar[frozenset[str]] = frozenset(
@@ -282,6 +286,12 @@ class ValidationIssue(BaseModel):
282
286
  parts.append("")
283
287
  parts.append("</details>")
284
288
 
289
+ # Add check ID at the bottom if available
290
+ if self.check_id:
291
+ parts.append("")
292
+ parts.append("---")
293
+ parts.append(f"*Check: `{self.check_id}`*")
294
+
285
295
  return "\n".join(parts)
286
296
 
287
297