iam-policy-validator 1.4.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.4.0.dist-info/METADATA +1022 -0
- iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
- iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.4.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 +27 -0
- iam_validator/checks/action_condition_enforcement.py +727 -0
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +70 -0
- iam_validator/checks/policy_size.py +151 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +282 -0
- iam_validator/checks/resource_validation.py +108 -0
- iam_validator/checks/security_best_practices.py +536 -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 +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +434 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +260 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +539 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +666 -0
- iam_validator/core/access_analyzer_report.py +643 -0
- iam_validator/core/aws_fetcher.py +880 -0
- iam_validator/core/aws_global_conditions.py +137 -0
- iam_validator/core/check_registry.py +469 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/config_loader.py +452 -0
- iam_validator/core/defaults.py +393 -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 +434 -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 +187 -0
- iam_validator/core/models.py +298 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +338 -0
- iam_validator/core/report.py +859 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +795 -0
- iam_validator/integrations/ms_teams.py +442 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Security best practices check - validates security anti-patterns."""
|
|
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.models import Statement, ValidationIssue
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from iam_validator.core.models import IAMPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SecurityBestPracticesCheck(PolicyCheck):
|
|
20
|
+
"""Checks for common security anti-patterns and best practices violations."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def check_id(self) -> str:
|
|
24
|
+
return "security_best_practices"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return "Checks for common security anti-patterns"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def default_severity(self) -> str:
|
|
32
|
+
return "warning"
|
|
33
|
+
|
|
34
|
+
async def execute(
|
|
35
|
+
self,
|
|
36
|
+
statement: Statement,
|
|
37
|
+
statement_idx: int,
|
|
38
|
+
fetcher: AWSServiceFetcher,
|
|
39
|
+
config: CheckConfig,
|
|
40
|
+
) -> list[ValidationIssue]:
|
|
41
|
+
"""Execute security best practices checks on a statement."""
|
|
42
|
+
issues = []
|
|
43
|
+
|
|
44
|
+
# Only check Allow statements
|
|
45
|
+
if statement.effect != "Allow":
|
|
46
|
+
return issues
|
|
47
|
+
|
|
48
|
+
statement_sid = statement.sid
|
|
49
|
+
line_number = statement.line_number
|
|
50
|
+
actions = statement.get_actions()
|
|
51
|
+
resources = statement.get_resources()
|
|
52
|
+
|
|
53
|
+
# Check 1: Wildcard action check
|
|
54
|
+
if self._is_sub_check_enabled(config, "wildcard_action_check"):
|
|
55
|
+
if "*" in actions:
|
|
56
|
+
severity = self._get_sub_check_severity(config, "wildcard_action_check", "warning")
|
|
57
|
+
sub_check_config = config.config.get("wildcard_action_check", {})
|
|
58
|
+
|
|
59
|
+
message = sub_check_config.get("message", "Statement allows all actions (*)")
|
|
60
|
+
suggestion_text = sub_check_config.get(
|
|
61
|
+
"suggestion", "Consider limiting to specific actions needed"
|
|
62
|
+
)
|
|
63
|
+
example = sub_check_config.get("example", "")
|
|
64
|
+
|
|
65
|
+
# Combine suggestion + example like action_condition_enforcement does
|
|
66
|
+
suggestion = (
|
|
67
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
issues.append(
|
|
71
|
+
ValidationIssue(
|
|
72
|
+
severity=severity,
|
|
73
|
+
statement_sid=statement_sid,
|
|
74
|
+
statement_index=statement_idx,
|
|
75
|
+
issue_type="overly_permissive",
|
|
76
|
+
message=message,
|
|
77
|
+
suggestion=suggestion,
|
|
78
|
+
line_number=line_number,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Check 2: Wildcard resource check
|
|
83
|
+
if self._is_sub_check_enabled(config, "wildcard_resource_check"):
|
|
84
|
+
if "*" in resources:
|
|
85
|
+
# Check if all actions are in the allowed_wildcards list
|
|
86
|
+
# allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
|
|
87
|
+
# to all matching AWS actions using the AWS API, then checking if the policy's
|
|
88
|
+
# actions are in that expanded list. This ensures only validated AWS actions
|
|
89
|
+
# are allowed with Resource: "*".
|
|
90
|
+
allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(
|
|
91
|
+
config, fetcher
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Check if ALL actions (excluding full wildcard "*") are in the expanded list
|
|
95
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
96
|
+
|
|
97
|
+
if allowed_wildcards_expanded and non_wildcard_actions:
|
|
98
|
+
# Check if all actions are in the expanded allowed list (exact match)
|
|
99
|
+
all_actions_allowed = all(
|
|
100
|
+
action in allowed_wildcards_expanded for action in non_wildcard_actions
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# If all actions are in the expanded list, skip the wildcard resource warning
|
|
104
|
+
if all_actions_allowed:
|
|
105
|
+
# All actions are safe, Resource: "*" is acceptable
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
# Some actions are not in allowed list, flag the issue
|
|
109
|
+
self._add_wildcard_resource_issue(
|
|
110
|
+
issues,
|
|
111
|
+
config,
|
|
112
|
+
statement_sid,
|
|
113
|
+
statement_idx,
|
|
114
|
+
line_number,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
# No allowed_wildcards configured OR only has "*" action
|
|
118
|
+
# Always flag wildcard resources in these cases
|
|
119
|
+
self._add_wildcard_resource_issue(
|
|
120
|
+
issues, config, statement_sid, statement_idx, line_number
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Check 3: Critical - both wildcards together
|
|
124
|
+
if self._is_sub_check_enabled(config, "full_wildcard_check"):
|
|
125
|
+
if "*" in actions and "*" in resources:
|
|
126
|
+
severity = self._get_sub_check_severity(config, "full_wildcard_check", "error")
|
|
127
|
+
sub_check_config = config.config.get("full_wildcard_check", {})
|
|
128
|
+
|
|
129
|
+
message = sub_check_config.get(
|
|
130
|
+
"message",
|
|
131
|
+
"Statement allows all actions on all resources - CRITICAL SECURITY RISK",
|
|
132
|
+
)
|
|
133
|
+
suggestion_text = sub_check_config.get(
|
|
134
|
+
"suggestion",
|
|
135
|
+
"This grants full administrative access. Restrict to specific actions and resources.",
|
|
136
|
+
)
|
|
137
|
+
example = sub_check_config.get("example", "")
|
|
138
|
+
|
|
139
|
+
# Combine suggestion + example
|
|
140
|
+
suggestion = (
|
|
141
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
issues.append(
|
|
145
|
+
ValidationIssue(
|
|
146
|
+
severity=severity,
|
|
147
|
+
statement_sid=statement_sid,
|
|
148
|
+
statement_index=statement_idx,
|
|
149
|
+
issue_type="security_risk",
|
|
150
|
+
message=message,
|
|
151
|
+
suggestion=suggestion,
|
|
152
|
+
line_number=line_number,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Check 4: Service-level wildcards (e.g., "iam:*", "s3:*")
|
|
157
|
+
if self._is_sub_check_enabled(config, "service_wildcard_check"):
|
|
158
|
+
allowed_services = self._get_allowed_service_wildcards(config)
|
|
159
|
+
|
|
160
|
+
for action in actions:
|
|
161
|
+
# Skip full wildcard (covered by wildcard_action_check)
|
|
162
|
+
if action == "*":
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Check if it's a service-level wildcard (e.g., "iam:*", "s3:*")
|
|
166
|
+
if ":" in action and action.endswith(":*"):
|
|
167
|
+
service = action.split(":")[0]
|
|
168
|
+
|
|
169
|
+
# Check if this service is in the allowed list
|
|
170
|
+
if service not in allowed_services:
|
|
171
|
+
severity = self._get_sub_check_severity(
|
|
172
|
+
config, "service_wildcard_check", "warning"
|
|
173
|
+
)
|
|
174
|
+
sub_check_config = config.config.get("service_wildcard_check", {})
|
|
175
|
+
|
|
176
|
+
# Get message template and replace placeholders
|
|
177
|
+
message_template = sub_check_config.get(
|
|
178
|
+
"message",
|
|
179
|
+
"Service-level wildcard '{action}' grants all permissions for {service} service",
|
|
180
|
+
)
|
|
181
|
+
suggestion_template = sub_check_config.get(
|
|
182
|
+
"suggestion",
|
|
183
|
+
"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*'.",
|
|
184
|
+
)
|
|
185
|
+
example_template = sub_check_config.get("example", "")
|
|
186
|
+
|
|
187
|
+
message = message_template.format(action=action, service=service)
|
|
188
|
+
suggestion_text = suggestion_template.format(action=action, service=service)
|
|
189
|
+
example = (
|
|
190
|
+
example_template.format(action=action, service=service)
|
|
191
|
+
if example_template
|
|
192
|
+
else ""
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Combine suggestion + example
|
|
196
|
+
suggestion = (
|
|
197
|
+
f"{suggestion_text}\nExample:\n{example}"
|
|
198
|
+
if example
|
|
199
|
+
else suggestion_text
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
issues.append(
|
|
203
|
+
ValidationIssue(
|
|
204
|
+
severity=severity,
|
|
205
|
+
statement_sid=statement_sid,
|
|
206
|
+
statement_index=statement_idx,
|
|
207
|
+
issue_type="overly_permissive",
|
|
208
|
+
message=message,
|
|
209
|
+
action=action,
|
|
210
|
+
suggestion=suggestion,
|
|
211
|
+
line_number=line_number,
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Check 5: Sensitive actions without conditions
|
|
216
|
+
if self._is_sub_check_enabled(config, "sensitive_action_check"):
|
|
217
|
+
has_conditions = statement.condition is not None and len(statement.condition) > 0
|
|
218
|
+
|
|
219
|
+
# Expand wildcards to actual actions using AWS API
|
|
220
|
+
expanded_actions = await expand_wildcard_actions(actions, fetcher)
|
|
221
|
+
|
|
222
|
+
# Check if sensitive actions match using any_of/all_of logic
|
|
223
|
+
is_sensitive, matched_actions = check_sensitive_actions(
|
|
224
|
+
expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if is_sensitive and not has_conditions:
|
|
228
|
+
severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
|
|
229
|
+
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
230
|
+
|
|
231
|
+
# Create appropriate message based on matched actions using configurable templates
|
|
232
|
+
if len(matched_actions) == 1:
|
|
233
|
+
message_template = sub_check_config.get(
|
|
234
|
+
"message_single",
|
|
235
|
+
"Sensitive action '{action}' should have conditions to limit when it can be used",
|
|
236
|
+
)
|
|
237
|
+
message = message_template.format(action=matched_actions[0])
|
|
238
|
+
else:
|
|
239
|
+
action_list = "', '".join(matched_actions)
|
|
240
|
+
message_template = sub_check_config.get(
|
|
241
|
+
"message_multiple",
|
|
242
|
+
"Sensitive actions '{actions}' should have conditions to limit when they can be used",
|
|
243
|
+
)
|
|
244
|
+
message = message_template.format(actions=action_list)
|
|
245
|
+
|
|
246
|
+
suggestion_text = sub_check_config.get(
|
|
247
|
+
"suggestion",
|
|
248
|
+
"Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
|
|
249
|
+
)
|
|
250
|
+
example = sub_check_config.get("example", "")
|
|
251
|
+
|
|
252
|
+
# Combine suggestion + example
|
|
253
|
+
suggestion = (
|
|
254
|
+
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
issues.append(
|
|
258
|
+
ValidationIssue(
|
|
259
|
+
severity=severity,
|
|
260
|
+
statement_sid=statement_sid,
|
|
261
|
+
statement_index=statement_idx,
|
|
262
|
+
issue_type="missing_condition",
|
|
263
|
+
message=message,
|
|
264
|
+
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
265
|
+
suggestion=suggestion,
|
|
266
|
+
line_number=line_number,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return issues
|
|
271
|
+
|
|
272
|
+
async def execute_policy(
|
|
273
|
+
self,
|
|
274
|
+
policy: "IAMPolicy",
|
|
275
|
+
policy_file: str,
|
|
276
|
+
fetcher: AWSServiceFetcher,
|
|
277
|
+
config: CheckConfig,
|
|
278
|
+
**kwargs,
|
|
279
|
+
) -> list[ValidationIssue]:
|
|
280
|
+
"""
|
|
281
|
+
Execute policy-level security checks.
|
|
282
|
+
|
|
283
|
+
This method examines the entire policy to detect privilege escalation patterns
|
|
284
|
+
and other security issues that span multiple statements.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
policy: The complete IAM policy to check
|
|
288
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
289
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
290
|
+
config: Configuration for this check instance
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of ValidationIssue objects found by this check
|
|
294
|
+
"""
|
|
295
|
+
del policy_file, fetcher # Not used in current implementation
|
|
296
|
+
issues = []
|
|
297
|
+
|
|
298
|
+
# Only check if sensitive_action_check is enabled
|
|
299
|
+
if not self._is_sub_check_enabled(config, "sensitive_action_check"):
|
|
300
|
+
return issues
|
|
301
|
+
|
|
302
|
+
# Collect all actions from all Allow statements across the entire policy
|
|
303
|
+
all_actions: set[str] = set()
|
|
304
|
+
statement_map: dict[
|
|
305
|
+
str, list[tuple[int, str | None]]
|
|
306
|
+
] = {} # action -> [(stmt_idx, sid), ...]
|
|
307
|
+
|
|
308
|
+
for idx, statement in enumerate(policy.statement):
|
|
309
|
+
if statement.effect == "Allow":
|
|
310
|
+
actions = statement.get_actions()
|
|
311
|
+
# Filter out wildcards for privilege escalation detection
|
|
312
|
+
filtered_actions = [a for a in actions if a != "*"]
|
|
313
|
+
|
|
314
|
+
for action in filtered_actions:
|
|
315
|
+
all_actions.add(action)
|
|
316
|
+
if action not in statement_map:
|
|
317
|
+
statement_map[action] = []
|
|
318
|
+
statement_map[action].append((idx, statement.sid))
|
|
319
|
+
|
|
320
|
+
# Get configuration for sensitive actions
|
|
321
|
+
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
322
|
+
if not isinstance(sub_check_config, dict):
|
|
323
|
+
return issues
|
|
324
|
+
|
|
325
|
+
sensitive_actions_config = sub_check_config.get("sensitive_actions")
|
|
326
|
+
sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
|
|
327
|
+
|
|
328
|
+
# Check for privilege escalation patterns using all_of logic
|
|
329
|
+
# We need to check both exact actions and patterns
|
|
330
|
+
policy_issues = []
|
|
331
|
+
|
|
332
|
+
# Check sensitive_actions configuration
|
|
333
|
+
if sensitive_actions_config:
|
|
334
|
+
policy_issues.extend(
|
|
335
|
+
check_policy_level_actions(
|
|
336
|
+
list(all_actions),
|
|
337
|
+
statement_map,
|
|
338
|
+
sensitive_actions_config,
|
|
339
|
+
config,
|
|
340
|
+
"actions",
|
|
341
|
+
self._get_sub_check_severity,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Check sensitive_action_patterns configuration
|
|
346
|
+
if sensitive_patterns_config:
|
|
347
|
+
policy_issues.extend(
|
|
348
|
+
check_policy_level_actions(
|
|
349
|
+
list(all_actions),
|
|
350
|
+
statement_map,
|
|
351
|
+
sensitive_patterns_config,
|
|
352
|
+
config,
|
|
353
|
+
"patterns",
|
|
354
|
+
self._get_sub_check_severity,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
issues.extend(policy_issues)
|
|
359
|
+
return issues
|
|
360
|
+
|
|
361
|
+
def _is_sub_check_enabled(self, config: CheckConfig, sub_check_name: str) -> bool:
|
|
362
|
+
"""Check if a sub-check is enabled in the configuration."""
|
|
363
|
+
if sub_check_name not in config.config:
|
|
364
|
+
return True # Enabled by default
|
|
365
|
+
|
|
366
|
+
sub_check_config = config.config.get(sub_check_name, {})
|
|
367
|
+
if isinstance(sub_check_config, dict):
|
|
368
|
+
return sub_check_config.get("enabled", True)
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def _get_sub_check_severity(
|
|
372
|
+
self, config: CheckConfig, sub_check_name: str, default: str
|
|
373
|
+
) -> str:
|
|
374
|
+
"""Get severity for a sub-check."""
|
|
375
|
+
if sub_check_name not in config.config:
|
|
376
|
+
return default
|
|
377
|
+
|
|
378
|
+
sub_check_config = config.config.get(sub_check_name, {})
|
|
379
|
+
if isinstance(sub_check_config, dict):
|
|
380
|
+
return sub_check_config.get("severity", default)
|
|
381
|
+
return default
|
|
382
|
+
|
|
383
|
+
def _add_wildcard_resource_issue(
|
|
384
|
+
self,
|
|
385
|
+
issues: list[ValidationIssue],
|
|
386
|
+
config: CheckConfig,
|
|
387
|
+
statement_sid: str | None,
|
|
388
|
+
statement_idx: int,
|
|
389
|
+
line_number: int | None,
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Add a wildcard resource issue to the issues list.
|
|
392
|
+
|
|
393
|
+
This is a helper method to avoid code duplication when adding
|
|
394
|
+
wildcard resource warnings.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
issues: List to append the issue to
|
|
398
|
+
config: Check configuration
|
|
399
|
+
statement_sid: Statement ID
|
|
400
|
+
statement_idx: Statement index
|
|
401
|
+
line_number: Line number in the policy file
|
|
402
|
+
"""
|
|
403
|
+
severity = self._get_sub_check_severity(config, "wildcard_resource_check", "warning")
|
|
404
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
405
|
+
|
|
406
|
+
message = sub_check_config.get("message", "Statement applies to all resources (*)")
|
|
407
|
+
suggestion_text = sub_check_config.get(
|
|
408
|
+
"suggestion", "Consider limiting to specific resources"
|
|
409
|
+
)
|
|
410
|
+
example = sub_check_config.get("example", "")
|
|
411
|
+
|
|
412
|
+
# Combine suggestion + example
|
|
413
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
414
|
+
|
|
415
|
+
issues.append(
|
|
416
|
+
ValidationIssue(
|
|
417
|
+
severity=severity,
|
|
418
|
+
statement_sid=statement_sid,
|
|
419
|
+
statement_index=statement_idx,
|
|
420
|
+
issue_type="overly_permissive",
|
|
421
|
+
message=message,
|
|
422
|
+
suggestion=suggestion,
|
|
423
|
+
line_number=line_number,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
|
|
428
|
+
"""
|
|
429
|
+
Get list of services that are allowed to use service-level wildcards.
|
|
430
|
+
|
|
431
|
+
This allows configuration like:
|
|
432
|
+
service_wildcard_check:
|
|
433
|
+
allowed_services:
|
|
434
|
+
- "logs" # Allow "logs:*"
|
|
435
|
+
- "cloudwatch" # Allow "cloudwatch:*"
|
|
436
|
+
|
|
437
|
+
Returns empty set if no exceptions are configured.
|
|
438
|
+
"""
|
|
439
|
+
sub_check_config = config.config.get("service_wildcard_check", {})
|
|
440
|
+
|
|
441
|
+
if isinstance(sub_check_config, dict):
|
|
442
|
+
allowed = sub_check_config.get("allowed_services", [])
|
|
443
|
+
if allowed and isinstance(allowed, list):
|
|
444
|
+
return set(allowed)
|
|
445
|
+
|
|
446
|
+
return set()
|
|
447
|
+
|
|
448
|
+
def _get_allowed_wildcards_for_resources(self, config: CheckConfig) -> frozenset[str]:
|
|
449
|
+
"""Get allowed_wildcards for resource check configuration.
|
|
450
|
+
|
|
451
|
+
This checks for explicit allowed_wildcards configuration in wildcard_resource_check.
|
|
452
|
+
If not configured, it falls back to the parent security_best_practices_check's allowed_wildcards.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
config: The check configuration
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
A frozenset of allowed wildcard patterns
|
|
459
|
+
"""
|
|
460
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
461
|
+
if isinstance(sub_check_config, dict) and "allowed_wildcards" in sub_check_config:
|
|
462
|
+
# Explicitly configured in wildcard_resource_check (override)
|
|
463
|
+
allowed_wildcards = sub_check_config.get("allowed_wildcards", [])
|
|
464
|
+
if isinstance(allowed_wildcards, list):
|
|
465
|
+
return frozenset(allowed_wildcards)
|
|
466
|
+
elif isinstance(allowed_wildcards, set | frozenset):
|
|
467
|
+
return frozenset(allowed_wildcards)
|
|
468
|
+
return frozenset()
|
|
469
|
+
|
|
470
|
+
# Fall back to parent security_best_practices_check's allowed_wildcards
|
|
471
|
+
parent_allowed_wildcards = config.config.get("allowed_wildcards", [])
|
|
472
|
+
if isinstance(parent_allowed_wildcards, list):
|
|
473
|
+
return frozenset(parent_allowed_wildcards)
|
|
474
|
+
elif isinstance(parent_allowed_wildcards, set | frozenset):
|
|
475
|
+
return frozenset(parent_allowed_wildcards)
|
|
476
|
+
|
|
477
|
+
# No configuration found, return empty set (flag all Resource: "*")
|
|
478
|
+
return frozenset()
|
|
479
|
+
|
|
480
|
+
async def _get_expanded_allowed_wildcards(
|
|
481
|
+
self, config: CheckConfig, fetcher: AWSServiceFetcher
|
|
482
|
+
) -> frozenset[str]:
|
|
483
|
+
"""Get and expand allowed_wildcards configuration.
|
|
484
|
+
|
|
485
|
+
This method retrieves wildcard patterns from the allowed_wildcards config
|
|
486
|
+
and expands them using the AWS API to get all matching actual AWS actions.
|
|
487
|
+
|
|
488
|
+
How it works:
|
|
489
|
+
1. Retrieves patterns from config (e.g., ["ec2:Describe*", "s3:List*"])
|
|
490
|
+
2. Expands each pattern using AWS API:
|
|
491
|
+
- "ec2:Describe*" → ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
492
|
+
- "s3:List*" → ["s3:ListBucket", "s3:ListObjects", ...]
|
|
493
|
+
3. Returns a set of all expanded actions
|
|
494
|
+
|
|
495
|
+
This allows you to:
|
|
496
|
+
- Specify patterns like "ec2:Describe*" in config
|
|
497
|
+
- Have the validator allow specific actions like "ec2:DescribeInstances" with Resource: "*"
|
|
498
|
+
- Ensure only real AWS actions (validated via API) are allowed
|
|
499
|
+
|
|
500
|
+
Example:
|
|
501
|
+
Config: allowed_wildcards: ["ec2:Describe*"]
|
|
502
|
+
Expands to: ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
503
|
+
Policy: "Action": ["ec2:DescribeInstances"], "Resource": "*"
|
|
504
|
+
Result: ✅ Allowed (ec2:DescribeInstances is in expanded list)
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
config: The check configuration
|
|
508
|
+
fetcher: AWS service fetcher for expanding wildcards via AWS API
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
A frozenset of all expanded action names from the configured patterns
|
|
512
|
+
"""
|
|
513
|
+
# Check wildcard_resource_check first for override
|
|
514
|
+
sub_check_config = config.config.get("wildcard_resource_check", {})
|
|
515
|
+
patterns_to_expand: list[str] = []
|
|
516
|
+
|
|
517
|
+
if isinstance(sub_check_config, dict) and "allowed_wildcards" in sub_check_config:
|
|
518
|
+
# Explicitly configured in wildcard_resource_check (override)
|
|
519
|
+
patterns = sub_check_config.get("allowed_wildcards", [])
|
|
520
|
+
if isinstance(patterns, list):
|
|
521
|
+
patterns_to_expand = patterns
|
|
522
|
+
else:
|
|
523
|
+
# Fall back to parent security_best_practices_check's allowed_wildcards
|
|
524
|
+
parent_patterns = config.config.get("allowed_wildcards", [])
|
|
525
|
+
if isinstance(parent_patterns, list):
|
|
526
|
+
patterns_to_expand = parent_patterns
|
|
527
|
+
|
|
528
|
+
# If no patterns configured, return empty set
|
|
529
|
+
if not patterns_to_expand:
|
|
530
|
+
return frozenset()
|
|
531
|
+
|
|
532
|
+
# Expand the wildcard patterns using the AWS API
|
|
533
|
+
# This converts patterns like "ec2:Describe*" to actual AWS actions
|
|
534
|
+
expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
|
|
535
|
+
|
|
536
|
+
return frozenset(expanded_actions)
|