iam-policy-validator 1.7.1__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 (51) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
  2. iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +81 -36
  6. iam_validator/checks/action_resource_matching.py +75 -37
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +7 -7
  9. iam_validator/checks/condition_type_mismatch.py +10 -8
  10. iam_validator/checks/full_wildcard.py +2 -8
  11. iam_validator/checks/mfa_condition_check.py +8 -8
  12. iam_validator/checks/policy_structure.py +577 -0
  13. iam_validator/checks/policy_type_validation.py +48 -32
  14. iam_validator/checks/principal_validation.py +86 -150
  15. iam_validator/checks/resource_validation.py +8 -8
  16. iam_validator/checks/sensitive_action.py +9 -11
  17. iam_validator/checks/service_wildcard.py +4 -10
  18. iam_validator/checks/set_operator_validation.py +11 -11
  19. iam_validator/checks/sid_uniqueness.py +8 -4
  20. iam_validator/checks/trust_policy_validation.py +512 -0
  21. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +5 -9
  24. iam_validator/checks/wildcard_resource.py +5 -9
  25. iam_validator/commands/validate.py +8 -14
  26. iam_validator/core/__init__.py +1 -2
  27. iam_validator/core/access_analyzer.py +1 -1
  28. iam_validator/core/access_analyzer_report.py +2 -2
  29. iam_validator/core/aws_fetcher.py +159 -64
  30. iam_validator/core/check_registry.py +83 -79
  31. iam_validator/core/config/condition_requirements.py +69 -17
  32. iam_validator/core/config/config_loader.py +1 -2
  33. iam_validator/core/config/defaults.py +74 -59
  34. iam_validator/core/config/service_principals.py +40 -3
  35. iam_validator/core/constants.py +57 -0
  36. iam_validator/core/formatters/console.py +10 -1
  37. iam_validator/core/formatters/csv.py +2 -1
  38. iam_validator/core/formatters/enhanced.py +42 -8
  39. iam_validator/core/formatters/markdown.py +2 -1
  40. iam_validator/core/ignore_patterns.py +297 -0
  41. iam_validator/core/models.py +35 -10
  42. iam_validator/core/policy_checks.py +34 -474
  43. iam_validator/core/policy_loader.py +98 -18
  44. iam_validator/core/report.py +65 -24
  45. iam_validator/integrations/github_integration.py +4 -5
  46. iam_validator/utils/__init__.py +4 -0
  47. iam_validator/utils/terminal.py +22 -0
  48. iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
  49. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  50. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  51. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -30,6 +30,10 @@ async def execute_policy(
30
30
  """
31
31
  issues = []
32
32
 
33
+ # Handle policies with no statements
34
+ if not policy.statement:
35
+ return issues
36
+
33
37
  # Check if any statement has Principal
34
38
  has_any_principal = any(
35
39
  stmt.principal is not None or stmt.not_principal is not None for stmt in policy.statement
@@ -37,24 +41,36 @@ async def execute_policy(
37
41
 
38
42
  # If policy has Principal but type is IDENTITY_POLICY (default), provide helpful info
39
43
  if has_any_principal and policy_type == "IDENTITY_POLICY":
44
+ # Check if it's a trust policy
45
+ from iam_validator.checks.policy_structure import is_trust_policy
46
+
47
+ if is_trust_policy(policy):
48
+ hint_msg = (
49
+ "Policy contains assume role actions - this is a TRUST POLICY. "
50
+ "Use --policy-type TRUST_POLICY for proper validation (suppresses missing Resource warnings, "
51
+ "enables trust-specific validation)"
52
+ )
53
+ suggestion_msg = "iam-validator validate --path <file> --policy-type TRUST_POLICY"
54
+ else:
55
+ hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use --policy-type RESOURCE_POLICY"
56
+ suggestion_msg = "iam-validator validate --path <file> --policy-type RESOURCE_POLICY"
57
+
40
58
  issues.append(
41
59
  ValidationIssue(
42
60
  severity="info",
43
61
  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.",
62
+ message=hint_msg,
46
63
  statement_index=0,
47
64
  statement_sid=None,
48
65
  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",
66
+ suggestion=suggestion_msg,
51
67
  )
52
68
  )
53
69
  # Don't run further checks if we're just hinting
54
70
  return issues
55
71
 
56
- # Resource policies MUST have Principal
57
- if policy_type == "RESOURCE_POLICY":
72
+ # Resource policies and Trust policies MUST have Principal
73
+ if policy_type in ("RESOURCE_POLICY", "TRUST_POLICY"):
58
74
  for idx, statement in enumerate(policy.statement):
59
75
  has_principal = statement.principal is not None or statement.not_principal is not None
60
76
 
@@ -63,13 +79,13 @@ async def execute_policy(
63
79
  ValidationIssue(
64
80
  severity="error",
65
81
  issue_type="missing_principal",
66
- message="Resource policy statement missing required Principal element. "
82
+ message="Resource policy statement missing required `Principal` element. "
67
83
  "Resource-based policies (S3 bucket policies, SNS topic policies, etc.) "
68
- "must include a Principal element to specify who can access the resource.",
84
+ "must include a `Principal` element to specify who can access the resource.",
69
85
  statement_index=idx,
70
86
  statement_sid=statement.sid,
71
87
  line_number=statement.line_number,
72
- suggestion="Add a Principal element to specify who can access this resource.\n"
88
+ suggestion="Add a `Principal` element to specify who can access this resource.\n"
73
89
  "Example:\n"
74
90
  "```json\n"
75
91
  "{\n"
@@ -94,14 +110,14 @@ async def execute_policy(
94
110
  ValidationIssue(
95
111
  severity="warning",
96
112
  issue_type="unexpected_principal",
97
- message="Identity policy should not contain Principal element. "
113
+ message="Identity policy should not contain `Principal` element. "
98
114
  "Identity-based policies (attached to IAM users, groups, or roles) "
99
- "do not need a Principal element because the principal is implicit "
115
+ "do not need a `Principal` element because the principal is implicit "
100
116
  "(the entity the policy is attached to).",
101
117
  statement_index=idx,
102
118
  statement_sid=statement.sid,
103
119
  line_number=statement.line_number,
104
- suggestion="Remove the Principal element from this identity policy statement.\n"
120
+ suggestion="Remove the `Principal` element from this identity policy statement.\n"
105
121
  "Example:\n"
106
122
  "```json\n"
107
123
  "{\n"
@@ -123,13 +139,13 @@ async def execute_policy(
123
139
  ValidationIssue(
124
140
  severity="error",
125
141
  issue_type="invalid_principal",
126
- message="Service Control Policy must not contain Principal element. "
142
+ message="Service Control Policy must not contain `Principal` element. "
127
143
  "Service Control Policies (SCPs) in AWS Organizations do not support "
128
- "the Principal element. They apply to all principals in the organization or OU.",
144
+ "the `Principal` element. They apply to all principals in the organization or OU.",
129
145
  statement_index=idx,
130
146
  statement_sid=statement.sid,
131
147
  line_number=statement.line_number,
132
- suggestion="Remove the Principal element from this SCP statement.\n"
148
+ suggestion="Remove the `Principal` element from this SCP statement.\n"
133
149
  "Example:\n"
134
150
  "```json\n"
135
151
  "{\n"
@@ -177,7 +193,7 @@ async def execute_policy(
177
193
  ValidationIssue(
178
194
  severity="error",
179
195
  issue_type="invalid_rcp_not_principal",
180
- message="Resource Control Policy must not contain NotPrincipal element. "
196
+ message="Resource Control Policy must not contain `NotPrincipal` element. "
181
197
  "RCPs only support Principal with value '*'. Use Condition elements "
182
198
  "to restrict specific principals.",
183
199
  statement_index=idx,
@@ -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,13 +250,13 @@ 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 "
259
+ suggestion="Replace `*` with service-specific actions from supported "
244
260
  f"services: {', '.join(sorted(rcp_supported_services))}",
245
261
  )
246
262
  )
@@ -275,12 +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 "
299
+ suggestion="Replace `NotAction` with `Action` element listing the specific "
284
300
  "actions to deny.",
285
301
  )
286
302
  )
@@ -294,11 +310,11 @@ async def execute_policy(
294
310
  ValidationIssue(
295
311
  severity="error",
296
312
  issue_type="missing_rcp_resource",
297
- message="Resource Control Policy statement must have Resource or NotResource element.",
313
+ message="Resource Control Policy statement must have `Resource` or `NotResource` element.",
298
314
  statement_index=idx,
299
315
  statement_sid=statement.sid,
300
316
  line_number=statement.line_number,
301
- suggestion='Add Resource: "*" or specify specific resource ARNs.',
317
+ suggestion='Add `Resource: "*"` or specify specific resource ARNs.',
302
318
  )
303
319
  )
304
320
 
@@ -4,36 +4,34 @@ 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
@@ -41,6 +39,7 @@ from typing import Any
41
39
 
42
40
  from iam_validator.core.aws_fetcher 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
 
@@ -83,22 +82,13 @@ class PrincipalValidationCheck(PolicyCheck):
83
82
  if statement.principal is None and statement.not_principal is None:
84
83
  return issues
85
84
 
86
- # Get configuration
85
+ # Get configuration (defaults match defaults.py)
87
86
  blocked_principals = config.config.get("blocked_principals", ["*"])
88
87
  allowed_principals = config.config.get("allowed_principals", [])
89
- require_conditions_for = config.config.get("require_conditions_for", {})
90
88
  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
- )
89
+ # Default: "aws:*" allows ALL AWS service principals (*.amazonaws.com)
90
+ # This matches the default in defaults.py:251
91
+ allowed_service_principals = config.config.get("allowed_service_principals", ["aws:*"])
102
92
 
103
93
  # Extract principals from statement
104
94
  principals = self._extract_principals(statement)
@@ -112,13 +102,13 @@ class PrincipalValidationCheck(PolicyCheck):
112
102
  ValidationIssue(
113
103
  severity=self.get_severity(config),
114
104
  issue_type="blocked_principal",
115
- message=f"Blocked principal detected: {principal}. "
105
+ message=f"Blocked principal detected: `{principal}`. "
116
106
  f"This principal is explicitly blocked by your security policy.",
117
107
  statement_index=statement_idx,
118
108
  statement_sid=statement.sid,
119
109
  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.",
110
+ suggestion=f"Remove the `Principal` `{principal}` or add appropriate `Condition`s to restrict access. "
111
+ "Consider using more specific `Principal`s instead of `*` (wildcard).",
122
112
  )
123
113
  )
124
114
  continue
@@ -131,45 +121,25 @@ class PrincipalValidationCheck(PolicyCheck):
131
121
  ValidationIssue(
132
122
  severity=self.get_severity(config),
133
123
  issue_type="unauthorized_principal",
134
- message=f"Principal not in allowed list: {principal}. "
135
- f"Only principals in the allowed_principals whitelist are permitted.",
124
+ message=f"`Principal` not in allowed list: `{principal}`. "
125
+ f"Only principals in the `allowed_principals` whitelist are permitted.",
136
126
  statement_index=statement_idx,
137
127
  statement_sid=statement.sid,
138
128
  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.",
129
+ suggestion=f"Add `{principal}` to the `allowed_principals` list in your config, "
130
+ "or use a `Principal` that matches an allowed pattern.",
141
131
  )
142
132
  )
143
133
  continue
144
134
 
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(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
135
+ # Check principal_condition_requirements (supports any_of/all_of/none_of)
170
136
  if principal_condition_requirements:
171
137
  condition_issues = self._validate_principal_condition_requirements(
172
- statement, statement_idx, principals, principal_condition_requirements, config
138
+ statement,
139
+ statement_idx,
140
+ principals,
141
+ principal_condition_requirements,
142
+ config,
173
143
  )
174
144
  issues.extend(condition_issues)
175
145
 
@@ -193,7 +163,7 @@ class PrincipalValidationCheck(PolicyCheck):
193
163
  principals.append(statement.principal)
194
164
  elif isinstance(statement.principal, dict):
195
165
  # Dict with AWS, Service, Federated, etc.
196
- for key, value in statement.principal.items():
166
+ for _, value in statement.principal.items():
197
167
  if isinstance(value, str):
198
168
  principals.append(value)
199
169
  elif isinstance(value, list):
@@ -204,7 +174,7 @@ class PrincipalValidationCheck(PolicyCheck):
204
174
  if isinstance(statement.not_principal, str):
205
175
  principals.append(statement.not_principal)
206
176
  elif isinstance(statement.not_principal, dict):
207
- for key, value in statement.not_principal.items():
177
+ for _, value in statement.not_principal.items():
208
178
  if isinstance(value, str):
209
179
  principals.append(value)
210
180
  elif isinstance(value, list):
@@ -220,13 +190,17 @@ class PrincipalValidationCheck(PolicyCheck):
220
190
  Args:
221
191
  principal: The principal to check
222
192
  blocked_list: List of blocked principal patterns
223
- service_whitelist: List of allowed service principals
193
+ service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
224
194
 
225
195
  Returns:
226
196
  True if the principal is blocked
227
197
  """
228
- # Service principals are never blocked
229
- if principal in service_whitelist:
198
+ # Check if service_whitelist contains "aws:*" (allow all AWS service principals)
199
+ if "aws:*" in service_whitelist and is_aws_service_principal(principal):
200
+ return False
201
+
202
+ # Service principals in explicit whitelist are never blocked
203
+ if is_aws_service_principal(principal) and principal in service_whitelist:
230
204
  return False
231
205
 
232
206
  # Check against blocked list (supports wildcards)
@@ -249,13 +223,17 @@ class PrincipalValidationCheck(PolicyCheck):
249
223
  Args:
250
224
  principal: The principal to check
251
225
  allowed_list: List of allowed principal patterns
252
- service_whitelist: List of allowed service principals
226
+ service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
253
227
 
254
228
  Returns:
255
229
  True if the principal is allowed
256
230
  """
257
- # Service principals are always allowed
258
- if principal in service_whitelist:
231
+ # Check if service_whitelist contains "aws:*" (allow all AWS service principals)
232
+ if "aws:*" in service_whitelist and is_aws_service_principal(principal):
233
+ return True
234
+
235
+ # Service principals in explicit whitelist are always allowed
236
+ if is_aws_service_principal(principal) and principal in service_whitelist:
259
237
  return True
260
238
 
261
239
  # Check against allowed list (supports wildcards)
@@ -270,52 +248,6 @@ class PrincipalValidationCheck(PolicyCheck):
270
248
 
271
249
  return False
272
250
 
273
- def _get_required_conditions(
274
- self, principal: str, requirements: dict[str, list[str]]
275
- ) -> list[str]:
276
- """Get required condition keys for a principal.
277
-
278
- Args:
279
- principal: The principal to check
280
- requirements: Dict mapping principal patterns to required condition keys
281
-
282
- Returns:
283
- List of required condition keys
284
- """
285
- for pattern, condition_keys in requirements.items():
286
- # Special case: "*" pattern should only match literal "*" (public access)
287
- if pattern == "*":
288
- if principal == "*":
289
- return condition_keys
290
- elif fnmatch.fnmatch(principal, pattern):
291
- return condition_keys
292
- return []
293
-
294
- def _check_required_conditions(
295
- self, statement: Statement, required_keys: list[str]
296
- ) -> list[str]:
297
- """Check if statement has required condition keys.
298
-
299
- Args:
300
- statement: The statement to check
301
- required_keys: List of required condition keys
302
-
303
- Returns:
304
- List of missing condition keys
305
- """
306
- if not statement.condition:
307
- return required_keys
308
-
309
- # Flatten all condition keys from all condition operators
310
- present_keys = set()
311
- for operator_conditions in statement.condition.values():
312
- if isinstance(operator_conditions, dict):
313
- present_keys.update(operator_conditions.keys())
314
-
315
- # Find missing keys
316
- missing = [key for key in required_keys if key not in present_keys]
317
- return missing
318
-
319
251
  def _validate_principal_condition_requirements(
320
252
  self,
321
253
  statement: Statement,
@@ -469,6 +401,7 @@ class PrincipalValidationCheck(PolicyCheck):
469
401
  # Create a combined error for any_of
470
402
  condition_keys = [cond.get("condition_key", "unknown") for cond in any_of]
471
403
  severity = requirement.get("severity", self.get_severity(config))
404
+ matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
472
405
  issues.append(
473
406
  ValidationIssue(
474
407
  severity=severity,
@@ -476,8 +409,8 @@ class PrincipalValidationCheck(PolicyCheck):
476
409
  statement_index=statement_idx,
477
410
  issue_type="missing_principal_condition_any_of",
478
411
  message=(
479
- f"Principals {matching_principals} require at least ONE of these conditions: "
480
- f"{', '.join(condition_keys)}"
412
+ f"`Principal`s `{matching_principals_str}` require at least ONE of these conditions: "
413
+ f"{', '.join(f'`{c}`' for c in condition_keys)}"
481
414
  ),
482
415
  suggestion=self._build_any_of_suggestion(any_of),
483
416
  line_number=statement.line_number,
@@ -629,15 +562,20 @@ class PrincipalValidationCheck(PolicyCheck):
629
562
  or self.get_severity(config)
630
563
  )
631
564
 
565
+ suggestion_text, example_code = self._build_condition_suggestion(
566
+ condition_key, description, example, expected_value, operator
567
+ )
568
+
569
+ matching_principals_formatted = ", ".join(f"`{p}`" for p in matching_principals)
570
+
632
571
  return ValidationIssue(
633
572
  severity=severity,
634
573
  statement_sid=statement.sid,
635
574
  statement_index=statement_idx,
636
575
  issue_type="missing_principal_condition",
637
- message=f"{message_prefix} Principal(s) {matching_principals} require condition '{condition_key}'",
638
- suggestion=self._build_condition_suggestion(
639
- condition_key, description, example, expected_value, operator
640
- ),
576
+ message=f"{message_prefix} Principal(s) {matching_principals_formatted} require condition `{condition_key}`",
577
+ suggestion=suggestion_text,
578
+ example=example_code,
641
579
  line_number=statement.line_number,
642
580
  )
643
581
 
@@ -648,8 +586,8 @@ class PrincipalValidationCheck(PolicyCheck):
648
586
  example: str,
649
587
  expected_value: Any = None,
650
588
  operator: str = "StringEquals",
651
- ) -> str:
652
- """Build a helpful suggestion for adding the missing condition.
589
+ ) -> tuple[str, str]:
590
+ """Build suggestion and example for adding the missing condition.
653
591
 
654
592
  Args:
655
593
  condition_key: The condition key
@@ -659,19 +597,16 @@ class PrincipalValidationCheck(PolicyCheck):
659
597
  operator: Condition operator
660
598
 
661
599
  Returns:
662
- Suggestion string
600
+ Tuple of (suggestion_text, example_code)
663
601
  """
664
- parts = []
665
-
666
- if description:
667
- parts.append(description)
602
+ suggestion = description if description else f"Add condition: {condition_key}"
668
603
 
669
604
  # Build example based on condition key type
670
605
  if example:
671
- parts.append(f"Example:\n```json\n{example}\n```")
606
+ example_code = example
672
607
  else:
673
608
  # Auto-generate example
674
- example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
609
+ example_lines = [f' "{operator}": {{']
675
610
 
676
611
  if isinstance(expected_value, list):
677
612
  value_str = (
@@ -698,9 +633,9 @@ class PrincipalValidationCheck(PolicyCheck):
698
633
  example_lines.append(f' "{condition_key}": {value_str}')
699
634
  example_lines.append(" }")
700
635
 
701
- parts.append("\n".join(example_lines))
636
+ example_code = "\n".join(example_lines)
702
637
 
703
- return ". ".join(parts) if parts else f"Add condition: {condition_key}"
638
+ return suggestion, example_code
704
639
 
705
640
  def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
706
641
  """Build suggestion for any_of conditions.
@@ -719,11 +654,11 @@ class PrincipalValidationCheck(PolicyCheck):
719
654
  description = cond.get("description", "")
720
655
  expected_value = cond.get("expected_value")
721
656
 
722
- option = f"\nOption {i}: {condition_key}"
657
+ option = f"\n- **Option {i}**: `{condition_key}`"
723
658
  if description:
724
659
  option += f" - {description}"
725
660
  if expected_value is not None:
726
- option += f" (value: {expected_value})"
661
+ option += f" (value: `{expected_value}`)"
727
662
 
728
663
  suggestions.append(option)
729
664
 
@@ -755,11 +690,12 @@ class PrincipalValidationCheck(PolicyCheck):
755
690
  description = condition_requirement.get("description", "")
756
691
  expected_value = condition_requirement.get("expected_value")
757
692
 
758
- message = f"FORBIDDEN: Principal(s) {matching_principals} must NOT have condition '{condition_key}'"
693
+ matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
694
+ message = f"FORBIDDEN: `Principal`s `{matching_principals_str}` must NOT have `Condition` `{condition_key}`"
759
695
  if expected_value is not None:
760
- message += f" with value '{expected_value}'"
696
+ message += f" with value `{expected_value}`"
761
697
 
762
- suggestion = f"Remove the '{condition_key}' condition from the statement"
698
+ suggestion = f"Remove the `{condition_key}` `Condition` from the statement"
763
699
  if description:
764
700
  suggestion += f". {description}"
765
701
 
@@ -75,7 +75,7 @@ class ResourceValidationCheck(PolicyCheck):
75
75
  issue_type="invalid_resource",
76
76
  message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
77
77
  resource=resource[:100] + "...",
78
- suggestion="ARN is too long and may be invalid",
78
+ suggestion="`ARN` is too long and may be invalid",
79
79
  line_number=line_number,
80
80
  )
81
81
  )
@@ -101,9 +101,9 @@ class ResourceValidationCheck(PolicyCheck):
101
101
  statement_sid=statement_sid,
102
102
  statement_index=statement_idx,
103
103
  issue_type="invalid_resource",
104
- message=f"Invalid ARN format even after normalizing template variables: {resource}",
104
+ message=f"Invalid `ARN` format even after normalizing template variables: `{resource}`",
105
105
  resource=resource,
106
- suggestion="ARN should follow format: arn:partition:service:region:account-id:resource (template variables like ${aws_account_id} are supported)",
106
+ suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource` (template variables like `${aws_account_id}` are supported)",
107
107
  line_number=line_number,
108
108
  )
109
109
  )
@@ -114,13 +114,13 @@ class ResourceValidationCheck(PolicyCheck):
114
114
  statement_sid=statement_sid,
115
115
  statement_index=statement_idx,
116
116
  issue_type="invalid_resource",
117
- message=f"Invalid ARN format: {resource}",
117
+ message=f"Invalid `ARN` format: `{resource}`",
118
118
  resource=resource,
119
- suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
119
+ suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource`",
120
120
  line_number=line_number,
121
121
  )
122
122
  )
123
- except Exception:
123
+ except Exception: # pylint: disable=broad-exception-caught
124
124
  # If regex matching fails (shouldn't happen with length check), treat as invalid
125
125
  issues.append(
126
126
  ValidationIssue(
@@ -128,9 +128,9 @@ class ResourceValidationCheck(PolicyCheck):
128
128
  statement_sid=statement_sid,
129
129
  statement_index=statement_idx,
130
130
  issue_type="invalid_resource",
131
- message=f"Could not validate ARN format: {resource}",
131
+ message=f"Could not validate `ARN` format: `{resource}`",
132
132
  resource=resource,
133
- suggestion="ARN validation failed - may contain unexpected characters",
133
+ suggestion="`ARN` validation failed - may contain unexpected characters",
134
134
  line_number=line_number,
135
135
  )
136
136
  )