iam-policy-validator 1.14.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.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Wildcard resource check - detects Resource: '*' in IAM policies."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from iam_validator.checks.utils.action_parser import get_action_case_insensitive, parse_action
|
|
8
|
+
from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
|
|
9
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
10
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
11
|
+
from iam_validator.core.models import ActionDetail, ServiceDetail, Statement, ValidationIssue
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Module-level cache for action resource support lookups.
|
|
16
|
+
# Maps action name (e.g., "s3:GetObject") to whether it supports resource-level permissions.
|
|
17
|
+
# True = supports resources (should be flagged for wildcard)
|
|
18
|
+
# False = doesn't support resources (wildcard is appropriate)
|
|
19
|
+
# None = unknown (be conservative, assume it supports resources)
|
|
20
|
+
_action_resource_support_cache: dict[str, bool | None] = {}
|
|
21
|
+
|
|
22
|
+
# Module-level cache for action access level lookups.
|
|
23
|
+
# Maps action name (e.g., "s3:ListBuckets") to its access level.
|
|
24
|
+
# "list" = list-level action (safe with wildcards)
|
|
25
|
+
# Other values or None = unknown
|
|
26
|
+
_action_access_level_cache: dict[str, str | None] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_access_level(action_detail: ActionDetail) -> str:
|
|
30
|
+
"""Derive access level from action annotations.
|
|
31
|
+
|
|
32
|
+
AWS API provides Properties dict with boolean flags instead of AccessLevel string.
|
|
33
|
+
We derive the access level from these flags.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
action_detail: Action detail object with annotations
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Access level string: "permissions-management", "tagging", "write", "list", or "read"
|
|
40
|
+
"""
|
|
41
|
+
if not action_detail.annotations:
|
|
42
|
+
return "unknown"
|
|
43
|
+
|
|
44
|
+
props = action_detail.annotations.get("Properties", {})
|
|
45
|
+
if not props:
|
|
46
|
+
return "unknown"
|
|
47
|
+
|
|
48
|
+
# Check flags in priority order
|
|
49
|
+
if props.get("IsPermissionManagement"):
|
|
50
|
+
return "permissions-management"
|
|
51
|
+
if props.get("IsTaggingOnly"):
|
|
52
|
+
return "tagging"
|
|
53
|
+
if props.get("IsWrite"):
|
|
54
|
+
return "write"
|
|
55
|
+
if props.get("IsList"):
|
|
56
|
+
return "list"
|
|
57
|
+
|
|
58
|
+
# Default to read if none of the above
|
|
59
|
+
return "read"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def clear_resource_support_cache() -> None:
|
|
63
|
+
"""Clear the action resource support and access level caches.
|
|
64
|
+
|
|
65
|
+
This is primarily useful for testing to ensure a clean state between tests.
|
|
66
|
+
In production, the cache persists for the lifetime of the process, which is
|
|
67
|
+
beneficial as AWS action definitions don't change frequently.
|
|
68
|
+
"""
|
|
69
|
+
_action_resource_support_cache.clear()
|
|
70
|
+
_action_access_level_cache.clear()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WildcardResourceCheck(PolicyCheck):
|
|
74
|
+
"""Checks for wildcard resources (Resource: '*') which grant access to all resources."""
|
|
75
|
+
|
|
76
|
+
check_id: ClassVar[str] = "wildcard_resource"
|
|
77
|
+
description: ClassVar[str] = "Checks for wildcard resources (*)"
|
|
78
|
+
default_severity: ClassVar[str] = "medium"
|
|
79
|
+
|
|
80
|
+
async def execute(
|
|
81
|
+
self,
|
|
82
|
+
statement: Statement,
|
|
83
|
+
statement_idx: int,
|
|
84
|
+
fetcher: AWSServiceFetcher,
|
|
85
|
+
config: CheckConfig,
|
|
86
|
+
) -> list[ValidationIssue]:
|
|
87
|
+
"""Execute wildcard resource check on a statement."""
|
|
88
|
+
issues = []
|
|
89
|
+
|
|
90
|
+
# Only check Allow statements
|
|
91
|
+
if statement.effect != "Allow":
|
|
92
|
+
return issues
|
|
93
|
+
|
|
94
|
+
actions = statement.get_actions()
|
|
95
|
+
resources = statement.get_resources()
|
|
96
|
+
|
|
97
|
+
# Check for wildcard resource (Resource: "*")
|
|
98
|
+
if "*" in resources:
|
|
99
|
+
# First, filter out actions that don't support resource-level permissions
|
|
100
|
+
# These actions legitimately require Resource: "*"
|
|
101
|
+
actions_requiring_specific_resources = await self._filter_actions_requiring_resources(
|
|
102
|
+
actions, fetcher
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# If all actions don't support resources, wildcard is appropriate - no issue
|
|
106
|
+
if not actions_requiring_specific_resources:
|
|
107
|
+
return issues
|
|
108
|
+
|
|
109
|
+
# Use filtered actions for the rest of the check
|
|
110
|
+
actions = actions_requiring_specific_resources
|
|
111
|
+
# Check if all actions are in the allowed_wildcards list
|
|
112
|
+
# allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
|
|
113
|
+
# to all matching AWS actions using the AWS API, then checking if the policy's
|
|
114
|
+
# actions are in that expanded list. This ensures only validated AWS actions
|
|
115
|
+
# are allowed with Resource: "*".
|
|
116
|
+
allowed_wildcards_config = config.config.get("allowed_wildcards", [])
|
|
117
|
+
allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
|
|
118
|
+
|
|
119
|
+
# Check if ALL actions (excluding full wildcard "*") are in the expanded list
|
|
120
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
121
|
+
|
|
122
|
+
if (allowed_wildcards_config or allowed_wildcards_expanded) and non_wildcard_actions:
|
|
123
|
+
# Strategy 1: Check literal pattern match (fast path)
|
|
124
|
+
# If policy action matches config pattern literally, allow it
|
|
125
|
+
# Example: Policy has "iam:Get*", config has "iam:Get*" -> match
|
|
126
|
+
all_actions_allowed_literal = all(
|
|
127
|
+
action in allowed_wildcards_config for action in non_wildcard_actions
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if all_actions_allowed_literal:
|
|
131
|
+
# All actions match literally, Resource: "*" is acceptable
|
|
132
|
+
return issues
|
|
133
|
+
|
|
134
|
+
# Strategy 2: Check expanded pattern match (comprehensive path)
|
|
135
|
+
# Expand both policy actions and config patterns, then compare
|
|
136
|
+
# Example: Policy has "iam:Get*" -> ["iam:GetUser", ...],
|
|
137
|
+
# config has "iam:Get*" -> ["iam:GetUser", ...] -> all match
|
|
138
|
+
if allowed_wildcards_expanded:
|
|
139
|
+
expanded_statement_actions = await expand_wildcard_actions(
|
|
140
|
+
non_wildcard_actions, fetcher
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Check if all expanded actions are in the expanded allowed list (exact match)
|
|
144
|
+
all_actions_allowed_expanded = all(
|
|
145
|
+
action in allowed_wildcards_expanded
|
|
146
|
+
for action in expanded_statement_actions
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# If all actions are in the expanded list, skip the wildcard resource warning
|
|
150
|
+
if all_actions_allowed_expanded:
|
|
151
|
+
# All actions are safe, Resource: "*" is acceptable
|
|
152
|
+
return issues
|
|
153
|
+
|
|
154
|
+
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
155
|
+
# Build a helpful message showing which actions require specific resources
|
|
156
|
+
custom_message = config.config.get("message")
|
|
157
|
+
if custom_message:
|
|
158
|
+
message = custom_message
|
|
159
|
+
else:
|
|
160
|
+
# Build default message with action list
|
|
161
|
+
# Note: actions_requiring_specific_resources is guaranteed non-empty here
|
|
162
|
+
# because we return early above if it's empty
|
|
163
|
+
sorted_actions = sorted(actions_requiring_specific_resources)
|
|
164
|
+
if len(sorted_actions) <= 5:
|
|
165
|
+
action_list = ", ".join(f"`{a}`" for a in sorted_actions)
|
|
166
|
+
else:
|
|
167
|
+
action_list = ", ".join(f"`{a}`" for a in sorted_actions[:5])
|
|
168
|
+
action_list += f" (+{len(sorted_actions) - 5} more)"
|
|
169
|
+
message = (
|
|
170
|
+
f'Statement applies to all resources `"*"`. '
|
|
171
|
+
f"Actions that support resource-level permissions: {action_list}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
suggestion = config.config.get(
|
|
175
|
+
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
176
|
+
)
|
|
177
|
+
example = config.config.get("example", "")
|
|
178
|
+
|
|
179
|
+
issues.append(
|
|
180
|
+
ValidationIssue(
|
|
181
|
+
severity=self.get_severity(config),
|
|
182
|
+
statement_sid=statement.sid,
|
|
183
|
+
statement_index=statement_idx,
|
|
184
|
+
issue_type="overly_permissive",
|
|
185
|
+
message=message,
|
|
186
|
+
suggestion=suggestion,
|
|
187
|
+
example=example if example else None,
|
|
188
|
+
line_number=statement.line_number,
|
|
189
|
+
field_name="resource",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return issues
|
|
194
|
+
|
|
195
|
+
async def _get_expanded_allowed_wildcards(
|
|
196
|
+
self, config: CheckConfig, fetcher: AWSServiceFetcher
|
|
197
|
+
) -> frozenset[str]:
|
|
198
|
+
"""Get and expand allowed_wildcards configuration.
|
|
199
|
+
|
|
200
|
+
This method retrieves wildcard patterns from the allowed_wildcards config
|
|
201
|
+
and expands them using the AWS API to get all matching actual AWS actions.
|
|
202
|
+
|
|
203
|
+
How it works:
|
|
204
|
+
1. Retrieves patterns from config (e.g., ["ec2:Describe*", "s3:List*"])
|
|
205
|
+
2. Expands each pattern using AWS API:
|
|
206
|
+
- "ec2:Describe*" → ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
207
|
+
- "s3:List*" → ["s3:ListBucket", "s3:ListObjects", ...]
|
|
208
|
+
3. Returns a set of all expanded actions
|
|
209
|
+
|
|
210
|
+
This allows you to:
|
|
211
|
+
- Specify patterns like "ec2:Describe*" in config
|
|
212
|
+
- Have the validator allow specific actions like "ec2:DescribeInstances" with Resource: "*"
|
|
213
|
+
- Ensure only real AWS actions (validated via API) are allowed
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
Config: allowed_wildcards: ["ec2:Describe*"]
|
|
217
|
+
Expands to: ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
218
|
+
Policy: "Action": ["ec2:DescribeInstances"], "Resource": "*"
|
|
219
|
+
Result: ✅ Allowed (ec2:DescribeInstances is in expanded list)
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
config: The check configuration
|
|
223
|
+
fetcher: AWS service fetcher for expanding wildcards via AWS API
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A frozenset of all expanded action names from the configured patterns
|
|
227
|
+
"""
|
|
228
|
+
patterns_to_expand = config.config.get("allowed_wildcards", [])
|
|
229
|
+
|
|
230
|
+
# If no patterns configured, return empty set
|
|
231
|
+
if not patterns_to_expand or not isinstance(patterns_to_expand, list):
|
|
232
|
+
return frozenset()
|
|
233
|
+
|
|
234
|
+
# Expand the wildcard patterns using the AWS API
|
|
235
|
+
# This converts patterns like "ec2:Describe*" to actual AWS actions
|
|
236
|
+
expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
|
|
237
|
+
|
|
238
|
+
return frozenset(expanded_actions)
|
|
239
|
+
|
|
240
|
+
async def _filter_actions_requiring_resources(
|
|
241
|
+
self, actions: list[str], fetcher: AWSServiceFetcher
|
|
242
|
+
) -> list[str]:
|
|
243
|
+
"""Filter actions to only those that should be flagged for wildcard resources.
|
|
244
|
+
|
|
245
|
+
This method filters out actions that legitimately use Resource: "*":
|
|
246
|
+
1. Actions that don't support resource-level permissions (e.g., sts:GetCallerIdentity)
|
|
247
|
+
2. List-level actions (e.g., s3:ListBuckets) - these only enumerate resources
|
|
248
|
+
and are not dangerous with wildcards
|
|
249
|
+
|
|
250
|
+
Examples of actions filtered out:
|
|
251
|
+
- iam:ListUsers (list-level, must use Resource: "*")
|
|
252
|
+
- sts:GetCallerIdentity (must use Resource: "*")
|
|
253
|
+
- ec2:DescribeInstances (must use Resource: "*")
|
|
254
|
+
- s3:ListAllMyBuckets (list-level)
|
|
255
|
+
|
|
256
|
+
This method uses a module-level cache to avoid repeated lookups and
|
|
257
|
+
fetches all required services in parallel for better performance.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
actions: List of actions from the policy statement
|
|
261
|
+
fetcher: AWS service fetcher for looking up action definitions
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of actions that should be flagged for wildcard resource usage
|
|
265
|
+
"""
|
|
266
|
+
actions_requiring_resources = []
|
|
267
|
+
# Actions that need service lookup, grouped by service
|
|
268
|
+
service_actions: dict[str, list[tuple[str, str]]] = {} # service -> [(action, action_name)]
|
|
269
|
+
|
|
270
|
+
for action in actions:
|
|
271
|
+
# Full wildcard "*" - keep it (it's too broad to determine)
|
|
272
|
+
if action == "*":
|
|
273
|
+
actions_requiring_resources.append(action)
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Parse action using the utility
|
|
277
|
+
parsed = parse_action(action)
|
|
278
|
+
if not parsed:
|
|
279
|
+
# Malformed action - keep it (be conservative)
|
|
280
|
+
actions_requiring_resources.append(action)
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Wildcard in service or action name - keep it (can't determine resource support)
|
|
284
|
+
if parsed.has_wildcard:
|
|
285
|
+
actions_requiring_resources.append(action)
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
service = parsed.service
|
|
289
|
+
action_name = parsed.action_name
|
|
290
|
+
|
|
291
|
+
# Check module-level caches first
|
|
292
|
+
if action in _action_resource_support_cache and action in _action_access_level_cache:
|
|
293
|
+
cached_resource_support = _action_resource_support_cache[action]
|
|
294
|
+
cached_access_level = _action_access_level_cache[action]
|
|
295
|
+
|
|
296
|
+
# Skip list-level actions - they're safe with wildcards
|
|
297
|
+
if cached_access_level == "list":
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if cached_resource_support is True or cached_resource_support is None:
|
|
301
|
+
# Supports resources or unknown - include it
|
|
302
|
+
actions_requiring_resources.append(action)
|
|
303
|
+
# If False, action doesn't support resources - skip it
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# Group actions by service for parallel fetching
|
|
307
|
+
if service not in service_actions:
|
|
308
|
+
service_actions[service] = []
|
|
309
|
+
service_actions[service].append((action, action_name))
|
|
310
|
+
|
|
311
|
+
# If no services to look up, return early
|
|
312
|
+
if not service_actions:
|
|
313
|
+
return actions_requiring_resources
|
|
314
|
+
|
|
315
|
+
# Fetch all services in parallel
|
|
316
|
+
services = list(service_actions.keys())
|
|
317
|
+
results = await asyncio.gather(
|
|
318
|
+
*[fetcher.fetch_service_by_name(s) for s in services],
|
|
319
|
+
return_exceptions=True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Build service cache from successful results
|
|
323
|
+
service_cache: dict[str, ServiceDetail | None] = {}
|
|
324
|
+
for service, result in zip(services, results):
|
|
325
|
+
if isinstance(result, BaseException):
|
|
326
|
+
logger.debug(f"Could not look up service {service}: {result}")
|
|
327
|
+
# Mark service as failed - will keep all its actions (conservative)
|
|
328
|
+
service_cache[service] = None
|
|
329
|
+
else:
|
|
330
|
+
# Result is ServiceDetail when not an exception
|
|
331
|
+
service_cache[service] = result
|
|
332
|
+
|
|
333
|
+
# Process actions using cached service data
|
|
334
|
+
for service, action_list in service_actions.items():
|
|
335
|
+
service_detail = service_cache.get(service)
|
|
336
|
+
|
|
337
|
+
if not service_detail:
|
|
338
|
+
# Unknown service - keep all its actions (be conservative)
|
|
339
|
+
for action, _ in action_list:
|
|
340
|
+
_action_resource_support_cache[action] = None # Cache as unknown
|
|
341
|
+
_action_access_level_cache[action] = None # Cache as unknown
|
|
342
|
+
actions_requiring_resources.append(action)
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
for action, action_name in action_list:
|
|
346
|
+
# Use case-insensitive lookup since AWS actions are case-insensitive
|
|
347
|
+
action_detail = get_action_case_insensitive(service_detail.actions, action_name)
|
|
348
|
+
if not action_detail:
|
|
349
|
+
# Unknown action - keep it (be conservative)
|
|
350
|
+
_action_resource_support_cache[action] = None # Cache as unknown
|
|
351
|
+
_action_access_level_cache[action] = None # Cache as unknown
|
|
352
|
+
actions_requiring_resources.append(action)
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Get action's access level and cache it
|
|
356
|
+
access_level = _get_access_level(action_detail)
|
|
357
|
+
_action_access_level_cache[action] = access_level
|
|
358
|
+
|
|
359
|
+
# Skip list-level actions - they only enumerate resources and are safe with wildcards
|
|
360
|
+
if access_level == "list":
|
|
361
|
+
_action_resource_support_cache[action] = False # Mark as not needing resources
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Check if action supports resource-level permissions
|
|
365
|
+
# action_detail.resources is empty for actions that don't support resources
|
|
366
|
+
supports_resources = bool(action_detail.resources)
|
|
367
|
+
_action_resource_support_cache[action] = supports_resources # Cache result
|
|
368
|
+
|
|
369
|
+
if supports_resources:
|
|
370
|
+
# Action supports resources - should be flagged for wildcard
|
|
371
|
+
actions_requiring_resources.append(action)
|
|
372
|
+
# Else: action doesn't support resources, Resource: "*" is appropriate
|
|
373
|
+
|
|
374
|
+
return actions_requiring_resources
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""CLI commands for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
from .analyze import AnalyzeCommand
|
|
4
|
+
from .cache import CacheCommand
|
|
5
|
+
from .completion import CompletionCommand
|
|
6
|
+
from .download_services import DownloadServicesCommand
|
|
7
|
+
from .post_to_pr import PostToPRCommand
|
|
8
|
+
from .query import QueryCommand
|
|
9
|
+
from .validate import ValidateCommand
|
|
10
|
+
|
|
11
|
+
# All available commands
|
|
12
|
+
ALL_COMMANDS = [
|
|
13
|
+
ValidateCommand(),
|
|
14
|
+
PostToPRCommand(),
|
|
15
|
+
AnalyzeCommand(),
|
|
16
|
+
CacheCommand(),
|
|
17
|
+
DownloadServicesCommand(),
|
|
18
|
+
QueryCommand(),
|
|
19
|
+
CompletionCommand(),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ValidateCommand",
|
|
24
|
+
"PostToPRCommand",
|
|
25
|
+
"AnalyzeCommand",
|
|
26
|
+
"CacheCommand",
|
|
27
|
+
"DownloadServicesCommand",
|
|
28
|
+
"QueryCommand",
|
|
29
|
+
"CompletionCommand",
|
|
30
|
+
"ALL_COMMANDS",
|
|
31
|
+
]
|