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
@@ -47,6 +47,7 @@ KNOWN_CHECK_IDS = frozenset(
47
47
  "service_wildcard",
48
48
  "sensitive_action",
49
49
  "action_condition_enforcement",
50
+ "not_action_not_resource",
50
51
  ]
51
52
  )
52
53
 
@@ -82,6 +83,7 @@ class CheckConfigSchema(BaseModel):
82
83
  severity: str | None = None
83
84
  description: str | None = None
84
85
  ignore_patterns: list[dict[str, Any]] = []
86
+ hide_severities: list[str] | None = None # Per-check severity filtering
85
87
 
86
88
  @field_validator("severity")
87
89
  @classmethod
@@ -90,6 +92,18 @@ class CheckConfigSchema(BaseModel):
90
92
  raise ValueError(f"Invalid severity: {v}. Must be one of: {sorted(SEVERITY_LEVELS)}")
91
93
  return v
92
94
 
95
+ @field_validator("hide_severities")
96
+ @classmethod
97
+ def validate_hide_severities(cls, v: list[str] | None) -> list[str] | None:
98
+ if v is not None:
99
+ for severity in v:
100
+ if severity not in SEVERITY_LEVELS:
101
+ raise ValueError(
102
+ f"Invalid severity in hide_severities: {severity}. "
103
+ f"Must be one of: {sorted(SEVERITY_LEVELS)}"
104
+ )
105
+ return v
106
+
93
107
 
94
108
  class IgnoreSettingsSchema(BaseModel):
95
109
  """Schema for ignore settings."""
@@ -122,6 +136,7 @@ class SettingsSchema(BaseModel):
122
136
  severity_labels: dict[str, str | list[str]] = {}
123
137
  ignore_settings: IgnoreSettingsSchema = IgnoreSettingsSchema()
124
138
  documentation: DocumentationSettingsSchema = DocumentationSettingsSchema()
139
+ hide_severities: list[str] | None = None # Global severity filtering
125
140
 
126
141
  @field_validator("fail_on_severity")
127
142
  @classmethod
@@ -134,6 +149,18 @@ class SettingsSchema(BaseModel):
134
149
  )
135
150
  return v
136
151
 
152
+ @field_validator("hide_severities")
153
+ @classmethod
154
+ def validate_hide_severities(cls, v: list[str] | None) -> list[str] | None:
155
+ if v is not None:
156
+ for severity in v:
157
+ if severity not in SEVERITY_LEVELS:
158
+ raise ValueError(
159
+ f"Invalid severity in hide_severities: {severity}. "
160
+ f"Must be one of: {sorted(SEVERITY_LEVELS)}"
161
+ )
162
+ return v
163
+
137
164
 
138
165
  class CustomCheckSchema(BaseModel):
139
166
  """Schema for custom check definitions."""
@@ -446,6 +473,9 @@ class ConfigLoader:
446
473
  config: Loaded configuration
447
474
  registry: Check registry to configure
448
475
  """
476
+ # Get global hide_severities from settings (for fallback)
477
+ global_hide_severities = config.settings.get("hide_severities")
478
+
449
479
  # Configure built-in checks
450
480
  for check in registry.get_all_checks():
451
481
  check_id = check.check_id
@@ -455,6 +485,13 @@ class ConfigLoader:
455
485
  existing_config = registry.get_config(check_id)
456
486
  existing_enabled = existing_config.enabled if existing_config else True
457
487
 
488
+ # Parse hide_severities: per-check overrides global
489
+ hide_severities = check_config_dict.get("hide_severities")
490
+ if hide_severities is None:
491
+ hide_severities = global_hide_severities
492
+ if hide_severities is not None:
493
+ hide_severities = frozenset(hide_severities)
494
+
458
495
  # Create CheckConfig object
459
496
  # If there's explicit config, use it; otherwise preserve existing enabled state
460
497
  check_config = CheckConfig(
@@ -464,9 +501,8 @@ class ConfigLoader:
464
501
  config=check_config_dict,
465
502
  description=check_config_dict.get("description", check.description),
466
503
  root_config=config.config_dict, # Pass full config for cross-check access
467
- ignore_patterns=check_config_dict.get(
468
- "ignore_patterns", []
469
- ), # NEW: Ignore patterns
504
+ ignore_patterns=check_config_dict.get("ignore_patterns", []),
505
+ hide_severities=hide_severities,
470
506
  )
471
507
 
472
508
  registry.configure_check(check_id, check_config)
@@ -110,6 +110,12 @@ DEFAULT_CONFIG = {
110
110
  # Include AWS documentation links alongside org docs
111
111
  "include_aws_docs": True,
112
112
  },
113
+ # Severity filtering - hide specific severity levels from output
114
+ # When set, issues with these severities will be filtered out globally
115
+ # Can be overridden per-check using check-level hide_severities
116
+ # Valid values: "error", "warning", "info", "critical", "high", "medium", "low"
117
+ # Example: ["low", "info"] - hide low and info severity findings
118
+ "hide_severities": None,
113
119
  },
114
120
  # ========================================================================
115
121
  # AWS IAM Validation Checks (17 checks total)
@@ -77,6 +77,17 @@ MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
77
77
  # Low severity issues (informational)
78
78
  LOW_SEVERITY_LEVELS = ("info", "low")
79
79
 
80
+ # Severity configuration with emoji and action guidance for PR comments
81
+ SEVERITY_CONFIG = {
82
+ "critical": {"emoji": "🔴", "action": "Block deployment"},
83
+ "high": {"emoji": "🟠", "action": "Fix before merge"},
84
+ "medium": {"emoji": "🟡", "action": "Address soon"},
85
+ "low": {"emoji": "🔵", "action": "Consider fixing"},
86
+ "error": {"emoji": "❌", "action": "Must fix - AWS will reject"},
87
+ "warning": {"emoji": "⚠️", "action": "Review"},
88
+ "info": {"emoji": "ℹ️", "action": "Optional"},
89
+ }
90
+
80
91
  # ============================================================================
81
92
  # GitHub Integration
82
93
  # ============================================================================
@@ -161,10 +172,6 @@ AWS_TAG_KEY_ALLOWED_CHARS = r"a-zA-Z0-9 +\-=._:/@"
161
172
  # Maximum length for AWS tag keys (per AWS documentation)
162
173
  AWS_TAG_KEY_MAX_LENGTH = 128
163
174
 
164
- # Tag-key placeholder patterns used in AWS service definitions
165
- # These patterns indicate where a tag key should be substituted
166
- AWS_TAG_KEY_PLACEHOLDERS = ("/tag-key", "/${TagKey}", "/${tag-key}")
167
-
168
175
  # --- Tag Value Constraints ---
169
176
  # Allowed characters in AWS tag values: letters, numbers, spaces, and + - = . _ : / @
170
177
  # Same character set as tag keys
@@ -126,12 +126,24 @@ class Statement(BaseModel):
126
126
  return []
127
127
  return [self.action] if isinstance(self.action, str) else self.action
128
128
 
129
+ def get_not_actions(self) -> list[str]:
130
+ """Get list of NotAction values, handling both string and list formats."""
131
+ if self.not_action is None:
132
+ return []
133
+ return [self.not_action] if isinstance(self.not_action, str) else self.not_action
134
+
129
135
  def get_resources(self) -> list[str]:
130
136
  """Get list of resources, handling both string and list formats."""
131
137
  if self.resource is None:
132
138
  return []
133
139
  return [self.resource] if isinstance(self.resource, str) else self.resource
134
140
 
141
+ def get_not_resources(self) -> list[str]:
142
+ """Get list of NotResource values, handling both string and list formats."""
143
+ if self.not_resource is None:
144
+ return []
145
+ return [self.not_resource] if isinstance(self.not_resource, str) else self.not_resource
146
+
135
147
 
136
148
  class IAMPolicy(BaseModel):
137
149
  """IAM policy document."""
@@ -179,6 +191,8 @@ class ValidationIssue(BaseModel):
179
191
  documentation_url: str | None = None
180
192
  # Step-by-step remediation guidance
181
193
  remediation_steps: list[str] | None = None
194
+ # Risk category for classification (e.g., "privilege_escalation", "data_exfiltration")
195
+ risk_category: str | None = None
182
196
 
183
197
  # Severity level constants (ClassVar to avoid Pydantic treating them as fields)
184
198
  VALID_SEVERITIES: ClassVar[frozenset[str]] = frozenset(
@@ -226,18 +240,23 @@ class ValidationIssue(BaseModel):
226
240
  Returns:
227
241
  Formatted comment string
228
242
  """
229
- severity_emoji = {
230
- # IAM validity severities
231
- "error": "",
232
- "warning": "⚠️",
233
- "info": "ℹ️",
234
- # Security severities
235
- "critical": "🔴",
236
- "high": "🟠",
237
- "medium": "🟡",
238
- "low": "🔵",
239
- }
240
- emoji = severity_emoji.get(self.severity, "•")
243
+ # Get severity config with emoji and action guidance
244
+ severity_config = constants.SEVERITY_CONFIG.get(
245
+ self.severity, {"emoji": "", "action": "Review"}
246
+ )
247
+ emoji = severity_config["emoji"]
248
+ action = severity_config["action"]
249
+
250
+ # Get risk category icon if available
251
+ from iam_validator.core.config.check_documentation import RISK_CATEGORY_ICONS
252
+
253
+ risk_icon = ""
254
+ if self.risk_category:
255
+ icon = RISK_CATEGORY_ICONS.get(self.risk_category, "")
256
+ if icon:
257
+ # Format risk category for display (e.g., "privilege_escalation" -> "Privilege Escalation")
258
+ category_display = self.risk_category.replace("_", " ").title()
259
+ risk_icon = f" | {icon} {category_display}"
241
260
 
242
261
  parts = []
243
262
 
@@ -263,13 +282,19 @@ class ValidationIssue(BaseModel):
263
282
  )
264
283
  parts.append(f"<!-- finding-id: {finding_hash} -->\n")
265
284
 
285
+ # Main issue header with severity, action guidance, and risk category
286
+ parts.append(f"{emoji} **{self.severity.upper()}** - {action}{risk_icon}")
287
+ parts.append("")
288
+
266
289
  # Build statement context for better navigation
267
290
  statement_context = f"Statement[{self.statement_index}]"
268
291
  if self.statement_sid:
269
292
  statement_context = f"`{self.statement_sid}` ({statement_context})"
293
+ if self.line_number:
294
+ statement_context = f"{statement_context} (line {self.line_number})"
270
295
 
271
- # Main issue header with statement context
272
- parts.append(f"{emoji} **{self.severity.upper()}** in **{statement_context}**")
296
+ # Statement context on its own line
297
+ parts.append(f"**Statement:** {statement_context}")
273
298
  parts.append("")
274
299
 
275
300
  # Show message immediately (not collapsed)
@@ -0,0 +1,162 @@
1
+ """IAM Policy Validator MCP Server.
2
+
3
+ This module provides an MCP (Model Context Protocol) server for AI assistants
4
+ to interact with the IAM Policy Validator. It exposes tools for:
5
+ - Validating IAM policies
6
+ - Generating policies from templates or descriptions
7
+ - Querying AWS service definitions
8
+ - Managing session-wide policy configurations
9
+
10
+ The server uses FastMCP and provides a security-first approach to policy generation.
11
+
12
+ Configuration:
13
+ The MCP server uses the same configuration format as the CLI validator.
14
+ You can load configuration from a YAML file using --config or set it
15
+ programmatically using SessionConfigManager.
16
+ """
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ from iam_validator.mcp.models import (
21
+ ActionDetails,
22
+ GenerationResult,
23
+ PolicySummary,
24
+ ValidationResult,
25
+ )
26
+ from iam_validator.mcp.session_config import (
27
+ CustomInstructionsManager,
28
+ SessionConfigManager,
29
+ merge_conditions,
30
+ )
31
+
32
+ if TYPE_CHECKING:
33
+ from fastmcp import FastMCP
34
+
35
+
36
+ def create_server() -> "FastMCP":
37
+ """Create and configure the MCP server.
38
+
39
+ Returns:
40
+ FastMCP: Configured MCP server instance
41
+
42
+ Raises:
43
+ ImportError: If fastmcp is not installed
44
+ """
45
+ try:
46
+ from iam_validator.mcp.server import create_server as _create_server
47
+
48
+ return _create_server()
49
+ except ImportError as e:
50
+ raise ImportError(
51
+ "fastmcp is required for MCP server. Install with: uv sync --extra mcp"
52
+ ) from e
53
+
54
+
55
+ def run_server() -> None:
56
+ """Run the MCP server.
57
+
58
+ This is the entry point for the iam-validator-mcp command.
59
+ Supports configuration and custom instructions at startup.
60
+
61
+ Usage:
62
+ iam-validator-mcp
63
+ iam-validator-mcp --config /path/to/config.yaml
64
+ iam-validator-mcp --instructions "Always require MFA for sensitive actions"
65
+ iam-validator-mcp --instructions-file /path/to/instructions.md
66
+
67
+ Custom instructions can also be set via:
68
+ - Environment variable: IAM_VALIDATOR_MCP_INSTRUCTIONS
69
+ - Config file: custom_instructions key in YAML config
70
+
71
+ Raises:
72
+ ImportError: If fastmcp is not installed
73
+ """
74
+ import argparse
75
+ import sys
76
+ from pathlib import Path
77
+
78
+ parser = argparse.ArgumentParser(
79
+ prog="iam-validator-mcp",
80
+ description="IAM Policy Validator MCP Server for AI assistants",
81
+ )
82
+ parser.add_argument(
83
+ "--config",
84
+ type=str,
85
+ metavar="FILE",
86
+ help="Path to configuration YAML file to load at startup",
87
+ )
88
+ parser.add_argument(
89
+ "--instructions",
90
+ type=str,
91
+ metavar="TEXT",
92
+ help="Custom instructions to append to default LLM instructions",
93
+ )
94
+ parser.add_argument(
95
+ "--instructions-file",
96
+ type=str,
97
+ metavar="FILE",
98
+ help="Path to file containing custom instructions (markdown, txt)",
99
+ )
100
+ args = parser.parse_args()
101
+
102
+ # Load config if provided (may include custom_instructions)
103
+ if args.config:
104
+ config_path = Path(args.config)
105
+ if not config_path.exists():
106
+ print(f"Error: Config file not found: {args.config}", file=sys.stderr)
107
+ sys.exit(1)
108
+
109
+ try:
110
+ config, warnings = SessionConfigManager.load_from_file(str(config_path))
111
+
112
+ for warning in warnings:
113
+ print(f"Warning: {warning}", file=sys.stderr)
114
+
115
+ print(f"Loaded config from: {args.config}", file=sys.stderr)
116
+
117
+ except Exception as e:
118
+ print(f"Error loading config: {e}", file=sys.stderr)
119
+ sys.exit(1)
120
+
121
+ # Load custom instructions from CLI arguments (overrides config/env)
122
+ if args.instructions_file:
123
+ instructions_path = Path(args.instructions_file)
124
+ if not instructions_path.exists():
125
+ print(
126
+ f"Error: Instructions file not found: {args.instructions_file}",
127
+ file=sys.stderr,
128
+ )
129
+ sys.exit(1)
130
+
131
+ try:
132
+ CustomInstructionsManager.load_from_file(str(instructions_path))
133
+ print(f"Loaded instructions from: {args.instructions_file}", file=sys.stderr)
134
+ except Exception as e:
135
+ print(f"Error loading instructions: {e}", file=sys.stderr)
136
+ sys.exit(1)
137
+
138
+ elif args.instructions:
139
+ CustomInstructionsManager.set_instructions(args.instructions, source="cli")
140
+ print("Custom instructions set from CLI argument", file=sys.stderr)
141
+
142
+ try:
143
+ from iam_validator.mcp.server import run_server as _run_server
144
+
145
+ _run_server()
146
+ except ImportError as e:
147
+ raise ImportError(
148
+ "fastmcp is required for MCP server. Install with: uv sync --extra mcp"
149
+ ) from e
150
+
151
+
152
+ __all__ = [
153
+ "create_server",
154
+ "run_server",
155
+ "ValidationResult",
156
+ "GenerationResult",
157
+ "PolicySummary",
158
+ "ActionDetails",
159
+ "SessionConfigManager",
160
+ "CustomInstructionsManager",
161
+ "merge_conditions",
162
+ ]
@@ -0,0 +1,118 @@
1
+ """Pydantic models for MCP tool request/response types.
2
+
3
+ This module defines MCP-specific models that extend the core validation models
4
+ for use with the FastMCP server implementation.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from iam_validator.core.models import ValidationIssue
12
+
13
+
14
+ class ValidationResult(BaseModel):
15
+ """Result of policy validation.
16
+
17
+ Used by validation tools to return validation status and issues found.
18
+ """
19
+
20
+ is_valid: bool = Field(
21
+ description="Whether the policy passed validation (no errors or warnings)"
22
+ )
23
+ issues: list[ValidationIssue] = Field(
24
+ default_factory=list, description="List of validation issues found"
25
+ )
26
+ policy_file: str | None = Field(
27
+ default=None, description="Path to the policy file that was validated"
28
+ )
29
+ policy_type_detected: str | None = Field(
30
+ default=None,
31
+ description="The policy type used for validation: 'identity', 'resource', or 'trust'. "
32
+ "Shows auto-detected type when policy_type was not explicitly provided.",
33
+ )
34
+
35
+
36
+ class GenerationResult(BaseModel):
37
+ """Result of policy generation.
38
+
39
+ Returned by all policy generation tools (from description, template, or actions).
40
+ Always includes validation results and security notes.
41
+ """
42
+
43
+ policy: dict[str, Any] = Field(description="The generated IAM policy document")
44
+ validation: ValidationResult = Field(description="Validation results for the generated policy")
45
+ security_notes: list[str] = Field(
46
+ default_factory=list,
47
+ description="Security warnings and auto-applied conditions (e.g., 'Auto-added MFA condition')",
48
+ )
49
+ template_used: str | None = Field(
50
+ default=None, description="Name of the template used for generation (if applicable)"
51
+ )
52
+
53
+
54
+ class PolicySummary(BaseModel):
55
+ """Summary of a policy's structure and contents.
56
+
57
+ Provides high-level statistics about a policy for quick analysis.
58
+ """
59
+
60
+ total_statements: int = Field(description="Total number of statements in the policy")
61
+ allow_statements: int = Field(description="Number of statements with Effect: Allow")
62
+ deny_statements: int = Field(description="Number of statements with Effect: Deny")
63
+ services_used: list[str] = Field(
64
+ default_factory=list, description="List of AWS services referenced (e.g., ['s3', 'ec2'])"
65
+ )
66
+ actions_count: int = Field(description="Total number of unique actions across all statements")
67
+ has_wildcards: bool = Field(
68
+ description="Whether the policy contains wildcard actions or resources"
69
+ )
70
+ has_conditions: bool = Field(description="Whether the policy contains any conditions")
71
+
72
+
73
+ class ActionDetails(BaseModel):
74
+ """Details about an AWS action.
75
+
76
+ Returned by query tools to provide comprehensive information about an IAM action.
77
+ """
78
+
79
+ action: str = Field(description="Full action name (e.g., 's3:GetObject')")
80
+ service: str = Field(description="AWS service prefix (e.g., 's3', 'ec2')")
81
+ access_level: str = Field(
82
+ description="Access level category: Read, Write, List, Tagging, or Permissions management"
83
+ )
84
+ resource_types: list[str] = Field(
85
+ default_factory=list,
86
+ description="Resource types this action can be applied to (e.g., ['bucket', 'object'])",
87
+ )
88
+ condition_keys: list[str] = Field(
89
+ default_factory=list,
90
+ description="Condition keys that can be used with this action",
91
+ )
92
+ description: str | None = Field(
93
+ default=None, description="Human-readable description of what the action does"
94
+ )
95
+
96
+
97
+ class EnforcementResult(BaseModel):
98
+ """Result of security enforcement on a policy.
99
+
100
+ Returned by the security enforcement layer after applying required conditions
101
+ and validating security constraints.
102
+ """
103
+
104
+ policy: dict[str, Any] = Field(
105
+ description="The policy after security enforcement (with auto-added conditions)"
106
+ )
107
+ warnings: list[str] = Field(
108
+ default_factory=list,
109
+ description="Security warnings for issues that were auto-fixed (e.g., 'Added MFA condition')",
110
+ )
111
+ errors: list[str] = Field(
112
+ default_factory=list,
113
+ description="Security errors that could not be auto-fixed (generation should fail)",
114
+ )
115
+ conditions_added: list[str] = Field(
116
+ default_factory=list,
117
+ description="List of conditions that were automatically added (e.g., 'aws:MultiFactorAuthPresent')",
118
+ )