iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.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.9.0.dist-info}/METADATA +127 -6
- iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +559 -207
- iam_validator/checks/action_resource_matching.py +12 -15
- iam_validator/checks/action_validation.py +7 -13
- iam_validator/checks/condition_key_validation.py +7 -13
- iam_validator/checks/condition_type_mismatch.py +15 -22
- iam_validator/checks/full_wildcard.py +9 -13
- iam_validator/checks/mfa_condition_check.py +8 -17
- iam_validator/checks/policy_size.py +6 -39
- iam_validator/checks/policy_structure.py +547 -0
- iam_validator/checks/policy_type_validation.py +61 -46
- iam_validator/checks/principal_validation.py +71 -148
- iam_validator/checks/resource_validation.py +13 -20
- iam_validator/checks/sensitive_action.py +15 -18
- iam_validator/checks/service_wildcard.py +8 -14
- iam_validator/checks/set_operator_validation.py +21 -28
- iam_validator/checks/sid_uniqueness.py +16 -42
- iam_validator/checks/trust_policy_validation.py +506 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +2 -2
- iam_validator/checks/wildcard_action.py +9 -13
- iam_validator/checks/wildcard_resource.py +9 -13
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +15 -9
- iam_validator/core/__init__.py +2 -3
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +24 -1028
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +612 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +379 -0
- iam_validator/core/check_registry.py +165 -93
- 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/constants.py +17 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +38 -475
- iam_validator/core/policy_loader.py +27 -4
- iam_validator/sdk/__init__.py +1 -1
- iam_validator/sdk/context.py +1 -1
- iam_validator/sdk/helpers.py +1 -1
- iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,506 @@
|
|
|
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, ClassVar
|
|
38
|
+
|
|
39
|
+
from iam_validator.core.aws_service 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
|
+
check_id: ClassVar[str] = "trust_policy_validation"
|
|
83
|
+
description: ClassVar[str] = (
|
|
84
|
+
"Validates trust policies for role assumption security and action-principal coupling"
|
|
85
|
+
)
|
|
86
|
+
default_severity: ClassVar[str] = "high"
|
|
87
|
+
|
|
88
|
+
async def execute(
|
|
89
|
+
self,
|
|
90
|
+
statement: Statement,
|
|
91
|
+
statement_idx: int,
|
|
92
|
+
fetcher: AWSServiceFetcher,
|
|
93
|
+
config: CheckConfig,
|
|
94
|
+
) -> list[ValidationIssue]:
|
|
95
|
+
"""Execute trust policy validation on a single statement.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
statement: The statement to validate
|
|
99
|
+
statement_idx: Index of the statement in the policy
|
|
100
|
+
fetcher: AWS service fetcher instance
|
|
101
|
+
config: Configuration for this check
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of validation issues
|
|
105
|
+
"""
|
|
106
|
+
issues = []
|
|
107
|
+
|
|
108
|
+
# Skip if no principal (trust policies must have principals)
|
|
109
|
+
if statement.principal is None and statement.not_principal is None:
|
|
110
|
+
return issues
|
|
111
|
+
|
|
112
|
+
# Get actions from statement
|
|
113
|
+
actions = self._get_actions(statement)
|
|
114
|
+
if not actions:
|
|
115
|
+
return issues
|
|
116
|
+
|
|
117
|
+
# Get validation rules (use custom rules if provided, otherwise defaults)
|
|
118
|
+
validation_rules = config.config.get("validation_rules", self.DEFAULT_RULES)
|
|
119
|
+
|
|
120
|
+
# Check each assume action
|
|
121
|
+
for action in actions:
|
|
122
|
+
# Skip wildcard actions (too broad to validate specifically)
|
|
123
|
+
if action == "*" or action == "sts:*":
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Find matching rule (exact matches for assume actions)
|
|
127
|
+
rule = self._find_matching_rule(action, validation_rules)
|
|
128
|
+
if not rule:
|
|
129
|
+
continue # Not an assume action we validate
|
|
130
|
+
|
|
131
|
+
# Validate principal type for this action
|
|
132
|
+
principal_issues = self._validate_principal_type(
|
|
133
|
+
statement, action, rule, statement_idx, config
|
|
134
|
+
)
|
|
135
|
+
issues.extend(principal_issues)
|
|
136
|
+
|
|
137
|
+
# Validate provider ARN format if required
|
|
138
|
+
if "provider_pattern" in rule:
|
|
139
|
+
provider_issues = self._validate_provider_format(
|
|
140
|
+
statement, action, rule, statement_idx, config
|
|
141
|
+
)
|
|
142
|
+
issues.extend(provider_issues)
|
|
143
|
+
|
|
144
|
+
# Validate required conditions
|
|
145
|
+
if "required_conditions" in rule:
|
|
146
|
+
condition_issues = self._validate_required_conditions(
|
|
147
|
+
statement, action, rule, statement_idx, config
|
|
148
|
+
)
|
|
149
|
+
issues.extend(condition_issues)
|
|
150
|
+
|
|
151
|
+
return issues
|
|
152
|
+
|
|
153
|
+
def _get_actions(self, statement: Statement) -> list[str]:
|
|
154
|
+
"""Extract actions from statement.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
statement: IAM policy statement
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of action strings
|
|
161
|
+
"""
|
|
162
|
+
if statement.action is None:
|
|
163
|
+
return []
|
|
164
|
+
return [statement.action] if isinstance(statement.action, str) else statement.action
|
|
165
|
+
|
|
166
|
+
def _find_matching_rule(self, action: str, rules: dict[str, Any]) -> dict[str, Any] | None:
|
|
167
|
+
"""Find validation rule matching the action.
|
|
168
|
+
|
|
169
|
+
Supports wildcards in action names.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
action: Action to find rule for (e.g., "sts:AssumeRole")
|
|
173
|
+
rules: Validation rules dict
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Matching rule dict or None
|
|
177
|
+
"""
|
|
178
|
+
# Exact match first (performance optimization)
|
|
179
|
+
if action in rules:
|
|
180
|
+
return rules[action]
|
|
181
|
+
|
|
182
|
+
# Check for wildcard patterns in action
|
|
183
|
+
for rule_action, rule_config in rules.items():
|
|
184
|
+
# Support wildcards in the action being validated
|
|
185
|
+
if "*" in action:
|
|
186
|
+
pattern = action.replace("*", ".*")
|
|
187
|
+
if re.match(f"^{pattern}$", rule_action):
|
|
188
|
+
return rule_config
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _extract_principal_types(self, statement: Statement) -> dict[str, list[str]]:
|
|
193
|
+
"""Extract principals grouped by type (AWS, Service, Federated, etc.).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
statement: IAM policy statement
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict mapping principal type to list of principal values
|
|
200
|
+
"""
|
|
201
|
+
principal_types: dict[str, list[str]] = {}
|
|
202
|
+
|
|
203
|
+
if statement.principal:
|
|
204
|
+
if isinstance(statement.principal, str):
|
|
205
|
+
# Simple string principal like "*"
|
|
206
|
+
principal_types["AWS"] = [statement.principal]
|
|
207
|
+
elif isinstance(statement.principal, dict):
|
|
208
|
+
for key, value in statement.principal.items():
|
|
209
|
+
if isinstance(value, str):
|
|
210
|
+
principal_types[key] = [value]
|
|
211
|
+
elif isinstance(value, list):
|
|
212
|
+
principal_types[key] = value
|
|
213
|
+
|
|
214
|
+
return principal_types
|
|
215
|
+
|
|
216
|
+
def _validate_principal_type(
|
|
217
|
+
self,
|
|
218
|
+
statement: Statement,
|
|
219
|
+
action: str,
|
|
220
|
+
rule: dict[str, Any],
|
|
221
|
+
statement_idx: int,
|
|
222
|
+
config: CheckConfig,
|
|
223
|
+
) -> list[ValidationIssue]:
|
|
224
|
+
"""Validate that principal type matches the assume action.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
statement: IAM policy statement
|
|
228
|
+
action: Assume action being validated
|
|
229
|
+
rule: Validation rule for this action
|
|
230
|
+
statement_idx: Statement index
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of validation issues
|
|
234
|
+
"""
|
|
235
|
+
issues = []
|
|
236
|
+
|
|
237
|
+
allowed_types = rule.get("allowed_principal_types", [])
|
|
238
|
+
if not allowed_types:
|
|
239
|
+
return issues
|
|
240
|
+
|
|
241
|
+
principal_types = self._extract_principal_types(statement)
|
|
242
|
+
|
|
243
|
+
# Check if any principal type is not allowed
|
|
244
|
+
for principal_type, principals in principal_types.items():
|
|
245
|
+
if principal_type not in allowed_types:
|
|
246
|
+
principals_list = ", ".join(f"`{p}`" for p in principals)
|
|
247
|
+
allowed_list = ", ".join(f"`{t}`" for t in allowed_types)
|
|
248
|
+
|
|
249
|
+
issues.append(
|
|
250
|
+
ValidationIssue(
|
|
251
|
+
severity=self.get_severity(config),
|
|
252
|
+
issue_type="invalid_principal_type_for_assume_action",
|
|
253
|
+
message=f"Action `{action}` should not use `Principal` type `{principal_type}`. "
|
|
254
|
+
f"Expected principal types: {allowed_list}",
|
|
255
|
+
statement_index=statement_idx,
|
|
256
|
+
statement_sid=statement.sid,
|
|
257
|
+
line_number=statement.line_number,
|
|
258
|
+
action=action,
|
|
259
|
+
suggestion=f"For `{action}`, use {allowed_list} principal type instead of `{principal_type}`. "
|
|
260
|
+
f"\n\nFound principals: `{principals_list}`\n\n"
|
|
261
|
+
f"{rule.get('description', '')}",
|
|
262
|
+
example=self._get_example_for_action(
|
|
263
|
+
action, allowed_types[0] if allowed_types else "AWS"
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return issues
|
|
269
|
+
|
|
270
|
+
def _validate_provider_format(
|
|
271
|
+
self,
|
|
272
|
+
statement: Statement,
|
|
273
|
+
action: str,
|
|
274
|
+
rule: dict[str, Any],
|
|
275
|
+
statement_idx: int,
|
|
276
|
+
config: CheckConfig,
|
|
277
|
+
) -> list[ValidationIssue]:
|
|
278
|
+
"""Validate that federated provider ARN matches expected format.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
statement: IAM policy statement
|
|
282
|
+
action: Assume action being validated
|
|
283
|
+
rule: Validation rule for this action
|
|
284
|
+
statement_idx: Statement index
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of validation issues
|
|
288
|
+
"""
|
|
289
|
+
issues = []
|
|
290
|
+
|
|
291
|
+
provider_pattern = rule.get("provider_pattern")
|
|
292
|
+
if not provider_pattern:
|
|
293
|
+
return issues
|
|
294
|
+
|
|
295
|
+
principal_types = self._extract_principal_types(statement)
|
|
296
|
+
federated_principals = principal_types.get("Federated", [])
|
|
297
|
+
|
|
298
|
+
for principal in federated_principals:
|
|
299
|
+
if not re.match(provider_pattern, principal):
|
|
300
|
+
provider_type = "SAML" if "saml-provider" in provider_pattern else "OIDC"
|
|
301
|
+
|
|
302
|
+
issues.append(
|
|
303
|
+
ValidationIssue(
|
|
304
|
+
severity=self.get_severity(config),
|
|
305
|
+
issue_type="invalid_provider_format",
|
|
306
|
+
message=f"Federated principal `{principal}` does not match expected `{provider_type}` provider format for `{action}`",
|
|
307
|
+
statement_index=statement_idx,
|
|
308
|
+
statement_sid=statement.sid,
|
|
309
|
+
line_number=statement.line_number,
|
|
310
|
+
action=action,
|
|
311
|
+
suggestion=f"For `{action}`, use a valid `{provider_type}` provider ARN.\n\n"
|
|
312
|
+
f"Expected pattern: `{provider_pattern}`\n"
|
|
313
|
+
f"Found: `{principal}`",
|
|
314
|
+
example=self._get_provider_example(provider_type),
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return issues
|
|
319
|
+
|
|
320
|
+
def _validate_required_conditions(
|
|
321
|
+
self,
|
|
322
|
+
statement: Statement,
|
|
323
|
+
action: str,
|
|
324
|
+
rule: dict[str, Any],
|
|
325
|
+
statement_idx: int,
|
|
326
|
+
config: CheckConfig,
|
|
327
|
+
) -> list[ValidationIssue]:
|
|
328
|
+
"""Validate that required conditions are present.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
statement: IAM policy statement
|
|
332
|
+
action: Assume action being validated
|
|
333
|
+
rule: Validation rule for this action
|
|
334
|
+
statement_idx: Statement index
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of validation issues
|
|
338
|
+
"""
|
|
339
|
+
issues = []
|
|
340
|
+
|
|
341
|
+
required_conditions = rule.get("required_conditions", [])
|
|
342
|
+
if not required_conditions:
|
|
343
|
+
return issues
|
|
344
|
+
|
|
345
|
+
# Get all condition keys from statement
|
|
346
|
+
condition_keys = set()
|
|
347
|
+
if statement.condition:
|
|
348
|
+
for _operator, keys_dict in statement.condition.items():
|
|
349
|
+
if isinstance(keys_dict, dict):
|
|
350
|
+
condition_keys.update(keys_dict.keys())
|
|
351
|
+
|
|
352
|
+
# Check for missing required conditions (supports wildcards like *:aud)
|
|
353
|
+
missing_conditions = []
|
|
354
|
+
for required_cond in required_conditions:
|
|
355
|
+
if "*:" in required_cond:
|
|
356
|
+
# Wildcard pattern - check if any key ends with the suffix
|
|
357
|
+
suffix = required_cond.split("*:")[1]
|
|
358
|
+
if not any(key.endswith(f":{suffix}") for key in condition_keys):
|
|
359
|
+
missing_conditions.append(required_cond)
|
|
360
|
+
else:
|
|
361
|
+
# Exact match
|
|
362
|
+
if required_cond not in condition_keys:
|
|
363
|
+
missing_conditions.append(required_cond)
|
|
364
|
+
|
|
365
|
+
if missing_conditions:
|
|
366
|
+
missing_list = ", ".join(f"`{c}`" for c in missing_conditions)
|
|
367
|
+
|
|
368
|
+
issues.append(
|
|
369
|
+
ValidationIssue(
|
|
370
|
+
severity=self.get_severity(config),
|
|
371
|
+
issue_type="missing_required_condition_for_assume_action",
|
|
372
|
+
message=f"Action `{action}` is missing required conditions: `{missing_list}`",
|
|
373
|
+
statement_index=statement_idx,
|
|
374
|
+
statement_sid=statement.sid,
|
|
375
|
+
line_number=statement.line_number,
|
|
376
|
+
action=action,
|
|
377
|
+
suggestion=f"Add required condition(s) to restrict when `{action}` can be performed. "
|
|
378
|
+
f"Missing: `{missing_list}`\n\n"
|
|
379
|
+
f"{rule.get('description', '')}",
|
|
380
|
+
example=self._get_condition_example(action, required_conditions[0]),
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return issues
|
|
385
|
+
|
|
386
|
+
def _get_example_for_action(self, action: str, principal_type: str) -> str:
|
|
387
|
+
"""Generate example JSON for an assume action.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
action: Assume action
|
|
391
|
+
principal_type: Expected principal type
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
JSON example string
|
|
395
|
+
"""
|
|
396
|
+
examples = {
|
|
397
|
+
("sts:AssumeRole", "AWS"): """{
|
|
398
|
+
"Effect": "Allow",
|
|
399
|
+
"Principal": {
|
|
400
|
+
"AWS": "arn:aws:iam::123456789012:root"
|
|
401
|
+
},
|
|
402
|
+
"Action": "sts:AssumeRole",
|
|
403
|
+
"Condition": {
|
|
404
|
+
"StringEquals": {
|
|
405
|
+
"sts:ExternalId": "unique-external-id"
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}""",
|
|
409
|
+
("sts:AssumeRole", "Service"): """{
|
|
410
|
+
"Effect": "Allow",
|
|
411
|
+
"Principal": {
|
|
412
|
+
"Service": "lambda.amazonaws.com"
|
|
413
|
+
},
|
|
414
|
+
"Action": "sts:AssumeRole"
|
|
415
|
+
}""",
|
|
416
|
+
("sts:AssumeRoleWithSAML", "Federated"): """{
|
|
417
|
+
"Effect": "Allow",
|
|
418
|
+
"Principal": {
|
|
419
|
+
"Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
|
|
420
|
+
},
|
|
421
|
+
"Action": "sts:AssumeRoleWithSAML",
|
|
422
|
+
"Condition": {
|
|
423
|
+
"StringEquals": {
|
|
424
|
+
"SAML:aud": "https://signin.aws.amazon.com/saml"
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}""",
|
|
428
|
+
("sts:AssumeRoleWithWebIdentity", "Federated"): """{
|
|
429
|
+
"Effect": "Allow",
|
|
430
|
+
"Principal": {
|
|
431
|
+
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
|
|
432
|
+
},
|
|
433
|
+
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
434
|
+
"Condition": {
|
|
435
|
+
"StringEquals": {
|
|
436
|
+
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
|
|
437
|
+
},
|
|
438
|
+
"StringLike": {
|
|
439
|
+
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}""",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return examples.get((action, principal_type), "")
|
|
446
|
+
|
|
447
|
+
def _get_provider_example(self, provider_type: str) -> str:
|
|
448
|
+
"""Get example provider ARN.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
provider_type: Type of provider (SAML or OIDC)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Example ARN string
|
|
455
|
+
"""
|
|
456
|
+
if provider_type == "SAML":
|
|
457
|
+
return """{
|
|
458
|
+
"Principal": {
|
|
459
|
+
"Federated": "arn:aws:iam::123456789012:saml-provider/MyProvider"
|
|
460
|
+
}
|
|
461
|
+
}"""
|
|
462
|
+
else: # OIDC
|
|
463
|
+
return """{
|
|
464
|
+
"Principal": {
|
|
465
|
+
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
|
|
466
|
+
}
|
|
467
|
+
}"""
|
|
468
|
+
|
|
469
|
+
def _get_condition_example(self, action: str, condition_key: str) -> str:
|
|
470
|
+
"""Get example condition for an action.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
action: Assume action
|
|
474
|
+
condition_key: Required condition key
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
JSON example string
|
|
478
|
+
"""
|
|
479
|
+
examples = {
|
|
480
|
+
"SAML:aud": """{
|
|
481
|
+
"Condition": {
|
|
482
|
+
"StringEquals": {
|
|
483
|
+
"SAML:aud": "https://signin.aws.amazon.com/saml"
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}""",
|
|
487
|
+
"sts:ExternalId": """{
|
|
488
|
+
"Condition": {
|
|
489
|
+
"StringEquals": {
|
|
490
|
+
"sts:ExternalId": "unique-external-id-shared-with-trusted-party"
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}""",
|
|
494
|
+
"aws:PrincipalOrgID": """{
|
|
495
|
+
"Condition": {
|
|
496
|
+
"StringEquals": {
|
|
497
|
+
"aws:PrincipalOrgID": "o-123456789"
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}""",
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return examples.get(
|
|
504
|
+
condition_key,
|
|
505
|
+
f'{{\n "Condition": {{\n "StringEquals": {{\n "{condition_key}": "value"\n }}\n }}\n}}',
|
|
506
|
+
)
|
|
@@ -5,15 +5,13 @@ configurations, supporting exact matches, regex patterns, and any_of/all_of logi
|
|
|
5
5
|
|
|
6
6
|
Performance optimizations:
|
|
7
7
|
- Uses frozenset for O(1) lookups
|
|
8
|
-
- LRU cache for compiled regex patterns
|
|
8
|
+
- Centralized LRU cache for compiled regex patterns (from ignore_patterns module)
|
|
9
9
|
- Lazy loading of default actions from modular data structure
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import re
|
|
13
|
-
from functools import lru_cache
|
|
14
|
-
|
|
15
12
|
from iam_validator.core.check_registry import CheckConfig
|
|
16
13
|
from iam_validator.core.config.sensitive_actions import get_sensitive_actions
|
|
14
|
+
from iam_validator.core.ignore_patterns import compile_pattern
|
|
17
15
|
|
|
18
16
|
# Lazy-loaded default set of sensitive actions
|
|
19
17
|
# This will be loaded only when first accessed
|
|
@@ -37,7 +35,9 @@ def _get_default_sensitive_actions() -> frozenset[str]:
|
|
|
37
35
|
return _DEFAULT_SENSITIVE_ACTIONS_CACHE
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
def get_sensitive_actions_by_categories(
|
|
38
|
+
def get_sensitive_actions_by_categories(
|
|
39
|
+
categories: list[str] | None = None,
|
|
40
|
+
) -> frozenset[str]:
|
|
41
41
|
"""
|
|
42
42
|
Get sensitive actions filtered by categories.
|
|
43
43
|
|
|
@@ -66,25 +66,10 @@ def get_sensitive_actions_by_categories(categories: list[str] | None = None) ->
|
|
|
66
66
|
DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
# Global regex pattern cache for performance
|
|
70
|
-
@lru_cache(maxsize=256)
|
|
71
|
-
def compile_pattern(pattern: str) -> re.Pattern[str] | None:
|
|
72
|
-
"""Compile and cache regex patterns.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
pattern: Regex pattern string
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
Compiled pattern or None if invalid
|
|
79
|
-
"""
|
|
80
|
-
try:
|
|
81
|
-
return re.compile(pattern)
|
|
82
|
-
except re.error:
|
|
83
|
-
return None
|
|
84
|
-
|
|
85
|
-
|
|
86
69
|
def check_sensitive_actions(
|
|
87
|
-
actions: list[str],
|
|
70
|
+
actions: list[str],
|
|
71
|
+
config: CheckConfig,
|
|
72
|
+
default_actions: frozenset[str] | None = None,
|
|
88
73
|
) -> tuple[bool, list[str]]:
|
|
89
74
|
"""
|
|
90
75
|
Check if actions match sensitive action criteria with any_of/all_of support.
|
|
@@ -115,6 +100,10 @@ def check_sensitive_actions(
|
|
|
115
100
|
# Use all categories if no specific categories configured
|
|
116
101
|
default_actions = _get_default_sensitive_actions()
|
|
117
102
|
|
|
103
|
+
# Apply ignore_patterns to filter out default actions
|
|
104
|
+
# This allows users to exclude specific actions from the default 490 actions
|
|
105
|
+
default_actions = config.filter_actions(default_actions)
|
|
106
|
+
|
|
118
107
|
# Filter out wildcards
|
|
119
108
|
filtered_actions = [a for a in actions if a != "*"]
|
|
120
109
|
if not filtered_actions:
|
|
@@ -141,6 +130,17 @@ def check_sensitive_actions(
|
|
|
141
130
|
matched_set = set(actions_matched) | set(patterns_matched)
|
|
142
131
|
matched_actions = list(matched_set)
|
|
143
132
|
|
|
133
|
+
# Apply ignore_patterns to filter the final matched actions
|
|
134
|
+
# This ensures ignore_patterns work for:
|
|
135
|
+
# 1. Default actions (490 actions from Python modules)
|
|
136
|
+
# 2. Custom sensitive_actions configuration
|
|
137
|
+
# 3. Custom sensitive_action_patterns configuration
|
|
138
|
+
if matched_actions and config.ignore_patterns:
|
|
139
|
+
filtered_matched = config.filter_actions(frozenset(matched_actions))
|
|
140
|
+
matched_actions = list(filtered_matched)
|
|
141
|
+
# Update is_sensitive based on filtered results
|
|
142
|
+
is_sensitive = len(matched_actions) > 0
|
|
143
|
+
|
|
144
144
|
return is_sensitive, matched_actions
|
|
145
145
|
|
|
146
146
|
|
|
@@ -243,7 +243,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
243
243
|
# Each item can be a string pattern, or a dict with any_of/all_of
|
|
244
244
|
if isinstance(item, str):
|
|
245
245
|
# Simple string pattern - check if any action matches
|
|
246
|
-
# Use cached compiled pattern
|
|
246
|
+
# Use cached compiled pattern from centralized ignore_patterns module
|
|
247
247
|
compiled = compile_pattern(item)
|
|
248
248
|
if compiled:
|
|
249
249
|
for action in actions:
|
|
@@ -262,7 +262,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
262
262
|
# any_of: at least one action must match at least one pattern
|
|
263
263
|
if "any_of" in config:
|
|
264
264
|
matched = set()
|
|
265
|
-
# Pre-compile all patterns
|
|
265
|
+
# Pre-compile all patterns using centralized cache
|
|
266
266
|
compiled_patterns = [compile_pattern(p) for p in config["any_of"]]
|
|
267
267
|
|
|
268
268
|
for action in actions:
|
|
@@ -274,7 +274,7 @@ def check_patterns_config(actions: list[str], config) -> tuple[bool, list[str]]:
|
|
|
274
274
|
|
|
275
275
|
# all_of: at least one action must match ALL patterns
|
|
276
276
|
if "all_of" in config:
|
|
277
|
-
# Pre-compile all patterns
|
|
277
|
+
# Pre-compile all patterns using centralized cache
|
|
278
278
|
compiled_patterns = [compile_pattern(p) for p in config["all_of"]]
|
|
279
279
|
# Filter out invalid patterns
|
|
280
280
|
compiled_patterns = [p for p in compiled_patterns if p]
|
|
@@ -7,7 +7,7 @@ to their actual action names using the AWS Service Reference API.
|
|
|
7
7
|
import re
|
|
8
8
|
from functools import lru_cache
|
|
9
9
|
|
|
10
|
-
from iam_validator.core.
|
|
10
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# Global cache for compiled wildcard patterns (shared across checks)
|
|
@@ -72,7 +72,7 @@ async def expand_wildcard_actions(actions: list[str], fetcher: AWSServiceFetcher
|
|
|
72
72
|
available_actions = list(service_detail.actions.keys())
|
|
73
73
|
|
|
74
74
|
# Match wildcard pattern against available actions
|
|
75
|
-
_, matched_actions = fetcher.
|
|
75
|
+
_, matched_actions = fetcher.match_wildcard_action(action_name, available_actions)
|
|
76
76
|
|
|
77
77
|
# Add expanded actions with service prefix
|
|
78
78
|
for matched_action in matched_actions:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Wildcard action check - detects Action: '*' in IAM policies."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
4
6
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
5
7
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
6
8
|
|
|
@@ -8,17 +10,9 @@ from iam_validator.core.models import Statement, ValidationIssue
|
|
|
8
10
|
class WildcardActionCheck(PolicyCheck):
|
|
9
11
|
"""Checks for wildcard actions (Action: '*') which grant all permissions."""
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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"
|
|
13
|
+
check_id: ClassVar[str] = "wildcard_action"
|
|
14
|
+
description: ClassVar[str] = "Checks for wildcard actions (*)"
|
|
15
|
+
default_severity: ClassVar[str] = "medium"
|
|
22
16
|
|
|
23
17
|
async def execute(
|
|
24
18
|
self,
|
|
@@ -38,7 +32,9 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
38
32
|
|
|
39
33
|
# Check for wildcard action (Action: "*")
|
|
40
34
|
if "*" in actions:
|
|
41
|
-
message = config.config.get(
|
|
35
|
+
message = config.config.get(
|
|
36
|
+
"message", 'Statement allows all actions `"*"` (wildcard action).'
|
|
37
|
+
)
|
|
42
38
|
suggestion = config.config.get(
|
|
43
39
|
"suggestion",
|
|
44
40
|
"Replace wildcard with specific actions needed for your use case",
|