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.
Files changed (56) hide show
  1. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +127 -6
  2. iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +5 -3
  6. iam_validator/checks/action_condition_enforcement.py +559 -207
  7. iam_validator/checks/action_resource_matching.py +12 -15
  8. iam_validator/checks/action_validation.py +7 -13
  9. iam_validator/checks/condition_key_validation.py +7 -13
  10. iam_validator/checks/condition_type_mismatch.py +15 -22
  11. iam_validator/checks/full_wildcard.py +9 -13
  12. iam_validator/checks/mfa_condition_check.py +8 -17
  13. iam_validator/checks/policy_size.py +6 -39
  14. iam_validator/checks/policy_structure.py +547 -0
  15. iam_validator/checks/policy_type_validation.py +61 -46
  16. iam_validator/checks/principal_validation.py +71 -148
  17. iam_validator/checks/resource_validation.py +13 -20
  18. iam_validator/checks/sensitive_action.py +15 -18
  19. iam_validator/checks/service_wildcard.py +8 -14
  20. iam_validator/checks/set_operator_validation.py +21 -28
  21. iam_validator/checks/sid_uniqueness.py +16 -42
  22. iam_validator/checks/trust_policy_validation.py +506 -0
  23. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  24. iam_validator/checks/utils/wildcard_expansion.py +2 -2
  25. iam_validator/checks/wildcard_action.py +9 -13
  26. iam_validator/checks/wildcard_resource.py +9 -13
  27. iam_validator/commands/cache.py +4 -3
  28. iam_validator/commands/validate.py +15 -9
  29. iam_validator/core/__init__.py +2 -3
  30. iam_validator/core/access_analyzer.py +1 -1
  31. iam_validator/core/access_analyzer_report.py +2 -2
  32. iam_validator/core/aws_fetcher.py +24 -1028
  33. iam_validator/core/aws_service/__init__.py +21 -0
  34. iam_validator/core/aws_service/cache.py +108 -0
  35. iam_validator/core/aws_service/client.py +205 -0
  36. iam_validator/core/aws_service/fetcher.py +612 -0
  37. iam_validator/core/aws_service/parsers.py +149 -0
  38. iam_validator/core/aws_service/patterns.py +51 -0
  39. iam_validator/core/aws_service/storage.py +291 -0
  40. iam_validator/core/aws_service/validators.py +379 -0
  41. iam_validator/core/check_registry.py +165 -93
  42. iam_validator/core/config/condition_requirements.py +69 -17
  43. iam_validator/core/config/defaults.py +58 -52
  44. iam_validator/core/config/service_principals.py +40 -3
  45. iam_validator/core/constants.py +17 -0
  46. iam_validator/core/ignore_patterns.py +297 -0
  47. iam_validator/core/models.py +15 -5
  48. iam_validator/core/policy_checks.py +38 -475
  49. iam_validator/core/policy_loader.py +27 -4
  50. iam_validator/sdk/__init__.py +1 -1
  51. iam_validator/sdk/context.py +1 -1
  52. iam_validator/sdk/helpers.py +1 -1
  53. iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
  54. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
  55. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
  56. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,7 @@ This check runs automatically based on:
11
11
  2. Auto-detection: If any statement has a Principal, provides helpful guidance
12
12
  """
13
13
 
14
+ from iam_validator.core.constants import RCP_SUPPORTED_SERVICES
14
15
  from iam_validator.core.models import IAMPolicy, ValidationIssue
15
16
 
16
17
 
@@ -30,6 +31,10 @@ async def execute_policy(
30
31
  """
31
32
  issues = []
32
33
 
34
+ # Handle policies with no statements
35
+ if not policy.statement:
36
+ return issues
37
+
33
38
  # Check if any statement has Principal
34
39
  has_any_principal = any(
35
40
  stmt.principal is not None or stmt.not_principal is not None for stmt in policy.statement
@@ -37,24 +42,36 @@ async def execute_policy(
37
42
 
38
43
  # If policy has Principal but type is IDENTITY_POLICY (default), provide helpful info
39
44
  if has_any_principal and policy_type == "IDENTITY_POLICY":
45
+ # Check if it's a trust policy
46
+ from iam_validator.checks.policy_structure import is_trust_policy
47
+
48
+ if is_trust_policy(policy):
49
+ hint_msg = (
50
+ "Policy contains assume role actions - this is a TRUST POLICY. "
51
+ "Use `--policy-type TRUST_POLICY` for proper validation (suppresses missing Resource warnings, "
52
+ "enables trust-specific validation)"
53
+ )
54
+ suggestion_msg = "iam-validator validate --path <file> --policy-type TRUST_POLICY"
55
+ else:
56
+ hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use `--policy-type RESOURCE_POLICY`"
57
+ suggestion_msg = "iam-validator validate --path <file> --policy-type RESOURCE_POLICY"
58
+
40
59
  issues.append(
41
60
  ValidationIssue(
42
61
  severity="info",
43
62
  issue_type="policy_type_hint",
44
- message="Policy contains Principal element - this suggests it's a resource policy. "
45
- "Use --policy-type RESOURCE_POLICY for proper validation.",
63
+ message=hint_msg,
46
64
  statement_index=0,
47
65
  statement_sid=None,
48
66
  line_number=None,
49
- suggestion="If this is a resource policy (S3 bucket policy, SNS topic policy, etc.), "
50
- "run validation with: iam-validator validate --path <file> --policy-type RESOURCE_POLICY",
67
+ suggestion=suggestion_msg,
51
68
  )
52
69
  )
53
70
  # Don't run further checks if we're just hinting
54
71
  return issues
55
72
 
56
- # Resource policies MUST have Principal
57
- if policy_type == "RESOURCE_POLICY":
73
+ # Resource policies and Trust policies MUST have Principal
74
+ if policy_type in ("RESOURCE_POLICY", "TRUST_POLICY"):
58
75
  for idx, statement in enumerate(policy.statement):
59
76
  has_principal = statement.principal is not None or statement.not_principal is not None
60
77
 
@@ -63,13 +80,13 @@ async def execute_policy(
63
80
  ValidationIssue(
64
81
  severity="error",
65
82
  issue_type="missing_principal",
66
- message="Resource policy statement missing required Principal element. "
83
+ message="Resource policy statement missing required `Principal` element. "
67
84
  "Resource-based policies (S3 bucket policies, SNS topic policies, etc.) "
68
- "must include a Principal element to specify who can access the resource.",
85
+ "must include a `Principal` element to specify who can access the resource.",
69
86
  statement_index=idx,
70
87
  statement_sid=statement.sid,
71
88
  line_number=statement.line_number,
72
- suggestion="Add a Principal element to specify who can access this resource.\n"
89
+ suggestion="Add a `Principal` element to specify who can access this resource.\n"
73
90
  "Example:\n"
74
91
  "```json\n"
75
92
  "{\n"
@@ -94,14 +111,14 @@ async def execute_policy(
94
111
  ValidationIssue(
95
112
  severity="warning",
96
113
  issue_type="unexpected_principal",
97
- message="Identity policy should not contain Principal element. "
114
+ message="Identity policy should not contain `Principal` element. "
98
115
  "Identity-based policies (attached to IAM users, groups, or roles) "
99
- "do not need a Principal element because the principal is implicit "
116
+ "do not need a `Principal` element because the principal is implicit "
100
117
  "(the entity the policy is attached to).",
101
118
  statement_index=idx,
102
119
  statement_sid=statement.sid,
103
120
  line_number=statement.line_number,
104
- suggestion="Remove the Principal element from this identity policy statement.\n"
121
+ suggestion="Remove the `Principal` element from this identity policy statement.\n"
105
122
  "Example:\n"
106
123
  "```json\n"
107
124
  "{\n"
@@ -123,13 +140,13 @@ async def execute_policy(
123
140
  ValidationIssue(
124
141
  severity="error",
125
142
  issue_type="invalid_principal",
126
- message="Service Control Policy must not contain Principal element. "
143
+ message="Service Control Policy must not contain `Principal` element. "
127
144
  "Service Control Policies (SCPs) in AWS Organizations do not support "
128
- "the Principal element. They apply to all principals in the organization or OU.",
145
+ "the `Principal` element. They apply to all principals in the organization or OU.",
129
146
  statement_index=idx,
130
147
  statement_sid=statement.sid,
131
148
  line_number=statement.line_number,
132
- suggestion="Remove the Principal element from this SCP statement.\n"
149
+ suggestion="Remove the `Principal` element from this SCP statement.\n"
133
150
  "Example:\n"
134
151
  "```json\n"
135
152
  "{\n"
@@ -148,8 +165,8 @@ async def execute_policy(
148
165
 
149
166
  # Resource Control Policies (RCPs) have very strict requirements
150
167
  elif policy_type == "RESOURCE_CONTROL_POLICY":
151
- # RCP supported services (as of 2025)
152
- rcp_supported_services = {"s3", "sts", "sqs", "secretsmanager", "kms"}
168
+ # Use the centralized list of RCP supported services from constants
169
+ rcp_supported_services = RCP_SUPPORTED_SERVICES
153
170
 
154
171
  for idx, statement in enumerate(policy.statement):
155
172
  # 1. Effect MUST be Deny (only RCPFullAWSAccess can use Allow)
@@ -158,13 +175,13 @@ async def execute_policy(
158
175
  ValidationIssue(
159
176
  severity="error",
160
177
  issue_type="invalid_rcp_effect",
161
- message="Resource Control Policy statement must have Effect: Deny. "
162
- "For RCPs that you create, the Effect value must be 'Deny'. "
163
- "Only the AWS-managed RCPFullAWSAccess policy can use 'Allow'.",
178
+ message="Resource Control Policy statement must have `Effect: Deny`. "
179
+ "For RCPs that you create, the `Effect` value must be `Deny`. "
180
+ "Only the AWS-managed `RCPFullAWSAccess` policy can use `Allow`.",
164
181
  statement_index=idx,
165
182
  statement_sid=statement.sid,
166
183
  line_number=statement.line_number,
167
- suggestion='Change the Effect to "Deny" for this RCP statement.',
184
+ suggestion="Change the `Effect` to `Deny` for this RCP statement.",
168
185
  )
169
186
  )
170
187
 
@@ -177,14 +194,13 @@ async def execute_policy(
177
194
  ValidationIssue(
178
195
  severity="error",
179
196
  issue_type="invalid_rcp_not_principal",
180
- message="Resource Control Policy must not contain NotPrincipal element. "
181
- "RCPs only support Principal with value '*'. Use Condition elements "
197
+ message="Resource Control Policy must not contain `NotPrincipal` element. "
198
+ "RCPs only support `Principal` with value `*`. Use `Condition` elements "
182
199
  "to restrict specific principals.",
183
200
  statement_index=idx,
184
201
  statement_sid=statement.sid,
185
202
  line_number=statement.line_number,
186
- suggestion="Remove NotPrincipal and use Principal: '*' with Condition "
187
- "elements to restrict access.",
203
+ suggestion='Remove `NotPrincipal` and use `Principal: "*"` with `Condition` elements to restrict access.',
188
204
  )
189
205
  )
190
206
  elif not has_principal:
@@ -192,13 +208,13 @@ async def execute_policy(
192
208
  ValidationIssue(
193
209
  severity="error",
194
210
  issue_type="missing_rcp_principal",
195
- message="Resource Control Policy statement must have Principal: '*'. "
196
- "RCPs require the Principal element with value '*'. Use Condition "
211
+ message='Resource Control Policy statement must have `Principal: "*"`. '
212
+ 'RCPs require the `Principal` element with value `"*"`. Use `Condition` '
197
213
  "elements to restrict specific principals.",
198
214
  statement_index=idx,
199
215
  statement_sid=statement.sid,
200
216
  line_number=statement.line_number,
201
- suggestion='Add Principal: "*" to this RCP statement.',
217
+ suggestion='Add `Principal: "*"` to this RCP statement.',
202
218
  )
203
219
  )
204
220
  elif statement.principal != "*":
@@ -209,13 +225,13 @@ async def execute_policy(
209
225
  ValidationIssue(
210
226
  severity="error",
211
227
  issue_type="invalid_rcp_principal",
212
- message="Resource Control Policy Principal must be '*'. "
213
- f"Found: {statement.principal}. RCPs can only specify '*' in the "
214
- "Principal element. Use Condition elements to restrict specific principals.",
228
+ message=f'Resource Control Policy `Principal` must be `"*"`. '
229
+ f'Found: `{statement.principal}`. RCPs can only specify `"*"` in the '
230
+ "`Principal` element. Use `Condition` elements to restrict specific principals.",
215
231
  statement_index=idx,
216
232
  statement_sid=statement.sid,
217
233
  line_number=statement.line_number,
218
- suggestion='Change Principal to "*" and use Condition elements to restrict access.',
234
+ suggestion='Change `Principal` to `"*"` and use `Condition` elements to restrict access.',
219
235
  )
220
236
  )
221
237
 
@@ -234,14 +250,14 @@ async def execute_policy(
234
250
  ValidationIssue(
235
251
  severity="error",
236
252
  issue_type="invalid_rcp_wildcard_action",
237
- message="Resource Control Policy must not use '*' alone in Action element. "
238
- "Customer-managed RCPs cannot use '*' as the action wildcard. "
239
- "Use service-specific wildcards like 's3:*' instead.",
253
+ message="Resource Control Policy must not use `*` alone in `Action` element. "
254
+ "Customer-managed RCPs cannot use `*` as the action wildcard. "
255
+ "Use service-specific wildcards like `s3:*` instead.",
240
256
  statement_index=idx,
241
257
  statement_sid=statement.sid,
242
258
  line_number=statement.line_number,
243
- suggestion="Replace '*' with service-specific actions from supported "
244
- f"services: {', '.join(sorted(rcp_supported_services))}",
259
+ suggestion="Replace `*` with service-specific actions from supported "
260
+ f"services: {', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
245
261
  )
246
262
  )
247
263
  else:
@@ -259,13 +275,13 @@ async def execute_policy(
259
275
  severity="error",
260
276
  issue_type="unsupported_rcp_service",
261
277
  message=f"Resource Control Policy contains actions from unsupported services: "
262
- f"{', '.join(unsupported_actions)}. RCPs only support these services: "
263
- f"{', '.join(sorted(rcp_supported_services))}",
278
+ f"{', '.join(f'`{a}`' for a in unsupported_actions)}. RCPs only support these services: "
279
+ f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
264
280
  statement_index=idx,
265
281
  statement_sid=statement.sid,
266
282
  line_number=statement.line_number,
267
283
  suggestion=f"Use only actions from supported RCP services: "
268
- f"{', '.join(sorted(rcp_supported_services))}",
284
+ f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
269
285
  )
270
286
  )
271
287
 
@@ -275,13 +291,12 @@ async def execute_policy(
275
291
  ValidationIssue(
276
292
  severity="error",
277
293
  issue_type="invalid_rcp_not_action",
278
- message="Resource Control Policy must not contain NotAction element. "
279
- "RCPs do not support NotAction. Use Action element instead.",
294
+ message="Resource Control Policy must not contain `NotAction` element. "
295
+ "RCPs do not support `NotAction`. Use `Action` element instead.",
280
296
  statement_index=idx,
281
297
  statement_sid=statement.sid,
282
298
  line_number=statement.line_number,
283
- suggestion="Replace NotAction with Action element listing the specific "
284
- "actions to deny.",
299
+ suggestion="Replace `NotAction` with `Action` element listing the specific actions to deny.",
285
300
  )
286
301
  )
287
302
 
@@ -294,11 +309,11 @@ async def execute_policy(
294
309
  ValidationIssue(
295
310
  severity="error",
296
311
  issue_type="missing_rcp_resource",
297
- message="Resource Control Policy statement must have Resource or NotResource element.",
312
+ message="Resource Control Policy statement must have `Resource` or `NotResource` element.",
298
313
  statement_index=idx,
299
314
  statement_sid=statement.sid,
300
315
  line_number=statement.line_number,
301
- suggestion='Add Resource: "*" or specify specific resource ARNs.',
316
+ suggestion='Add `Resource: "*"` or specify specific resource ARNs.',
302
317
  )
303
318
  )
304
319
 
@@ -4,60 +4,53 @@ Validates Principal elements in resource-based policies for security best practi
4
4
  This check enforces:
5
5
  - Blocked principals (e.g., public access via "*")
6
6
  - Allowed principals whitelist (optional)
7
- - Required conditions for specific principals (simple format)
8
- - Rich condition requirements for principals (advanced format with all_of/any_of)
7
+ - Rich condition requirements for principals (supports any_of/all_of/none_of)
9
8
  - Service principal validation
10
9
 
11
- Only runs for RESOURCE_POLICY type policies.
12
-
13
- Configuration supports TWO formats:
14
-
15
- 1. Simple format (backward compatible):
16
- require_conditions_for:
17
- "*": ["aws:SourceArn", "aws:SourceAccount"]
18
- "arn:aws:iam::*:root": ["aws:PrincipalOrgID"]
19
-
20
- 2. Advanced format with rich condition requirements:
21
- principal_condition_requirements:
22
- - principals:
23
- - "*"
24
- severity: critical
25
- required_conditions:
26
- all_of:
27
- - condition_key: "aws:SourceArn"
28
- description: "Limit by source ARN"
29
- - condition_key: "aws:SourceAccount"
30
-
31
- - principals:
32
- - "arn:aws:iam::*:root"
33
- required_conditions:
34
- - condition_key: "aws:PrincipalOrgID"
35
- expected_value: "o-xxxxx"
36
- operator: "StringEquals"
10
+ Only runs for RESOURCE_POLICY and TRUST_POLICY types.
11
+
12
+ Configuration format:
13
+
14
+ principal_condition_requirements:
15
+ - principals:
16
+ - "*" # Can be a list of principal patterns
17
+ severity: critical # Optional: override default severity
18
+ required_conditions:
19
+ any_of: # At least ONE of these conditions must be present
20
+ - condition_key: "aws:SourceArn"
21
+ description: "Limit by source ARN"
22
+ - condition_key: "aws:SourceAccount"
23
+ expected_value: "123456789012" # Optional: validate specific value
24
+ operator: "StringEquals" # Optional: validate specific operator
25
+
26
+ - principals:
27
+ - "arn:aws:iam::*:root"
28
+ required_conditions:
29
+ all_of: # ALL of these conditions must be present
30
+ - condition_key: "aws:PrincipalOrgID"
31
+ expected_value: "o-xxxxx"
32
+ - condition_key: "aws:SourceAccount"
33
+
34
+ Supports: any_of, all_of, none_of, and expected_value (single value or list)
37
35
  """
38
36
 
39
37
  import fnmatch
40
- from typing import Any
38
+ from typing import Any, ClassVar
41
39
 
42
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
40
+ from iam_validator.core.aws_service import AWSServiceFetcher
43
41
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
42
+ from iam_validator.core.config.service_principals import is_aws_service_principal
44
43
  from iam_validator.core.models import Statement, ValidationIssue
45
44
 
46
45
 
47
46
  class PrincipalValidationCheck(PolicyCheck):
48
47
  """Validates Principal elements in resource policies."""
49
48
 
50
- @property
51
- def check_id(self) -> str:
52
- return "principal_validation"
53
-
54
- @property
55
- def description(self) -> str:
56
- return "Validates Principal elements in resource policies for security best practices"
57
-
58
- @property
59
- def default_severity(self) -> str:
60
- return "high"
49
+ check_id: ClassVar[str] = "principal_validation"
50
+ description: ClassVar[str] = (
51
+ "Validates Principal elements in resource policies for security best practices"
52
+ )
53
+ default_severity: ClassVar[str] = "high"
61
54
 
62
55
  async def execute(
63
56
  self,
@@ -83,22 +76,13 @@ class PrincipalValidationCheck(PolicyCheck):
83
76
  if statement.principal is None and statement.not_principal is None:
84
77
  return issues
85
78
 
86
- # Get configuration
79
+ # Get configuration (defaults match defaults.py)
87
80
  blocked_principals = config.config.get("blocked_principals", ["*"])
88
81
  allowed_principals = config.config.get("allowed_principals", [])
89
- require_conditions_for = config.config.get("require_conditions_for", {})
90
82
  principal_condition_requirements = config.config.get("principal_condition_requirements", [])
91
- allowed_service_principals = config.config.get(
92
- "allowed_service_principals",
93
- [
94
- "cloudfront.amazonaws.com",
95
- "s3.amazonaws.com",
96
- "sns.amazonaws.com",
97
- "lambda.amazonaws.com",
98
- "logs.amazonaws.com",
99
- "events.amazonaws.com",
100
- ],
101
- )
83
+ # Default: "aws:*" allows ALL AWS service principals (*.amazonaws.com)
84
+ # This matches the default in defaults.py:251
85
+ allowed_service_principals = config.config.get("allowed_service_principals", ["aws:*"])
102
86
 
103
87
  # Extract principals from statement
104
88
  principals = self._extract_principals(statement)
@@ -117,8 +101,8 @@ class PrincipalValidationCheck(PolicyCheck):
117
101
  statement_index=statement_idx,
118
102
  statement_sid=statement.sid,
119
103
  line_number=statement.line_number,
120
- suggestion=f"Remove the principal `{principal}` or add appropriate conditions to restrict access. "
121
- "Consider using more specific principals instead of wildcards.",
104
+ suggestion=f"Remove the `Principal` `{principal}` or add appropriate `Condition`s to restrict access. "
105
+ "Consider using more specific `Principal`s instead of `*` (wildcard).",
122
106
  )
123
107
  )
124
108
  continue
@@ -131,42 +115,18 @@ class PrincipalValidationCheck(PolicyCheck):
131
115
  ValidationIssue(
132
116
  severity=self.get_severity(config),
133
117
  issue_type="unauthorized_principal",
134
- message=f"Principal not in allowed list: `{principal}`. "
135
- f"Only principals in the `allowed_principals` whitelist are permitted.",
118
+ message=f"`Principal` not in allowed list: `{principal}`. "
119
+ f"Only principals in the `allowed_principals` allow-list are permitted.",
136
120
  statement_index=statement_idx,
137
121
  statement_sid=statement.sid,
138
122
  line_number=statement.line_number,
139
- suggestion=f"Add '{principal}' to the allowed_principals list in your config, "
140
- "or use a principal that matches an allowed pattern.",
123
+ suggestion=f"Add `{principal}` to the `allowed_principals` list in your config, "
124
+ "or use a `Principal` that matches an allowed pattern.",
141
125
  )
142
126
  )
143
127
  continue
144
128
 
145
- # Check simple format: require_conditions_for (backward compatible)
146
- required_conditions = self._get_required_conditions(principal, require_conditions_for)
147
- if required_conditions:
148
- missing_conditions = self._check_required_conditions(statement, required_conditions)
149
- if missing_conditions:
150
- issues.append(
151
- ValidationIssue(
152
- severity=self.get_severity(config),
153
- issue_type="missing_principal_conditions",
154
- message=f"Principal `{principal}` requires conditions: {', '.join(f'`{c}`' for c in missing_conditions)}. "
155
- f"This principal must have these condition keys to restrict access.",
156
- statement_index=statement_idx,
157
- statement_sid=statement.sid,
158
- line_number=statement.line_number,
159
- suggestion=f"Add conditions to restrict access:\n"
160
- f"Example:\n"
161
- f'"Condition": {{\n'
162
- f' "StringEquals": {{\n'
163
- f' "{missing_conditions[0]}": "value"\n'
164
- f" }}\n"
165
- f"}}",
166
- )
167
- )
168
-
169
- # Check advanced format: principal_condition_requirements
129
+ # Check principal_condition_requirements (supports any_of/all_of/none_of)
170
130
  if principal_condition_requirements:
171
131
  condition_issues = self._validate_principal_condition_requirements(
172
132
  statement,
@@ -197,7 +157,7 @@ class PrincipalValidationCheck(PolicyCheck):
197
157
  principals.append(statement.principal)
198
158
  elif isinstance(statement.principal, dict):
199
159
  # Dict with AWS, Service, Federated, etc.
200
- for key, value in statement.principal.items():
160
+ for _, value in statement.principal.items():
201
161
  if isinstance(value, str):
202
162
  principals.append(value)
203
163
  elif isinstance(value, list):
@@ -208,7 +168,7 @@ class PrincipalValidationCheck(PolicyCheck):
208
168
  if isinstance(statement.not_principal, str):
209
169
  principals.append(statement.not_principal)
210
170
  elif isinstance(statement.not_principal, dict):
211
- for key, value in statement.not_principal.items():
171
+ for _, value in statement.not_principal.items():
212
172
  if isinstance(value, str):
213
173
  principals.append(value)
214
174
  elif isinstance(value, list):
@@ -224,13 +184,17 @@ class PrincipalValidationCheck(PolicyCheck):
224
184
  Args:
225
185
  principal: The principal to check
226
186
  blocked_list: List of blocked principal patterns
227
- service_whitelist: List of allowed service principals
187
+ service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
228
188
 
229
189
  Returns:
230
190
  True if the principal is blocked
231
191
  """
232
- # Service principals are never blocked
233
- if principal in service_whitelist:
192
+ # Check if service_whitelist contains "aws:*" (allow all AWS service principals)
193
+ if "aws:*" in service_whitelist and is_aws_service_principal(principal):
194
+ return False
195
+
196
+ # Service principals in explicit whitelist are never blocked
197
+ if is_aws_service_principal(principal) and principal in service_whitelist:
234
198
  return False
235
199
 
236
200
  # Check against blocked list (supports wildcards)
@@ -253,13 +217,17 @@ class PrincipalValidationCheck(PolicyCheck):
253
217
  Args:
254
218
  principal: The principal to check
255
219
  allowed_list: List of allowed principal patterns
256
- service_whitelist: List of allowed service principals
220
+ service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
257
221
 
258
222
  Returns:
259
223
  True if the principal is allowed
260
224
  """
261
- # Service principals are always allowed
262
- if principal in service_whitelist:
225
+ # Check if service_whitelist contains "aws:*" (allow all AWS service principals)
226
+ if "aws:*" in service_whitelist and is_aws_service_principal(principal):
227
+ return True
228
+
229
+ # Service principals in explicit whitelist are always allowed
230
+ if is_aws_service_principal(principal) and principal in service_whitelist:
263
231
  return True
264
232
 
265
233
  # Check against allowed list (supports wildcards)
@@ -274,52 +242,6 @@ class PrincipalValidationCheck(PolicyCheck):
274
242
 
275
243
  return False
276
244
 
277
- def _get_required_conditions(
278
- self, principal: str, requirements: dict[str, list[str]]
279
- ) -> list[str]:
280
- """Get required condition keys for a principal.
281
-
282
- Args:
283
- principal: The principal to check
284
- requirements: Dict mapping principal patterns to required condition keys
285
-
286
- Returns:
287
- List of required condition keys
288
- """
289
- for pattern, condition_keys in requirements.items():
290
- # Special case: "*" pattern should only match literal "*" (public access)
291
- if pattern == "*":
292
- if principal == "*":
293
- return condition_keys
294
- elif fnmatch.fnmatch(principal, pattern):
295
- return condition_keys
296
- return []
297
-
298
- def _check_required_conditions(
299
- self, statement: Statement, required_keys: list[str]
300
- ) -> list[str]:
301
- """Check if statement has required condition keys.
302
-
303
- Args:
304
- statement: The statement to check
305
- required_keys: List of required condition keys
306
-
307
- Returns:
308
- List of missing condition keys
309
- """
310
- if not statement.condition:
311
- return required_keys
312
-
313
- # Flatten all condition keys from all condition operators
314
- present_keys = set()
315
- for operator_conditions in statement.condition.values():
316
- if isinstance(operator_conditions, dict):
317
- present_keys.update(operator_conditions.keys())
318
-
319
- # Find missing keys
320
- missing = [key for key in required_keys if key not in present_keys]
321
- return missing
322
-
323
245
  def _validate_principal_condition_requirements(
324
246
  self,
325
247
  statement: Statement,
@@ -480,8 +402,8 @@ class PrincipalValidationCheck(PolicyCheck):
480
402
  statement_index=statement_idx,
481
403
  issue_type="missing_principal_condition_any_of",
482
404
  message=(
483
- f"Principals {matching_principals} require at least ONE of these conditions: "
484
- f"{', '.join(condition_keys)}"
405
+ f"`Principal`s `{', '.join(f'`{p}`' for p in matching_principals)}` require at least ONE of these conditions: "
406
+ f"{', '.join(f'`{c}`' for c in condition_keys)}"
485
407
  ),
486
408
  suggestion=self._build_any_of_suggestion(any_of),
487
409
  line_number=statement.line_number,
@@ -642,7 +564,7 @@ class PrincipalValidationCheck(PolicyCheck):
642
564
  statement_sid=statement.sid,
643
565
  statement_index=statement_idx,
644
566
  issue_type="missing_principal_condition",
645
- message=f"{message_prefix} Principal(s) {matching_principals} require condition '{condition_key}'",
567
+ message=f"{message_prefix} Principal(s) {', '.join(f'`{p}`' for p in matching_principals)} require condition `{condition_key}`",
646
568
  suggestion=suggestion_text,
647
569
  example=example_code,
648
570
  line_number=statement.line_number,
@@ -668,7 +590,7 @@ class PrincipalValidationCheck(PolicyCheck):
668
590
  Returns:
669
591
  Tuple of (suggestion_text, example_code)
670
592
  """
671
- suggestion = description if description else f"Add condition: {condition_key}"
593
+ suggestion = description if description else f"Add condition: `{condition_key}`"
672
594
 
673
595
  # Build example based on condition key type
674
596
  if example:
@@ -723,11 +645,11 @@ class PrincipalValidationCheck(PolicyCheck):
723
645
  description = cond.get("description", "")
724
646
  expected_value = cond.get("expected_value")
725
647
 
726
- option = f"\nOption {i}: {condition_key}"
648
+ option = f"\n- **Option {i}**: `{condition_key}`"
727
649
  if description:
728
650
  option += f" - {description}"
729
651
  if expected_value is not None:
730
- option += f" (value: {expected_value})"
652
+ option += f" (value: `{expected_value}`)"
731
653
 
732
654
  suggestions.append(option)
733
655
 
@@ -759,11 +681,12 @@ class PrincipalValidationCheck(PolicyCheck):
759
681
  description = condition_requirement.get("description", "")
760
682
  expected_value = condition_requirement.get("expected_value")
761
683
 
762
- message = f"FORBIDDEN: Principal(s) {matching_principals} must NOT have condition '{condition_key}'"
684
+ matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
685
+ message = f"FORBIDDEN: `Principal`s `{matching_principals_str}` must NOT have `Condition` `{condition_key}`"
763
686
  if expected_value is not None:
764
- message += f" with value '{expected_value}'"
687
+ message += f" with value `{expected_value}`"
765
688
 
766
- suggestion = f"Remove the '{condition_key}' condition from the statement"
689
+ suggestion = f"Remove the `{condition_key}` `Condition` from the statement"
767
690
  if description:
768
691
  suggestion += f". {description}"
769
692