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,263 @@
1
+ """Organization configuration tools for MCP server.
2
+
3
+ This module provides the underlying implementations for MCP tools
4
+ that manage session-wide validator configurations.
5
+
6
+ The session config is used to control which checks are enabled, their
7
+ severity levels, and other validator settings. All validation is done
8
+ by the IAM validator's built-in checks - not by separate guardrail logic.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from iam_validator.mcp.session_config import SessionConfigManager
14
+
15
+
16
+ async def set_organization_config_impl(
17
+ config: dict[str, Any],
18
+ ) -> dict[str, Any]:
19
+ """Set session-wide validator configuration.
20
+
21
+ This sets the validator configuration for the MCP session. The config
22
+ uses the same format as the CLI validator's YAML configuration files.
23
+
24
+ Args:
25
+ config: Validator configuration dictionary. Supports:
26
+ - settings: Global settings (fail_on_severity, parallel, etc.)
27
+ - Check IDs as keys with enabled/severity/options
28
+
29
+ Returns:
30
+ Dictionary with success status, applied config, and any warnings
31
+
32
+ Example:
33
+ >>> await set_organization_config_impl({
34
+ ... "settings": {"fail_on_severity": ["error", "critical"]},
35
+ ... "wildcard_action": {"enabled": True, "severity": "critical"},
36
+ ... "sensitive_action": {"enabled": False}
37
+ ... })
38
+ """
39
+ warnings: list[str] = []
40
+
41
+ try:
42
+ validator_config = SessionConfigManager.set_config(config, source="session")
43
+
44
+ # Return the applied settings for confirmation
45
+ applied_config = {
46
+ "settings": validator_config.settings,
47
+ "checks": validator_config.checks_config,
48
+ }
49
+
50
+ return {
51
+ "success": True,
52
+ "applied_config": applied_config,
53
+ "warnings": warnings,
54
+ }
55
+ except Exception as e:
56
+ return {
57
+ "success": False,
58
+ "applied_config": None,
59
+ "warnings": warnings,
60
+ "error": str(e),
61
+ }
62
+
63
+
64
+ async def get_organization_config_impl() -> dict[str, Any]:
65
+ """Get the current session validator configuration.
66
+
67
+ Returns:
68
+ Dictionary with has_config, config, and source
69
+ """
70
+ config = SessionConfigManager.get_config()
71
+
72
+ if config is None:
73
+ return {
74
+ "has_config": False,
75
+ "config": None,
76
+ "source": "none",
77
+ }
78
+
79
+ return {
80
+ "has_config": True,
81
+ "config": {
82
+ "settings": config.settings,
83
+ "checks": config.checks_config,
84
+ },
85
+ "source": SessionConfigManager.get_config_source(),
86
+ }
87
+
88
+
89
+ async def clear_organization_config_impl() -> dict[str, str]:
90
+ """Clear the session validator configuration.
91
+
92
+ Returns:
93
+ Dictionary with status
94
+ """
95
+ had_config = SessionConfigManager.clear_config()
96
+
97
+ return {
98
+ "status": "cleared" if had_config else "no_config_set",
99
+ }
100
+
101
+
102
+ async def load_organization_config_from_yaml_impl(
103
+ yaml_content: str,
104
+ ) -> dict[str, Any]:
105
+ """Load validator configuration from YAML content.
106
+
107
+ Args:
108
+ yaml_content: YAML configuration string (same format as CLI config files)
109
+
110
+ Returns:
111
+ Dictionary with success status, applied config, warnings, and errors
112
+ """
113
+ try:
114
+ config, warnings = SessionConfigManager.load_from_yaml(yaml_content)
115
+
116
+ return {
117
+ "success": True,
118
+ "applied_config": {
119
+ "settings": config.settings,
120
+ "checks": config.checks_config,
121
+ },
122
+ "warnings": warnings,
123
+ }
124
+ except Exception as e:
125
+ return {
126
+ "success": False,
127
+ "applied_config": None,
128
+ "warnings": [],
129
+ "error": str(e),
130
+ }
131
+
132
+
133
+ async def check_org_compliance_impl(
134
+ policy: dict[str, Any],
135
+ ) -> dict[str, Any]:
136
+ """Check if a policy passes validation with the session configuration.
137
+
138
+ This runs the full validator with the session configuration and returns
139
+ the validation results. It does NOT use separate guardrail logic - all
140
+ checking is done by the validator's built-in checks.
141
+
142
+ Args:
143
+ policy: IAM policy as a dictionary
144
+
145
+ Returns:
146
+ Dictionary with compliance status and validation issues
147
+ """
148
+ from iam_validator.mcp.tools.validation import validate_policy
149
+
150
+ config = SessionConfigManager.get_config()
151
+
152
+ if config is None:
153
+ # No session config - validate with defaults
154
+ result = await validate_policy(policy=policy, use_org_config=False)
155
+ return {
156
+ "compliant": result.is_valid,
157
+ "has_org_config": False,
158
+ "violations": [
159
+ {"type": issue.issue_type, "message": issue.message, "severity": issue.severity}
160
+ for issue in result.issues
161
+ ],
162
+ "warnings": ["No session config set - using default validator settings"],
163
+ "suggestions": [issue.suggestion for issue in result.issues if issue.suggestion],
164
+ }
165
+
166
+ # Validate with the session config
167
+ result = await validate_policy(policy=policy, use_org_config=True)
168
+
169
+ violations = [
170
+ {"type": issue.issue_type, "message": issue.message, "severity": issue.severity}
171
+ for issue in result.issues
172
+ ]
173
+
174
+ suggestions = [issue.suggestion for issue in result.issues if issue.suggestion]
175
+
176
+ return {
177
+ "compliant": result.is_valid,
178
+ "has_org_config": True,
179
+ "violations": violations,
180
+ "warnings": [],
181
+ "suggestions": suggestions,
182
+ }
183
+
184
+
185
+ async def validate_with_config_impl(
186
+ policy: dict[str, Any],
187
+ config: dict[str, Any],
188
+ policy_type: str | None = None,
189
+ ) -> dict[str, Any]:
190
+ """Validate a policy with explicit inline configuration.
191
+
192
+ This runs validation with the provided config without affecting
193
+ the session configuration.
194
+
195
+ Args:
196
+ policy: IAM policy to validate
197
+ config: Inline configuration (same format as CLI config files)
198
+ policy_type: Type of policy. If None, auto-detects from policy structure.
199
+
200
+ Returns:
201
+ Dictionary with validation results
202
+ """
203
+ import tempfile
204
+ from pathlib import Path
205
+
206
+ import yaml
207
+
208
+ from iam_validator.mcp.tools.validation import validate_policy
209
+
210
+ # Create a temporary config file for the validator
211
+ temp_config_path: str | None = None
212
+ try:
213
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
214
+ yaml.dump(config, f)
215
+ temp_config_path = f.name
216
+
217
+ # Run validation with the temp config (bypasses session config)
218
+ validation_result = await validate_policy(
219
+ policy=policy,
220
+ policy_type=policy_type,
221
+ config_path=temp_config_path,
222
+ use_org_config=False,
223
+ )
224
+ except Exception as e:
225
+ return {
226
+ "is_valid": False,
227
+ "issues": [],
228
+ "error": str(e),
229
+ "config_applied": None,
230
+ }
231
+ finally:
232
+ if temp_config_path:
233
+ try:
234
+ Path(temp_config_path).unlink()
235
+ except OSError:
236
+ pass
237
+
238
+ # Build issues list
239
+ issues = [
240
+ {
241
+ "severity": issue.severity,
242
+ "message": issue.message,
243
+ "suggestion": issue.suggestion,
244
+ "check_id": issue.check_id,
245
+ }
246
+ for issue in validation_result.issues
247
+ ]
248
+
249
+ return {
250
+ "is_valid": validation_result.is_valid,
251
+ "issues": issues,
252
+ "config_applied": config,
253
+ }
254
+
255
+
256
+ __all__ = [
257
+ "set_organization_config_impl",
258
+ "get_organization_config_impl",
259
+ "clear_organization_config_impl",
260
+ "load_organization_config_from_yaml_impl",
261
+ "check_org_compliance_impl",
262
+ "validate_with_config_impl",
263
+ ]
@@ -0,0 +1,395 @@
1
+ """Query tools for the MCP server.
2
+
3
+ This module provides query tools for querying AWS service definitions,
4
+ listing validation checks, analyzing policies, and querying sensitive actions.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from iam_validator.core.aws_service import AWSServiceFetcher
10
+ from iam_validator.core.check_registry import create_default_registry
11
+ from iam_validator.core.config.sensitive_actions import (
12
+ CREDENTIAL_EXPOSURE_ACTIONS,
13
+ DATA_ACCESS_ACTIONS,
14
+ PRIV_ESC_ACTIONS,
15
+ RESOURCE_EXPOSURE_ACTIONS,
16
+ )
17
+ from iam_validator.mcp.models import ActionDetails, PolicySummary
18
+ from iam_validator.sdk import get_actions_by_access_level, parse_policy, query_arn_types
19
+ from iam_validator.sdk import get_policy_summary as sdk_get_policy_summary
20
+ from iam_validator.sdk import query_action_details as sdk_query_action_details
21
+ from iam_validator.sdk import query_condition_keys as sdk_query_condition_keys
22
+
23
+
24
+ async def query_service_actions(
25
+ service: str, access_level: str | None = None, fetcher: AWSServiceFetcher | None = None
26
+ ) -> list[str]:
27
+ """Get all actions for a service, optionally filtered by access level.
28
+
29
+ Args:
30
+ service: AWS service prefix (e.g., "s3", "iam", "ec2")
31
+ access_level: Optional filter by access level (read|write|list|tagging|permissions-management)
32
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
33
+
34
+ Returns:
35
+ List of action names (e.g., ["s3:GetObject", "s3:PutObject"])
36
+
37
+ Example:
38
+ >>> actions = await query_service_actions("s3")
39
+ >>> write_actions = await query_service_actions("s3", "write")
40
+ """
41
+ # Use provided fetcher or create a new one
42
+ if fetcher is not None:
43
+ _fetcher = fetcher
44
+ should_close = False
45
+ else:
46
+ _fetcher = AWSServiceFetcher()
47
+ await _fetcher.__aenter__()
48
+ should_close = True
49
+
50
+ try:
51
+ if access_level:
52
+ # Validate access level
53
+ valid_levels = ["read", "write", "list", "tagging", "permissions-management"]
54
+ if access_level.lower() not in valid_levels:
55
+ raise ValueError(
56
+ f"Invalid access level '{access_level}'. "
57
+ f"Must be one of: {', '.join(valid_levels)}"
58
+ )
59
+ return await get_actions_by_access_level(_fetcher, service, access_level) # type: ignore
60
+
61
+ # Get all actions (no filter)
62
+ from iam_validator.sdk import query_actions
63
+
64
+ actions = await query_actions(_fetcher, service)
65
+ return [action["action"] for action in actions]
66
+ finally:
67
+ if should_close:
68
+ await _fetcher.__aexit__(None, None, None)
69
+
70
+
71
+ async def query_action_details(
72
+ action: str, fetcher: AWSServiceFetcher | None = None
73
+ ) -> ActionDetails | None:
74
+ """Get detailed information about a specific action.
75
+
76
+ Args:
77
+ action: Full action name (e.g., "s3:GetObject", "iam:CreateUser")
78
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
79
+
80
+ Returns:
81
+ ActionDetails object with comprehensive action metadata, or None if not found
82
+
83
+ Example:
84
+ >>> details = await query_action_details("s3:GetObject")
85
+ >>> print(f"Access level: {details.access_level}")
86
+ >>> print(f"Resource types: {details.resource_types}")
87
+ """
88
+ # Parse service and action name
89
+ if ":" not in action:
90
+ raise ValueError(f"Invalid action format '{action}'. Expected 'service:action'")
91
+
92
+ service, action_name = action.split(":", 1)
93
+
94
+ # Use provided fetcher or create a new one
95
+ if fetcher is not None:
96
+ _fetcher = fetcher
97
+ should_close = False
98
+ else:
99
+ _fetcher = AWSServiceFetcher()
100
+ await _fetcher.__aenter__()
101
+ should_close = True
102
+
103
+ try:
104
+ try:
105
+ details = await sdk_query_action_details(_fetcher, service, action_name)
106
+
107
+ return ActionDetails(
108
+ action=details["action"],
109
+ service=details["service"],
110
+ access_level=details["access_level"],
111
+ resource_types=details["resource_types"],
112
+ condition_keys=details["condition_keys"],
113
+ description=details.get("description"),
114
+ )
115
+ except ValueError:
116
+ # Action not found
117
+ return None
118
+ finally:
119
+ if should_close:
120
+ await _fetcher.__aexit__(None, None, None)
121
+
122
+
123
+ async def expand_wildcard_action(
124
+ pattern: str, fetcher: AWSServiceFetcher | None = None
125
+ ) -> list[str]:
126
+ """Expand wildcards like "s3:Get*" to specific actions.
127
+
128
+ Args:
129
+ pattern: Action pattern with wildcards (e.g., "s3:Get*", "iam:*User*")
130
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
131
+
132
+ Returns:
133
+ List of matching action names
134
+
135
+ Example:
136
+ >>> actions = await expand_wildcard_action("s3:Get*")
137
+ >>> # Returns: ["s3:GetObject", "s3:GetObjectAcl", ...]
138
+ """
139
+ # Use provided fetcher or create a new one
140
+ if fetcher is not None:
141
+ _fetcher = fetcher
142
+ should_close = False
143
+ else:
144
+ _fetcher = AWSServiceFetcher()
145
+ await _fetcher.__aenter__()
146
+ should_close = True
147
+
148
+ try:
149
+ try:
150
+ return await _fetcher.expand_wildcard_action(pattern)
151
+ except Exception as e:
152
+ raise ValueError(f"Failed to expand wildcard action '{pattern}': {e}") from e
153
+ finally:
154
+ if should_close:
155
+ await _fetcher.__aexit__(None, None, None)
156
+
157
+
158
+ async def query_condition_keys(service: str, fetcher: AWSServiceFetcher | None = None) -> list[str]:
159
+ """Get all condition keys for a service.
160
+
161
+ Args:
162
+ service: AWS service prefix (e.g., "s3", "iam")
163
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
164
+
165
+ Returns:
166
+ List of condition key names (e.g., ["s3:prefix", "s3:x-amz-acl"])
167
+
168
+ Example:
169
+ >>> keys = await query_condition_keys("s3")
170
+ >>> print(f"S3 has {len(keys)} condition keys")
171
+ """
172
+ # Use provided fetcher or create a new one
173
+ if fetcher is not None:
174
+ _fetcher = fetcher
175
+ should_close = False
176
+ else:
177
+ _fetcher = AWSServiceFetcher()
178
+ await _fetcher.__aenter__()
179
+ should_close = True
180
+
181
+ try:
182
+ keys = await sdk_query_condition_keys(_fetcher, service)
183
+ return [key["condition_key"] for key in keys]
184
+ finally:
185
+ if should_close:
186
+ await _fetcher.__aexit__(None, None, None)
187
+
188
+
189
+ async def query_arn_formats(
190
+ service: str, fetcher: AWSServiceFetcher | None = None
191
+ ) -> list[dict[str, Any]]:
192
+ """Get ARN formats for a service's resources.
193
+
194
+ Args:
195
+ service: AWS service prefix (e.g., "s3", "iam")
196
+ fetcher: Optional shared AWSServiceFetcher instance. If None, creates a new one.
197
+
198
+ Returns:
199
+ List of dictionaries with resource_type and arn_formats keys
200
+
201
+ Example:
202
+ >>> arns = await query_arn_formats("s3")
203
+ >>> for arn in arns:
204
+ ... print(f"{arn['resource_type']}: {arn['arn_formats']}")
205
+ """
206
+ # Use provided fetcher or create a new one
207
+ if fetcher is not None:
208
+ _fetcher = fetcher
209
+ should_close = False
210
+ else:
211
+ _fetcher = AWSServiceFetcher()
212
+ await _fetcher.__aenter__()
213
+ should_close = True
214
+
215
+ try:
216
+ return await query_arn_types(_fetcher, service)
217
+ finally:
218
+ if should_close:
219
+ await _fetcher.__aexit__(None, None, None)
220
+
221
+
222
+ async def list_checks() -> list[dict[str, Any]]:
223
+ """List all available validation checks with id, description, severity.
224
+
225
+ Returns:
226
+ List of dictionaries with check_id, description, and default_severity
227
+
228
+ Example:
229
+ >>> checks = await list_checks()
230
+ >>> for check in checks:
231
+ ... print(f"{check['check_id']}: {check['description']}")
232
+ """
233
+ registry = create_default_registry()
234
+ checks = []
235
+
236
+ for check_id, check_instance in registry._checks.items():
237
+ checks.append(
238
+ {
239
+ "check_id": check_id,
240
+ "description": check_instance.description,
241
+ "default_severity": check_instance.default_severity,
242
+ }
243
+ )
244
+
245
+ # Sort by check_id for consistent ordering
246
+ return sorted(checks, key=lambda x: x["check_id"])
247
+
248
+
249
+ async def get_policy_summary(policy: dict[str, Any]) -> PolicySummary:
250
+ """Analyze a policy and return summary statistics.
251
+
252
+ Args:
253
+ policy: IAM policy as a dictionary
254
+
255
+ Returns:
256
+ PolicySummary object with statistics about the policy
257
+
258
+ Example:
259
+ >>> summary = await get_policy_summary(policy_dict)
260
+ >>> print(f"Total statements: {summary.total_statements}")
261
+ >>> print(f"Services used: {summary.services_used}")
262
+ """
263
+ # Parse policy using SDK
264
+ iam_policy = parse_policy(policy)
265
+
266
+ # Get summary from SDK
267
+ summary = sdk_get_policy_summary(iam_policy)
268
+
269
+ # Extract services from actions
270
+ services = set()
271
+ for action in summary["actions"]:
272
+ if ":" in action:
273
+ service = action.split(":")[0]
274
+ services.add(service)
275
+
276
+ return PolicySummary(
277
+ total_statements=summary["statement_count"],
278
+ allow_statements=summary["allow_statements"],
279
+ deny_statements=summary["deny_statements"],
280
+ services_used=sorted(services),
281
+ actions_count=summary["action_count"],
282
+ has_wildcards=summary["has_wildcard_actions"] or summary["has_wildcard_resources"],
283
+ has_conditions=summary["condition_key_count"] > 0,
284
+ )
285
+
286
+
287
+ async def list_sensitive_actions(category: str | None = None) -> list[str]:
288
+ """List sensitive actions, optionally filtered by category.
289
+
290
+ Args:
291
+ category: Optional category filter (credential_exposure|data_access|privilege_escalation|resource_exposure)
292
+
293
+ Returns:
294
+ List of sensitive action names
295
+
296
+ Example:
297
+ >>> all_sensitive = await list_sensitive_actions()
298
+ >>> credential_actions = await list_sensitive_actions("credential_exposure")
299
+ """
300
+ if category is None:
301
+ # Return all sensitive actions
302
+ all_actions = (
303
+ CREDENTIAL_EXPOSURE_ACTIONS
304
+ | DATA_ACCESS_ACTIONS
305
+ | PRIV_ESC_ACTIONS
306
+ | RESOURCE_EXPOSURE_ACTIONS
307
+ )
308
+ return sorted(all_actions)
309
+
310
+ # Normalize category name
311
+ category_lower = category.lower()
312
+
313
+ # Map category to action set
314
+ category_map = {
315
+ "credential_exposure": CREDENTIAL_EXPOSURE_ACTIONS,
316
+ "data_access": DATA_ACCESS_ACTIONS,
317
+ "privilege_escalation": PRIV_ESC_ACTIONS,
318
+ "priv_esc": PRIV_ESC_ACTIONS, # Alias
319
+ "resource_exposure": RESOURCE_EXPOSURE_ACTIONS,
320
+ }
321
+
322
+ if category_lower not in category_map:
323
+ valid_categories = [k for k in category_map.keys() if not k.endswith("_esc")]
324
+ raise ValueError(
325
+ f"Invalid category '{category}'. Must be one of: {', '.join(valid_categories)}"
326
+ )
327
+
328
+ return sorted(category_map[category_lower])
329
+
330
+
331
+ async def get_condition_requirements(action: str) -> dict[str, Any] | None:
332
+ """Get required conditions for an action.
333
+
334
+ This function checks if the action has any condition requirements
335
+ based on the condition requirements configuration.
336
+
337
+ Args:
338
+ action: Full action name (e.g., "iam:PassRole", "s3:GetObject")
339
+
340
+ Returns:
341
+ Dictionary with condition requirements including severity, suggestion_text,
342
+ and required_conditions, or None if no requirements found.
343
+
344
+ Example:
345
+ >>> req = await get_condition_requirements("iam:PassRole")
346
+ >>> if req:
347
+ ... print(req["severity"]) # "high"
348
+ ... print(req["suggestion_text"]) # Guidance on how to fix
349
+ """
350
+ import re
351
+
352
+ try:
353
+ from iam_validator.core.config.condition_requirements import (
354
+ CONDITION_REQUIREMENTS,
355
+ )
356
+ except ImportError:
357
+ return None
358
+
359
+ # CONDITION_REQUIREMENTS is a list of requirement dicts
360
+ # Each has either "actions" (list) or "action_patterns" (regex list)
361
+ for requirement in CONDITION_REQUIREMENTS:
362
+ # Check direct action match
363
+ if "actions" in requirement and action in requirement["actions"]:
364
+ return {
365
+ "action": action,
366
+ "severity": requirement.get("severity", "medium"),
367
+ "suggestion_text": requirement.get("suggestion_text", ""),
368
+ "required_conditions": requirement.get("required_conditions", []),
369
+ }
370
+
371
+ # Check pattern match
372
+ if "action_patterns" in requirement:
373
+ for pattern in requirement["action_patterns"]:
374
+ if re.match(pattern, action):
375
+ return {
376
+ "action": action,
377
+ "severity": requirement.get("severity", "medium"),
378
+ "suggestion_text": requirement.get("suggestion_text", ""),
379
+ "required_conditions": requirement.get("required_conditions", []),
380
+ }
381
+
382
+ return None
383
+
384
+
385
+ __all__ = [
386
+ "query_service_actions",
387
+ "query_action_details",
388
+ "expand_wildcard_action",
389
+ "query_condition_keys",
390
+ "query_arn_formats",
391
+ "list_checks",
392
+ "get_policy_summary",
393
+ "list_sensitive_actions",
394
+ "get_condition_requirements",
395
+ ]