iam-policy-validator 1.7.2__py3-none-any.whl → 1.8.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.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -6
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +61 -23
- iam_validator/checks/action_resource_matching.py +6 -2
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +1 -1
- iam_validator/checks/condition_type_mismatch.py +6 -6
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +65 -133
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +7 -3
- iam_validator/checks/service_wildcard.py +2 -2
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +3 -1
- iam_validator/checks/wildcard_resource.py +3 -1
- iam_validator/commands/validate.py +6 -12
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +45 -43
- iam_validator/core/check_registry.py +83 -79
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/defaults.py +58 -52
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +31 -472
- iam_validator/core/policy_loader.py +27 -4
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -73,7 +73,7 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
73
73
|
|
|
74
74
|
# First pass: Identify set operators and Null checks
|
|
75
75
|
for operator, conditions in statement.condition.items():
|
|
76
|
-
base_operator,
|
|
76
|
+
base_operator, _operator_type, set_prefix = normalize_operator(operator)
|
|
77
77
|
|
|
78
78
|
# Track Null checks
|
|
79
79
|
if base_operator == "Null":
|
|
@@ -87,22 +87,22 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
87
87
|
|
|
88
88
|
# Second pass: Validate set operator usage
|
|
89
89
|
for operator, conditions in statement.condition.items():
|
|
90
|
-
base_operator,
|
|
90
|
+
base_operator, _operator_type, set_prefix = normalize_operator(operator)
|
|
91
91
|
|
|
92
92
|
if not set_prefix:
|
|
93
93
|
continue
|
|
94
94
|
|
|
95
95
|
# Check each condition key used with a set operator
|
|
96
|
-
for condition_key,
|
|
96
|
+
for condition_key, _condition_values in conditions.items():
|
|
97
97
|
# Issue 1: Set operator used with single-valued context key (anti-pattern)
|
|
98
98
|
if not is_multivalued_context_key(condition_key):
|
|
99
99
|
issues.append(
|
|
100
100
|
ValidationIssue(
|
|
101
101
|
severity=self.get_severity(config),
|
|
102
102
|
message=(
|
|
103
|
-
f"Set operator
|
|
104
|
-
f"condition key
|
|
105
|
-
f"Set operators are designed for multivalued context keys like
|
|
103
|
+
f"Set operator `{set_prefix}` should not be used with single-valued "
|
|
104
|
+
f"condition key `{condition_key}`. This can lead to overly permissive policies. "
|
|
105
|
+
f"Set operators are designed for multivalued context keys like `aws:TagKeys`. "
|
|
106
106
|
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
107
107
|
),
|
|
108
108
|
statement_sid=statement_sid,
|
|
@@ -120,9 +120,9 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
120
120
|
ValidationIssue(
|
|
121
121
|
severity="warning",
|
|
122
122
|
message=(
|
|
123
|
-
f"Security risk: ForAllValues with Allow effect on
|
|
123
|
+
f"Security risk: ForAllValues with Allow effect on `{condition_key}` "
|
|
124
124
|
f"should include a Null condition check. Without it, requests with missing "
|
|
125
|
-
f'
|
|
125
|
+
f'`{condition_key}` will be granted access. Add: `"Null": {{"{condition_key}": "false"}}`. '
|
|
126
126
|
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
127
127
|
),
|
|
128
128
|
statement_sid=statement_sid,
|
|
@@ -140,10 +140,10 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
140
140
|
ValidationIssue(
|
|
141
141
|
severity="warning",
|
|
142
142
|
message=(
|
|
143
|
-
f"Unpredictable behavior: ForAnyValue with Deny effect on
|
|
143
|
+
f"Unpredictable behavior: `ForAnyValue` with `Deny` effect on `{condition_key}` "
|
|
144
144
|
f"should include a Null condition check. Without it, requests with missing "
|
|
145
|
-
f"
|
|
146
|
-
f'Add:
|
|
145
|
+
f"`{condition_key}` will evaluate to `No match` instead of denying access. "
|
|
146
|
+
f'Add: `"Null": {{"{condition_key}": "false"}}`. '
|
|
147
147
|
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
148
148
|
),
|
|
149
149
|
statement_sid=statement_sid,
|
|
@@ -35,6 +35,10 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
|
|
|
35
35
|
# No spaces allowed
|
|
36
36
|
sid_pattern = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
37
37
|
|
|
38
|
+
# Handle policies with no statements
|
|
39
|
+
if not policy.statement:
|
|
40
|
+
return []
|
|
41
|
+
|
|
38
42
|
# Collect all SIDs (ignoring None/empty values) and check format
|
|
39
43
|
sids_with_indices: list[tuple[str, int]] = []
|
|
40
44
|
for idx, statement in enumerate(policy.statement):
|
|
@@ -43,15 +47,15 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
|
|
|
43
47
|
if not sid_pattern.match(statement.sid):
|
|
44
48
|
# Identify the issue
|
|
45
49
|
if " " in statement.sid:
|
|
46
|
-
issue_msg = f"Statement ID
|
|
50
|
+
issue_msg = f"Statement ID `{statement.sid}` contains spaces, which are not allowed by AWS"
|
|
47
51
|
suggestion = (
|
|
48
|
-
f"Remove spaces from the SID. Example:
|
|
52
|
+
f"Remove spaces from the SID. Example: `{statement.sid.replace(' ', '')}`"
|
|
49
53
|
)
|
|
50
54
|
else:
|
|
51
55
|
invalid_chars = "".join(
|
|
52
56
|
set(c for c in statement.sid if not c.isalnum() and c not in "_-")
|
|
53
57
|
)
|
|
54
|
-
issue_msg = f"Statement ID
|
|
58
|
+
issue_msg = f"Statement ID `{statement.sid}` contains invalid characters: `{invalid_chars}`"
|
|
55
59
|
suggestion = (
|
|
56
60
|
"SIDs must contain only alphanumeric characters, hyphens, and underscores"
|
|
57
61
|
)
|
|
@@ -91,7 +95,7 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
|
|
|
91
95
|
statement_sid=duplicate_sid,
|
|
92
96
|
statement_index=idx,
|
|
93
97
|
issue_type="duplicate_sid",
|
|
94
|
-
message=f"Statement ID `{duplicate_sid}` is used **{count} times** in this policy (found in statements {statement_numbers})",
|
|
98
|
+
message=f"Statement ID `{duplicate_sid}` is used **{count} times** in this policy (found in statements `{statement_numbers}`)",
|
|
95
99
|
suggestion="Change this SID to a unique value. Statement IDs help identify and reference specific statements, so duplicates can cause confusion.",
|
|
96
100
|
line_number=statement.line_number,
|
|
97
101
|
)
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Trust Policy Validation Check.
|
|
2
|
+
|
|
3
|
+
Validates trust policies (role assumption policies) for security best practices.
|
|
4
|
+
This check ensures that assume role actions have appropriate principals and conditions.
|
|
5
|
+
|
|
6
|
+
Trust policies are resource-based policies attached to IAM roles that control
|
|
7
|
+
who can assume the role and under what conditions.
|
|
8
|
+
|
|
9
|
+
Key Validations:
|
|
10
|
+
1. Action-Principal Type Matching
|
|
11
|
+
- sts:AssumeRole → AWS or Service principals
|
|
12
|
+
- sts:AssumeRoleWithSAML → Federated (SAML provider) principals
|
|
13
|
+
- sts:AssumeRoleWithWebIdentity → Federated (OIDC provider) principals
|
|
14
|
+
|
|
15
|
+
2. Provider ARN Validation
|
|
16
|
+
- SAML providers must match: arn:aws:iam::account:saml-provider/name
|
|
17
|
+
- OIDC providers must match: arn:aws:iam::account:oidc-provider/domain
|
|
18
|
+
|
|
19
|
+
3. Required Conditions
|
|
20
|
+
- SAML: Requires SAML:aud condition
|
|
21
|
+
- OIDC: Requires provider-specific audience/subject conditions
|
|
22
|
+
- Cross-account: Should have ExternalId or PrincipalOrgID
|
|
23
|
+
|
|
24
|
+
Complements existing checks:
|
|
25
|
+
- principal_validation: Validates which principals are allowed/blocked
|
|
26
|
+
- action_condition_enforcement: Validates required conditions for actions
|
|
27
|
+
- trust_policy_validation: Validates action-principal coupling and trust-specific rules
|
|
28
|
+
|
|
29
|
+
This check is DISABLED by default. Enable it for trust policy validation:
|
|
30
|
+
|
|
31
|
+
trust_policy_validation:
|
|
32
|
+
enabled: true
|
|
33
|
+
severity: high
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
from typing import TYPE_CHECKING, Any
|
|
38
|
+
|
|
39
|
+
from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
40
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
41
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TrustPolicyValidationCheck(PolicyCheck):
|
|
48
|
+
"""Validates trust policies for role assumption security."""
|
|
49
|
+
|
|
50
|
+
# Default validation rules for assume actions
|
|
51
|
+
DEFAULT_RULES = {
|
|
52
|
+
"sts:AssumeRole": {
|
|
53
|
+
"allowed_principal_types": ["AWS", "Service"],
|
|
54
|
+
"description": "Standard role assumption",
|
|
55
|
+
},
|
|
56
|
+
"sts:AssumeRoleWithSAML": {
|
|
57
|
+
"allowed_principal_types": ["Federated"],
|
|
58
|
+
"provider_pattern": r"^arn:aws:iam::\d{12}:saml-provider/[\w+=,.@-]+$",
|
|
59
|
+
"required_conditions": ["SAML:aud"],
|
|
60
|
+
"description": "SAML-based federated role assumption",
|
|
61
|
+
},
|
|
62
|
+
"sts:AssumeRoleWithWebIdentity": {
|
|
63
|
+
"allowed_principal_types": ["Federated"],
|
|
64
|
+
"provider_pattern": r"^arn:aws:iam::\d{12}:oidc-provider/[\w./-]+$",
|
|
65
|
+
"required_conditions": ["*:aud"], # Require audience condition (provider-specific key)
|
|
66
|
+
"description": "OIDC-based federated role assumption",
|
|
67
|
+
},
|
|
68
|
+
"sts:TagSession": {
|
|
69
|
+
"allowed_principal_types": ["AWS", "Service", "Federated"],
|
|
70
|
+
"description": "Session tagging during role assumption (can be combined with any assume action)",
|
|
71
|
+
},
|
|
72
|
+
"sts:SetSourceIdentity": {
|
|
73
|
+
"allowed_principal_types": ["AWS", "Service", "Federated"],
|
|
74
|
+
"description": "Set source identity during role assumption (tracks original identity through role chains)",
|
|
75
|
+
},
|
|
76
|
+
"sts:SetContext": {
|
|
77
|
+
"allowed_principal_types": ["AWS", "Service", "Federated"],
|
|
78
|
+
"description": "Set session context during role assumption",
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def check_id(self) -> str:
|
|
84
|
+
return "trust_policy_validation"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def description(self) -> str:
|
|
88
|
+
return "Validates trust policies for role assumption security and action-principal coupling"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def default_severity(self) -> str:
|
|
92
|
+
return "high"
|
|
93
|
+
|
|
94
|
+
async def execute(
|
|
95
|
+
self,
|
|
96
|
+
statement: Statement,
|
|
97
|
+
statement_idx: int,
|
|
98
|
+
fetcher: AWSServiceFetcher,
|
|
99
|
+
config: CheckConfig,
|
|
100
|
+
) -> list[ValidationIssue]:
|
|
101
|
+
"""Execute trust policy validation on a single statement.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
statement: The statement to validate
|
|
105
|
+
statement_idx: Index of the statement in the policy
|
|
106
|
+
fetcher: AWS service fetcher instance
|
|
107
|
+
config: Configuration for this check
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of validation issues
|
|
111
|
+
"""
|
|
112
|
+
issues = []
|
|
113
|
+
|
|
114
|
+
# Skip if no principal (trust policies must have principals)
|
|
115
|
+
if statement.principal is None and statement.not_principal is None:
|
|
116
|
+
return issues
|
|
117
|
+
|
|
118
|
+
# Get actions from statement
|
|
119
|
+
actions = self._get_actions(statement)
|
|
120
|
+
if not actions:
|
|
121
|
+
return issues
|
|
122
|
+
|
|
123
|
+
# Get validation rules (use custom rules if provided, otherwise defaults)
|
|
124
|
+
validation_rules = config.config.get("validation_rules", self.DEFAULT_RULES)
|
|
125
|
+
|
|
126
|
+
# Check each assume action
|
|
127
|
+
for action in actions:
|
|
128
|
+
# Skip wildcard actions (too broad to validate specifically)
|
|
129
|
+
if action == "*" or action == "sts:*":
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Find matching rule (exact matches for assume actions)
|
|
133
|
+
rule = self._find_matching_rule(action, validation_rules)
|
|
134
|
+
if not rule:
|
|
135
|
+
continue # Not an assume action we validate
|
|
136
|
+
|
|
137
|
+
# Validate principal type for this action
|
|
138
|
+
principal_issues = self._validate_principal_type(
|
|
139
|
+
statement, action, rule, statement_idx, config
|
|
140
|
+
)
|
|
141
|
+
issues.extend(principal_issues)
|
|
142
|
+
|
|
143
|
+
# Validate provider ARN format if required
|
|
144
|
+
if "provider_pattern" in rule:
|
|
145
|
+
provider_issues = self._validate_provider_format(
|
|
146
|
+
statement, action, rule, statement_idx, config
|
|
147
|
+
)
|
|
148
|
+
issues.extend(provider_issues)
|
|
149
|
+
|
|
150
|
+
# Validate required conditions
|
|
151
|
+
if "required_conditions" in rule:
|
|
152
|
+
condition_issues = self._validate_required_conditions(
|
|
153
|
+
statement, action, rule, statement_idx, config
|
|
154
|
+
)
|
|
155
|
+
issues.extend(condition_issues)
|
|
156
|
+
|
|
157
|
+
return issues
|
|
158
|
+
|
|
159
|
+
def _get_actions(self, statement: Statement) -> list[str]:
|
|
160
|
+
"""Extract actions from statement.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
statement: IAM policy statement
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of action strings
|
|
167
|
+
"""
|
|
168
|
+
if statement.action is None:
|
|
169
|
+
return []
|
|
170
|
+
return [statement.action] if isinstance(statement.action, str) else statement.action
|
|
171
|
+
|
|
172
|
+
def _find_matching_rule(self, action: str, rules: dict[str, Any]) -> dict[str, Any] | None:
|
|
173
|
+
"""Find validation rule matching the action.
|
|
174
|
+
|
|
175
|
+
Supports wildcards in action names.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
action: Action to find rule for (e.g., "sts:AssumeRole")
|
|
179
|
+
rules: Validation rules dict
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Matching rule dict or None
|
|
183
|
+
"""
|
|
184
|
+
# Exact match first (performance optimization)
|
|
185
|
+
if action in rules:
|
|
186
|
+
return rules[action]
|
|
187
|
+
|
|
188
|
+
# Check for wildcard patterns in action
|
|
189
|
+
for rule_action, rule_config in rules.items():
|
|
190
|
+
# Support wildcards in the action being validated
|
|
191
|
+
if "*" in action:
|
|
192
|
+
pattern = action.replace("*", ".*")
|
|
193
|
+
if re.match(f"^{pattern}$", rule_action):
|
|
194
|
+
return rule_config
|
|
195
|
+
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def _extract_principal_types(self, statement: Statement) -> dict[str, list[str]]:
|
|
199
|
+
"""Extract principals grouped by type (AWS, Service, Federated, etc.).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
statement: IAM policy statement
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Dict mapping principal type to list of principal values
|
|
206
|
+
"""
|
|
207
|
+
principal_types: dict[str, list[str]] = {}
|
|
208
|
+
|
|
209
|
+
if statement.principal:
|
|
210
|
+
if isinstance(statement.principal, str):
|
|
211
|
+
# Simple string principal like "*"
|
|
212
|
+
principal_types["AWS"] = [statement.principal]
|
|
213
|
+
elif isinstance(statement.principal, dict):
|
|
214
|
+
for key, value in statement.principal.items():
|
|
215
|
+
if isinstance(value, str):
|
|
216
|
+
principal_types[key] = [value]
|
|
217
|
+
elif isinstance(value, list):
|
|
218
|
+
principal_types[key] = value
|
|
219
|
+
|
|
220
|
+
return principal_types
|
|
221
|
+
|
|
222
|
+
def _validate_principal_type(
|
|
223
|
+
self,
|
|
224
|
+
statement: Statement,
|
|
225
|
+
action: str,
|
|
226
|
+
rule: dict[str, Any],
|
|
227
|
+
statement_idx: int,
|
|
228
|
+
config: CheckConfig,
|
|
229
|
+
) -> list[ValidationIssue]:
|
|
230
|
+
"""Validate that principal type matches the assume action.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
statement: IAM policy statement
|
|
234
|
+
action: Assume action being validated
|
|
235
|
+
rule: Validation rule for this action
|
|
236
|
+
statement_idx: Statement index
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
List of validation issues
|
|
240
|
+
"""
|
|
241
|
+
issues = []
|
|
242
|
+
|
|
243
|
+
allowed_types = rule.get("allowed_principal_types", [])
|
|
244
|
+
if not allowed_types:
|
|
245
|
+
return issues
|
|
246
|
+
|
|
247
|
+
principal_types = self._extract_principal_types(statement)
|
|
248
|
+
|
|
249
|
+
# Check if any principal type is not allowed
|
|
250
|
+
for principal_type, principals in principal_types.items():
|
|
251
|
+
if principal_type not in allowed_types:
|
|
252
|
+
principals_list = ", ".join(f"`{p}`" for p in principals)
|
|
253
|
+
allowed_list = ", ".join(f"`{t}`" for t in allowed_types)
|
|
254
|
+
|
|
255
|
+
issues.append(
|
|
256
|
+
ValidationIssue(
|
|
257
|
+
severity=self.get_severity(config),
|
|
258
|
+
issue_type="invalid_principal_type_for_assume_action",
|
|
259
|
+
message=f"Action `{action}` should not use Principal type `{principal_type}`. "
|
|
260
|
+
f"Expected principal types: {allowed_list}",
|
|
261
|
+
statement_index=statement_idx,
|
|
262
|
+
statement_sid=statement.sid,
|
|
263
|
+
line_number=statement.line_number,
|
|
264
|
+
action=action,
|
|
265
|
+
suggestion=f"For `{action}`, use {allowed_list} principal type instead of `{principal_type}`. "
|
|
266
|
+
f"\n\nFound principals: `{principals_list}`\n\n"
|
|
267
|
+
f"{rule.get('description', '')}",
|
|
268
|
+
example=self._get_example_for_action(
|
|
269
|
+
action, allowed_types[0] if allowed_types else "AWS"
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return issues
|
|
275
|
+
|
|
276
|
+
def _validate_provider_format(
|
|
277
|
+
self,
|
|
278
|
+
statement: Statement,
|
|
279
|
+
action: str,
|
|
280
|
+
rule: dict[str, Any],
|
|
281
|
+
statement_idx: int,
|
|
282
|
+
config: CheckConfig,
|
|
283
|
+
) -> list[ValidationIssue]:
|
|
284
|
+
"""Validate that federated provider ARN matches expected format.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
statement: IAM policy statement
|
|
288
|
+
action: Assume action being validated
|
|
289
|
+
rule: Validation rule for this action
|
|
290
|
+
statement_idx: Statement index
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of validation issues
|
|
294
|
+
"""
|
|
295
|
+
issues = []
|
|
296
|
+
|
|
297
|
+
provider_pattern = rule.get("provider_pattern")
|
|
298
|
+
if not provider_pattern:
|
|
299
|
+
return issues
|
|
300
|
+
|
|
301
|
+
principal_types = self._extract_principal_types(statement)
|
|
302
|
+
federated_principals = principal_types.get("Federated", [])
|
|
303
|
+
|
|
304
|
+
for principal in federated_principals:
|
|
305
|
+
if not re.match(provider_pattern, principal):
|
|
306
|
+
provider_type = "SAML" if "saml-provider" in provider_pattern else "OIDC"
|
|
307
|
+
|
|
308
|
+
issues.append(
|
|
309
|
+
ValidationIssue(
|
|
310
|
+
severity=self.get_severity(config),
|
|
311
|
+
issue_type="invalid_provider_format",
|
|
312
|
+
message=f"Federated principal `{principal}` does not match expected `{provider_type}` provider format for `{action}`",
|
|
313
|
+
statement_index=statement_idx,
|
|
314
|
+
statement_sid=statement.sid,
|
|
315
|
+
line_number=statement.line_number,
|
|
316
|
+
action=action,
|
|
317
|
+
suggestion=f"For `{action}`, use a valid `{provider_type}` provider ARN.\n\n"
|
|
318
|
+
f"Expected pattern: `{provider_pattern}`\n"
|
|
319
|
+
f"Found: `{principal}`",
|
|
320
|
+
example=self._get_provider_example(provider_type),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return issues
|
|
325
|
+
|
|
326
|
+
def _validate_required_conditions(
|
|
327
|
+
self,
|
|
328
|
+
statement: Statement,
|
|
329
|
+
action: str,
|
|
330
|
+
rule: dict[str, Any],
|
|
331
|
+
statement_idx: int,
|
|
332
|
+
config: CheckConfig,
|
|
333
|
+
) -> list[ValidationIssue]:
|
|
334
|
+
"""Validate that required conditions are present.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
statement: IAM policy statement
|
|
338
|
+
action: Assume action being validated
|
|
339
|
+
rule: Validation rule for this action
|
|
340
|
+
statement_idx: Statement index
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
List of validation issues
|
|
344
|
+
"""
|
|
345
|
+
issues = []
|
|
346
|
+
|
|
347
|
+
required_conditions = rule.get("required_conditions", [])
|
|
348
|
+
if not required_conditions:
|
|
349
|
+
return issues
|
|
350
|
+
|
|
351
|
+
# Get all condition keys from statement
|
|
352
|
+
condition_keys = set()
|
|
353
|
+
if statement.condition:
|
|
354
|
+
for _operator, keys_dict in statement.condition.items():
|
|
355
|
+
if isinstance(keys_dict, dict):
|
|
356
|
+
condition_keys.update(keys_dict.keys())
|
|
357
|
+
|
|
358
|
+
# Check for missing required conditions (supports wildcards like *:aud)
|
|
359
|
+
missing_conditions = []
|
|
360
|
+
for required_cond in required_conditions:
|
|
361
|
+
if "*:" in required_cond:
|
|
362
|
+
# Wildcard pattern - check if any key ends with the suffix
|
|
363
|
+
suffix = required_cond.split("*:")[1]
|
|
364
|
+
if not any(key.endswith(f":{suffix}") for key in condition_keys):
|
|
365
|
+
missing_conditions.append(required_cond)
|
|
366
|
+
else:
|
|
367
|
+
# Exact match
|
|
368
|
+
if required_cond not in condition_keys:
|
|
369
|
+
missing_conditions.append(required_cond)
|
|
370
|
+
|
|
371
|
+
if missing_conditions:
|
|
372
|
+
missing_list = ", ".join(f"`{c}`" for c in missing_conditions)
|
|
373
|
+
|
|
374
|
+
issues.append(
|
|
375
|
+
ValidationIssue(
|
|
376
|
+
severity=self.get_severity(config),
|
|
377
|
+
issue_type="missing_required_condition_for_assume_action",
|
|
378
|
+
message=f"Action `{action}` is missing required conditions: `{missing_list}`",
|
|
379
|
+
statement_index=statement_idx,
|
|
380
|
+
statement_sid=statement.sid,
|
|
381
|
+
line_number=statement.line_number,
|
|
382
|
+
action=action,
|
|
383
|
+
suggestion=f"Add required condition(s) to restrict when `{action}` can be performed. "
|
|
384
|
+
f"Missing: `{missing_list}`\n\n"
|
|
385
|
+
f"{rule.get('description', '')}",
|
|
386
|
+
example=self._get_condition_example(action, required_conditions[0]),
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return issues
|
|
391
|
+
|
|
392
|
+
def _get_example_for_action(self, action: str, principal_type: str) -> str:
|
|
393
|
+
"""Generate example JSON for an assume action.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
action: Assume action
|
|
397
|
+
principal_type: Expected principal type
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
JSON example string
|
|
401
|
+
"""
|
|
402
|
+
examples = {
|
|
403
|
+
("sts:AssumeRole", "AWS"): """{
|
|
404
|
+
"Effect": "Allow",
|
|
405
|
+
"Principal": {
|
|
406
|
+
"AWS": "arn:aws:iam::123456789012:root"
|
|
407
|
+
},
|
|
408
|
+
"Action": "sts:AssumeRole",
|
|
409
|
+
"Condition": {
|
|
410
|
+
"StringEquals": {
|
|
411
|
+
"sts:ExternalId": "unique-external-id"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}""",
|
|
415
|
+
("sts:AssumeRole", "Service"): """{
|
|
416
|
+
"Effect": "Allow",
|
|
417
|
+
"Principal": {
|
|
418
|
+
"Service": "lambda.amazonaws.com"
|
|
419
|
+
},
|
|
420
|
+
"Action": "sts:AssumeRole"
|
|
421
|
+
}""",
|
|
422
|
+
("sts:AssumeRoleWithSAML", "Federated"): """{
|
|
423
|
+
"Effect": "Allow",
|
|
424
|
+
"Principal": {
|
|
425
|
+
"Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
|
|
426
|
+
},
|
|
427
|
+
"Action": "sts:AssumeRoleWithSAML",
|
|
428
|
+
"Condition": {
|
|
429
|
+
"StringEquals": {
|
|
430
|
+
"SAML:aud": "https://signin.aws.amazon.com/saml"
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}""",
|
|
434
|
+
("sts:AssumeRoleWithWebIdentity", "Federated"): """{
|
|
435
|
+
"Effect": "Allow",
|
|
436
|
+
"Principal": {
|
|
437
|
+
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
|
|
438
|
+
},
|
|
439
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
440
|
+
"Condition": {
|
|
441
|
+
"StringEquals": {
|
|
442
|
+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
|
|
443
|
+
},
|
|
444
|
+
"StringLike": {
|
|
445
|
+
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}""",
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return examples.get((action, principal_type), "")
|
|
452
|
+
|
|
453
|
+
def _get_provider_example(self, provider_type: str) -> str:
|
|
454
|
+
"""Get example provider ARN.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
provider_type: Type of provider (SAML or OIDC)
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Example ARN string
|
|
461
|
+
"""
|
|
462
|
+
if provider_type == "SAML":
|
|
463
|
+
return """{
|
|
464
|
+
"Principal": {
|
|
465
|
+
"Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
|
|
466
|
+
}
|
|
467
|
+
}"""
|
|
468
|
+
else: # OIDC
|
|
469
|
+
return """{
|
|
470
|
+
"Principal": {
|
|
471
|
+
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
|
|
472
|
+
}
|
|
473
|
+
}"""
|
|
474
|
+
|
|
475
|
+
def _get_condition_example(self, action: str, condition_key: str) -> str:
|
|
476
|
+
"""Get example condition for an action.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
action: Assume action
|
|
480
|
+
condition_key: Required condition key
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
JSON example string
|
|
484
|
+
"""
|
|
485
|
+
examples = {
|
|
486
|
+
"SAML:aud": """{
|
|
487
|
+
"Condition": {
|
|
488
|
+
"StringEquals": {
|
|
489
|
+
"SAML:aud": "https://signin.aws.amazon.com/saml"
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}""",
|
|
493
|
+
"sts:ExternalId": """{
|
|
494
|
+
"Condition": {
|
|
495
|
+
"StringEquals": {
|
|
496
|
+
"sts:ExternalId": "unique-external-id-shared-with-trusted-party"
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}""",
|
|
500
|
+
"aws:PrincipalOrgID": """{
|
|
501
|
+
"Condition": {
|
|
502
|
+
"StringEquals": {
|
|
503
|
+
"aws:PrincipalOrgID": "o-123456789"
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}""",
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return examples.get(
|
|
510
|
+
condition_key,
|
|
511
|
+
f'{{\n "Condition": {{\n "StringEquals": {{\n "{condition_key}": "value"\n }}\n }}\n}}',
|
|
512
|
+
)
|