iam-policy-validator 1.7.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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
- iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
- iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +43 -0
- iam_validator/checks/action_condition_enforcement.py +884 -0
- iam_validator/checks/action_resource_matching.py +441 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +92 -0
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/full_wildcard.py +71 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/policy_size.py +147 -0
- iam_validator/checks/policy_type_validation.py +305 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/resource_validation.py +138 -0
- iam_validator/checks/sensitive_action.py +254 -0
- iam_validator/checks/service_wildcard.py +107 -0
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/checks/wildcard_action.py +67 -0
- iam_validator/checks/wildcard_resource.py +135 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +531 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +600 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +940 -0
- iam_validator/core/check_registry.py +607 -0
- iam_validator/core/cli.py +134 -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 +104 -0
- iam_validator/core/config/condition_requirements.py +155 -0
- iam_validator/core/config/config_loader.py +472 -0
- 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/constants.py +74 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +440 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/models.py +327 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +424 -0
- iam_validator/core/report.py +872 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +815 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +187 -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 +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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Resource validation check - validates ARN formats."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
6
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
7
|
+
from iam_validator.core.constants import DEFAULT_ARN_VALIDATION_PATTERN, MAX_ARN_LENGTH
|
|
8
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
9
|
+
from iam_validator.sdk.arn_matching import (
|
|
10
|
+
has_template_variables,
|
|
11
|
+
normalize_template_variables,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResourceValidationCheck(PolicyCheck):
|
|
16
|
+
"""Validates ARN format for resources."""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def check_id(self) -> str:
|
|
20
|
+
return "resource_validation"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def description(self) -> str:
|
|
24
|
+
return "Validates ARN format for resources"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def default_severity(self) -> str:
|
|
28
|
+
return "error"
|
|
29
|
+
|
|
30
|
+
async def execute(
|
|
31
|
+
self,
|
|
32
|
+
statement: Statement,
|
|
33
|
+
statement_idx: int,
|
|
34
|
+
fetcher: AWSServiceFetcher,
|
|
35
|
+
config: CheckConfig,
|
|
36
|
+
) -> list[ValidationIssue]:
|
|
37
|
+
"""Execute resource ARN validation on a statement."""
|
|
38
|
+
issues = []
|
|
39
|
+
|
|
40
|
+
# Get resources from statement
|
|
41
|
+
resources = statement.get_resources()
|
|
42
|
+
statement_sid = statement.sid
|
|
43
|
+
line_number = statement.line_number
|
|
44
|
+
|
|
45
|
+
# Get ARN pattern from config, or use default
|
|
46
|
+
# Pattern allows wildcards (*) in region and account fields
|
|
47
|
+
arn_pattern_str = config.config.get("arn_pattern", DEFAULT_ARN_VALIDATION_PATTERN)
|
|
48
|
+
|
|
49
|
+
# Compile pattern
|
|
50
|
+
try:
|
|
51
|
+
arn_pattern = re.compile(arn_pattern_str)
|
|
52
|
+
except re.error:
|
|
53
|
+
# Fallback to default pattern if custom pattern is invalid
|
|
54
|
+
arn_pattern = re.compile(DEFAULT_ARN_VALIDATION_PATTERN)
|
|
55
|
+
|
|
56
|
+
# Check if template variable support is enabled (default: true)
|
|
57
|
+
# Try global settings first, then check-specific config
|
|
58
|
+
allow_template_variables = config.root_config.get("settings", {}).get(
|
|
59
|
+
"allow_template_variables",
|
|
60
|
+
config.config.get("allow_template_variables", True),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for resource in resources:
|
|
64
|
+
# Skip wildcard resources (handled by security checks)
|
|
65
|
+
if resource == "*":
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# Validate ARN length to prevent ReDoS attacks
|
|
69
|
+
if len(resource) > MAX_ARN_LENGTH:
|
|
70
|
+
issues.append(
|
|
71
|
+
ValidationIssue(
|
|
72
|
+
severity=self.get_severity(config),
|
|
73
|
+
statement_sid=statement_sid,
|
|
74
|
+
statement_index=statement_idx,
|
|
75
|
+
issue_type="invalid_resource",
|
|
76
|
+
message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
|
|
77
|
+
resource=resource[:100] + "...",
|
|
78
|
+
suggestion="ARN is too long and may be invalid",
|
|
79
|
+
line_number=line_number,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Check if resource contains template variables
|
|
85
|
+
has_templates = has_template_variables(resource)
|
|
86
|
+
|
|
87
|
+
# If template variables are found and allowed, normalize them for validation
|
|
88
|
+
validation_resource = resource
|
|
89
|
+
if has_templates and allow_template_variables:
|
|
90
|
+
validation_resource = normalize_template_variables(resource)
|
|
91
|
+
|
|
92
|
+
# Validate ARN format
|
|
93
|
+
try:
|
|
94
|
+
if not arn_pattern.match(validation_resource):
|
|
95
|
+
# If original resource had templates and normalization didn't help,
|
|
96
|
+
# provide a more informative message
|
|
97
|
+
if has_templates and allow_template_variables:
|
|
98
|
+
issues.append(
|
|
99
|
+
ValidationIssue(
|
|
100
|
+
severity=self.get_severity(config),
|
|
101
|
+
statement_sid=statement_sid,
|
|
102
|
+
statement_index=statement_idx,
|
|
103
|
+
issue_type="invalid_resource",
|
|
104
|
+
message=f"Invalid ARN format even after normalizing template variables: {resource}",
|
|
105
|
+
resource=resource,
|
|
106
|
+
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource (template variables like ${aws_account_id} are supported)",
|
|
107
|
+
line_number=line_number,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
issues.append(
|
|
112
|
+
ValidationIssue(
|
|
113
|
+
severity=self.get_severity(config),
|
|
114
|
+
statement_sid=statement_sid,
|
|
115
|
+
statement_index=statement_idx,
|
|
116
|
+
issue_type="invalid_resource",
|
|
117
|
+
message=f"Invalid ARN format: {resource}",
|
|
118
|
+
resource=resource,
|
|
119
|
+
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
|
|
120
|
+
line_number=line_number,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
except Exception:
|
|
124
|
+
# If regex matching fails (shouldn't happen with length check), treat as invalid
|
|
125
|
+
issues.append(
|
|
126
|
+
ValidationIssue(
|
|
127
|
+
severity=self.get_severity(config),
|
|
128
|
+
statement_sid=statement_sid,
|
|
129
|
+
statement_index=statement_idx,
|
|
130
|
+
issue_type="invalid_resource",
|
|
131
|
+
message=f"Could not validate ARN format: {resource}",
|
|
132
|
+
resource=resource,
|
|
133
|
+
suggestion="ARN validation failed - may contain unexpected characters",
|
|
134
|
+
line_number=line_number,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return issues
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Sensitive action check - detects sensitive actions without IAM conditions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
|
|
6
|
+
from iam_validator.checks.utils.sensitive_action_matcher import (
|
|
7
|
+
DEFAULT_SENSITIVE_ACTIONS,
|
|
8
|
+
check_sensitive_actions,
|
|
9
|
+
)
|
|
10
|
+
from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
|
|
11
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
12
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
13
|
+
from iam_validator.core.config.sensitive_actions import get_category_for_action
|
|
14
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from iam_validator.core.models import IAMPolicy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SensitiveActionCheck(PolicyCheck):
|
|
21
|
+
"""Checks for sensitive actions without IAM conditions to limit their use."""
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def check_id(self) -> str:
|
|
25
|
+
return "sensitive_action"
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def description(self) -> str:
|
|
29
|
+
return "Checks for sensitive actions without conditions"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def default_severity(self) -> str:
|
|
33
|
+
return "medium"
|
|
34
|
+
|
|
35
|
+
def _get_severity_for_action(self, action: str, config: CheckConfig) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Get severity for a specific action, considering category-based overrides.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
action: The AWS action to check
|
|
41
|
+
config: Check configuration
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Severity level for the action (considers category overrides)
|
|
45
|
+
"""
|
|
46
|
+
# Check if category severities are configured
|
|
47
|
+
category_severities = config.config.get("category_severities", {})
|
|
48
|
+
if not category_severities:
|
|
49
|
+
return self.get_severity(config)
|
|
50
|
+
|
|
51
|
+
# Get the category for this action
|
|
52
|
+
category = get_category_for_action(action)
|
|
53
|
+
if category and category in category_severities:
|
|
54
|
+
return category_severities[category]
|
|
55
|
+
|
|
56
|
+
# Fall back to default severity
|
|
57
|
+
return self.get_severity(config)
|
|
58
|
+
|
|
59
|
+
def _get_category_specific_suggestion(
|
|
60
|
+
self, action: str, config: CheckConfig
|
|
61
|
+
) -> tuple[str, str]:
|
|
62
|
+
"""
|
|
63
|
+
Get category-specific suggestion and example for an action.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
action: The AWS action to check
|
|
67
|
+
config: Check configuration
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (suggestion_text, example_text) tailored to the action's category
|
|
71
|
+
"""
|
|
72
|
+
category = get_category_for_action(action)
|
|
73
|
+
|
|
74
|
+
# Get category suggestions from config (ABAC-focused by default)
|
|
75
|
+
# See: iam_validator/core/config/category_suggestions.py
|
|
76
|
+
category_suggestions = config.config.get("category_suggestions", {})
|
|
77
|
+
|
|
78
|
+
# Get category-specific content or fall back to generic ABAC guidance
|
|
79
|
+
if category and category in category_suggestions:
|
|
80
|
+
return (
|
|
81
|
+
category_suggestions[category]["suggestion"],
|
|
82
|
+
category_suggestions[category]["example"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Generic ABAC fallback for uncategorized actions
|
|
86
|
+
return (
|
|
87
|
+
"Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
|
|
88
|
+
"• Match principal tags to resource tags (aws:PrincipalTag/X = aws:ResourceTag/X)\n"
|
|
89
|
+
"• Require MFA (aws:MultiFactorAuthPresent = true)\n"
|
|
90
|
+
"• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
|
|
91
|
+
'"Condition": {\n'
|
|
92
|
+
' "StringEquals": {\n'
|
|
93
|
+
' "aws:PrincipalTag/owner": "${aws:ResourceTag/owner}"\n'
|
|
94
|
+
" }\n"
|
|
95
|
+
"}",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def execute(
|
|
99
|
+
self,
|
|
100
|
+
statement: Statement,
|
|
101
|
+
statement_idx: int,
|
|
102
|
+
fetcher: AWSServiceFetcher,
|
|
103
|
+
config: CheckConfig,
|
|
104
|
+
) -> list[ValidationIssue]:
|
|
105
|
+
"""Execute sensitive action check on a statement."""
|
|
106
|
+
issues = []
|
|
107
|
+
|
|
108
|
+
# Only check Allow statements
|
|
109
|
+
if statement.effect != "Allow":
|
|
110
|
+
return issues
|
|
111
|
+
|
|
112
|
+
actions = statement.get_actions()
|
|
113
|
+
has_conditions = statement.condition is not None and len(statement.condition) > 0
|
|
114
|
+
|
|
115
|
+
# Expand wildcards to actual actions using AWS API
|
|
116
|
+
expanded_actions = await expand_wildcard_actions(actions, fetcher)
|
|
117
|
+
|
|
118
|
+
# Check if sensitive actions match using any_of/all_of logic
|
|
119
|
+
is_sensitive, matched_actions = check_sensitive_actions(
|
|
120
|
+
expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if is_sensitive and not has_conditions:
|
|
124
|
+
# Create appropriate message based on matched actions using configurable templates
|
|
125
|
+
if len(matched_actions) == 1:
|
|
126
|
+
message_template = config.config.get(
|
|
127
|
+
"message_single",
|
|
128
|
+
"Sensitive action '{action}' should have conditions to limit when it can be used",
|
|
129
|
+
)
|
|
130
|
+
message = message_template.format(action=matched_actions[0])
|
|
131
|
+
else:
|
|
132
|
+
action_list = "', '".join(matched_actions)
|
|
133
|
+
message_template = config.config.get(
|
|
134
|
+
"message_multiple",
|
|
135
|
+
"Sensitive actions '{actions}' should have conditions to limit when they can be used",
|
|
136
|
+
)
|
|
137
|
+
message = message_template.format(actions=action_list)
|
|
138
|
+
|
|
139
|
+
# Get category-specific suggestion and example (or use config defaults)
|
|
140
|
+
# Use the first matched action to determine the category
|
|
141
|
+
suggestion_text, example = self._get_category_specific_suggestion(
|
|
142
|
+
matched_actions[0], config
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Combine suggestion + example
|
|
146
|
+
suggestion = (
|
|
147
|
+
f"{suggestion_text}\n\nExample:\n```json\n{example}\n```"
|
|
148
|
+
if example
|
|
149
|
+
else suggestion_text
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Determine severity based on the highest severity action in the list
|
|
153
|
+
# If single action, use its category severity
|
|
154
|
+
# If multiple actions, use the highest severity among them
|
|
155
|
+
severity = self.get_severity(config) # Default
|
|
156
|
+
if matched_actions:
|
|
157
|
+
# Get severity for first action (or highest if we want to be more sophisticated)
|
|
158
|
+
severity = self._get_severity_for_action(matched_actions[0], config)
|
|
159
|
+
|
|
160
|
+
issues.append(
|
|
161
|
+
ValidationIssue(
|
|
162
|
+
severity=severity,
|
|
163
|
+
statement_sid=statement.sid,
|
|
164
|
+
statement_index=statement_idx,
|
|
165
|
+
issue_type="missing_condition",
|
|
166
|
+
message=message,
|
|
167
|
+
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
168
|
+
suggestion=suggestion,
|
|
169
|
+
line_number=statement.line_number,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return issues
|
|
174
|
+
|
|
175
|
+
async def execute_policy(
|
|
176
|
+
self,
|
|
177
|
+
policy: "IAMPolicy",
|
|
178
|
+
policy_file: str,
|
|
179
|
+
fetcher: AWSServiceFetcher,
|
|
180
|
+
config: CheckConfig,
|
|
181
|
+
**kwargs,
|
|
182
|
+
) -> list[ValidationIssue]:
|
|
183
|
+
"""
|
|
184
|
+
Execute policy-level sensitive action checks.
|
|
185
|
+
|
|
186
|
+
This method examines the entire policy to detect privilege escalation patterns
|
|
187
|
+
and other security issues that span multiple statements.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
policy: The complete IAM policy to check
|
|
191
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
192
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
193
|
+
config: Configuration for this check instance
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of ValidationIssue objects found by this check
|
|
197
|
+
"""
|
|
198
|
+
del policy_file, fetcher # Not used in current implementation
|
|
199
|
+
issues = []
|
|
200
|
+
|
|
201
|
+
# Collect all actions from all Allow statements across the entire policy
|
|
202
|
+
all_actions: set[str] = set()
|
|
203
|
+
statement_map: dict[
|
|
204
|
+
str, list[tuple[int, str | None]]
|
|
205
|
+
] = {} # action -> [(stmt_idx, sid), ...]
|
|
206
|
+
|
|
207
|
+
for idx, statement in enumerate(policy.statement):
|
|
208
|
+
if statement.effect == "Allow":
|
|
209
|
+
actions = statement.get_actions()
|
|
210
|
+
# Filter out wildcards for privilege escalation detection
|
|
211
|
+
filtered_actions = [a for a in actions if a != "*"]
|
|
212
|
+
|
|
213
|
+
for action in filtered_actions:
|
|
214
|
+
all_actions.add(action)
|
|
215
|
+
if action not in statement_map:
|
|
216
|
+
statement_map[action] = []
|
|
217
|
+
statement_map[action].append((idx, statement.sid))
|
|
218
|
+
|
|
219
|
+
# Get configuration for sensitive actions
|
|
220
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
221
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
222
|
+
|
|
223
|
+
# Check for privilege escalation patterns using all_of logic
|
|
224
|
+
# We need to check both exact actions and patterns
|
|
225
|
+
policy_issues = []
|
|
226
|
+
|
|
227
|
+
# Check sensitive_actions configuration
|
|
228
|
+
if sensitive_actions_config:
|
|
229
|
+
policy_issues.extend(
|
|
230
|
+
check_policy_level_actions(
|
|
231
|
+
list(all_actions),
|
|
232
|
+
statement_map,
|
|
233
|
+
sensitive_actions_config,
|
|
234
|
+
config,
|
|
235
|
+
"actions",
|
|
236
|
+
self.get_severity,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Check sensitive_action_patterns configuration
|
|
241
|
+
if sensitive_patterns_config:
|
|
242
|
+
policy_issues.extend(
|
|
243
|
+
check_policy_level_actions(
|
|
244
|
+
list(all_actions),
|
|
245
|
+
statement_map,
|
|
246
|
+
sensitive_patterns_config,
|
|
247
|
+
config,
|
|
248
|
+
"patterns",
|
|
249
|
+
self.get_severity,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
issues.extend(policy_issues)
|
|
254
|
+
return issues
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Service wildcard check - detects service-level wildcards like 'iam:*', 's3:*'."""
|
|
2
|
+
|
|
3
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
4
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
5
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ServiceWildcardCheck(PolicyCheck):
|
|
9
|
+
"""Checks for service-level wildcards (e.g., 'iam:*', 's3:*') which grant all permissions for a service."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def check_id(self) -> str:
|
|
13
|
+
return "service_wildcard"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def description(self) -> str:
|
|
17
|
+
return "Checks for service-level wildcards (e.g., 'iam:*', 's3:*')"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def default_severity(self) -> str:
|
|
21
|
+
return "high"
|
|
22
|
+
|
|
23
|
+
async def execute(
|
|
24
|
+
self,
|
|
25
|
+
statement: Statement,
|
|
26
|
+
statement_idx: int,
|
|
27
|
+
fetcher: AWSServiceFetcher,
|
|
28
|
+
config: CheckConfig,
|
|
29
|
+
) -> list[ValidationIssue]:
|
|
30
|
+
"""Execute service wildcard check on a statement."""
|
|
31
|
+
issues = []
|
|
32
|
+
|
|
33
|
+
# Only check Allow statements
|
|
34
|
+
if statement.effect != "Allow":
|
|
35
|
+
return issues
|
|
36
|
+
|
|
37
|
+
actions = statement.get_actions()
|
|
38
|
+
allowed_services = self._get_allowed_service_wildcards(config)
|
|
39
|
+
|
|
40
|
+
for action in actions:
|
|
41
|
+
# Skip full wildcard (covered by wildcard_action check)
|
|
42
|
+
if action == "*":
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
# Check if it's a service-level wildcard (e.g., "iam:*", "s3:*")
|
|
46
|
+
if ":" in action and action.endswith(":*"):
|
|
47
|
+
service = action.split(":")[0]
|
|
48
|
+
|
|
49
|
+
# Check if this service is in the allowed list
|
|
50
|
+
if service not in allowed_services:
|
|
51
|
+
# Get message template and replace placeholders
|
|
52
|
+
message_template = config.config.get(
|
|
53
|
+
"message",
|
|
54
|
+
"Service-level wildcard '{action}' grants all permissions for {service} service",
|
|
55
|
+
)
|
|
56
|
+
suggestion_template = config.config.get(
|
|
57
|
+
"suggestion",
|
|
58
|
+
"Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
|
|
59
|
+
)
|
|
60
|
+
example_template = config.config.get("example", "")
|
|
61
|
+
|
|
62
|
+
message = message_template.format(action=action, service=service)
|
|
63
|
+
suggestion_text = suggestion_template.format(action=action, service=service)
|
|
64
|
+
example = (
|
|
65
|
+
example_template.format(action=action, service=service)
|
|
66
|
+
if example_template
|
|
67
|
+
else ""
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Combine suggestion + example
|
|
71
|
+
suggestion = (
|
|
72
|
+
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
73
|
+
if example
|
|
74
|
+
else suggestion_text
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
issues.append(
|
|
78
|
+
ValidationIssue(
|
|
79
|
+
severity=self.get_severity(config),
|
|
80
|
+
statement_sid=statement.sid,
|
|
81
|
+
statement_index=statement_idx,
|
|
82
|
+
issue_type="overly_permissive",
|
|
83
|
+
message=message,
|
|
84
|
+
action=action,
|
|
85
|
+
suggestion=suggestion,
|
|
86
|
+
line_number=statement.line_number,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return issues
|
|
91
|
+
|
|
92
|
+
def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
|
|
93
|
+
"""
|
|
94
|
+
Get list of services that are allowed to use service-level wildcards.
|
|
95
|
+
|
|
96
|
+
This allows configuration like:
|
|
97
|
+
service_wildcard:
|
|
98
|
+
allowed_services:
|
|
99
|
+
- "logs" # Allow "logs:*"
|
|
100
|
+
- "cloudwatch" # Allow "cloudwatch:*"
|
|
101
|
+
|
|
102
|
+
Returns empty set if no exceptions are configured.
|
|
103
|
+
"""
|
|
104
|
+
allowed = config.config.get("allowed_services", [])
|
|
105
|
+
if allowed and isinstance(allowed, list):
|
|
106
|
+
return set(allowed)
|
|
107
|
+
return set()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Set Operator Validation Check.
|
|
2
|
+
|
|
3
|
+
Validates proper usage of ForAllValues and ForAnyValue set operators in IAM policies.
|
|
4
|
+
|
|
5
|
+
Based on AWS IAM best practices:
|
|
6
|
+
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
10
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
11
|
+
from iam_validator.core.condition_validators import (
|
|
12
|
+
is_multivalued_context_key,
|
|
13
|
+
normalize_operator,
|
|
14
|
+
)
|
|
15
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SetOperatorValidationCheck(PolicyCheck):
|
|
19
|
+
"""Check for proper usage of ForAllValues and ForAnyValue set operators."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def check_id(self) -> str:
|
|
23
|
+
"""Unique identifier for this check."""
|
|
24
|
+
return "set_operator_validation"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
"""Description of what this check does."""
|
|
29
|
+
return "Validates proper usage of ForAllValues and ForAnyValue set operators"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def default_severity(self) -> str:
|
|
33
|
+
"""Default severity level for issues found by this check."""
|
|
34
|
+
return "error"
|
|
35
|
+
|
|
36
|
+
async def execute(
|
|
37
|
+
self,
|
|
38
|
+
statement: Statement,
|
|
39
|
+
statement_idx: int,
|
|
40
|
+
fetcher: AWSServiceFetcher,
|
|
41
|
+
config: CheckConfig,
|
|
42
|
+
) -> list[ValidationIssue]:
|
|
43
|
+
"""
|
|
44
|
+
Execute the set operator validation check.
|
|
45
|
+
|
|
46
|
+
Validates:
|
|
47
|
+
1. ForAllValues/ForAnyValue not used with single-valued context keys (anti-pattern)
|
|
48
|
+
2. ForAllValues with Allow effect includes Null condition check (security)
|
|
49
|
+
3. ForAnyValue with Deny effect includes Null condition check (predictability)
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
statement: The IAM statement to check
|
|
53
|
+
statement_idx: Index of this statement in the policy
|
|
54
|
+
fetcher: AWS service fetcher (unused but required by interface)
|
|
55
|
+
config: Check configuration
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of validation issues found
|
|
59
|
+
"""
|
|
60
|
+
issues = []
|
|
61
|
+
|
|
62
|
+
# Only check statements with conditions
|
|
63
|
+
if not statement.condition:
|
|
64
|
+
return issues
|
|
65
|
+
|
|
66
|
+
statement_sid = statement.sid
|
|
67
|
+
line_number = statement.line_number
|
|
68
|
+
effect = statement.effect
|
|
69
|
+
|
|
70
|
+
# Track which condition keys have set operators and Null checks
|
|
71
|
+
set_operator_keys: dict[str, str] = {} # key -> operator prefix
|
|
72
|
+
null_checked_keys: set[str] = set()
|
|
73
|
+
|
|
74
|
+
# First pass: Identify set operators and Null checks
|
|
75
|
+
for operator, conditions in statement.condition.items():
|
|
76
|
+
base_operator, operator_type, set_prefix = normalize_operator(operator)
|
|
77
|
+
|
|
78
|
+
# Track Null checks
|
|
79
|
+
if base_operator == "Null":
|
|
80
|
+
for condition_key in conditions.keys():
|
|
81
|
+
null_checked_keys.add(condition_key)
|
|
82
|
+
|
|
83
|
+
# Track set operators
|
|
84
|
+
if set_prefix in ["ForAllValues", "ForAnyValue"]:
|
|
85
|
+
for condition_key in conditions.keys():
|
|
86
|
+
set_operator_keys[condition_key] = set_prefix
|
|
87
|
+
|
|
88
|
+
# Second pass: Validate set operator usage
|
|
89
|
+
for operator, conditions in statement.condition.items():
|
|
90
|
+
base_operator, operator_type, set_prefix = normalize_operator(operator)
|
|
91
|
+
|
|
92
|
+
if not set_prefix:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Check each condition key used with a set operator
|
|
96
|
+
for condition_key, condition_values in conditions.items():
|
|
97
|
+
# Issue 1: Set operator used with single-valued context key (anti-pattern)
|
|
98
|
+
if not is_multivalued_context_key(condition_key):
|
|
99
|
+
issues.append(
|
|
100
|
+
ValidationIssue(
|
|
101
|
+
severity=self.get_severity(config),
|
|
102
|
+
message=(
|
|
103
|
+
f"Set operator '{set_prefix}' should not be used with single-valued "
|
|
104
|
+
f"condition key '{condition_key}'. This can lead to overly permissive policies. "
|
|
105
|
+
f"Set operators are designed for multivalued context keys like 'aws:TagKeys'. "
|
|
106
|
+
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
107
|
+
),
|
|
108
|
+
statement_sid=statement_sid,
|
|
109
|
+
statement_index=statement_idx,
|
|
110
|
+
issue_type="set_operator_on_single_valued_key",
|
|
111
|
+
condition_key=condition_key,
|
|
112
|
+
line_number=line_number,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Issue 2: ForAllValues with Allow effect without Null check (security risk)
|
|
117
|
+
if set_prefix == "ForAllValues" and effect == "Allow":
|
|
118
|
+
if condition_key not in null_checked_keys:
|
|
119
|
+
issues.append(
|
|
120
|
+
ValidationIssue(
|
|
121
|
+
severity="warning",
|
|
122
|
+
message=(
|
|
123
|
+
f"Security risk: ForAllValues with Allow effect on '{condition_key}' "
|
|
124
|
+
f"should include a Null condition check. Without it, requests with missing "
|
|
125
|
+
f'\'{condition_key}\' will be granted access. Add: \'"Null": {{"{condition_key}": "false"}}\'. '
|
|
126
|
+
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
127
|
+
),
|
|
128
|
+
statement_sid=statement_sid,
|
|
129
|
+
statement_index=statement_idx,
|
|
130
|
+
issue_type="forallvalues_allow_without_null_check",
|
|
131
|
+
condition_key=condition_key,
|
|
132
|
+
line_number=line_number,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Issue 3: ForAnyValue with Deny effect without Null check (unpredictable)
|
|
137
|
+
if set_prefix == "ForAnyValue" and effect == "Deny":
|
|
138
|
+
if condition_key not in null_checked_keys:
|
|
139
|
+
issues.append(
|
|
140
|
+
ValidationIssue(
|
|
141
|
+
severity="warning",
|
|
142
|
+
message=(
|
|
143
|
+
f"Unpredictable behavior: ForAnyValue with Deny effect on '{condition_key}' "
|
|
144
|
+
f"should include a Null condition check. Without it, requests with missing "
|
|
145
|
+
f"'{condition_key}' will evaluate to 'No match' instead of denying access. "
|
|
146
|
+
f'Add: \'"Null": {{"{condition_key}": "false"}}\'. '
|
|
147
|
+
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
148
|
+
),
|
|
149
|
+
statement_sid=statement_sid,
|
|
150
|
+
statement_index=statement_idx,
|
|
151
|
+
issue_type="foranyvalue_deny_without_null_check",
|
|
152
|
+
condition_key=condition_key,
|
|
153
|
+
line_number=line_number,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return issues
|