iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.1__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.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
  2. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
  3. iam_policy_validator-1.15.1.dist-info/entry_points.txt +4 -0
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +2 -0
  6. iam_validator/checks/action_validation.py +91 -27
  7. iam_validator/checks/not_action_not_resource.py +163 -0
  8. iam_validator/checks/resource_validation.py +132 -81
  9. iam_validator/checks/wildcard_resource.py +136 -6
  10. iam_validator/commands/__init__.py +3 -0
  11. iam_validator/commands/cache.py +66 -24
  12. iam_validator/commands/completion.py +94 -15
  13. iam_validator/commands/mcp.py +210 -0
  14. iam_validator/commands/query.py +489 -65
  15. iam_validator/core/aws_service/__init__.py +5 -1
  16. iam_validator/core/aws_service/cache.py +20 -0
  17. iam_validator/core/aws_service/fetcher.py +180 -11
  18. iam_validator/core/aws_service/storage.py +14 -6
  19. iam_validator/core/aws_service/validators.py +68 -51
  20. iam_validator/core/check_registry.py +100 -35
  21. iam_validator/core/config/aws_global_conditions.py +18 -9
  22. iam_validator/core/config/check_documentation.py +104 -51
  23. iam_validator/core/config/config_loader.py +39 -3
  24. iam_validator/core/config/defaults.py +6 -0
  25. iam_validator/core/constants.py +11 -4
  26. iam_validator/core/models.py +39 -14
  27. iam_validator/mcp/__init__.py +162 -0
  28. iam_validator/mcp/models.py +118 -0
  29. iam_validator/mcp/server.py +2928 -0
  30. iam_validator/mcp/session_config.py +319 -0
  31. iam_validator/mcp/templates/__init__.py +79 -0
  32. iam_validator/mcp/templates/builtin.py +856 -0
  33. iam_validator/mcp/tools/__init__.py +72 -0
  34. iam_validator/mcp/tools/generation.py +888 -0
  35. iam_validator/mcp/tools/org_config_tools.py +263 -0
  36. iam_validator/mcp/tools/query.py +395 -0
  37. iam_validator/mcp/tools/validation.py +376 -0
  38. iam_validator/sdk/__init__.py +2 -0
  39. iam_validator/sdk/policy_utils.py +31 -5
  40. iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
  41. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
  42. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,376 @@
1
+ """Validation tools for MCP server.
2
+
3
+ This module provides MCP tools for validating IAM policies using the existing
4
+ SDK validation functionality. All functions wrap the core validation logic
5
+ from iam_validator.sdk without reimplementing it.
6
+ """
7
+
8
+ import atexit
9
+ import json
10
+ import tempfile
11
+ from collections.abc import Generator
12
+ from contextlib import contextmanager
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import yaml
17
+
18
+ from iam_validator.core.models import IAMPolicy
19
+ from iam_validator.core.policy_checks import validate_policies
20
+ from iam_validator.mcp.models import ValidationResult
21
+
22
+ # Track temp files for cleanup on exit (safety net for abnormal termination)
23
+ _temp_files_to_cleanup: set[Path] = set()
24
+
25
+
26
+ def _cleanup_temp_files() -> None:
27
+ """Clean up any remaining temp files on process exit."""
28
+ for temp_path in list(_temp_files_to_cleanup):
29
+ try:
30
+ if temp_path.exists():
31
+ temp_path.unlink()
32
+ except OSError:
33
+ pass
34
+ _temp_files_to_cleanup.clear()
35
+
36
+
37
+ atexit.register(_cleanup_temp_files)
38
+
39
+
40
+ @contextmanager
41
+ def _temp_config_file(
42
+ session_config: Any,
43
+ ) -> Generator[str | None, None, None]:
44
+ """Context manager for temporary config file with guaranteed cleanup.
45
+
46
+ Creates a temporary YAML config file from ValidatorConfig and ensures
47
+ cleanup even if exceptions occur or process is killed.
48
+
49
+ Args:
50
+ session_config: ValidatorConfig instance from SessionConfigManager
51
+
52
+ Yields:
53
+ Path to temporary config file, or None if no config provided
54
+ """
55
+ if session_config is None:
56
+ yield None
57
+ return
58
+
59
+ # ValidatorConfig already has the right structure - just dump its config_dict
60
+ config_dict = session_config.config_dict
61
+
62
+ # Create temp file and register for cleanup
63
+ temp_path: Path | None = None
64
+ try:
65
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
66
+ yaml.dump(config_dict, f)
67
+ temp_path = Path(f.name)
68
+ _temp_files_to_cleanup.add(temp_path)
69
+
70
+ yield str(temp_path)
71
+ finally:
72
+ # Clean up temp file
73
+ if temp_path:
74
+ try:
75
+ if temp_path.exists():
76
+ temp_path.unlink()
77
+ except OSError:
78
+ pass
79
+ _temp_files_to_cleanup.discard(temp_path)
80
+
81
+
82
+ # Trust policy actions (case-insensitive prefixes for matching)
83
+ _TRUST_POLICY_ACTIONS = frozenset(
84
+ [
85
+ "sts:assumerole",
86
+ "sts:assumerolewithwebidentity",
87
+ "sts:assumerolewithsaml",
88
+ ]
89
+ )
90
+
91
+
92
+ def _is_trust_action(action: str) -> bool:
93
+ """Check if an action indicates a trust policy (case-insensitive)."""
94
+ action_lower = action.lower()
95
+ # Check exact match or if it's a wildcard that would include assume role
96
+ return action_lower in _TRUST_POLICY_ACTIONS or action_lower in ("sts:*", "*")
97
+
98
+
99
+ def _detect_policy_type(policy: dict[str, Any]) -> str:
100
+ """Auto-detect policy type from structure.
101
+
102
+ Analyzes ALL statements in the policy to determine the appropriate policy type
103
+ based on AWS IAM policy patterns. Uses case-insensitive matching for actions.
104
+
105
+ Args:
106
+ policy: IAM policy dictionary to analyze
107
+
108
+ Returns:
109
+ - "trust" if ANY statement contains sts:AssumeRole* actions with Principal
110
+ - "resource" if ANY statement contains Principal/NotPrincipal without trust actions
111
+ - "identity" otherwise (default, identity-based policy)
112
+ """
113
+ statements = policy.get("Statement", [])
114
+ if isinstance(statements, dict):
115
+ statements = [statements]
116
+
117
+ has_principal = False
118
+ has_trust_action = False
119
+
120
+ for stmt in statements:
121
+ # Check for Principal in any statement
122
+ if "Principal" in stmt or "NotPrincipal" in stmt:
123
+ has_principal = True
124
+
125
+ # Check for trust policy actions (case-insensitive)
126
+ actions = stmt.get("Action", [])
127
+ if isinstance(actions, str):
128
+ actions = [actions]
129
+
130
+ for action in actions:
131
+ if _is_trust_action(action):
132
+ has_trust_action = True
133
+ break
134
+
135
+ # Determine type based on all statements
136
+ if has_principal:
137
+ if has_trust_action:
138
+ return "trust"
139
+ return "resource"
140
+
141
+ return "identity"
142
+
143
+
144
+ async def validate_policy(
145
+ policy: dict[str, Any],
146
+ policy_type: str | None = None,
147
+ config_path: str | None = None,
148
+ use_org_config: bool = True,
149
+ ) -> ValidationResult:
150
+ """Validate an IAM policy dictionary.
151
+
152
+ This tool validates a policy object against AWS IAM rules and security best
153
+ practices. It runs all enabled checks and returns detailed validation results.
154
+
155
+ Policy Type Auto-Detection:
156
+ If policy_type is None (default), the policy type is automatically detected:
157
+ - "trust" if contains sts:AssumeRole action (trust/assume role policy)
158
+ - "resource" if contains Principal/NotPrincipal (resource-based policy)
159
+ - "identity" otherwise (identity-based policy attached to users/roles/groups)
160
+
161
+ Configuration priority:
162
+ 1. config_path (if provided) - explicit YAML config file path
163
+ 2. Session org config (if use_org_config=True and config set)
164
+ 3. Default validator configuration
165
+
166
+ Args:
167
+ policy: IAM policy as a Python dictionary (must contain Version and Statement)
168
+ policy_type: Type of policy to validate. If None (default), auto-detects from structure.
169
+ Explicit options:
170
+ - "identity": Identity-based policy (attached to users/roles/groups)
171
+ - "resource": Resource-based policy (attached to resources like S3 buckets)
172
+ - "trust": Trust policy (role assumption policy)
173
+ config_path: Optional path to YAML configuration file
174
+ use_org_config: Whether to use session organization config (default: True)
175
+
176
+ Returns:
177
+ ValidationResult with:
178
+ - is_valid: True if no errors/warnings found
179
+ - issues: List of ValidationIssue objects with details
180
+ - policy_file: Set to "inline-policy" for dict validation
181
+ - policy_type_detected: The policy type used (auto-detected or provided)
182
+
183
+ Example:
184
+ >>> policy = {
185
+ ... "Version": "2012-10-17",
186
+ ... "Statement": [{
187
+ ... "Effect": "Allow",
188
+ ... "Action": "s3:GetObject",
189
+ ... "Resource": "arn:aws:s3:::my-bucket/*"
190
+ ... }]
191
+ ... }
192
+ >>> result = await validate_policy(policy)
193
+ >>> print(f"Valid: {result.is_valid}, Issues: {len(result.issues)}")
194
+ """
195
+ # Auto-detect policy type if not provided
196
+ effective_policy_type = policy_type
197
+ if effective_policy_type is None:
198
+ effective_policy_type = _detect_policy_type(policy)
199
+
200
+ # Map user-friendly policy type names to internal constants
201
+ policy_type_mapping = {
202
+ "identity": "IDENTITY_POLICY",
203
+ "resource": "RESOURCE_POLICY",
204
+ "trust": "TRUST_POLICY",
205
+ "scp": "SERVICE_CONTROL_POLICY",
206
+ "rcp": "RESOURCE_CONTROL_POLICY",
207
+ }
208
+
209
+ # Normalize the policy type
210
+ normalized_type = policy_type_mapping.get(effective_policy_type.lower(), "IDENTITY_POLICY")
211
+
212
+ # Parse the dict into an IAMPolicy model
213
+ iam_policy = IAMPolicy(**policy)
214
+
215
+ # Determine config path and session_config to use
216
+ session_config = None
217
+ if not config_path and use_org_config:
218
+ # Try to use session config
219
+ from iam_validator.mcp.session_config import SessionConfigManager
220
+
221
+ session_config = SessionConfigManager.get_config()
222
+
223
+ # Use context manager for temp file to ensure cleanup
224
+ with _temp_config_file(session_config) as temp_path:
225
+ effective_config_path = config_path or temp_path
226
+
227
+ # Use validate_policies to perform validation with policy_type support
228
+ # This handles all the validation logic including check execution
229
+ results = await validate_policies(
230
+ policies=[("inline-policy", iam_policy)],
231
+ config_path=effective_config_path,
232
+ policy_type=normalized_type, # type: ignore
233
+ )
234
+
235
+ # Get the first (and only) result
236
+ sdk_result = results[0] if results else None
237
+ if not sdk_result:
238
+ # Fallback if no results returned (shouldn't happen)
239
+ from iam_validator.core.models import PolicyValidationResult
240
+
241
+ sdk_result = PolicyValidationResult(
242
+ policy_file="inline-policy",
243
+ is_valid=False,
244
+ issues=[],
245
+ )
246
+
247
+ # Convert SDK result to MCP ValidationResult
248
+ return ValidationResult(
249
+ is_valid=sdk_result.is_valid,
250
+ issues=sdk_result.issues,
251
+ policy_file=sdk_result.policy_file,
252
+ policy_type_detected=effective_policy_type,
253
+ )
254
+
255
+
256
+ async def validate_policy_json(
257
+ policy_json: str, policy_type: str | None = None
258
+ ) -> ValidationResult:
259
+ """Validate an IAM policy from a JSON string.
260
+
261
+ This tool parses a JSON string into a policy object and validates it.
262
+ Useful when working with policy text from files, API responses, or user input.
263
+
264
+ Policy Type Auto-Detection:
265
+ If policy_type is None (default), the policy type is automatically detected
266
+ from the policy structure (see validate_policy for details).
267
+
268
+ Args:
269
+ policy_json: IAM policy as a JSON string
270
+ policy_type: Type of policy to validate. If None (default), auto-detects.
271
+ Options: "identity", "resource", "trust"
272
+
273
+ Returns:
274
+ ValidationResult with validation status and issues
275
+
276
+ Raises:
277
+ Returns ValidationResult with parsing error if JSON is invalid
278
+
279
+ Example:
280
+ >>> policy_json = '''
281
+ ... {
282
+ ... "Version": "2012-10-17",
283
+ ... "Statement": [{
284
+ ... "Effect": "Allow",
285
+ ... "Action": "*",
286
+ ... "Resource": "*"
287
+ ... }]
288
+ ... }
289
+ ... '''
290
+ >>> result = await validate_policy_json(policy_json)
291
+ >>> for issue in result.issues:
292
+ ... print(f"{issue.severity}: {issue.message}")
293
+ """
294
+ try:
295
+ # Parse JSON string to dict
296
+ policy_dict = json.loads(policy_json)
297
+ except json.JSONDecodeError as e:
298
+ # Return validation result with parsing error
299
+ from iam_validator.core.models import ValidationIssue
300
+
301
+ return ValidationResult(
302
+ is_valid=False,
303
+ issues=[
304
+ ValidationIssue(
305
+ severity="error",
306
+ statement_index=-1,
307
+ issue_type="json_parse_error",
308
+ message=f"Failed to parse policy JSON: {e}",
309
+ suggestion="Ensure the policy is valid JSON format",
310
+ check_id="policy_structure",
311
+ )
312
+ ],
313
+ policy_file="inline-policy",
314
+ )
315
+
316
+ # Validate the parsed policy dict
317
+ return await validate_policy(policy=policy_dict, policy_type=policy_type)
318
+
319
+
320
+ async def quick_validate(policy: dict[str, Any]) -> dict[str, Any]:
321
+ """Quick pass/fail validation check for a policy.
322
+
323
+ This is a lightweight validation that returns just the essential information:
324
+ whether the policy is valid, the number of issues found, and critical issues.
325
+ Useful for rapid validation without detailed issue analysis.
326
+
327
+ Args:
328
+ policy: IAM policy as a Python dictionary
329
+
330
+ Returns:
331
+ Dictionary containing:
332
+ - is_valid (bool): Whether the policy passed validation
333
+ - issue_count (int): Total number of issues found
334
+ - critical_issues (list[str]): List of critical/high severity issue messages
335
+ - sensitive_actions_found (int): Count of sensitive actions detected
336
+ - wildcards_detected (bool): Whether wildcards were found in actions/resources
337
+
338
+ Example:
339
+ >>> policy = {"Version": "2012-10-17", "Statement": [...]}
340
+ >>> result = await quick_validate(policy)
341
+ >>> if result["is_valid"]:
342
+ ... print("Policy is valid!")
343
+ >>> else:
344
+ ... print(f"Found {result['issue_count']} issues")
345
+ ... for msg in result["critical_issues"]:
346
+ ... print(f" - {msg}")
347
+ """
348
+ # Use validate_policy to get full results
349
+ validation_result = await validate_policy(policy=policy)
350
+
351
+ # Filter critical and high severity issues
352
+ critical_issues = []
353
+ sensitive_actions_count = 0
354
+ wildcards_detected = False
355
+
356
+ for issue in validation_result.issues:
357
+ severity = issue.severity.lower()
358
+ if severity in {"critical", "high", "error"}:
359
+ critical_issues.append(issue.message)
360
+
361
+ # Count sensitive action issues
362
+ if issue.check_id == "sensitive_action":
363
+ sensitive_actions_count += 1
364
+
365
+ # Detect wildcard issues
366
+ if issue.check_id in {"wildcard_action", "wildcard_resource", "service_wildcard"}:
367
+ wildcards_detected = True
368
+
369
+ # Return simplified result with enhanced fields
370
+ return {
371
+ "is_valid": validation_result.is_valid,
372
+ "issue_count": len(validation_result.issues),
373
+ "critical_issues": critical_issues,
374
+ "sensitive_actions_found": sensitive_actions_count,
375
+ "wildcards_detected": wildcards_detected,
376
+ }
@@ -99,6 +99,7 @@ from iam_validator.sdk.helpers import CheckHelper, expand_actions
99
99
  from iam_validator.sdk.policy_utils import (
100
100
  extract_actions,
101
101
  extract_condition_keys,
102
+ extract_condition_keys_from_statement,
102
103
  extract_resources,
103
104
  find_statements_with_action,
104
105
  find_statements_with_resource,
@@ -153,6 +154,7 @@ __all__ = [
153
154
  "extract_actions",
154
155
  "extract_resources",
155
156
  "extract_condition_keys",
157
+ "extract_condition_keys_from_statement",
156
158
  "find_statements_with_action",
157
159
  "find_statements_with_resource",
158
160
  "merge_policies",
@@ -176,6 +176,36 @@ def extract_resources(policy: IAMPolicy) -> list[str]:
176
176
  return sorted(resources)
177
177
 
178
178
 
179
+ def extract_condition_keys_from_statement(statement: Statement) -> set[str]:
180
+ """
181
+ Extract all condition keys from a single statement.
182
+
183
+ Args:
184
+ statement: Statement to extract condition keys from
185
+
186
+ Returns:
187
+ Set of condition key names (e.g., {"aws:ResourceAccount", "aws:SourceIp"})
188
+
189
+ Example:
190
+ >>> stmt = Statement(
191
+ ... Effect="Allow",
192
+ ... Action=["s3:GetObject"],
193
+ ... Resource=["*"],
194
+ ... Condition={"StringEquals": {"aws:ResourceAccount": "123456789012"}}
195
+ ... )
196
+ >>> keys = extract_condition_keys_from_statement(stmt)
197
+ >>> print(keys) # {"aws:ResourceAccount"}
198
+ """
199
+ if not statement.condition:
200
+ return set()
201
+
202
+ keys: set[str] = set()
203
+ for operator_block in statement.condition.values():
204
+ if isinstance(operator_block, dict):
205
+ keys.update(operator_block.keys())
206
+ return keys
207
+
208
+
179
209
  def extract_condition_keys(policy: IAMPolicy) -> list[str]:
180
210
  """
181
211
  Extract all condition keys used in a policy.
@@ -197,11 +227,7 @@ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
197
227
  return []
198
228
 
199
229
  for stmt in policy.statement:
200
- if stmt.condition:
201
- # Condition format: {"StringEquals": {"aws:username": "johndoe"}}
202
- for _, key_values in stmt.condition.items():
203
- if isinstance(key_values, dict):
204
- condition_keys.update(key_values.keys())
230
+ condition_keys.update(extract_condition_keys_from_statement(stmt))
205
231
 
206
232
  return sorted(condition_keys)
207
233
 
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- iam-validator = iam_validator.core.cli:main