iam-policy-validator 1.1.0__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.1.0.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +2 -2
- {iam_policy_validator-1.1.0.dist-info → iam_policy_validator-1.1.1.dist-info}/RECORD +25 -19
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +18 -138
- iam_validator/checks/security_best_practices.py +152 -402
- 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/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 +1 -0
- iam_validator/core/defaults.py +103 -73
- iam_validator/core/formatters/enhanced.py +6 -1
- iam_validator/core/policy_checks.py +21 -2
- iam_validator/core/report.py +8 -1
- {iam_policy_validator-1.1.0.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.1.0.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.1.0.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"
|
|
@@ -119,33 +85,39 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
119
85
|
# Check 2: Wildcard resource check
|
|
120
86
|
if self._is_sub_check_enabled(config, "wildcard_resource_check"):
|
|
121
87
|
if "*" in resources:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
136
|
-
)
|
|
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
|
+
)
|
|
137
101
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
147
120
|
)
|
|
148
|
-
)
|
|
149
121
|
|
|
150
122
|
# Check 3: Critical - both wildcards together
|
|
151
123
|
if self._is_sub_check_enabled(config, "full_wildcard_check"):
|
|
@@ -243,8 +215,13 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
243
215
|
if self._is_sub_check_enabled(config, "sensitive_action_check"):
|
|
244
216
|
has_conditions = statement.condition is not None and len(statement.condition) > 0
|
|
245
217
|
|
|
218
|
+
# Expand wildcards to actual actions using AWS API
|
|
219
|
+
expanded_actions = await expand_wildcard_actions(actions, fetcher)
|
|
220
|
+
|
|
246
221
|
# Check if sensitive actions match using any_of/all_of logic
|
|
247
|
-
is_sensitive, matched_actions =
|
|
222
|
+
is_sensitive, matched_actions = check_sensitive_actions(
|
|
223
|
+
expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
|
|
224
|
+
)
|
|
248
225
|
|
|
249
226
|
if is_sensitive and not has_conditions:
|
|
250
227
|
severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
|
|
@@ -353,164 +330,32 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
353
330
|
# Check sensitive_actions configuration
|
|
354
331
|
if sensitive_actions_config:
|
|
355
332
|
policy_issues.extend(
|
|
356
|
-
|
|
333
|
+
check_policy_level_actions(
|
|
357
334
|
list(all_actions),
|
|
358
335
|
statement_map,
|
|
359
336
|
sensitive_actions_config,
|
|
360
337
|
config,
|
|
361
338
|
"actions",
|
|
339
|
+
self._get_sub_check_severity,
|
|
362
340
|
)
|
|
363
341
|
)
|
|
364
342
|
|
|
365
343
|
# Check sensitive_action_patterns configuration
|
|
366
344
|
if sensitive_patterns_config:
|
|
367
345
|
policy_issues.extend(
|
|
368
|
-
|
|
346
|
+
check_policy_level_actions(
|
|
369
347
|
list(all_actions),
|
|
370
348
|
statement_map,
|
|
371
349
|
sensitive_patterns_config,
|
|
372
350
|
config,
|
|
373
351
|
"patterns",
|
|
352
|
+
self._get_sub_check_severity,
|
|
374
353
|
)
|
|
375
354
|
)
|
|
376
355
|
|
|
377
356
|
issues.extend(policy_issues)
|
|
378
357
|
return issues
|
|
379
358
|
|
|
380
|
-
def _check_policy_level_actions(
|
|
381
|
-
self,
|
|
382
|
-
all_actions: list[str],
|
|
383
|
-
statement_map: dict[str, list[tuple[int, str | None]]],
|
|
384
|
-
config,
|
|
385
|
-
check_config: CheckConfig,
|
|
386
|
-
check_type: str,
|
|
387
|
-
) -> list[ValidationIssue]:
|
|
388
|
-
"""
|
|
389
|
-
Check for policy-level privilege escalation patterns.
|
|
390
|
-
|
|
391
|
-
Args:
|
|
392
|
-
all_actions: All actions across the entire policy
|
|
393
|
-
statement_map: Mapping of action -> [(statement_idx, sid), ...]
|
|
394
|
-
config: The sensitive_actions or sensitive_action_patterns configuration
|
|
395
|
-
check_config: Full check configuration
|
|
396
|
-
check_type: Either "actions" (exact match) or "patterns" (regex match)
|
|
397
|
-
|
|
398
|
-
Returns:
|
|
399
|
-
List of ValidationIssue objects
|
|
400
|
-
"""
|
|
401
|
-
import re
|
|
402
|
-
|
|
403
|
-
issues = []
|
|
404
|
-
|
|
405
|
-
if not config:
|
|
406
|
-
return issues
|
|
407
|
-
|
|
408
|
-
# Handle list of items (could be simple strings or dicts with all_of/any_of)
|
|
409
|
-
if isinstance(config, list):
|
|
410
|
-
for item in config:
|
|
411
|
-
if isinstance(item, dict) and "all_of" in item:
|
|
412
|
-
# This is a privilege escalation pattern - all actions must be present
|
|
413
|
-
required_actions = item["all_of"]
|
|
414
|
-
matched_actions = []
|
|
415
|
-
|
|
416
|
-
if check_type == "actions":
|
|
417
|
-
# Exact matching
|
|
418
|
-
matched_actions = [a for a in all_actions if a in required_actions]
|
|
419
|
-
else:
|
|
420
|
-
# Pattern matching - for each pattern, find actions that match
|
|
421
|
-
for pattern in required_actions:
|
|
422
|
-
for action in all_actions:
|
|
423
|
-
try:
|
|
424
|
-
if re.match(pattern, action):
|
|
425
|
-
matched_actions.append(action)
|
|
426
|
-
break # Found at least one match for this pattern
|
|
427
|
-
except re.error:
|
|
428
|
-
continue
|
|
429
|
-
|
|
430
|
-
# Check if ALL required actions/patterns are present
|
|
431
|
-
if len(matched_actions) >= len(required_actions):
|
|
432
|
-
# Privilege escalation detected!
|
|
433
|
-
severity = self._get_sub_check_severity(
|
|
434
|
-
check_config, "sensitive_action_check", "error"
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
# Collect which statements these actions appear in
|
|
438
|
-
statement_refs = []
|
|
439
|
-
for action in matched_actions:
|
|
440
|
-
if action in statement_map:
|
|
441
|
-
for stmt_idx, sid in statement_map[action]:
|
|
442
|
-
sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
|
|
443
|
-
statement_refs.append(f"Statement {sid_str}: {action}")
|
|
444
|
-
|
|
445
|
-
action_list = "', '".join(matched_actions)
|
|
446
|
-
stmt_details = "\n - ".join(statement_refs)
|
|
447
|
-
|
|
448
|
-
issues.append(
|
|
449
|
-
ValidationIssue(
|
|
450
|
-
severity=severity,
|
|
451
|
-
statement_sid=None, # Policy-level issue
|
|
452
|
-
statement_index=-1, # -1 indicates policy-level issue
|
|
453
|
-
issue_type="privilege_escalation",
|
|
454
|
-
message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
|
|
455
|
-
suggestion=f"These actions combined allow privilege escalation. Consider:\n"
|
|
456
|
-
f" 1. Splitting into separate policies for different users/roles\n"
|
|
457
|
-
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
458
|
-
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
459
|
-
f"Actions found in:\n - {stmt_details}",
|
|
460
|
-
line_number=None,
|
|
461
|
-
)
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Handle dict with all_of at the top level
|
|
465
|
-
elif isinstance(config, dict) and "all_of" in config:
|
|
466
|
-
required_actions = config["all_of"]
|
|
467
|
-
matched_actions = []
|
|
468
|
-
|
|
469
|
-
if check_type == "actions":
|
|
470
|
-
matched_actions = [a for a in all_actions if a in required_actions]
|
|
471
|
-
else:
|
|
472
|
-
for pattern in required_actions:
|
|
473
|
-
for action in all_actions:
|
|
474
|
-
try:
|
|
475
|
-
if re.match(pattern, action):
|
|
476
|
-
matched_actions.append(action)
|
|
477
|
-
break
|
|
478
|
-
except re.error:
|
|
479
|
-
continue
|
|
480
|
-
|
|
481
|
-
if len(matched_actions) >= len(required_actions):
|
|
482
|
-
severity = self._get_sub_check_severity(
|
|
483
|
-
check_config, "sensitive_action_check", "error"
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
statement_refs = []
|
|
487
|
-
for action in matched_actions:
|
|
488
|
-
if action in statement_map:
|
|
489
|
-
for stmt_idx, sid in statement_map[action]:
|
|
490
|
-
sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
|
|
491
|
-
statement_refs.append(f"Statement {sid_str}: {action}")
|
|
492
|
-
|
|
493
|
-
action_list = "', '".join(matched_actions)
|
|
494
|
-
stmt_details = "\n - ".join(statement_refs)
|
|
495
|
-
|
|
496
|
-
issues.append(
|
|
497
|
-
ValidationIssue(
|
|
498
|
-
severity=severity,
|
|
499
|
-
statement_sid=None,
|
|
500
|
-
statement_index=-1, # -1 indicates policy-level issue
|
|
501
|
-
issue_type="privilege_escalation",
|
|
502
|
-
message=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
|
|
503
|
-
suggestion=f"These actions combined allow privilege escalation. Consider:\n"
|
|
504
|
-
f" 1. Splitting into separate policies for different users/roles\n"
|
|
505
|
-
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
506
|
-
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
507
|
-
f"Actions found in:\n - {stmt_details}",
|
|
508
|
-
line_number=None,
|
|
509
|
-
)
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
return issues
|
|
513
|
-
|
|
514
359
|
def _is_sub_check_enabled(self, config: CheckConfig, sub_check_name: str) -> bool:
|
|
515
360
|
"""Check if a sub-check is enabled in the configuration."""
|
|
516
361
|
if sub_check_name not in config.config:
|
|
@@ -533,6 +378,50 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
533
378
|
return sub_check_config.get("severity", default)
|
|
534
379
|
return default
|
|
535
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
|
+
|
|
536
425
|
def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
|
|
537
426
|
"""
|
|
538
427
|
Get list of services that are allowed to use service-level wildcards.
|
|
@@ -554,212 +443,73 @@ class SecurityBestPracticesCheck(PolicyCheck):
|
|
|
554
443
|
|
|
555
444
|
return set()
|
|
556
445
|
|
|
557
|
-
def
|
|
558
|
-
self,
|
|
559
|
-
) ->
|
|
560
|
-
"""
|
|
561
|
-
Check if actions match sensitive action criteria with any_of/all_of support.
|
|
562
|
-
|
|
563
|
-
Returns:
|
|
564
|
-
tuple[bool, list[str]]: (is_sensitive, matched_actions)
|
|
565
|
-
- is_sensitive: True if the actions match the sensitive criteria
|
|
566
|
-
- matched_actions: List of actions that matched the criteria
|
|
567
|
-
"""
|
|
568
|
-
# Filter out wildcards
|
|
569
|
-
filtered_actions = [a for a in actions if a != "*"]
|
|
570
|
-
if not filtered_actions:
|
|
571
|
-
return False, []
|
|
572
|
-
|
|
573
|
-
# Get configuration for both sensitive_actions and sensitive_action_patterns
|
|
574
|
-
sub_check_config = config.config.get("sensitive_action_check", {})
|
|
575
|
-
if not isinstance(sub_check_config, dict):
|
|
576
|
-
return False, []
|
|
577
|
-
|
|
578
|
-
sensitive_actions_config = sub_check_config.get("sensitive_actions")
|
|
579
|
-
sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
|
|
580
|
-
|
|
581
|
-
# Check sensitive_actions (exact matches)
|
|
582
|
-
actions_match, actions_matched = self._check_actions_config(
|
|
583
|
-
filtered_actions, sensitive_actions_config
|
|
584
|
-
)
|
|
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.
|
|
585
450
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
)
|
|
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.
|
|
590
454
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
matched_set = set(actions_matched) | set(patterns_matched)
|
|
595
|
-
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
|
|
596
458
|
|
|
597
|
-
|
|
459
|
+
Returns:
|
|
460
|
+
True if the action matches any pattern in the allowlist
|
|
598
461
|
|
|
599
|
-
|
|
462
|
+
Note:
|
|
463
|
+
Exact matches use O(1) set lookup for performance.
|
|
464
|
+
Pattern matches (wildcards in allowlist) require O(n) iteration.
|
|
600
465
|
"""
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
|
608
477
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
# If no config, fall back to defaults with any_of logic
|
|
614
|
-
# DEFAULT_SENSITIVE_ACTIONS is already a frozenset for O(1) lookups
|
|
615
|
-
matched = [a for a in actions if a in self.DEFAULT_SENSITIVE_ACTIONS]
|
|
616
|
-
return len(matched) > 0, matched
|
|
617
|
-
|
|
618
|
-
# Handle simple list with potential mixed items
|
|
619
|
-
if isinstance(config, list):
|
|
620
|
-
# Use set for O(1) membership checks
|
|
621
|
-
all_matched = set()
|
|
622
|
-
actions_set = set(actions) # Convert once for O(1) lookups
|
|
623
|
-
|
|
624
|
-
for item in config:
|
|
625
|
-
# Each item can be a string, or a dict with any_of/all_of
|
|
626
|
-
if isinstance(item, str):
|
|
627
|
-
# Simple string - check if action matches (O(1) lookup)
|
|
628
|
-
if item in actions_set:
|
|
629
|
-
all_matched.add(item)
|
|
630
|
-
elif isinstance(item, dict):
|
|
631
|
-
# Recurse for dict items
|
|
632
|
-
matches, matched = self._check_actions_config(actions, item)
|
|
633
|
-
if matches:
|
|
634
|
-
all_matched.update(matched)
|
|
635
|
-
|
|
636
|
-
return len(all_matched) > 0, list(all_matched)
|
|
637
|
-
|
|
638
|
-
# Handle dict with any_of/all_of
|
|
639
|
-
if isinstance(config, dict):
|
|
640
|
-
# any_of: at least one action must match
|
|
641
|
-
if "any_of" in config:
|
|
642
|
-
# Convert once for O(1) intersection
|
|
643
|
-
any_of_set = set(config["any_of"])
|
|
644
|
-
actions_set = set(actions)
|
|
645
|
-
matched = list(any_of_set & actions_set)
|
|
646
|
-
return len(matched) > 0, matched
|
|
647
|
-
|
|
648
|
-
# all_of: all specified actions must be present in the statement
|
|
649
|
-
if "all_of" in config:
|
|
650
|
-
all_of_set = set(config["all_of"])
|
|
651
|
-
actions_set = set(actions)
|
|
652
|
-
matched = list(all_of_set & actions_set)
|
|
653
|
-
# All required actions must be present
|
|
654
|
-
return all_of_set.issubset(actions_set), matched
|
|
655
|
-
|
|
656
|
-
return False, []
|
|
657
|
-
|
|
658
|
-
def _check_patterns_config(self, actions: list[str], config) -> tuple[bool, list[str]]:
|
|
659
|
-
"""
|
|
660
|
-
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
|
|
661
482
|
|
|
662
|
-
|
|
663
|
-
- Simple list: ["^pattern1.*", "^pattern2.*"] (backward compatible, any_of logic)
|
|
664
|
-
- any_of: {"any_of": ["^pattern1.*", "^pattern2.*"]}
|
|
665
|
-
- all_of: {"all_of": ["^pattern1.*", "^pattern2.*"]}
|
|
666
|
-
- Multiple groups: [{"all_of": [...]}, {"any_of": [...]}, "^pattern.*"]
|
|
483
|
+
return False
|
|
667
484
|
|
|
668
|
-
|
|
669
|
-
|
|
485
|
+
def _get_allowed_wildcards_for_resources(self, config: CheckConfig) -> frozenset[str]:
|
|
486
|
+
"""Get allowed_wildcards for resource check configuration.
|
|
670
487
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
"""
|
|
674
|
-
if not config:
|
|
675
|
-
return False, []
|
|
676
|
-
|
|
677
|
-
# Handle simple list with potential mixed items
|
|
678
|
-
if isinstance(config, list):
|
|
679
|
-
# Use set for O(1) membership checks instead of list
|
|
680
|
-
all_matched = set()
|
|
681
|
-
|
|
682
|
-
for item in config:
|
|
683
|
-
# Each item can be a string pattern, or a dict with any_of/all_of
|
|
684
|
-
if isinstance(item, str):
|
|
685
|
-
# Simple string pattern - check if any action matches
|
|
686
|
-
# Use cached compiled pattern
|
|
687
|
-
compiled = _compile_pattern(item)
|
|
688
|
-
if compiled:
|
|
689
|
-
for action in actions:
|
|
690
|
-
if compiled.match(action):
|
|
691
|
-
all_matched.add(action)
|
|
692
|
-
elif isinstance(item, dict):
|
|
693
|
-
# Recurse for dict items
|
|
694
|
-
matches, matched = self._check_patterns_config(actions, item)
|
|
695
|
-
if matches:
|
|
696
|
-
all_matched.update(matched)
|
|
697
|
-
|
|
698
|
-
return len(all_matched) > 0, list(all_matched)
|
|
699
|
-
|
|
700
|
-
# Handle dict with any_of/all_of
|
|
701
|
-
if isinstance(config, dict):
|
|
702
|
-
# any_of: at least one action must match at least one pattern
|
|
703
|
-
if "any_of" in config:
|
|
704
|
-
matched = set()
|
|
705
|
-
# Pre-compile all patterns
|
|
706
|
-
compiled_patterns = [_compile_pattern(p) for p in config["any_of"]]
|
|
707
|
-
|
|
708
|
-
for action in actions:
|
|
709
|
-
for compiled in compiled_patterns:
|
|
710
|
-
if compiled and compiled.match(action):
|
|
711
|
-
matched.add(action)
|
|
712
|
-
break
|
|
713
|
-
return len(matched) > 0, list(matched)
|
|
714
|
-
|
|
715
|
-
# all_of: at least one action must match ALL patterns
|
|
716
|
-
if "all_of" in config:
|
|
717
|
-
# Pre-compile all patterns
|
|
718
|
-
compiled_patterns = [_compile_pattern(p) for p in config["all_of"]]
|
|
719
|
-
# Filter out invalid patterns
|
|
720
|
-
compiled_patterns = [p for p in compiled_patterns if p]
|
|
721
|
-
|
|
722
|
-
if not compiled_patterns:
|
|
723
|
-
return False, []
|
|
724
|
-
|
|
725
|
-
matched = set()
|
|
726
|
-
for action in actions:
|
|
727
|
-
# Check if this action matches ALL patterns
|
|
728
|
-
if all(compiled.match(action) for compiled in compiled_patterns):
|
|
729
|
-
matched.add(action)
|
|
730
|
-
|
|
731
|
-
return len(matched) > 0, list(matched)
|
|
732
|
-
|
|
733
|
-
return False, []
|
|
734
|
-
|
|
735
|
-
def _matches_sensitive_pattern(self, action: str, config: CheckConfig) -> bool:
|
|
736
|
-
"""
|
|
737
|
-
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.
|
|
738
490
|
|
|
739
|
-
|
|
491
|
+
Args:
|
|
492
|
+
config: The check configuration
|
|
740
493
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
- "^iam:.*" # All IAM actions
|
|
744
|
-
- ".*:Delete.*" # Any delete action
|
|
745
|
-
- "s3:PutBucket.*" # S3 bucket modification actions
|
|
494
|
+
Returns:
|
|
495
|
+
A frozenset of allowed wildcard patterns
|
|
746
496
|
"""
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
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()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for IAM policy checks."""
|