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.
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/METADATA +16 -11
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/RECORD +41 -28
- iam_policy_validator-1.15.1.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 +68 -51
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +18 -9
- 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 +2 -0
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.7.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.7.dist-info → iam_policy_validator-1.15.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,103 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
20
20
|
description: ClassVar[str] = "Validates ARN format for resources"
|
|
21
21
|
default_severity: ClassVar[str] = "error"
|
|
22
22
|
|
|
23
|
+
def _validate_resource(
|
|
24
|
+
self,
|
|
25
|
+
resource: str,
|
|
26
|
+
arn_pattern: re.Pattern[str],
|
|
27
|
+
allow_template_variables: bool,
|
|
28
|
+
config: CheckConfig,
|
|
29
|
+
statement_sid: str | None,
|
|
30
|
+
statement_idx: int,
|
|
31
|
+
line_number: int | None,
|
|
32
|
+
field_name: str,
|
|
33
|
+
) -> ValidationIssue | None:
|
|
34
|
+
"""Validate a single resource ARN and return an issue if invalid.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
resource: The resource ARN string to validate
|
|
38
|
+
arn_pattern: Compiled regex pattern for ARN validation
|
|
39
|
+
allow_template_variables: Whether to allow template variables
|
|
40
|
+
config: Check configuration
|
|
41
|
+
statement_sid: Statement ID for error reporting
|
|
42
|
+
statement_idx: Statement index for error reporting
|
|
43
|
+
line_number: Line number for error reporting
|
|
44
|
+
field_name: Field name ("resource" or "not_resource") for error reporting
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ValidationIssue if resource is invalid, None otherwise
|
|
48
|
+
"""
|
|
49
|
+
# Skip wildcard resources (handled by security checks)
|
|
50
|
+
if resource == "*":
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
# Validate ARN length to prevent ReDoS attacks
|
|
54
|
+
if len(resource) > MAX_ARN_LENGTH:
|
|
55
|
+
return ValidationIssue(
|
|
56
|
+
severity=self.get_severity(config),
|
|
57
|
+
statement_sid=statement_sid,
|
|
58
|
+
statement_index=statement_idx,
|
|
59
|
+
issue_type="invalid_resource",
|
|
60
|
+
message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
|
|
61
|
+
resource=resource[:100] + "...",
|
|
62
|
+
suggestion="`ARN` is too long and may be invalid",
|
|
63
|
+
line_number=line_number,
|
|
64
|
+
field_name=field_name,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Check if resource contains template variables
|
|
68
|
+
has_templates = has_template_variables(resource)
|
|
69
|
+
|
|
70
|
+
# If template variables are found and allowed, normalize them for validation
|
|
71
|
+
validation_resource = resource
|
|
72
|
+
if has_templates and allow_template_variables:
|
|
73
|
+
validation_resource = normalize_template_variables(resource)
|
|
74
|
+
|
|
75
|
+
# Validate ARN format
|
|
76
|
+
try:
|
|
77
|
+
if not arn_pattern.match(validation_resource):
|
|
78
|
+
# If original resource had templates and normalization didn't help,
|
|
79
|
+
# provide a more informative message
|
|
80
|
+
if has_templates and allow_template_variables:
|
|
81
|
+
return ValidationIssue(
|
|
82
|
+
severity=self.get_severity(config),
|
|
83
|
+
statement_sid=statement_sid,
|
|
84
|
+
statement_index=statement_idx,
|
|
85
|
+
issue_type="invalid_resource",
|
|
86
|
+
message=f"Invalid `ARN` format even after normalizing template variables: `{resource}`",
|
|
87
|
+
resource=resource,
|
|
88
|
+
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource` (template variables like `${aws_account_id}` are supported)",
|
|
89
|
+
line_number=line_number,
|
|
90
|
+
field_name=field_name,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
return ValidationIssue(
|
|
94
|
+
severity=self.get_severity(config),
|
|
95
|
+
statement_sid=statement_sid,
|
|
96
|
+
statement_index=statement_idx,
|
|
97
|
+
issue_type="invalid_resource",
|
|
98
|
+
message=f"Invalid `ARN` format: `{resource}`",
|
|
99
|
+
resource=resource,
|
|
100
|
+
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource`",
|
|
101
|
+
line_number=line_number,
|
|
102
|
+
field_name=field_name,
|
|
103
|
+
)
|
|
104
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
105
|
+
# If regex matching fails (shouldn't happen with length check), treat as invalid
|
|
106
|
+
return ValidationIssue(
|
|
107
|
+
severity=self.get_severity(config),
|
|
108
|
+
statement_sid=statement_sid,
|
|
109
|
+
statement_index=statement_idx,
|
|
110
|
+
issue_type="invalid_resource",
|
|
111
|
+
message=f"Could not validate `ARN` format: `{resource}`",
|
|
112
|
+
resource=resource,
|
|
113
|
+
suggestion="`ARN` validation failed - may contain unexpected characters",
|
|
114
|
+
line_number=line_number,
|
|
115
|
+
field_name=field_name,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return None
|
|
119
|
+
|
|
23
120
|
async def execute(
|
|
24
121
|
self,
|
|
25
122
|
statement: Statement,
|
|
@@ -27,11 +124,14 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
27
124
|
fetcher: AWSServiceFetcher,
|
|
28
125
|
config: CheckConfig,
|
|
29
126
|
) -> list[ValidationIssue]:
|
|
30
|
-
"""Execute resource ARN validation on a statement.
|
|
127
|
+
"""Execute resource ARN validation on a statement.
|
|
128
|
+
|
|
129
|
+
Validates both Resource and NotResource fields to ensure all specified
|
|
130
|
+
ARNs follow the correct format.
|
|
131
|
+
"""
|
|
132
|
+
del fetcher # Unused
|
|
31
133
|
issues = []
|
|
32
134
|
|
|
33
|
-
# Get resources from statement
|
|
34
|
-
resources = statement.get_resources()
|
|
35
135
|
statement_sid = statement.sid
|
|
36
136
|
line_number = statement.line_number
|
|
37
137
|
|
|
@@ -53,83 +153,34 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
53
153
|
config.config.get("allow_template_variables", True),
|
|
54
154
|
)
|
|
55
155
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Validate ARN format
|
|
87
|
-
try:
|
|
88
|
-
if not arn_pattern.match(validation_resource):
|
|
89
|
-
# If original resource had templates and normalization didn't help,
|
|
90
|
-
# provide a more informative message
|
|
91
|
-
if has_templates and allow_template_variables:
|
|
92
|
-
issues.append(
|
|
93
|
-
ValidationIssue(
|
|
94
|
-
severity=self.get_severity(config),
|
|
95
|
-
statement_sid=statement_sid,
|
|
96
|
-
statement_index=statement_idx,
|
|
97
|
-
issue_type="invalid_resource",
|
|
98
|
-
message=f"Invalid `ARN` format even after normalizing template variables: `{resource}`",
|
|
99
|
-
resource=resource,
|
|
100
|
-
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource` (template variables like `${aws_account_id}` are supported)",
|
|
101
|
-
line_number=line_number,
|
|
102
|
-
field_name="resource",
|
|
103
|
-
)
|
|
104
|
-
)
|
|
105
|
-
else:
|
|
106
|
-
issues.append(
|
|
107
|
-
ValidationIssue(
|
|
108
|
-
severity=self.get_severity(config),
|
|
109
|
-
statement_sid=statement_sid,
|
|
110
|
-
statement_index=statement_idx,
|
|
111
|
-
issue_type="invalid_resource",
|
|
112
|
-
message=f"Invalid `ARN` format: `{resource}`",
|
|
113
|
-
resource=resource,
|
|
114
|
-
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource`",
|
|
115
|
-
line_number=line_number,
|
|
116
|
-
field_name="resource",
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
except Exception: # pylint: disable=broad-exception-caught
|
|
120
|
-
# If regex matching fails (shouldn't happen with length check), treat as invalid
|
|
121
|
-
issues.append(
|
|
122
|
-
ValidationIssue(
|
|
123
|
-
severity=self.get_severity(config),
|
|
124
|
-
statement_sid=statement_sid,
|
|
125
|
-
statement_index=statement_idx,
|
|
126
|
-
issue_type="invalid_resource",
|
|
127
|
-
message=f"Could not validate `ARN` format: `{resource}`",
|
|
128
|
-
resource=resource,
|
|
129
|
-
suggestion="`ARN` validation failed - may contain unexpected characters",
|
|
130
|
-
line_number=line_number,
|
|
131
|
-
field_name="resource",
|
|
132
|
-
)
|
|
133
|
-
)
|
|
156
|
+
# Validate Resource field
|
|
157
|
+
for resource in statement.get_resources():
|
|
158
|
+
issue = self._validate_resource(
|
|
159
|
+
resource=resource,
|
|
160
|
+
arn_pattern=arn_pattern,
|
|
161
|
+
allow_template_variables=allow_template_variables,
|
|
162
|
+
config=config,
|
|
163
|
+
statement_sid=statement_sid,
|
|
164
|
+
statement_idx=statement_idx,
|
|
165
|
+
line_number=line_number,
|
|
166
|
+
field_name="resource",
|
|
167
|
+
)
|
|
168
|
+
if issue:
|
|
169
|
+
issues.append(issue)
|
|
170
|
+
|
|
171
|
+
# Validate NotResource field (same validation - typos in NotResource are equally problematic)
|
|
172
|
+
for resource in statement.get_not_resources():
|
|
173
|
+
issue = self._validate_resource(
|
|
174
|
+
resource=resource,
|
|
175
|
+
arn_pattern=arn_pattern,
|
|
176
|
+
allow_template_variables=allow_template_variables,
|
|
177
|
+
config=config,
|
|
178
|
+
statement_sid=statement_sid,
|
|
179
|
+
statement_idx=statement_idx,
|
|
180
|
+
line_number=line_number,
|
|
181
|
+
field_name="not_resource",
|
|
182
|
+
)
|
|
183
|
+
if issue:
|
|
184
|
+
issues.append(issue)
|
|
134
185
|
|
|
135
186
|
return issues
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
"""Wildcard resource check - detects Resource: '*' in IAM policies.
|
|
1
|
+
"""Wildcard resource check - detects Resource: '*' in IAM policies.
|
|
2
|
+
|
|
3
|
+
This check detects statements with Resource: '*' that could grant overly broad access.
|
|
4
|
+
It intelligently adjusts severity based on conditions that restrict resource scope:
|
|
5
|
+
|
|
6
|
+
- Global resource-scoping conditions (aws:ResourceAccount, aws:ResourceOrgID, aws:ResourceOrgPaths)
|
|
7
|
+
always lower severity since they apply to all services.
|
|
8
|
+
- Resource tag conditions (aws:ResourceTag/*) lower severity only if ALL actions in the
|
|
9
|
+
statement support the condition (validated against AWS service definitions).
|
|
10
|
+
"""
|
|
2
11
|
|
|
3
12
|
import asyncio
|
|
4
13
|
import logging
|
|
@@ -8,7 +17,9 @@ from iam_validator.checks.utils.action_parser import get_action_case_insensitive
|
|
|
8
17
|
from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
|
|
9
18
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
10
19
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
20
|
+
from iam_validator.core.config.aws_global_conditions import GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS
|
|
11
21
|
from iam_validator.core.models import ActionDetail, ServiceDetail, Statement, ValidationIssue
|
|
22
|
+
from iam_validator.sdk.policy_utils import extract_condition_keys_from_statement
|
|
12
23
|
|
|
13
24
|
logger = logging.getLogger(__name__)
|
|
14
25
|
|
|
@@ -70,6 +81,54 @@ def clear_resource_support_cache() -> None:
|
|
|
70
81
|
_action_access_level_cache.clear()
|
|
71
82
|
|
|
72
83
|
|
|
84
|
+
def _has_global_resource_scoping(condition_keys: set[str]) -> bool:
|
|
85
|
+
"""Check if any global resource-scoping conditions are present.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
condition_keys: Set of condition keys from the statement
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if any global resource-scoping condition is present
|
|
92
|
+
"""
|
|
93
|
+
return bool(condition_keys & GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _validate_condition_key_support(
|
|
97
|
+
actions: list[str],
|
|
98
|
+
condition_key: str,
|
|
99
|
+
fetcher: AWSServiceFetcher,
|
|
100
|
+
) -> tuple[bool, list[str]]:
|
|
101
|
+
"""Validate if all actions support a specific condition key.
|
|
102
|
+
|
|
103
|
+
This is a generic function that works for any condition key,
|
|
104
|
+
including aws:ResourceTag/*, service-specific tags, etc.
|
|
105
|
+
|
|
106
|
+
Uses parallel execution for performance when validating multiple actions.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
actions: List of actions to validate
|
|
110
|
+
condition_key: The condition key to check support for
|
|
111
|
+
fetcher: AWS service fetcher for looking up service definitions
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tuple of (all_support, unsupported_actions) where all_support is True
|
|
115
|
+
if all actions support the condition key
|
|
116
|
+
"""
|
|
117
|
+
# Validate all actions in parallel for performance using centralized fetcher method
|
|
118
|
+
results = await asyncio.gather(
|
|
119
|
+
*[fetcher.is_condition_key_supported(action, condition_key) for action in actions],
|
|
120
|
+
return_exceptions=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
unsupported = []
|
|
124
|
+
for action, result in zip(actions, results):
|
|
125
|
+
# Treat exceptions as unsupported (conservative)
|
|
126
|
+
if isinstance(result, BaseException) or not result:
|
|
127
|
+
unsupported.append(action)
|
|
128
|
+
|
|
129
|
+
return (len(unsupported) == 0, unsupported)
|
|
130
|
+
|
|
131
|
+
|
|
73
132
|
class WildcardResourceCheck(PolicyCheck):
|
|
74
133
|
"""Checks for wildcard resources (Resource: '*') which grant access to all resources."""
|
|
75
134
|
|
|
@@ -152,6 +211,15 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
152
211
|
return issues
|
|
153
212
|
|
|
154
213
|
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
214
|
+
# First, determine if severity should be adjusted based on conditions
|
|
215
|
+
base_severity = self.get_severity(config)
|
|
216
|
+
adjusted_severity, adjustment_reason = await self._determine_severity_adjustment(
|
|
217
|
+
statement,
|
|
218
|
+
actions_requiring_specific_resources,
|
|
219
|
+
fetcher,
|
|
220
|
+
base_severity,
|
|
221
|
+
)
|
|
222
|
+
|
|
155
223
|
# Build a helpful message showing which actions require specific resources
|
|
156
224
|
custom_message = config.config.get("message")
|
|
157
225
|
if custom_message:
|
|
@@ -166,10 +234,11 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
166
234
|
else:
|
|
167
235
|
action_list = ", ".join(f"`{a}`" for a in sorted_actions[:5])
|
|
168
236
|
action_list += f" (+{len(sorted_actions) - 5} more)"
|
|
169
|
-
message = (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
237
|
+
message = 'Statement applies to all resources (`"*"`)'
|
|
238
|
+
|
|
239
|
+
# Add adjustment reason if present
|
|
240
|
+
if adjustment_reason:
|
|
241
|
+
message += f". {adjustment_reason}"
|
|
173
242
|
|
|
174
243
|
suggestion = config.config.get(
|
|
175
244
|
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
@@ -178,7 +247,7 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
178
247
|
|
|
179
248
|
issues.append(
|
|
180
249
|
ValidationIssue(
|
|
181
|
-
severity=
|
|
250
|
+
severity=adjusted_severity,
|
|
182
251
|
statement_sid=statement.sid,
|
|
183
252
|
statement_index=statement_idx,
|
|
184
253
|
issue_type="overly_permissive",
|
|
@@ -237,6 +306,67 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
237
306
|
|
|
238
307
|
return frozenset(expanded_actions)
|
|
239
308
|
|
|
309
|
+
async def _determine_severity_adjustment(
|
|
310
|
+
self,
|
|
311
|
+
statement: Statement,
|
|
312
|
+
actions: list[str],
|
|
313
|
+
fetcher: AWSServiceFetcher,
|
|
314
|
+
base_severity: str,
|
|
315
|
+
) -> tuple[str, str | None]:
|
|
316
|
+
"""Determine if severity should be adjusted based on resource-scoping conditions.
|
|
317
|
+
|
|
318
|
+
This method checks if the statement has conditions that meaningfully restrict
|
|
319
|
+
resource scope:
|
|
320
|
+
1. Global resource-scoping conditions (aws:ResourceAccount, etc.) always lower severity
|
|
321
|
+
2. Resource tag conditions (aws:ResourceTag/*) lower severity only if ALL actions support them
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
statement: The policy statement being checked
|
|
325
|
+
actions: List of actions that require specific resources
|
|
326
|
+
fetcher: AWS service fetcher for validating condition key support
|
|
327
|
+
base_severity: The default severity level
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Tuple of (adjusted_severity, reason) where reason explains the adjustment
|
|
331
|
+
"""
|
|
332
|
+
condition_keys = extract_condition_keys_from_statement(statement)
|
|
333
|
+
if not condition_keys:
|
|
334
|
+
return (base_severity, None)
|
|
335
|
+
|
|
336
|
+
# Check for global resource-scoping conditions (always valid for all services)
|
|
337
|
+
if _has_global_resource_scoping(condition_keys):
|
|
338
|
+
global_keys = condition_keys & GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS
|
|
339
|
+
return (
|
|
340
|
+
"low",
|
|
341
|
+
f"Severity lowered: resource scope restricted by `{', '.join(sorted(global_keys))}`",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Check for aws:ResourceTag conditions (must validate per-action support)
|
|
345
|
+
resource_tag_keys = {k for k in condition_keys if k.startswith("aws:ResourceTag/")}
|
|
346
|
+
if resource_tag_keys:
|
|
347
|
+
# Use the first tag key for validation (all should have same support pattern)
|
|
348
|
+
tag_key = next(iter(resource_tag_keys))
|
|
349
|
+
all_support, unsupported = await _validate_condition_key_support(
|
|
350
|
+
actions, tag_key, fetcher
|
|
351
|
+
)
|
|
352
|
+
if all_support:
|
|
353
|
+
return (
|
|
354
|
+
"low",
|
|
355
|
+
f"Severity lowered: resource scope restricted by `{', '.join(sorted(resource_tag_keys))}`",
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
# Tag condition present but not all actions support it
|
|
359
|
+
unsupported_display = unsupported[:3]
|
|
360
|
+
more = f" (+{len(unsupported) - 3} more)" if len(unsupported) > 3 else ""
|
|
361
|
+
return (
|
|
362
|
+
base_severity,
|
|
363
|
+
f"Note: `aws:ResourceTag` condition found but these actions don't support "
|
|
364
|
+
f"resource tags: `{', '.join(unsupported_display)}`{more}",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Has conditions but none that scope resources
|
|
368
|
+
return (base_severity, None)
|
|
369
|
+
|
|
240
370
|
async def _filter_actions_requiring_resources(
|
|
241
371
|
self, actions: list[str], fetcher: AWSServiceFetcher
|
|
242
372
|
) -> list[str]:
|
|
@@ -4,6 +4,7 @@ from .analyze import AnalyzeCommand
|
|
|
4
4
|
from .cache import CacheCommand
|
|
5
5
|
from .completion import CompletionCommand
|
|
6
6
|
from .download_services import DownloadServicesCommand
|
|
7
|
+
from .mcp import MCPCommand
|
|
7
8
|
from .post_to_pr import PostToPRCommand
|
|
8
9
|
from .query import QueryCommand
|
|
9
10
|
from .validate import ValidateCommand
|
|
@@ -17,6 +18,7 @@ ALL_COMMANDS = [
|
|
|
17
18
|
DownloadServicesCommand(),
|
|
18
19
|
QueryCommand(),
|
|
19
20
|
CompletionCommand(),
|
|
21
|
+
MCPCommand(),
|
|
20
22
|
]
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
@@ -27,5 +29,6 @@ __all__ = [
|
|
|
27
29
|
"DownloadServicesCommand",
|
|
28
30
|
"QueryCommand",
|
|
29
31
|
"CompletionCommand",
|
|
32
|
+
"MCPCommand",
|
|
30
33
|
"ALL_COMMANDS",
|
|
31
34
|
]
|
iam_validator/commands/cache.py
CHANGED
|
@@ -40,7 +40,7 @@ Examples:
|
|
|
40
40
|
# Clear all cached AWS service definitions
|
|
41
41
|
iam-validator cache clear
|
|
42
42
|
|
|
43
|
-
# Refresh
|
|
43
|
+
# Refresh all cached services with fresh data
|
|
44
44
|
iam-validator cache refresh
|
|
45
45
|
|
|
46
46
|
# Pre-fetch common AWS services
|
|
@@ -88,7 +88,7 @@ Examples:
|
|
|
88
88
|
|
|
89
89
|
# Refresh subcommand
|
|
90
90
|
refresh_parser = subparsers.add_parser(
|
|
91
|
-
"refresh", help="
|
|
91
|
+
"refresh", help="Refresh all cached AWS services with fresh data"
|
|
92
92
|
)
|
|
93
93
|
refresh_parser.add_argument(
|
|
94
94
|
"--config",
|
|
@@ -314,44 +314,86 @@ Examples:
|
|
|
314
314
|
async def _refresh_cache(
|
|
315
315
|
self, cache_enabled: bool, cache_ttl_seconds: int, cache_directory: str | None
|
|
316
316
|
) -> int:
|
|
317
|
-
"""
|
|
317
|
+
"""Refresh all cached services with fresh data from AWS."""
|
|
318
318
|
if not cache_enabled:
|
|
319
319
|
console.print("[red]Error:[/red] Cache is disabled in config")
|
|
320
320
|
console.print("Enable cache by setting 'cache_enabled: true' in your config")
|
|
321
321
|
return 1
|
|
322
322
|
|
|
323
|
-
|
|
323
|
+
# Get cache directory
|
|
324
|
+
cache_dir = (
|
|
325
|
+
Path(cache_directory) if cache_directory else ServiceFileStorage.get_cache_directory()
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if not cache_dir.exists():
|
|
329
|
+
console.print("[yellow]Cache directory does not exist, nothing to refresh[/yellow]")
|
|
330
|
+
console.print("Run 'iam-validator cache prefetch' to populate the cache first")
|
|
331
|
+
return 0
|
|
332
|
+
|
|
333
|
+
# Get list of cached services from cache files
|
|
334
|
+
cache_files = list(cache_dir.glob("*.json"))
|
|
335
|
+
if not cache_files:
|
|
336
|
+
console.print("[yellow]No services cached yet, nothing to refresh[/yellow]")
|
|
337
|
+
console.print("Run 'iam-validator cache prefetch' to populate the cache first")
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
# Extract service names from cache files
|
|
341
|
+
cached_services: list[str] = []
|
|
342
|
+
for f in cache_files:
|
|
343
|
+
if f.stem == "services_list":
|
|
344
|
+
continue # Skip the services list file, we'll refresh it separately
|
|
345
|
+
# Extract service name (before underscore or full name)
|
|
346
|
+
name = f.stem.split("_")[0] if "_" in f.stem else f.stem
|
|
347
|
+
cached_services.append(name)
|
|
348
|
+
|
|
349
|
+
cached_services.sort()
|
|
350
|
+
console.print(f"[cyan]Refreshing {len(cached_services)} cached services...[/cyan]")
|
|
324
351
|
|
|
325
|
-
# Create fetcher and clear cache
|
|
326
352
|
async with AWSServiceFetcher(
|
|
327
353
|
enable_cache=cache_enabled,
|
|
328
354
|
cache_ttl=cache_ttl_seconds,
|
|
329
355
|
cache_dir=cache_directory,
|
|
330
|
-
prefetch_common=False, #
|
|
356
|
+
prefetch_common=False, # We'll refresh manually
|
|
331
357
|
) as fetcher:
|
|
332
|
-
#
|
|
333
|
-
console.print("
|
|
334
|
-
await fetcher.clear_caches()
|
|
335
|
-
|
|
336
|
-
# Prefetch common services
|
|
337
|
-
console.print("Fetching fresh AWS service definitions...")
|
|
358
|
+
# First refresh the services list
|
|
359
|
+
console.print("Refreshing AWS services list...")
|
|
338
360
|
services = await fetcher.fetch_services()
|
|
339
|
-
console.print(f"[green]✓[/green]
|
|
361
|
+
console.print(f"[green]✓[/green] Refreshed services list ({len(services)} services)")
|
|
362
|
+
|
|
363
|
+
# Build a set of valid service names for validation
|
|
364
|
+
valid_services = {svc.service for svc in services}
|
|
365
|
+
|
|
366
|
+
# Refresh each cached service
|
|
367
|
+
console.print(f"Refreshing {len(cached_services)} cached service definitions...")
|
|
368
|
+
refreshed = 0
|
|
369
|
+
failed = 0
|
|
370
|
+
skipped = 0
|
|
371
|
+
|
|
372
|
+
for service_name in cached_services:
|
|
373
|
+
# Skip services that no longer exist in AWS
|
|
374
|
+
if service_name not in valid_services:
|
|
375
|
+
logger.warning(f"Service '{service_name}' no longer exists, skipping")
|
|
376
|
+
skipped += 1
|
|
377
|
+
continue
|
|
340
378
|
|
|
341
|
-
# Prefetch common services
|
|
342
|
-
console.print("Pre-fetching common services...")
|
|
343
|
-
prefetched = 0
|
|
344
|
-
for service_name in fetcher.COMMON_SERVICES:
|
|
345
379
|
try:
|
|
346
380
|
await fetcher.fetch_service_by_name(service_name)
|
|
347
|
-
|
|
381
|
+
refreshed += 1
|
|
348
382
|
except Exception as e:
|
|
349
|
-
logger.warning(f"Failed to
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
logger.warning(f"Failed to refresh {service_name}: {e}")
|
|
384
|
+
failed += 1
|
|
385
|
+
|
|
386
|
+
# Print summary
|
|
387
|
+
if failed == 0 and skipped == 0:
|
|
388
|
+
console.print(f"[green]✓[/green] Refreshed {refreshed} services successfully")
|
|
389
|
+
else:
|
|
390
|
+
console.print(
|
|
391
|
+
f"[yellow]![/yellow] Refreshed {refreshed} services, "
|
|
392
|
+
f"{failed} failed, {skipped} skipped (no longer exist)"
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
console.print("[green]✓[/green] Cache refresh complete")
|
|
396
|
+
return 0 if failed == 0 else 1
|
|
355
397
|
|
|
356
398
|
async def _prefetch_services(
|
|
357
399
|
self, cache_enabled: bool, cache_ttl_seconds: int, cache_directory: str | None
|