iam-policy-validator 1.4.0__py3-none-any.whl → 1.6.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.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
- iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +20 -4
- iam_validator/checks/action_condition_enforcement.py +165 -8
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +24 -2
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/principal_validation.py +497 -3
- iam_validator/checks/sensitive_action.py +250 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/validate.py +72 -13
- iam_validator/core/aws_fetcher.py +114 -64
- iam_validator/core/check_registry.py +167 -29
- 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 +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
- iam_validator/core/config/defaults.py +523 -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 +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -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 +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
- iam_policy_validator-1.4.0.dist-info/RECORD +0 -56
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/checks/security_best_practices.py +0 -536
- iam_validator/core/aws_global_conditions.py +0 -137
- iam_validator/core/defaults.py +0 -393
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,6 +5,7 @@ This built-in check ensures that specific actions have required conditions.
|
|
|
5
5
|
Supports ALL types of conditions: MFA, IP, VPC, time, tags, encryption, etc.
|
|
6
6
|
|
|
7
7
|
Supports advanced "all_of" and "any_of" logic for both actions and conditions.
|
|
8
|
+
Supports both STATEMENT-LEVEL and POLICY-LEVEL enforcement.
|
|
8
9
|
|
|
9
10
|
Common use cases:
|
|
10
11
|
- iam:PassRole must have iam:PassedToService condition
|
|
@@ -12,6 +13,7 @@ Common use cases:
|
|
|
12
13
|
- Actions must have source IP restrictions
|
|
13
14
|
- Resources must have required tags
|
|
14
15
|
- Combine multiple conditions (MFA + IP + Tags)
|
|
16
|
+
- Policy-level: Ensure ALL statements granting certain actions have MFA
|
|
15
17
|
|
|
16
18
|
Configuration in iam-validator.yaml:
|
|
17
19
|
|
|
@@ -21,6 +23,7 @@ Configuration in iam-validator.yaml:
|
|
|
21
23
|
severity: error
|
|
22
24
|
description: "Enforce specific conditions for specific actions"
|
|
23
25
|
|
|
26
|
+
# STATEMENT-LEVEL: Check individual statements (default)
|
|
24
27
|
action_condition_requirements:
|
|
25
28
|
# BASIC: Simple action with required condition
|
|
26
29
|
- actions:
|
|
@@ -90,15 +93,56 @@ Configuration in iam-validator.yaml:
|
|
|
90
93
|
- "iam:*"
|
|
91
94
|
- "s3:DeleteBucket"
|
|
92
95
|
description: "These dangerous actions should never be used"
|
|
96
|
+
|
|
97
|
+
# POLICY-LEVEL: Scan entire policy and enforce conditions across all matching statements
|
|
98
|
+
policy_level_requirements:
|
|
99
|
+
# Example: If ANY statement grants privilege escalation actions,
|
|
100
|
+
# then ALL such statements must have MFA
|
|
101
|
+
- actions:
|
|
102
|
+
any_of:
|
|
103
|
+
- "iam:CreateUser"
|
|
104
|
+
- "iam:AttachUserPolicy"
|
|
105
|
+
- "iam:PutUserPolicy"
|
|
106
|
+
scope: "policy"
|
|
107
|
+
required_conditions:
|
|
108
|
+
- condition_key: "aws:MultiFactorAuthPresent"
|
|
109
|
+
expected_value: true
|
|
110
|
+
description: "Privilege escalation actions require MFA across entire policy"
|
|
111
|
+
severity: "critical"
|
|
112
|
+
|
|
113
|
+
# Example: All admin actions across the policy must have MFA
|
|
114
|
+
- actions:
|
|
115
|
+
any_of:
|
|
116
|
+
- "iam:*"
|
|
117
|
+
- "s3:*"
|
|
118
|
+
scope: "policy"
|
|
119
|
+
required_conditions:
|
|
120
|
+
all_of:
|
|
121
|
+
- condition_key: "aws:MultiFactorAuthPresent"
|
|
122
|
+
expected_value: true
|
|
123
|
+
- condition_key: "aws:SourceIp"
|
|
124
|
+
apply_to: "all_matching_statements"
|
|
125
|
+
|
|
126
|
+
# Example: Ensure no statement in the policy allows dangerous combinations
|
|
127
|
+
- actions:
|
|
128
|
+
all_of:
|
|
129
|
+
- "iam:CreateAccessKey"
|
|
130
|
+
- "iam:UpdateAccessKey"
|
|
131
|
+
scope: "policy"
|
|
132
|
+
severity: "critical"
|
|
133
|
+
description: "Dangerous combination of actions detected in policy"
|
|
93
134
|
"""
|
|
94
135
|
|
|
95
136
|
import re
|
|
96
|
-
from typing import Any
|
|
137
|
+
from typing import TYPE_CHECKING, Any
|
|
97
138
|
|
|
98
139
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
99
140
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
100
141
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
101
142
|
|
|
143
|
+
if TYPE_CHECKING:
|
|
144
|
+
from iam_validator.core.models import IAMPolicy
|
|
145
|
+
|
|
102
146
|
|
|
103
147
|
class ActionConditionEnforcementCheck(PolicyCheck):
|
|
104
148
|
"""Enforces specific condition requirements for specific actions with all_of/any_of support."""
|
|
@@ -122,7 +166,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
122
166
|
fetcher: AWSServiceFetcher,
|
|
123
167
|
config: CheckConfig,
|
|
124
168
|
) -> list[ValidationIssue]:
|
|
125
|
-
"""Execute condition enforcement check."""
|
|
169
|
+
"""Execute statement-level condition enforcement check."""
|
|
126
170
|
issues = []
|
|
127
171
|
|
|
128
172
|
# Only check Allow statements
|
|
@@ -186,6 +230,124 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
186
230
|
|
|
187
231
|
return issues
|
|
188
232
|
|
|
233
|
+
async def execute_policy(
|
|
234
|
+
self,
|
|
235
|
+
policy: "IAMPolicy",
|
|
236
|
+
policy_file: str,
|
|
237
|
+
fetcher: AWSServiceFetcher,
|
|
238
|
+
config: CheckConfig,
|
|
239
|
+
**kwargs,
|
|
240
|
+
) -> list[ValidationIssue]:
|
|
241
|
+
"""
|
|
242
|
+
Execute policy-level condition enforcement check.
|
|
243
|
+
|
|
244
|
+
This method scans the entire policy and enforces that ALL statements granting
|
|
245
|
+
certain actions must have specific conditions. This is useful for ensuring
|
|
246
|
+
consistent security controls across the entire policy.
|
|
247
|
+
|
|
248
|
+
Example use case:
|
|
249
|
+
- "If ANY statement in the policy grants iam:CreateUser, iam:AttachUserPolicy,
|
|
250
|
+
or iam:PutUserPolicy, then ALL such statements must have MFA condition."
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
policy: The complete IAM policy to check
|
|
254
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
255
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
256
|
+
config: Configuration for this check instance
|
|
257
|
+
**kwargs: Additional context (policy_type, etc.)
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of ValidationIssue objects found by this check
|
|
261
|
+
"""
|
|
262
|
+
del policy_file, kwargs # Not used in current implementation
|
|
263
|
+
issues = []
|
|
264
|
+
|
|
265
|
+
# Get policy-level requirements from config
|
|
266
|
+
policy_level_requirements = config.config.get("policy_level_requirements", [])
|
|
267
|
+
if not policy_level_requirements:
|
|
268
|
+
return issues
|
|
269
|
+
|
|
270
|
+
# Process each policy-level requirement
|
|
271
|
+
for requirement in policy_level_requirements:
|
|
272
|
+
# Collect all statements that match the action criteria
|
|
273
|
+
matching_statements: list[tuple[int, Statement, list[str]]] = []
|
|
274
|
+
|
|
275
|
+
for idx, statement in enumerate(policy.statement):
|
|
276
|
+
# Only check Allow statements
|
|
277
|
+
if statement.effect != "Allow":
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
statement_actions = statement.get_actions()
|
|
281
|
+
|
|
282
|
+
# Check if this statement matches the action requirement
|
|
283
|
+
actions_match, matching_actions = await self._check_action_match(
|
|
284
|
+
statement_actions, requirement, fetcher
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if actions_match and matching_actions:
|
|
288
|
+
matching_statements.append((idx, statement, matching_actions))
|
|
289
|
+
|
|
290
|
+
# If no statements match, skip this requirement
|
|
291
|
+
if not matching_statements:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# Now validate that ALL matching statements have the required conditions
|
|
295
|
+
required_conditions_config = requirement.get("required_conditions", [])
|
|
296
|
+
if not required_conditions_config:
|
|
297
|
+
# No conditions specified, just report that actions were found
|
|
298
|
+
description = requirement.get("description", "")
|
|
299
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
300
|
+
|
|
301
|
+
# Create a summary issue for all matching statements
|
|
302
|
+
all_actions = set()
|
|
303
|
+
statement_refs = []
|
|
304
|
+
for idx, stmt, actions in matching_statements:
|
|
305
|
+
all_actions.update(actions)
|
|
306
|
+
sid_info = f" (SID: {stmt.sid})" if stmt.sid else ""
|
|
307
|
+
statement_refs.append(f"Statement #{idx + 1}{sid_info}")
|
|
308
|
+
|
|
309
|
+
# Use the first matching statement's index for the issue
|
|
310
|
+
first_idx, first_stmt, _ = matching_statements[0]
|
|
311
|
+
|
|
312
|
+
issues.append(
|
|
313
|
+
ValidationIssue(
|
|
314
|
+
severity=severity,
|
|
315
|
+
statement_sid=first_stmt.sid,
|
|
316
|
+
statement_index=first_idx,
|
|
317
|
+
issue_type="policy_level_action_detected",
|
|
318
|
+
message=f"POLICY-LEVEL: Actions {sorted(all_actions)} found in {len(matching_statements)} statement(s). {description}",
|
|
319
|
+
action=", ".join(sorted(all_actions)),
|
|
320
|
+
suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
|
|
321
|
+
line_number=first_stmt.line_number,
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# Validate conditions for each matching statement
|
|
327
|
+
for idx, statement, matching_actions in matching_statements:
|
|
328
|
+
condition_issues = self._validate_conditions(
|
|
329
|
+
statement,
|
|
330
|
+
idx,
|
|
331
|
+
required_conditions_config,
|
|
332
|
+
matching_actions,
|
|
333
|
+
config,
|
|
334
|
+
requirement,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Add policy-level context to each issue
|
|
338
|
+
for issue in condition_issues:
|
|
339
|
+
# Modify the message to indicate this is part of policy-level enforcement
|
|
340
|
+
issue.message = f"POLICY-LEVEL: {issue.message}"
|
|
341
|
+
issue.suggestion = (
|
|
342
|
+
f"{issue.suggestion}\n\n"
|
|
343
|
+
f"Note: This is enforced at the policy level. "
|
|
344
|
+
f"Found {len(matching_statements)} statement(s) with these actions in the policy."
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
issues.extend(condition_issues)
|
|
348
|
+
|
|
349
|
+
return issues
|
|
350
|
+
|
|
189
351
|
async def _check_action_match(
|
|
190
352
|
self, statement_actions: list[str], requirement: dict[str, Any], fetcher: AWSServiceFetcher
|
|
191
353
|
) -> tuple[bool, list[str]]:
|
|
@@ -607,10 +769,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
607
769
|
statement_sid=statement.sid,
|
|
608
770
|
statement_index=statement_idx,
|
|
609
771
|
issue_type="missing_required_condition",
|
|
610
|
-
message=(
|
|
611
|
-
f"{message_prefix} Action(s) {matching_actions} require condition '{condition_key}'. "
|
|
612
|
-
f"{description}"
|
|
613
|
-
),
|
|
772
|
+
message=f"{message_prefix} Action(s) {matching_actions} require condition '{condition_key}'",
|
|
614
773
|
action=", ".join(matching_actions),
|
|
615
774
|
condition_key=condition_key,
|
|
616
775
|
suggestion=self._build_suggestion(
|
|
@@ -707,8 +866,6 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
707
866
|
)
|
|
708
867
|
if expected_value is not None:
|
|
709
868
|
message += f" with value '{expected_value}'"
|
|
710
|
-
if description:
|
|
711
|
-
message += f". {description}"
|
|
712
869
|
|
|
713
870
|
suggestion = f"Remove the '{condition_key}' condition from the statement"
|
|
714
871
|
if description:
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource-Action Matching check.
|
|
3
|
+
|
|
4
|
+
This check validates that resources in a policy statement match the required
|
|
5
|
+
resource types for the actions. This catches common mistakes like:
|
|
6
|
+
|
|
7
|
+
- s3:GetObject with bucket ARN (needs object ARN: arn:aws:s3:::bucket/*)
|
|
8
|
+
- s3:ListBucket with object ARN (needs bucket ARN: arn:aws:s3:::bucket)
|
|
9
|
+
- iam:ListUsers with user ARN (needs wildcard: *)
|
|
10
|
+
|
|
11
|
+
This is inspired by Parliament's RESOURCE_MISMATCH check.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
Policy with mismatch:
|
|
15
|
+
{
|
|
16
|
+
"Effect": "Allow",
|
|
17
|
+
"Action": "s3:GetObject",
|
|
18
|
+
"Resource": "arn:aws:s3:::mybucket" # Missing /* for object path!
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
25
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
26
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
27
|
+
from iam_validator.sdk.arn_matching import (
|
|
28
|
+
arn_strictly_valid,
|
|
29
|
+
convert_aws_pattern_to_wildcard,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ActionResourceMatchingCheck(PolicyCheck):
|
|
34
|
+
"""
|
|
35
|
+
Validates that resources match the required types for actions.
|
|
36
|
+
|
|
37
|
+
This check helps identify policies that are syntactically valid but won't
|
|
38
|
+
work as intended because the resource ARNs don't match what the action requires.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def check_id(self) -> str:
|
|
43
|
+
return "action_resource_matching"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def description(self) -> str:
|
|
47
|
+
return "Validates that resources match required types for actions"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def default_severity(self) -> str:
|
|
51
|
+
return "medium" # Security issue, not IAM validity error
|
|
52
|
+
|
|
53
|
+
async def execute(
|
|
54
|
+
self,
|
|
55
|
+
statement: Statement,
|
|
56
|
+
statement_idx: int,
|
|
57
|
+
fetcher: AWSServiceFetcher,
|
|
58
|
+
config: CheckConfig,
|
|
59
|
+
) -> list[ValidationIssue]:
|
|
60
|
+
"""
|
|
61
|
+
Execute resource-action matching validation on a statement.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
statement: The IAM policy statement to check
|
|
65
|
+
statement_idx: Index of the statement in the policy
|
|
66
|
+
fetcher: AWS service fetcher for action definitions
|
|
67
|
+
config: Configuration for this check
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of ValidationIssue objects for resource mismatches
|
|
71
|
+
"""
|
|
72
|
+
issues = []
|
|
73
|
+
|
|
74
|
+
# Get actions and resources
|
|
75
|
+
actions = statement.get_actions()
|
|
76
|
+
resources = statement.get_resources()
|
|
77
|
+
statement_sid = statement.sid
|
|
78
|
+
line_number = statement.line_number
|
|
79
|
+
|
|
80
|
+
# Skip if we have a wildcard resource (handled by other checks)
|
|
81
|
+
if "*" in resources:
|
|
82
|
+
return issues
|
|
83
|
+
|
|
84
|
+
# Check each action
|
|
85
|
+
for action in actions:
|
|
86
|
+
# Skip wildcard actions
|
|
87
|
+
if action == "*" or ":" not in action:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Parse service and action name
|
|
91
|
+
try:
|
|
92
|
+
service, action_name = action.split(":", 1)
|
|
93
|
+
except ValueError:
|
|
94
|
+
continue # Invalid action format, handled by action_validation
|
|
95
|
+
|
|
96
|
+
# Skip wildcard actions
|
|
97
|
+
if "*" in service or "*" in action_name:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
# Get service definition
|
|
101
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
102
|
+
if not service_detail:
|
|
103
|
+
continue # Unknown service, handled by action_validation
|
|
104
|
+
|
|
105
|
+
# Get action definition
|
|
106
|
+
action_detail = service_detail.actions.get(action_name)
|
|
107
|
+
if not action_detail:
|
|
108
|
+
continue # Unknown action, handled by action_validation
|
|
109
|
+
|
|
110
|
+
# Get required resource types for this action
|
|
111
|
+
required_resources = action_detail.resources or []
|
|
112
|
+
|
|
113
|
+
# If action requires no specific resources, it needs Resource: "*"
|
|
114
|
+
if not required_resources:
|
|
115
|
+
# Check if all resources are "*"
|
|
116
|
+
if not all(r == "*" for r in resources):
|
|
117
|
+
issues.append(
|
|
118
|
+
self._create_mismatch_issue(
|
|
119
|
+
action=action,
|
|
120
|
+
required_format="*",
|
|
121
|
+
required_type="*",
|
|
122
|
+
provided_resources=resources,
|
|
123
|
+
statement_idx=statement_idx,
|
|
124
|
+
statement_sid=statement_sid,
|
|
125
|
+
line_number=line_number,
|
|
126
|
+
config=config,
|
|
127
|
+
reason=f'Action {action} can only use Resource: "*"',
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Check if ANY policy resource matches ANY required resource type
|
|
133
|
+
match_found = False
|
|
134
|
+
|
|
135
|
+
for req_resource in required_resources:
|
|
136
|
+
# Get the resource type name from the action's required resources
|
|
137
|
+
resource_name = req_resource.get("Name", "")
|
|
138
|
+
if not resource_name:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Look up the full resource type definition in the service's resources
|
|
142
|
+
# The action's Resources field only has names like {"Name": "object"}
|
|
143
|
+
# The service's Resources field has full definitions with ARN formats
|
|
144
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
145
|
+
if not resource_type:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Get the ARN pattern (first format from ARNFormats array)
|
|
149
|
+
arn_pattern = resource_type.arn_pattern
|
|
150
|
+
if not arn_pattern:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Convert AWS pattern format (${Partition}, ${BucketName}) to wildcards (*)
|
|
154
|
+
# AWS provides patterns like: arn:${Partition}:s3:::${BucketName}/${ObjectName}
|
|
155
|
+
# We need wildcards like: arn:*:s3:::*/*
|
|
156
|
+
wildcard_pattern = convert_aws_pattern_to_wildcard(arn_pattern)
|
|
157
|
+
|
|
158
|
+
# Check if any policy resource matches this ARN pattern
|
|
159
|
+
for resource in resources:
|
|
160
|
+
if arn_strictly_valid(wildcard_pattern, resource, resource_name):
|
|
161
|
+
match_found = True
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if match_found:
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
# If no match found, create an issue
|
|
168
|
+
if not match_found and required_resources:
|
|
169
|
+
# Build helpful error message with required formats
|
|
170
|
+
# Look up each resource type in the service to get ARN patterns
|
|
171
|
+
required_formats = []
|
|
172
|
+
for req_res in required_resources:
|
|
173
|
+
res_name = req_res.get("Name", "")
|
|
174
|
+
if not res_name:
|
|
175
|
+
continue
|
|
176
|
+
res_type = service_detail.resources.get(res_name)
|
|
177
|
+
if res_type and res_type.arn_pattern:
|
|
178
|
+
required_formats.append(
|
|
179
|
+
{
|
|
180
|
+
"type": res_name,
|
|
181
|
+
"format": res_type.arn_pattern,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
issues.append(
|
|
186
|
+
self._create_mismatch_issue(
|
|
187
|
+
action=action,
|
|
188
|
+
required_format=required_formats[0]["format"] if required_formats else "",
|
|
189
|
+
required_type=required_formats[0]["type"] if required_formats else "",
|
|
190
|
+
provided_resources=resources,
|
|
191
|
+
statement_idx=statement_idx,
|
|
192
|
+
statement_sid=statement_sid,
|
|
193
|
+
line_number=line_number,
|
|
194
|
+
config=config,
|
|
195
|
+
all_required_formats=required_formats,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return issues
|
|
200
|
+
|
|
201
|
+
def _create_mismatch_issue(
|
|
202
|
+
self,
|
|
203
|
+
action: str,
|
|
204
|
+
required_format: str,
|
|
205
|
+
required_type: str,
|
|
206
|
+
provided_resources: list[str],
|
|
207
|
+
statement_idx: int,
|
|
208
|
+
statement_sid: str | None,
|
|
209
|
+
line_number: int | None,
|
|
210
|
+
config: CheckConfig,
|
|
211
|
+
all_required_formats: list[dict] | None = None,
|
|
212
|
+
reason: str | None = None,
|
|
213
|
+
) -> ValidationIssue:
|
|
214
|
+
"""Create a validation issue for resource mismatch."""
|
|
215
|
+
# Build helpful message
|
|
216
|
+
if reason:
|
|
217
|
+
message = reason
|
|
218
|
+
elif all_required_formats and len(all_required_formats) > 1:
|
|
219
|
+
types = ", ".join(f["type"] for f in all_required_formats)
|
|
220
|
+
message = (
|
|
221
|
+
f"No resources match for action '{action}'. This action requires one of: {types}"
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
message = (
|
|
225
|
+
f"No resources match for action '{action}'. "
|
|
226
|
+
f"This action requires resource type: {required_type}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Build suggestion with examples
|
|
230
|
+
suggestion = self._get_suggestion(action, required_format, provided_resources)
|
|
231
|
+
|
|
232
|
+
return ValidationIssue(
|
|
233
|
+
severity=self.get_severity(config),
|
|
234
|
+
statement_sid=statement_sid,
|
|
235
|
+
statement_index=statement_idx,
|
|
236
|
+
issue_type="resource_mismatch",
|
|
237
|
+
message=message,
|
|
238
|
+
action=action,
|
|
239
|
+
resource=", ".join(provided_resources)
|
|
240
|
+
if len(provided_resources) <= 3
|
|
241
|
+
else f"{provided_resources[0]}...",
|
|
242
|
+
suggestion=suggestion,
|
|
243
|
+
line_number=line_number,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _get_suggestion(
|
|
247
|
+
self,
|
|
248
|
+
action: str,
|
|
249
|
+
required_format: str,
|
|
250
|
+
provided_resources: list[str],
|
|
251
|
+
) -> str:
|
|
252
|
+
"""
|
|
253
|
+
Generate helpful suggestion for fixing the mismatch.
|
|
254
|
+
|
|
255
|
+
This function is service-agnostic and extracts resource type information
|
|
256
|
+
from the ARN pattern to provide contextual examples.
|
|
257
|
+
"""
|
|
258
|
+
if not required_format:
|
|
259
|
+
return "Check AWS documentation for required resource types for this action"
|
|
260
|
+
|
|
261
|
+
# Extract action name for contextual hints (e.g., "GetObject" from "s3:GetObject")
|
|
262
|
+
action_name = action.split(":")[1] if ":" in action else action
|
|
263
|
+
|
|
264
|
+
# Special case: Wildcard resource
|
|
265
|
+
if required_format == "*":
|
|
266
|
+
return (
|
|
267
|
+
f'Action {action} can only use Resource: "*" (wildcard).\n'
|
|
268
|
+
f" This action does not support resource-level permissions.\n"
|
|
269
|
+
f' Example: "Resource": "*"'
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Extract resource type from ARN pattern
|
|
273
|
+
# Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
|
|
274
|
+
# Examples:
|
|
275
|
+
# arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
|
|
276
|
+
# arn:${Partition}:iam::${Account}:user/${UserName} -> user
|
|
277
|
+
resource_type = self._extract_resource_type_from_pattern(required_format)
|
|
278
|
+
|
|
279
|
+
# Build service-specific suggestion
|
|
280
|
+
suggestion_parts = []
|
|
281
|
+
|
|
282
|
+
# Add action description
|
|
283
|
+
suggestion_parts.append(f"Action {action} requires {resource_type} resource type.")
|
|
284
|
+
|
|
285
|
+
# Add expected format
|
|
286
|
+
suggestion_parts.append(f" Expected format: {required_format}")
|
|
287
|
+
|
|
288
|
+
# Add practical example based on the pattern
|
|
289
|
+
example = self._generate_example_arn(required_format)
|
|
290
|
+
if example:
|
|
291
|
+
suggestion_parts.append(f" Example: {example}")
|
|
292
|
+
|
|
293
|
+
# Add helpful context for common patterns
|
|
294
|
+
context = self._get_resource_context(action_name, resource_type, required_format)
|
|
295
|
+
if context:
|
|
296
|
+
suggestion_parts.append(f" {context}")
|
|
297
|
+
|
|
298
|
+
suggestion = "\n".join(suggestion_parts)
|
|
299
|
+
|
|
300
|
+
# Add current resources to help user understand the mismatch
|
|
301
|
+
if provided_resources and len(provided_resources) <= 3:
|
|
302
|
+
current = ", ".join(provided_resources)
|
|
303
|
+
suggestion += f"\n Current resources: {current}"
|
|
304
|
+
|
|
305
|
+
return suggestion
|
|
306
|
+
|
|
307
|
+
def _extract_resource_type_from_pattern(self, pattern: str) -> str:
|
|
308
|
+
"""
|
|
309
|
+
Extract the resource type from an ARN pattern.
|
|
310
|
+
|
|
311
|
+
Examples:
|
|
312
|
+
arn:${Partition}:s3:::${BucketName}/${ObjectName} -> "object"
|
|
313
|
+
arn:${Partition}:iam::${Account}:user/${UserName} -> "user"
|
|
314
|
+
arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId} -> "instance"
|
|
315
|
+
"""
|
|
316
|
+
# Split ARN by colons to get resource part
|
|
317
|
+
parts = pattern.split(":")
|
|
318
|
+
if len(parts) < 6:
|
|
319
|
+
return "resource"
|
|
320
|
+
|
|
321
|
+
# Resource part is everything after the 5th colon
|
|
322
|
+
resource_part = ":".join(parts[5:])
|
|
323
|
+
|
|
324
|
+
# Extract resource type (part before / or entire string)
|
|
325
|
+
if "/" in resource_part:
|
|
326
|
+
resource_type = resource_part.split("/")[0]
|
|
327
|
+
elif ":" in resource_part:
|
|
328
|
+
resource_type = resource_part.split(":")[0]
|
|
329
|
+
else:
|
|
330
|
+
resource_type = resource_part
|
|
331
|
+
|
|
332
|
+
# Remove template variables like ${...}
|
|
333
|
+
import re
|
|
334
|
+
|
|
335
|
+
resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
|
|
336
|
+
|
|
337
|
+
return resource_type.strip() or "resource"
|
|
338
|
+
|
|
339
|
+
def _generate_example_arn(self, pattern: str) -> str:
|
|
340
|
+
"""
|
|
341
|
+
Generate a practical example ARN based on the pattern.
|
|
342
|
+
|
|
343
|
+
Converts AWS template variables to realistic examples.
|
|
344
|
+
"""
|
|
345
|
+
import re
|
|
346
|
+
|
|
347
|
+
example = pattern
|
|
348
|
+
|
|
349
|
+
# Common substitutions
|
|
350
|
+
substitutions = {
|
|
351
|
+
r"\$\{Partition\}": "aws",
|
|
352
|
+
r"\$\{Region\}": "us-east-1",
|
|
353
|
+
r"\$\{Account\}": "123456789012",
|
|
354
|
+
r"\$\{BucketName\}": "my-bucket",
|
|
355
|
+
r"\$\{ObjectName\}": "*",
|
|
356
|
+
r"\$\{UserName\}": "my-user",
|
|
357
|
+
r"\$\{UserNameWithPath\}": "my-user",
|
|
358
|
+
r"\$\{RoleName\}": "my-role",
|
|
359
|
+
r"\$\{RoleNameWithPath\}": "my-role",
|
|
360
|
+
r"\$\{GroupName\}": "my-group",
|
|
361
|
+
r"\$\{PolicyName\}": "my-policy",
|
|
362
|
+
r"\$\{FunctionName\}": "my-function",
|
|
363
|
+
r"\$\{TableName\}": "MyTable",
|
|
364
|
+
r"\$\{QueueName\}": "MyQueue",
|
|
365
|
+
r"\$\{TopicName\}": "MyTopic",
|
|
366
|
+
r"\$\{InstanceId\}": "i-1234567890abcdef0",
|
|
367
|
+
r"\$\{VolumeId\}": "vol-1234567890abcdef0",
|
|
368
|
+
r"\$\{SnapshotId\}": "snap-1234567890abcdef0",
|
|
369
|
+
r"\$\{KeyId\}": "my-key",
|
|
370
|
+
r"\$\{StreamName\}": "MyStream",
|
|
371
|
+
r"\$\{LayerName\}": "my-layer",
|
|
372
|
+
r"\$\{Token\}": "*",
|
|
373
|
+
r"\$\{[^}]+\}": "*", # Catch-all for any remaining variables
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for pattern_var, replacement in substitutions.items():
|
|
377
|
+
example = re.sub(pattern_var, replacement, example)
|
|
378
|
+
|
|
379
|
+
return example
|
|
380
|
+
|
|
381
|
+
def _get_resource_context(self, action_name: str, resource_type: str, pattern: str) -> str:
|
|
382
|
+
"""
|
|
383
|
+
Provide helpful context about resource requirements.
|
|
384
|
+
|
|
385
|
+
Analyzes the ARN pattern structure and action type to provide
|
|
386
|
+
generic, service-agnostic guidance that works for any AWS service.
|
|
387
|
+
"""
|
|
388
|
+
contexts = []
|
|
389
|
+
|
|
390
|
+
# Detect path separator patterns (e.g., bucket/object, layer:version)
|
|
391
|
+
if "/" in pattern:
|
|
392
|
+
# Pattern has path separator - resource needs it too
|
|
393
|
+
parts = pattern.split("/")
|
|
394
|
+
if len(parts) > 1 and "${" in parts[-1]:
|
|
395
|
+
# Last part is a variable like ${ObjectName}, ${InstanceId}
|
|
396
|
+
contexts.append("ARN must include path separator (/) with resource identifier")
|
|
397
|
+
|
|
398
|
+
# Detect colon-separated resource identifiers
|
|
399
|
+
resource_part = ":".join(pattern.split(":")[5:]) if pattern.count(":") >= 5 else ""
|
|
400
|
+
if resource_part.count(":") > 0 and "${" in resource_part:
|
|
401
|
+
# Resource section uses colons, like function:version or layer:version
|
|
402
|
+
contexts.append("ARN uses colon (:) separators in resource section")
|
|
403
|
+
|
|
404
|
+
# Detect List/Describe actions (often need wildcards)
|
|
405
|
+
if (
|
|
406
|
+
action_name.startswith("List")
|
|
407
|
+
or action_name.startswith("Describe")
|
|
408
|
+
or action_name.startswith("Get")
|
|
409
|
+
):
|
|
410
|
+
# Some Get/List actions require specific resources, others need "*"
|
|
411
|
+
# Only suggest wildcard if pattern is actually "*"
|
|
412
|
+
if pattern == "*":
|
|
413
|
+
contexts.append("This action does not support resource-level permissions")
|
|
414
|
+
|
|
415
|
+
# Generic resource type matching hint
|
|
416
|
+
if resource_type and resource_type != "resource":
|
|
417
|
+
# Avoid redundant message if resource type is obvious
|
|
418
|
+
if not any(
|
|
419
|
+
word in resource_type
|
|
420
|
+
for word in ["object", "bucket", "function", "instance", "user", "role"]
|
|
421
|
+
):
|
|
422
|
+
contexts.append(f"Resource ARN must be of type '{resource_type}'")
|
|
423
|
+
|
|
424
|
+
return "Note: " + " | ".join(contexts) if contexts else ""
|