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.
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -6
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +61 -23
- iam_validator/checks/action_resource_matching.py +6 -2
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +1 -1
- iam_validator/checks/condition_type_mismatch.py +6 -6
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +65 -133
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +7 -3
- iam_validator/checks/service_wildcard.py +2 -2
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +3 -1
- iam_validator/checks/wildcard_resource.py +3 -1
- iam_validator/commands/validate.py +6 -12
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +45 -43
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/defaults.py +58 -52
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +31 -472
- iam_validator/core/policy_loader.py +27 -4
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {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)
|
iam_validator/core/models.py
CHANGED
|
@@ -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="
|
|
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="
|
|
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
|
|