iam-policy-validator 1.0.4__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +88 -10
- iam_policy_validator-1.1.1.dist-info/RECORD +53 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_condition_enforcement.py +112 -28
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +18 -138
- iam_validator/checks/security_best_practices.py +241 -400
- 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 +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +89 -0
- iam_validator/commands/__init__.py +3 -1
- iam_validator/commands/cache.py +402 -0
- iam_validator/commands/validate.py +7 -5
- iam_validator/core/access_analyzer_report.py +2 -1
- iam_validator/core/aws_fetcher.py +79 -19
- iam_validator/core/check_registry.py +3 -0
- iam_validator/core/cli.py +1 -1
- iam_validator/core/config_loader.py +40 -3
- iam_validator/core/defaults.py +334 -0
- iam_validator/core/formatters/__init__.py +2 -0
- iam_validator/core/formatters/console.py +44 -7
- iam_validator/core/formatters/csv.py +7 -2
- iam_validator/core/formatters/enhanced.py +433 -0
- iam_validator/core/formatters/html.py +127 -37
- iam_validator/core/formatters/markdown.py +10 -2
- iam_validator/core/models.py +30 -6
- iam_validator/core/policy_checks.py +21 -2
- iam_validator/core/report.py +112 -26
- iam_policy_validator-1.0.4.dist-info/RECORD +0 -45
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""Security best practices check - validates security anti-patterns."""
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
from re import Pattern
|
|
6
3
|
from typing import TYPE_CHECKING
|
|
7
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 (
|
|
11
|
+
compile_wildcard_pattern,
|
|
12
|
+
expand_wildcard_actions,
|
|
13
|
+
)
|
|
8
14
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
9
15
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
10
16
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
@@ -13,49 +19,9 @@ if TYPE_CHECKING:
|
|
|
13
19
|
from iam_validator.core.models import IAMPolicy
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
# Global regex pattern cache for performance
|
|
17
|
-
@lru_cache(maxsize=256)
|
|
18
|
-
def _compile_pattern(pattern: str) -> Pattern[str] | None:
|
|
19
|
-
"""Compile and cache regex patterns.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
pattern: Regex pattern string
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
Compiled pattern or None if invalid
|
|
26
|
-
"""
|
|
27
|
-
try:
|
|
28
|
-
return re.compile(pattern)
|
|
29
|
-
except re.error:
|
|
30
|
-
return None
|
|
31
|
-
|
|
32
|
-
|
|
33
22
|
class SecurityBestPracticesCheck(PolicyCheck):
|
|
34
23
|
"""Checks for common security anti-patterns and best practices violations."""
|
|
35
24
|
|
|
36
|
-
# Default set of sensitive actions that should have conditions
|
|
37
|
-
# Using frozenset for O(1) lookups and immutability
|
|
38
|
-
DEFAULT_SENSITIVE_ACTIONS = frozenset(
|
|
39
|
-
{
|
|
40
|
-
"iam:CreateUser",
|
|
41
|
-
"iam:CreateRole",
|
|
42
|
-
"iam:PutUserPolicy",
|
|
43
|
-
"iam:PutRolePolicy",
|
|
44
|
-
"iam:AttachUserPolicy",
|
|
45
|
-
"iam:AttachRolePolicy",
|
|
46
|
-
"iam:CreateAccessKey",
|
|
47
|
-
"iam:DeleteUser",
|
|
48
|
-
"iam:DeleteRole",
|
|
49
|
-
"s3:DeleteBucket",
|
|
50
|
-
"s3:PutBucketPolicy",
|
|
51
|
-
"s3:DeleteBucketPolicy",
|
|
52
|
-
"ec2:TerminateInstances",
|
|
53
|
-
"ec2:DeleteVolume",
|
|
54
|
-
"rds:DeleteDBInstance",
|
|
55
|
-
"lambda:DeleteFunction",
|
|
56
|
-
}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
25
|
@property
|
|
60
26
|
def check_id(self) -> str:
|
|
61
27
|
return "security_best_practices"
|
|
@@ -91,14 +57,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
91
57
|
if self._is_sub_check_enabled(config, "wildcard_action_check"):
|
|
92
58
|
if "*" in actions:
|
|
93
59
|
severity = self._get_sub_check_severity(config, "wildcard_action_check", "warning")
|
|
60
|
+
sub_check_config = config.config.get("wildcard_action_check", {})
|
|
61
|
+
|
|
62
|
+
message = sub_check_config.get("message", "Statement allows all actions (*)")
|
|
63
|
+
suggestion_text = sub_check_config.get(
|
|
64
|
+
"suggestion", "Consider limiting to specific actions needed"
|
|
65
|
+
)
|
|
66
|
+
example = sub_check_config.get("example", "")
|
|
67
|
+
|
|
68
|
+
# Combine suggestion + example like action_condition_enforcement does
|
|
69
|
+
suggestion = (
|
|
70
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
71
|
+
)
|
|
72
|
+
|
|
94
73
|
issues.append(
|
|
95
74
|
ValidationIssue(
|
|
96
75
|
severity=severity,
|
|
97
76
|
statement_sid=statement_sid,
|
|
98
77
|
statement_index=statement_idx,
|
|
99
78
|
issue_type="overly_permissive",
|
|
100
|
-
message=
|
|
101
|
-
suggestion=
|
|
79
|
+
message=message,
|
|
80
|
+
suggestion=suggestion,
|
|
102
81
|
line_number=line_number,
|
|
103
82
|
)
|
|
104
83
|
)
|
|
@@ -106,33 +85,69 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
106
85
|
# Check 2: Wildcard resource check
|
|
107
86
|
if self._is_sub_check_enabled(config, "wildcard_resource_check"):
|
|
108
87
|
if "*" in resources:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
88
|
+
# Check if all actions are in the allowed_wildcards list
|
|
89
|
+
# This allows Resource: "*" when only safe read-only wildcard actions are used
|
|
90
|
+
allowed_wildcards = self._get_allowed_wildcards_for_resources(config)
|
|
91
|
+
|
|
92
|
+
# Check if ALL actions (excluding full wildcard "*") match allowed patterns
|
|
93
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
94
|
+
|
|
95
|
+
if allowed_wildcards and non_wildcard_actions:
|
|
96
|
+
# Check if all actions are allowed wildcards
|
|
97
|
+
all_actions_allowed = all(
|
|
98
|
+
self._is_action_allowed_wildcard(action, allowed_wildcards)
|
|
99
|
+
for action in non_wildcard_actions
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# If all actions are in the allowed list, skip the wildcard resource warning
|
|
103
|
+
if all_actions_allowed:
|
|
104
|
+
# All actions are safe wildcards, Resource: "*" is acceptable
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
# Some actions are not in allowed list, flag the issue
|
|
108
|
+
self._add_wildcard_resource_issue(
|
|
109
|
+
issues,
|
|
110
|
+
config,
|
|
111
|
+
statement_sid,
|
|
112
|
+
statement_idx,
|
|
113
|
+
line_number,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
# No allowed_wildcards configured OR only has "*" action
|
|
117
|
+
# Always flag wildcard resources in these cases
|
|
118
|
+
self._add_wildcard_resource_issue(
|
|
119
|
+
issues, config, statement_sid, statement_idx, line_number
|
|
121
120
|
)
|
|
122
|
-
)
|
|
123
121
|
|
|
124
122
|
# Check 3: Critical - both wildcards together
|
|
125
123
|
if self._is_sub_check_enabled(config, "full_wildcard_check"):
|
|
126
124
|
if "*" in actions and "*" in resources:
|
|
127
125
|
severity = self._get_sub_check_severity(config, "full_wildcard_check", "error")
|
|
126
|
+
sub_check_config = config.config.get("full_wildcard_check", {})
|
|
127
|
+
|
|
128
|
+
message = sub_check_config.get(
|
|
129
|
+
"message",
|
|
130
|
+
"Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
131
|
+
)
|
|
132
|
+
suggestion_text = sub_check_config.get(
|
|
133
|
+
"suggestion",
|
|
134
|
+
"This grants full administrative access. Restrict to specific actions and resources.",
|
|
135
|
+
)
|
|
136
|
+
example = sub_check_config.get("example", "")
|
|
137
|
+
|
|
138
|
+
# Combine suggestion + example
|
|
139
|
+
suggestion = (
|
|
140
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
141
|
+
)
|
|
142
|
+
|
|
128
143
|
issues.append(
|
|
129
144
|
ValidationIssue(
|
|
130
145
|
severity=severity,
|
|
131
146
|
statement_sid=statement_sid,
|
|
132
147
|
statement_index=statement_idx,
|
|
133
148
|
issue_type="security_risk",
|
|
134
|
-
message=
|
|
135
|
-
suggestion=
|
|
149
|
+
message=message,
|
|
150
|
+
suggestion=suggestion,
|
|
136
151
|
line_number=line_number,
|
|
137
152
|
)
|
|
138
153
|
)
|
|
@@ -155,15 +170,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
155
170
|
severity = self._get_sub_check_severity(
|
|
156
171
|
config, "service_wildcard_check", "warning"
|
|
157
172
|
)
|
|
173
|
+
sub_check_config = config.config.get("service_wildcard_check", {})
|
|
174
|
+
|
|
175
|
+
# Get message template and replace placeholders
|
|
176
|
+
message_template = sub_check_config.get(
|
|
177
|
+
"message",
|
|
178
|
+
"Service-level wildcard '{action}' grants all permissions for {service} service",
|
|
179
|
+
)
|
|
180
|
+
suggestion_template = sub_check_config.get(
|
|
181
|
+
"suggestion",
|
|
182
|
+
"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*'.",
|
|
183
|
+
)
|
|
184
|
+
example_template = sub_check_config.get("example", "")
|
|
185
|
+
|
|
186
|
+
message = message_template.format(action=action, service=service)
|
|
187
|
+
suggestion_text = suggestion_template.format(action=action, service=service)
|
|
188
|
+
example = (
|
|
189
|
+
example_template.format(action=action, service=service)
|
|
190
|
+
if example_template
|
|
191
|
+
else ""
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Combine suggestion + example
|
|
195
|
+
suggestion = (
|
|
196
|
+
f"{suggestion_text}\nExample:\n{example}"
|
|
197
|
+
if example
|
|
198
|
+
else suggestion_text
|
|
199
|
+
)
|
|
200
|
+
|
|
158
201
|
issues.append(
|
|
159
202
|
ValidationIssue(
|
|
160
203
|
severity=severity,
|
|
161
204
|
statement_sid=statement_sid,
|
|
162
205
|
statement_index=statement_idx,
|
|
163
206
|
issue_type="overly_permissive",
|
|
164
|
-
message=
|
|
207
|
+
message=message,
|
|
165
208
|
action=action,
|
|
166
|
-
suggestion=
|
|
209
|
+
suggestion=suggestion,
|
|
167
210
|
line_number=line_number,
|
|
168
211
|
)
|
|
169
212
|
)
|
|
@@ -172,18 +215,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
172
215
|
if self._is_sub_check_enabled(config, "sensitive_action_check"):
|
|
173
216
|
has_conditions = statement.condition is not None and len(statement.condition) > 0
|
|
174
217
|
|
|
218
|
+
# Expand wildcards to actual actions using AWS API
|
|
219
|
+
expanded_actions = await expand_wildcard_actions(actions, fetcher)
|
|
220
|
+
|
|
175
221
|
# Check if sensitive actions match using any_of/all_of logic
|
|
176
|
-
is_sensitive, matched_actions =
|
|
222
|
+
is_sensitive, matched_actions = check_sensitive_actions(
|
|
223
|
+
expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
|
|
224
|
+
)
|
|
177
225
|
|
|
178
226
|
if is_sensitive and not has_conditions:
|
|
179
227
|
severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
|
|
228
|
+
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
180
229
|
|
|
181
|
-
# Create appropriate message based on matched actions
|
|
230
|
+
# Create appropriate message based on matched actions using configurable templates
|
|
182
231
|
if len(matched_actions) == 1:
|
|
183
|
-
|
|
232
|
+
message_template = sub_check_config.get(
|
|
233
|
+
"message_single",
|
|
234
|
+
"Sensitive action '{action}' should have conditions to limit when it can be used",
|
|
235
|
+
)
|
|
236
|
+
message = message_template.format(action=matched_actions[0])
|
|
184
237
|
else:
|
|
185
238
|
action_list = "', '".join(matched_actions)
|
|
186
|
-
|
|
239
|
+
message_template = sub_check_config.get(
|
|
240
|
+
"message_multiple",
|
|
241
|
+
"Sensitive actions '{actions}' should have conditions to limit when they can be used",
|
|
242
|
+
)
|
|
243
|
+
message = message_template.format(actions=action_list)
|
|
244
|
+
|
|
245
|
+
suggestion_text = sub_check_config.get(
|
|
246
|
+
"suggestion",
|
|
247
|
+
"Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
|
|
248
|
+
)
|
|
249
|
+
example = sub_check_config.get("example", "")
|
|
250
|
+
|
|
251
|
+
# Combine suggestion + example
|
|
252
|
+
suggestion = (
|
|
253
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
254
|
+
)
|
|
187
255
|
|
|
188
256
|
issues.append(
|
|
189
257
|
ValidationIssue(
|
|
@@ -193,7 +261,7 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
193
261
|
issue_type="missing_condition",
|
|
194
262
|
message=message,
|
|
195
263
|
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
196
|
-
suggestion=
|
|
264
|
+
suggestion=suggestion,
|
|
197
265
|
line_number=line_number,
|
|
198
266
|
)
|
|
199
267
|
)
|
|
@@ -262,164 +330,32 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
262
330
|
# Check sensitive_actions configuration
|
|
263
331
|
if sensitive_actions_config:
|
|
264
332
|
policy_issues.extend(
|
|
265
|
-
|
|
333
|
+
check_policy_level_actions(
|
|
266
334
|
list(all_actions),
|
|
267
335
|
statement_map,
|
|
268
336
|
sensitive_actions_config,
|
|
269
337
|
config,
|
|
270
338
|
"actions",
|
|
339
|
+
self._get_sub_check_severity,
|
|
271
340
|
)
|
|
272
341
|
)
|
|
273
342
|
|
|
274
343
|
# Check sensitive_action_patterns configuration
|
|
275
344
|
if sensitive_patterns_config:
|
|
276
345
|
policy_issues.extend(
|
|
277
|
-
|
|
346
|
+
check_policy_level_actions(
|
|
278
347
|
list(all_actions),
|
|
279
348
|
statement_map,
|
|
280
349
|
sensitive_patterns_config,
|
|
281
350
|
config,
|
|
282
351
|
"patterns",
|
|
352
|
+
self._get_sub_check_severity,
|
|
283
353
|
)
|
|
284
354
|
)
|
|
285
355
|
|
|
286
356
|
issues.extend(policy_issues)
|
|
287
357
|
return issues
|
|
288
358
|
|
|
289
|
-
def _check_policy_level_actions(
|
|
290
|
-
self,
|
|
291
|
-
all_actions: list[str],
|
|
292
|
-
statement_map: dict[str, list[tuple[int, str | None]]],
|
|
293
|
-
config,
|
|
294
|
-
check_config: CheckConfig,
|
|
295
|
-
check_type: str,
|
|
296
|
-
) -> list[ValidationIssue]:
|
|
297
|
-
"""
|
|
298
|
-
Check for policy-level privilege escalation patterns.
|
|
299
|
-
|
|
300
|
-
Args:
|
|
301
|
-
all_actions: All actions across the entire policy
|
|
302
|
-
statement_map: Mapping of action -> [(statement_idx, sid), ...]
|
|
303
|
-
config: The sensitive_actions or sensitive_action_patterns configuration
|
|
304
|
-
check_config: Full check configuration
|
|
305
|
-
check_type: Either "actions" (exact match) or "patterns" (regex match)
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
List of ValidationIssue objects
|
|
309
|
-
"""
|
|
310
|
-
import re
|
|
311
|
-
|
|
312
|
-
issues = []
|
|
313
|
-
|
|
314
|
-
if not config:
|
|
315
|
-
return issues
|
|
316
|
-
|
|
317
|
-
# Handle list of items (could be simple strings or dicts with all_of/any_of)
|
|
318
|
-
if isinstance(config, list):
|
|
319
|
-
for item in config:
|
|
320
|
-
if isinstance(item, dict) and "all_of" in item:
|
|
321
|
-
# This is a privilege escalation pattern - all actions must be present
|
|
322
|
-
required_actions = item["all_of"]
|
|
323
|
-
matched_actions = []
|
|
324
|
-
|
|
325
|
-
if check_type == "actions":
|
|
326
|
-
# Exact matching
|
|
327
|
-
matched_actions = [a for a in all_actions if a in required_actions]
|
|
328
|
-
else:
|
|
329
|
-
# Pattern matching - for each pattern, find actions that match
|
|
330
|
-
for pattern in required_actions:
|
|
331
|
-
for action in all_actions:
|
|
332
|
-
try:
|
|
333
|
-
if re.match(pattern, action):
|
|
334
|
-
matched_actions.append(action)
|
|
335
|
-
break # Found at least one match for this pattern
|
|
336
|
-
except re.error:
|
|
337
|
-
continue
|
|
338
|
-
|
|
339
|
-
# Check if ALL required actions/patterns are present
|
|
340
|
-
if len(matched_actions) >= len(required_actions):
|
|
341
|
-
# Privilege escalation detected!
|
|
342
|
-
severity = self._get_sub_check_severity(
|
|
343
|
-
check_config, "sensitive_action_check", "error"
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
# Collect which statements these actions appear in
|
|
347
|
-
statement_refs = []
|
|
348
|
-
for action in matched_actions:
|
|
349
|
-
if action in statement_map:
|
|
350
|
-
for stmt_idx, sid in statement_map[action]:
|
|
351
|
-
sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
|
|
352
|
-
statement_refs.append(f"Statement {sid_str}: {action}")
|
|
353
|
-
|
|
354
|
-
action_list = "', '".join(matched_actions)
|
|
355
|
-
stmt_details = "\n - ".join(statement_refs)
|
|
356
|
-
|
|
357
|
-
issues.append(
|
|
358
|
-
ValidationIssue(
|
|
359
|
-
severity=severity,
|
|
360
|
-
statement_sid=None, # Policy-level issue
|
|
361
|
-
statement_index=-1, # -1 indicates policy-level issue
|
|
362
|
-
issue_type="privilege_escalation",
|
|
363
|
-
message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
|
|
364
|
-
suggestion=f"These actions combined allow privilege escalation. Consider:\n"
|
|
365
|
-
f" 1. Splitting into separate policies for different users/roles\n"
|
|
366
|
-
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
367
|
-
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
368
|
-
f"Actions found in:\n - {stmt_details}",
|
|
369
|
-
line_number=None,
|
|
370
|
-
)
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
# Handle dict with all_of at the top level
|
|
374
|
-
elif isinstance(config, dict) and "all_of" in config:
|
|
375
|
-
required_actions = config["all_of"]
|
|
376
|
-
matched_actions = []
|
|
377
|
-
|
|
378
|
-
if check_type == "actions":
|
|
379
|
-
matched_actions = [a for a in all_actions if a in required_actions]
|
|
380
|
-
else:
|
|
381
|
-
for pattern in required_actions:
|
|
382
|
-
for action in all_actions:
|
|
383
|
-
try:
|
|
384
|
-
if re.match(pattern, action):
|
|
385
|
-
matched_actions.append(action)
|
|
386
|
-
break
|
|
387
|
-
except re.error:
|
|
388
|
-
continue
|
|
389
|
-
|
|
390
|
-
if len(matched_actions) >= len(required_actions):
|
|
391
|
-
severity = self._get_sub_check_severity(
|
|
392
|
-
check_config, "sensitive_action_check", "error"
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
statement_refs = []
|
|
396
|
-
for action in matched_actions:
|
|
397
|
-
if action in statement_map:
|
|
398
|
-
for stmt_idx, sid in statement_map[action]:
|
|
399
|
-
sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
|
|
400
|
-
statement_refs.append(f"Statement {sid_str}: {action}")
|
|
401
|
-
|
|
402
|
-
action_list = "', '".join(matched_actions)
|
|
403
|
-
stmt_details = "\n - ".join(statement_refs)
|
|
404
|
-
|
|
405
|
-
issues.append(
|
|
406
|
-
ValidationIssue(
|
|
407
|
-
severity=severity,
|
|
408
|
-
statement_sid=None,
|
|
409
|
-
statement_index=-1, # -1 indicates policy-level issue
|
|
410
|
-
issue_type="privilege_escalation",
|
|
411
|
-
message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
|
|
412
|
-
suggestion=f"These actions combined allow privilege escalation. Consider:\n"
|
|
413
|
-
f" 1. Splitting into separate policies for different users/roles\n"
|
|
414
|
-
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
415
|
-
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
416
|
-
f"Actions found in:\n - {stmt_details}",
|
|
417
|
-
line_number=None,
|
|
418
|
-
)
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
return issues
|
|
422
|
-
|
|
423
359
|
def _is_sub_check_enabled(self, config: CheckConfig, sub_check_name: str) -> bool:
|
|
424
360
|
"""Check if a sub-check is enabled in the configuration."""
|
|
425
361
|
if sub_check_name not in config.config:
|
|
@@ -442,6 +378,50 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
442
378
|
return sub_check_config.get("severity", default)
|
|
443
379
|
return default
|
|
444
380
|
|
|
381
|
+
def _add_wildcard_resource_issue(
|
|
382
|
+
self,
|
|
383
|
+
issues: list[ValidationIssue],
|
|
384
|
+
config: CheckConfig,
|
|
385
|
+
statement_sid: str | None,
|
|
386
|
+
statement_idx: int,
|
|
387
|
+
line_number: int | None,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Add a wildcard resource issue to the issues list.
|
|
390
|
+
|
|
391
|
+
This is a helper method to avoid code duplication when adding
|
|
392
|
+
wildcard resource warnings.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
issues: List to append the issue to
|
|
396
|
+
config: Check configuration
|
|
397
|
+
statement_sid: Statement ID
|
|
398
|
+
statement_idx: Statement index
|
|
399
|
+
line_number: Line number in the policy file
|
|
400
|
+
"""
|
|
401
|
+
severity = self._get_sub_check_severity(config, "wildcard_resource_check", "warning")
|
|
402
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
403
|
+
|
|
404
|
+
message = sub_check_config.get("message", "Statement applies to all resources (*)")
|
|
405
|
+
suggestion_text = sub_check_config.get(
|
|
406
|
+
"suggestion", "Consider limiting to specific resources"
|
|
407
|
+
)
|
|
408
|
+
example = sub_check_config.get("example", "")
|
|
409
|
+
|
|
410
|
+
# Combine suggestion + example
|
|
411
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
412
|
+
|
|
413
|
+
issues.append(
|
|
414
|
+
ValidationIssue(
|
|
415
|
+
severity=severity,
|
|
416
|
+
statement_sid=statement_sid,
|
|
417
|
+
statement_index=statement_idx,
|
|
418
|
+
issue_type="overly_permissive",
|
|
419
|
+
message=message,
|
|
420
|
+
suggestion=suggestion,
|
|
421
|
+
line_number=line_number,
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
445
425
|
def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
|
|
446
426
|
"""
|
|
447
427
|
Get list of services that are allowed to use service-level wildcards.
|
|
@@ -463,212 +443,73 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
463
443
|
|
|
464
444
|
return set()
|
|
465
445
|
|
|
466
|
-
def
|
|
467
|
-
self,
|
|
468
|
-
) ->
|
|
469
|
-
"""
|
|
470
|
-
Check if actions match sensitive action criteria with any_of/all_of support.
|
|
471
|
-
|
|
472
|
-
Returns:
|
|
473
|
-
tuple[bool, list[str]]: (is_sensitive, matched_actions)
|
|
474
|
-
- is_sensitive: True if the actions match the sensitive criteria
|
|
475
|
-
- matched_actions: List of actions that matched the criteria
|
|
476
|
-
"""
|
|
477
|
-
# Filter out wildcards
|
|
478
|
-
filtered_actions = [a for a in actions if a != "*"]
|
|
479
|
-
if not filtered_actions:
|
|
480
|
-
return False, []
|
|
481
|
-
|
|
482
|
-
# Get configuration for both sensitive_actions and sensitive_action_patterns
|
|
483
|
-
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
484
|
-
if not isinstance(sub_check_config, dict):
|
|
485
|
-
return False, []
|
|
486
|
-
|
|
487
|
-
sensitive_actions_config = sub_check_config.get("sensitive_actions")
|
|
488
|
-
sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
|
|
489
|
-
|
|
490
|
-
# Check sensitive_actions (exact matches)
|
|
491
|
-
actions_match, actions_matched = self._check_actions_config(
|
|
492
|
-
filtered_actions, sensitive_actions_config
|
|
493
|
-
)
|
|
446
|
+
def _is_action_allowed_wildcard(
|
|
447
|
+
self, action: str, allowed_wildcards: frozenset[str] | list[str] | set[str]
|
|
448
|
+
) -> bool:
|
|
449
|
+
"""Check if an action matches the allowed_wildcards list.
|
|
494
450
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
451
|
+
This method checks if a given action is in the allowed_wildcards configuration
|
|
452
|
+
from action_validation_check. This is used to determine if wildcard resources
|
|
453
|
+
are acceptable when only safe wildcard actions are used.
|
|
499
454
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
matched_set = set(actions_matched) | set(patterns_matched)
|
|
504
|
-
matched_actions = list(matched_set)
|
|
455
|
+
Args:
|
|
456
|
+
action: The action to check (e.g., "s3:List*", "ec2:DescribeInstances")
|
|
457
|
+
allowed_wildcards: Set or list of allowed wildcard patterns
|
|
505
458
|
|
|
506
|
-
|
|
459
|
+
Returns:
|
|
460
|
+
True if the action matches any pattern in the allowlist
|
|
507
461
|
|
|
508
|
-
|
|
462
|
+
Note:
|
|
463
|
+
Exact matches use O(1) set lookup for performance.
|
|
464
|
+
Pattern matches (wildcards in allowlist) require O(n) iteration.
|
|
509
465
|
"""
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
-
|
|
515
|
-
|
|
516
|
-
|
|
466
|
+
# Fast O(1) exact match using set membership
|
|
467
|
+
if action in allowed_wildcards:
|
|
468
|
+
return True
|
|
469
|
+
|
|
470
|
+
# Pattern match - check if action matches any pattern in allowlist
|
|
471
|
+
# This is needed when allowlist contains wildcards like "s3:*"
|
|
472
|
+
# Uses cached compiled patterns for 20-30x speedup
|
|
473
|
+
for pattern in allowed_wildcards:
|
|
474
|
+
# Skip exact matches (already checked above)
|
|
475
|
+
if "*" not in pattern:
|
|
476
|
+
continue
|
|
517
477
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
# If no config, fall back to defaults with any_of logic
|
|
523
|
-
# DEFAULT_SENSITIVE_ACTIONS is already a frozenset for O(1) lookups
|
|
524
|
-
matched = [a for a in actions if a in self.DEFAULT_SENSITIVE_ACTIONS]
|
|
525
|
-
return len(matched) > 0, matched
|
|
526
|
-
|
|
527
|
-
# Handle simple list with potential mixed items
|
|
528
|
-
if isinstance(config, list):
|
|
529
|
-
# Use set for O(1) membership checks
|
|
530
|
-
all_matched = set()
|
|
531
|
-
actions_set = set(actions) # Convert once for O(1) lookups
|
|
532
|
-
|
|
533
|
-
for item in config:
|
|
534
|
-
# Each item can be a string, or a dict with any_of/all_of
|
|
535
|
-
if isinstance(item, str):
|
|
536
|
-
# Simple string - check if action matches (O(1) lookup)
|
|
537
|
-
if item in actions_set:
|
|
538
|
-
all_matched.add(item)
|
|
539
|
-
elif isinstance(item, dict):
|
|
540
|
-
# Recurse for dict items
|
|
541
|
-
matches, matched = self._check_actions_config(actions, item)
|
|
542
|
-
if matches:
|
|
543
|
-
all_matched.update(matched)
|
|
544
|
-
|
|
545
|
-
return len(all_matched) > 0, list(all_matched)
|
|
546
|
-
|
|
547
|
-
# Handle dict with any_of/all_of
|
|
548
|
-
if isinstance(config, dict):
|
|
549
|
-
# any_of: at least one action must match
|
|
550
|
-
if "any_of" in config:
|
|
551
|
-
# Convert once for O(1) intersection
|
|
552
|
-
any_of_set = set(config["any_of"])
|
|
553
|
-
actions_set = set(actions)
|
|
554
|
-
matched = list(any_of_set & actions_set)
|
|
555
|
-
return len(matched) > 0, matched
|
|
556
|
-
|
|
557
|
-
# all_of: all specified actions must be present in the statement
|
|
558
|
-
if "all_of" in config:
|
|
559
|
-
all_of_set = set(config["all_of"])
|
|
560
|
-
actions_set = set(actions)
|
|
561
|
-
matched = list(all_of_set & actions_set)
|
|
562
|
-
# All required actions must be present
|
|
563
|
-
return all_of_set.issubset(actions_set), matched
|
|
564
|
-
|
|
565
|
-
return False, []
|
|
566
|
-
|
|
567
|
-
def _check_patterns_config(self, actions: list[str], config) -> tuple[bool, list[str]]:
|
|
568
|
-
"""
|
|
569
|
-
Check actions against sensitive_action_patterns configuration.
|
|
478
|
+
# Use cached compiled pattern
|
|
479
|
+
compiled = compile_wildcard_pattern(pattern)
|
|
480
|
+
if compiled.match(action):
|
|
481
|
+
return True
|
|
570
482
|
|
|
571
|
-
|
|
572
|
-
- Simple list: ["^pattern1.*", "^pattern2.*"] (backward compatible, any_of logic)
|
|
573
|
-
- any_of: {"any_of": ["^pattern1.*", "^pattern2.*"]}
|
|
574
|
-
- all_of: {"all_of": ["^pattern1.*", "^pattern2.*"]}
|
|
575
|
-
- Multiple groups: [{"all_of": [...]}, {"any_of": [...]}, "^pattern.*"]
|
|
483
|
+
return False
|
|
576
484
|
|
|
577
|
-
|
|
578
|
-
|
|
485
|
+
def _get_allowed_wildcards_for_resources(self, config: CheckConfig) -> frozenset[str]:
|
|
486
|
+
"""Get allowed_wildcards for resource check configuration.
|
|
579
487
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
"""
|
|
583
|
-
if not config:
|
|
584
|
-
return False, []
|
|
585
|
-
|
|
586
|
-
# Handle simple list with potential mixed items
|
|
587
|
-
if isinstance(config, list):
|
|
588
|
-
# Use set for O(1) membership checks instead of list
|
|
589
|
-
all_matched = set()
|
|
590
|
-
|
|
591
|
-
for item in config:
|
|
592
|
-
# Each item can be a string pattern, or a dict with any_of/all_of
|
|
593
|
-
if isinstance(item, str):
|
|
594
|
-
# Simple string pattern - check if any action matches
|
|
595
|
-
# Use cached compiled pattern
|
|
596
|
-
compiled = _compile_pattern(item)
|
|
597
|
-
if compiled:
|
|
598
|
-
for action in actions:
|
|
599
|
-
if compiled.match(action):
|
|
600
|
-
all_matched.add(action)
|
|
601
|
-
elif isinstance(item, dict):
|
|
602
|
-
# Recurse for dict items
|
|
603
|
-
matches, matched = self._check_patterns_config(actions, item)
|
|
604
|
-
if matches:
|
|
605
|
-
all_matched.update(matched)
|
|
606
|
-
|
|
607
|
-
return len(all_matched) > 0, list(all_matched)
|
|
608
|
-
|
|
609
|
-
# Handle dict with any_of/all_of
|
|
610
|
-
if isinstance(config, dict):
|
|
611
|
-
# any_of: at least one action must match at least one pattern
|
|
612
|
-
if "any_of" in config:
|
|
613
|
-
matched = set()
|
|
614
|
-
# Pre-compile all patterns
|
|
615
|
-
compiled_patterns = [_compile_pattern(p) for p in config["any_of"]]
|
|
616
|
-
|
|
617
|
-
for action in actions:
|
|
618
|
-
for compiled in compiled_patterns:
|
|
619
|
-
if compiled and compiled.match(action):
|
|
620
|
-
matched.add(action)
|
|
621
|
-
break
|
|
622
|
-
return len(matched) > 0, list(matched)
|
|
623
|
-
|
|
624
|
-
# all_of: at least one action must match ALL patterns
|
|
625
|
-
if "all_of" in config:
|
|
626
|
-
# Pre-compile all patterns
|
|
627
|
-
compiled_patterns = [_compile_pattern(p) for p in config["all_of"]]
|
|
628
|
-
# Filter out invalid patterns
|
|
629
|
-
compiled_patterns = [p for p in compiled_patterns if p]
|
|
630
|
-
|
|
631
|
-
if not compiled_patterns:
|
|
632
|
-
return False, []
|
|
633
|
-
|
|
634
|
-
matched = set()
|
|
635
|
-
for action in actions:
|
|
636
|
-
# Check if this action matches ALL patterns
|
|
637
|
-
if all(compiled.match(action) for compiled in compiled_patterns):
|
|
638
|
-
matched.add(action)
|
|
639
|
-
|
|
640
|
-
return len(matched) > 0, list(matched)
|
|
641
|
-
|
|
642
|
-
return False, []
|
|
643
|
-
|
|
644
|
-
def _matches_sensitive_pattern(self, action: str, config: CheckConfig) -> bool:
|
|
645
|
-
"""
|
|
646
|
-
DEPRECATED: Use _check_sensitive_actions instead.
|
|
488
|
+
This checks for explicit allowed_wildcards configuration in wildcard_resource_check.
|
|
489
|
+
If not configured, it falls back to the parent security_best_practices_check's allowed_wildcards.
|
|
647
490
|
|
|
648
|
-
|
|
491
|
+
Args:
|
|
492
|
+
config: The check configuration
|
|
649
493
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
- "^iam:.*" # All IAM actions
|
|
653
|
-
- ".*:Delete.*" # Any delete action
|
|
654
|
-
- "s3:PutBucket.*" # S3 bucket modification actions
|
|
494
|
+
Returns:
|
|
495
|
+
A frozenset of allowed wildcard patterns
|
|
655
496
|
"""
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
return
|
|
497
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
498
|
+
if isinstance(sub_check_config, dict) and "allowed_wildcards" in sub_check_config:
|
|
499
|
+
# Explicitly configured in wildcard_resource_check (override)
|
|
500
|
+
allowed_wildcards = sub_check_config.get("allowed_wildcards", [])
|
|
501
|
+
if isinstance(allowed_wildcards, list):
|
|
502
|
+
return frozenset(allowed_wildcards)
|
|
503
|
+
elif isinstance(allowed_wildcards, set | frozenset):
|
|
504
|
+
return frozenset(allowed_wildcards)
|
|
505
|
+
return frozenset()
|
|
506
|
+
|
|
507
|
+
# Fall back to parent security_best_practices_check's allowed_wildcards
|
|
508
|
+
parent_allowed_wildcards = config.config.get("allowed_wildcards", [])
|
|
509
|
+
if isinstance(parent_allowed_wildcards, list):
|
|
510
|
+
return frozenset(parent_allowed_wildcards)
|
|
511
|
+
elif isinstance(parent_allowed_wildcards, set | frozenset):
|
|
512
|
+
return frozenset(parent_allowed_wildcards)
|
|
513
|
+
|
|
514
|
+
# No configuration found, return empty set (flag all Resource: "*")
|
|
515
|
+
return frozenset()
|