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,319 @@
1
+ """Session configuration management for MCP server.
2
+
3
+ This module provides session-scoped configuration management using the core
4
+ ValidatorConfig system. It stores the validator configuration for the MCP
5
+ session lifetime, enabling consistent validation across tool calls.
6
+
7
+ Example usage:
8
+ # Set session config from a YAML file
9
+ SessionConfigManager.load_from_file("/path/to/config.yaml")
10
+
11
+ # Or set from YAML content
12
+ SessionConfigManager.load_from_yaml(yaml_content)
13
+
14
+ # Or set from a dictionary
15
+ SessionConfigManager.set_config({"settings": {"fail_on_severity": ["error", "critical"]}})
16
+
17
+ # Get the current config
18
+ config = SessionConfigManager.get_config()
19
+ if config:
20
+ # Use config for validation
21
+ check_config = config.get_check_config("wildcard_action")
22
+ """
23
+
24
+ from typing import Any
25
+
26
+ import yaml
27
+
28
+ from iam_validator.core.config.config_loader import ValidatorConfig
29
+
30
+
31
+ class SessionConfigManager:
32
+ """Manages session-scoped configuration for MCP tools.
33
+
34
+ This class provides session-scoped storage for ValidatorConfig.
35
+ The config is stored as a class variable and persists for the lifetime
36
+ of the MCP session.
37
+
38
+ The configuration uses the same schema as the CLI validator, so you can
39
+ use the same YAML configuration files for both CLI and MCP usage.
40
+ """
41
+
42
+ _session_config: ValidatorConfig | None = None
43
+ _config_source: str = "none"
44
+
45
+ @classmethod
46
+ def set_config(cls, config_dict: dict[str, Any], source: str = "session") -> ValidatorConfig:
47
+ """Set the session configuration from a dictionary.
48
+
49
+ Args:
50
+ config_dict: Configuration dictionary (same format as YAML config files)
51
+ source: Source identifier ("session", "yaml", "file")
52
+
53
+ Returns:
54
+ The created ValidatorConfig instance
55
+ """
56
+ cls._session_config = ValidatorConfig(config_dict, use_defaults=True)
57
+ cls._config_source = source
58
+ return cls._session_config
59
+
60
+ @classmethod
61
+ def get_config(cls) -> ValidatorConfig | None:
62
+ """Get the current session configuration.
63
+
64
+ Returns:
65
+ Current ValidatorConfig, or None if not set
66
+ """
67
+ return cls._session_config
68
+
69
+ @classmethod
70
+ def get_config_source(cls) -> str:
71
+ """Get the source of the current configuration.
72
+
73
+ Returns:
74
+ Source identifier: "session", "yaml", "file", or "none"
75
+ """
76
+ return cls._config_source
77
+
78
+ @classmethod
79
+ def clear_config(cls) -> bool:
80
+ """Clear the session configuration.
81
+
82
+ Returns:
83
+ True if config was cleared, False if no config was set
84
+ """
85
+ had_config = cls._session_config is not None
86
+ cls._session_config = None
87
+ cls._config_source = "none"
88
+ return had_config
89
+
90
+ @classmethod
91
+ def has_config(cls) -> bool:
92
+ """Check if a session configuration is set.
93
+
94
+ Returns:
95
+ True if config is set, False otherwise
96
+ """
97
+ return cls._session_config is not None
98
+
99
+ @classmethod
100
+ def load_from_yaml(cls, yaml_content: str) -> tuple[ValidatorConfig, list[str]]:
101
+ """Load session configuration from YAML content.
102
+
103
+ Args:
104
+ yaml_content: YAML string containing configuration
105
+
106
+ Returns:
107
+ Tuple of (ValidatorConfig, list of warnings)
108
+
109
+ Raises:
110
+ ValueError: If YAML parsing or validation fails
111
+ """
112
+ warnings: list[str] = []
113
+
114
+ try:
115
+ config_dict = yaml.safe_load(yaml_content)
116
+ except yaml.YAMLError as e:
117
+ raise ValueError(f"Invalid YAML: {e}") from e
118
+
119
+ if not isinstance(config_dict, dict):
120
+ raise ValueError("YAML content must be a dictionary")
121
+
122
+ # Support legacy "organization" key for backwards compatibility
123
+ if "organization" in config_dict:
124
+ org_config = config_dict.pop("organization")
125
+ # Merge organization settings into settings
126
+ if "settings" not in config_dict:
127
+ config_dict["settings"] = {}
128
+ config_dict["settings"].update(org_config)
129
+ warnings.append("Migrated 'organization' key to 'settings'")
130
+
131
+ # Extract custom_instructions if present (MCP-specific setting)
132
+ if "custom_instructions" in config_dict:
133
+ custom_instructions = config_dict.pop("custom_instructions")
134
+ if isinstance(custom_instructions, str) and custom_instructions.strip():
135
+ CustomInstructionsManager.set_instructions(custom_instructions, source="config")
136
+ warnings.append("Loaded custom instructions from config")
137
+
138
+ config = cls.set_config(config_dict, source="yaml")
139
+ return config, warnings
140
+
141
+ @classmethod
142
+ def load_from_file(cls, file_path: str) -> tuple[ValidatorConfig, list[str]]:
143
+ """Load session configuration from a YAML file.
144
+
145
+ Args:
146
+ file_path: Path to YAML configuration file
147
+
148
+ Returns:
149
+ Tuple of (ValidatorConfig, list of warnings)
150
+
151
+ Raises:
152
+ ValueError: If file reading or parsing fails
153
+ FileNotFoundError: If file doesn't exist
154
+ """
155
+ from pathlib import Path
156
+
157
+ path = Path(file_path)
158
+ if not path.exists():
159
+ raise FileNotFoundError(f"Configuration file not found: {file_path}")
160
+
161
+ yaml_content = path.read_text()
162
+ config, warnings = cls.load_from_yaml(yaml_content)
163
+ cls._config_source = "file"
164
+ return config, warnings
165
+
166
+
167
+ def merge_conditions(
168
+ base_conditions: dict[str, Any] | None,
169
+ required_conditions: dict[str, Any],
170
+ ) -> dict[str, Any]:
171
+ """Merge required conditions into base conditions.
172
+
173
+ This performs a deep merge of condition blocks, combining operators
174
+ and their nested conditions appropriately.
175
+
176
+ Args:
177
+ base_conditions: Existing conditions (may be None)
178
+ required_conditions: Required conditions to merge in
179
+
180
+ Returns:
181
+ Merged conditions dictionary
182
+ """
183
+ if not required_conditions:
184
+ return base_conditions or {}
185
+
186
+ if not base_conditions:
187
+ return required_conditions.copy()
188
+
189
+ result = base_conditions.copy()
190
+
191
+ for operator, conditions in required_conditions.items():
192
+ if operator in result:
193
+ # Merge conditions under the same operator
194
+ if isinstance(result[operator], dict) and isinstance(conditions, dict):
195
+ result[operator] = {**result[operator], **conditions}
196
+ else:
197
+ # Can't merge non-dict values, required takes precedence
198
+ result[operator] = conditions
199
+ else:
200
+ result[operator] = conditions
201
+
202
+ return result
203
+
204
+
205
+ class CustomInstructionsManager:
206
+ """Manages custom LLM instructions for the MCP server.
207
+
208
+ Custom instructions are appended to the default MCP server instructions,
209
+ allowing organizations to add their own policy generation guidelines.
210
+
211
+ Instructions can be set via:
212
+ - YAML config file (custom_instructions key)
213
+ - Environment variable (IAM_VALIDATOR_MCP_INSTRUCTIONS)
214
+ - CLI argument (--instructions or --instructions-file)
215
+ - MCP tool (set_custom_instructions)
216
+
217
+ Example YAML config:
218
+ custom_instructions: |
219
+ ## Organization-Specific Rules
220
+ - All policies must include aws:PrincipalOrgID condition
221
+ - Use resource tags for access control where possible
222
+ - S3 buckets must have encryption conditions
223
+ """
224
+
225
+ _custom_instructions: str | None = None
226
+ _instructions_source: str = "none"
227
+
228
+ @classmethod
229
+ def set_instructions(cls, instructions: str, source: str = "api") -> None:
230
+ """Set custom instructions.
231
+
232
+ Args:
233
+ instructions: Custom instructions text (markdown supported)
234
+ source: Source identifier ("api", "env", "file", "config")
235
+ """
236
+ stripped = instructions.strip() if instructions else ""
237
+ cls._custom_instructions = stripped if stripped else None
238
+ cls._instructions_source = source if cls._custom_instructions else "none"
239
+
240
+ @classmethod
241
+ def get_instructions(cls) -> str | None:
242
+ """Get custom instructions.
243
+
244
+ Returns:
245
+ Custom instructions string, or None if not set
246
+ """
247
+ return cls._custom_instructions
248
+
249
+ @classmethod
250
+ def get_source(cls) -> str:
251
+ """Get the source of custom instructions.
252
+
253
+ Returns:
254
+ Source identifier: "api", "env", "file", "config", or "none"
255
+ """
256
+ return cls._instructions_source
257
+
258
+ @classmethod
259
+ def clear_instructions(cls) -> bool:
260
+ """Clear custom instructions.
261
+
262
+ Returns:
263
+ True if instructions were cleared, False if none were set
264
+ """
265
+ had_instructions = cls._custom_instructions is not None
266
+ cls._custom_instructions = None
267
+ cls._instructions_source = "none"
268
+ return had_instructions
269
+
270
+ @classmethod
271
+ def has_instructions(cls) -> bool:
272
+ """Check if custom instructions are set.
273
+
274
+ Returns:
275
+ True if instructions are set, False otherwise
276
+ """
277
+ return cls._custom_instructions is not None
278
+
279
+ @classmethod
280
+ def load_from_env(cls) -> bool:
281
+ """Load custom instructions from environment variable.
282
+
283
+ Checks IAM_VALIDATOR_MCP_INSTRUCTIONS environment variable.
284
+
285
+ Returns:
286
+ True if instructions were loaded, False otherwise
287
+ """
288
+ import os
289
+
290
+ env_instructions = os.environ.get("IAM_VALIDATOR_MCP_INSTRUCTIONS")
291
+ if env_instructions:
292
+ cls.set_instructions(env_instructions, source="env")
293
+ return True
294
+ return False
295
+
296
+ @classmethod
297
+ def load_from_file(cls, file_path: str) -> None:
298
+ """Load custom instructions from a file.
299
+
300
+ Args:
301
+ file_path: Path to file containing instructions (markdown, txt)
302
+
303
+ Raises:
304
+ FileNotFoundError: If file doesn't exist
305
+ """
306
+ from pathlib import Path
307
+
308
+ path = Path(file_path)
309
+ if not path.exists():
310
+ raise FileNotFoundError(f"Instructions file not found: {file_path}")
311
+
312
+ cls.set_instructions(path.read_text(), source="file")
313
+
314
+
315
+ __all__ = [
316
+ "SessionConfigManager",
317
+ "CustomInstructionsManager",
318
+ "merge_conditions",
319
+ ]
@@ -0,0 +1,79 @@
1
+ """IAM policy templates for MCP server.
2
+
3
+ This package provides built-in policy templates that can be used to generate
4
+ common IAM policies with variable substitution. Templates are validated
5
+ through the security enforcement layer before being returned.
6
+
7
+ Available templates:
8
+ - s3-read-only: S3 bucket read-only access
9
+ - s3-read-write: S3 bucket read-write access
10
+ - lambda-basic-execution: Basic Lambda execution role
11
+ - lambda-s3-trigger: Lambda with S3 event trigger permissions
12
+ - dynamodb-crud: DynamoDB table CRUD operations
13
+ - cloudwatch-logs: CloudWatch Logs write permissions
14
+ - secrets-manager-read: Secrets Manager read access
15
+ - kms-encrypt-decrypt: KMS key encryption/decryption
16
+ - ec2-describe: EC2 describe-only permissions
17
+ - ecs-task-execution: ECS task execution role
18
+ """
19
+
20
+ from .builtin import get_template as _get_template
21
+ from .builtin import list_templates as _list_templates
22
+ from .builtin import render_template as _render_template
23
+
24
+
25
+ def list_templates() -> list[str]:
26
+ """List all available template names.
27
+
28
+ Returns:
29
+ List of template names that can be used with load_template()
30
+ """
31
+ templates = _list_templates()
32
+ return [t["name"] for t in templates]
33
+
34
+
35
+ def get_template_variables(template_name: str) -> list[str]:
36
+ """Get the required variables for a template.
37
+
38
+ Args:
39
+ template_name: Name of the template
40
+
41
+ Returns:
42
+ List of variable names required by the template
43
+
44
+ Raises:
45
+ ValueError: If template_name is not found
46
+ """
47
+ template = _get_template(template_name)
48
+ if not template:
49
+ raise ValueError(
50
+ f"Template '{template_name}' not found. "
51
+ f"Available templates: {', '.join(list_templates())}"
52
+ )
53
+
54
+ return [var["name"] for var in template["variables"]]
55
+
56
+
57
+ def load_template(template_name: str, variables: dict[str, str] | None = None) -> dict:
58
+ """Load a template and substitute variables.
59
+
60
+ Args:
61
+ template_name: Name of the template to load
62
+ variables: Dictionary of variable values to substitute
63
+
64
+ Returns:
65
+ Policy dictionary with variables substituted
66
+
67
+ Raises:
68
+ ValueError: If template_name is not found or required variables are missing
69
+ """
70
+ if variables is None:
71
+ variables = {}
72
+ return _render_template(template_name, variables)
73
+
74
+
75
+ __all__ = [
76
+ "list_templates",
77
+ "get_template_variables",
78
+ "load_template",
79
+ ]