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.
Files changed (38) hide show
  1. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -6
  2. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +61 -23
  6. iam_validator/checks/action_resource_matching.py +6 -2
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +1 -1
  9. iam_validator/checks/condition_type_mismatch.py +6 -6
  10. iam_validator/checks/policy_structure.py +577 -0
  11. iam_validator/checks/policy_type_validation.py +48 -32
  12. iam_validator/checks/principal_validation.py +65 -133
  13. iam_validator/checks/resource_validation.py +8 -8
  14. iam_validator/checks/sensitive_action.py +7 -3
  15. iam_validator/checks/service_wildcard.py +2 -2
  16. iam_validator/checks/set_operator_validation.py +11 -11
  17. iam_validator/checks/sid_uniqueness.py +8 -4
  18. iam_validator/checks/trust_policy_validation.py +512 -0
  19. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  20. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  21. iam_validator/checks/wildcard_action.py +3 -1
  22. iam_validator/checks/wildcard_resource.py +3 -1
  23. iam_validator/commands/validate.py +6 -12
  24. iam_validator/core/__init__.py +1 -2
  25. iam_validator/core/access_analyzer.py +1 -1
  26. iam_validator/core/access_analyzer_report.py +2 -2
  27. iam_validator/core/aws_fetcher.py +45 -43
  28. iam_validator/core/check_registry.py +83 -79
  29. iam_validator/core/config/condition_requirements.py +69 -17
  30. iam_validator/core/config/defaults.py +58 -52
  31. iam_validator/core/config/service_principals.py +40 -3
  32. iam_validator/core/ignore_patterns.py +297 -0
  33. iam_validator/core/models.py +15 -5
  34. iam_validator/core/policy_checks.py +31 -472
  35. iam_validator/core/policy_loader.py +27 -4
  36. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  37. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  38. {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, operator_type, set_prefix = normalize_operator(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, operator_type, set_prefix = normalize_operator(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, condition_values in conditions.items():
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 '{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'. "
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 '{condition_key}' "
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'\'{condition_key}\' will be granted access. Add: \'"Null": {{"{condition_key}": "false"}}\'. '
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 '{condition_key}' "
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"'{condition_key}' will evaluate to 'No match' instead of denying access. "
146
- f'Add: \'"Null": {{"{condition_key}": "false"}}\'. '
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 '{statement.sid}' contains spaces, which are not allowed by AWS"
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: '{statement.sid.replace(' ', '')}'"
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 '{statement.sid}' contains invalid characters: {invalid_chars}"
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
+ )