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