iam-policy-validator 1.14.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.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check Registry for IAM Policy Validator.
|
|
3
|
+
|
|
4
|
+
This module provides a pluggable check system that allows:
|
|
5
|
+
1. Registering built-in and custom checks
|
|
6
|
+
2. Enabling/disabling checks via configuration
|
|
7
|
+
3. Configuring check behavior
|
|
8
|
+
4. Easy extension without modifying core code
|
|
9
|
+
5. Parallel execution of checks for performance
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from abc import ABC
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
18
|
+
from iam_validator.core.ignore_patterns import IgnorePatternMatcher
|
|
19
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from iam_validator.core.models import IAMPolicy
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CheckConfig:
|
|
27
|
+
"""Configuration for a single check."""
|
|
28
|
+
|
|
29
|
+
check_id: str
|
|
30
|
+
enabled: bool = True
|
|
31
|
+
severity: str | None = None # Override default severity
|
|
32
|
+
config: dict[str, Any] = field(default_factory=dict) # Check-specific config
|
|
33
|
+
description: str = ""
|
|
34
|
+
root_config: dict[str, Any] = field(default_factory=dict) # Full config for cross-check access
|
|
35
|
+
ignore_patterns: list[dict[str, Any]] = field(default_factory=list) # NEW: Ignore patterns
|
|
36
|
+
"""
|
|
37
|
+
List of patterns to ignore findings.
|
|
38
|
+
|
|
39
|
+
Each pattern is a dict with optional fields:
|
|
40
|
+
- filepath: Regex to match file path
|
|
41
|
+
- action: Regex to match action name
|
|
42
|
+
- resource: Regex to match resource
|
|
43
|
+
- sid: Exact SID to match (or regex if ends with .*)
|
|
44
|
+
- condition_key: Regex to match condition key
|
|
45
|
+
|
|
46
|
+
Multiple fields in one pattern = AND logic
|
|
47
|
+
Multiple patterns = OR logic (any pattern matches → ignore)
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
ignore_patterns:
|
|
51
|
+
- filepath: "test/.*|examples/.*"
|
|
52
|
+
- filepath: "policies/readonly-.*"
|
|
53
|
+
action: ".*:(Get|List|Describe).*"
|
|
54
|
+
- sid: "AllowReadOnlyAccess"
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def should_ignore(self, issue: ValidationIssue, filepath: str = "") -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Check if issue should be ignored based on ignore patterns.
|
|
60
|
+
|
|
61
|
+
Uses centralized IgnorePatternMatcher for high-performance filtering
|
|
62
|
+
with cached compiled regex patterns.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
issue: The validation issue to check
|
|
66
|
+
filepath: Path to the policy file
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the issue should be ignored
|
|
70
|
+
|
|
71
|
+
Performance:
|
|
72
|
+
- Cached regex compilation (LRU cache)
|
|
73
|
+
- Early exit optimization
|
|
74
|
+
"""
|
|
75
|
+
return IgnorePatternMatcher.should_ignore_issue(issue, filepath, self.ignore_patterns)
|
|
76
|
+
|
|
77
|
+
def filter_actions(self, actions: frozenset[str]) -> frozenset[str]:
|
|
78
|
+
"""
|
|
79
|
+
Filter actions based on action ignore patterns.
|
|
80
|
+
|
|
81
|
+
Uses centralized IgnorePatternMatcher for high-performance filtering
|
|
82
|
+
with cached compiled regex patterns.
|
|
83
|
+
|
|
84
|
+
This is useful for checks that need to filter a set of actions before
|
|
85
|
+
creating ValidationIssues (e.g., sensitive_action check).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
actions: Set of actions to filter
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Filtered set of actions (actions matching ignore patterns removed)
|
|
92
|
+
|
|
93
|
+
Performance:
|
|
94
|
+
- Cached regex compilation (LRU cache)
|
|
95
|
+
- Early exit per action on first match
|
|
96
|
+
"""
|
|
97
|
+
return IgnorePatternMatcher.filter_actions(actions, self.ignore_patterns)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PolicyCheck(ABC):
|
|
101
|
+
"""
|
|
102
|
+
Base class for all policy checks.
|
|
103
|
+
|
|
104
|
+
To create a custom check:
|
|
105
|
+
1. Inherit from this class
|
|
106
|
+
2. Implement check_id and description (required)
|
|
107
|
+
3. Implement either execute() OR execute_policy() (or both)
|
|
108
|
+
4. Register with CheckRegistry
|
|
109
|
+
|
|
110
|
+
Two ways to define check_id and description:
|
|
111
|
+
|
|
112
|
+
Option 1 - Class attributes (simpler, recommended for static values):
|
|
113
|
+
from typing import ClassVar
|
|
114
|
+
|
|
115
|
+
class MyCheck(PolicyCheck):
|
|
116
|
+
check_id: ClassVar[str] = "my_check"
|
|
117
|
+
description: ClassVar[str] = "My check description"
|
|
118
|
+
|
|
119
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
Note: ClassVar annotation is required for Pylance type checker compatibility.
|
|
123
|
+
|
|
124
|
+
Option 2 - Property decorators (more flexible, supports dynamic values):
|
|
125
|
+
class MyCheck(PolicyCheck):
|
|
126
|
+
@property
|
|
127
|
+
def check_id(self) -> str:
|
|
128
|
+
return "my_check"
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def description(self) -> str:
|
|
132
|
+
return "My check description"
|
|
133
|
+
|
|
134
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
Statement-level check example:
|
|
138
|
+
from typing import ClassVar
|
|
139
|
+
|
|
140
|
+
class MyStatementCheck(PolicyCheck):
|
|
141
|
+
check_id: ClassVar[str] = "my_statement_check"
|
|
142
|
+
description: ClassVar[str] = "Validates individual statements"
|
|
143
|
+
|
|
144
|
+
async def execute(self, statement, statement_idx, fetcher, config):
|
|
145
|
+
issues = []
|
|
146
|
+
# Your validation logic here
|
|
147
|
+
return issues
|
|
148
|
+
|
|
149
|
+
Policy-level check example:
|
|
150
|
+
from typing import ClassVar
|
|
151
|
+
|
|
152
|
+
class MyPolicyCheck(PolicyCheck):
|
|
153
|
+
check_id: ClassVar[str] = "my_policy_check"
|
|
154
|
+
description: ClassVar[str] = "Validates entire policy"
|
|
155
|
+
|
|
156
|
+
async def execute_policy(self, policy, policy_file, fetcher, config, **kwargs):
|
|
157
|
+
issues = []
|
|
158
|
+
# Your validation logic here
|
|
159
|
+
return issues
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def check_id(self) -> str:
|
|
164
|
+
"""Unique identifier for this check (e.g., 'action_validation')."""
|
|
165
|
+
raise NotImplementedError("Subclasses must define check_id")
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def description(self) -> str:
|
|
169
|
+
"""Human-readable description of what this check does."""
|
|
170
|
+
raise NotImplementedError("Subclasses must define description")
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def default_severity(self) -> str:
|
|
174
|
+
"""Default severity level for issues found by this check."""
|
|
175
|
+
return "warning"
|
|
176
|
+
|
|
177
|
+
def __init_subclass__(cls, **kwargs):
|
|
178
|
+
"""
|
|
179
|
+
Validate that subclasses override at least one execution method.
|
|
180
|
+
|
|
181
|
+
This ensures checks implement either execute() OR execute_policy() (or both).
|
|
182
|
+
If neither is overridden, the check would never produce any results.
|
|
183
|
+
"""
|
|
184
|
+
super().__init_subclass__(**kwargs)
|
|
185
|
+
|
|
186
|
+
# Skip validation for abstract classes
|
|
187
|
+
if ABC in cls.__bases__:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Check if at least one method is overridden
|
|
191
|
+
has_execute = cls.execute is not PolicyCheck.execute
|
|
192
|
+
has_execute_policy = cls.execute_policy is not PolicyCheck.execute_policy
|
|
193
|
+
|
|
194
|
+
if not has_execute and not has_execute_policy:
|
|
195
|
+
raise TypeError(
|
|
196
|
+
f"Check '{cls.__name__}' must override at least one of: "
|
|
197
|
+
"execute() for statement-level checks, or "
|
|
198
|
+
"execute_policy() for policy-level checks"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def execute(
|
|
202
|
+
self,
|
|
203
|
+
statement: Statement,
|
|
204
|
+
statement_idx: int,
|
|
205
|
+
fetcher: AWSServiceFetcher,
|
|
206
|
+
config: CheckConfig,
|
|
207
|
+
) -> list[ValidationIssue]:
|
|
208
|
+
"""
|
|
209
|
+
Execute the check on a policy statement.
|
|
210
|
+
|
|
211
|
+
This method is called for statement-level checks. If your check only needs
|
|
212
|
+
to examine the entire policy (not individual statements), you can leave this
|
|
213
|
+
as the default implementation and override execute_policy() instead.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
statement: The IAM policy statement to check
|
|
217
|
+
statement_idx: Index of the statement in the policy
|
|
218
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
219
|
+
config: Configuration for this check instance
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of ValidationIssue objects found by this check
|
|
223
|
+
"""
|
|
224
|
+
del statement, statement_idx, fetcher, config # Unused in default implementation
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
async def execute_policy(
|
|
228
|
+
self,
|
|
229
|
+
policy: "IAMPolicy",
|
|
230
|
+
policy_file: str,
|
|
231
|
+
fetcher: AWSServiceFetcher,
|
|
232
|
+
config: CheckConfig,
|
|
233
|
+
**kwargs,
|
|
234
|
+
) -> list[ValidationIssue]:
|
|
235
|
+
"""
|
|
236
|
+
Execute the check on the entire policy (optional method).
|
|
237
|
+
|
|
238
|
+
This method is for checks that need to examine all statements together,
|
|
239
|
+
such as checking for duplicate SIDs or cross-statement relationships.
|
|
240
|
+
|
|
241
|
+
By default, this returns an empty list. Override this method if your
|
|
242
|
+
check needs access to the full policy.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
policy: The complete IAM policy to check
|
|
246
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
247
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
248
|
+
config: Configuration for this check instance
|
|
249
|
+
**kwargs: Additional context (policy_type, etc.)
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of ValidationIssue objects found by this check
|
|
253
|
+
"""
|
|
254
|
+
del policy, policy_file, fetcher, config # Unused in default implementation
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
def get_severity(self, config: CheckConfig) -> str:
|
|
258
|
+
"""Get the severity level, respecting config overrides."""
|
|
259
|
+
return config.severity or self.default_severity
|
|
260
|
+
|
|
261
|
+
def is_policy_level_check(self) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Check if this is a policy-level check.
|
|
264
|
+
|
|
265
|
+
Returns True if the check overrides execute_policy() method.
|
|
266
|
+
This helps the registry know whether to call execute_policy() or execute().
|
|
267
|
+
"""
|
|
268
|
+
# Check if execute_policy has been overridden from the base class
|
|
269
|
+
return type(self).execute_policy is not PolicyCheck.execute_policy
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class CheckRegistry:
|
|
273
|
+
"""
|
|
274
|
+
Registry for managing validation checks.
|
|
275
|
+
|
|
276
|
+
Supports parallel execution of checks for improved performance.
|
|
277
|
+
|
|
278
|
+
Usage:
|
|
279
|
+
registry = CheckRegistry()
|
|
280
|
+
registry.register(ActionValidationCheck())
|
|
281
|
+
registry.register(MyCustomCheck())
|
|
282
|
+
|
|
283
|
+
# Get all enabled checks
|
|
284
|
+
checks = registry.get_enabled_checks()
|
|
285
|
+
|
|
286
|
+
# Configure checks
|
|
287
|
+
registry.configure_check('action_validation', CheckConfig(
|
|
288
|
+
check_id='action_validation',
|
|
289
|
+
enabled=True,
|
|
290
|
+
severity='error'
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
# Execute checks in parallel
|
|
294
|
+
issues = await registry.execute_checks_parallel(statement, idx, fetcher)
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(self, enable_parallel: bool = True):
|
|
298
|
+
"""
|
|
299
|
+
Initialize the registry.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
enable_parallel: If True, execute checks in parallel (default: True)
|
|
303
|
+
"""
|
|
304
|
+
self._checks: dict[str, PolicyCheck] = {}
|
|
305
|
+
self._configs: dict[str, CheckConfig] = {}
|
|
306
|
+
self.enable_parallel = enable_parallel
|
|
307
|
+
|
|
308
|
+
def register(self, check: PolicyCheck) -> None:
|
|
309
|
+
"""
|
|
310
|
+
Register a new check.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
check: PolicyCheck instance to register
|
|
314
|
+
"""
|
|
315
|
+
self._checks[check.check_id] = check
|
|
316
|
+
|
|
317
|
+
# Create default config if not exists
|
|
318
|
+
if check.check_id not in self._configs:
|
|
319
|
+
self._configs[check.check_id] = CheckConfig(
|
|
320
|
+
check_id=check.check_id,
|
|
321
|
+
enabled=True,
|
|
322
|
+
description=check.description,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def unregister(self, check_id: str) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Unregister a check by ID.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
check_id: ID of the check to unregister
|
|
331
|
+
"""
|
|
332
|
+
if check_id in self._checks:
|
|
333
|
+
del self._checks[check_id]
|
|
334
|
+
if check_id in self._configs:
|
|
335
|
+
del self._configs[check_id]
|
|
336
|
+
|
|
337
|
+
def configure_check(self, check_id: str, config: CheckConfig) -> None:
|
|
338
|
+
"""
|
|
339
|
+
Configure a registered check.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
check_id: ID of the check to configure
|
|
343
|
+
config: Configuration to apply
|
|
344
|
+
"""
|
|
345
|
+
if check_id not in self._checks:
|
|
346
|
+
raise ValueError(f"Check '{check_id}' is not registered")
|
|
347
|
+
self._configs[check_id] = config
|
|
348
|
+
|
|
349
|
+
def get_all_checks(self) -> list[PolicyCheck]:
|
|
350
|
+
"""Get all registered checks (enabled and disabled)."""
|
|
351
|
+
return list(self._checks.values())
|
|
352
|
+
|
|
353
|
+
def get_enabled_checks(self) -> list[PolicyCheck]:
|
|
354
|
+
"""Get only enabled checks."""
|
|
355
|
+
return [
|
|
356
|
+
check
|
|
357
|
+
for check_id, check in self._checks.items()
|
|
358
|
+
if self._configs.get(check_id, CheckConfig(check_id=check_id)).enabled
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
def get_check(self, check_id: str) -> PolicyCheck | None:
|
|
362
|
+
"""Get a specific check by ID."""
|
|
363
|
+
return self._checks.get(check_id)
|
|
364
|
+
|
|
365
|
+
def get_config(self, check_id: str) -> CheckConfig | None:
|
|
366
|
+
"""Get configuration for a specific check."""
|
|
367
|
+
return self._configs.get(check_id)
|
|
368
|
+
|
|
369
|
+
def is_enabled(self, check_id: str) -> bool:
|
|
370
|
+
"""Check if a specific check is enabled."""
|
|
371
|
+
config = self._configs.get(check_id)
|
|
372
|
+
return config.enabled if config else False
|
|
373
|
+
|
|
374
|
+
def list_checks(self) -> list[dict[str, Any]]:
|
|
375
|
+
"""
|
|
376
|
+
List all checks with their status and description.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of dicts with check information
|
|
380
|
+
"""
|
|
381
|
+
result = []
|
|
382
|
+
for check_id, check in self._checks.items():
|
|
383
|
+
config = self._configs.get(check_id, CheckConfig(check_id=check_id))
|
|
384
|
+
result.append(
|
|
385
|
+
{
|
|
386
|
+
"check_id": check_id,
|
|
387
|
+
"description": check.description,
|
|
388
|
+
"enabled": config.enabled,
|
|
389
|
+
"severity": config.severity or check.default_severity,
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
async def execute_checks_parallel(
|
|
395
|
+
self,
|
|
396
|
+
statement: Statement,
|
|
397
|
+
statement_idx: int,
|
|
398
|
+
fetcher: AWSServiceFetcher,
|
|
399
|
+
filepath: str = "",
|
|
400
|
+
) -> list[ValidationIssue]:
|
|
401
|
+
"""
|
|
402
|
+
Execute all enabled checks in parallel for maximum performance.
|
|
403
|
+
|
|
404
|
+
This method runs all enabled checks concurrently using asyncio.gather(),
|
|
405
|
+
which can significantly speed up validation when multiple checks are enabled.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
statement: The IAM policy statement to validate
|
|
409
|
+
statement_idx: Index of the statement in the policy
|
|
410
|
+
fetcher: AWS service fetcher for API calls
|
|
411
|
+
filepath: Path to the policy file (for ignore_patterns filtering)
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
List of all ValidationIssue objects from all checks (filtered by ignore_patterns)
|
|
415
|
+
"""
|
|
416
|
+
enabled_checks = self.get_enabled_checks()
|
|
417
|
+
|
|
418
|
+
if not enabled_checks:
|
|
419
|
+
return []
|
|
420
|
+
|
|
421
|
+
if not self.enable_parallel or len(enabled_checks) == 1:
|
|
422
|
+
# Run sequentially if parallel disabled or only one check
|
|
423
|
+
all_issues = []
|
|
424
|
+
for check in enabled_checks:
|
|
425
|
+
config = self.get_config(check.check_id)
|
|
426
|
+
if config:
|
|
427
|
+
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
428
|
+
# Inject check_id into each issue
|
|
429
|
+
for issue in issues:
|
|
430
|
+
if issue.check_id is None:
|
|
431
|
+
issue.check_id = check.check_id
|
|
432
|
+
# Filter issues based on ignore_patterns
|
|
433
|
+
filtered_issues = [
|
|
434
|
+
issue for issue in issues if not config.should_ignore(issue, filepath)
|
|
435
|
+
]
|
|
436
|
+
all_issues.extend(filtered_issues)
|
|
437
|
+
return all_issues
|
|
438
|
+
|
|
439
|
+
# Execute all checks in parallel
|
|
440
|
+
tasks = []
|
|
441
|
+
configs = []
|
|
442
|
+
for check in enabled_checks:
|
|
443
|
+
config = self.get_config(check.check_id)
|
|
444
|
+
if config:
|
|
445
|
+
task = check.execute(statement, statement_idx, fetcher, config)
|
|
446
|
+
tasks.append(task)
|
|
447
|
+
configs.append(config)
|
|
448
|
+
|
|
449
|
+
# Wait for all checks to complete
|
|
450
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
451
|
+
|
|
452
|
+
# Collect all issues, handling any exceptions and applying ignore_patterns
|
|
453
|
+
all_issues = []
|
|
454
|
+
for idx, result in enumerate(results):
|
|
455
|
+
if isinstance(result, Exception):
|
|
456
|
+
# Log error but continue with other checks
|
|
457
|
+
check = enabled_checks[idx]
|
|
458
|
+
print(f"Warning: Check '{check.check_id}' failed: {result}")
|
|
459
|
+
elif isinstance(result, list):
|
|
460
|
+
check = enabled_checks[idx]
|
|
461
|
+
config = configs[idx]
|
|
462
|
+
# Inject check_id into each issue
|
|
463
|
+
for issue in result:
|
|
464
|
+
if issue.check_id is None:
|
|
465
|
+
issue.check_id = check.check_id
|
|
466
|
+
# Filter issues based on ignore_patterns
|
|
467
|
+
filtered_issues = [
|
|
468
|
+
issue for issue in result if not config.should_ignore(issue, filepath)
|
|
469
|
+
]
|
|
470
|
+
all_issues.extend(filtered_issues)
|
|
471
|
+
|
|
472
|
+
return all_issues
|
|
473
|
+
|
|
474
|
+
async def execute_checks_sequential(
|
|
475
|
+
self,
|
|
476
|
+
statement: Statement,
|
|
477
|
+
statement_idx: int,
|
|
478
|
+
fetcher: AWSServiceFetcher,
|
|
479
|
+
) -> list[ValidationIssue]:
|
|
480
|
+
"""
|
|
481
|
+
Execute all enabled checks sequentially.
|
|
482
|
+
|
|
483
|
+
Useful for debugging or when parallel execution causes issues.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
statement: The IAM policy statement to validate
|
|
487
|
+
statement_idx: Index of the statement in the policy
|
|
488
|
+
fetcher: AWS service fetcher for API calls
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
List of all ValidationIssue objects from all checks
|
|
492
|
+
"""
|
|
493
|
+
all_issues = []
|
|
494
|
+
enabled_checks = self.get_enabled_checks()
|
|
495
|
+
|
|
496
|
+
for check in enabled_checks:
|
|
497
|
+
config = self.get_config(check.check_id)
|
|
498
|
+
if config:
|
|
499
|
+
try:
|
|
500
|
+
issues = await check.execute(statement, statement_idx, fetcher, config)
|
|
501
|
+
all_issues.extend(issues)
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
504
|
+
|
|
505
|
+
return all_issues
|
|
506
|
+
|
|
507
|
+
async def execute_policy_checks(
|
|
508
|
+
self,
|
|
509
|
+
policy: "IAMPolicy",
|
|
510
|
+
policy_file: str,
|
|
511
|
+
fetcher: AWSServiceFetcher,
|
|
512
|
+
policy_type: str = "IDENTITY_POLICY",
|
|
513
|
+
**kwargs,
|
|
514
|
+
) -> list[ValidationIssue]:
|
|
515
|
+
"""
|
|
516
|
+
Execute all enabled policy-level checks.
|
|
517
|
+
|
|
518
|
+
Policy-level checks examine the entire policy at once, which is useful for
|
|
519
|
+
checks that need to see relationships between statements (e.g., duplicate SIDs).
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
policy: The complete IAM policy to validate
|
|
523
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
524
|
+
fetcher: AWS service fetcher for API calls
|
|
525
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
526
|
+
**kwargs: Additional arguments to pass to checks (e.g., raw_policy_dict)
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of all ValidationIssue objects from all policy-level checks
|
|
530
|
+
"""
|
|
531
|
+
all_issues = []
|
|
532
|
+
enabled_checks = self.get_enabled_checks()
|
|
533
|
+
|
|
534
|
+
# Filter to only policy-level checks
|
|
535
|
+
policy_level_checks = [c for c in enabled_checks if c.is_policy_level_check()]
|
|
536
|
+
|
|
537
|
+
if not policy_level_checks:
|
|
538
|
+
return []
|
|
539
|
+
|
|
540
|
+
if not self.enable_parallel or len(policy_level_checks) == 1:
|
|
541
|
+
# Run sequentially if parallel disabled or only one check
|
|
542
|
+
for check in policy_level_checks:
|
|
543
|
+
config = self.get_config(check.check_id)
|
|
544
|
+
if config:
|
|
545
|
+
try:
|
|
546
|
+
issues = await check.execute_policy(
|
|
547
|
+
policy,
|
|
548
|
+
policy_file,
|
|
549
|
+
fetcher,
|
|
550
|
+
config,
|
|
551
|
+
policy_type=policy_type,
|
|
552
|
+
**kwargs,
|
|
553
|
+
)
|
|
554
|
+
# Inject check_id into each issue
|
|
555
|
+
for issue in issues:
|
|
556
|
+
if issue.check_id is None:
|
|
557
|
+
issue.check_id = check.check_id
|
|
558
|
+
# Filter issues based on ignore_patterns
|
|
559
|
+
filtered_issues = [
|
|
560
|
+
issue
|
|
561
|
+
for issue in issues
|
|
562
|
+
if not config.should_ignore(issue, policy_file)
|
|
563
|
+
]
|
|
564
|
+
all_issues.extend(filtered_issues)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
567
|
+
return all_issues
|
|
568
|
+
|
|
569
|
+
# Execute all policy-level checks in parallel
|
|
570
|
+
tasks = []
|
|
571
|
+
configs = []
|
|
572
|
+
for check in policy_level_checks:
|
|
573
|
+
config = self.get_config(check.check_id)
|
|
574
|
+
if config:
|
|
575
|
+
task = check.execute_policy(
|
|
576
|
+
policy, policy_file, fetcher, config, policy_type=policy_type, **kwargs
|
|
577
|
+
)
|
|
578
|
+
tasks.append(task)
|
|
579
|
+
configs.append(config)
|
|
580
|
+
|
|
581
|
+
# Wait for all checks to complete
|
|
582
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
583
|
+
|
|
584
|
+
# Collect all issues, handling any exceptions and applying ignore_patterns
|
|
585
|
+
for idx, result in enumerate(results):
|
|
586
|
+
if isinstance(result, Exception):
|
|
587
|
+
# Log error but continue with other checks
|
|
588
|
+
check = policy_level_checks[idx]
|
|
589
|
+
print(f"Warning: Check '{check.check_id}' failed: {result}")
|
|
590
|
+
elif isinstance(result, list):
|
|
591
|
+
check = policy_level_checks[idx]
|
|
592
|
+
config = configs[idx]
|
|
593
|
+
# Inject check_id into each issue
|
|
594
|
+
for issue in result:
|
|
595
|
+
if issue.check_id is None:
|
|
596
|
+
issue.check_id = check.check_id
|
|
597
|
+
# Filter issues based on ignore_patterns
|
|
598
|
+
filtered_issues = [
|
|
599
|
+
issue for issue in result if not config.should_ignore(issue, policy_file)
|
|
600
|
+
]
|
|
601
|
+
all_issues.extend(filtered_issues)
|
|
602
|
+
|
|
603
|
+
return all_issues
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def create_default_registry(
|
|
607
|
+
enable_parallel: bool = True, include_builtin_checks: bool = True
|
|
608
|
+
) -> CheckRegistry:
|
|
609
|
+
"""
|
|
610
|
+
Create a registry with all built-in checks registered.
|
|
611
|
+
|
|
612
|
+
This is a factory function that will be called when no custom
|
|
613
|
+
registry is provided.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
enable_parallel: If True, checks will execute in parallel (default: True)
|
|
617
|
+
include_builtin_checks: If True, register built-in checks (default: True)
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
CheckRegistry with all built-in checks registered (if include_builtin_checks=True)
|
|
621
|
+
"""
|
|
622
|
+
registry = CheckRegistry(enable_parallel=enable_parallel)
|
|
623
|
+
|
|
624
|
+
if include_builtin_checks:
|
|
625
|
+
# Import and register built-in checks
|
|
626
|
+
from iam_validator import checks
|
|
627
|
+
|
|
628
|
+
# 0. FUNDAMENTAL STRUCTURE (Must run FIRST - validates basic policy structure)
|
|
629
|
+
registry.register(
|
|
630
|
+
checks.PolicyStructureCheck()
|
|
631
|
+
) # Policy-level: Validates required fields, conflicts, valid values
|
|
632
|
+
|
|
633
|
+
# 1. POLICY STRUCTURE (Checks that examine the entire policy, not individual statements)
|
|
634
|
+
registry.register(
|
|
635
|
+
checks.SidUniquenessCheck()
|
|
636
|
+
) # Policy-level: Duplicate SID detection across statements
|
|
637
|
+
registry.register(checks.PolicySizeCheck()) # Policy-level: Size limit validation
|
|
638
|
+
|
|
639
|
+
# 2. IAM VALIDITY (AWS syntax validation - must pass before deeper checks)
|
|
640
|
+
registry.register(checks.ActionValidationCheck()) # Validate actions against AWS API
|
|
641
|
+
registry.register(checks.ResourceValidationCheck()) # Validate resource ARNs
|
|
642
|
+
registry.register(checks.ConditionKeyValidationCheck()) # Validate condition keys
|
|
643
|
+
|
|
644
|
+
# 3. TYPE VALIDATION (Condition operator type checking)
|
|
645
|
+
registry.register(checks.ConditionTypeMismatchCheck()) # Operator-value type compatibility
|
|
646
|
+
registry.register(checks.SetOperatorValidationCheck()) # ForAllValues/ForAnyValue usage
|
|
647
|
+
|
|
648
|
+
# 4. RESOURCE MATCHING (Action-resource relationship validation)
|
|
649
|
+
registry.register(
|
|
650
|
+
checks.ActionResourceMatchingCheck()
|
|
651
|
+
) # ARN type matching and resource constraints
|
|
652
|
+
|
|
653
|
+
# 5. SECURITY - WILDCARDS (Security best practices for wildcards)
|
|
654
|
+
registry.register(checks.WildcardActionCheck()) # Wildcard action detection
|
|
655
|
+
registry.register(checks.WildcardResourceCheck()) # Wildcard resource detection
|
|
656
|
+
registry.register(checks.FullWildcardCheck()) # Full wildcard (*) detection
|
|
657
|
+
registry.register(checks.ServiceWildcardCheck()) # Service-level wildcard detection
|
|
658
|
+
|
|
659
|
+
# 6. SECURITY - ADVANCED (Sensitive actions and condition enforcement)
|
|
660
|
+
registry.register(
|
|
661
|
+
checks.SensitiveActionCheck()
|
|
662
|
+
) # Policy-level: Privilege escalation detection (all_of across statements)
|
|
663
|
+
registry.register(
|
|
664
|
+
checks.ActionConditionEnforcementCheck()
|
|
665
|
+
) # Statement + Policy-level: Condition enforcement (any_of/all_of/none_of)
|
|
666
|
+
registry.register(checks.MFAConditionCheck()) # MFA anti-pattern detection
|
|
667
|
+
|
|
668
|
+
# 7. PRINCIPAL VALIDATION (Resource policy specific)
|
|
669
|
+
registry.register(
|
|
670
|
+
checks.PrincipalValidationCheck()
|
|
671
|
+
) # Principal validation (resource policies)
|
|
672
|
+
registry.register(
|
|
673
|
+
checks.TrustPolicyValidationCheck()
|
|
674
|
+
) # Trust policy validation (role assumption policies)
|
|
675
|
+
|
|
676
|
+
# Note: policy_type_validation is a standalone function (not a class-based check)
|
|
677
|
+
# and is called separately in the validation flow
|
|
678
|
+
|
|
679
|
+
return registry
|