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,656 @@
|
|
|
1
|
+
"""IAM Policy Validation Module.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive validation of IAM policies including:
|
|
4
|
+
- Action validation against AWS Service Reference API
|
|
5
|
+
- Condition key validation
|
|
6
|
+
- Resource ARN format validation
|
|
7
|
+
- Security best practices checks
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
16
|
+
from iam_validator.core.check_registry import CheckRegistry
|
|
17
|
+
from iam_validator.core.models import (
|
|
18
|
+
IAMPolicy,
|
|
19
|
+
PolicyType,
|
|
20
|
+
PolicyValidationResult,
|
|
21
|
+
Statement,
|
|
22
|
+
ValidationIssue,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _should_fail_on_issue(
|
|
29
|
+
issue: ValidationIssue, fail_on_severities: list[str] | None = None
|
|
30
|
+
) -> bool:
|
|
31
|
+
"""Determine if an issue should cause validation to fail.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
issue: Validation issue to check
|
|
35
|
+
fail_on_severities: List of severity levels that should cause failure
|
|
36
|
+
Defaults to ["error"] if not specified
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if the issue should cause validation to fail
|
|
40
|
+
"""
|
|
41
|
+
if not fail_on_severities:
|
|
42
|
+
fail_on_severities = ["error"] # Default: only fail on errors
|
|
43
|
+
|
|
44
|
+
# Check if issue severity is in the fail list
|
|
45
|
+
return issue.severity in fail_on_severities
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PolicyValidator:
|
|
49
|
+
"""Validates IAM policies for correctness and security."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, fetcher: AWSServiceFetcher):
|
|
52
|
+
"""Initialize the validator.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
fetcher: AWS service fetcher instance
|
|
56
|
+
"""
|
|
57
|
+
self.fetcher = fetcher
|
|
58
|
+
self._file_cache: dict[str, list[str]] = {}
|
|
59
|
+
|
|
60
|
+
def _find_field_line(
|
|
61
|
+
self, policy_file: str, statement_line: int, search_term: str
|
|
62
|
+
) -> int | None:
|
|
63
|
+
"""Find the specific line number for a field within a statement.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
policy_file: Path to the policy file
|
|
67
|
+
statement_line: Line number where the statement starts (Sid/first field line)
|
|
68
|
+
search_term: The term to search for (e.g., action name, resource ARN)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Line number where the field is found, or None
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
# Cache file contents
|
|
75
|
+
if policy_file not in self._file_cache:
|
|
76
|
+
with open(policy_file, encoding="utf-8") as f:
|
|
77
|
+
self._file_cache[policy_file] = f.readlines()
|
|
78
|
+
|
|
79
|
+
lines = self._file_cache[policy_file]
|
|
80
|
+
|
|
81
|
+
# Need to go back to find the opening brace of the statement
|
|
82
|
+
# Look backwards from statement_line to find the opening {
|
|
83
|
+
statement_start = statement_line
|
|
84
|
+
for i in range(statement_line - 1, max(0, statement_line - 10), -1):
|
|
85
|
+
if "{" in lines[i]:
|
|
86
|
+
statement_start = i + 1 # Convert to 1-indexed
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
# Now search from the statement opening brace
|
|
90
|
+
brace_depth = 0
|
|
91
|
+
in_statement = False
|
|
92
|
+
|
|
93
|
+
for i, line in enumerate(lines[statement_start - 1 :], start=statement_start):
|
|
94
|
+
# Track braces to stay within statement bounds
|
|
95
|
+
for char in line:
|
|
96
|
+
if char == "{":
|
|
97
|
+
brace_depth += 1
|
|
98
|
+
in_statement = True
|
|
99
|
+
elif char == "}":
|
|
100
|
+
brace_depth -= 1
|
|
101
|
+
|
|
102
|
+
# Search for the term in this line
|
|
103
|
+
if in_statement and search_term in line:
|
|
104
|
+
return i
|
|
105
|
+
|
|
106
|
+
# Exit if we've left the statement
|
|
107
|
+
if in_statement and brace_depth == 0:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.debug(f"Could not find field line in {policy_file}: {e}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
async def validate_policy(
|
|
117
|
+
self, policy: IAMPolicy, policy_file: str, policy_type: PolicyType = "IDENTITY_POLICY"
|
|
118
|
+
) -> PolicyValidationResult:
|
|
119
|
+
"""Validate a complete IAM policy.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
policy: IAM policy to validate
|
|
123
|
+
policy_file: Path to the policy file
|
|
124
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
PolicyValidationResult with all findings
|
|
128
|
+
"""
|
|
129
|
+
result = PolicyValidationResult(
|
|
130
|
+
policy_file=policy_file, is_valid=True, policy_type=policy_type
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Apply automatic policy-type validation (not configurable - always runs)
|
|
134
|
+
from iam_validator.checks import policy_type_validation
|
|
135
|
+
|
|
136
|
+
policy_type_issues = await policy_type_validation.execute_policy(
|
|
137
|
+
policy, policy_file, policy_type=policy_type
|
|
138
|
+
)
|
|
139
|
+
result.issues.extend(policy_type_issues)
|
|
140
|
+
|
|
141
|
+
for idx, statement in enumerate(policy.statement):
|
|
142
|
+
# Get line number for this statement
|
|
143
|
+
statement_line = statement.line_number
|
|
144
|
+
|
|
145
|
+
# Validate actions
|
|
146
|
+
# Optimization: Batch actions by service and cache line lookups
|
|
147
|
+
actions = statement.get_actions()
|
|
148
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
149
|
+
|
|
150
|
+
# Group actions by service prefix for batch validation
|
|
151
|
+
from collections import defaultdict
|
|
152
|
+
|
|
153
|
+
actions_by_service = defaultdict(list)
|
|
154
|
+
for action in non_wildcard_actions:
|
|
155
|
+
if ":" in action:
|
|
156
|
+
service_prefix = action.split(":")[0]
|
|
157
|
+
actions_by_service[service_prefix].append(action)
|
|
158
|
+
else:
|
|
159
|
+
# Invalid action format, validate individually
|
|
160
|
+
actions_by_service["_invalid"].append(action)
|
|
161
|
+
|
|
162
|
+
# Pre-fetch all required services in parallel
|
|
163
|
+
if actions_by_service:
|
|
164
|
+
service_prefixes = [s for s in actions_by_service.keys() if s != "_invalid"]
|
|
165
|
+
# Batch fetch services to warm up cache
|
|
166
|
+
fetch_results = await asyncio.gather(
|
|
167
|
+
*[self.fetcher.fetch_service_by_name(s) for s in service_prefixes],
|
|
168
|
+
return_exceptions=True, # Don't fail if a service doesn't exist
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Log any service fetch failures for debugging
|
|
172
|
+
# Note: Individual action validation will still work and report proper errors
|
|
173
|
+
for i, fetch_result in enumerate(fetch_results):
|
|
174
|
+
if isinstance(fetch_result, Exception):
|
|
175
|
+
service_name = service_prefixes[i]
|
|
176
|
+
logger.debug(
|
|
177
|
+
f"Pre-fetch failed for service '{service_name}': {fetch_result}. "
|
|
178
|
+
"Will validate actions individually."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Cache action line lookups to avoid repeated file searches
|
|
182
|
+
action_line_cache = {}
|
|
183
|
+
|
|
184
|
+
for action in non_wildcard_actions:
|
|
185
|
+
# Look up line number once per action (cached)
|
|
186
|
+
if action not in action_line_cache:
|
|
187
|
+
action_line = None
|
|
188
|
+
if statement_line:
|
|
189
|
+
# Search for the full action string in quotes to avoid partial matches
|
|
190
|
+
# Try full action first (e.g., "s3:GetObject")
|
|
191
|
+
action_line = self._find_field_line(
|
|
192
|
+
policy_file, statement_line, f'"{action}"'
|
|
193
|
+
)
|
|
194
|
+
# If not found, try just the action part after colon
|
|
195
|
+
if not action_line and ":" in action:
|
|
196
|
+
action_name = action.split(":")[-1]
|
|
197
|
+
action_line = self._find_field_line(
|
|
198
|
+
policy_file, statement_line, f'"{action_name}"'
|
|
199
|
+
)
|
|
200
|
+
action_line_cache[action] = action_line or statement_line
|
|
201
|
+
|
|
202
|
+
await self._validate_action(
|
|
203
|
+
action,
|
|
204
|
+
idx,
|
|
205
|
+
statement.sid,
|
|
206
|
+
action_line_cache[action],
|
|
207
|
+
result,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Validate condition keys if present
|
|
211
|
+
# Optimization: Cache condition line lookups and batch validations
|
|
212
|
+
if statement.condition:
|
|
213
|
+
# Pre-filter non-wildcard actions once
|
|
214
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
215
|
+
|
|
216
|
+
# Cache condition key line numbers to avoid repeated file searches
|
|
217
|
+
condition_line_cache = {}
|
|
218
|
+
|
|
219
|
+
for operator, conditions in statement.condition.items():
|
|
220
|
+
for condition_key in conditions.keys():
|
|
221
|
+
# Look up line number once per condition key
|
|
222
|
+
if condition_key not in condition_line_cache:
|
|
223
|
+
condition_line = None
|
|
224
|
+
if statement_line:
|
|
225
|
+
condition_line = self._find_field_line(
|
|
226
|
+
policy_file, statement_line, condition_key
|
|
227
|
+
)
|
|
228
|
+
condition_line_cache[condition_key] = condition_line or statement_line
|
|
229
|
+
|
|
230
|
+
# Validate condition key against all non-wildcard actions
|
|
231
|
+
for action in non_wildcard_actions:
|
|
232
|
+
await self._validate_condition_key(
|
|
233
|
+
action,
|
|
234
|
+
condition_key,
|
|
235
|
+
idx,
|
|
236
|
+
statement.sid,
|
|
237
|
+
condition_line_cache[condition_key],
|
|
238
|
+
result,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Validate resources
|
|
242
|
+
resources = statement.get_resources()
|
|
243
|
+
for resource in resources:
|
|
244
|
+
if resource != "*": # Skip wildcard resources
|
|
245
|
+
# Try to find specific resource line
|
|
246
|
+
resource_line = None
|
|
247
|
+
if statement_line:
|
|
248
|
+
resource_line = self._find_field_line(policy_file, statement_line, resource)
|
|
249
|
+
self._validate_resource(
|
|
250
|
+
resource,
|
|
251
|
+
idx,
|
|
252
|
+
statement.sid,
|
|
253
|
+
resource_line or statement_line,
|
|
254
|
+
result,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Security best practice checks
|
|
258
|
+
self._check_security_best_practices(statement, idx, statement_line, result, policy_file)
|
|
259
|
+
|
|
260
|
+
# Update final validation status
|
|
261
|
+
# Default to failing only on "error" severity for legacy validator
|
|
262
|
+
result.is_valid = len([i for i in result.issues if _should_fail_on_issue(i)]) == 0
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
async def _validate_action(
|
|
267
|
+
self,
|
|
268
|
+
action: str,
|
|
269
|
+
statement_idx: int,
|
|
270
|
+
statement_sid: str | None,
|
|
271
|
+
line_number: int | None,
|
|
272
|
+
result: PolicyValidationResult,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Validate a single action."""
|
|
275
|
+
result.actions_checked += 1
|
|
276
|
+
|
|
277
|
+
# Handle wildcard patterns like "s3:Get*"
|
|
278
|
+
if "*" in action and action != "*":
|
|
279
|
+
# Validate the service prefix exists
|
|
280
|
+
try:
|
|
281
|
+
service_prefix = action.split(":")[0]
|
|
282
|
+
await self.fetcher.fetch_service_by_name(service_prefix)
|
|
283
|
+
# For now, accept wildcard actions if service exists
|
|
284
|
+
logger.debug(f"Wildcard action validated: {action}")
|
|
285
|
+
return
|
|
286
|
+
except Exception:
|
|
287
|
+
result.issues.append(
|
|
288
|
+
ValidationIssue(
|
|
289
|
+
severity="warning",
|
|
290
|
+
statement_sid=statement_sid,
|
|
291
|
+
statement_index=statement_idx,
|
|
292
|
+
issue_type="wildcard_action",
|
|
293
|
+
message=f"Wildcard action '{action}' uses unverified service",
|
|
294
|
+
action=action,
|
|
295
|
+
suggestion="Consider being more specific with action permissions",
|
|
296
|
+
line_number=line_number,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
is_valid, error_msg, is_wildcard = await self.fetcher.validate_action(action)
|
|
302
|
+
|
|
303
|
+
if not is_valid:
|
|
304
|
+
result.issues.append(
|
|
305
|
+
ValidationIssue(
|
|
306
|
+
severity="error",
|
|
307
|
+
statement_sid=statement_sid,
|
|
308
|
+
statement_index=statement_idx,
|
|
309
|
+
issue_type="invalid_action",
|
|
310
|
+
message=error_msg or f"Invalid action: {action}",
|
|
311
|
+
action=action,
|
|
312
|
+
line_number=line_number,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def _validate_condition_key(
|
|
317
|
+
self,
|
|
318
|
+
action: str,
|
|
319
|
+
condition_key: str,
|
|
320
|
+
statement_idx: int,
|
|
321
|
+
statement_sid: str | None,
|
|
322
|
+
line_number: int | None,
|
|
323
|
+
result: PolicyValidationResult,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Validate a condition key against an action."""
|
|
326
|
+
result.condition_keys_checked += 1
|
|
327
|
+
|
|
328
|
+
is_valid, error_msg = await self.fetcher.validate_condition_key(action, condition_key)
|
|
329
|
+
|
|
330
|
+
if not is_valid:
|
|
331
|
+
result.issues.append(
|
|
332
|
+
ValidationIssue(
|
|
333
|
+
severity="warning",
|
|
334
|
+
statement_sid=statement_sid,
|
|
335
|
+
statement_index=statement_idx,
|
|
336
|
+
issue_type="invalid_condition_key",
|
|
337
|
+
message=error_msg or f"Invalid condition key: {condition_key}",
|
|
338
|
+
action=action,
|
|
339
|
+
condition_key=condition_key,
|
|
340
|
+
line_number=line_number,
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _validate_resource(
|
|
345
|
+
self,
|
|
346
|
+
resource: str,
|
|
347
|
+
statement_idx: int,
|
|
348
|
+
statement_sid: str | None,
|
|
349
|
+
line_number: int | None,
|
|
350
|
+
result: PolicyValidationResult,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Validate resource ARN format."""
|
|
353
|
+
result.resources_checked += 1
|
|
354
|
+
|
|
355
|
+
# Basic ARN format: arn:partition:service:region:account-id:resource-type/resource-id
|
|
356
|
+
arn_pattern = r"^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\-]+:[a-z0-9\-]*:[0-9]*:.+$"
|
|
357
|
+
|
|
358
|
+
if not re.match(arn_pattern, resource, re.IGNORECASE):
|
|
359
|
+
result.issues.append(
|
|
360
|
+
ValidationIssue(
|
|
361
|
+
severity="error",
|
|
362
|
+
statement_sid=statement_sid,
|
|
363
|
+
statement_index=statement_idx,
|
|
364
|
+
issue_type="invalid_resource",
|
|
365
|
+
message=f"Invalid ARN format: {resource}",
|
|
366
|
+
resource=resource,
|
|
367
|
+
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
|
|
368
|
+
line_number=line_number,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _check_security_best_practices(
|
|
373
|
+
self,
|
|
374
|
+
statement: Statement,
|
|
375
|
+
statement_idx: int,
|
|
376
|
+
line_number: int | None,
|
|
377
|
+
result: PolicyValidationResult,
|
|
378
|
+
policy_file: str,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Check for security best practices."""
|
|
381
|
+
|
|
382
|
+
# Check for overly permissive wildcards
|
|
383
|
+
actions = statement.get_actions()
|
|
384
|
+
resources = statement.get_resources()
|
|
385
|
+
|
|
386
|
+
if statement.effect == "Allow":
|
|
387
|
+
# Check for "*" in actions
|
|
388
|
+
if "*" in actions:
|
|
389
|
+
# Try to find "Action" field line
|
|
390
|
+
action_field_line = None
|
|
391
|
+
if line_number:
|
|
392
|
+
action_field_line = self._find_field_line(policy_file, line_number, '"Action"')
|
|
393
|
+
result.issues.append(
|
|
394
|
+
ValidationIssue(
|
|
395
|
+
severity="warning",
|
|
396
|
+
statement_sid=statement.sid,
|
|
397
|
+
statement_index=statement_idx,
|
|
398
|
+
issue_type="overly_permissive",
|
|
399
|
+
message="Statement allows all actions (*)",
|
|
400
|
+
suggestion="Consider limiting to specific actions needed",
|
|
401
|
+
line_number=action_field_line or line_number,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Check for "*" in resources
|
|
406
|
+
if "*" in resources:
|
|
407
|
+
# Try to find "Resource" field line
|
|
408
|
+
resource_field_line = None
|
|
409
|
+
if line_number:
|
|
410
|
+
resource_field_line = self._find_field_line(
|
|
411
|
+
policy_file, line_number, '"Resource"'
|
|
412
|
+
)
|
|
413
|
+
result.issues.append(
|
|
414
|
+
ValidationIssue(
|
|
415
|
+
severity="warning",
|
|
416
|
+
statement_sid=statement.sid,
|
|
417
|
+
statement_index=statement_idx,
|
|
418
|
+
issue_type="overly_permissive",
|
|
419
|
+
message="Statement applies to all resources (*)",
|
|
420
|
+
suggestion="Consider limiting to specific resources",
|
|
421
|
+
line_number=resource_field_line or line_number,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Check for both wildcards
|
|
426
|
+
if "*" in actions and "*" in resources:
|
|
427
|
+
result.issues.append(
|
|
428
|
+
ValidationIssue(
|
|
429
|
+
severity="error",
|
|
430
|
+
statement_sid=statement.sid,
|
|
431
|
+
statement_index=statement_idx,
|
|
432
|
+
issue_type="security_risk",
|
|
433
|
+
message="Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
434
|
+
suggestion="This grants full administrative access. Restrict to specific actions and resources.",
|
|
435
|
+
line_number=line_number,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Check for missing conditions on sensitive actions
|
|
440
|
+
sensitive_actions = [
|
|
441
|
+
"iam:PassRole",
|
|
442
|
+
"iam:CreateUser",
|
|
443
|
+
"iam:CreateRole",
|
|
444
|
+
"iam:PutUserPolicy",
|
|
445
|
+
"iam:PutRolePolicy",
|
|
446
|
+
"s3:DeleteBucket",
|
|
447
|
+
"s3:PutBucketPolicy",
|
|
448
|
+
"ec2:TerminateInstances",
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
for action in actions:
|
|
452
|
+
if action in sensitive_actions and not statement.condition:
|
|
453
|
+
# Try to find specific action line
|
|
454
|
+
action_line = None
|
|
455
|
+
if line_number:
|
|
456
|
+
action_name = action.split(":")[-1] if ":" in action else action
|
|
457
|
+
action_line = self._find_field_line(policy_file, line_number, action_name)
|
|
458
|
+
result.issues.append(
|
|
459
|
+
ValidationIssue(
|
|
460
|
+
severity="warning",
|
|
461
|
+
statement_sid=statement.sid,
|
|
462
|
+
statement_index=statement_idx,
|
|
463
|
+
issue_type="missing_condition",
|
|
464
|
+
message=f"Sensitive action '{action}' has no conditions",
|
|
465
|
+
action=action,
|
|
466
|
+
suggestion="Consider adding conditions to restrict when this action can be performed",
|
|
467
|
+
line_number=action_line or line_number,
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
async def validate_policies(
|
|
473
|
+
policies: list[tuple[str, IAMPolicy]],
|
|
474
|
+
config_path: str | None = None,
|
|
475
|
+
use_registry: bool = True,
|
|
476
|
+
custom_checks_dir: str | None = None,
|
|
477
|
+
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
478
|
+
) -> list[PolicyValidationResult]:
|
|
479
|
+
"""Validate multiple policies concurrently.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
policies: List of (file_path, policy) tuples
|
|
483
|
+
config_path: Optional path to configuration file
|
|
484
|
+
use_registry: If True, use CheckRegistry system; if False, use legacy validator
|
|
485
|
+
custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
|
|
486
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
List of validation results
|
|
490
|
+
"""
|
|
491
|
+
if not use_registry:
|
|
492
|
+
# Legacy path - use old PolicyValidator
|
|
493
|
+
# Load config for cache settings even in legacy mode
|
|
494
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
495
|
+
|
|
496
|
+
config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
|
|
497
|
+
cache_enabled = config.get_setting("cache_enabled", True)
|
|
498
|
+
cache_ttl_hours = config.get_setting("cache_ttl_hours", 168)
|
|
499
|
+
cache_directory = config.get_setting("cache_directory", None)
|
|
500
|
+
aws_services_dir = config.get_setting("aws_services_dir", None)
|
|
501
|
+
cache_ttl_seconds = cache_ttl_hours * 3600
|
|
502
|
+
|
|
503
|
+
async with AWSServiceFetcher(
|
|
504
|
+
enable_cache=cache_enabled,
|
|
505
|
+
cache_ttl=cache_ttl_seconds,
|
|
506
|
+
cache_dir=cache_directory,
|
|
507
|
+
aws_services_dir=aws_services_dir,
|
|
508
|
+
) as fetcher:
|
|
509
|
+
validator = PolicyValidator(fetcher)
|
|
510
|
+
|
|
511
|
+
tasks = [
|
|
512
|
+
validator.validate_policy(policy, file_path, policy_type)
|
|
513
|
+
for file_path, policy in policies
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
results = await asyncio.gather(*tasks)
|
|
517
|
+
|
|
518
|
+
return list(results)
|
|
519
|
+
|
|
520
|
+
# New path - use CheckRegistry system
|
|
521
|
+
from iam_validator.core.check_registry import create_default_registry
|
|
522
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
523
|
+
|
|
524
|
+
# Load configuration
|
|
525
|
+
config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
|
|
526
|
+
|
|
527
|
+
# Create registry with or without built-in checks based on configuration
|
|
528
|
+
enable_parallel = config.get_setting("parallel_execution", True)
|
|
529
|
+
enable_builtin_checks = config.get_setting("enable_builtin_checks", True)
|
|
530
|
+
|
|
531
|
+
registry = create_default_registry(
|
|
532
|
+
enable_parallel=enable_parallel, include_builtin_checks=enable_builtin_checks
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if not enable_builtin_checks:
|
|
536
|
+
logger.info("Built-in checks disabled - using only custom checks")
|
|
537
|
+
|
|
538
|
+
# Apply configuration to built-in checks (if they were registered)
|
|
539
|
+
if enable_builtin_checks:
|
|
540
|
+
ConfigLoader.apply_config_to_registry(config, registry)
|
|
541
|
+
|
|
542
|
+
# Load custom checks from explicit module paths (old method)
|
|
543
|
+
custom_checks = ConfigLoader.load_custom_checks(config, registry)
|
|
544
|
+
if custom_checks:
|
|
545
|
+
logger.info(
|
|
546
|
+
f"Loaded {len(custom_checks)} custom checks from modules: {', '.join(custom_checks)}"
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# Auto-discover custom checks from directory (new method)
|
|
550
|
+
# Priority: CLI arg > config file > default None
|
|
551
|
+
checks_dir = custom_checks_dir or config.custom_checks_dir
|
|
552
|
+
if checks_dir:
|
|
553
|
+
checks_dir_path = Path(checks_dir).resolve()
|
|
554
|
+
discovered_checks = ConfigLoader.discover_checks_in_directory(checks_dir_path, registry)
|
|
555
|
+
if discovered_checks:
|
|
556
|
+
logger.info(
|
|
557
|
+
f"Auto-discovered {len(discovered_checks)} custom checks from {checks_dir_path}"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Apply configuration again to include custom checks
|
|
561
|
+
# This allows configuring auto-discovered checks via the config file
|
|
562
|
+
ConfigLoader.apply_config_to_registry(config, registry)
|
|
563
|
+
|
|
564
|
+
# Get fail_on_severity setting from config
|
|
565
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error"])
|
|
566
|
+
|
|
567
|
+
# Get cache settings from config
|
|
568
|
+
cache_enabled = config.get_setting("cache_enabled", True)
|
|
569
|
+
cache_ttl_hours = config.get_setting("cache_ttl_hours", 168) # 7 days default
|
|
570
|
+
cache_directory = config.get_setting("cache_directory", None)
|
|
571
|
+
aws_services_dir = config.get_setting("aws_services_dir", None)
|
|
572
|
+
cache_ttl_seconds = cache_ttl_hours * 3600
|
|
573
|
+
|
|
574
|
+
# Validate policies using registry
|
|
575
|
+
async with AWSServiceFetcher(
|
|
576
|
+
enable_cache=cache_enabled,
|
|
577
|
+
cache_ttl=cache_ttl_seconds,
|
|
578
|
+
cache_dir=cache_directory,
|
|
579
|
+
aws_services_dir=aws_services_dir,
|
|
580
|
+
) as fetcher:
|
|
581
|
+
tasks = [
|
|
582
|
+
_validate_policy_with_registry(
|
|
583
|
+
policy, file_path, registry, fetcher, fail_on_severities, policy_type
|
|
584
|
+
)
|
|
585
|
+
for file_path, policy in policies
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
results = await asyncio.gather(*tasks)
|
|
589
|
+
|
|
590
|
+
return list(results)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
async def _validate_policy_with_registry(
|
|
594
|
+
policy: IAMPolicy,
|
|
595
|
+
policy_file: str,
|
|
596
|
+
registry: CheckRegistry,
|
|
597
|
+
fetcher: AWSServiceFetcher,
|
|
598
|
+
fail_on_severities: list[str] | None = None,
|
|
599
|
+
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
600
|
+
) -> PolicyValidationResult:
|
|
601
|
+
"""Validate a single policy using the CheckRegistry system.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
policy: IAM policy to validate
|
|
605
|
+
policy_file: Path to the policy file
|
|
606
|
+
registry: CheckRegistry instance with configured checks
|
|
607
|
+
fetcher: AWS service fetcher instance
|
|
608
|
+
fail_on_severities: List of severity levels that should cause validation to fail
|
|
609
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
PolicyValidationResult with all findings
|
|
613
|
+
"""
|
|
614
|
+
result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
|
|
615
|
+
|
|
616
|
+
# Apply automatic policy-type validation (not configurable - always runs)
|
|
617
|
+
from iam_validator.checks import policy_type_validation
|
|
618
|
+
|
|
619
|
+
policy_type_issues = await policy_type_validation.execute_policy(
|
|
620
|
+
policy, policy_file, policy_type=policy_type
|
|
621
|
+
)
|
|
622
|
+
result.issues.extend(policy_type_issues)
|
|
623
|
+
|
|
624
|
+
# Run policy-level checks first (checks that need to see the entire policy)
|
|
625
|
+
# These checks examine relationships between statements, not individual statements
|
|
626
|
+
policy_level_issues = await registry.execute_policy_checks(
|
|
627
|
+
policy, policy_file, fetcher, policy_type
|
|
628
|
+
)
|
|
629
|
+
result.issues.extend(policy_level_issues)
|
|
630
|
+
|
|
631
|
+
# Execute all statement-level checks for each statement
|
|
632
|
+
for idx, statement in enumerate(policy.statement):
|
|
633
|
+
# Execute all registered checks in parallel (with ignore_patterns filtering)
|
|
634
|
+
issues = await registry.execute_checks_parallel(statement, idx, fetcher, policy_file)
|
|
635
|
+
|
|
636
|
+
# Add issues to result
|
|
637
|
+
result.issues.extend(issues)
|
|
638
|
+
|
|
639
|
+
# Update counters (approximate based on what was checked)
|
|
640
|
+
actions = statement.get_actions()
|
|
641
|
+
resources = statement.get_resources()
|
|
642
|
+
|
|
643
|
+
result.actions_checked += len([a for a in actions if a != "*"])
|
|
644
|
+
result.resources_checked += len([r for r in resources if r != "*"])
|
|
645
|
+
|
|
646
|
+
# Count condition keys if present
|
|
647
|
+
if statement.condition:
|
|
648
|
+
for conditions in statement.condition.values():
|
|
649
|
+
result.condition_keys_checked += len(conditions)
|
|
650
|
+
|
|
651
|
+
# Update final validation status based on fail_on_severities configuration
|
|
652
|
+
result.is_valid = (
|
|
653
|
+
len([i for i in result.issues if _should_fail_on_issue(i, fail_on_severities)]) == 0
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
return result
|