iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
- iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
- iam_validator/__version__.py +4 -2
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +81 -36
- iam_validator/checks/action_resource_matching.py +75 -37
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +10 -8
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +86 -150
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +9 -11
- iam_validator/checks/service_wildcard.py +4 -10
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +5 -9
- iam_validator/checks/wildcard_resource.py +5 -9
- iam_validator/commands/validate.py +8 -14
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +159 -64
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +74 -59
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +35 -10
- iam_validator/core/policy_checks.py +34 -474
- iam_validator/core/policy_loader.py +98 -18
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/terminal.py +22 -0
- iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,18 +9,19 @@ This module provides comprehensive validation of IAM policies including:
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import logging
|
|
12
|
-
import re
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
|
|
14
|
+
from iam_validator.core import constants
|
|
15
15
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
16
|
-
from iam_validator.core.check_registry import CheckRegistry
|
|
16
|
+
from iam_validator.core.check_registry import CheckRegistry, create_default_registry
|
|
17
|
+
from iam_validator.core.config.config_loader import ConfigLoader
|
|
17
18
|
from iam_validator.core.models import (
|
|
18
19
|
IAMPolicy,
|
|
19
20
|
PolicyType,
|
|
20
21
|
PolicyValidationResult,
|
|
21
|
-
Statement,
|
|
22
22
|
ValidationIssue,
|
|
23
23
|
)
|
|
24
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
@@ -45,482 +46,23 @@ def _should_fail_on_issue(
|
|
|
45
46
|
return issue.severity in fail_on_severities
|
|
46
47
|
|
|
47
48
|
|
|
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
49
|
async def validate_policies(
|
|
473
|
-
policies: list[tuple[str, IAMPolicy]],
|
|
50
|
+
policies: list[tuple[str, IAMPolicy]] | list[tuple[str, IAMPolicy, dict]],
|
|
474
51
|
config_path: str | None = None,
|
|
475
|
-
use_registry: bool = True,
|
|
476
52
|
custom_checks_dir: str | None = None,
|
|
477
53
|
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
478
54
|
) -> list[PolicyValidationResult]:
|
|
479
55
|
"""Validate multiple policies concurrently.
|
|
480
56
|
|
|
481
57
|
Args:
|
|
482
|
-
policies: List of (file_path, policy) tuples
|
|
58
|
+
policies: List of (file_path, policy) or (file_path, policy, raw_dict) tuples
|
|
483
59
|
config_path: Optional path to configuration file
|
|
484
|
-
use_registry: If True, use CheckRegistry system; if False, use legacy validator
|
|
485
60
|
custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
|
|
486
61
|
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
487
62
|
|
|
488
63
|
Returns:
|
|
489
64
|
List of validation results
|
|
490
65
|
"""
|
|
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
66
|
# Load configuration
|
|
525
67
|
config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
|
|
526
68
|
|
|
@@ -566,10 +108,10 @@ async def validate_policies(
|
|
|
566
108
|
|
|
567
109
|
# Get cache settings from config
|
|
568
110
|
cache_enabled = config.get_setting("cache_enabled", True)
|
|
569
|
-
cache_ttl_hours = config.get_setting("cache_ttl_hours",
|
|
111
|
+
cache_ttl_hours = config.get_setting("cache_ttl_hours", constants.DEFAULT_CACHE_TTL_HOURS)
|
|
570
112
|
cache_directory = config.get_setting("cache_directory", None)
|
|
571
113
|
aws_services_dir = config.get_setting("aws_services_dir", None)
|
|
572
|
-
cache_ttl_seconds = cache_ttl_hours *
|
|
114
|
+
cache_ttl_seconds = cache_ttl_hours * constants.SECONDS_PER_HOUR
|
|
573
115
|
|
|
574
116
|
# Validate policies using registry
|
|
575
117
|
async with AWSServiceFetcher(
|
|
@@ -580,9 +122,15 @@ async def validate_policies(
|
|
|
580
122
|
) as fetcher:
|
|
581
123
|
tasks = [
|
|
582
124
|
_validate_policy_with_registry(
|
|
583
|
-
|
|
125
|
+
item[1], # policy
|
|
126
|
+
item[0], # file_path
|
|
127
|
+
registry,
|
|
128
|
+
fetcher,
|
|
129
|
+
fail_on_severities,
|
|
130
|
+
policy_type,
|
|
131
|
+
item[2] if len(item) == 3 else None, # raw_dict (optional)
|
|
584
132
|
)
|
|
585
|
-
for
|
|
133
|
+
for item in policies
|
|
586
134
|
]
|
|
587
135
|
|
|
588
136
|
results = await asyncio.gather(*tasks)
|
|
@@ -597,6 +145,7 @@ async def _validate_policy_with_registry(
|
|
|
597
145
|
fetcher: AWSServiceFetcher,
|
|
598
146
|
fail_on_severities: list[str] | None = None,
|
|
599
147
|
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
148
|
+
raw_policy_dict: dict | None = None,
|
|
600
149
|
) -> PolicyValidationResult:
|
|
601
150
|
"""Validate a single policy using the CheckRegistry system.
|
|
602
151
|
|
|
@@ -607,34 +156,45 @@ async def _validate_policy_with_registry(
|
|
|
607
156
|
fetcher: AWS service fetcher instance
|
|
608
157
|
fail_on_severities: List of severity levels that should cause validation to fail
|
|
609
158
|
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
159
|
+
raw_policy_dict: Raw policy dictionary for structural validation (optional, will be loaded if not provided)
|
|
610
160
|
|
|
611
161
|
Returns:
|
|
612
162
|
PolicyValidationResult with all findings
|
|
613
163
|
"""
|
|
614
164
|
result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
|
|
615
165
|
|
|
166
|
+
# Load raw dict if not provided (for structural validation)
|
|
167
|
+
if raw_policy_dict is None:
|
|
168
|
+
loader = PolicyLoader()
|
|
169
|
+
loaded_result = loader.load_from_file(policy_file, return_raw_dict=True)
|
|
170
|
+
if loaded_result and isinstance(loaded_result, tuple):
|
|
171
|
+
raw_policy_dict = loaded_result[1]
|
|
172
|
+
|
|
616
173
|
# Apply automatic policy-type validation (not configurable - always runs)
|
|
617
|
-
|
|
174
|
+
# Note: Import here to avoid circular import (policy_checks -> checks -> sdk -> policy_checks)
|
|
175
|
+
from iam_validator.checks import ( # pylint: disable=import-outside-toplevel
|
|
176
|
+
policy_type_validation,
|
|
177
|
+
)
|
|
618
178
|
|
|
619
179
|
policy_type_issues = await policy_type_validation.execute_policy(
|
|
620
180
|
policy, policy_file, policy_type=policy_type
|
|
621
181
|
)
|
|
622
|
-
result.issues.extend(policy_type_issues)
|
|
182
|
+
result.issues.extend(policy_type_issues) # pylint: disable=no-member
|
|
623
183
|
|
|
624
184
|
# Run policy-level checks first (checks that need to see the entire policy)
|
|
625
185
|
# These checks examine relationships between statements, not individual statements
|
|
626
186
|
policy_level_issues = await registry.execute_policy_checks(
|
|
627
|
-
policy, policy_file, fetcher, policy_type
|
|
187
|
+
policy, policy_file, fetcher, policy_type, raw_policy_dict=raw_policy_dict
|
|
628
188
|
)
|
|
629
|
-
result.issues.extend(policy_level_issues)
|
|
189
|
+
result.issues.extend(policy_level_issues) # pylint: disable=no-member
|
|
630
190
|
|
|
631
191
|
# Execute all statement-level checks for each statement
|
|
632
|
-
for idx, statement in enumerate(policy.statement):
|
|
192
|
+
for idx, statement in enumerate(policy.statement or []):
|
|
633
193
|
# Execute all registered checks in parallel (with ignore_patterns filtering)
|
|
634
194
|
issues = await registry.execute_checks_parallel(statement, idx, fetcher, policy_file)
|
|
635
195
|
|
|
636
196
|
# Add issues to result
|
|
637
|
-
result.issues.extend(issues)
|
|
197
|
+
result.issues.extend(issues) # pylint: disable=no-member
|
|
638
198
|
|
|
639
199
|
# Update counters (approximate based on what was checked)
|
|
640
200
|
actions = statement.get_actions()
|