iam-policy-validator 1.7.2__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 (38) hide show
  1. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -6
  2. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +61 -23
  6. iam_validator/checks/action_resource_matching.py +6 -2
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +1 -1
  9. iam_validator/checks/condition_type_mismatch.py +6 -6
  10. iam_validator/checks/policy_structure.py +577 -0
  11. iam_validator/checks/policy_type_validation.py +48 -32
  12. iam_validator/checks/principal_validation.py +65 -133
  13. iam_validator/checks/resource_validation.py +8 -8
  14. iam_validator/checks/sensitive_action.py +7 -3
  15. iam_validator/checks/service_wildcard.py +2 -2
  16. iam_validator/checks/set_operator_validation.py +11 -11
  17. iam_validator/checks/sid_uniqueness.py +8 -4
  18. iam_validator/checks/trust_policy_validation.py +512 -0
  19. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  20. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  21. iam_validator/checks/wildcard_action.py +3 -1
  22. iam_validator/checks/wildcard_resource.py +3 -1
  23. iam_validator/commands/validate.py +6 -12
  24. iam_validator/core/__init__.py +1 -2
  25. iam_validator/core/access_analyzer.py +1 -1
  26. iam_validator/core/access_analyzer_report.py +2 -2
  27. iam_validator/core/aws_fetcher.py +45 -43
  28. iam_validator/core/check_registry.py +83 -79
  29. iam_validator/core/config/condition_requirements.py +69 -17
  30. iam_validator/core/config/defaults.py +58 -52
  31. iam_validator/core/config/service_principals.py +40 -3
  32. iam_validator/core/ignore_patterns.py +297 -0
  33. iam_validator/core/models.py +15 -5
  34. iam_validator/core/policy_checks.py +31 -472
  35. iam_validator/core/policy_loader.py +27 -4
  36. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  37. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  38. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -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