iam-policy-validator 1.14.7__py3-none-any.whl → 1.15.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
  2. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
  3. iam_policy_validator-1.15.1.dist-info/entry_points.txt +4 -0
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +2 -0
  6. iam_validator/checks/action_validation.py +91 -27
  7. iam_validator/checks/not_action_not_resource.py +163 -0
  8. iam_validator/checks/resource_validation.py +132 -81
  9. iam_validator/checks/wildcard_resource.py +136 -6
  10. iam_validator/commands/__init__.py +3 -0
  11. iam_validator/commands/cache.py +66 -24
  12. iam_validator/commands/completion.py +94 -15
  13. iam_validator/commands/mcp.py +210 -0
  14. iam_validator/commands/query.py +489 -65
  15. iam_validator/core/aws_service/__init__.py +5 -1
  16. iam_validator/core/aws_service/cache.py +20 -0
  17. iam_validator/core/aws_service/fetcher.py +180 -11
  18. iam_validator/core/aws_service/storage.py +14 -6
  19. iam_validator/core/aws_service/validators.py +68 -51
  20. iam_validator/core/check_registry.py +100 -35
  21. iam_validator/core/config/aws_global_conditions.py +18 -9
  22. iam_validator/core/config/check_documentation.py +104 -51
  23. iam_validator/core/config/config_loader.py +39 -3
  24. iam_validator/core/config/defaults.py +6 -0
  25. iam_validator/core/constants.py +11 -4
  26. iam_validator/core/models.py +39 -14
  27. iam_validator/mcp/__init__.py +162 -0
  28. iam_validator/mcp/models.py +118 -0
  29. iam_validator/mcp/server.py +2928 -0
  30. iam_validator/mcp/session_config.py +319 -0
  31. iam_validator/mcp/templates/__init__.py +79 -0
  32. iam_validator/mcp/templates/builtin.py +856 -0
  33. iam_validator/mcp/tools/__init__.py +72 -0
  34. iam_validator/mcp/tools/generation.py +888 -0
  35. iam_validator/mcp/tools/org_config_tools.py +263 -0
  36. iam_validator/mcp/tools/query.py +395 -0
  37. iam_validator/mcp/tools/validation.py +376 -0
  38. iam_validator/sdk/__init__.py +2 -0
  39. iam_validator/sdk/policy_utils.py +31 -5
  40. iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
  41. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
  42. {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,888 @@
1
+ """Policy generation tools for MCP server.
2
+
3
+ This module provides MCP tools for generating IAM policies from templates,
4
+ explicit actions, and natural language descriptions. All generated policies
5
+ are validated and optionally enriched with security conditions.
6
+ """
7
+
8
+ import asyncio
9
+ import functools
10
+ import re
11
+ from typing import Any
12
+
13
+ from iam_validator.core.aws_service import AWSServiceFetcher
14
+ from iam_validator.core.config.category_suggestions import DEFAULT_CATEGORY_SUGGESTIONS
15
+ from iam_validator.core.config.check_documentation import CheckDocumentationRegistry
16
+ from iam_validator.core.config.sensitive_actions import SENSITIVE_ACTION_CATEGORIES
17
+ from iam_validator.mcp.models import GenerationResult, ValidationResult
18
+ from iam_validator.sdk import query_actions
19
+
20
+ # Pre-build action→category index at module load time for O(1) lookups
21
+ _ACTION_CATEGORY_INDEX: dict[str, str] = {}
22
+ for _category_id, _category_data in SENSITIVE_ACTION_CATEGORIES.items():
23
+ for _action in _category_data["actions"]:
24
+ _ACTION_CATEGORY_INDEX[_action] = _category_id
25
+
26
+ # Pre-compiled regex pattern cache for condition requirement matching
27
+ _COMPILED_PATTERNS: dict[str, re.Pattern[str]] = {}
28
+
29
+
30
+ def _get_category_for_action(action: str) -> str | None:
31
+ """Get the category for a sensitive action using pre-built index.
32
+
33
+ Args:
34
+ action: AWS action name to check
35
+
36
+ Returns:
37
+ Category name if action is sensitive, None otherwise
38
+ """
39
+ return _ACTION_CATEGORY_INDEX.get(action)
40
+
41
+
42
+ def _get_compiled_pattern(pattern: str) -> re.Pattern[str]:
43
+ """Get a compiled regex pattern, using cache for efficiency."""
44
+ if pattern not in _COMPILED_PATTERNS:
45
+ _COMPILED_PATTERNS[pattern] = re.compile(pattern)
46
+ return _COMPILED_PATTERNS[pattern]
47
+
48
+
49
+ def _get_auto_conditions(actions: list[str]) -> tuple[dict[str, Any], list[str]]:
50
+ """Get auto-applied conditions based on action requirements.
51
+
52
+ Analyzes the actions list against CONDITION_REQUIREMENTS and returns
53
+ conditions that should be automatically applied along with explanatory notes.
54
+
55
+ Args:
56
+ actions: List of AWS actions to analyze
57
+
58
+ Returns:
59
+ Tuple of (conditions_dict, notes_list) where:
60
+ - conditions_dict: Dictionary of conditions to apply
61
+ - notes_list: List of strings explaining what was auto-added
62
+ """
63
+ from iam_validator.core.config.condition_requirements import CONDITION_REQUIREMENTS
64
+
65
+ auto_conditions: dict[str, Any] = {}
66
+ notes: list[str] = []
67
+
68
+ for action in actions:
69
+ for requirement in CONDITION_REQUIREMENTS:
70
+ # Check if this requirement applies to this action
71
+ action_matches = False
72
+
73
+ # Check direct action match
74
+ if "actions" in requirement and action in requirement["actions"]:
75
+ action_matches = True
76
+
77
+ # Check pattern match using pre-compiled regex
78
+ if not action_matches and "action_patterns" in requirement:
79
+ for pattern in requirement["action_patterns"]:
80
+ if _get_compiled_pattern(pattern).match(action):
81
+ action_matches = True
82
+ break
83
+
84
+ if not action_matches:
85
+ continue
86
+
87
+ # This requirement applies - extract conditions
88
+ required_conditions = requirement.get("required_conditions", [])
89
+
90
+ # Handle list of conditions (simple case)
91
+ if isinstance(required_conditions, list):
92
+ for cond in required_conditions:
93
+ condition_key = cond.get("condition_key")
94
+ expected_value = cond.get("expected_value")
95
+ description = cond.get("description", "")
96
+
97
+ if condition_key:
98
+ if expected_value is not None:
99
+ # We have a specific value - auto-add the condition
100
+ # Determine the operator based on value type
101
+ if isinstance(expected_value, bool):
102
+ operator = "Bool"
103
+ value = "true" if expected_value else "false"
104
+ elif isinstance(expected_value, str):
105
+ if expected_value.startswith("${"):
106
+ # Policy variable - use StringEquals
107
+ operator = "StringEquals"
108
+ value = expected_value
109
+ else:
110
+ operator = "StringEquals"
111
+ value = expected_value
112
+ else:
113
+ # Default to StringEquals for other types
114
+ operator = "StringEquals"
115
+ value = str(expected_value)
116
+
117
+ # Add to auto_conditions
118
+ if operator not in auto_conditions:
119
+ auto_conditions[operator] = {}
120
+ auto_conditions[operator][condition_key] = value
121
+
122
+ # Add note
123
+ note_desc = (
124
+ description if description else f"Required for {condition_key}"
125
+ )
126
+ notes.append(f"Auto-added {condition_key} for {action}: {note_desc}")
127
+ else:
128
+ # No expected_value - add recommendation note only
129
+ note_desc = (
130
+ description if description else f"Consider adding {condition_key}"
131
+ )
132
+ notes.append(f"Recommendation for {action}: {note_desc}")
133
+
134
+ # Handle complex conditions with any_of/none_of
135
+ elif isinstance(required_conditions, dict):
136
+ # For any_of, we apply the first option (most common pattern)
137
+ if "any_of" in required_conditions:
138
+ options = required_conditions["any_of"]
139
+ if options:
140
+ # Apply the first option (typically the strongest control)
141
+ first_option = options[0]
142
+ condition_key = first_option.get("condition_key")
143
+ expected_value = first_option.get("expected_value")
144
+ description = first_option.get("description", "")
145
+
146
+ if condition_key and expected_value is not None:
147
+ # Determine operator
148
+ if isinstance(expected_value, bool):
149
+ operator = "Bool"
150
+ value = "true" if expected_value else "false"
151
+ elif isinstance(expected_value, str):
152
+ operator = "StringEquals"
153
+ value = expected_value
154
+ else:
155
+ operator = "StringEquals"
156
+ value = str(expected_value)
157
+
158
+ # Add to auto_conditions
159
+ if operator not in auto_conditions:
160
+ auto_conditions[operator] = {}
161
+ auto_conditions[operator][condition_key] = value
162
+
163
+ # Add note with "one of" context
164
+ notes.append(
165
+ f"Auto-added {condition_key} for {action} (one of {len(options)} options): {description}"
166
+ )
167
+
168
+ # For none_of, we skip auto-adding (validation will catch violations)
169
+ # These are negative conditions that prevent bad configs
170
+
171
+ return auto_conditions, notes
172
+
173
+
174
+ async def generate_policy_from_template(
175
+ template_name: str,
176
+ variables: dict[str, str],
177
+ config_path: str | None = None,
178
+ ) -> GenerationResult:
179
+ """Generate an IAM policy from a built-in template.
180
+
181
+ This tool loads a pre-defined policy template, substitutes the provided
182
+ variables, and validates the generated policy using the IAM validator's
183
+ built-in checks. Any security issues are reported through validation results.
184
+
185
+ Args:
186
+ template_name: Name of the template to use. Available templates:
187
+ - s3-read-only: S3 bucket read-only access
188
+ - s3-read-write: S3 bucket read-write access
189
+ - lambda-basic-execution: Basic Lambda execution role
190
+ - lambda-s3-trigger: Lambda with S3 event trigger permissions
191
+ - dynamodb-crud: DynamoDB table CRUD operations
192
+ - cloudwatch-logs: CloudWatch Logs write permissions
193
+ - secrets-manager-read: Secrets Manager read access
194
+ - kms-encrypt-decrypt: KMS key encryption/decryption
195
+ - ec2-describe: EC2 describe-only permissions
196
+ - ecs-task-execution: ECS task execution role
197
+ variables: Dictionary of variable values to substitute in the template.
198
+ Required variables depend on the template (see list_templates).
199
+ config_path: Optional path to YAML configuration file for validation.
200
+ Uses the same config format as the CLI validator.
201
+
202
+ Returns:
203
+ GenerationResult with:
204
+ - policy: The generated IAM policy
205
+ - validation: Validation results from built-in checks
206
+ - security_notes: Security warnings from validation
207
+ - template_used: Name of the template used
208
+
209
+ Raises:
210
+ ValueError: If template not found or required variables missing
211
+
212
+ Example:
213
+ >>> result = await generate_policy_from_template(
214
+ ... template_name="s3-read-only",
215
+ ... variables={"bucket_name": "my-data", "prefix": "logs/"}
216
+ ... )
217
+ >>> print(result.policy)
218
+ """
219
+ from iam_validator.mcp.templates import load_template
220
+ from iam_validator.mcp.tools.validation import validate_policy
221
+
222
+ # Load and substitute template
223
+ policy = load_template(template_name, variables)
224
+
225
+ # Validate the generated policy using the validator's built-in checks
226
+ validation_result = await validate_policy(
227
+ policy=policy,
228
+ policy_type="identity",
229
+ config_path=config_path,
230
+ use_org_config=False, # Config is passed explicitly via config_path
231
+ )
232
+
233
+ # Extract security notes from validation issues
234
+ security_notes: list[str] = []
235
+ for issue in validation_result.issues:
236
+ if issue.severity in ("high", "critical", "error"):
237
+ security_notes.append(f"{issue.severity.upper()}: {issue.message}")
238
+
239
+ return GenerationResult(
240
+ policy=policy,
241
+ validation=ValidationResult(
242
+ is_valid=validation_result.is_valid,
243
+ issues=validation_result.issues,
244
+ policy_file=validation_result.policy_file,
245
+ ),
246
+ security_notes=security_notes,
247
+ template_used=template_name,
248
+ )
249
+
250
+
251
+ async def build_minimal_policy(
252
+ actions: list[str],
253
+ resources: list[str],
254
+ conditions: dict[str, Any] | None = None,
255
+ config_path: str | None = None,
256
+ fetcher: AWSServiceFetcher | None = None,
257
+ ) -> GenerationResult:
258
+ """Build a minimal IAM policy from explicit actions and resources.
259
+
260
+ This tool constructs a policy statement from the provided actions and resources.
261
+ It validates that actions exist in AWS, checks for sensitive actions, and
262
+ validates the generated policy using the validator's built-in checks.
263
+
264
+ Args:
265
+ actions: List of AWS actions to allow (e.g., ["s3:GetObject", "s3:ListBucket"])
266
+ resources: List of resource ARNs (e.g., ["arn:aws:s3:::my-bucket/*"])
267
+ conditions: Optional conditions to add to the statement
268
+ config_path: Optional path to YAML configuration file for validation.
269
+ Uses the same config format as the CLI validator.
270
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
271
+
272
+ Returns:
273
+ GenerationResult with:
274
+ - policy: The generated IAM policy
275
+ - validation: Validation results from built-in checks
276
+ - security_notes: Security warnings from validation
277
+
278
+ Example:
279
+ >>> result = await build_minimal_policy(
280
+ ... actions=["s3:GetObject", "s3:ListBucket"],
281
+ ... resources=["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"]
282
+ ... )
283
+ >>> print(result.policy)
284
+ """
285
+ from iam_validator.core.models import ValidationIssue
286
+
287
+ security_notes: list[str] = []
288
+ effective_conditions = conditions.copy() if conditions else {}
289
+
290
+ # Use provided fetcher or create a new one
291
+ if fetcher is not None:
292
+ # Use shared fetcher directly (no context manager)
293
+ _fetcher = fetcher
294
+ should_close = False
295
+ else:
296
+ # Create new fetcher with context manager
297
+ _fetcher = AWSServiceFetcher()
298
+ await _fetcher.__aenter__()
299
+ should_close = True
300
+
301
+ try:
302
+ # Separate wildcard and exact actions for validation
303
+ wildcard_actions = []
304
+ exact_actions = []
305
+ for action in actions:
306
+ if "*" in action:
307
+ if action == "*":
308
+ # Block bare wildcard
309
+ return GenerationResult(
310
+ policy={},
311
+ validation=ValidationResult(
312
+ is_valid=False,
313
+ issues=[
314
+ ValidationIssue(
315
+ severity="error",
316
+ statement_index=0,
317
+ issue_type="bare_wildcard_not_allowed",
318
+ message="Action: '*' is not allowed in generated policies",
319
+ suggestion="Specify explicit actions instead of using wildcard",
320
+ check_id="policy_generation",
321
+ )
322
+ ],
323
+ policy_file="generated-policy",
324
+ ),
325
+ security_notes=["Policy generation blocked: bare wildcard action detected"],
326
+ )
327
+ wildcard_actions.append(action)
328
+ else:
329
+ exact_actions.append(action)
330
+
331
+ # Validate wildcard actions (must be done individually - expand each)
332
+ for action in wildcard_actions:
333
+ try:
334
+ await _fetcher.expand_wildcard_action(action)
335
+ except Exception:
336
+ # Invalid wildcard
337
+ return GenerationResult(
338
+ policy={},
339
+ validation=ValidationResult(
340
+ is_valid=False,
341
+ issues=[
342
+ ValidationIssue(
343
+ severity="error",
344
+ statement_index=0,
345
+ issue_type="invalid_wildcard_action",
346
+ message=f"Wildcard action '{action}' cannot be expanded to valid actions",
347
+ suggestion="Verify the action pattern is correct",
348
+ check_id="policy_generation",
349
+ )
350
+ ],
351
+ policy_file="generated-policy",
352
+ ),
353
+ security_notes=[],
354
+ )
355
+
356
+ # Batch validate exact actions (more efficient - fetches each service once)
357
+ if exact_actions:
358
+ validation_results = await _fetcher.validate_actions_batch(exact_actions)
359
+ for action, (is_valid, error, _) in validation_results.items():
360
+ if not is_valid:
361
+ return GenerationResult(
362
+ policy={},
363
+ validation=ValidationResult(
364
+ is_valid=False,
365
+ issues=[
366
+ ValidationIssue(
367
+ severity="error",
368
+ statement_index=0,
369
+ issue_type="invalid_action",
370
+ message=f"Action '{action}' is not valid: {error}",
371
+ suggestion="Verify the action name is correct",
372
+ check_id="policy_generation",
373
+ )
374
+ ],
375
+ policy_file="generated-policy",
376
+ ),
377
+ security_notes=[],
378
+ )
379
+
380
+ # Check for bare Resource: "*" with write actions
381
+ if "*" in resources:
382
+ # Check if any actions are write-level
383
+ has_write_actions = False
384
+ for action in actions:
385
+ if "*" not in action:
386
+ try:
387
+ # Check access level
388
+ service = action.split(":")[0]
389
+ action_list = await query_actions(_fetcher, service, access_level="write")
390
+ if any(a["action"] == action for a in action_list):
391
+ has_write_actions = True
392
+ break
393
+ except Exception:
394
+ pass
395
+
396
+ if has_write_actions:
397
+ return GenerationResult(
398
+ policy={},
399
+ validation=ValidationResult(
400
+ is_valid=False,
401
+ issues=[
402
+ ValidationIssue(
403
+ severity="error",
404
+ statement_index=0,
405
+ issue_type="bare_wildcard_resource_not_allowed",
406
+ message="Resource: '*' with write actions is not allowed",
407
+ suggestion="Specify explicit resource ARNs instead of using wildcard",
408
+ check_id="policy_generation",
409
+ )
410
+ ],
411
+ policy_file="generated-policy",
412
+ ),
413
+ security_notes=[
414
+ "Policy generation blocked: bare wildcard resource with write actions"
415
+ ],
416
+ )
417
+
418
+ # Check for sensitive actions and add warnings
419
+ sensitive_action_notes: list[dict[str, Any]] = []
420
+ for action in actions:
421
+ if "*" not in action:
422
+ category = _get_category_for_action(action)
423
+ if category:
424
+ category_data = SENSITIVE_ACTION_CATEGORIES[category]
425
+ sensitive_action_notes.append(
426
+ {
427
+ "action": action,
428
+ "category": category,
429
+ "severity": category_data["severity"],
430
+ "description": category_data["description"],
431
+ }
432
+ )
433
+ security_notes.append(
434
+ f"Warning: '{action}' is a sensitive action ({category_data['name']})"
435
+ )
436
+
437
+ # Auto-add required conditions based on CONDITION_REQUIREMENTS
438
+ auto_conditions, auto_notes = _get_auto_conditions(actions)
439
+ if auto_conditions:
440
+ # Merge auto-conditions into effective_conditions
441
+ from iam_validator.mcp.session_config import merge_conditions
442
+
443
+ effective_conditions = merge_conditions(effective_conditions, auto_conditions)
444
+ # Add notes about what was auto-added
445
+ security_notes.extend(auto_notes)
446
+
447
+ # Group actions by service for cleaner policy structure
448
+ actions_by_service: dict[str, list[str]] = {}
449
+ for action in actions:
450
+ service = action.split(":")[0]
451
+ if service not in actions_by_service:
452
+ actions_by_service[service] = []
453
+ actions_by_service[service].append(action)
454
+
455
+ # Build the policy
456
+ statement: dict[str, Any] = {
457
+ "Sid": "GeneratedPolicy",
458
+ "Effect": "Allow",
459
+ "Action": sorted(actions), # Keep all actions in one statement for now
460
+ "Resource": resources if isinstance(resources, list) else [resources],
461
+ }
462
+
463
+ # Add conditions if provided or auto-generated
464
+ if effective_conditions:
465
+ statement["Condition"] = effective_conditions
466
+
467
+ policy: dict[str, Any] = {
468
+ "Version": "2012-10-17",
469
+ "Statement": [statement],
470
+ }
471
+
472
+ # Validate the generated policy using the validator's built-in checks
473
+ from iam_validator.mcp.tools.validation import validate_policy
474
+
475
+ validation_result = await validate_policy(
476
+ policy=policy,
477
+ policy_type="identity",
478
+ config_path=config_path,
479
+ use_org_config=False, # Config is passed explicitly via config_path
480
+ )
481
+
482
+ # Add high-severity issues to security notes
483
+ for issue in validation_result.issues:
484
+ if issue.severity in ("high", "critical", "error"):
485
+ security_notes.append(f"{issue.severity.upper()}: {issue.message}")
486
+
487
+ return GenerationResult(
488
+ policy=policy,
489
+ validation=ValidationResult(
490
+ is_valid=validation_result.is_valid,
491
+ issues=validation_result.issues,
492
+ policy_file=validation_result.policy_file,
493
+ ),
494
+ security_notes=security_notes,
495
+ )
496
+ finally:
497
+ # Clean up fetcher if we created it
498
+ if should_close:
499
+ await _fetcher.__aexit__(None, None, None)
500
+
501
+
502
+ @functools.lru_cache(maxsize=1)
503
+ def _get_cached_templates() -> tuple[dict[str, Any], ...]:
504
+ """Build template list once, return tuple for immutability.
505
+
506
+ This helper is cached with lru_cache to avoid rebuilding
507
+ the template list on every call to list_templates().
508
+
509
+ Returns:
510
+ Tuple of template dictionaries (immutable for caching)
511
+ """
512
+ from iam_validator.mcp.templates.builtin import (
513
+ list_templates as get_templates_metadata,
514
+ )
515
+
516
+ templates_metadata = get_templates_metadata()
517
+ return tuple(
518
+ {
519
+ "name": tmpl["name"],
520
+ "description": tmpl["description"],
521
+ "variables": [
522
+ {
523
+ "name": var["name"],
524
+ "description": var["description"],
525
+ "required": var.get("required", True),
526
+ }
527
+ for var in tmpl["variables"]
528
+ ],
529
+ }
530
+ for tmpl in templates_metadata
531
+ )
532
+
533
+
534
+ async def list_templates() -> list[dict[str, Any]]:
535
+ """List all available policy templates with their metadata.
536
+
537
+ Returns:
538
+ List of template dictionaries, each containing:
539
+ - name: Template identifier (use with generate_policy_from_template)
540
+ - description: Human-readable description
541
+ - variables: List of variable objects with:
542
+ - name: Variable name to use in the variables dict
543
+ - description: What value to provide
544
+ - required: Whether the variable is required
545
+
546
+ Example:
547
+ >>> templates = await list_templates()
548
+ >>> for tmpl in templates:
549
+ ... print(f"{tmpl['name']}: {tmpl['description']}")
550
+ ... for var in tmpl['variables']:
551
+ ... print(f" - {var['name']}: {var['description']}")
552
+ """
553
+ return list(_get_cached_templates())
554
+
555
+
556
+ async def suggest_actions(
557
+ description: str, service: str | None = None, fetcher: AWSServiceFetcher | None = None
558
+ ) -> list[str]:
559
+ """Suggest AWS actions based on a natural language description.
560
+
561
+ This tool uses keyword pattern matching to suggest appropriate AWS actions
562
+ based on the task description. It's useful for discovering actions when
563
+ building policies from scratch.
564
+
565
+ Args:
566
+ description: Natural language description of the desired permissions
567
+ (e.g., "read files from S3", "invoke Lambda functions")
568
+ service: Optional AWS service to limit suggestions to (e.g., "s3", "lambda")
569
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
570
+
571
+ Returns:
572
+ List of suggested action names
573
+
574
+ Example:
575
+ >>> actions = await suggest_actions("read and write DynamoDB tables", "dynamodb")
576
+ >>> print(actions)
577
+ ['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem', ...]
578
+ """
579
+ description_lower = description.lower()
580
+
581
+ # Keyword mapping for access levels
582
+ access_level_keywords = {
583
+ "read": ["read", "get", "describe", "view", "download", "retrieve", "fetch"],
584
+ "write": [
585
+ "write",
586
+ "put",
587
+ "create",
588
+ "update",
589
+ "modify",
590
+ "upload",
591
+ "edit",
592
+ "change",
593
+ ],
594
+ "list": ["list", "enumerate", "browse", "search", "query", "scan"],
595
+ "tagging": ["tag", "untag", "label"],
596
+ "permissions-management": [
597
+ "permission",
598
+ "policy",
599
+ "grant",
600
+ "revoke",
601
+ "attach",
602
+ "detach",
603
+ ],
604
+ }
605
+
606
+ # Determine which access levels match the description
607
+ matched_access_levels = []
608
+ for access_level, keywords in access_level_keywords.items():
609
+ if any(keyword in description_lower for keyword in keywords):
610
+ matched_access_levels.append(access_level)
611
+
612
+ # If no specific access level matched, default to read + list
613
+ if not matched_access_levels:
614
+ matched_access_levels = ["read", "list"]
615
+
616
+ # Service detection from description if not provided
617
+ if service is None:
618
+ service_keywords = {
619
+ "s3": ["s3", "bucket", "object", "file", "storage"],
620
+ "lambda": ["lambda", "function", "invoke"],
621
+ "dynamodb": ["dynamodb", "table", "item", "nosql"],
622
+ "ec2": ["ec2", "instance", "vm", "virtual machine"],
623
+ "iam": ["iam", "user", "role", "permission", "policy"],
624
+ "cloudwatch": ["cloudwatch", "log", "metric", "monitoring"],
625
+ "secretsmanager": ["secret", "credential", "password"],
626
+ "kms": ["kms", "encrypt", "decrypt", "key"],
627
+ "rds": ["rds", "database", "db", "sql"],
628
+ "sns": ["sns", "notification", "topic", "publish"],
629
+ "sqs": ["sqs", "queue", "message"],
630
+ }
631
+
632
+ for svc, keywords in service_keywords.items():
633
+ if any(keyword in description_lower for keyword in keywords):
634
+ service = svc
635
+ break
636
+
637
+ if service is None:
638
+ # No service detected, return empty list
639
+ return []
640
+
641
+ # Use provided fetcher or create a new one
642
+ if fetcher is not None:
643
+ # Use shared fetcher directly
644
+ _fetcher = fetcher
645
+ should_close = False
646
+ else:
647
+ # Create new fetcher with context manager
648
+ _fetcher = AWSServiceFetcher()
649
+ await _fetcher.__aenter__()
650
+ should_close = True
651
+
652
+ try:
653
+ # Query all access levels in parallel for better performance
654
+ async def query_level(level: str) -> list[str]:
655
+ try:
656
+ actions = await query_actions(
657
+ _fetcher,
658
+ service,
659
+ access_level=level, # type: ignore
660
+ )
661
+ return [a["action"] for a in actions]
662
+ except Exception:
663
+ # Service might not exist or other error
664
+ return []
665
+
666
+ results = await asyncio.gather(*[query_level(level) for level in matched_access_levels])
667
+ # Flatten results and deduplicate using a set
668
+ suggested_actions: set[str] = set()
669
+ for result in results:
670
+ suggested_actions.update(result)
671
+ finally:
672
+ # Clean up fetcher if we created it
673
+ if should_close:
674
+ await _fetcher.__aexit__(None, None, None)
675
+
676
+ # Return sorted list
677
+ return sorted(suggested_actions)
678
+
679
+
680
+ async def get_required_conditions(actions: list[str]) -> dict[str, Any]:
681
+ """Get the conditions required for a list of actions.
682
+
683
+ This tool looks up condition requirements from the IAM Policy Validator's
684
+ configuration and returns the requirements for the given actions.
685
+
686
+ Args:
687
+ actions: List of AWS actions to analyze
688
+
689
+ Returns:
690
+ Dictionary with:
691
+ - requirements: List of condition requirements from the validator config
692
+ - actions_matched: Which actions matched requirements
693
+ - summary: Human-readable summary of what conditions are needed
694
+
695
+ Example:
696
+ >>> conditions = await get_required_conditions(["iam:PassRole", "s3:GetObject"])
697
+ >>> print(conditions["summary"])
698
+ """
699
+ from iam_validator.core.config.condition_requirements import CONDITION_REQUIREMENTS
700
+
701
+ matched_requirements: list[dict[str, Any]] = []
702
+ actions_matched: list[str] = []
703
+
704
+ for action in actions:
705
+ for requirement in CONDITION_REQUIREMENTS:
706
+ # Check direct action match
707
+ if "actions" in requirement and action in requirement["actions"]:
708
+ matched_requirements.append(requirement)
709
+ actions_matched.append(action)
710
+ break
711
+
712
+ # Check pattern match
713
+ if "action_patterns" in requirement:
714
+ for pattern in requirement["action_patterns"]:
715
+ if re.match(pattern, action):
716
+ matched_requirements.append(requirement)
717
+ actions_matched.append(action)
718
+ break
719
+
720
+ # Build a summary
721
+ summary_parts = []
722
+ for req in matched_requirements:
723
+ if "suggestion_text" in req:
724
+ summary_parts.append(req["suggestion_text"])
725
+
726
+ return {
727
+ "requirements": matched_requirements,
728
+ "actions_matched": list(set(actions_matched)),
729
+ "summary": "\n\n".join(summary_parts)
730
+ if summary_parts
731
+ else "No specific condition requirements found for these actions.",
732
+ }
733
+
734
+
735
+ def _get_remediation_from_validator(action: str, category: str) -> dict[str, Any]:
736
+ """Get remediation guidance for a sensitive action from the validator's data.
737
+
738
+ Uses the IAM validator's category_suggestions module for ABAC-focused
739
+ remediation guidance, and CheckDocumentationRegistry for general check documentation.
740
+
741
+ Args:
742
+ action: AWS action name
743
+ category: Risk category (credential_exposure, data_access, priv_esc, resource_exposure)
744
+
745
+ Returns:
746
+ Dictionary with remediation guidance from the validator
747
+ """
748
+ # Get category suggestions from validator (ABAC-focused guidance)
749
+ category_suggestions = DEFAULT_CATEGORY_SUGGESTIONS.get(category, {})
750
+
751
+ # Check for action-specific override first
752
+ action_overrides = category_suggestions.get("action_overrides", {})
753
+ if action in action_overrides:
754
+ override = action_overrides[action]
755
+ suggestion = override.get("suggestion", "")
756
+ example = override.get("example", "")
757
+ else:
758
+ # Fall back to category-level guidance
759
+ suggestion = category_suggestions.get("suggestion", "")
760
+ example = category_suggestions.get("example", "")
761
+
762
+ # Get check documentation from the validator's registry
763
+ check_doc = CheckDocumentationRegistry.get("sensitive_action")
764
+ remediation_steps = check_doc.remediation_steps if check_doc else []
765
+ documentation_url = check_doc.documentation_url if check_doc else None
766
+ risk_explanation = check_doc.risk_explanation if check_doc else None
767
+
768
+ # Determine risk level from category severity
769
+ category_data = SENSITIVE_ACTION_CATEGORIES.get(category, {})
770
+ risk_level = "CRITICAL" if category_data.get("severity") == "critical" else "HIGH"
771
+
772
+ return {
773
+ "risk_level": risk_level,
774
+ "suggestion": suggestion,
775
+ "condition_example": example,
776
+ "remediation_steps": remediation_steps,
777
+ "documentation_url": documentation_url,
778
+ "risk_explanation": risk_explanation,
779
+ }
780
+
781
+
782
+ async def check_sensitive_actions(actions: list[str]) -> dict[str, Any]:
783
+ """Check if any actions in the list are sensitive and get remediation guidance.
784
+
785
+ This tool analyzes actions against the IAM Policy Validator's sensitive
786
+ actions catalog and returns remediation guidance sourced directly from
787
+ the validator's configuration.
788
+
789
+ The sensitive actions catalog contains 490+ actions across 4 categories,
790
+ sourced from https://github.com/primeharbor/sensitive_iam_actions
791
+
792
+ IMPORTANT FOR AI CLIENTS: To fix sensitive action findings:
793
+ 1. Add the suggested IAM conditions to your policy statement
794
+ 2. The condition_example field contains ready-to-use JSON
795
+ 3. After adding conditions, re-validate to confirm the fix
796
+ 4. If issues persist, the action may need additional restrictions
797
+
798
+ Args:
799
+ actions: List of AWS actions to check
800
+
801
+ Returns:
802
+ Dictionary containing:
803
+ - sensitive_actions: List of sensitive actions with remediation
804
+ - action: The action name
805
+ - category: Risk category
806
+ - severity: critical or high
807
+ - description: Category description
808
+ - remediation: Guidance from the IAM validator including:
809
+ - risk_level: CRITICAL or HIGH
810
+ - suggestion: ABAC-focused guidance on what conditions to add
811
+ - condition_example: Ready-to-use JSON condition block
812
+ - remediation_steps: Step-by-step fix guidance
813
+ - documentation_url: AWS documentation link
814
+ - total_checked: Number of actions checked
815
+ - sensitive_count: Number of sensitive actions found
816
+ - categories_found: List of unique risk categories
817
+ - has_critical: Whether any CRITICAL actions were found
818
+ - summary: Quick summary with key recommendations
819
+ - fix_guidance: Clear instructions for AI clients on how to resolve
820
+
821
+ Example:
822
+ >>> result = await check_sensitive_actions(["iam:PassRole", "s3:GetObject"])
823
+ >>> for item in result["sensitive_actions"]:
824
+ ... print(f"Action: {item['action']}")
825
+ ... print(f"Fix: Add this condition block:")
826
+ ... print(item['remediation']['condition_example'])
827
+ """
828
+ sensitive_actions_found: list[dict[str, Any]] = []
829
+ categories_found: set[str] = set()
830
+ has_critical = False
831
+
832
+ for action in actions:
833
+ category = _get_category_for_action(action)
834
+ if category:
835
+ category_data = SENSITIVE_ACTION_CATEGORIES[category]
836
+ categories_found.add(category)
837
+
838
+ if category_data["severity"] == "critical":
839
+ has_critical = True
840
+
841
+ # Get remediation from validator's data (not duplicated logic)
842
+ remediation = _get_remediation_from_validator(action, category)
843
+
844
+ sensitive_actions_found.append(
845
+ {
846
+ "action": action,
847
+ "category": category,
848
+ "severity": category_data["severity"],
849
+ "description": category_data["description"],
850
+ "remediation": remediation,
851
+ }
852
+ )
853
+
854
+ # Generate summary with actionable guidance
855
+ summary = ""
856
+ fix_guidance = ""
857
+ if sensitive_actions_found:
858
+ summary = f"Found {len(sensitive_actions_found)} sensitive action(s). "
859
+ if has_critical:
860
+ summary += "CRITICAL actions detected - require MFA and strict conditions. "
861
+ if "credential_exposure" in categories_found:
862
+ summary += "Credential exposure risk present. "
863
+ if "priv_esc" in categories_found:
864
+ summary += "Privilege escalation risk present. "
865
+
866
+ # Clear fix guidance for AI clients to prevent loops
867
+ fix_guidance = (
868
+ "To resolve these findings:\n"
869
+ "1. Add IAM conditions to each statement containing sensitive actions\n"
870
+ "2. Use the condition_example from each finding as a starting point\n"
871
+ "3. Customize placeholder values (e.g., replace IP ranges, tag values)\n"
872
+ "4. Re-validate the policy after adding conditions\n"
873
+ "5. If the same action is still flagged, the validator's sensitive_action "
874
+ "check may require specific conditions - see the suggestion field for details"
875
+ )
876
+ else:
877
+ summary = "No sensitive actions detected."
878
+ fix_guidance = "No action required - no sensitive actions found in the provided list."
879
+
880
+ return {
881
+ "sensitive_actions": sensitive_actions_found,
882
+ "total_checked": len(actions),
883
+ "sensitive_count": len(sensitive_actions_found),
884
+ "categories_found": sorted(categories_found),
885
+ "has_critical": has_critical,
886
+ "summary": summary,
887
+ "fix_guidance": fix_guidance,
888
+ }