iam-policy-validator 1.5.0__py3-none-any.whl → 1.6.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.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +9 -3
- iam_validator/checks/action_condition_enforcement.py +164 -2
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +3 -1
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/sensitive_action.py +78 -6
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/validate.py +44 -11
- iam_validator/core/aws_fetcher.py +89 -52
- iam_validator/core/check_registry.py +165 -21
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +13 -15
- 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 +5 -385
- iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
- iam_validator/core/config/defaults.py +187 -54
- iam_validator/core/config/sensitive_actions.py +620 -81
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -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
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/core/aws_global_conditions.py +0 -137
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for working with IAM policies.
|
|
3
|
+
|
|
4
|
+
This module provides functions for parsing, manipulating, and inspecting
|
|
5
|
+
IAM policy documents programmatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from iam_validator.core.models import IAMPolicy, Statement
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_policy(policy: str | dict) -> IAMPolicy:
|
|
15
|
+
"""
|
|
16
|
+
Parse a policy from JSON string or dict.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
policy: IAM policy as JSON string or Python dict
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Parsed IAMPolicy object
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If policy is invalid JSON or missing required fields
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> policy_str = '{"Version": "2012-10-17", "Statement": [...]}'
|
|
29
|
+
>>> policy = parse_policy(policy_str)
|
|
30
|
+
>>> print(f"Version: {policy.version}")
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(policy, str):
|
|
33
|
+
try:
|
|
34
|
+
policy_dict = json.loads(policy)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
37
|
+
else:
|
|
38
|
+
policy_dict = policy
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
return IAMPolicy(**policy_dict)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise ValueError(f"Invalid IAM policy format: {e}") from e
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_policy(policy: IAMPolicy) -> IAMPolicy:
|
|
47
|
+
"""
|
|
48
|
+
Normalize policy format (ensure statements are in list format).
|
|
49
|
+
|
|
50
|
+
AWS allows Statement to be a single object or an array. This function
|
|
51
|
+
ensures it's always an array for consistent processing.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
policy: IAMPolicy to normalize
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Normalized IAMPolicy with Statement as list
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> policy = parse_policy(policy_json)
|
|
61
|
+
>>> normalized = normalize_policy(policy)
|
|
62
|
+
>>> assert isinstance(normalized.statement, list)
|
|
63
|
+
"""
|
|
64
|
+
# Pydantic model already handles this via Field(alias="Statement")
|
|
65
|
+
# which expects a list, but we can ensure it's always a list
|
|
66
|
+
statements: list[Statement] = (
|
|
67
|
+
policy.statement if isinstance(policy.statement, list) else [policy.statement]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Normalize actions and resources in each statement
|
|
71
|
+
normalized_statements: list[Statement] = []
|
|
72
|
+
for stmt in statements:
|
|
73
|
+
action = [stmt.action] if isinstance(stmt.action, str) else stmt.action
|
|
74
|
+
resource = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
|
|
75
|
+
not_action = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
|
|
76
|
+
not_resource = (
|
|
77
|
+
[stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Create a new statement with normalized fields
|
|
81
|
+
# Use capitalized field names (aliases) for Pydantic model construction
|
|
82
|
+
normalized_stmt = Statement(
|
|
83
|
+
Sid=stmt.sid,
|
|
84
|
+
Effect=stmt.effect,
|
|
85
|
+
Action=action,
|
|
86
|
+
NotAction=not_action,
|
|
87
|
+
Resource=resource,
|
|
88
|
+
NotResource=not_resource,
|
|
89
|
+
Condition=stmt.condition,
|
|
90
|
+
Principal=stmt.principal,
|
|
91
|
+
NotPrincipal=stmt.not_principal,
|
|
92
|
+
)
|
|
93
|
+
normalized_statements.append(normalized_stmt)
|
|
94
|
+
|
|
95
|
+
# Return a new policy with normalized statements
|
|
96
|
+
# Use capitalized field names (aliases) for Pydantic model construction
|
|
97
|
+
return IAMPolicy(
|
|
98
|
+
Version=policy.version,
|
|
99
|
+
Statement=normalized_statements,
|
|
100
|
+
Id=policy.id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def extract_actions(policy: IAMPolicy) -> list[str]:
|
|
105
|
+
"""
|
|
106
|
+
Extract all actions from a policy.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
policy: IAMPolicy to extract actions from
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of all unique actions in the policy
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
>>> policy = parse_policy(policy_json)
|
|
116
|
+
>>> actions = extract_actions(policy)
|
|
117
|
+
>>> print(f"Policy uses {len(actions)} actions")
|
|
118
|
+
"""
|
|
119
|
+
actions = set()
|
|
120
|
+
|
|
121
|
+
for stmt in policy.statement:
|
|
122
|
+
# Handle Action field
|
|
123
|
+
if stmt.action:
|
|
124
|
+
stmt_actions = [stmt.action] if isinstance(stmt.action, str) else stmt.action
|
|
125
|
+
actions.update(stmt_actions)
|
|
126
|
+
|
|
127
|
+
# Handle NotAction field
|
|
128
|
+
if stmt.not_action:
|
|
129
|
+
not_actions = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
|
|
130
|
+
actions.update(not_actions)
|
|
131
|
+
|
|
132
|
+
return sorted(actions)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_resources(policy: IAMPolicy) -> list[str]:
|
|
136
|
+
"""
|
|
137
|
+
Extract all resources from a policy.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
policy: IAMPolicy to extract resources from
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of all unique resources in the policy
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> policy = parse_policy(policy_json)
|
|
147
|
+
>>> resources = extract_resources(policy)
|
|
148
|
+
>>> for arn in resources:
|
|
149
|
+
... print(f"Resource: {arn}")
|
|
150
|
+
"""
|
|
151
|
+
resources = set()
|
|
152
|
+
|
|
153
|
+
for stmt in policy.statement:
|
|
154
|
+
# Handle Resource field
|
|
155
|
+
if stmt.resource:
|
|
156
|
+
stmt_resources = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
|
|
157
|
+
resources.update(stmt_resources)
|
|
158
|
+
|
|
159
|
+
# Handle NotResource field
|
|
160
|
+
if stmt.not_resource:
|
|
161
|
+
not_resources = (
|
|
162
|
+
[stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
|
|
163
|
+
)
|
|
164
|
+
resources.update(not_resources)
|
|
165
|
+
|
|
166
|
+
return sorted(resources)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def extract_condition_keys(policy: IAMPolicy) -> list[str]:
|
|
170
|
+
"""
|
|
171
|
+
Extract all condition keys used in a policy.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
policy: IAMPolicy to extract condition keys from
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of all unique condition keys in the policy
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> policy = parse_policy(policy_json)
|
|
181
|
+
>>> keys = extract_condition_keys(policy)
|
|
182
|
+
>>> print(f"Policy uses condition keys: {', '.join(keys)}")
|
|
183
|
+
"""
|
|
184
|
+
condition_keys = set()
|
|
185
|
+
|
|
186
|
+
for stmt in policy.statement:
|
|
187
|
+
if stmt.condition:
|
|
188
|
+
# Condition format: {"StringEquals": {"aws:username": "johndoe"}}
|
|
189
|
+
for operator, key_values in stmt.condition.items():
|
|
190
|
+
if isinstance(key_values, dict):
|
|
191
|
+
condition_keys.update(key_values.keys())
|
|
192
|
+
|
|
193
|
+
return sorted(condition_keys)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statement]:
|
|
197
|
+
"""
|
|
198
|
+
Find all statements containing a specific action.
|
|
199
|
+
|
|
200
|
+
Supports exact match and wildcard patterns.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
policy: IAMPolicy to search
|
|
204
|
+
action: Action to search for (e.g., "s3:GetObject" or "s3:*")
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of Statement objects containing the action
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> policy = parse_policy(policy_json)
|
|
211
|
+
>>> stmts = find_statements_with_action(policy, "s3:GetObject")
|
|
212
|
+
>>> for stmt in stmts:
|
|
213
|
+
... print(f"Statement {stmt.sid} allows s3:GetObject")
|
|
214
|
+
"""
|
|
215
|
+
import fnmatch
|
|
216
|
+
|
|
217
|
+
matching_statements = []
|
|
218
|
+
|
|
219
|
+
for stmt in policy.statement:
|
|
220
|
+
stmt_actions = stmt.get_actions()
|
|
221
|
+
|
|
222
|
+
# Check if action matches any statement action (with wildcard support)
|
|
223
|
+
for stmt_action in stmt_actions:
|
|
224
|
+
if fnmatch.fnmatch(action, stmt_action) or fnmatch.fnmatch(stmt_action, action):
|
|
225
|
+
matching_statements.append(stmt)
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
return matching_statements
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Statement]:
|
|
232
|
+
"""
|
|
233
|
+
Find all statements containing a specific resource.
|
|
234
|
+
|
|
235
|
+
Supports exact match and wildcard patterns.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
policy: IAMPolicy to search
|
|
239
|
+
resource: Resource ARN to search for
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
List of Statement objects containing the resource
|
|
243
|
+
|
|
244
|
+
Example:
|
|
245
|
+
>>> policy = parse_policy(policy_json)
|
|
246
|
+
>>> stmts = find_statements_with_resource(policy, "arn:aws:s3:::my-bucket/*")
|
|
247
|
+
>>> print(f"Found {len(stmts)} statements with this resource")
|
|
248
|
+
"""
|
|
249
|
+
import fnmatch
|
|
250
|
+
|
|
251
|
+
matching_statements = []
|
|
252
|
+
|
|
253
|
+
for stmt in policy.statement:
|
|
254
|
+
stmt_resources = stmt.get_resources()
|
|
255
|
+
|
|
256
|
+
# Check if resource matches any statement resource (with wildcard support)
|
|
257
|
+
for stmt_resource in stmt_resources:
|
|
258
|
+
if fnmatch.fnmatch(resource, stmt_resource) or fnmatch.fnmatch(stmt_resource, resource):
|
|
259
|
+
matching_statements.append(stmt)
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
return matching_statements
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
|
|
266
|
+
"""
|
|
267
|
+
Merge multiple policies into one.
|
|
268
|
+
|
|
269
|
+
Combines all statements from multiple policies into a single policy document.
|
|
270
|
+
Uses the version from the first policy.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
*policies: IAMPolicy objects to merge
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
New IAMPolicy with all statements combined
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
>>> policy1 = parse_policy(json1)
|
|
280
|
+
>>> policy2 = parse_policy(json2)
|
|
281
|
+
>>> merged = merge_policies(policy1, policy2)
|
|
282
|
+
>>> print(f"Merged policy has {len(merged.statement)} statements")
|
|
283
|
+
"""
|
|
284
|
+
if not policies:
|
|
285
|
+
raise ValueError("At least one policy must be provided")
|
|
286
|
+
|
|
287
|
+
all_statements: list[Statement] = []
|
|
288
|
+
for policy in policies:
|
|
289
|
+
all_statements.extend(policy.statement)
|
|
290
|
+
|
|
291
|
+
# Use capitalized field names (aliases) for Pydantic model construction
|
|
292
|
+
return IAMPolicy(
|
|
293
|
+
Version=policies[0].version,
|
|
294
|
+
Statement=all_statements,
|
|
295
|
+
Id=None, # Clear ID when merging
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
|
|
300
|
+
"""
|
|
301
|
+
Get a summary of policy contents.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
policy: IAMPolicy to summarize
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dictionary with summary statistics
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
>>> policy = parse_policy(policy_json)
|
|
311
|
+
>>> summary = get_policy_summary(policy)
|
|
312
|
+
>>> print(f"Statements: {summary['statement_count']}")
|
|
313
|
+
>>> print(f"Actions: {summary['action_count']}")
|
|
314
|
+
>>> print(f"Resources: {summary['resource_count']}")
|
|
315
|
+
"""
|
|
316
|
+
actions = extract_actions(policy)
|
|
317
|
+
resources = extract_resources(policy)
|
|
318
|
+
condition_keys = extract_condition_keys(policy)
|
|
319
|
+
|
|
320
|
+
# Count allow vs deny statements
|
|
321
|
+
allow_count = sum(1 for s in policy.statement if s.effect.lower() == "allow")
|
|
322
|
+
deny_count = sum(1 for s in policy.statement if s.effect.lower() == "deny")
|
|
323
|
+
|
|
324
|
+
# Check for wildcards
|
|
325
|
+
has_wildcard_actions = any("*" in action for action in actions)
|
|
326
|
+
has_wildcard_resources = any("*" in resource for resource in resources)
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
"version": policy.version,
|
|
330
|
+
"statement_count": len(policy.statement),
|
|
331
|
+
"allow_statements": allow_count,
|
|
332
|
+
"deny_statements": deny_count,
|
|
333
|
+
"action_count": len(actions),
|
|
334
|
+
"resource_count": len(resources),
|
|
335
|
+
"condition_key_count": len(condition_keys),
|
|
336
|
+
"has_wildcard_actions": has_wildcard_actions,
|
|
337
|
+
"has_wildcard_resources": has_wildcard_resources,
|
|
338
|
+
"actions": actions,
|
|
339
|
+
"resources": resources,
|
|
340
|
+
"condition_keys": condition_keys,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def policy_to_json(policy: IAMPolicy, indent: int = 2) -> str:
|
|
345
|
+
"""
|
|
346
|
+
Convert IAMPolicy to formatted JSON string.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
policy: IAMPolicy to convert
|
|
350
|
+
indent: Number of spaces for indentation (default: 2)
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Formatted JSON string
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
>>> policy = parse_policy(policy_dict)
|
|
357
|
+
>>> json_str = policy_to_json(policy)
|
|
358
|
+
>>> print(json_str)
|
|
359
|
+
"""
|
|
360
|
+
policy_dict = policy.model_dump(by_alias=True, exclude_none=True)
|
|
361
|
+
return json.dumps(policy_dict, indent=indent)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def policy_to_dict(policy: IAMPolicy) -> dict[str, Any]:
|
|
365
|
+
"""
|
|
366
|
+
Convert IAMPolicy to Python dictionary.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
policy: IAMPolicy to convert
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Policy as Python dict with AWS field names (Version, Statement, etc.)
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
>>> policy = parse_policy(policy_json)
|
|
376
|
+
>>> policy_dict = policy_to_dict(policy)
|
|
377
|
+
>>> print(policy_dict["Version"])
|
|
378
|
+
"""
|
|
379
|
+
return policy.model_dump(by_alias=True, exclude_none=True)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def is_resource_policy(policy: IAMPolicy) -> bool:
|
|
383
|
+
"""
|
|
384
|
+
Check if policy appears to be a resource policy (vs identity policy).
|
|
385
|
+
|
|
386
|
+
Resource policies have a Principal field, identity policies don't.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
policy: IAMPolicy to check
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
True if policy appears to be a resource policy
|
|
393
|
+
|
|
394
|
+
Example:
|
|
395
|
+
>>> policy = parse_policy(bucket_policy_json)
|
|
396
|
+
>>> if is_resource_policy(policy):
|
|
397
|
+
... print("This is an S3 bucket policy or similar")
|
|
398
|
+
"""
|
|
399
|
+
return any(stmt.principal is not None for stmt in policy.statement)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def has_public_access(policy: IAMPolicy) -> bool:
|
|
403
|
+
"""
|
|
404
|
+
Check if policy grants public access (Principal: "*").
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
policy: IAMPolicy to check
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
True if any statement has Principal set to "*"
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
>>> policy = parse_policy(policy_json)
|
|
414
|
+
>>> if has_public_access(policy):
|
|
415
|
+
... print("WARNING: This policy allows public access!")
|
|
416
|
+
"""
|
|
417
|
+
for stmt in policy.statement:
|
|
418
|
+
if stmt.principal == "*":
|
|
419
|
+
return True
|
|
420
|
+
if isinstance(stmt.principal, dict):
|
|
421
|
+
# Check for {"AWS": "*"} or {"Service": "*"}
|
|
422
|
+
for value in stmt.principal.values():
|
|
423
|
+
if value == "*" or (isinstance(value, list) and "*" in value):
|
|
424
|
+
return True
|
|
425
|
+
return False
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convenience functions for common validation scenarios.
|
|
3
|
+
|
|
4
|
+
This module provides high-level, easy-to-use functions for common IAM policy
|
|
5
|
+
validation tasks without requiring deep knowledge of the internal API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from iam_validator.core.config.config_loader import ValidatorConfig
|
|
11
|
+
from iam_validator.core.models import PolicyValidationResult, ValidationIssue
|
|
12
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
13
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def validate_file(
|
|
17
|
+
file_path: str | Path,
|
|
18
|
+
config_path: str | None = None,
|
|
19
|
+
config: ValidatorConfig | None = None,
|
|
20
|
+
) -> PolicyValidationResult:
|
|
21
|
+
"""
|
|
22
|
+
Validate a single IAM policy file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
file_path: Path to the policy file (JSON or YAML)
|
|
26
|
+
config_path: Optional path to configuration file
|
|
27
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
PolicyValidationResult for the policy
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> result = await validate_file("policy.json")
|
|
34
|
+
>>> if result.is_valid:
|
|
35
|
+
... print("Policy is valid!")
|
|
36
|
+
>>> else:
|
|
37
|
+
... for issue in result.issues:
|
|
38
|
+
... print(f"{issue.severity}: {issue.message}")
|
|
39
|
+
"""
|
|
40
|
+
loader = PolicyLoader()
|
|
41
|
+
policies = loader.load_from_path(str(file_path))
|
|
42
|
+
|
|
43
|
+
if not policies:
|
|
44
|
+
raise ValueError(f"No IAM policies found in {file_path}")
|
|
45
|
+
|
|
46
|
+
results = await validate_policies(
|
|
47
|
+
policies,
|
|
48
|
+
config_path=config_path,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
results[0]
|
|
53
|
+
if results
|
|
54
|
+
else PolicyValidationResult(
|
|
55
|
+
policy_file=str(file_path),
|
|
56
|
+
is_valid=False,
|
|
57
|
+
issues=[],
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def validate_directory(
|
|
63
|
+
dir_path: str | Path,
|
|
64
|
+
config_path: str | None = None,
|
|
65
|
+
config: ValidatorConfig | None = None,
|
|
66
|
+
recursive: bool = True,
|
|
67
|
+
) -> list[PolicyValidationResult]:
|
|
68
|
+
"""
|
|
69
|
+
Validate all IAM policies in a directory.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
dir_path: Path to directory containing policy files
|
|
73
|
+
config_path: Optional path to configuration file
|
|
74
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
75
|
+
recursive: Whether to search subdirectories (default: True)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of PolicyValidationResults for all policies found
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> results = await validate_directory("./policies")
|
|
82
|
+
>>> valid_count = sum(1 for r in results if r.is_valid)
|
|
83
|
+
>>> print(f"{valid_count}/{len(results)} policies are valid")
|
|
84
|
+
"""
|
|
85
|
+
loader = PolicyLoader()
|
|
86
|
+
policies = loader.load_from_path(str(dir_path))
|
|
87
|
+
|
|
88
|
+
if not policies:
|
|
89
|
+
raise ValueError(f"No IAM policies found in {dir_path}")
|
|
90
|
+
|
|
91
|
+
return await validate_policies(
|
|
92
|
+
policies,
|
|
93
|
+
config_path=config_path,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def validate_json(
|
|
98
|
+
policy_json: dict,
|
|
99
|
+
policy_name: str = "inline-policy",
|
|
100
|
+
config_path: str | None = None,
|
|
101
|
+
config: ValidatorConfig | None = None,
|
|
102
|
+
) -> PolicyValidationResult:
|
|
103
|
+
"""
|
|
104
|
+
Validate an IAM policy from a Python dictionary.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
policy_json: IAM policy as a Python dict
|
|
108
|
+
policy_name: Name to identify this policy in results
|
|
109
|
+
config_path: Optional path to configuration file
|
|
110
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
PolicyValidationResult for the policy
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> policy = {
|
|
117
|
+
... "Version": "2012-10-17",
|
|
118
|
+
... "Statement": [{
|
|
119
|
+
... "Effect": "Allow",
|
|
120
|
+
... "Action": "s3:GetObject",
|
|
121
|
+
... "Resource": "arn:aws:s3:::my-bucket/*"
|
|
122
|
+
... }]
|
|
123
|
+
... }
|
|
124
|
+
>>> result = await validate_json(policy)
|
|
125
|
+
>>> print(f"Valid: {result.is_valid}")
|
|
126
|
+
"""
|
|
127
|
+
from iam_validator.core.models import IAMPolicy
|
|
128
|
+
|
|
129
|
+
# Parse the dict into an IAMPolicy
|
|
130
|
+
policy = IAMPolicy(**policy_json)
|
|
131
|
+
|
|
132
|
+
results = await validate_policies(
|
|
133
|
+
[(policy_name, policy)],
|
|
134
|
+
config_path=config_path,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
results[0]
|
|
139
|
+
if results
|
|
140
|
+
else PolicyValidationResult(
|
|
141
|
+
policy_file=policy_name,
|
|
142
|
+
is_valid=False,
|
|
143
|
+
issues=[],
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def quick_validate(
|
|
149
|
+
policy: str | Path | dict,
|
|
150
|
+
config_path: str | None = None,
|
|
151
|
+
config: ValidatorConfig | None = None,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Quick validation returning just True/False.
|
|
155
|
+
|
|
156
|
+
Automatically detects whether input is a file path, directory, or dict.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
policy: File path, directory path, or policy dict
|
|
160
|
+
config_path: Optional path to configuration file
|
|
161
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if all policies are valid, False otherwise
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> if await quick_validate("policy.json"):
|
|
168
|
+
... print("Policy is valid!")
|
|
169
|
+
>>> else:
|
|
170
|
+
... print("Policy has issues")
|
|
171
|
+
"""
|
|
172
|
+
# If dict, validate as JSON
|
|
173
|
+
if isinstance(policy, dict):
|
|
174
|
+
result = await validate_json(policy, config_path=config_path)
|
|
175
|
+
return result.is_valid
|
|
176
|
+
|
|
177
|
+
# Convert to Path for easier handling
|
|
178
|
+
policy_path = Path(policy)
|
|
179
|
+
|
|
180
|
+
if not policy_path.exists():
|
|
181
|
+
raise FileNotFoundError(f"Path does not exist: {policy}")
|
|
182
|
+
|
|
183
|
+
# If directory, validate all files in it
|
|
184
|
+
if policy_path.is_dir():
|
|
185
|
+
results = await validate_directory(policy_path, config_path=config_path)
|
|
186
|
+
return all(r.is_valid for r in results)
|
|
187
|
+
|
|
188
|
+
# Otherwise, validate single file
|
|
189
|
+
result = await validate_file(policy_path, config_path=config_path)
|
|
190
|
+
return result.is_valid
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def get_issues(
|
|
194
|
+
policy: str | Path | dict,
|
|
195
|
+
min_severity: str = "medium",
|
|
196
|
+
config_path: str | None = None,
|
|
197
|
+
config: ValidatorConfig | None = None,
|
|
198
|
+
) -> list[ValidationIssue]:
|
|
199
|
+
"""
|
|
200
|
+
Get just the issues from validation, filtered by severity.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
policy: File path, directory path, or policy dict
|
|
204
|
+
min_severity: Minimum severity to include (critical, high, medium, low, info)
|
|
205
|
+
config_path: Optional path to configuration file
|
|
206
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of ValidationIssues meeting the severity threshold
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> issues = await get_issues("policy.json", min_severity="high")
|
|
213
|
+
>>> for issue in issues:
|
|
214
|
+
... print(f"{issue.severity}: {issue.message}")
|
|
215
|
+
"""
|
|
216
|
+
# Severity ranking for filtering
|
|
217
|
+
severity_rank = {
|
|
218
|
+
"critical": 5,
|
|
219
|
+
"high": 4,
|
|
220
|
+
"medium": 3,
|
|
221
|
+
"low": 2,
|
|
222
|
+
"info": 1,
|
|
223
|
+
"warning": 3, # Treat warning as medium
|
|
224
|
+
"error": 4, # Treat error as high
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
min_rank = severity_rank.get(min_severity.lower(), 0)
|
|
228
|
+
|
|
229
|
+
# Get validation results
|
|
230
|
+
if isinstance(policy, dict):
|
|
231
|
+
result = await validate_json(policy, config_path=config_path)
|
|
232
|
+
results = [result]
|
|
233
|
+
else:
|
|
234
|
+
policy_path = Path(policy)
|
|
235
|
+
if policy_path.is_dir():
|
|
236
|
+
results = await validate_directory(policy_path, config_path=config_path)
|
|
237
|
+
else:
|
|
238
|
+
result = await validate_file(policy_path, config_path=config_path)
|
|
239
|
+
results = [result]
|
|
240
|
+
|
|
241
|
+
# Collect and filter issues
|
|
242
|
+
all_issues = []
|
|
243
|
+
for result in results:
|
|
244
|
+
for issue in result.issues:
|
|
245
|
+
issue_rank = severity_rank.get(issue.severity.lower(), 0)
|
|
246
|
+
if issue_rank >= min_rank:
|
|
247
|
+
all_issues.append(issue)
|
|
248
|
+
|
|
249
|
+
return all_issues
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def count_issues_by_severity(
|
|
253
|
+
policy: str | Path | dict,
|
|
254
|
+
config_path: str | None = None,
|
|
255
|
+
config: ValidatorConfig | None = None,
|
|
256
|
+
) -> dict[str, int]:
|
|
257
|
+
"""
|
|
258
|
+
Count issues grouped by severity level.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
policy: File path, directory path, or policy dict
|
|
262
|
+
config_path: Optional path to configuration file
|
|
263
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary mapping severity levels to counts
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> counts = await count_issues_by_severity("./policies")
|
|
270
|
+
>>> print(f"Critical: {counts.get('critical', 0)}")
|
|
271
|
+
>>> print(f"High: {counts.get('high', 0)}")
|
|
272
|
+
>>> print(f"Medium: {counts.get('medium', 0)}")
|
|
273
|
+
"""
|
|
274
|
+
# Get all issues (no filtering)
|
|
275
|
+
all_issues = await get_issues(policy, min_severity="info", config_path=config_path)
|
|
276
|
+
|
|
277
|
+
# Count by severity
|
|
278
|
+
counts: dict[str, int] = {}
|
|
279
|
+
for issue in all_issues:
|
|
280
|
+
severity = issue.severity.lower()
|
|
281
|
+
counts[severity] = counts.get(severity, 0) + 1
|
|
282
|
+
|
|
283
|
+
return counts
|