iam-policy-validator 1.7.0__py3-none-any.whl → 1.7.2__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/METADATA +428 -0
  2. {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +37 -36
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/action_condition_enforcement.py +22 -13
  5. iam_validator/checks/action_resource_matching.py +70 -36
  6. iam_validator/checks/condition_key_validation.py +7 -7
  7. iam_validator/checks/condition_type_mismatch.py +8 -6
  8. iam_validator/checks/full_wildcard.py +2 -8
  9. iam_validator/checks/mfa_condition_check.py +8 -8
  10. iam_validator/checks/principal_validation.py +24 -20
  11. iam_validator/checks/sensitive_action.py +3 -9
  12. iam_validator/checks/service_wildcard.py +2 -8
  13. iam_validator/checks/sid_uniqueness.py +1 -1
  14. iam_validator/checks/utils/sensitive_action_matcher.py +1 -2
  15. iam_validator/checks/utils/wildcard_expansion.py +1 -2
  16. iam_validator/checks/wildcard_action.py +2 -8
  17. iam_validator/checks/wildcard_resource.py +2 -8
  18. iam_validator/commands/validate.py +2 -2
  19. iam_validator/core/aws_fetcher.py +115 -22
  20. iam_validator/core/config/config_loader.py +1 -2
  21. iam_validator/core/config/defaults.py +16 -7
  22. iam_validator/core/constants.py +57 -0
  23. iam_validator/core/formatters/console.py +10 -1
  24. iam_validator/core/formatters/csv.py +2 -1
  25. iam_validator/core/formatters/enhanced.py +42 -8
  26. iam_validator/core/formatters/markdown.py +2 -1
  27. iam_validator/core/models.py +22 -7
  28. iam_validator/core/policy_checks.py +5 -4
  29. iam_validator/core/policy_loader.py +71 -14
  30. iam_validator/core/report.py +65 -24
  31. iam_validator/integrations/github_integration.py +4 -5
  32. iam_validator/utils/__init__.py +4 -0
  33. iam_validator/utils/regex.py +7 -8
  34. iam_validator/utils/terminal.py +22 -0
  35. iam_policy_validator-1.7.0.dist-info/METADATA +0 -1057
  36. {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
  37. {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
  38. {iam_policy_validator-1.7.0.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,8 @@ Example:
21
21
  This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
22
22
  """
23
23
 
24
+ import re
25
+
24
26
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
25
27
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
26
28
  from iam_validator.core.models import Statement, ValidationIssue
@@ -231,18 +233,23 @@ class ActionResourceMatchingCheck(PolicyCheck):
231
233
  if reason:
232
234
  message = reason
233
235
  elif all_required_formats and len(all_required_formats) > 1:
234
- types = ", ".join(f["type"] for f in all_required_formats)
236
+ types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
235
237
  message = (
236
- f"No resources match for action '{action}'. This action requires one of: {types}"
238
+ f"No resources match for action `{action}`. This action requires one of: {types}"
237
239
  )
238
240
  else:
239
241
  message = (
240
- f"No resources match for action '{action}'. "
241
- f"This action requires resource type: {required_type}"
242
+ f"No resources match for action `{action}`. "
243
+ f"This action requires resource type: `{required_type}`"
242
244
  )
243
245
 
244
246
  # Build suggestion with examples
245
- suggestion = self._get_suggestion(action, required_format, provided_resources)
247
+ suggestion = self._get_suggestion(
248
+ action=action,
249
+ required_format=required_format,
250
+ provided_resources=provided_resources,
251
+ all_required_formats=all_required_formats,
252
+ )
246
253
 
247
254
  return ValidationIssue(
248
255
  severity=self.get_severity(config),
@@ -265,6 +272,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
265
272
  action: str,
266
273
  required_format: str,
267
274
  provided_resources: list[str],
275
+ all_required_formats: list[dict] | None = None,
268
276
  ) -> str:
269
277
  """
270
278
  Generate helpful suggestion for fixing the mismatch.
@@ -281,44 +289,75 @@ class ActionResourceMatchingCheck(PolicyCheck):
281
289
  # Special case: Wildcard resource
282
290
  if required_format == "*":
283
291
  return (
284
- f'Action {action} can only use Resource: "*" (wildcard).\n'
292
+ f'Action `{action}` can only use Resource: "*" (wildcard).\n'
285
293
  f" This action does not support resource-level permissions.\n"
286
294
  f' Example: "Resource": "*"'
287
295
  )
288
296
 
289
- # Extract resource type from ARN pattern
290
- # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
291
- # Examples:
292
- # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
293
- # arn:${Partition}:iam::${Account}:user/${UserName} -> user
294
- resource_type = self._extract_resource_type_from_pattern(required_format)
295
-
296
- # Build service-specific suggestion
297
+ # Build service-specific suggestion with proper markdown formatting
297
298
  suggestion_parts = []
298
299
 
299
- # Add action description
300
- suggestion_parts.append(f"Action {action} requires {resource_type} resource type.")
300
+ # If multiple resource types are valid, show all of them
301
+ if all_required_formats and len(all_required_formats) > 1:
302
+ resource_types = [fmt["type"] for fmt in all_required_formats]
303
+ suggestion_parts.append(
304
+ f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
305
+ )
306
+ suggestion_parts.append("")
301
307
 
302
- # Add expected format
303
- suggestion_parts.append(f" Expected format: {required_format}")
308
+ # Show format and example for each resource type
309
+ for fmt in all_required_formats:
310
+ resource_type = fmt["type"]
311
+ arn_format = fmt["format"]
304
312
 
305
- # Add practical example based on the pattern
306
- example = self._generate_example_arn(required_format)
307
- if example:
308
- suggestion_parts.append(f" Example: {example}")
313
+ suggestion_parts.append(
314
+ f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
315
+ )
316
+ suggestion_parts.append("```")
317
+ suggestion_parts.append(arn_format)
318
+ suggestion_parts.append("```")
309
319
 
310
- # Add helpful context for common patterns
311
- context = self._get_resource_context(action_name, resource_type, required_format)
312
- if context:
313
- suggestion_parts.append(f" {context}")
320
+ # Add practical example
321
+ example = self._generate_example_arn(arn_format)
322
+ if example:
323
+ suggestion_parts.append(f"Example: `{example}`")
314
324
 
315
- suggestion = "\n".join(suggestion_parts)
325
+ suggestion_parts.append("")
326
+ else:
327
+ # Single resource type - show detailed info
328
+ # Extract resource type from ARN pattern
329
+ # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
330
+ # Examples:
331
+ # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
332
+ # arn:${Partition}:iam::${Account}:user/${UserName} -> user
333
+ resource_type = self._extract_resource_type_from_pattern(required_format)
334
+
335
+ # Add action description
336
+ suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
337
+ suggestion_parts.append("")
338
+
339
+ # Add expected format in code block
340
+ suggestion_parts.append("**Expected format:**")
341
+ suggestion_parts.append(f"```\n{required_format}\n```")
342
+
343
+ # Add practical example based on the pattern
344
+ example = self._generate_example_arn(required_format)
345
+ if example:
346
+ suggestion_parts.append("**Example:**")
347
+ suggestion_parts.append(f"```\n{example}\n```")
348
+
349
+ # Add helpful context for common patterns
350
+ context = self._get_resource_context(action_name, resource_type, required_format)
351
+ if context:
352
+ suggestion_parts.append(f"**Note:** {context}")
316
353
 
317
354
  # Add current resources to help user understand the mismatch
318
355
  if provided_resources and len(provided_resources) <= 3:
319
- current = ", ".join(provided_resources)
320
- suggestion += f"\n Current resources: {current}"
356
+ suggestion_parts.append("**Current resources:**")
357
+ for resource in provided_resources:
358
+ suggestion_parts.append(f"- `{resource}`")
321
359
 
360
+ suggestion = "\n".join(suggestion_parts)
322
361
  return suggestion
323
362
 
324
363
  def _extract_resource_type_from_pattern(self, pattern: str) -> str:
@@ -340,17 +379,14 @@ class ActionResourceMatchingCheck(PolicyCheck):
340
379
 
341
380
  # Extract resource type (part before / or entire string)
342
381
  if "/" in resource_part:
343
- resource_type = resource_part.split("/")[0]
382
+ resource_type = resource_part.split("/", maxsplit=1)[0]
344
383
  elif ":" in resource_part:
345
- resource_type = resource_part.split(":")[0]
384
+ resource_type = resource_part.split(":", maxsplit=1)[0]
346
385
  else:
347
386
  resource_type = resource_part
348
387
 
349
388
  # Remove template variables like ${...}
350
- import re
351
-
352
389
  resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
353
-
354
390
  return resource_type.strip() or "resource"
355
391
 
356
392
  def _generate_example_arn(self, pattern: str) -> str:
@@ -359,8 +395,6 @@ class ActionResourceMatchingCheck(PolicyCheck):
359
395
 
360
396
  Converts AWS template variables to realistic examples.
361
397
  """
362
- import re
363
-
364
398
  example = pattern
365
399
 
366
400
  # Common substitutions
@@ -52,26 +52,26 @@ class ConditionKeyValidationCheck(PolicyCheck):
52
52
  continue
53
53
 
54
54
  # Validate against action and resource types
55
- is_valid, error_msg, warning_msg = await fetcher.validate_condition_key(
56
- action, condition_key, resources
57
- )
55
+ result = await fetcher.validate_condition_key(action, condition_key, resources)
58
56
 
59
- if not is_valid:
57
+ if not result.is_valid:
60
58
  issues.append(
61
59
  ValidationIssue(
62
60
  severity=self.get_severity(config),
63
61
  statement_sid=statement_sid,
64
62
  statement_index=statement_idx,
65
63
  issue_type="invalid_condition_key",
66
- message=error_msg or f"Invalid condition key: {condition_key}",
64
+ message=result.error_message
65
+ or f"Invalid condition key: {condition_key}",
67
66
  action=action,
68
67
  condition_key=condition_key,
69
68
  line_number=line_number,
69
+ suggestion=result.suggestion,
70
70
  )
71
71
  )
72
72
  # Only report once per condition key (not per action)
73
73
  break
74
- elif warning_msg and warn_on_global_keys:
74
+ elif result.warning_message and warn_on_global_keys:
75
75
  # Add warning for global condition keys with action-specific keys
76
76
  # Only if warn_on_global_condition_keys is enabled
77
77
  issues.append(
@@ -80,7 +80,7 @@ class ConditionKeyValidationCheck(PolicyCheck):
80
80
  statement_sid=statement_sid,
81
81
  statement_index=statement_idx,
82
82
  issue_type="global_condition_key_with_action_specific",
83
- message=warning_msg,
83
+ message=result.warning_message,
84
84
  action=action,
85
85
  condition_key=condition_key,
86
86
  line_number=line_number,
@@ -107,8 +107,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
107
107
  ValidationIssue(
108
108
  severity="warning",
109
109
  message=(
110
- f"Type mismatch (usable but not recommended): Operator '{operator}' expects "
111
- f"{operator_type} values, but condition key '{condition_key}' is type {key_type}. "
110
+ f"Type mismatch (usable but not recommended): Operator `{operator}` expects "
111
+ f"{operator_type} values, but condition key `{condition_key}` is type {key_type}. "
112
112
  f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
113
113
  ),
114
114
  statement_sid=statement_sid,
@@ -123,8 +123,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
123
123
  ValidationIssue(
124
124
  severity=self.get_severity(config),
125
125
  message=(
126
- f"Type mismatch: Operator '{operator}' expects {operator_type} values, "
127
- f"but condition key '{condition_key}' is type {key_type}."
126
+ f"Type mismatch: Operator `{operator}` expects {operator_type} values, "
127
+ f"but condition key `{condition_key}` is type {key_type}."
128
128
  ),
129
129
  statement_sid=statement_sid,
130
130
  statement_index=statement_idx,
@@ -141,7 +141,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
141
141
  ValidationIssue(
142
142
  severity=self.get_severity(config),
143
143
  message=(
144
- f"Invalid value format for condition key '{condition_key}': {error_msg}"
144
+ f"Invalid value format for condition key `{condition_key}`: {error_msg}"
145
145
  ),
146
146
  statement_sid=statement_sid,
147
147
  statement_index=statement_idx,
@@ -172,7 +172,9 @@ class ConditionTypeMismatchCheck(PolicyCheck):
172
172
  Returns:
173
173
  Type string or None if not found
174
174
  """
175
- from iam_validator.core.config.aws_global_conditions import get_global_conditions
175
+ from iam_validator.core.config.aws_global_conditions import (
176
+ get_global_conditions,
177
+ )
176
178
 
177
179
  # Check if it's a global condition key
178
180
  global_conditions = get_global_conditions()
@@ -43,19 +43,12 @@ class FullWildcardCheck(PolicyCheck):
43
43
  "message",
44
44
  "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
45
45
  )
46
- suggestion_text = config.config.get(
46
+ suggestion = config.config.get(
47
47
  "suggestion",
48
48
  "This grants full administrative access. Replace both wildcards with specific actions and resources to follow least-privilege principle",
49
49
  )
50
50
  example = config.config.get("example", "")
51
51
 
52
- # Combine suggestion + example
53
- suggestion = (
54
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
55
- if example
56
- else suggestion_text
57
- )
58
-
59
52
  issues.append(
60
53
  ValidationIssue(
61
54
  severity=self.get_severity(config),
@@ -64,6 +57,7 @@ class FullWildcardCheck(PolicyCheck):
64
57
  issue_type="security_risk",
65
58
  message=message,
66
59
  suggestion=suggestion,
60
+ example=example if example else None,
67
61
  line_number=statement.line_number,
68
62
  )
69
63
  )
@@ -70,11 +70,11 @@ class MFAConditionCheck(PolicyCheck):
70
70
  ValidationIssue(
71
71
  severity=self.get_severity(config),
72
72
  message=(
73
- "Dangerous MFA condition pattern detected. "
74
- 'Using {"Bool": {"aws:MultiFactorAuthPresent": "false"}} does not enforce MFA '
75
- "because aws:MultiFactorAuthPresent may not exist in the request context. "
76
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement, '
77
- "or use BoolIfExists in a Deny statement."
73
+ "**Dangerous MFA condition pattern detected.** "
74
+ 'Using `{"Bool": {"aws:MultiFactorAuthPresent": "false"}}` does not enforce MFA '
75
+ "because `aws:MultiFactorAuthPresent` may not exist in the request context. "
76
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement, '
77
+ "or use `BoolIfExists` in a Deny statement."
78
78
  ),
79
79
  statement_sid=statement_sid,
80
80
  statement_index=statement_idx,
@@ -97,10 +97,10 @@ class MFAConditionCheck(PolicyCheck):
97
97
  ValidationIssue(
98
98
  severity=self.get_severity(config),
99
99
  message=(
100
- "Dangerous MFA condition pattern detected. "
101
- 'Using {"Null": {"aws:MultiFactorAuthPresent": "false"}} only checks if the key exists, '
100
+ "**Dangerous MFA condition pattern detected.** "
101
+ 'Using `{"Null": {"aws:MultiFactorAuthPresent": "false"}}` only checks if the key exists, '
102
102
  "not whether MFA was actually used. This does not enforce MFA. "
103
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement instead.'
103
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement instead.'
104
104
  ),
105
105
  statement_sid=statement_sid,
106
106
  statement_index=statement_idx,
@@ -112,12 +112,12 @@ class PrincipalValidationCheck(PolicyCheck):
112
112
  ValidationIssue(
113
113
  severity=self.get_severity(config),
114
114
  issue_type="blocked_principal",
115
- message=f"Blocked principal detected: {principal}. "
115
+ message=f"Blocked principal detected: `{principal}`. "
116
116
  f"This principal is explicitly blocked by your security policy.",
117
117
  statement_index=statement_idx,
118
118
  statement_sid=statement.sid,
119
119
  line_number=statement.line_number,
120
- suggestion=f"Remove the principal '{principal}' or add appropriate conditions to restrict access. "
120
+ suggestion=f"Remove the principal `{principal}` or add appropriate conditions to restrict access. "
121
121
  "Consider using more specific principals instead of wildcards.",
122
122
  )
123
123
  )
@@ -131,8 +131,8 @@ class PrincipalValidationCheck(PolicyCheck):
131
131
  ValidationIssue(
132
132
  severity=self.get_severity(config),
133
133
  issue_type="unauthorized_principal",
134
- message=f"Principal not in allowed list: {principal}. "
135
- f"Only principals in the allowed_principals whitelist are permitted.",
134
+ message=f"Principal not in allowed list: `{principal}`. "
135
+ f"Only principals in the `allowed_principals` whitelist are permitted.",
136
136
  statement_index=statement_idx,
137
137
  statement_sid=statement.sid,
138
138
  line_number=statement.line_number,
@@ -151,7 +151,7 @@ class PrincipalValidationCheck(PolicyCheck):
151
151
  ValidationIssue(
152
152
  severity=self.get_severity(config),
153
153
  issue_type="missing_principal_conditions",
154
- message=f"Principal '{principal}' requires conditions: {', '.join(missing_conditions)}. "
154
+ message=f"Principal `{principal}` requires conditions: {', '.join(f'`{c}`' for c in missing_conditions)}. "
155
155
  f"This principal must have these condition keys to restrict access.",
156
156
  statement_index=statement_idx,
157
157
  statement_sid=statement.sid,
@@ -169,7 +169,11 @@ class PrincipalValidationCheck(PolicyCheck):
169
169
  # Check advanced format: principal_condition_requirements
170
170
  if principal_condition_requirements:
171
171
  condition_issues = self._validate_principal_condition_requirements(
172
- statement, statement_idx, principals, principal_condition_requirements, config
172
+ statement,
173
+ statement_idx,
174
+ principals,
175
+ principal_condition_requirements,
176
+ config,
173
177
  )
174
178
  issues.extend(condition_issues)
175
179
 
@@ -629,15 +633,18 @@ class PrincipalValidationCheck(PolicyCheck):
629
633
  or self.get_severity(config)
630
634
  )
631
635
 
636
+ suggestion_text, example_code = self._build_condition_suggestion(
637
+ condition_key, description, example, expected_value, operator
638
+ )
639
+
632
640
  return ValidationIssue(
633
641
  severity=severity,
634
642
  statement_sid=statement.sid,
635
643
  statement_index=statement_idx,
636
644
  issue_type="missing_principal_condition",
637
645
  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
- ),
646
+ suggestion=suggestion_text,
647
+ example=example_code,
641
648
  line_number=statement.line_number,
642
649
  )
643
650
 
@@ -648,8 +655,8 @@ class PrincipalValidationCheck(PolicyCheck):
648
655
  example: str,
649
656
  expected_value: Any = None,
650
657
  operator: str = "StringEquals",
651
- ) -> str:
652
- """Build a helpful suggestion for adding the missing condition.
658
+ ) -> tuple[str, str]:
659
+ """Build suggestion and example for adding the missing condition.
653
660
 
654
661
  Args:
655
662
  condition_key: The condition key
@@ -659,19 +666,16 @@ class PrincipalValidationCheck(PolicyCheck):
659
666
  operator: Condition operator
660
667
 
661
668
  Returns:
662
- Suggestion string
669
+ Tuple of (suggestion_text, example_code)
663
670
  """
664
- parts = []
665
-
666
- if description:
667
- parts.append(description)
671
+ suggestion = description if description else f"Add condition: {condition_key}"
668
672
 
669
673
  # Build example based on condition key type
670
674
  if example:
671
- parts.append(f"Example:\n```json\n{example}\n```")
675
+ example_code = example
672
676
  else:
673
677
  # Auto-generate example
674
- example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
678
+ example_lines = [f' "{operator}": {{']
675
679
 
676
680
  if isinstance(expected_value, list):
677
681
  value_str = (
@@ -698,9 +702,9 @@ class PrincipalValidationCheck(PolicyCheck):
698
702
  example_lines.append(f' "{condition_key}": {value_str}')
699
703
  example_lines.append(" }")
700
704
 
701
- parts.append("\n".join(example_lines))
705
+ example_code = "\n".join(example_lines)
702
706
 
703
- return ". ".join(parts) if parts else f"Add condition: {condition_key}"
707
+ return suggestion, example_code
704
708
 
705
709
  def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
706
710
  """Build suggestion for any_of conditions.
@@ -85,7 +85,7 @@ class SensitiveActionCheck(PolicyCheck):
85
85
  # Generic ABAC fallback for uncategorized actions
86
86
  return (
87
87
  "Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
88
- "• Match principal tags to resource tags (aws:PrincipalTag/X = aws:ResourceTag/X)\n"
88
+ "• Match principal tags to resource tags (aws:PrincipalTag/<tag-name> = aws:ResourceTag/<tag-name>)\n"
89
89
  "• Require MFA (aws:MultiFactorAuthPresent = true)\n"
90
90
  "• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
91
91
  '"Condition": {\n'
@@ -142,13 +142,6 @@ class SensitiveActionCheck(PolicyCheck):
142
142
  matched_actions[0], config
143
143
  )
144
144
 
145
- # Combine suggestion + example
146
- suggestion = (
147
- f"{suggestion_text}\n\nExample:\n```json\n{example}\n```"
148
- if example
149
- else suggestion_text
150
- )
151
-
152
145
  # Determine severity based on the highest severity action in the list
153
146
  # If single action, use its category severity
154
147
  # If multiple actions, use the highest severity among them
@@ -165,7 +158,8 @@ class SensitiveActionCheck(PolicyCheck):
165
158
  issue_type="missing_condition",
166
159
  message=message,
167
160
  action=(matched_actions[0] if len(matched_actions) == 1 else None),
168
- suggestion=suggestion,
161
+ suggestion=suggestion_text,
162
+ example=example if example else None,
169
163
  line_number=statement.line_number,
170
164
  )
171
165
  )
@@ -60,20 +60,13 @@ class ServiceWildcardCheck(PolicyCheck):
60
60
  example_template = config.config.get("example", "")
61
61
 
62
62
  message = message_template.format(action=action, service=service)
63
- suggestion_text = suggestion_template.format(action=action, service=service)
63
+ suggestion = suggestion_template.format(action=action, service=service)
64
64
  example = (
65
65
  example_template.format(action=action, service=service)
66
66
  if example_template
67
67
  else ""
68
68
  )
69
69
 
70
- # Combine suggestion + example
71
- suggestion = (
72
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
73
- if example
74
- else suggestion_text
75
- )
76
-
77
70
  issues.append(
78
71
  ValidationIssue(
79
72
  severity=self.get_severity(config),
@@ -83,6 +76,7 @@ class ServiceWildcardCheck(PolicyCheck):
83
76
  message=message,
84
77
  action=action,
85
78
  suggestion=suggestion,
79
+ example=example if example else None,
86
80
  line_number=statement.line_number,
87
81
  )
88
82
  )
@@ -91,7 +91,7 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
91
91
  statement_sid=duplicate_sid,
92
92
  statement_index=idx,
93
93
  issue_type="duplicate_sid",
94
- message=f"Statement ID '{duplicate_sid}' is used {count} times in this policy (found in statements {statement_numbers})",
94
+ message=f"Statement ID `{duplicate_sid}` is used **{count} times** in this policy (found in statements {statement_numbers})",
95
95
  suggestion="Change this SID to a unique value. Statement IDs help identify and reference specific statements, so duplicates can cause confusion.",
96
96
  line_number=statement.line_number,
97
97
  )
@@ -11,7 +11,6 @@ Performance optimizations:
11
11
 
12
12
  import re
13
13
  from functools import lru_cache
14
- from re import Pattern
15
14
 
16
15
  from iam_validator.core.check_registry import CheckConfig
17
16
  from iam_validator.core.config.sensitive_actions import get_sensitive_actions
@@ -69,7 +68,7 @@ DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
69
68
 
70
69
  # Global regex pattern cache for performance
71
70
  @lru_cache(maxsize=256)
72
- def compile_pattern(pattern: str) -> Pattern[str] | None:
71
+ def compile_pattern(pattern: str) -> re.Pattern[str] | None:
73
72
  """Compile and cache regex patterns.
74
73
 
75
74
  Args:
@@ -6,7 +6,6 @@ to their actual action names using the AWS Service Reference API.
6
6
 
7
7
  import re
8
8
  from functools import lru_cache
9
- from re import Pattern
10
9
 
11
10
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
11
 
@@ -14,7 +13,7 @@ from iam_validator.core.aws_fetcher import AWSServiceFetcher
14
13
  # Global cache for compiled wildcard patterns (shared across checks)
15
14
  # Using lru_cache for O(1) pattern reuse and 20-30x performance improvement
16
15
  @lru_cache(maxsize=512)
17
- def compile_wildcard_pattern(pattern: str) -> Pattern[str]:
16
+ def compile_wildcard_pattern(pattern: str) -> re.Pattern[str]:
18
17
  """Compile and cache wildcard patterns for O(1) reuse.
19
18
 
20
19
  Args:
@@ -39,19 +39,12 @@ class WildcardActionCheck(PolicyCheck):
39
39
  # Check for wildcard action (Action: "*")
40
40
  if "*" in actions:
41
41
  message = config.config.get("message", "Statement allows all actions (*)")
42
- suggestion_text = config.config.get(
42
+ suggestion = config.config.get(
43
43
  "suggestion",
44
44
  "Replace wildcard with specific actions needed for your use case",
45
45
  )
46
46
  example = config.config.get("example", "")
47
47
 
48
- # Combine suggestion + example
49
- suggestion = (
50
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
51
- if example
52
- else suggestion_text
53
- )
54
-
55
48
  issues.append(
56
49
  ValidationIssue(
57
50
  severity=self.get_severity(config),
@@ -60,6 +53,7 @@ class WildcardActionCheck(PolicyCheck):
60
53
  issue_type="overly_permissive",
61
54
  message=message,
62
55
  suggestion=suggestion,
56
+ example=example if example else None,
63
57
  line_number=statement.line_number,
64
58
  )
65
59
  )
@@ -63,18 +63,11 @@ class WildcardResourceCheck(PolicyCheck):
63
63
 
64
64
  # Flag the issue if actions are not all allowed or no allowed_wildcards configured
65
65
  message = config.config.get("message", "Statement applies to all resources (*)")
66
- suggestion_text = config.config.get(
66
+ suggestion = config.config.get(
67
67
  "suggestion", "Replace wildcard with specific resource ARNs"
68
68
  )
69
69
  example = config.config.get("example", "")
70
70
 
71
- # Combine suggestion + example
72
- suggestion = (
73
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
74
- if example
75
- else suggestion_text
76
- )
77
-
78
71
  issues.append(
79
72
  ValidationIssue(
80
73
  severity=self.get_severity(config),
@@ -83,6 +76,7 @@ class WildcardResourceCheck(PolicyCheck):
83
76
  issue_type="overly_permissive",
84
77
  message=message,
85
78
  suggestion=suggestion,
79
+ example=example if example else None,
86
80
  line_number=statement.line_number,
87
81
  )
88
82
  )
@@ -254,9 +254,9 @@ Examples:
254
254
  policy_type=policy_type,
255
255
  )
256
256
 
257
- # Generate report
257
+ # Generate report (include parsing errors if any)
258
258
  generator = ReportGenerator()
259
- report = generator.generate_report(results)
259
+ report = generator.generate_report(results, parsing_errors=loader.parsing_errors)
260
260
 
261
261
  # Output results
262
262
  if args.format is None: