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.
Files changed (42) hide show
  1. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
  2. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +9 -3
  5. iam_validator/checks/action_condition_enforcement.py +164 -2
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +3 -1
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/mfa_condition_check.py +112 -0
  10. iam_validator/checks/sensitive_action.py +78 -6
  11. iam_validator/checks/set_operator_validation.py +157 -0
  12. iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
  13. iam_validator/commands/cache.py +1 -1
  14. iam_validator/commands/validate.py +44 -11
  15. iam_validator/core/aws_fetcher.py +89 -52
  16. iam_validator/core/check_registry.py +165 -21
  17. iam_validator/core/condition_validators.py +626 -0
  18. iam_validator/core/config/__init__.py +13 -15
  19. iam_validator/core/config/aws_global_conditions.py +160 -0
  20. iam_validator/core/config/category_suggestions.py +104 -0
  21. iam_validator/core/config/condition_requirements.py +5 -385
  22. iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
  23. iam_validator/core/config/defaults.py +187 -54
  24. iam_validator/core/config/sensitive_actions.py +620 -81
  25. iam_validator/core/models.py +14 -1
  26. iam_validator/core/policy_checks.py +4 -4
  27. iam_validator/core/pr_commenter.py +1 -1
  28. iam_validator/sdk/__init__.py +187 -0
  29. iam_validator/sdk/arn_matching.py +274 -0
  30. iam_validator/sdk/context.py +222 -0
  31. iam_validator/sdk/exceptions.py +48 -0
  32. iam_validator/sdk/helpers.py +177 -0
  33. iam_validator/sdk/policy_utils.py +425 -0
  34. iam_validator/sdk/shortcuts.py +283 -0
  35. iam_validator/utils/__init__.py +31 -0
  36. iam_validator/utils/cache.py +105 -0
  37. iam_validator/utils/regex.py +206 -0
  38. iam_validator/checks/action_resource_constraint.py +0 -151
  39. iam_validator/core/aws_global_conditions.py +0 -137
  40. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  41. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  42. {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