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