iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
- iam_policy_validator-1.15.1.dist-info/entry_points.txt +4 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_validation.py +91 -27
- iam_validator/checks/not_action_not_resource.py +163 -0
- iam_validator/checks/resource_validation.py +132 -81
- iam_validator/checks/wildcard_resource.py +136 -6
- iam_validator/commands/__init__.py +3 -0
- iam_validator/commands/cache.py +66 -24
- iam_validator/commands/completion.py +94 -15
- iam_validator/commands/mcp.py +210 -0
- iam_validator/commands/query.py +489 -65
- iam_validator/core/aws_service/__init__.py +5 -1
- iam_validator/core/aws_service/cache.py +20 -0
- iam_validator/core/aws_service/fetcher.py +180 -11
- iam_validator/core/aws_service/storage.py +14 -6
- iam_validator/core/aws_service/validators.py +68 -51
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +18 -9
- iam_validator/core/config/check_documentation.py +104 -51
- iam_validator/core/config/config_loader.py +39 -3
- iam_validator/core/config/defaults.py +6 -0
- iam_validator/core/constants.py +11 -4
- iam_validator/core/models.py +39 -14
- iam_validator/mcp/__init__.py +162 -0
- iam_validator/mcp/models.py +118 -0
- iam_validator/mcp/server.py +2928 -0
- iam_validator/mcp/session_config.py +319 -0
- iam_validator/mcp/templates/__init__.py +79 -0
- iam_validator/mcp/templates/builtin.py +856 -0
- iam_validator/mcp/tools/__init__.py +72 -0
- iam_validator/mcp/tools/generation.py +888 -0
- iam_validator/mcp/tools/org_config_tools.py +263 -0
- iam_validator/mcp/tools/query.py +395 -0
- iam_validator/mcp/tools/validation.py +376 -0
- iam_validator/sdk/__init__.py +2 -0
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.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"]
|