iam-policy-validator 1.14.6__py3-none-any.whl → 1.15.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 (43) hide show
  1. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/METADATA +34 -23
  2. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/RECORD +42 -29
  3. iam_policy_validator-1.15.0.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 +32 -41
  20. iam_validator/core/check_registry.py +100 -35
  21. iam_validator/core/config/aws_global_conditions.py +13 -0
  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 +64 -63
  39. iam_validator/sdk/context.py +3 -2
  40. iam_validator/sdk/policy_utils.py +31 -5
  41. iam_policy_validator-1.14.6.dist-info/entry_points.txt +0 -2
  42. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/WHEEL +0 -0
  43. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,2928 @@
1
+ """FastMCP server implementation for IAM Policy Validator.
2
+
3
+ This module creates and configures the MCP server with all validation,
4
+ generation, and query tools registered. It serves as the main entry point
5
+ for the MCP server functionality.
6
+
7
+ Optimizations:
8
+ - Shared AWSServiceFetcher instance via lifespan context
9
+ - Cached check registry for repeated list_checks calls
10
+ - Pagination support for large result sets
11
+ - Batch operation tools for reduced round-trips
12
+ - MCP Resources for static data (templates, checks)
13
+ """
14
+
15
+ import functools
16
+ import logging
17
+ from contextlib import asynccontextmanager
18
+ from typing import Any
19
+
20
+ from fastmcp import Context, FastMCP
21
+
22
+ from iam_validator.core.aws_service import AWSServiceFetcher
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # =============================================================================
27
+ # Lifespan Management - Shared Resources
28
+ # =============================================================================
29
+
30
+
31
+ @asynccontextmanager
32
+ async def server_lifespan(_server: FastMCP):
33
+ """Manage server lifecycle with shared resources.
34
+
35
+ This context manager initializes expensive resources once at startup
36
+ and shares them across all tool invocations via the Context object.
37
+ """
38
+ # Initialize shared AWSServiceFetcher
39
+ fetcher = AWSServiceFetcher(
40
+ prefetch_common=True, # Pre-fetch common services at startup
41
+ memory_cache_size=512, # Larger cache for server use
42
+ )
43
+ await fetcher.__aenter__()
44
+
45
+ try:
46
+ # Store fetcher in server context for tools to access
47
+ yield {"fetcher": fetcher}
48
+ finally:
49
+ # Cleanup on shutdown
50
+ await fetcher.__aexit__(None, None, None)
51
+
52
+
53
+ def get_shared_fetcher(ctx: Any) -> AWSServiceFetcher | None:
54
+ """Get the shared AWSServiceFetcher from context.
55
+
56
+ Args:
57
+ ctx: FastMCP Context object from tool invocation
58
+
59
+ Returns:
60
+ Shared AWSServiceFetcher instance, or None if not available
61
+
62
+ Note:
63
+ When None is returned, callers typically create a new fetcher instance.
64
+ This is logged as a warning since it may lead to:
65
+ - Redundant HTTP connections
66
+ - Cache misses (new fetcher has empty cache)
67
+ - Potential performance degradation
68
+ """
69
+ if ctx and hasattr(ctx, "request_context") and ctx.request_context:
70
+ lifespan_ctx = ctx.request_context.lifespan_context
71
+ if lifespan_ctx and "fetcher" in lifespan_ctx:
72
+ return lifespan_ctx["fetcher"]
73
+
74
+ logger.warning(
75
+ "Shared fetcher unavailable from context. "
76
+ "A new fetcher instance will be created, which may impact performance."
77
+ )
78
+ return None
79
+
80
+
81
+ # =============================================================================
82
+ # Cached Registry for list_checks
83
+ # =============================================================================
84
+
85
+
86
+ @functools.lru_cache(maxsize=1)
87
+ def _get_cached_checks() -> tuple[dict[str, Any], ...]:
88
+ """Get cached check registry (initialized once, thread-safe via lru_cache)."""
89
+ from iam_validator.core.check_registry import create_default_registry
90
+
91
+ registry = create_default_registry()
92
+ return tuple(
93
+ sorted(
94
+ [
95
+ {
96
+ "check_id": check_id,
97
+ "description": check_instance.description,
98
+ "default_severity": check_instance.default_severity,
99
+ }
100
+ for check_id, check_instance in registry._checks.items()
101
+ ],
102
+ key=lambda x: x["check_id"],
103
+ )
104
+ )
105
+
106
+
107
+ # =============================================================================
108
+ # Base Instructions (constant)
109
+ # =============================================================================
110
+
111
+ BASE_INSTRUCTIONS = """
112
+ You are an AWS IAM security expert. Your mission: generate secure, least-privilege IAM policies that protect organizations from privilege escalation, data breaches, and unauthorized access.
113
+
114
+ ## CORE SECURITY PRINCIPLES
115
+
116
+ 1. **LEAST PRIVILEGE** - Grant only permissions needed for the specific task
117
+ 2. **EXPLICIT DENY** - Use Deny statements for critical restrictions
118
+ 3. **RESOURCE SCOPING** - Always scope to specific ARNs, never wildcards for write operations
119
+ 4. **CONDITION GUARDS** - Add conditions for sensitive actions (MFA, IP, time, service principals)
120
+ 5. **DEFENSE IN DEPTH** - Layer multiple security controls
121
+
122
+ ## ABSOLUTE RULES (NEVER VIOLATE)
123
+
124
+ - NEVER generate `"Action": "*"` - this grants full admin access
125
+ - NEVER generate `"Resource": "*"` with write/delete/modify actions
126
+ - NEVER allow `iam:*`, `sts:AssumeRole`, or `kms:*` without conditions
127
+ - NEVER guess ARN formats - use query_arn_formats to get correct patterns
128
+ - ALWAYS validate actions exist in AWS - typos create security gaps
129
+ - ALWAYS present security_notes from generation tools to the user
130
+
131
+ ## CRITICAL: NO VALIDATION LOOPS
132
+
133
+ ⛔ **HARD LIMIT: Maximum 2 validate_policy calls per policy request**
134
+
135
+ After 2 validations, you MUST present the policy to the user regardless of remaining issues but listing them to the user and asking for futher instructions.
136
+ Warnings (high/medium/low) are INFORMATIONAL - present them, don't try to fix them.
137
+
138
+ ### What to Fix vs Present
139
+
140
+ | Severity | What to do |
141
+ | ----------------------- | ------------------------------------------------ |
142
+ | error | Fix it - policy won't work in AWS |
143
+ | critical | Fix it - severe security risk |
144
+ | high/medium/low/warning | **STOP** - Present policy with these as warnings |
145
+
146
+ ### Workflow (STRICT)
147
+ 1. Generate policy using template or build_minimal_policy
148
+ 2. validate_policy (call #1)
149
+ 3. If "error" or "critical": apply fix from `example` field
150
+ 4. validate_policy (call #2) - **THIS IS YOUR LAST VALIDATION**
151
+ 5. **STOP** - Present policy to user with any remaining warnings
152
+
153
+ ### You MUST present the policy when:
154
+ - You have called validate_policy twice
155
+ - Only high/medium/low/warning severity issues remain
156
+ - The policy has no error/critical issues
157
+ - You need user input (e.g., "what resource ARN?")
158
+
159
+ ### Signs you are stuck in a loop (STOP NOW):
160
+ - You've called validate_policy more than twice
161
+ - The same warning keeps appearing
162
+ - You're trying to "fix" high/medium/low severity issues
163
+
164
+ **When in doubt: PRESENT THE POLICY. Let the user decide.**
165
+
166
+ ## SENSITIVE ACTION CATEGORIES (490+ actions tracked)
167
+
168
+ | Category | Risk | Examples | Required Mitigation |
169
+ | -------------------- | -------- | ---------------------------------------- | --------------------------- |
170
+ | credential_exposure | CRITICAL | sts:AssumeRole, iam:CreateAccessKey | MFA, source IP, time limits |
171
+ | privilege_escalation | CRITICAL | iam:AttachUserPolicy, iam:PassRole | Strict resource scope, MFA |
172
+ | data_access | HIGH | s3:GetObject, dynamodb:Scan | Resource scope, encryption |
173
+ | resource_exposure | HIGH | s3:PutBucketPolicy, lambda:AddPermission | Explicit deny patterns |
174
+
175
+ ## POLICY GENERATION WORKFLOW
176
+
177
+ ```
178
+ ┌─────────────────────────────────────────────────────────────────────┐
179
+ │ 1. UNDERSTAND THE REQUEST │
180
+ │ → What AWS service(s)? │
181
+ │ → What operations (read/write/admin)? │
182
+ │ → What specific resources (ARNs)? │
183
+ │ → What's the principal (Lambda, EC2, role)? |
184
+ │ → Are there existing org restrictions? (get_organization_config) │
185
+ └─────────────────────────────────────────────────────────────────────┘
186
+
187
+ ┌─────────────────────────────────────────────────────────────────┐
188
+ │ 2. CHOOSE GENERATION APPROACH │
189
+ │ │
190
+ │ Template exists? → generate_policy_from_template │
191
+ │ Custom needs? → build_minimal_policy │
192
+ │ Unknown actions? → suggest_actions → build_minimal_policy │
193
+ │ │
194
+ │ Use list_templates first to check for pre-built secure │
195
+ │ templates (s3-read-only, lambda-basic-execution, etc.) │
196
+ └─────────────────────────────────────────────────────────────────┘
197
+
198
+ ┌─────────────────────────────────────────────────────────────────┐
199
+ │ 3. VALIDATE ONCE │
200
+ │ │
201
+ │ validate_policy → check for issues │
202
+ │ ⚠️ VALIDATE ONLY ONCE - DO NOT LOOP │
203
+ │ │
204
+ │ BLOCKING (must fix before presenting): │
205
+ │ → severity="error" - Policy won't work in AWS │
206
+ │ → severity="critical" - Severe security risk │
207
+ │ │
208
+ │ NON-BLOCKING (present with warnings): │
209
+ │ → severity="high/medium/low/warning" - Security advice │
210
+ │ → Present policy WITH these warnings, let user decide │
211
+ └─────────────────────────────────────────────────────────────────┘
212
+
213
+ ┌─────────────────────────────────────────────────────────────────┐
214
+ │ 4. PRESENT TO USER (even with warnings) │
215
+ │ │
216
+ │ → Show the policy immediately after ONE validation │
217
+ │ → List any warnings/suggestions for user awareness │
218
+ │ → DO NOT keep fixing and re-validating in a loop │
219
+ │ → Let the user decide if they want changes │
220
+ └─────────────────────────────────────────────────────────────────┘
221
+ ```
222
+
223
+ ## TOOL SELECTION GUIDE
224
+
225
+ | Task | Primary Tool | Fallback |
226
+ | --------------------- | ---------------------------------------------- | ---------------------------- |
227
+ | Create policy | list_templates → generate_policy_from_template | build_minimal_policy |
228
+ | Validate policy | validate_policy | quick_validate (summary) |
229
+ | Fix structural issues | fix_policy_issues | - |
230
+ | Get fix guidance | get_issue_guidance | read issue.example field |
231
+ | Find actions | query_service_actions | suggest_actions |
232
+ | Check action risks | check_sensitive_actions | get_required_conditions |
233
+ | Get ARN formats | query_arn_formats | query_action_details |
234
+ | Expand wildcards | expand_wildcard_action | - |
235
+ | Batch operations | validate_policies_batch, query_actions_batch | - |
236
+ | Session config | set_organization_config, check_org_compliance | - |
237
+
238
+ ### Tool Hierarchy (prefer tools higher in list)
239
+ 1. **validate_policy** - Full validation with detailed issue information
240
+ 2. **fix_policy_issues** - Structural fixes only (Version, SIDs, action case)
241
+ 3. **get_issue_guidance** - Detailed fix instructions for specific check_ids
242
+
243
+ ## ANTI-PATTERNS TO PREVENT
244
+
245
+ 1. **Overly Broad Resources**
246
+ BAD: `"Resource": "arn:aws:s3:::*"`
247
+ GOOD: `"Resource": "arn:aws:s3:::my-specific-bucket/*"`
248
+
249
+ 2. **Service Wildcards Without Conditions**
250
+ BAD: `"Action": "s3:*"`
251
+ GOOD: `"Action": ["s3:GetObject", "s3:ListBucket"]` with specific resources
252
+
253
+ 3. **PassRole Without Service Restriction**
254
+ BAD: `"Action": "iam:PassRole", "Resource": "*"`
255
+ GOOD: Add `"Condition": {"StringEquals": {"iam:PassedToService": "lambda.amazonaws.com"}}`
256
+
257
+ 4. **Missing Secure Transport**
258
+ For S3, always add: `"Condition": {"StringLike": {"aws:ResourceAccount": "<aws-account-id> OR ${aws:PrincipalAccount}"}}`
259
+
260
+ 5. **Cross-Account Without Controls**
261
+ If Principal includes external accounts, require: source IP, or org restrictions
262
+
263
+ ## TRUST POLICY SPECIFICS
264
+
265
+ Trust policies control WHO can assume a role. Key differences:
266
+ - Principal is REQUIRED (AWS account, service, or federated user)
267
+ - Resource is NOT used (the role itself is the resource)
268
+ - Action is typically `sts:AssumeRole` only
269
+
270
+ Use validate_policy with auto-detection - it recognizes trust policies automatically.
271
+ For cross-account: generate_policy_from_template("cross-account-assume-role")
272
+
273
+ ## HANDLING USER REQUESTS
274
+
275
+ **"Give me full access to..."**
276
+ → Explain the security risks
277
+ → Ask: "What specific operations do you need?"
278
+ → Use suggest_actions to find minimal permissions
279
+ → Never generate `"Action": "*"`
280
+
281
+ **"Just make it work"**
282
+ → Still apply least privilege
283
+ → Validate thoroughly
284
+ → Present with security_notes explaining any risks
285
+
286
+ **After 3 failed fix attempts**
287
+ → Stop and ask user for clarification
288
+ → Present the specific blockers clearly
289
+ → Suggest alternatives
290
+
291
+ ## EXAMPLE INTERACTIONS
292
+
293
+ ### Example 1: Lambda needs S3 access
294
+ User: "Create a policy for my Lambda to read from S3 bucket my-data-bucket"
295
+
296
+ Your workflow:
297
+ 1. list_templates → find "s3-read-only" template
298
+ 2. generate_policy_from_template("s3-read-only", {"bucket_name": "my-data-bucket"})
299
+ 3. validate_policy ONCE → check result
300
+ 4. Present policy to user with any warnings (DO NOT re-validate)
301
+
302
+ ### Example 2: User requests overly broad access
303
+ User: "Give me full S3 access"
304
+
305
+ Your workflow:
306
+ 1. Ask: "What specific S3 operations do you need? (read, write, delete, list)"
307
+ 2. After clarification → query_service_actions("s3", access_level="read")
308
+ 3. build_minimal_policy with specific actions and resources
309
+ 4. validate_policy ONCE and present immediately (warnings are informational)
310
+
311
+ ### Example 3: Validation returns warnings (DO NOT LOOP)
312
+ After validate_policy returns warnings like "wildcard_resource" or "sensitive_action":
313
+
314
+ WRONG approach (causes infinite loop):
315
+ ❌ validate → fix → validate → fix → validate...
316
+
317
+ CORRECT approach:
318
+ ✅ validate ONCE → present policy WITH warnings → let user decide
319
+ ✅ Say: "Here's your policy. Note: it has these security considerations: [list warnings]"
320
+ ✅ Only fix if user explicitly asks for changes
321
+
322
+ ## VALIDATION ISSUE FIELDS
323
+
324
+ Each issue contains actionable guidance:
325
+ - `severity`: error/warning/critical/high/medium/low
326
+ - `message`: What's wrong
327
+ - `suggestion`: How to fix it
328
+ - `example`: **USE THIS** - shows the exact correct format
329
+ - `check_id`: For get_issue_guidance lookup
330
+ - `risk_explanation`: Why this matters
331
+ - `remediation_steps`: Step-by-step fix
332
+
333
+ ## RESOURCES AND PROMPTS AVAILABLE
334
+
335
+ ### Prompts (use for guided workflows)
336
+ - `generate_secure_policy` - Step-by-step policy creation with validation
337
+ - `fix_policy_issues_workflow` - Systematic issue fixing (max 2 iterations)
338
+ - `review_policy_security` - Security analysis without modification
339
+
340
+ ### Resources (reference data)
341
+ - `iam://templates` - Pre-built secure templates
342
+ - `iam://checks` - All 19 validation checks
343
+ - `iam://sensitive-categories` - Sensitive action categories
344
+ - `iam://config-schema` - Configuration settings schema
345
+ - `iam://config-examples` - Example configurations
346
+ - `iam://workflow-examples` - Detailed step-by-step examples
347
+
348
+ Read iam://workflow-examples for comprehensive usage patterns.
349
+
350
+ ## IAM ACTION AND POLICY FORMATTING RULES
351
+
352
+ ### Action Formatting (CRITICAL)
353
+ - **Service prefix MUST be lowercase**: `s3:GetObject` ✓, `S3:GetObject` ✗
354
+ - **Action name uses PascalCase**: `s3:GetObject` ✓, `s3:getobject` ✗
355
+ - **Full format**: `<service>:<ActionName>` (e.g., `lambda:InvokeFunction`)
356
+ - **Wildcards**: Use `*` for patterns (`s3:Get*`, `s3:*Object`, `s3:*`)
357
+ - **Common mistakes to avoid**:
358
+ - `S3:GetObject` → should be `s3:GetObject` (lowercase service)
359
+ - `s3:getObject` → should be `s3:GetObject` (PascalCase action)
360
+ - `s3.GetObject` → should be `s3:GetObject` (colon separator)
361
+ - `arn:aws:s3:::bucket` in Action → Actions are not ARNs
362
+
363
+ ### Policy Structure (REQUIRED FORMAT)
364
+ ```json
365
+ {
366
+ "Version": "2012-10-17",
367
+ "Statement": [
368
+ {
369
+ "Sid": "UniqueStatementId",
370
+ "Effect": "Allow",
371
+ "Action": ["service:ActionName"],
372
+ "Resource": ["arn:aws:service:region:account:resource"]
373
+ }
374
+ ]
375
+ }
376
+ ```
377
+
378
+ ### Version Field
379
+ - ALWAYS use `"Version": "2012-10-17"` (current version)
380
+ - `"2008-10-17"` is deprecated and lacks features like policy variables
381
+
382
+ ### Statement Fields
383
+ | Field | Required | Type | Valid Values |
384
+ | ----------- | ----------- | ------------- | ------------------------------------------- |
385
+ | Effect | Yes | string | `"Allow"` or `"Deny"` |
386
+ | Action | Yes* | string/array | Service actions like `"s3:GetObject"` |
387
+ | NotAction | No* | string/array | Actions to exclude |
388
+ | Resource | Yes* | string/array | ARNs like `"arn:aws:s3:::bucket/*"` |
389
+ | NotResource | No* | string/array | Resources to exclude |
390
+ | Principal | Conditional | string/object | For resource policies only |
391
+ | Condition | No | object | Condition operators and keys |
392
+ | Sid | No | string | Statement identifier (unique within policy) |
393
+
394
+ *Either Action or NotAction required; Either Resource or NotResource required
395
+
396
+ ### Resource ARN Formatting
397
+ - **Format**: `arn:aws:<service>:<region>:<account>:<resource>`
398
+ - **S3 buckets**: `arn:aws:s3:::<bucket-name>` (no region/account)
399
+ - **S3 objects**: `arn:aws:s3:::<bucket-name>/<key-path>`
400
+ - **DynamoDB tables**: `arn:aws:dynamodb:<region>:<account>:table/<table-name>`
401
+ - **Lambda functions**: `arn:aws:lambda:<region>:<account>:function:<function-name>`
402
+ - **Wildcards**: Use `*` for patterns (`arn:aws:s3:::my-bucket/*`)
403
+ - Use query_arn_formats to get correct ARN patterns for any service
404
+
405
+ ### Condition Block Formatting
406
+ ```json
407
+ "Condition": {
408
+ "<ConditionOperator>": {
409
+ "<ConditionKey>": "<value>"
410
+ }
411
+ }
412
+ ```
413
+
414
+ **Common operators**:
415
+ - `StringEquals`, `StringNotEquals`, `StringLike`, `StringNotLike`
416
+ - `ArnEquals`, `ArnLike`, `ArnNotEquals`, `ArnNotLike`
417
+ - `NumericEquals`, `NumericLessThan`, `NumericGreaterThan`
418
+ - `DateEquals`, `DateLessThan`, `DateGreaterThan`
419
+ - `Bool` (for boolean conditions like `aws:SecureTransport`)
420
+ - `IpAddress`, `NotIpAddress` (for source IP restrictions)
421
+
422
+ **Set operators** (for multi-value keys):
423
+ - `ForAllValues:StringEquals` - All values must match
424
+ - `ForAnyValue:StringEquals` - At least one value must match
425
+
426
+ ### Principal Formatting (Resource Policies Only)
427
+ ```json
428
+ "Principal": {
429
+ "AWS": "arn:aws:iam::123456789012:role/RoleName"
430
+ }
431
+ ```
432
+ Or for service principals:
433
+ ```json
434
+ "Principal": {
435
+ "Service": "lambda.amazonaws.com"
436
+ }
437
+ ```
438
+
439
+ ### Common Formatting Errors to Catch
440
+ 1. **Missing Version**: Always include `"Version": "2012-10-17"`
441
+ 2. **Effect typos**: `"allow"` → `"Allow"`, `"DENY"` → `"Deny"`
442
+ 3. **Invalid ARN format**: Missing colons or wrong segment count
443
+ 4. **Single string vs array**: `"Action": "s3:GetObject"` works but `["s3:GetObject"]` preferred
444
+
445
+ ALWAYS validate actions exist using query_action_details or validate_policy before presenting to users.
446
+ """
447
+
448
+
449
+ def get_instructions() -> str:
450
+ """Build full instructions including any custom instructions.
451
+
452
+ Returns:
453
+ Combined base instructions + custom instructions (if set)
454
+ """
455
+ from iam_validator.mcp.session_config import CustomInstructionsManager
456
+
457
+ custom = CustomInstructionsManager.get_instructions()
458
+ if custom:
459
+ return f"{BASE_INSTRUCTIONS}\n\n## ORGANIZATION-SPECIFIC INSTRUCTIONS\n\n{custom}"
460
+ return BASE_INSTRUCTIONS
461
+
462
+
463
+ # Create the MCP server instance with lifespan
464
+ mcp = FastMCP(
465
+ name="IAM Policy Validator",
466
+ lifespan=server_lifespan,
467
+ instructions=BASE_INSTRUCTIONS, # Will be updated dynamically in run_server()
468
+ )
469
+
470
+
471
+ # =============================================================================
472
+ # Validation Tools
473
+ # =============================================================================
474
+
475
+
476
+ @mcp.tool()
477
+ async def validate_policy(
478
+ policy: dict[str, Any],
479
+ policy_type: str | None = None,
480
+ verbose: bool = True,
481
+ use_org_config: bool = True,
482
+ ) -> dict[str, Any]:
483
+ """Validate an IAM policy.
484
+
485
+ Validates a policy against AWS IAM rules and security best practices.
486
+ Runs all enabled checks and returns validation results.
487
+
488
+ Policy Type Auto-Detection:
489
+ If policy_type is None (default), the policy type is automatically detected:
490
+ - "trust" if contains sts:AssumeRole action (trust/assume role policy)
491
+ - "resource" if contains Principal/NotPrincipal (resource-based policy)
492
+ - "identity" otherwise (identity-based policy attached to users/roles/groups)
493
+
494
+ If an organization config is set and use_org_config=True, the validation
495
+ will use organization-specific check overrides, ignore patterns, and
496
+ severity settings.
497
+
498
+ Args:
499
+ policy: IAM policy as a dictionary
500
+ policy_type: Type of policy to validate. If None (default), auto-detects from structure.
501
+ Explicit options:
502
+ - "identity": Identity-based policy (attached to users/roles/groups)
503
+ - "resource": Resource-based policy (attached to resources like S3 buckets)
504
+ - "trust": Trust policy (role assumption policy)
505
+ verbose: If True (default), return all issue fields. If False, return only
506
+ essential fields (severity, message, suggestion, check_id) to reduce tokens.
507
+ use_org_config: Whether to apply session organization config (default: True)
508
+
509
+ Returns:
510
+ Dictionary with:
511
+ - is_valid: True if no errors/warnings found
512
+ - issues: List of validation issues
513
+ - policy_file: Source identifier
514
+ """
515
+ from iam_validator.mcp.tools.validation import validate_policy as _validate
516
+
517
+ result = await _validate(policy=policy, policy_type=policy_type, use_org_config=use_org_config)
518
+
519
+ # Build issue list based on verbosity
520
+ if verbose:
521
+ issues = [
522
+ {
523
+ "severity": issue.severity,
524
+ "message": issue.message,
525
+ "suggestion": issue.suggestion,
526
+ "example": issue.example,
527
+ "check_id": issue.check_id,
528
+ "statement_index": issue.statement_index,
529
+ "action": getattr(issue, "action", None),
530
+ "resource": getattr(issue, "resource", None),
531
+ "field_name": getattr(issue, "field_name", None),
532
+ "risk_explanation": issue.risk_explanation,
533
+ "documentation_url": issue.documentation_url,
534
+ "remediation_steps": issue.remediation_steps,
535
+ }
536
+ for issue in result.issues
537
+ ]
538
+ else:
539
+ # Lean response - only essential fields
540
+ issues = [
541
+ {
542
+ "severity": issue.severity,
543
+ "message": issue.message,
544
+ "suggestion": issue.suggestion,
545
+ "check_id": issue.check_id,
546
+ }
547
+ for issue in result.issues
548
+ ]
549
+
550
+ return {
551
+ "is_valid": result.is_valid,
552
+ "issues": issues,
553
+ "policy_file": result.policy_file,
554
+ }
555
+
556
+
557
+ @mcp.tool()
558
+ async def quick_validate(policy: dict[str, Any]) -> dict[str, Any]:
559
+ """Quick pass/fail validation check for a policy.
560
+
561
+ Lightweight validation that returns essential information:
562
+ whether the policy is valid, number of issues, and critical issues.
563
+
564
+ Args:
565
+ policy: IAM policy as a Python dictionary
566
+
567
+ Returns:
568
+ Dictionary with:
569
+ - is_valid: Whether the policy passed validation
570
+ - issue_count: Total number of issues found
571
+ - critical_issues: List of critical/high severity issue messages
572
+ """
573
+ from iam_validator.mcp.tools.validation import quick_validate as _quick_validate
574
+
575
+ return await _quick_validate(policy=policy)
576
+
577
+
578
+ # =============================================================================
579
+ # Generation Tools
580
+ # =============================================================================
581
+
582
+
583
+ @mcp.tool()
584
+ async def generate_policy_from_template(
585
+ template_name: str,
586
+ variables: dict[str, str],
587
+ ) -> dict[str, Any]:
588
+ """Generate an IAM policy from a built-in template.
589
+
590
+ IMPORTANT: Call list_templates first to see available templates and their
591
+ required variables with descriptions.
592
+
593
+ Args:
594
+ template_name: Template name from list_templates. Common templates:
595
+ - s3-read-only: Read from S3 bucket
596
+ - s3-read-write: Read/write to S3 bucket
597
+ - lambda-basic-execution: Basic Lambda with CloudWatch logs
598
+ - lambda-s3-trigger: Lambda triggered by S3 events
599
+ - dynamodb-crud: DynamoDB table operations
600
+ - cloudwatch-logs: Write to CloudWatch Logs
601
+ variables: Dictionary of variable values. Get required variables from
602
+ list_templates. Common variables:
603
+ - bucket_name: S3 bucket name (without arn: prefix)
604
+ - function_name: Lambda function name
605
+ - table_name: DynamoDB table name
606
+ - account_id: 12-digit AWS account ID
607
+ - region: AWS region (e.g., us-east-1)
608
+
609
+ Returns:
610
+ Dictionary with:
611
+ - policy: The generated IAM policy (ready to use)
612
+ - validation: Validation results with any issues found
613
+ - security_notes: Security warnings to review
614
+ - template_used: Template name for reference
615
+
616
+ Example:
617
+ # First check what variables lambda-s3-trigger needs:
618
+ templates = await list_templates()
619
+
620
+ # Then generate with all required variables:
621
+ result = await generate_policy_from_template(
622
+ template_name="lambda-s3-trigger",
623
+ variables={
624
+ "bucket_name": "my-bucket",
625
+ "function_name": "my-function",
626
+ "account_id": "123456789012",
627
+ "region": "us-east-1"
628
+ }
629
+ )
630
+ """
631
+ from iam_validator.mcp.tools.generation import (
632
+ generate_policy_from_template as _generate,
633
+ )
634
+
635
+ result = await _generate(template_name=template_name, variables=variables)
636
+ return {
637
+ "policy": result.policy,
638
+ "validation": {
639
+ "is_valid": result.validation.is_valid,
640
+ "issues": [
641
+ {
642
+ "severity": issue.severity,
643
+ "message": issue.message,
644
+ "suggestion": issue.suggestion,
645
+ "example": issue.example,
646
+ "check_id": issue.check_id,
647
+ "risk_explanation": issue.risk_explanation,
648
+ "remediation_steps": issue.remediation_steps,
649
+ }
650
+ for issue in result.validation.issues
651
+ ],
652
+ },
653
+ "security_notes": result.security_notes,
654
+ "template_used": result.template_used,
655
+ }
656
+
657
+
658
+ @mcp.tool()
659
+ async def build_minimal_policy(
660
+ actions: list[str],
661
+ resources: list[str],
662
+ conditions: dict[str, Any] | None = None,
663
+ ) -> dict[str, Any]:
664
+ """Build a minimal IAM policy from explicit actions and resources.
665
+
666
+ Constructs a policy statement from provided actions and resources.
667
+ Validates that actions exist in AWS using built-in checks, warns about
668
+ sensitive actions, and returns security notes from validation.
669
+
670
+ Args:
671
+ actions: List of AWS actions (e.g., ["s3:GetObject", "s3:ListBucket"])
672
+ resources: List of resource ARNs (e.g., ["arn:aws:s3:::my-bucket/*"])
673
+ conditions: Optional conditions to add to the statement
674
+
675
+ Returns:
676
+ Dictionary with:
677
+ - policy: The generated IAM policy
678
+ - validation: Validation results from built-in checks
679
+ - security_notes: Security warnings from validation
680
+ """
681
+ from iam_validator.mcp.tools.generation import build_minimal_policy as _build
682
+
683
+ result = await _build(actions=actions, resources=resources, conditions=conditions)
684
+ return {
685
+ "policy": result.policy,
686
+ "validation": {
687
+ "is_valid": result.validation.is_valid,
688
+ "issues": [
689
+ {
690
+ "severity": issue.severity,
691
+ "message": issue.message,
692
+ "suggestion": issue.suggestion,
693
+ "example": issue.example,
694
+ "check_id": issue.check_id,
695
+ "risk_explanation": issue.risk_explanation,
696
+ "remediation_steps": issue.remediation_steps,
697
+ }
698
+ for issue in result.validation.issues
699
+ ],
700
+ },
701
+ "security_notes": result.security_notes,
702
+ }
703
+
704
+
705
+ @mcp.tool()
706
+ async def list_templates() -> list[dict[str, Any]]:
707
+ """List all available policy templates with their required variables.
708
+
709
+ IMPORTANT: Always call this BEFORE generate_policy_from_template to see
710
+ what variables each template requires.
711
+
712
+ Returns:
713
+ List of template dictionaries, each containing:
714
+ - name: Template identifier (pass to generate_policy_from_template)
715
+ - description: What the template does
716
+ - variables: List of required variables with:
717
+ - name: Variable name (key for the variables dict)
718
+ - description: What value to provide (e.g., "AWS account ID (12-digit number)")
719
+ - required: Whether the variable must be provided
720
+ """
721
+ from iam_validator.mcp.tools.generation import list_templates as _list_templates
722
+
723
+ return await _list_templates()
724
+
725
+
726
+ @mcp.tool()
727
+ async def suggest_actions(
728
+ description: str,
729
+ service: str | None = None,
730
+ ) -> list[str]:
731
+ """Suggest AWS actions based on a natural language description.
732
+
733
+ Uses keyword pattern matching to suggest appropriate AWS actions.
734
+ Useful for discovering actions when building policies.
735
+
736
+ Args:
737
+ description: Natural language description (e.g., "read files from S3")
738
+ service: Optional AWS service to limit suggestions (e.g., "s3", "lambda")
739
+
740
+ Returns:
741
+ List of suggested action names
742
+ """
743
+ from iam_validator.mcp.tools.generation import suggest_actions as _suggest
744
+
745
+ return await _suggest(description=description, service=service)
746
+
747
+
748
+ @mcp.tool()
749
+ async def get_required_conditions(actions: list[str]) -> dict[str, Any]:
750
+ """Get the conditions required for a list of actions.
751
+
752
+ Analyzes actions and returns condition requirements based on security
753
+ best practices (e.g., MFA for sensitive actions, IP restrictions).
754
+
755
+ Args:
756
+ actions: List of AWS actions to analyze
757
+
758
+ Returns:
759
+ Dictionary mapping condition keys to required values, grouped by type
760
+ """
761
+ from iam_validator.mcp.tools.generation import (
762
+ get_required_conditions as _get_conditions,
763
+ )
764
+
765
+ return await _get_conditions(actions=actions)
766
+
767
+
768
+ @mcp.tool()
769
+ async def check_sensitive_actions(actions: list[str]) -> dict[str, Any]:
770
+ """Check if any actions are sensitive and get remediation guidance.
771
+
772
+ Analyzes actions against the sensitive actions catalog (490+ actions)
773
+ and returns risk category, severity, and **REMEDIATION GUIDANCE** including
774
+ recommended IAM conditions and mitigation steps.
775
+
776
+ Args:
777
+ actions: List of AWS actions to check (e.g., ["iam:PassRole", "s3:GetObject"])
778
+
779
+ Returns:
780
+ Dictionary with:
781
+ - sensitive_actions: List of dictionaries for each sensitive action found
782
+ - action: The action name
783
+ - category: Risk category (credential_exposure, data_access, priv_esc, resource_exposure)
784
+ - severity: Severity level (critical or high)
785
+ - description: Category description
786
+ - remediation: Mitigation guidance including:
787
+ - risk_level: CRITICAL or HIGH
788
+ - why_dangerous: Explanation of the risk
789
+ - recommended_conditions: List of IAM conditions to add
790
+ - mitigation_steps: Steps to reduce risk
791
+ - condition_example: JSON example of the condition block to add
792
+ - specific_guidance: Action-specific advice (for iam:PassRole, sts:AssumeRole, etc.)
793
+ - total_checked: Number of actions checked
794
+ - sensitive_count: Number of sensitive actions found
795
+ - categories_found: List of unique risk categories found
796
+ - has_critical: Whether any critical severity actions were found
797
+ - summary: Quick summary with top recommendations
798
+
799
+ Example response for iam:PassRole:
800
+ {
801
+ "action": "iam:PassRole",
802
+ "category": "priv_esc",
803
+ "severity": "critical",
804
+ "remediation": {
805
+ "recommended_conditions": [{"condition": "iam:PassedToService", ...}],
806
+ "condition_example": {"Condition": {"StringEquals": {"iam:PassedToService": "lambda.amazonaws.com"}}},
807
+ "specific_guidance": "Always restrict iam:PassRole to specific services..."
808
+ }
809
+ }
810
+ """
811
+ from iam_validator.mcp.tools.generation import (
812
+ check_sensitive_actions as _check_sensitive,
813
+ )
814
+
815
+ return await _check_sensitive(actions=actions)
816
+
817
+
818
+ # =============================================================================
819
+ # Query Tools
820
+ # =============================================================================
821
+
822
+
823
+ @mcp.tool()
824
+ async def query_service_actions(
825
+ service: str,
826
+ access_level: str | None = None,
827
+ limit: int | None = None,
828
+ offset: int = 0,
829
+ ) -> dict[str, Any]:
830
+ """Get all actions for a service, optionally filtered by access level.
831
+
832
+ Args:
833
+ service: AWS service prefix (e.g., "s3", "iam", "ec2")
834
+ access_level: Optional filter (read|write|list|tagging|permissions-management)
835
+ limit: Maximum number of actions to return (default: all)
836
+ offset: Number of actions to skip for pagination (default: 0)
837
+
838
+ Returns:
839
+ Dictionary with:
840
+ - actions: List of action names
841
+ - total: Total number of actions available
842
+ - has_more: Whether more actions are available
843
+ """
844
+ from iam_validator.mcp.tools.query import query_service_actions as _query
845
+
846
+ all_actions = await _query(service=service, access_level=access_level)
847
+ total = len(all_actions)
848
+
849
+ # Apply pagination
850
+ if offset:
851
+ all_actions = all_actions[offset:]
852
+ if limit:
853
+ all_actions = all_actions[:limit]
854
+
855
+ return {
856
+ "actions": all_actions,
857
+ "total": total,
858
+ "has_more": offset + len(all_actions) < total,
859
+ }
860
+
861
+
862
+ @mcp.tool()
863
+ async def query_action_details(action: str) -> dict[str, Any] | None:
864
+ """Get detailed information about a specific action.
865
+
866
+ Args:
867
+ action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
868
+
869
+ Returns:
870
+ Dictionary with action metadata (access_level, resource_types, condition_keys),
871
+ or None if not found
872
+ """
873
+ from iam_validator.mcp.tools.query import query_action_details as _query
874
+
875
+ result = await _query(action=action)
876
+ if result is None:
877
+ return None
878
+ return {
879
+ "action": result.action,
880
+ "service": result.service,
881
+ "access_level": result.access_level,
882
+ "resource_types": result.resource_types,
883
+ "condition_keys": result.condition_keys,
884
+ "description": result.description,
885
+ }
886
+
887
+
888
+ @mcp.tool()
889
+ async def expand_wildcard_action(pattern: str) -> list[str]:
890
+ """Expand wildcards like "s3:Get*" to specific actions.
891
+
892
+ Args:
893
+ pattern: Action pattern with wildcards (e.g., "s3:Get*", "iam:*User*")
894
+
895
+ Returns:
896
+ List of matching action names
897
+ """
898
+ from iam_validator.mcp.tools.query import expand_wildcard_action as _expand
899
+
900
+ return await _expand(pattern=pattern)
901
+
902
+
903
+ @mcp.tool()
904
+ async def query_condition_keys(service: str) -> list[str]:
905
+ """Get all condition keys for a service.
906
+
907
+ Args:
908
+ service: AWS service prefix (e.g., "s3", "iam")
909
+
910
+ Returns:
911
+ List of condition key names (e.g., ["s3:prefix", "s3:x-amz-acl"])
912
+ """
913
+ from iam_validator.mcp.tools.query import query_condition_keys as _query
914
+
915
+ return await _query(service=service)
916
+
917
+
918
+ @mcp.tool()
919
+ async def query_arn_formats(service: str) -> list[dict[str, Any]]:
920
+ """Get ARN formats for a service's resources.
921
+
922
+ Args:
923
+ service: AWS service prefix (e.g., "s3", "iam")
924
+
925
+ Returns:
926
+ List of dictionaries with resource_type and arn_formats keys
927
+ """
928
+ from iam_validator.mcp.tools.query import query_arn_formats as _query
929
+
930
+ return await _query(service=service)
931
+
932
+
933
+ @mcp.tool()
934
+ async def list_checks() -> list[dict[str, Any]]:
935
+ """List all available validation checks.
936
+
937
+ Returns:
938
+ List of dictionaries with check_id, description, and default_severity
939
+ """
940
+ # Use cached registry instead of creating new one each call
941
+ # Convert tuple back to list for API compatibility
942
+ return list(_get_cached_checks())
943
+
944
+
945
+ @mcp.tool()
946
+ async def get_policy_summary(policy: dict[str, Any]) -> dict[str, Any]:
947
+ """Analyze a policy and return summary statistics.
948
+
949
+ Args:
950
+ policy: IAM policy as a dictionary
951
+
952
+ Returns:
953
+ Dictionary with:
954
+ - total_statements: Number of statements
955
+ - allow_statements: Number of Allow statements
956
+ - deny_statements: Number of Deny statements
957
+ - services_used: List of AWS services referenced
958
+ - actions_count: Total number of actions
959
+ - has_wildcards: Whether policy contains wildcards
960
+ - has_conditions: Whether policy has conditions
961
+ """
962
+ from iam_validator.mcp.tools.query import get_policy_summary as _get_summary
963
+
964
+ result = await _get_summary(policy=policy)
965
+ return {
966
+ "total_statements": result.total_statements,
967
+ "allow_statements": result.allow_statements,
968
+ "deny_statements": result.deny_statements,
969
+ "services_used": result.services_used,
970
+ "actions_count": result.actions_count,
971
+ "has_wildcards": result.has_wildcards,
972
+ "has_conditions": result.has_conditions,
973
+ }
974
+
975
+
976
+ @mcp.tool()
977
+ async def list_sensitive_actions(
978
+ category: str | None = None,
979
+ limit: int | None = None,
980
+ offset: int = 0,
981
+ ) -> dict[str, Any]:
982
+ """List sensitive actions, optionally filtered by category.
983
+
984
+ The sensitive actions catalog contains 490+ actions across 4 categories.
985
+
986
+ Args:
987
+ category: Optional filter:
988
+ - credential_exposure: Actions that can expose credentials (46 actions)
989
+ - data_access: Actions that access data (109 actions)
990
+ - privilege_escalation: Actions that can escalate privileges (27 actions)
991
+ - resource_exposure: Actions that can expose resources (321 actions)
992
+ limit: Maximum number of actions to return (default: all)
993
+ offset: Number of actions to skip for pagination (default: 0)
994
+
995
+ Returns:
996
+ Dictionary with:
997
+ - actions: List of sensitive action names
998
+ - total: Total number of actions available
999
+ - has_more: Whether more actions are available
1000
+ """
1001
+ from iam_validator.mcp.tools.query import list_sensitive_actions as _list_sensitive
1002
+
1003
+ all_actions = await _list_sensitive(category=category)
1004
+ total = len(all_actions)
1005
+
1006
+ # Apply pagination
1007
+ if offset:
1008
+ all_actions = all_actions[offset:]
1009
+ if limit:
1010
+ all_actions = all_actions[:limit]
1011
+
1012
+ return {
1013
+ "actions": all_actions,
1014
+ "total": total,
1015
+ "has_more": offset + len(all_actions) < total,
1016
+ }
1017
+
1018
+
1019
+ @mcp.tool()
1020
+ async def get_condition_requirements_for_action(action: str) -> dict[str, Any] | None:
1021
+ """Get required conditions for a specific action.
1022
+
1023
+ Checks if the action has condition requirements based on the sensitive
1024
+ actions catalog and condition requirements configuration.
1025
+
1026
+ Args:
1027
+ action: Full action name (e.g., "iam:PassRole", "s3:GetObject")
1028
+
1029
+ Returns:
1030
+ Dictionary with condition requirements, or None if no requirements
1031
+ """
1032
+ from iam_validator.mcp.tools.query import get_condition_requirements as _get_reqs
1033
+
1034
+ return await _get_reqs(action=action)
1035
+
1036
+
1037
+ # =============================================================================
1038
+ # Fix and Help Tools
1039
+ # =============================================================================
1040
+
1041
+
1042
+ @mcp.tool()
1043
+ async def fix_policy_issues(
1044
+ policy: dict[str, Any],
1045
+ issues_to_fix: list[str] | None = None,
1046
+ policy_type: str | None = None,
1047
+ ) -> dict[str, Any]:
1048
+ """Attempt to automatically fix common structural policy issues.
1049
+
1050
+ This tool applies simple structural fixes. For security-related fixes
1051
+ (conditions, sensitive actions), use the suggestion and example fields
1052
+ from validate_policy to apply fixes manually.
1053
+
1054
+ Auto-fixable issues (structural only):
1055
+ - Missing Version field → adds "2012-10-17"
1056
+ - Duplicate SIDs → makes them unique
1057
+ - Action case normalization → converts "S3:GetObject" to "s3:GetObject"
1058
+
1059
+ NOT auto-fixable (require user input):
1060
+ - Action: "*" → requires user to specify which actions
1061
+ - Resource: "*" → requires user to specify which resources
1062
+ - Missing conditions → use validate_policy example field to see correct fix
1063
+ - Invalid actions → use query_service_actions to find valid actions
1064
+
1065
+ Args:
1066
+ policy: The IAM policy to fix
1067
+ issues_to_fix: Optional list of check_ids to fix. If None, attempts all fixes.
1068
+ Example: ["policy_structure", "sid_uniqueness", "action_validation"]
1069
+ policy_type: Type of policy. If None (default), auto-detects from structure.
1070
+ Options: "identity", "resource", "trust"
1071
+
1072
+ Returns:
1073
+ Dictionary with:
1074
+ - fixed_policy: The policy with structural fixes applied
1075
+ - fixes_applied: List of fixes that were applied
1076
+ - unfixed_issues: Issues that require manual intervention (with guidance)
1077
+ - validation: New validation result after fixes
1078
+ """
1079
+ import copy
1080
+
1081
+ from iam_validator.mcp.tools.validation import _detect_policy_type
1082
+ from iam_validator.mcp.tools.validation import validate_policy as _validate
1083
+
1084
+ fixed_policy = copy.deepcopy(policy)
1085
+ fixes_applied: list[str] = []
1086
+ unfixed_issues: list[dict[str, Any]] = []
1087
+
1088
+ # Auto-detect policy type if not provided
1089
+ effective_policy_type = policy_type if policy_type else _detect_policy_type(policy)
1090
+
1091
+ # First, validate to get current issues
1092
+ initial_result = await _validate(policy=policy, policy_type=effective_policy_type)
1093
+ issue_check_ids = {issue.check_id for issue in initial_result.issues if issue.check_id}
1094
+
1095
+ # Apply fixes based on check_ids
1096
+ def should_fix(check_id: str) -> bool:
1097
+ return issues_to_fix is None or check_id in issues_to_fix
1098
+
1099
+ # Fix 1: Missing or invalid Version (structural fix)
1100
+ if should_fix("policy_structure"):
1101
+ if "Version" not in fixed_policy or fixed_policy.get("Version") not in [
1102
+ "2012-10-17",
1103
+ "2008-10-17",
1104
+ ]:
1105
+ fixed_policy["Version"] = "2012-10-17"
1106
+ fixes_applied.append("Added Version: 2012-10-17")
1107
+
1108
+ # Fix 2: Duplicate SIDs (structural fix)
1109
+ if should_fix("sid_uniqueness") and "sid_uniqueness" in issue_check_ids:
1110
+ statements = fixed_policy.get("Statement", [])
1111
+ seen_sids: dict[str, int] = {}
1112
+ for i, stmt in enumerate(statements):
1113
+ sid = stmt.get("Sid")
1114
+ if sid:
1115
+ if sid in seen_sids:
1116
+ new_sid = f"{sid}_{i}"
1117
+ stmt["Sid"] = new_sid
1118
+ fixes_applied.append(f"Renamed duplicate SID '{sid}' to '{new_sid}'")
1119
+ else:
1120
+ seen_sids[sid] = i
1121
+
1122
+ # Fix 3: Normalize action case (service prefix should be lowercase)
1123
+ if should_fix("action_validation"):
1124
+ statements = fixed_policy.get("Statement", [])
1125
+ if isinstance(statements, dict):
1126
+ statements = [statements]
1127
+
1128
+ for stmt in statements:
1129
+ actions = stmt.get("Action", [])
1130
+ was_string = isinstance(actions, str)
1131
+ if was_string:
1132
+ actions = [actions]
1133
+
1134
+ normalized = []
1135
+ for action in actions:
1136
+ if ":" in action:
1137
+ service, name = action.split(":", 1)
1138
+ if service != service.lower():
1139
+ new_action = f"{service.lower()}:{name}"
1140
+ normalized.append(new_action)
1141
+ fixes_applied.append(f"Normalized action case: {action} → {new_action}")
1142
+ else:
1143
+ normalized.append(action)
1144
+ else:
1145
+ normalized.append(action)
1146
+
1147
+ if normalized:
1148
+ stmt["Action"] = (
1149
+ normalized[0] if (was_string and len(normalized) == 1) else normalized
1150
+ )
1151
+
1152
+ # Collect issues that require manual intervention
1153
+ # Include the example and suggestion from the validator for guidance
1154
+ for issue in initial_result.issues:
1155
+ check_id = issue.check_id or "unknown"
1156
+
1157
+ # Skip structural issues we can fix
1158
+ if check_id in {"policy_structure", "sid_uniqueness", "action_validation"}:
1159
+ continue
1160
+
1161
+ # All other issues need manual fix - include validator's guidance
1162
+ unfixed_issues.append(
1163
+ {
1164
+ "check_id": check_id,
1165
+ "message": issue.message,
1166
+ "suggestion": issue.suggestion,
1167
+ "example": issue.example,
1168
+ "severity": issue.severity,
1169
+ }
1170
+ )
1171
+
1172
+ # Re-validate the fixed policy
1173
+ final_result = await _validate(policy=fixed_policy, policy_type=effective_policy_type)
1174
+
1175
+ return {
1176
+ "fixed_policy": fixed_policy,
1177
+ "fixes_applied": fixes_applied,
1178
+ "unfixed_issues": unfixed_issues,
1179
+ "validation": {
1180
+ "is_valid": final_result.is_valid,
1181
+ "issue_count": len(final_result.issues),
1182
+ "issues": [
1183
+ {
1184
+ "severity": issue.severity,
1185
+ "message": issue.message,
1186
+ "suggestion": issue.suggestion,
1187
+ "example": issue.example,
1188
+ "check_id": issue.check_id,
1189
+ }
1190
+ for issue in final_result.issues
1191
+ ],
1192
+ },
1193
+ }
1194
+
1195
+
1196
+ @mcp.tool()
1197
+ async def get_issue_guidance(check_id: str) -> dict[str, Any]:
1198
+ """Get detailed guidance on how to fix a specific validation issue.
1199
+
1200
+ Use this when you encounter a validation issue and need detailed
1201
+ instructions on how to resolve it. Provides step-by-step fixes.
1202
+
1203
+ Args:
1204
+ check_id: The check ID from the validation issue (e.g., "wildcard_action",
1205
+ "action_validation", "sensitive_action")
1206
+
1207
+ Returns:
1208
+ Dictionary with:
1209
+ - check_id: The check identifier
1210
+ - description: What this check validates
1211
+ - common_causes: Why this issue typically occurs
1212
+ - fix_steps: Step-by-step instructions to fix
1213
+ - example_before: Example of problematic policy
1214
+ - example_after: Example of fixed policy
1215
+ - related_tools: MCP tools that can help fix this issue
1216
+ """
1217
+ guidance_db: dict[str, dict[str, Any]] = {
1218
+ "wildcard_action": {
1219
+ "check_id": "wildcard_action",
1220
+ "description": "Detects policies that use Action: '*' granting all permissions",
1221
+ "common_causes": [
1222
+ "Trying to grant broad access without knowing specific actions",
1223
+ "Copy-pasted from an overly permissive example",
1224
+ ],
1225
+ "fix_steps": [
1226
+ "1. Identify what the policy user actually needs to do",
1227
+ "2. Use suggest_actions('describe what you need', 'service') to find actions",
1228
+ "3. Replace '*' with the specific action list",
1229
+ "4. Re-validate with validate_policy",
1230
+ ],
1231
+ "example_before": '{"Action": "*", "Resource": "*"}',
1232
+ "example_after": '{"Action": ["s3:GetObject", "s3:ListBucket"], "Resource": "arn:aws:s3:::my-bucket/*"}',
1233
+ "related_tools": ["suggest_actions", "query_service_actions", "list_templates"],
1234
+ },
1235
+ "wildcard_resource": {
1236
+ "check_id": "wildcard_resource",
1237
+ "description": "Detects policies that use Resource: '*' granting access to all resources",
1238
+ "common_causes": [
1239
+ "Not knowing the correct ARN format",
1240
+ "Wanting the policy to work across multiple resources",
1241
+ ],
1242
+ "fix_steps": [
1243
+ "1. Determine which specific resources need access",
1244
+ "2. Use query_arn_formats('service') to get ARN patterns",
1245
+ "3. Replace '*' with specific ARNs or ARN patterns",
1246
+ "4. Re-validate with validate_policy",
1247
+ ],
1248
+ "example_before": '{"Action": ["s3:GetObject"], "Resource": "*"}',
1249
+ "example_after": '{"Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::my-bucket/*", "arn:aws:s3:::my-bucket"]}',
1250
+ "related_tools": ["query_arn_formats", "get_policy_summary"],
1251
+ },
1252
+ "action_validation": {
1253
+ "check_id": "action_validation",
1254
+ "description": "Detects actions that don't exist in AWS",
1255
+ "common_causes": [
1256
+ "Typo in action name (e.g., 'S3:GetObject' instead of 's3:GetObject')",
1257
+ "Using deprecated action name",
1258
+ "Wrong service prefix",
1259
+ ],
1260
+ "fix_steps": [
1261
+ "1. Check the service prefix is lowercase (s3, not S3)",
1262
+ "2. Use query_service_actions('service') to list valid actions",
1263
+ "3. Use query_action_details('service:action') to verify action exists",
1264
+ "4. Fix the action name and re-validate",
1265
+ ],
1266
+ "example_before": '{"Action": ["S3:GetObjects"]}',
1267
+ "example_after": '{"Action": ["s3:GetObject"]}',
1268
+ "related_tools": [
1269
+ "query_service_actions",
1270
+ "query_action_details",
1271
+ "expand_wildcard_action",
1272
+ ],
1273
+ },
1274
+ "sensitive_action": {
1275
+ "check_id": "sensitive_action",
1276
+ "description": "Detects high-risk actions that can lead to privilege escalation or data exposure",
1277
+ "common_causes": [
1278
+ "Granting IAM, STS, or KMS permissions without restrictions",
1279
+ "Allowing actions that can modify security settings",
1280
+ ],
1281
+ "fix_steps": [
1282
+ "1. Verify the sensitive action is truly needed",
1283
+ "2. Use check_sensitive_actions(['action']) to understand the risk",
1284
+ "3. Use get_required_conditions(['action']) to get recommended conditions",
1285
+ "4. Add conditions to restrict when the action can be used",
1286
+ "5. Re-validate with validate_policy",
1287
+ ],
1288
+ "example_before": '{"Action": ["iam:PassRole"], "Resource": "*"}',
1289
+ "example_after": '{"Action": ["iam:PassRole"], "Resource": "arn:aws:iam::123456789012:role/LambdaRole", "Condition": {"StringEquals": {"iam:PassedToService": "lambda.amazonaws.com"}}}',
1290
+ "related_tools": [
1291
+ "check_sensitive_actions",
1292
+ "get_required_conditions",
1293
+ "fix_policy_issues",
1294
+ ],
1295
+ },
1296
+ "action_condition_enforcement": {
1297
+ "check_id": "action_condition_enforcement",
1298
+ "description": "Detects sensitive actions that should have conditions but don't",
1299
+ "common_causes": [
1300
+ "Not aware that certain actions need conditions",
1301
+ "Conditions were forgotten during policy creation",
1302
+ ],
1303
+ "fix_steps": [
1304
+ "1. Use get_required_conditions(['action']) to see what's needed",
1305
+ "2. Add the Condition block to the statement",
1306
+ "3. Common conditions: MFA, SourceIp, PassedToService",
1307
+ "4. Use fix_policy_issues to auto-add basic conditions",
1308
+ ],
1309
+ "example_before": '{"Action": ["iam:CreateUser"], "Resource": "*"}',
1310
+ "example_after": '{"Action": ["iam:CreateUser"], "Resource": "*", "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}}',
1311
+ "related_tools": [
1312
+ "get_required_conditions",
1313
+ "fix_policy_issues",
1314
+ "check_sensitive_actions",
1315
+ ],
1316
+ },
1317
+ "policy_structure": {
1318
+ "check_id": "policy_structure",
1319
+ "description": "Detects missing or malformed policy structure",
1320
+ "common_causes": [
1321
+ "Missing Version field",
1322
+ "Missing Statement array",
1323
+ "Invalid Effect value",
1324
+ ],
1325
+ "fix_steps": [
1326
+ "1. Ensure Version is '2012-10-17' (recommended)",
1327
+ "2. Ensure Statement is an array of statement objects",
1328
+ "3. Each statement must have Effect, Action, and Resource",
1329
+ "4. Use fix_policy_issues to auto-fix structure issues",
1330
+ ],
1331
+ "example_before": '{"Statement": [{"Action": "s3:*"}]}',
1332
+ "example_after": '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::bucket/*"}]}',
1333
+ "related_tools": ["fix_policy_issues", "validate_policy"],
1334
+ },
1335
+ }
1336
+
1337
+ if check_id in guidance_db:
1338
+ return guidance_db[check_id]
1339
+
1340
+ # Generic guidance for unknown check_id
1341
+ return {
1342
+ "check_id": check_id,
1343
+ "description": f"Validation check: {check_id}",
1344
+ "common_causes": ["Check the validation message for specific details"],
1345
+ "fix_steps": [
1346
+ "1. Read the issue message and suggestion from validate_policy",
1347
+ "2. Use the example field if provided",
1348
+ "3. Use list_checks() to get more info about available checks",
1349
+ "4. Consult AWS IAM documentation",
1350
+ ],
1351
+ "example_before": "See the issue's example field",
1352
+ "example_after": "Apply the suggestion from the issue",
1353
+ "related_tools": ["validate_policy", "list_checks", "fix_policy_issues"],
1354
+ }
1355
+
1356
+
1357
+ # =============================================================================
1358
+ # Advanced Analysis Tools
1359
+ # =============================================================================
1360
+
1361
+
1362
+ @mcp.tool()
1363
+ async def get_check_details(check_id: str) -> dict[str, Any]:
1364
+ """Get full documentation for a specific validation check.
1365
+
1366
+ Returns comprehensive information about a check including its description,
1367
+ severity, configuration options, example violations, and fixes.
1368
+
1369
+ Args:
1370
+ check_id: The check ID (e.g., "wildcard_action", "sensitive_action")
1371
+
1372
+ Returns:
1373
+ Dictionary with:
1374
+ - check_id: The check identifier
1375
+ - description: Full description of what the check validates
1376
+ - default_severity: Default severity level
1377
+ - category: Check category (security, aws, structure)
1378
+ - example_violation: Example policy that would trigger this check
1379
+ - example_fix: How to fix the violation
1380
+ - configuration: Available configuration options
1381
+ - related_checks: Related check IDs
1382
+ """
1383
+ from iam_validator.core.check_registry import create_default_registry
1384
+
1385
+ registry = create_default_registry()
1386
+
1387
+ # Check metadata database
1388
+ check_metadata: dict[str, dict[str, Any]] = {
1389
+ "wildcard_action": {
1390
+ "category": "security",
1391
+ "example_violation": {"Effect": "Allow", "Action": "*", "Resource": "*"},
1392
+ "example_fix": {
1393
+ "Effect": "Allow",
1394
+ "Action": ["s3:GetObject", "s3:ListBucket"],
1395
+ "Resource": "arn:aws:s3:::my-bucket/*",
1396
+ },
1397
+ "configuration": {"enabled": True, "severity": "configurable"},
1398
+ "related_checks": ["wildcard_resource", "full_wildcard", "service_wildcard"],
1399
+ },
1400
+ "wildcard_resource": {
1401
+ "category": "security",
1402
+ "example_violation": {
1403
+ "Effect": "Allow",
1404
+ "Action": ["s3:PutObject"],
1405
+ "Resource": "*",
1406
+ },
1407
+ "example_fix": {
1408
+ "Effect": "Allow",
1409
+ "Action": ["s3:PutObject"],
1410
+ "Resource": "arn:aws:s3:::my-bucket/*",
1411
+ },
1412
+ "configuration": {"enabled": True, "severity": "configurable"},
1413
+ "related_checks": ["wildcard_action", "full_wildcard"],
1414
+ },
1415
+ "sensitive_action": {
1416
+ "category": "security",
1417
+ "example_violation": {
1418
+ "Effect": "Allow",
1419
+ "Action": ["iam:CreateAccessKey"],
1420
+ "Resource": "*",
1421
+ },
1422
+ "example_fix": {
1423
+ "Effect": "Allow",
1424
+ "Action": ["iam:CreateAccessKey"],
1425
+ "Resource": "arn:aws:iam::123456789012:user/${aws:username}",
1426
+ "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}},
1427
+ },
1428
+ "configuration": {"enabled": True, "severity": "high"},
1429
+ "related_checks": ["action_condition_enforcement"],
1430
+ },
1431
+ "action_validation": {
1432
+ "category": "aws",
1433
+ "example_violation": {"Effect": "Allow", "Action": ["S3:GetObjects"], "Resource": "*"},
1434
+ "example_fix": {"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*"},
1435
+ "configuration": {"enabled": True},
1436
+ "related_checks": ["policy_structure"],
1437
+ },
1438
+ "policy_structure": {
1439
+ "category": "structure",
1440
+ "example_violation": {"Statement": [{"Action": "s3:*"}]},
1441
+ "example_fix": {
1442
+ "Version": "2012-10-17",
1443
+ "Statement": [{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "*"}],
1444
+ },
1445
+ "configuration": {"enabled": True},
1446
+ "related_checks": ["sid_uniqueness"],
1447
+ },
1448
+ }
1449
+
1450
+ # Get check from registry
1451
+ if check_id in registry._checks:
1452
+ check = registry._checks[check_id]
1453
+ metadata = check_metadata.get(check_id, {})
1454
+
1455
+ return {
1456
+ "check_id": check_id,
1457
+ "description": check.description,
1458
+ "default_severity": check.default_severity,
1459
+ "category": metadata.get("category", "general"),
1460
+ "example_violation": metadata.get("example_violation"),
1461
+ "example_fix": metadata.get("example_fix"),
1462
+ "configuration": metadata.get("configuration", {"enabled": True}),
1463
+ "related_checks": metadata.get("related_checks", []),
1464
+ }
1465
+
1466
+ return {
1467
+ "check_id": check_id,
1468
+ "description": "Check not found",
1469
+ "default_severity": "unknown",
1470
+ "category": "unknown",
1471
+ "example_violation": None,
1472
+ "example_fix": None,
1473
+ "configuration": {},
1474
+ "related_checks": [],
1475
+ }
1476
+
1477
+
1478
+ @mcp.tool()
1479
+ async def explain_policy(policy: dict[str, Any]) -> dict[str, Any]:
1480
+ """Generate a human-readable explanation of what a policy allows or denies.
1481
+
1482
+ Analyzes the policy structure and produces a plain-language summary
1483
+ of the effective permissions, including security concerns.
1484
+
1485
+ Args:
1486
+ policy: IAM policy as a dictionary
1487
+
1488
+ Returns:
1489
+ Dictionary with:
1490
+ - summary: Brief one-line summary
1491
+ - statements: Detailed explanation of each statement
1492
+ - services_accessed: List of AWS services with access types
1493
+ - security_concerns: Identified security issues
1494
+ - recommendations: Suggested improvements
1495
+ """
1496
+ from iam_validator.mcp.tools.query import get_policy_summary as _get_summary
1497
+
1498
+ summary = await _get_summary(policy)
1499
+ statements = policy.get("Statement", [])
1500
+ if isinstance(statements, dict):
1501
+ statements = [statements]
1502
+
1503
+ statement_explanations = []
1504
+ security_concerns = []
1505
+ recommendations = []
1506
+ services_with_access: dict[str, set[str]] = {}
1507
+
1508
+ for idx, stmt in enumerate(statements):
1509
+ effect = stmt.get("Effect", "Allow")
1510
+ actions = stmt.get("Action", [])
1511
+ resources = stmt.get("Resource", [])
1512
+ conditions = stmt.get("Condition", {})
1513
+
1514
+ if isinstance(actions, str):
1515
+ actions = [actions]
1516
+ if isinstance(resources, str):
1517
+ resources = [resources]
1518
+
1519
+ # Analyze actions by service
1520
+ for action in actions:
1521
+ if ":" in action:
1522
+ service = action.split(":")[0]
1523
+ action_name = action.split(":")[1]
1524
+ if service not in services_with_access:
1525
+ services_with_access[service] = set()
1526
+
1527
+ if action_name == "*":
1528
+ services_with_access[service].add("full")
1529
+ elif (
1530
+ action_name.startswith("Get")
1531
+ or action_name.startswith("List")
1532
+ or action_name.startswith("Describe")
1533
+ ):
1534
+ services_with_access[service].add("read")
1535
+ elif (
1536
+ action_name.startswith("Put")
1537
+ or action_name.startswith("Create")
1538
+ or action_name.startswith("Update")
1539
+ ):
1540
+ services_with_access[service].add("write")
1541
+ elif action_name.startswith("Delete") or action_name.startswith("Remove"):
1542
+ services_with_access[service].add("delete")
1543
+ else:
1544
+ services_with_access[service].add("other")
1545
+ elif action == "*":
1546
+ security_concerns.append(f"Statement {idx}: Full admin access with Action: '*'")
1547
+ recommendations.append("Replace Action: '*' with specific actions")
1548
+
1549
+ # Check for wildcards
1550
+ if "*" in resources:
1551
+ if effect == "Allow":
1552
+ security_concerns.append(f"Statement {idx}: Allows access to all resources")
1553
+ recommendations.append(f"Statement {idx}: Scope resources to specific ARNs")
1554
+
1555
+ # Build explanation
1556
+ action_desc = ", ".join(actions[:3]) + ("..." if len(actions) > 3 else "")
1557
+ resource_desc = ", ".join(resources[:2]) + ("..." if len(resources) > 2 else "")
1558
+ condition_desc = f" with {len(conditions)} condition(s)" if conditions else ""
1559
+
1560
+ explanation = f"{effect}s {action_desc} on {resource_desc}{condition_desc}"
1561
+ statement_explanations.append(
1562
+ {
1563
+ "index": idx,
1564
+ "sid": stmt.get("Sid", f"Statement{idx}"),
1565
+ "effect": effect,
1566
+ "explanation": explanation,
1567
+ "action_count": len(actions),
1568
+ "has_conditions": bool(conditions),
1569
+ }
1570
+ )
1571
+
1572
+ # Build services summary
1573
+ services_summary = []
1574
+ for service, access_types in services_with_access.items():
1575
+ services_summary.append(
1576
+ {
1577
+ "service": service,
1578
+ "access_types": sorted(access_types),
1579
+ }
1580
+ )
1581
+
1582
+ # Generate summary
1583
+ total_allow = sum(1 for s in statements if s.get("Effect") == "Allow")
1584
+ total_deny = len(statements) - total_allow
1585
+ brief_summary = f"Policy with {len(statements)} statement(s): {total_allow} Allow, {total_deny} Deny across {len(services_with_access)} service(s)"
1586
+
1587
+ return {
1588
+ "summary": brief_summary,
1589
+ "statements": statement_explanations,
1590
+ "services_accessed": services_summary,
1591
+ "security_concerns": security_concerns,
1592
+ "recommendations": recommendations,
1593
+ "has_wildcards": summary.has_wildcards,
1594
+ "has_conditions": summary.has_conditions,
1595
+ }
1596
+
1597
+
1598
+ @mcp.tool()
1599
+ async def build_arn(
1600
+ service: str,
1601
+ resource_type: str,
1602
+ resource_name: str,
1603
+ region: str = "",
1604
+ account_id: str = "",
1605
+ partition: str = "aws",
1606
+ ) -> dict[str, Any]:
1607
+ """Build a valid ARN from components.
1608
+
1609
+ Helps construct ARNs with proper format validation for the specified service.
1610
+
1611
+ Args:
1612
+ service: AWS service (e.g., "s3", "lambda", "dynamodb")
1613
+ resource_type: Type of resource (e.g., "bucket", "function", "table")
1614
+ resource_name: Name of the resource
1615
+ region: AWS region (required for regional resources, empty for global)
1616
+ account_id: AWS account ID (12 digits, empty for some services like S3)
1617
+ partition: AWS partition (default: "aws", or "aws-cn", "aws-us-gov")
1618
+
1619
+ Returns:
1620
+ Dictionary with:
1621
+ - arn: The constructed ARN
1622
+ - valid: Whether the ARN format is valid
1623
+ - notes: Any notes about the ARN format
1624
+ """
1625
+ # ARN format patterns by service
1626
+ arn_patterns: dict[str, dict[str, Any]] = {
1627
+ "s3": {
1628
+ "bucket": {
1629
+ "format": "arn:{partition}:s3:::{resource}",
1630
+ "needs_region": False,
1631
+ "needs_account": False,
1632
+ },
1633
+ "object": {
1634
+ "format": "arn:{partition}:s3:::{resource}",
1635
+ "needs_region": False,
1636
+ "needs_account": False,
1637
+ },
1638
+ },
1639
+ "lambda": {
1640
+ "function": {
1641
+ "format": "arn:{partition}:lambda:{region}:{account}:function:{resource}",
1642
+ "needs_region": True,
1643
+ "needs_account": True,
1644
+ },
1645
+ },
1646
+ "dynamodb": {
1647
+ "table": {
1648
+ "format": "arn:{partition}:dynamodb:{region}:{account}:table/{resource}",
1649
+ "needs_region": True,
1650
+ "needs_account": True,
1651
+ },
1652
+ },
1653
+ "iam": {
1654
+ "user": {
1655
+ "format": "arn:{partition}:iam::{account}:user/{resource}",
1656
+ "needs_region": False,
1657
+ "needs_account": True,
1658
+ },
1659
+ "role": {
1660
+ "format": "arn:{partition}:iam::{account}:role/{resource}",
1661
+ "needs_region": False,
1662
+ "needs_account": True,
1663
+ },
1664
+ "policy": {
1665
+ "format": "arn:{partition}:iam::{account}:policy/{resource}",
1666
+ "needs_region": False,
1667
+ "needs_account": True,
1668
+ },
1669
+ },
1670
+ "sqs": {
1671
+ "queue": {
1672
+ "format": "arn:{partition}:sqs:{region}:{account}:{resource}",
1673
+ "needs_region": True,
1674
+ "needs_account": True,
1675
+ },
1676
+ },
1677
+ "sns": {
1678
+ "topic": {
1679
+ "format": "arn:{partition}:sns:{region}:{account}:{resource}",
1680
+ "needs_region": True,
1681
+ "needs_account": True,
1682
+ },
1683
+ },
1684
+ "ec2": {
1685
+ "instance": {
1686
+ "format": "arn:{partition}:ec2:{region}:{account}:instance/{resource}",
1687
+ "needs_region": True,
1688
+ "needs_account": True,
1689
+ },
1690
+ "vpc": {
1691
+ "format": "arn:{partition}:ec2:{region}:{account}:vpc/{resource}",
1692
+ "needs_region": True,
1693
+ "needs_account": True,
1694
+ },
1695
+ },
1696
+ "secretsmanager": {
1697
+ "secret": {
1698
+ "format": "arn:{partition}:secretsmanager:{region}:{account}:secret:{resource}",
1699
+ "needs_region": True,
1700
+ "needs_account": True,
1701
+ },
1702
+ },
1703
+ "kms": {
1704
+ "key": {
1705
+ "format": "arn:{partition}:kms:{region}:{account}:key/{resource}",
1706
+ "needs_region": True,
1707
+ "needs_account": True,
1708
+ },
1709
+ },
1710
+ }
1711
+
1712
+ notes: list[str] = []
1713
+ valid = True
1714
+
1715
+ # Get pattern for service/resource type
1716
+ service_patterns = arn_patterns.get(service.lower(), {})
1717
+ pattern_info = service_patterns.get(resource_type.lower())
1718
+
1719
+ if not pattern_info:
1720
+ # Generic fallback
1721
+ if region and account_id:
1722
+ arn = f"arn:{partition}:{service}:{region}:{account_id}:{resource_type}/{resource_name}"
1723
+ elif account_id:
1724
+ arn = f"arn:{partition}:{service}::{account_id}:{resource_type}/{resource_name}"
1725
+ else:
1726
+ arn = f"arn:{partition}:{service}:::{resource_type}/{resource_name}"
1727
+ notes.append("Unknown service/resource combination. Using generic format.")
1728
+ return {"arn": arn, "valid": True, "notes": notes}
1729
+
1730
+ # Validate required fields
1731
+ if pattern_info["needs_region"] and not region:
1732
+ notes.append(f"Region is required for {service}:{resource_type}")
1733
+ valid = False
1734
+ region = "{region}"
1735
+
1736
+ if pattern_info["needs_account"] and not account_id:
1737
+ notes.append(f"Account ID is required for {service}:{resource_type}")
1738
+ valid = False
1739
+ account_id = "{account_id}"
1740
+
1741
+ # Build ARN from pattern
1742
+ arn = pattern_info["format"].format(
1743
+ partition=partition,
1744
+ region=region,
1745
+ account=account_id,
1746
+ resource=resource_name,
1747
+ )
1748
+
1749
+ return {"arn": arn, "valid": valid, "notes": notes}
1750
+
1751
+
1752
+ @mcp.tool()
1753
+ async def compare_policies(
1754
+ policy_a: dict[str, Any],
1755
+ policy_b: dict[str, Any],
1756
+ ) -> dict[str, Any]:
1757
+ """Compare two IAM policies and highlight differences.
1758
+
1759
+ Analyzes both policies and shows what permissions differ between them.
1760
+
1761
+ Args:
1762
+ policy_a: First IAM policy (baseline)
1763
+ policy_b: Second IAM policy (comparison)
1764
+
1765
+ Returns:
1766
+ Dictionary with:
1767
+ - summary: Brief comparison summary
1768
+ - added_actions: Actions in policy_b but not in policy_a
1769
+ - removed_actions: Actions in policy_a but not in policy_b
1770
+ - added_resources: Resources in policy_b but not in policy_a
1771
+ - removed_resources: Resources in policy_a but not in policy_b
1772
+ - condition_changes: Differences in conditions
1773
+ - effect_changes: Statements with different effects
1774
+ """
1775
+
1776
+ def extract_policy_elements(policy: dict[str, Any]) -> dict[str, Any]:
1777
+ statements = policy.get("Statement", [])
1778
+ if isinstance(statements, dict):
1779
+ statements = [statements]
1780
+
1781
+ all_actions: set[str] = set()
1782
+ all_resources: set[str] = set()
1783
+ all_conditions: list[dict[str, Any]] = []
1784
+ effects: dict[str, str] = {}
1785
+
1786
+ for idx, stmt in enumerate(statements):
1787
+ sid = stmt.get("Sid", f"stmt_{idx}")
1788
+ effect = stmt.get("Effect", "Allow")
1789
+ effects[sid] = effect
1790
+
1791
+ actions = stmt.get("Action", [])
1792
+ if isinstance(actions, str):
1793
+ actions = [actions]
1794
+ all_actions.update(actions)
1795
+
1796
+ resources = stmt.get("Resource", [])
1797
+ if isinstance(resources, str):
1798
+ resources = [resources]
1799
+ all_resources.update(resources)
1800
+
1801
+ if "Condition" in stmt:
1802
+ all_conditions.append({"sid": sid, "condition": stmt["Condition"]})
1803
+
1804
+ return {
1805
+ "actions": all_actions,
1806
+ "resources": all_resources,
1807
+ "conditions": all_conditions,
1808
+ "effects": effects,
1809
+ }
1810
+
1811
+ elements_a = extract_policy_elements(policy_a)
1812
+ elements_b = extract_policy_elements(policy_b)
1813
+
1814
+ added_actions = sorted(elements_b["actions"] - elements_a["actions"])
1815
+ removed_actions = sorted(elements_a["actions"] - elements_b["actions"])
1816
+ added_resources = sorted(elements_b["resources"] - elements_a["resources"])
1817
+ removed_resources = sorted(elements_a["resources"] - elements_b["resources"])
1818
+
1819
+ # Compare effects for matching SIDs
1820
+ effect_changes = []
1821
+ common_sids = set(elements_a["effects"].keys()) & set(elements_b["effects"].keys())
1822
+ for sid in common_sids:
1823
+ if elements_a["effects"][sid] != elements_b["effects"][sid]:
1824
+ effect_changes.append(
1825
+ {
1826
+ "sid": sid,
1827
+ "policy_a": elements_a["effects"][sid],
1828
+ "policy_b": elements_b["effects"][sid],
1829
+ }
1830
+ )
1831
+
1832
+ # Summarize
1833
+ changes = []
1834
+ if added_actions:
1835
+ changes.append(f"{len(added_actions)} action(s) added")
1836
+ if removed_actions:
1837
+ changes.append(f"{len(removed_actions)} action(s) removed")
1838
+ if added_resources:
1839
+ changes.append(f"{len(added_resources)} resource(s) added")
1840
+ if removed_resources:
1841
+ changes.append(f"{len(removed_resources)} resource(s) removed")
1842
+ if effect_changes:
1843
+ changes.append(f"{len(effect_changes)} effect change(s)")
1844
+
1845
+ summary = ", ".join(changes) if changes else "No significant differences found"
1846
+
1847
+ return {
1848
+ "summary": summary,
1849
+ "added_actions": added_actions,
1850
+ "removed_actions": removed_actions,
1851
+ "added_resources": added_resources,
1852
+ "removed_resources": removed_resources,
1853
+ "condition_changes": {
1854
+ "policy_a_conditions": len(elements_a["conditions"]),
1855
+ "policy_b_conditions": len(elements_b["conditions"]),
1856
+ },
1857
+ "effect_changes": effect_changes,
1858
+ }
1859
+
1860
+
1861
+ # =============================================================================
1862
+ # Batch Operations (Reduced Round-Trips)
1863
+ # =============================================================================
1864
+
1865
+
1866
+ @mcp.tool()
1867
+ async def validate_policies_batch(
1868
+ policies: list[dict[str, Any]],
1869
+ ctx: Context,
1870
+ policy_type: str | None = None,
1871
+ verbose: bool = False,
1872
+ ) -> list[dict[str, Any]]:
1873
+ """Validate multiple IAM policies in a single call.
1874
+
1875
+ More efficient than calling validate_policy multiple times when you need
1876
+ to validate several policies at once. Validations run in parallel.
1877
+
1878
+ Policy Type Auto-Detection:
1879
+ If policy_type is None (default), each policy's type is automatically detected
1880
+ from its structure (see validate_policy for detection rules).
1881
+
1882
+ Args:
1883
+ ctx: FastMCP context (automatically passed by framework)
1884
+ policies: List of IAM policy dictionaries to validate
1885
+ policy_type: Type of policies. If None (default), auto-detects each policy.
1886
+ Options: "identity", "resource", "trust"
1887
+ verbose: If True, return all issue fields. If False (default), return
1888
+ only essential fields to reduce tokens.
1889
+
1890
+ Returns:
1891
+ List of validation results, each containing:
1892
+ - policy_index: Index of the policy in the input list
1893
+ - is_valid: Whether the policy passed validation
1894
+ - issues: List of validation issues
1895
+ """
1896
+ import asyncio
1897
+
1898
+ from iam_validator.mcp.tools.validation import validate_policy as _validate
1899
+
1900
+ # Ensure shared fetcher is available (validates actions exist)
1901
+ _ = get_shared_fetcher(ctx)
1902
+
1903
+ async def validate_one(idx: int, policy: dict[str, Any]) -> dict[str, Any]:
1904
+ result = await _validate(policy=policy, policy_type=policy_type)
1905
+
1906
+ if verbose:
1907
+ issues = [
1908
+ {
1909
+ "severity": issue.severity,
1910
+ "message": issue.message,
1911
+ "suggestion": issue.suggestion,
1912
+ "example": issue.example,
1913
+ "check_id": issue.check_id,
1914
+ "statement_index": issue.statement_index,
1915
+ "action": getattr(issue, "action", None),
1916
+ "resource": getattr(issue, "resource", None),
1917
+ "field_name": getattr(issue, "field_name", None),
1918
+ }
1919
+ for issue in result.issues
1920
+ ]
1921
+ else:
1922
+ issues = [
1923
+ {
1924
+ "severity": issue.severity,
1925
+ "message": issue.message,
1926
+ "check_id": issue.check_id,
1927
+ }
1928
+ for issue in result.issues
1929
+ ]
1930
+
1931
+ return {
1932
+ "policy_index": idx,
1933
+ "is_valid": result.is_valid,
1934
+ "issues": issues,
1935
+ }
1936
+
1937
+ # Run all validations in parallel
1938
+ results = await asyncio.gather(*[validate_one(i, p) for i, p in enumerate(policies)])
1939
+ return list(results)
1940
+
1941
+
1942
+ @mcp.tool()
1943
+ async def query_actions_batch(actions: list[str], ctx: Context) -> dict[str, dict[str, Any] | None]:
1944
+ """Get details for multiple actions in a single call.
1945
+
1946
+ More efficient than calling query_action_details multiple times when you
1947
+ need information about several actions at once.
1948
+
1949
+ Args:
1950
+ ctx: FastMCP context (automatically passed by framework)
1951
+ actions: List of full action names (e.g., ["s3:GetObject", "iam:CreateUser"])
1952
+
1953
+ Returns:
1954
+ Dictionary mapping action names to their details (or None if not found).
1955
+ Each action detail contains: service, access_level, resource_types, condition_keys
1956
+ """
1957
+ import asyncio
1958
+
1959
+ from iam_validator.mcp.tools.query import query_action_details as _query
1960
+
1961
+ # Use shared fetcher from context
1962
+ shared_fetcher = get_shared_fetcher(ctx)
1963
+
1964
+ async def query_one(action: str) -> tuple[str, dict[str, Any] | None]:
1965
+ """Query a single action and return (action, details) tuple."""
1966
+ try:
1967
+ details = await _query(action=action, fetcher=shared_fetcher)
1968
+ if details:
1969
+ return (
1970
+ action,
1971
+ {
1972
+ "service": details.service,
1973
+ "access_level": details.access_level,
1974
+ "resource_types": details.resource_types,
1975
+ "condition_keys": details.condition_keys,
1976
+ "description": details.description,
1977
+ },
1978
+ )
1979
+ return (action, None)
1980
+ except Exception:
1981
+ return (action, None)
1982
+
1983
+ # Run all queries in parallel
1984
+ query_results = await asyncio.gather(*[query_one(action) for action in actions])
1985
+ return dict(query_results)
1986
+
1987
+
1988
+ @mcp.tool()
1989
+ async def check_actions_batch(actions: list[str], ctx: Context) -> dict[str, Any]:
1990
+ """Validate and check sensitivity for multiple actions in one call.
1991
+
1992
+ Combines action validation and sensitivity checking into a single tool
1993
+ for efficient batch processing.
1994
+
1995
+ Args:
1996
+ ctx: FastMCP context (automatically passed by framework)
1997
+ actions: List of AWS actions to check (e.g., ["s3:GetObject", "iam:PassRole"])
1998
+
1999
+ Returns:
2000
+ Dictionary with:
2001
+ - valid_actions: List of actions that exist in AWS
2002
+ - invalid_actions: List of actions that don't exist (with error messages)
2003
+ - sensitive_actions: List of sensitive actions with their categories
2004
+ """
2005
+ import asyncio
2006
+
2007
+ from iam_validator.core.aws_service import AWSServiceFetcher
2008
+ from iam_validator.core.config.sensitive_actions import (
2009
+ SENSITIVE_ACTION_CATEGORIES,
2010
+ get_category_for_action,
2011
+ )
2012
+
2013
+ async def check_one_action(action: str, fetcher: AWSServiceFetcher) -> dict[str, Any]:
2014
+ """Check a single action for validity and sensitivity."""
2015
+ result: dict[str, Any] = {
2016
+ "action": action,
2017
+ "is_valid": False,
2018
+ "error": None,
2019
+ "sensitive": None,
2020
+ }
2021
+
2022
+ # Check if action is valid
2023
+ try:
2024
+ if "*" in action:
2025
+ # Wildcard - try to expand
2026
+ expanded = await fetcher.expand_wildcard_action(action)
2027
+ if expanded:
2028
+ result["is_valid"] = True
2029
+ else:
2030
+ result["error"] = "No matching actions"
2031
+ else:
2032
+ is_valid, error, _ = await fetcher.validate_action(action)
2033
+ if is_valid:
2034
+ result["is_valid"] = True
2035
+ else:
2036
+ result["error"] = error or "Unknown error"
2037
+ except Exception as e:
2038
+ result["error"] = str(e)
2039
+
2040
+ # Check sensitivity (even for invalid actions - they might be typos of sensitive ones)
2041
+ category = get_category_for_action(action)
2042
+ if category:
2043
+ category_data = SENSITIVE_ACTION_CATEGORIES[category]
2044
+ result["sensitive"] = {
2045
+ "category": category,
2046
+ "severity": category_data["severity"],
2047
+ "name": category_data["name"],
2048
+ }
2049
+
2050
+ return result
2051
+
2052
+ # Try to get shared fetcher from context, fall back to creating new one
2053
+ shared_fetcher = get_shared_fetcher(ctx)
2054
+ if shared_fetcher:
2055
+ # Use shared fetcher - run all checks in parallel
2056
+ check_results = await asyncio.gather(
2057
+ *[check_one_action(action, shared_fetcher) for action in actions]
2058
+ )
2059
+ else:
2060
+ # Fall back to creating new fetcher
2061
+ async with AWSServiceFetcher() as fetcher:
2062
+ check_results = await asyncio.gather(
2063
+ *[check_one_action(action, fetcher) for action in actions]
2064
+ )
2065
+
2066
+ # Aggregate results
2067
+ valid_actions: list[str] = []
2068
+ invalid_actions: list[dict[str, str]] = []
2069
+ sensitive_actions: list[dict[str, Any]] = []
2070
+
2071
+ for result in check_results:
2072
+ action = result["action"]
2073
+ if result["is_valid"]:
2074
+ valid_actions.append(action)
2075
+ elif result["error"]:
2076
+ invalid_actions.append({"action": action, "error": result["error"]})
2077
+
2078
+ if result["sensitive"]:
2079
+ sensitive_actions.append({"action": action, **result["sensitive"]})
2080
+
2081
+ return {
2082
+ "valid_actions": valid_actions,
2083
+ "invalid_actions": invalid_actions,
2084
+ "sensitive_actions": sensitive_actions,
2085
+ }
2086
+
2087
+
2088
+ # =============================================================================
2089
+ # Organization Configuration Tools
2090
+ # =============================================================================
2091
+
2092
+
2093
+ @mcp.tool()
2094
+ async def set_organization_config(
2095
+ config: dict[str, Any],
2096
+ ) -> dict[str, Any]:
2097
+ """Set validator configuration for this MCP session.
2098
+
2099
+ The configuration uses the same format as the iam-validator YAML config files.
2100
+ It applies to all subsequent validation operations until cleared or updated.
2101
+
2102
+ Args:
2103
+ config: Validator configuration dictionary with:
2104
+ - settings: Global settings
2105
+ - fail_on_severity: List of severities that cause failure
2106
+ (e.g., ["error", "critical", "high"])
2107
+ - parallel_execution: Enable parallel check execution (default: true)
2108
+ - fail_fast: Stop on first error (default: false)
2109
+ - Check IDs as keys with configuration:
2110
+ - enabled: Enable/disable the check (default: true)
2111
+ - severity: Override check severity (error|critical|high|medium|low|warning)
2112
+ - ignore_patterns: Patterns to skip (see docs for pattern syntax)
2113
+
2114
+ Returns:
2115
+ Dictionary with:
2116
+ - success: Whether config was set successfully
2117
+ - applied_config: The effective configuration (settings + checks)
2118
+ - warnings: Any configuration warnings
2119
+
2120
+ Example:
2121
+ >>> result = await set_organization_config({
2122
+ ... "settings": {"fail_on_severity": ["error", "critical"]},
2123
+ ... "wildcard_action": {"enabled": True, "severity": "critical"},
2124
+ ... "sensitive_action": {"enabled": True, "severity": "high"},
2125
+ ... "policy_size": {"enabled": False} # Disable a check
2126
+ ... })
2127
+ """
2128
+ from iam_validator.mcp.tools.org_config_tools import set_organization_config_impl
2129
+
2130
+ return await set_organization_config_impl(config)
2131
+
2132
+
2133
+ @mcp.tool()
2134
+ async def get_organization_config() -> dict[str, Any]:
2135
+ """Get the current organization configuration for this session.
2136
+
2137
+ Returns:
2138
+ Dictionary with:
2139
+ - has_config: Whether an organization config is currently set
2140
+ - config: The current configuration (or null if not set)
2141
+ - source: Where the config came from ("session", "yaml", or "none")
2142
+ """
2143
+ from iam_validator.mcp.tools.org_config_tools import get_organization_config_impl
2144
+
2145
+ return await get_organization_config_impl()
2146
+
2147
+
2148
+ @mcp.tool()
2149
+ async def clear_organization_config() -> dict[str, str]:
2150
+ """Clear the organization configuration for this session.
2151
+
2152
+ After clearing, validation and generation will use default settings
2153
+ without any organization-specific restrictions.
2154
+
2155
+ Returns:
2156
+ Dictionary with:
2157
+ - status: "cleared" if config was removed, "no_config_set" if none existed
2158
+ """
2159
+ from iam_validator.mcp.tools.org_config_tools import clear_organization_config_impl
2160
+
2161
+ return await clear_organization_config_impl()
2162
+
2163
+
2164
+ @mcp.tool()
2165
+ async def load_organization_config_from_yaml(
2166
+ yaml_content: str,
2167
+ ) -> dict[str, Any]:
2168
+ """Load validator configuration from YAML content.
2169
+
2170
+ Parses YAML configuration and sets it as the session config.
2171
+ Uses the same format as iam-validator.yaml configuration files.
2172
+
2173
+ Args:
2174
+ yaml_content: YAML configuration string. Example:
2175
+ settings:
2176
+ fail_on_severity:
2177
+ - error
2178
+ - critical
2179
+ - high
2180
+
2181
+ # Enable/disable/configure specific checks
2182
+ wildcard_action:
2183
+ enabled: true
2184
+ severity: critical
2185
+
2186
+ sensitive_action:
2187
+ enabled: true
2188
+ severity: high
2189
+ ignore_patterns:
2190
+ - action: "^s3:Get.*" # Ignore S3 read actions
2191
+
2192
+ policy_size:
2193
+ enabled: false # Disable this check
2194
+
2195
+ Returns:
2196
+ Dictionary with:
2197
+ - success: Whether config was loaded successfully
2198
+ - applied_config: The effective configuration (settings + checks)
2199
+ - warnings: Any warnings (unknown keys, etc.)
2200
+ - error: Error message if loading failed
2201
+ """
2202
+ from iam_validator.mcp.tools.org_config_tools import (
2203
+ load_organization_config_from_yaml_impl,
2204
+ )
2205
+
2206
+ return await load_organization_config_from_yaml_impl(yaml_content)
2207
+
2208
+
2209
+ @mcp.tool()
2210
+ async def check_org_compliance(
2211
+ policy: dict[str, Any],
2212
+ ) -> dict[str, Any]:
2213
+ """Validate a policy using the current session configuration.
2214
+
2215
+ Runs the full IAM validator with the session configuration applied.
2216
+ This includes all enabled checks with their configured severity levels
2217
+ and ignore patterns.
2218
+
2219
+ If no session config is set, uses default validator settings.
2220
+
2221
+ Args:
2222
+ policy: IAM policy as a dictionary
2223
+
2224
+ Returns:
2225
+ Dictionary with:
2226
+ - compliant: True if no issues exceed fail_on_severity threshold
2227
+ - has_org_config: Whether a session config is set
2228
+ - violations: List of validation issues found (type, message, severity)
2229
+ - warnings: List of warnings
2230
+ - suggestions: How to fix issues
2231
+ """
2232
+ from iam_validator.mcp.tools.org_config_tools import check_org_compliance_impl
2233
+
2234
+ return await check_org_compliance_impl(policy)
2235
+
2236
+
2237
+ @mcp.tool()
2238
+ async def validate_with_config(
2239
+ policy: dict[str, Any],
2240
+ config: dict[str, Any],
2241
+ policy_type: str | None = None,
2242
+ ) -> dict[str, Any]:
2243
+ """Validate a policy with explicit inline configuration.
2244
+
2245
+ Useful for one-off validation with specific settings without modifying
2246
+ the session config. The provided config is used only for this call.
2247
+
2248
+ Policy Type Auto-Detection:
2249
+ If policy_type is None (default), the policy type is automatically detected
2250
+ from the policy structure (see validate_policy for detection rules).
2251
+
2252
+ Args:
2253
+ policy: IAM policy to validate
2254
+ config: Inline configuration (same format as set_organization_config):
2255
+ - settings: Global settings (fail_on_severity, parallel_execution, etc.)
2256
+ - Check IDs as keys: {enabled, severity, ignore_patterns}
2257
+ policy_type: Type of policy ("identity", "resource", "trust")
2258
+
2259
+ Returns:
2260
+ Dictionary with:
2261
+ - is_valid: Whether the policy passed all checks
2262
+ - issues: List of validation issues (severity, message, suggestion, check_id)
2263
+ - config_applied: The configuration that was used
2264
+
2265
+ Example:
2266
+ >>> result = await validate_with_config(
2267
+ ... policy=my_policy,
2268
+ ... config={
2269
+ ... "settings": {"fail_on_severity": ["error"]},
2270
+ ... "wildcard_action": {"severity": "warning"} # Downgrade to warning
2271
+ ... }
2272
+ ... )
2273
+ """
2274
+ from iam_validator.mcp.tools.org_config_tools import validate_with_config_impl
2275
+
2276
+ return await validate_with_config_impl(policy, config, policy_type)
2277
+
2278
+
2279
+ # =============================================================================
2280
+ # Custom Instructions Tools
2281
+ # =============================================================================
2282
+
2283
+
2284
+ @mcp.tool()
2285
+ async def set_custom_instructions(
2286
+ instructions: str,
2287
+ ) -> dict[str, Any]:
2288
+ """Set custom instructions for policy generation.
2289
+
2290
+ Custom instructions are appended to the default MCP server instructions,
2291
+ allowing organizations to add their own policy generation guidelines.
2292
+
2293
+ These instructions will influence how the AI assistant generates and
2294
+ validates IAM policies during this session.
2295
+
2296
+ Args:
2297
+ instructions: Custom instructions text (markdown supported). Examples:
2298
+ - "All policies must include aws:PrincipalOrgID condition"
2299
+ - "Use resource tags for access control where possible"
2300
+ - "S3 buckets must have encryption conditions"
2301
+
2302
+ Returns:
2303
+ Dictionary with:
2304
+ - success: True if instructions were set
2305
+ - instructions_preview: First 200 chars of the instructions
2306
+ - previous_source: Source of any replaced instructions
2307
+
2308
+ Example:
2309
+ >>> await set_custom_instructions('''
2310
+ ... ## Organization Security Requirements
2311
+ ... - All policies must restrict to our AWS Organization
2312
+ ... - MFA is required for any IAM modifications
2313
+ ... - S3 access must include secure transport conditions
2314
+ ... ''')
2315
+ """
2316
+ from iam_validator.mcp.session_config import CustomInstructionsManager
2317
+
2318
+ previous_source = CustomInstructionsManager.get_source()
2319
+
2320
+ CustomInstructionsManager.set_instructions(instructions, source="api")
2321
+
2322
+ # Update the server instructions
2323
+ mcp.instructions = get_instructions()
2324
+
2325
+ preview = instructions[:200] + "..." if len(instructions) > 200 else instructions
2326
+
2327
+ return {
2328
+ "success": True,
2329
+ "instructions_preview": preview,
2330
+ "previous_source": previous_source,
2331
+ }
2332
+
2333
+
2334
+ @mcp.tool()
2335
+ async def get_custom_instructions() -> dict[str, Any]:
2336
+ """Get the current custom instructions.
2337
+
2338
+ Returns:
2339
+ Dictionary with:
2340
+ - has_instructions: Whether custom instructions are set
2341
+ - instructions: The custom instructions (or None)
2342
+ - source: Where the instructions came from (api, env, file, config, none)
2343
+ """
2344
+ from iam_validator.mcp.session_config import CustomInstructionsManager
2345
+
2346
+ instructions = CustomInstructionsManager.get_instructions()
2347
+
2348
+ return {
2349
+ "has_instructions": instructions is not None,
2350
+ "instructions": instructions,
2351
+ "source": CustomInstructionsManager.get_source(),
2352
+ }
2353
+
2354
+
2355
+ @mcp.tool()
2356
+ async def clear_custom_instructions() -> dict[str, str]:
2357
+ """Clear custom instructions, reverting to default server instructions.
2358
+
2359
+ Returns:
2360
+ Dictionary with:
2361
+ - status: "cleared" if instructions were removed, "no_instructions_set" if none existed
2362
+ """
2363
+ from iam_validator.mcp.session_config import CustomInstructionsManager
2364
+
2365
+ had_instructions = CustomInstructionsManager.clear_instructions()
2366
+
2367
+ # Reset to base instructions
2368
+ mcp.instructions = BASE_INSTRUCTIONS
2369
+
2370
+ return {
2371
+ "status": "cleared" if had_instructions else "no_instructions_set",
2372
+ }
2373
+
2374
+
2375
+ # =============================================================================
2376
+ # MCP Resources (Static Data - Client Cacheable)
2377
+ # =============================================================================
2378
+
2379
+
2380
+ @mcp.resource("iam://templates")
2381
+ async def templates_resource() -> str:
2382
+ """List of all available policy templates.
2383
+
2384
+ This resource provides metadata about built-in policy templates
2385
+ that can be used with generate_policy_from_template.
2386
+ """
2387
+ import json
2388
+
2389
+ from iam_validator.mcp.tools.generation import list_templates as _list_templates
2390
+
2391
+ templates = await _list_templates()
2392
+ return json.dumps(templates, indent=2)
2393
+
2394
+
2395
+ @mcp.resource("iam://checks")
2396
+ async def checks_resource() -> str:
2397
+ """List of all available validation checks.
2398
+
2399
+ This resource provides metadata about all validation checks
2400
+ including their IDs, descriptions, and default severities.
2401
+ """
2402
+ import json
2403
+
2404
+ return json.dumps(_get_cached_checks(), indent=2)
2405
+
2406
+
2407
+ @mcp.resource("iam://sensitive-categories")
2408
+ async def sensitive_categories_resource() -> str:
2409
+ """Sensitive action categories and their descriptions.
2410
+
2411
+ This resource describes the 4 categories of sensitive actions
2412
+ that the validator tracks.
2413
+ """
2414
+ import json
2415
+
2416
+ from iam_validator.core.config.sensitive_actions import SENSITIVE_ACTION_CATEGORIES
2417
+
2418
+ # Convert frozensets to lists for JSON serialization
2419
+ serializable = {
2420
+ category_id: {
2421
+ "name": data["name"],
2422
+ "description": data["description"],
2423
+ "severity": data["severity"],
2424
+ "action_count": len(data["actions"]),
2425
+ }
2426
+ for category_id, data in SENSITIVE_ACTION_CATEGORIES.items()
2427
+ }
2428
+
2429
+ return json.dumps(serializable, indent=2)
2430
+
2431
+
2432
+ @mcp.resource("iam://config-schema")
2433
+ def config_schema_resource() -> str:
2434
+ """JSON Schema for session configuration.
2435
+
2436
+ Returns the schema for valid configuration settings,
2437
+ useful for AI assistants to validate config before setting.
2438
+ """
2439
+ import json
2440
+
2441
+ from iam_validator.core.config.config_loader import SettingsSchema
2442
+
2443
+ return json.dumps(SettingsSchema.model_json_schema(), indent=2)
2444
+
2445
+
2446
+ @mcp.resource("iam://config-examples")
2447
+ def config_examples_resource() -> str:
2448
+ """Example configurations for common scenarios.
2449
+
2450
+ Provides examples for different security postures and use cases.
2451
+ These configurations use the same format as the CLI validator YAML config.
2452
+ All validation is done by the IAM validator's built-in checks.
2453
+ """
2454
+ return """
2455
+ # Configuration Examples
2456
+
2457
+ These configurations can be used with both the CLI (`--config`) and MCP server.
2458
+ They control which checks run and their severity levels.
2459
+
2460
+ ## 1. Enterprise Security (Strict)
2461
+ Maximum security - all wildcards are critical, sensitive actions flagged.
2462
+
2463
+ ```yaml
2464
+ settings:
2465
+ fail_on_severity:
2466
+ - error
2467
+ - critical
2468
+ - high
2469
+
2470
+ # Make all wildcard checks critical severity
2471
+ wildcard_action:
2472
+ enabled: true
2473
+ severity: critical
2474
+
2475
+ wildcard_resource:
2476
+ enabled: true
2477
+ severity: critical
2478
+
2479
+ full_wildcard:
2480
+ enabled: true
2481
+ severity: critical
2482
+
2483
+ service_wildcard:
2484
+ enabled: true
2485
+ severity: critical
2486
+
2487
+ # Flag all sensitive/privileged actions
2488
+ sensitive_action:
2489
+ enabled: true
2490
+ severity: high
2491
+
2492
+ # Require conditions on sensitive actions
2493
+ action_condition_enforcement:
2494
+ enabled: true
2495
+ severity: error
2496
+ ```
2497
+
2498
+ ## 2. Development Environment (Permissive)
2499
+ Relaxed settings for dev/sandbox - only catch critical issues.
2500
+
2501
+ ```yaml
2502
+ settings:
2503
+ fail_on_severity:
2504
+ - error
2505
+ - critical
2506
+
2507
+ # Disable sensitive action warnings in dev
2508
+ sensitive_action:
2509
+ enabled: false
2510
+
2511
+ # Lower severity for wildcards (warn but don't fail)
2512
+ wildcard_action:
2513
+ enabled: true
2514
+ severity: medium
2515
+
2516
+ wildcard_resource:
2517
+ enabled: true
2518
+ severity: medium
2519
+
2520
+ # Still catch full admin access
2521
+ full_wildcard:
2522
+ enabled: true
2523
+ severity: critical
2524
+ ```
2525
+
2526
+ ## 3. Compliance-Focused
2527
+ Emphasizes policy structure and AWS validation.
2528
+
2529
+ ```yaml
2530
+ settings:
2531
+ fail_on_severity:
2532
+ - error
2533
+ - critical
2534
+ - high
2535
+
2536
+ # Ensure all actions are valid AWS actions
2537
+ action_validation:
2538
+ enabled: true
2539
+ severity: error
2540
+
2541
+ # Validate condition keys and operators
2542
+ condition_key_validation:
2543
+ enabled: true
2544
+ severity: error
2545
+
2546
+ condition_type_mismatch:
2547
+ enabled: true
2548
+ severity: error
2549
+
2550
+ # Ensure proper policy structure
2551
+ policy_structure:
2552
+ enabled: true
2553
+ severity: error
2554
+
2555
+ # Check policy size limits
2556
+ policy_size:
2557
+ enabled: true
2558
+ severity: error
2559
+ ```
2560
+
2561
+ ## 4. Security Audit
2562
+ Comprehensive security review - everything enabled at high severity.
2563
+
2564
+ ```yaml
2565
+ settings:
2566
+ fail_on_severity:
2567
+ - error
2568
+ - critical
2569
+ - high
2570
+ - medium
2571
+
2572
+ # All security checks at high severity
2573
+ wildcard_action:
2574
+ enabled: true
2575
+ severity: high
2576
+
2577
+ wildcard_resource:
2578
+ enabled: true
2579
+ severity: high
2580
+
2581
+ full_wildcard:
2582
+ enabled: true
2583
+ severity: critical
2584
+
2585
+ service_wildcard:
2586
+ enabled: true
2587
+ severity: high
2588
+
2589
+ sensitive_action:
2590
+ enabled: true
2591
+ severity: high
2592
+
2593
+ action_condition_enforcement:
2594
+ enabled: true
2595
+ severity: high
2596
+
2597
+ # Catch NotAction/NotResource anti-patterns
2598
+ not_action_not_resource:
2599
+ enabled: true
2600
+ severity: high
2601
+ ```
2602
+
2603
+ ## 5. Minimal Validation
2604
+ Quick validation - only structural and critical issues.
2605
+
2606
+ ```yaml
2607
+ settings:
2608
+ fail_on_severity:
2609
+ - error
2610
+ - critical
2611
+ parallel: true
2612
+
2613
+ # Only critical checks
2614
+ policy_structure:
2615
+ enabled: true
2616
+ severity: error
2617
+
2618
+ full_wildcard:
2619
+ enabled: true
2620
+ severity: critical
2621
+
2622
+ # Disable detailed checks for speed
2623
+ action_validation:
2624
+ enabled: false
2625
+
2626
+ sensitive_action:
2627
+ enabled: false
2628
+
2629
+ condition_key_validation:
2630
+ enabled: false
2631
+ ```
2632
+ """
2633
+
2634
+
2635
+ @mcp.resource("iam://workflow-examples")
2636
+ def workflow_examples_resource() -> str:
2637
+ """Detailed workflow examples for common IAM policy tasks.
2638
+
2639
+ This resource contains step-by-step examples showing how to use
2640
+ the IAM Policy Validator tools effectively.
2641
+ """
2642
+ return """
2643
+ # IAM Policy Validator - Workflow Examples
2644
+
2645
+ ## Example 1: Create Policy from Template
2646
+
2647
+ USER: "I need a policy for Lambda to read from S3"
2648
+
2649
+ STEPS:
2650
+ 1. list_templates → found "lambda-s3-trigger"
2651
+ 2. ASK USER: "What's your S3 bucket name?"
2652
+ 3. generate_policy_from_template(
2653
+ template_name="lambda-s3-trigger",
2654
+ variables={"bucket_name": "user-bucket", "function_name": "my-func", ...}
2655
+ )
2656
+ 4. validate_policy on result
2657
+ 5. Present validated policy to user
2658
+
2659
+ ## Example 2: Validate Overly Permissive Policy
2660
+
2661
+ USER: "Validate this policy: {Action: *, Resource: *}"
2662
+
2663
+ STEPS:
2664
+ 1. validate_policy → returns issues (wildcard_action, wildcard_resource)
2665
+ 2. fix_policy_issues → unfixed_issues shows wildcards can't be auto-fixed
2666
+ 3. RESPOND to user:
2667
+ "This policy grants full admin access. I need to know:
2668
+ - Which AWS service(s) do you need access to?
2669
+ - What operations (read/write/delete)?
2670
+ - Which specific resources (bucket names, table names, etc.)?"
2671
+
2672
+ ## Example 3: Build Custom Policy
2673
+
2674
+ USER: "Create a policy to read DynamoDB table 'users' and write to S3 bucket 'backups'"
2675
+
2676
+ STEPS:
2677
+ 1. suggest_actions("read DynamoDB", "dynamodb") → get read actions
2678
+ 2. suggest_actions("write S3", "s3") → get write actions
2679
+ 3. build_minimal_policy(
2680
+ actions=["dynamodb:GetItem", "dynamodb:Query", "s3:PutObject"],
2681
+ resources=[
2682
+ "arn:aws:dynamodb:us-east-1:123456789012:table/users",
2683
+ "arn:aws:s3:::backups/*"
2684
+ ]
2685
+ )
2686
+ 4. validate_policy on result
2687
+ 5. Review security_notes and present to user
2688
+
2689
+ ## Example 4: Fix Validation Issues
2690
+
2691
+ USER provides policy with issues
2692
+
2693
+ STEPS:
2694
+ 1. validate_policy → returns is_valid=false with issues
2695
+ 2. For each issue, read the `example` field - it shows the exact fix
2696
+ 3. fix_policy_issues → applies auto-fixes (Version, SIDs)
2697
+ 4. For remaining unfixed_issues:
2698
+ - If wildcard: ask user for specific actions/resources
2699
+ - If missing condition: use get_required_conditions to see what's needed
2700
+ 5. Re-validate until is_valid=true
2701
+
2702
+ ## Example 5: Research Actions
2703
+
2704
+ USER: "What S3 write actions exist?"
2705
+
2706
+ STEPS:
2707
+ 1. query_service_actions(service="s3", access_level="write")
2708
+ 2. Present the list to user
2709
+ 3. If they pick actions, use check_sensitive_actions to warn about risks
2710
+
2711
+ ## Example 6: Batch Validation
2712
+
2713
+ USER provides multiple policies to check
2714
+
2715
+ STEPS:
2716
+ 1. validate_policies_batch(policies=[...], verbose=False)
2717
+ 2. For each result, show policy_index and is_valid
2718
+ 3. Detail issues only for invalid policies
2719
+ """
2720
+
2721
+
2722
+ # =============================================================================
2723
+ # Prompts - Guided Workflows for LLM Clients
2724
+ # =============================================================================
2725
+
2726
+
2727
+ @mcp.prompt
2728
+ def generate_secure_policy(
2729
+ service: str,
2730
+ operations: str,
2731
+ resources: str,
2732
+ principal_type: str = "Lambda function",
2733
+ ) -> str:
2734
+ """Generate a secure IAM policy with proper validation.
2735
+
2736
+ This prompt guides you through creating a least-privilege IAM policy
2737
+ that passes all critical validation checks.
2738
+
2739
+ Args:
2740
+ service: AWS service (e.g., "s3", "dynamodb", "lambda")
2741
+ operations: What operations are needed (e.g., "read objects", "write items")
2742
+ resources: Specific resources (e.g., "bucket my-app-data", "table users")
2743
+ principal_type: Who needs access (e.g., "Lambda function", "EC2 instance")
2744
+ """
2745
+ return f"""Generate a secure IAM policy for the following requirement:
2746
+
2747
+ **Service**: {service}
2748
+ **Operations needed**: {operations}
2749
+ **Resources**: {resources}
2750
+ **Principal**: {principal_type}
2751
+
2752
+ ## WORKFLOW (Follow these steps in order):
2753
+
2754
+ ### Step 1: Find a Template
2755
+ Call `list_templates` to check if a pre-built secure template exists for {service}.
2756
+ If found, use `generate_policy_from_template` with the resource values.
2757
+
2758
+ ### Step 2: If No Template, Build Manually
2759
+ 1. Call `query_service_actions("{service}")` to find exact action names
2760
+ 2. Call `query_arn_formats("{service}")` to get correct ARN patterns
2761
+ 3. Call `build_minimal_policy` with the specific actions and resources
2762
+
2763
+ ### Step 3: Validate ONCE
2764
+ Call `validate_policy` on the generated policy.
2765
+
2766
+ ### Step 4: Fix Only BLOCKING Issues
2767
+ BLOCKING issues (MUST fix): severity = "error" or "critical"
2768
+ - Use the `example` field from the issue - it shows the exact fix
2769
+ - Apply the fix directly
2770
+
2771
+ NON-BLOCKING issues (present with warnings): severity = "high", "medium", "low", "warning"
2772
+ - Do NOT try to fix these automatically
2773
+ - Present them to the user as security recommendations
2774
+
2775
+ ### Step 5: Present the Policy
2776
+ Show the final policy with:
2777
+ 1. The complete JSON policy
2778
+ 2. Any non-blocking warnings as "Security Considerations"
2779
+ 3. Explanation of what permissions are granted
2780
+
2781
+ ⚠️ IMPORTANT: Do NOT validate more than once. Do NOT loop trying to fix warnings.
2782
+ """
2783
+
2784
+
2785
+ @mcp.prompt
2786
+ def fix_policy_issues_workflow(policy_json: str, issues_description: str) -> str:
2787
+ """Systematic workflow to fix IAM policy validation issues.
2788
+
2789
+ Use this prompt when you have a policy with validation issues and need
2790
+ to fix them systematically without getting into a loop.
2791
+
2792
+ Args:
2793
+ policy_json: The IAM policy JSON that has issues
2794
+ issues_description: Description of the issues found (from validate_policy)
2795
+ """
2796
+ return f"""Fix the following IAM policy issues systematically:
2797
+
2798
+ **Current Policy**:
2799
+ ```json
2800
+ {policy_json}
2801
+ ```
2802
+
2803
+ **Issues Found**:
2804
+ {issues_description}
2805
+
2806
+ ## FIX WORKFLOW (Maximum 2 iterations):
2807
+
2808
+ ### Iteration 1: Fix All BLOCKING Issues
2809
+ For each issue with severity "error" or "critical":
2810
+ 1. Read the `example` field - it shows exactly how to fix it
2811
+ 2. Apply the fix to the policy
2812
+ 3. For structural issues (Version, Effect case), use `fix_policy_issues` tool
2813
+
2814
+ ### After Fixing:
2815
+ Call `validate_policy` ONE more time to verify blocking issues are resolved.
2816
+
2817
+ ### Iteration 2 (only if needed):
2818
+ If new "error" or "critical" issues appeared, fix those.
2819
+ If only "high/medium/low/warning" issues remain, STOP fixing.
2820
+
2821
+ ## STOP CONDITIONS (Present policy when ANY is true):
2822
+ ✅ No "error" or "critical" issues remain
2823
+ ✅ You've done 2 fix iterations
2824
+ ✅ Remaining issues are "high", "medium", "low", or "warning" severity
2825
+ ✅ Issues require user input (e.g., "specify resource ARN")
2826
+
2827
+ ## Final Output:
2828
+ Present the policy with:
2829
+ 1. The fixed JSON
2830
+ 2. List of remaining warnings (if any) as "Security Recommendations"
2831
+ 3. Note: "These recommendations are informational. The policy is valid for AWS."
2832
+
2833
+ ⚠️ DO NOT keep iterating to eliminate warnings - they are advisory only.
2834
+ """
2835
+
2836
+
2837
+ @mcp.prompt
2838
+ def review_policy_security(policy_json: str) -> str:
2839
+ """Review an existing IAM policy for security issues.
2840
+
2841
+ Use this prompt to analyze a policy the user provides and give
2842
+ security recommendations without modifying it.
2843
+
2844
+ Args:
2845
+ policy_json: The IAM policy JSON to review
2846
+ """
2847
+ return f"""Review this IAM policy for security issues:
2848
+
2849
+ ```json
2850
+ {policy_json}
2851
+ ```
2852
+
2853
+ ## REVIEW WORKFLOW:
2854
+
2855
+ ### Step 1: Validate
2856
+ Call `validate_policy` with the policy above.
2857
+
2858
+ ### Step 2: Check Sensitive Actions
2859
+ Call `check_sensitive_actions` to identify high-risk permissions.
2860
+
2861
+ ### Step 3: Analyze Results
2862
+ Categorize issues by severity:
2863
+ - 🔴 CRITICAL/ERROR: Must be fixed before deployment
2864
+ - 🟠 HIGH: Strong recommendation to address
2865
+ - 🟡 MEDIUM/WARNING: Best practice suggestions
2866
+ - 🟢 LOW: Minor improvements
2867
+
2868
+ ### Step 4: Present Findings
2869
+ Format your response as:
2870
+
2871
+ **Policy Status**: [VALID / HAS BLOCKING ISSUES]
2872
+
2873
+ **Critical Issues** (must fix):
2874
+ - [List any error/critical issues with the fix from the `example` field]
2875
+
2876
+ **Security Recommendations** (should consider):
2877
+ - [List high/medium issues with explanations]
2878
+
2879
+ **Sensitive Actions Detected**:
2880
+ - [List any sensitive actions and their risk category]
2881
+
2882
+ **Overall Assessment**:
2883
+ [Brief summary of the policy's security posture]
2884
+
2885
+ ⚠️ Do NOT attempt to fix the policy unless the user asks. Just report findings.
2886
+ """
2887
+
2888
+
2889
+ # =============================================================================
2890
+ # Server Entry Points
2891
+ # =============================================================================
2892
+
2893
+
2894
+ def create_server() -> FastMCP:
2895
+ """Create and return the configured MCP server instance.
2896
+
2897
+ Returns:
2898
+ FastMCP: The configured MCP server with all tools registered
2899
+ """
2900
+ return mcp
2901
+
2902
+
2903
+ def run_server() -> None:
2904
+ """Run the MCP server.
2905
+
2906
+ This is the entry point for the iam-validator-mcp command.
2907
+ Uses stdio transport by default for Claude Desktop integration.
2908
+
2909
+ Custom instructions are loaded from:
2910
+ 1. Environment variable: IAM_VALIDATOR_MCP_INSTRUCTIONS
2911
+ 2. Config file: custom_instructions key in YAML config
2912
+ 3. CLI: --instructions or --instructions-file arguments
2913
+
2914
+ These are appended to the default instructions.
2915
+ """
2916
+ from iam_validator.mcp.session_config import CustomInstructionsManager
2917
+
2918
+ # Try to load custom instructions from environment if not already set
2919
+ if not CustomInstructionsManager.has_instructions():
2920
+ CustomInstructionsManager.load_from_env()
2921
+
2922
+ # Apply custom instructions if any
2923
+ mcp.instructions = get_instructions()
2924
+
2925
+ mcp.run()
2926
+
2927
+
2928
+ __all__ = ["mcp", "create_server", "run_server", "get_instructions", "BASE_INSTRUCTIONS"]