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
|
@@ -0,0 +1,250 @@
|
|
|
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 = f"{suggestion_text}\n\nExample:\n{example}" if example else suggestion_text
|
|
147
|
+
|
|
148
|
+
# Determine severity based on the highest severity action in the list
|
|
149
|
+
# If single action, use its category severity
|
|
150
|
+
# If multiple actions, use the highest severity among them
|
|
151
|
+
severity = self.get_severity(config) # Default
|
|
152
|
+
if matched_actions:
|
|
153
|
+
# Get severity for first action (or highest if we want to be more sophisticated)
|
|
154
|
+
severity = self._get_severity_for_action(matched_actions[0], config)
|
|
155
|
+
|
|
156
|
+
issues.append(
|
|
157
|
+
ValidationIssue(
|
|
158
|
+
severity=severity,
|
|
159
|
+
statement_sid=statement.sid,
|
|
160
|
+
statement_index=statement_idx,
|
|
161
|
+
issue_type="missing_condition",
|
|
162
|
+
message=message,
|
|
163
|
+
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
164
|
+
suggestion=suggestion,
|
|
165
|
+
line_number=statement.line_number,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return issues
|
|
170
|
+
|
|
171
|
+
async def execute_policy(
|
|
172
|
+
self,
|
|
173
|
+
policy: "IAMPolicy",
|
|
174
|
+
policy_file: str,
|
|
175
|
+
fetcher: AWSServiceFetcher,
|
|
176
|
+
config: CheckConfig,
|
|
177
|
+
**kwargs,
|
|
178
|
+
) -> list[ValidationIssue]:
|
|
179
|
+
"""
|
|
180
|
+
Execute policy-level sensitive action checks.
|
|
181
|
+
|
|
182
|
+
This method examines the entire policy to detect privilege escalation patterns
|
|
183
|
+
and other security issues that span multiple statements.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
policy: The complete IAM policy to check
|
|
187
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
188
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
189
|
+
config: Configuration for this check instance
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of ValidationIssue objects found by this check
|
|
193
|
+
"""
|
|
194
|
+
del policy_file, fetcher # Not used in current implementation
|
|
195
|
+
issues = []
|
|
196
|
+
|
|
197
|
+
# Collect all actions from all Allow statements across the entire policy
|
|
198
|
+
all_actions: set[str] = set()
|
|
199
|
+
statement_map: dict[
|
|
200
|
+
str, list[tuple[int, str | None]]
|
|
201
|
+
] = {} # action -> [(stmt_idx, sid), ...]
|
|
202
|
+
|
|
203
|
+
for idx, statement in enumerate(policy.statement):
|
|
204
|
+
if statement.effect == "Allow":
|
|
205
|
+
actions = statement.get_actions()
|
|
206
|
+
# Filter out wildcards for privilege escalation detection
|
|
207
|
+
filtered_actions = [a for a in actions if a != "*"]
|
|
208
|
+
|
|
209
|
+
for action in filtered_actions:
|
|
210
|
+
all_actions.add(action)
|
|
211
|
+
if action not in statement_map:
|
|
212
|
+
statement_map[action] = []
|
|
213
|
+
statement_map[action].append((idx, statement.sid))
|
|
214
|
+
|
|
215
|
+
# Get configuration for sensitive actions
|
|
216
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
217
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
218
|
+
|
|
219
|
+
# Check for privilege escalation patterns using all_of logic
|
|
220
|
+
# We need to check both exact actions and patterns
|
|
221
|
+
policy_issues = []
|
|
222
|
+
|
|
223
|
+
# Check sensitive_actions configuration
|
|
224
|
+
if sensitive_actions_config:
|
|
225
|
+
policy_issues.extend(
|
|
226
|
+
check_policy_level_actions(
|
|
227
|
+
list(all_actions),
|
|
228
|
+
statement_map,
|
|
229
|
+
sensitive_actions_config,
|
|
230
|
+
config,
|
|
231
|
+
"actions",
|
|
232
|
+
self.get_severity,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Check sensitive_action_patterns configuration
|
|
237
|
+
if sensitive_patterns_config:
|
|
238
|
+
policy_issues.extend(
|
|
239
|
+
check_policy_level_actions(
|
|
240
|
+
list(all_actions),
|
|
241
|
+
statement_map,
|
|
242
|
+
sensitive_patterns_config,
|
|
243
|
+
config,
|
|
244
|
+
"patterns",
|
|
245
|
+
self.get_severity,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
issues.extend(policy_issues)
|
|
250
|
+
return issues
|
|
@@ -0,0 +1,105 @@
|
|
|
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{example}" if example else suggestion_text
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
issues.append(
|
|
76
|
+
ValidationIssue(
|
|
77
|
+
severity=self.get_severity(config),
|
|
78
|
+
statement_sid=statement.sid,
|
|
79
|
+
statement_index=statement_idx,
|
|
80
|
+
issue_type="overly_permissive",
|
|
81
|
+
message=message,
|
|
82
|
+
action=action,
|
|
83
|
+
suggestion=suggestion,
|
|
84
|
+
line_number=statement.line_number,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return issues
|
|
89
|
+
|
|
90
|
+
def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
|
|
91
|
+
"""
|
|
92
|
+
Get list of services that are allowed to use service-level wildcards.
|
|
93
|
+
|
|
94
|
+
This allows configuration like:
|
|
95
|
+
service_wildcard:
|
|
96
|
+
allowed_services:
|
|
97
|
+
- "logs" # Allow "logs:*"
|
|
98
|
+
- "cloudwatch" # Allow "cloudwatch:*"
|
|
99
|
+
|
|
100
|
+
Returns empty set if no exceptions are configured.
|
|
101
|
+
"""
|
|
102
|
+
allowed = config.config.get("allowed_services", [])
|
|
103
|
+
if allowed and isinstance(allowed, list):
|
|
104
|
+
return set(allowed)
|
|
105
|
+
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
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides functionality to match actions against sensitive action
|
|
4
4
|
configurations, supporting exact matches, regex patterns, and any_of/all_of logic.
|
|
5
|
+
|
|
6
|
+
Performance optimizations:
|
|
7
|
+
- Uses frozenset for O(1) lookups
|
|
8
|
+
- LRU cache for compiled regex patterns
|
|
9
|
+
- Lazy loading of default actions from modular data structure
|
|
5
10
|
"""
|
|
6
11
|
|
|
7
12
|
import re
|
|
@@ -9,30 +14,57 @@ from functools import lru_cache
|
|
|
9
14
|
from re import Pattern
|
|
10
15
|
|
|
11
16
|
from iam_validator.core.check_registry import CheckConfig
|
|
17
|
+
from iam_validator.core.config.sensitive_actions import get_sensitive_actions
|
|
18
|
+
|
|
19
|
+
# Lazy-loaded default set of sensitive actions
|
|
20
|
+
# This will be loaded only when first accessed
|
|
21
|
+
_DEFAULT_SENSITIVE_ACTIONS_CACHE: frozenset[str] | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_default_sensitive_actions() -> frozenset[str]:
|
|
25
|
+
"""
|
|
26
|
+
Get default sensitive actions with lazy loading and caching.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Frozenset of all default sensitive actions
|
|
30
|
+
|
|
31
|
+
Performance:
|
|
32
|
+
- First call: Loads from sensitive actions list
|
|
33
|
+
- Subsequent calls: O(1) cached lookup
|
|
34
|
+
"""
|
|
35
|
+
global _DEFAULT_SENSITIVE_ACTIONS_CACHE
|
|
36
|
+
if _DEFAULT_SENSITIVE_ACTIONS_CACHE is None:
|
|
37
|
+
_DEFAULT_SENSITIVE_ACTIONS_CACHE = get_sensitive_actions()
|
|
38
|
+
return _DEFAULT_SENSITIVE_ACTIONS_CACHE
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_sensitive_actions_by_categories(categories: list[str] | None = None) -> frozenset[str]:
|
|
42
|
+
"""
|
|
43
|
+
Get sensitive actions filtered by categories.
|
|
12
44
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
45
|
+
Args:
|
|
46
|
+
categories: List of category IDs to include. If None, returns all actions.
|
|
47
|
+
Valid categories: 'credential_exposure', 'data_access',
|
|
48
|
+
'priv_esc', 'resource_exposure'
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Frozenset of sensitive actions matching the specified categories
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> # Get all sensitive actions (default behavior)
|
|
55
|
+
>>> all_actions = get_sensitive_actions_by_categories()
|
|
56
|
+
|
|
57
|
+
>>> # Get only privilege escalation actions
|
|
58
|
+
>>> priv_esc = get_sensitive_actions_by_categories(['priv_esc'])
|
|
59
|
+
|
|
60
|
+
>>> # Get credential exposure and data access actions
|
|
61
|
+
>>> sensitive = get_sensitive_actions_by_categories(['credential_exposure', 'data_access'])
|
|
62
|
+
"""
|
|
63
|
+
return get_sensitive_actions(categories)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Export for backward compatibility
|
|
67
|
+
DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
36
68
|
|
|
37
69
|
|
|
38
70
|
# Global regex pattern cache for performance
|
|
@@ -61,15 +93,28 @@ def check_sensitive_actions(
|
|
|
61
93
|
Args:
|
|
62
94
|
actions: List of actions to check
|
|
63
95
|
config: Check configuration
|
|
64
|
-
default_actions: Default sensitive actions to use if no config (
|
|
96
|
+
default_actions: Default sensitive actions to use if no config (lazy-loaded)
|
|
65
97
|
|
|
66
98
|
Returns:
|
|
67
99
|
tuple[bool, list[str]]: (is_sensitive, matched_actions)
|
|
68
100
|
- is_sensitive: True if the actions match the sensitive criteria
|
|
69
101
|
- matched_actions: List of actions that matched the criteria
|
|
102
|
+
|
|
103
|
+
Performance:
|
|
104
|
+
- Uses lazy-loaded defaults (only loaded on first use)
|
|
105
|
+
- O(1) frozenset lookups for action matching
|
|
70
106
|
"""
|
|
71
|
-
if
|
|
72
|
-
|
|
107
|
+
# Check if categories are specified in config
|
|
108
|
+
categories = config.config.get("categories")
|
|
109
|
+
if categories is not None:
|
|
110
|
+
# If categories is an empty list, disable the check
|
|
111
|
+
if len(categories) == 0:
|
|
112
|
+
return False, []
|
|
113
|
+
# Get sensitive actions filtered by categories
|
|
114
|
+
default_actions = get_sensitive_actions_by_categories(categories)
|
|
115
|
+
elif default_actions is None:
|
|
116
|
+
# Use all categories if no specific categories configured
|
|
117
|
+
default_actions = _get_default_sensitive_actions()
|
|
73
118
|
|
|
74
119
|
# Filter out wildcards
|
|
75
120
|
filtered_actions = [a for a in actions if a != "*"]
|
|
@@ -77,12 +122,9 @@ def check_sensitive_actions(
|
|
|
77
122
|
return False, []
|
|
78
123
|
|
|
79
124
|
# Get configuration for both sensitive_actions and sensitive_action_patterns
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
sensitive_actions_config = sub_check_config.get("sensitive_actions")
|
|
85
|
-
sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
|
|
125
|
+
# Config is now flat (no longer nested under sensitive_action_check)
|
|
126
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
127
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
86
128
|
|
|
87
129
|
# Check sensitive_actions (exact matches)
|
|
88
130
|
actions_match, actions_matched = check_actions_config(
|