iam-policy-validator 1.3.1__py3-none-any.whl → 1.5.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.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +164 -19
- iam_policy_validator-1.5.0.dist-info/RECORD +67 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +15 -3
- iam_validator/checks/action_condition_enforcement.py +1 -6
- iam_validator/checks/condition_key_validation.py +21 -1
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/policy_size.py +1 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/sensitive_action.py +178 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/sid_uniqueness.py +45 -7
- iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/post_to_pr.py +7 -0
- iam_validator/commands/validate.py +204 -16
- iam_validator/core/aws_fetcher.py +25 -12
- iam_validator/core/check_registry.py +25 -21
- iam_validator/core/config/__init__.py +83 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/condition_requirements.py +535 -0
- iam_validator/core/config/defaults.py +390 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +133 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/config_loader.py +29 -9
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- iam_validator/core/models.py +13 -3
- iam_validator/core/policy_checks.py +39 -6
- iam_validator/core/pr_commenter.py +30 -9
- iam_policy_validator-1.3.1.dist-info/RECORD +0 -54
- iam_validator/checks/security_best_practices.py +0 -535
- iam_validator/core/defaults.py +0 -366
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
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.models import Statement, ValidationIssue
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from iam_validator.core.models import IAMPolicy
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SensitiveActionCheck(PolicyCheck):
|
|
20
|
+
"""Checks for sensitive actions without IAM conditions to limit their use."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def check_id(self) -> str:
|
|
24
|
+
return "sensitive_action"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return "Checks for sensitive actions without conditions"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def default_severity(self) -> str:
|
|
32
|
+
return "medium"
|
|
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 sensitive action check on a statement."""
|
|
42
|
+
issues = []
|
|
43
|
+
|
|
44
|
+
# Only check Allow statements
|
|
45
|
+
if statement.effect != "Allow":
|
|
46
|
+
return issues
|
|
47
|
+
|
|
48
|
+
actions = statement.get_actions()
|
|
49
|
+
has_conditions = statement.condition is not None and len(statement.condition) > 0
|
|
50
|
+
|
|
51
|
+
# Expand wildcards to actual actions using AWS API
|
|
52
|
+
expanded_actions = await expand_wildcard_actions(actions, fetcher)
|
|
53
|
+
|
|
54
|
+
# Check if sensitive actions match using any_of/all_of logic
|
|
55
|
+
is_sensitive, matched_actions = check_sensitive_actions(
|
|
56
|
+
expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if is_sensitive and not has_conditions:
|
|
60
|
+
# Create appropriate message based on matched actions using configurable templates
|
|
61
|
+
if len(matched_actions) == 1:
|
|
62
|
+
message_template = config.config.get(
|
|
63
|
+
"message_single",
|
|
64
|
+
"Sensitive action '{action}' should have conditions to limit when it can be used",
|
|
65
|
+
)
|
|
66
|
+
message = message_template.format(action=matched_actions[0])
|
|
67
|
+
else:
|
|
68
|
+
action_list = "', '".join(matched_actions)
|
|
69
|
+
message_template = config.config.get(
|
|
70
|
+
"message_multiple",
|
|
71
|
+
"Sensitive actions '{actions}' should have conditions to limit when they can be used",
|
|
72
|
+
)
|
|
73
|
+
message = message_template.format(actions=action_list)
|
|
74
|
+
|
|
75
|
+
suggestion_text = config.config.get(
|
|
76
|
+
"suggestion",
|
|
77
|
+
"Add IAM conditions to limit when this action can be used. Consider: ABAC (ResourceTag OR RequestTag matching ${aws:PrincipalTag}), IP restrictions (aws:SourceIp), MFA requirements (aws:MultiFactorAuthPresent), or time-based restrictions (aws:CurrentTime)",
|
|
78
|
+
)
|
|
79
|
+
example = config.config.get("example", "")
|
|
80
|
+
|
|
81
|
+
# Combine suggestion + example
|
|
82
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
83
|
+
|
|
84
|
+
issues.append(
|
|
85
|
+
ValidationIssue(
|
|
86
|
+
severity=self.get_severity(config),
|
|
87
|
+
statement_sid=statement.sid,
|
|
88
|
+
statement_index=statement_idx,
|
|
89
|
+
issue_type="missing_condition",
|
|
90
|
+
message=message,
|
|
91
|
+
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
92
|
+
suggestion=suggestion,
|
|
93
|
+
line_number=statement.line_number,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return issues
|
|
98
|
+
|
|
99
|
+
async def execute_policy(
|
|
100
|
+
self,
|
|
101
|
+
policy: "IAMPolicy",
|
|
102
|
+
policy_file: str,
|
|
103
|
+
fetcher: AWSServiceFetcher,
|
|
104
|
+
config: CheckConfig,
|
|
105
|
+
**kwargs,
|
|
106
|
+
) -> list[ValidationIssue]:
|
|
107
|
+
"""
|
|
108
|
+
Execute policy-level sensitive action checks.
|
|
109
|
+
|
|
110
|
+
This method examines the entire policy to detect privilege escalation patterns
|
|
111
|
+
and other security issues that span multiple statements.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
policy: The complete IAM policy to check
|
|
115
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
116
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
117
|
+
config: Configuration for this check instance
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of ValidationIssue objects found by this check
|
|
121
|
+
"""
|
|
122
|
+
del policy_file, fetcher # Not used in current implementation
|
|
123
|
+
issues = []
|
|
124
|
+
|
|
125
|
+
# Collect all actions from all Allow statements across the entire policy
|
|
126
|
+
all_actions: set[str] = set()
|
|
127
|
+
statement_map: dict[
|
|
128
|
+
str, list[tuple[int, str | None]]
|
|
129
|
+
] = {} # action -> [(stmt_idx, sid), ...]
|
|
130
|
+
|
|
131
|
+
for idx, statement in enumerate(policy.statement):
|
|
132
|
+
if statement.effect == "Allow":
|
|
133
|
+
actions = statement.get_actions()
|
|
134
|
+
# Filter out wildcards for privilege escalation detection
|
|
135
|
+
filtered_actions = [a for a in actions if a != "*"]
|
|
136
|
+
|
|
137
|
+
for action in filtered_actions:
|
|
138
|
+
all_actions.add(action)
|
|
139
|
+
if action not in statement_map:
|
|
140
|
+
statement_map[action] = []
|
|
141
|
+
statement_map[action].append((idx, statement.sid))
|
|
142
|
+
|
|
143
|
+
# Get configuration for sensitive actions
|
|
144
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
145
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
146
|
+
|
|
147
|
+
# Check for privilege escalation patterns using all_of logic
|
|
148
|
+
# We need to check both exact actions and patterns
|
|
149
|
+
policy_issues = []
|
|
150
|
+
|
|
151
|
+
# Check sensitive_actions configuration
|
|
152
|
+
if sensitive_actions_config:
|
|
153
|
+
policy_issues.extend(
|
|
154
|
+
check_policy_level_actions(
|
|
155
|
+
list(all_actions),
|
|
156
|
+
statement_map,
|
|
157
|
+
sensitive_actions_config,
|
|
158
|
+
config,
|
|
159
|
+
"actions",
|
|
160
|
+
self.get_severity,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Check sensitive_action_patterns configuration
|
|
165
|
+
if sensitive_patterns_config:
|
|
166
|
+
policy_issues.extend(
|
|
167
|
+
check_policy_level_actions(
|
|
168
|
+
list(all_actions),
|
|
169
|
+
statement_map,
|
|
170
|
+
sensitive_patterns_config,
|
|
171
|
+
config,
|
|
172
|
+
"patterns",
|
|
173
|
+
self.get_severity,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
issues.extend(policy_issues)
|
|
178
|
+
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()
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
"""Statement ID (SID) uniqueness check.
|
|
1
|
+
"""Statement ID (SID) uniqueness and format check.
|
|
2
|
+
|
|
3
|
+
This check validates that Statement IDs (Sids):
|
|
4
|
+
1. Are unique within a policy
|
|
5
|
+
2. Follow AWS naming requirements (alphanumeric, hyphens, underscores only - no spaces)
|
|
2
6
|
|
|
3
|
-
This check validates that Statement IDs (Sids) are unique within a policy.
|
|
4
7
|
According to AWS best practices, while not strictly required, having unique SIDs
|
|
5
8
|
makes it easier to reference specific statements and improves policy maintainability.
|
|
6
9
|
|
|
7
10
|
This is implemented as a policy-level check that runs once when processing the first
|
|
8
|
-
statement, examining all statements in the policy to find duplicates.
|
|
11
|
+
statement, examining all statements in the policy to find duplicates and format issues.
|
|
9
12
|
"""
|
|
10
13
|
|
|
14
|
+
import re
|
|
11
15
|
from collections import Counter
|
|
12
16
|
|
|
13
17
|
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
@@ -16,21 +20,54 @@ from iam_validator.core.models import IAMPolicy, Statement, ValidationIssue
|
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[ValidationIssue]:
|
|
19
|
-
"""Implementation of SID uniqueness checking.
|
|
23
|
+
"""Implementation of SID uniqueness and format checking.
|
|
20
24
|
|
|
21
25
|
Args:
|
|
22
26
|
policy: IAM policy to validate
|
|
23
27
|
severity: Severity level for issues found
|
|
24
28
|
|
|
25
29
|
Returns:
|
|
26
|
-
List of ValidationIssue objects for duplicate SIDs
|
|
30
|
+
List of ValidationIssue objects for duplicate or invalid SIDs
|
|
27
31
|
"""
|
|
28
32
|
issues: list[ValidationIssue] = []
|
|
29
33
|
|
|
30
|
-
#
|
|
34
|
+
# AWS SID requirements: alphanumeric characters, hyphens, and underscores only
|
|
35
|
+
# No spaces allowed
|
|
36
|
+
sid_pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
37
|
+
|
|
38
|
+
# Collect all SIDs (ignoring None/empty values) and check format
|
|
31
39
|
sids_with_indices: list[tuple[str, int]] = []
|
|
32
40
|
for idx, statement in enumerate(policy.statement):
|
|
33
41
|
if statement.sid: # Only check statements that have a SID
|
|
42
|
+
# Check SID format
|
|
43
|
+
if not sid_pattern.match(statement.sid):
|
|
44
|
+
# Identify the issue
|
|
45
|
+
if " " in statement.sid:
|
|
46
|
+
issue_msg = f"Statement ID '{statement.sid}' contains spaces, which are not allowed by AWS"
|
|
47
|
+
suggestion = (
|
|
48
|
+
f"Remove spaces from the SID. Example: '{statement.sid.replace(' ', '')}'"
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
invalid_chars = "".join(
|
|
52
|
+
set(c for c in statement.sid if not c.isalnum() and c not in "_-")
|
|
53
|
+
)
|
|
54
|
+
issue_msg = f"Statement ID '{statement.sid}' contains invalid characters: {invalid_chars}"
|
|
55
|
+
suggestion = (
|
|
56
|
+
"SIDs must contain only alphanumeric characters, hyphens, and underscores"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
issues.append(
|
|
60
|
+
ValidationIssue(
|
|
61
|
+
severity="error", # Invalid SID format is an error
|
|
62
|
+
statement_sid=statement.sid,
|
|
63
|
+
statement_index=idx,
|
|
64
|
+
issue_type="invalid_sid_format",
|
|
65
|
+
message=issue_msg,
|
|
66
|
+
suggestion=suggestion,
|
|
67
|
+
line_number=statement.line_number,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
34
71
|
sids_with_indices.append((statement.sid, idx))
|
|
35
72
|
|
|
36
73
|
# Find duplicates
|
|
@@ -76,7 +113,7 @@ class SidUniquenessCheck(PolicyCheck):
|
|
|
76
113
|
|
|
77
114
|
@property
|
|
78
115
|
def description(self) -> str:
|
|
79
|
-
return "Validates that Statement IDs (Sids) are unique
|
|
116
|
+
return "Validates that Statement IDs (Sids) are unique and follow AWS naming requirements (no spaces)"
|
|
80
117
|
|
|
81
118
|
@property
|
|
82
119
|
def default_severity(self) -> str:
|
|
@@ -113,6 +150,7 @@ class SidUniquenessCheck(PolicyCheck):
|
|
|
113
150
|
policy_file: str,
|
|
114
151
|
fetcher: AWSServiceFetcher,
|
|
115
152
|
config: CheckConfig,
|
|
153
|
+
**kwargs,
|
|
116
154
|
) -> list[ValidationIssue]:
|
|
117
155
|
"""Execute the SID uniqueness check on the entire policy.
|
|
118
156
|
|
|
@@ -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,32 @@ 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.
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"iam:PutUserPolicy",
|
|
29
|
-
"lambda:DeleteFunction",
|
|
30
|
-
"rds:DeleteDBInstance",
|
|
31
|
-
"s3:DeleteBucket",
|
|
32
|
-
"s3:DeleteBucketPolicy",
|
|
33
|
-
"s3:PutBucketPolicy",
|
|
34
|
-
}
|
|
35
|
-
)
|
|
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
|
+
# Export for backward compatibility
|
|
42
|
+
DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
# Global regex pattern cache for performance
|
|
@@ -61,15 +68,19 @@ def check_sensitive_actions(
|
|
|
61
68
|
Args:
|
|
62
69
|
actions: List of actions to check
|
|
63
70
|
config: Check configuration
|
|
64
|
-
default_actions: Default sensitive actions to use if no config (
|
|
71
|
+
default_actions: Default sensitive actions to use if no config (lazy-loaded)
|
|
65
72
|
|
|
66
73
|
Returns:
|
|
67
74
|
tuple[bool, list[str]]: (is_sensitive, matched_actions)
|
|
68
75
|
- is_sensitive: True if the actions match the sensitive criteria
|
|
69
76
|
- matched_actions: List of actions that matched the criteria
|
|
77
|
+
|
|
78
|
+
Performance:
|
|
79
|
+
- Uses lazy-loaded defaults (only loaded on first use)
|
|
80
|
+
- O(1) frozenset lookups for action matching
|
|
70
81
|
"""
|
|
71
82
|
if default_actions is None:
|
|
72
|
-
default_actions =
|
|
83
|
+
default_actions = _get_default_sensitive_actions()
|
|
73
84
|
|
|
74
85
|
# Filter out wildcards
|
|
75
86
|
filtered_actions = [a for a in actions if a != "*"]
|
|
@@ -77,12 +88,9 @@ def check_sensitive_actions(
|
|
|
77
88
|
return False, []
|
|
78
89
|
|
|
79
90
|
# 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")
|
|
91
|
+
# Config is now flat (no longer nested under sensitive_action_check)
|
|
92
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
93
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
86
94
|
|
|
87
95
|
# Check sensitive_actions (exact matches)
|
|
88
96
|
actions_match, actions_matched = check_actions_config(
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Wildcard action check - detects Action: '*' in IAM policies."""
|
|
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 WildcardActionCheck(PolicyCheck):
|
|
9
|
+
"""Checks for wildcard actions (Action: '*') which grant all permissions."""
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def check_id(self) -> str:
|
|
13
|
+
return "wildcard_action"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def description(self) -> str:
|
|
17
|
+
return "Checks for wildcard actions (*)"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def default_severity(self) -> str:
|
|
21
|
+
return "medium"
|
|
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 wildcard action 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
|
+
|
|
39
|
+
# Check for wildcard action (Action: "*")
|
|
40
|
+
if "*" in actions:
|
|
41
|
+
message = config.config.get("message", "Statement allows all actions (*)")
|
|
42
|
+
suggestion_text = config.config.get(
|
|
43
|
+
"suggestion", "Replace wildcard with specific actions needed for your use case"
|
|
44
|
+
)
|
|
45
|
+
example = config.config.get("example", "")
|
|
46
|
+
|
|
47
|
+
# Combine suggestion + example
|
|
48
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
49
|
+
|
|
50
|
+
issues.append(
|
|
51
|
+
ValidationIssue(
|
|
52
|
+
severity=self.get_severity(config),
|
|
53
|
+
statement_sid=statement.sid,
|
|
54
|
+
statement_index=statement_idx,
|
|
55
|
+
issue_type="overly_permissive",
|
|
56
|
+
message=message,
|
|
57
|
+
suggestion=suggestion,
|
|
58
|
+
line_number=statement.line_number,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return issues
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Wildcard resource check - detects Resource: '*' in IAM policies."""
|
|
2
|
+
|
|
3
|
+
from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
|
|
4
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
5
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
6
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WildcardResourceCheck(PolicyCheck):
|
|
10
|
+
"""Checks for wildcard resources (Resource: '*') which grant access to all resources."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def check_id(self) -> str:
|
|
14
|
+
return "wildcard_resource"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def description(self) -> str:
|
|
18
|
+
return "Checks for wildcard resources (*)"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def default_severity(self) -> str:
|
|
22
|
+
return "medium"
|
|
23
|
+
|
|
24
|
+
async def execute(
|
|
25
|
+
self,
|
|
26
|
+
statement: Statement,
|
|
27
|
+
statement_idx: int,
|
|
28
|
+
fetcher: AWSServiceFetcher,
|
|
29
|
+
config: CheckConfig,
|
|
30
|
+
) -> list[ValidationIssue]:
|
|
31
|
+
"""Execute wildcard resource check on a statement."""
|
|
32
|
+
issues = []
|
|
33
|
+
|
|
34
|
+
# Only check Allow statements
|
|
35
|
+
if statement.effect != "Allow":
|
|
36
|
+
return issues
|
|
37
|
+
|
|
38
|
+
actions = statement.get_actions()
|
|
39
|
+
resources = statement.get_resources()
|
|
40
|
+
|
|
41
|
+
# Check for wildcard resource (Resource: "*")
|
|
42
|
+
if "*" in resources:
|
|
43
|
+
# Check if all actions are in the allowed_wildcards list
|
|
44
|
+
# allowed_wildcards works by expanding wildcard patterns (like "ec2:Describe*")
|
|
45
|
+
# to all matching AWS actions using the AWS API, then checking if the policy's
|
|
46
|
+
# actions are in that expanded list. This ensures only validated AWS actions
|
|
47
|
+
# are allowed with Resource: "*".
|
|
48
|
+
allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
|
|
49
|
+
|
|
50
|
+
# Check if ALL actions (excluding full wildcard "*") are in the expanded list
|
|
51
|
+
non_wildcard_actions = [a for a in actions if a != "*"]
|
|
52
|
+
|
|
53
|
+
if allowed_wildcards_expanded and non_wildcard_actions:
|
|
54
|
+
# Check if all actions are in the expanded allowed list (exact match)
|
|
55
|
+
all_actions_allowed = all(
|
|
56
|
+
action in allowed_wildcards_expanded for action in non_wildcard_actions
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# If all actions are in the expanded list, skip the wildcard resource warning
|
|
60
|
+
if all_actions_allowed:
|
|
61
|
+
# All actions are safe, Resource: "*" is acceptable
|
|
62
|
+
return issues
|
|
63
|
+
|
|
64
|
+
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
65
|
+
message = config.config.get("message", "Statement applies to all resources (*)")
|
|
66
|
+
suggestion_text = config.config.get(
|
|
67
|
+
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
68
|
+
)
|
|
69
|
+
example = config.config.get("example", "")
|
|
70
|
+
|
|
71
|
+
# Combine suggestion + example
|
|
72
|
+
suggestion = f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
|
|
73
|
+
|
|
74
|
+
issues.append(
|
|
75
|
+
ValidationIssue(
|
|
76
|
+
severity=self.get_severity(config),
|
|
77
|
+
statement_sid=statement.sid,
|
|
78
|
+
statement_index=statement_idx,
|
|
79
|
+
issue_type="overly_permissive",
|
|
80
|
+
message=message,
|
|
81
|
+
suggestion=suggestion,
|
|
82
|
+
line_number=statement.line_number,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
async def _get_expanded_allowed_wildcards(
|
|
89
|
+
self, config: CheckConfig, fetcher: AWSServiceFetcher
|
|
90
|
+
) -> frozenset[str]:
|
|
91
|
+
"""Get and expand allowed_wildcards configuration.
|
|
92
|
+
|
|
93
|
+
This method retrieves wildcard patterns from the allowed_wildcards config
|
|
94
|
+
and expands them using the AWS API to get all matching actual AWS actions.
|
|
95
|
+
|
|
96
|
+
How it works:
|
|
97
|
+
1. Retrieves patterns from config (e.g., ["ec2:Describe*", "s3:List*"])
|
|
98
|
+
2. Expands each pattern using AWS API:
|
|
99
|
+
- "ec2:Describe*" → ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
100
|
+
- "s3:List*" → ["s3:ListBucket", "s3:ListObjects", ...]
|
|
101
|
+
3. Returns a set of all expanded actions
|
|
102
|
+
|
|
103
|
+
This allows you to:
|
|
104
|
+
- Specify patterns like "ec2:Describe*" in config
|
|
105
|
+
- Have the validator allow specific actions like "ec2:DescribeInstances" with Resource: "*"
|
|
106
|
+
- Ensure only real AWS actions (validated via API) are allowed
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
Config: allowed_wildcards: ["ec2:Describe*"]
|
|
110
|
+
Expands to: ["ec2:DescribeInstances", "ec2:DescribeImages", ...]
|
|
111
|
+
Policy: "Action": ["ec2:DescribeInstances"], "Resource": "*"
|
|
112
|
+
Result: ✅ Allowed (ec2:DescribeInstances is in expanded list)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
config: The check configuration
|
|
116
|
+
fetcher: AWS service fetcher for expanding wildcards via AWS API
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A frozenset of all expanded action names from the configured patterns
|
|
120
|
+
"""
|
|
121
|
+
patterns_to_expand = config.config.get("allowed_wildcards", [])
|
|
122
|
+
|
|
123
|
+
# If no patterns configured, return empty set
|
|
124
|
+
if not patterns_to_expand or not isinstance(patterns_to_expand, list):
|
|
125
|
+
return frozenset()
|
|
126
|
+
|
|
127
|
+
# Expand the wildcard patterns using the AWS API
|
|
128
|
+
# This converts patterns like "ec2:Describe*" to actual AWS actions
|
|
129
|
+
expanded_actions = await expand_wildcard_actions(patterns_to_expand, fetcher)
|
|
130
|
+
|
|
131
|
+
return frozenset(expanded_actions)
|