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
@@ -27,11 +27,13 @@ import os
27
27
  import re
28
28
  import sys
29
29
  import time
30
+ from dataclasses import dataclass
30
31
  from pathlib import Path
31
32
  from typing import Any
32
33
 
33
34
  import httpx
34
35
 
36
+ from iam_validator.core import constants
35
37
  from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
36
38
  from iam_validator.core.models import ServiceDetail, ServiceInfo
37
39
  from iam_validator.utils.cache import LRUCache
@@ -39,6 +41,23 @@ from iam_validator.utils.cache import LRUCache
39
41
  logger = logging.getLogger(__name__)
40
42
 
41
43
 
44
+ @dataclass
45
+ class ConditionKeyValidationResult:
46
+ """Result of condition key validation.
47
+
48
+ Attributes:
49
+ is_valid: True if the condition key is valid for the action
50
+ error_message: Short error message if invalid (shown prominently)
51
+ warning_message: Warning message if valid but not recommended
52
+ suggestion: Detailed suggestion with valid keys (shown in collapsible section)
53
+ """
54
+
55
+ is_valid: bool
56
+ error_message: str | None = None
57
+ warning_message: str | None = None
58
+ suggestion: str | None = None
59
+
60
+
42
61
  class CompiledPatterns:
43
62
  """Pre-compiled regex patterns for validation.
44
63
 
@@ -199,10 +218,10 @@ class AWSServiceFetcher:
199
218
 
200
219
  def __init__(
201
220
  self,
202
- timeout: float = 30.0,
221
+ timeout: float = constants.DEFAULT_HTTP_TIMEOUT_SECONDS,
203
222
  retries: int = 3,
204
223
  enable_cache: bool = True,
205
- cache_ttl: int = 604800,
224
+ cache_ttl: int = constants.DEFAULT_CACHE_TTL_SECONDS,
206
225
  memory_cache_size: int = 256,
207
226
  connection_pool_size: int = 50,
208
227
  keepalive_connections: int = 20,
@@ -303,7 +322,7 @@ class AWSServiceFetcher:
303
322
  limits=httpx.Limits(
304
323
  max_keepalive_connections=self.keepalive_connections,
305
324
  max_connections=self.connection_pool_size,
306
- keepalive_expiry=30.0, # Keep connections alive for 30 seconds
325
+ keepalive_expiry=constants.DEFAULT_HTTP_TIMEOUT_SECONDS, # Keep connections alive
307
326
  ),
308
327
  http2=True, # Enable HTTP/2 for multiplexing
309
328
  )
@@ -658,7 +677,7 @@ class AWSServiceFetcher:
658
677
  return service_detail
659
678
  except FileNotFoundError:
660
679
  pass
661
- raise ValueError(f"Service '{service_name}' not found in {self.aws_services_dir}")
680
+ raise ValueError(f"Service `{service_name}` not found in {self.aws_services_dir}")
662
681
 
663
682
  # Fetch service list and find URL from API
664
683
  services = await self.fetch_services()
@@ -676,7 +695,7 @@ class AWSServiceFetcher:
676
695
 
677
696
  return service_detail
678
697
 
679
- raise ValueError(f"Service '{service_name}' not found")
698
+ raise ValueError(f"Service `{service_name}` not found")
680
699
 
681
700
  async def fetch_multiple_services(self, service_names: list[str]) -> dict[str, ServiceDetail]:
682
701
  """Fetch multiple services concurrently with optimized batching."""
@@ -823,7 +842,7 @@ class AWSServiceFetcher:
823
842
 
824
843
  async def validate_condition_key(
825
844
  self, action: str, condition_key: str, resources: list[str] | None = None
826
- ) -> tuple[bool, str | None, str | None]:
845
+ ) -> ConditionKeyValidationResult:
827
846
  """
828
847
  Validate condition key against action and optionally resource types.
829
848
 
@@ -833,10 +852,11 @@ class AWSServiceFetcher:
833
852
  resources: Optional list of resource ARNs to validate against
834
853
 
835
854
  Returns:
836
- Tuple of (is_valid, error_message, warning_message)
855
+ ConditionKeyValidationResult with:
837
856
  - is_valid: True if key is valid (even with warning)
838
- - error_message: Error message if invalid (is_valid=False)
857
+ - error_message: Short error message if invalid (shown prominently)
839
858
  - warning_message: Warning message if valid but not recommended
859
+ - suggestion: Detailed suggestion with valid keys (shown in collapsible section)
840
860
  """
841
861
  try:
842
862
  from iam_validator.core.config.aws_global_conditions import (
@@ -852,10 +872,9 @@ class AWSServiceFetcher:
852
872
  if global_conditions.is_valid_global_key(condition_key):
853
873
  is_global_key = True
854
874
  else:
855
- return (
856
- False,
857
- f"Invalid AWS global condition key: '{condition_key}'.",
858
- None,
875
+ return ConditionKeyValidationResult(
876
+ is_valid=False,
877
+ error_message=f"Invalid AWS global condition key: `{condition_key}`.",
859
878
  )
860
879
 
861
880
  # Fetch service detail (cached)
@@ -863,7 +882,7 @@ class AWSServiceFetcher:
863
882
 
864
883
  # Check service-specific condition keys
865
884
  if condition_key in service_detail.condition_keys:
866
- return True, None, None
885
+ return ConditionKeyValidationResult(is_valid=True)
867
886
 
868
887
  # Check action-specific condition keys
869
888
  if action_name in service_detail.actions:
@@ -872,7 +891,7 @@ class AWSServiceFetcher:
872
891
  action_detail.action_condition_keys
873
892
  and condition_key in action_detail.action_condition_keys
874
893
  ):
875
- return True, None, None
894
+ return ConditionKeyValidationResult(is_valid=True)
876
895
 
877
896
  # Check resource-specific condition keys
878
897
  # Get resource types required by this action
@@ -886,7 +905,7 @@ class AWSServiceFetcher:
886
905
  resource_type = service_detail.resources.get(resource_name)
887
906
  if resource_type and resource_type.condition_keys:
888
907
  if condition_key in resource_type.condition_keys:
889
- return True, None, None
908
+ return ConditionKeyValidationResult(is_valid=True)
890
909
 
891
910
  # If it's a global key but the action has specific condition keys defined,
892
911
  # AWS allows it but the key may not be available in every request context
@@ -898,21 +917,95 @@ class AWSServiceFetcher:
898
917
  f"Verify that '{condition_key}' is available for this specific action's request context. "
899
918
  f"Consider using '*IfExists' operators (e.g., StringEqualsIfExists) if the key might be missing."
900
919
  )
901
- return True, None, warning_msg
920
+ return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
902
921
 
903
922
  # If it's a global key and action doesn't define specific keys, allow it
904
923
  if is_global_key:
905
- return True, None, None
924
+ return ConditionKeyValidationResult(is_valid=True)
925
+
926
+ # Short error message
927
+ error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
928
+
929
+ # Collect valid condition keys for this action
930
+ valid_keys = set()
906
931
 
907
- return (
908
- False,
909
- f"Condition key '{condition_key}' is not valid for action '{action}'",
910
- None,
932
+ # Add service-level condition keys
933
+ if service_detail.condition_keys:
934
+ if isinstance(service_detail.condition_keys, dict):
935
+ valid_keys.update(service_detail.condition_keys.keys())
936
+ elif isinstance(service_detail.condition_keys, list):
937
+ valid_keys.update(service_detail.condition_keys)
938
+
939
+ # Add action-specific condition keys
940
+ if action_name in service_detail.actions:
941
+ action_detail = service_detail.actions[action_name]
942
+ if action_detail.action_condition_keys:
943
+ if isinstance(action_detail.action_condition_keys, dict):
944
+ valid_keys.update(action_detail.action_condition_keys.keys())
945
+ elif isinstance(action_detail.action_condition_keys, list):
946
+ valid_keys.update(action_detail.action_condition_keys)
947
+
948
+ # Add resource-specific condition keys
949
+ if action_detail.resources:
950
+ for res_req in action_detail.resources:
951
+ resource_name = res_req.get("Name", "")
952
+ if resource_name:
953
+ resource_type = service_detail.resources.get(resource_name)
954
+ if resource_type and resource_type.condition_keys:
955
+ if isinstance(resource_type.condition_keys, dict):
956
+ valid_keys.update(resource_type.condition_keys.keys())
957
+ elif isinstance(resource_type.condition_keys, list):
958
+ valid_keys.update(resource_type.condition_keys)
959
+
960
+ # Build detailed suggestion with valid keys (goes in collapsible section)
961
+ suggestion_parts = []
962
+
963
+ if valid_keys:
964
+ # Sort and limit to first 10 keys for readability
965
+ sorted_keys = sorted(valid_keys)
966
+ suggestion_parts.append("**Valid condition keys for this action:**")
967
+ if len(sorted_keys) <= 10:
968
+ for key in sorted_keys:
969
+ suggestion_parts.append(f"- `{key}`")
970
+ else:
971
+ for key in sorted_keys[:10]:
972
+ suggestion_parts.append(f"- `{key}`")
973
+ suggestion_parts.append(f"- ... and {len(sorted_keys) - 10} more")
974
+
975
+ suggestion_parts.append("")
976
+ suggestion_parts.append(
977
+ "**Global condition keys** (e.g., `aws:ResourceOrgID`, `aws:RequestedRegion`, `aws:SourceIp`, `aws:SourceVpce`) "
978
+ "can also be used with any AWS action"
979
+ )
980
+ else:
981
+ # No action-specific keys - mention global keys
982
+ suggestion_parts.append(
983
+ "This action does not have specific condition keys defined.\n\n"
984
+ "However, you can use **global condition keys** such as:\n"
985
+ "- `aws:RequestedRegion`\n"
986
+ "- `aws:SourceIp`\n"
987
+ "- `aws:SourceVpce`\n"
988
+ "- `aws:UserAgent`\n"
989
+ "- `aws:CurrentTime`\n"
990
+ "- `aws:SecureTransport`\n"
991
+ "- `aws:PrincipalArn`\n"
992
+ "- And many others"
993
+ )
994
+
995
+ suggestion = "\n".join(suggestion_parts)
996
+
997
+ return ConditionKeyValidationResult(
998
+ is_valid=False,
999
+ error_message=error_msg,
1000
+ suggestion=suggestion,
911
1001
  )
912
1002
 
913
1003
  except Exception as e:
914
1004
  logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
915
- return False, f"Failed to validate condition key: {str(e)}", None
1005
+ return ConditionKeyValidationResult(
1006
+ is_valid=False,
1007
+ error_message=f"Failed to validate condition key: {str(e)}",
1008
+ )
916
1009
 
917
1010
  async def clear_caches(self) -> None:
918
1011
  """Clear all caches (memory and disk)."""
@@ -5,6 +5,7 @@ Loads and parses configuration from YAML files, environment variables,
5
5
  and command-line arguments.
6
6
  """
7
7
 
8
+ import importlib
8
9
  import importlib.util
9
10
  import inspect
10
11
  import logging
@@ -314,8 +315,6 @@ class ConfigLoader:
314
315
  module_name, class_name = parts
315
316
 
316
317
  # Import the module
317
- import importlib
318
-
319
318
  module = importlib.import_module(module_name)
320
319
  check_class = getattr(module, class_name)
321
320
 
@@ -16,6 +16,7 @@ Benefits of code-first approach:
16
16
  - 5-10x faster than YAML parsing
17
17
  """
18
18
 
19
+ from iam_validator.core import constants
19
20
  from iam_validator.core.config.category_suggestions import get_category_suggestions
20
21
  from iam_validator.core.config.condition_requirements import CONDITION_REQUIREMENTS
21
22
  from iam_validator.core.config.principal_requirements import (
@@ -70,11 +71,11 @@ DEFAULT_CONFIG = {
70
71
  # Cache AWS service definitions locally (persists between runs)
71
72
  "cache_enabled": True,
72
73
  # Cache TTL in hours (default: 168 = 7 days)
73
- "cache_ttl_hours": 168,
74
+ "cache_ttl_hours": constants.DEFAULT_CACHE_TTL_HOURS,
74
75
  # Severity levels that cause validation to fail
75
76
  # IAM Validity: error, warning, info
76
77
  # Security: critical, high, medium, low
77
- "fail_on_severity": ["error", "critical", "high"],
78
+ "fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
78
79
  },
79
80
  # ========================================================================
80
81
  # AWS IAM Validation Checks (17 checks total)
@@ -188,7 +189,7 @@ DEFAULT_CONFIG = {
188
189
  "enabled": True,
189
190
  "severity": "error", # IAM validity error
190
191
  "description": "Validates ARN format for resources",
191
- "arn_pattern": "^arn:(aws|aws-cn|aws-us-gov|aws-eusc|aws-iso|aws-iso-b|aws-iso-e|aws-iso-f):[a-z0-9\\-]+:[a-z0-9\\-*]*:[0-9*]*:.+$",
192
+ "arn_pattern": constants.DEFAULT_ARN_VALIDATION_PATTERN,
192
193
  },
193
194
  # ========================================================================
194
195
  # 9. PRINCIPAL VALIDATION
@@ -389,6 +390,18 @@ DEFAULT_CONFIG = {
389
390
  # Services that are allowed to use wildcards (default: logs, cloudwatch, xray)
390
391
  # See: iam_validator/core/config/wildcards.py
391
392
  "allowed_services": list(DEFAULT_SERVICE_WILDCARDS),
393
+ "message": "Service wildcard '{action}' grants all permissions for the {service} service",
394
+ "suggestion": (
395
+ "Replace '{action}' with specific actions needed for your use case to follow least-privilege principle.\n"
396
+ "Find valid {service} actions: https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
397
+ ),
398
+ "example": (
399
+ "Replace:\n"
400
+ ' "Action": ["{action}"]\n'
401
+ "\n"
402
+ "With specific actions:\n"
403
+ ' "Action": ["{service}:Describe*", "{service}:List*"]\n'
404
+ ),
392
405
  },
393
406
  # ========================================================================
394
407
  # 16. SENSITIVE ACTION
@@ -482,10 +495,6 @@ DEFAULT_CONFIG = {
482
495
  # - prevent_public_ip: Prevents 0.0.0.0/0 IP ranges
483
496
  #
484
497
  # See: iam_validator/core/config/condition_requirements.py
485
- # Python API:
486
- # from iam_validator.core.config import CONDITION_REQUIREMENTS
487
- # import copy
488
- # requirements = copy.deepcopy(CONDITION_REQUIREMENTS)
489
498
  "action_condition_enforcement": {
490
499
  "enabled": True,
491
500
  "severity": "high", # Default severity (can be overridden per-requirement)
@@ -62,6 +62,21 @@ DEFAULT_CONFIG_FILENAMES = [
62
62
  ".iam-validator.yml",
63
63
  ]
64
64
 
65
+ # ============================================================================
66
+ # Severity Levels
67
+ # ============================================================================
68
+ # Severity level groupings for filtering and categorization
69
+ # Used across formatters and report generation
70
+
71
+ # High severity issues that typically fail validation
72
+ HIGH_SEVERITY_LEVELS = ("error", "critical", "high")
73
+
74
+ # Medium severity issues (warnings)
75
+ MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
76
+
77
+ # Low severity issues (informational)
78
+ LOW_SEVERITY_LEVELS = ("info", "low")
79
+
65
80
  # ============================================================================
66
81
  # GitHub Integration
67
82
  # ============================================================================
@@ -72,3 +87,45 @@ BOT_IDENTIFIER = "🤖 IAM Policy Validator"
72
87
  # HTML comment markers for identifying bot-generated content (for cleanup/updates)
73
88
  SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
74
89
  REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
90
+
91
+ # GitHub comment size limits
92
+ # GitHub's actual limit is 65536 characters, but we use a smaller limit for safety
93
+ GITHUB_MAX_COMMENT_LENGTH = 65000 # Maximum single comment length
94
+ GITHUB_COMMENT_SPLIT_LIMIT = 60000 # Target size when splitting into multiple parts
95
+
96
+ # Comment size estimation parameters (used for multi-part comment splitting)
97
+ COMMENT_BASE_OVERHEAD_CHARS = 2000 # Base overhead for headers/footers
98
+ COMMENT_CHARS_PER_ISSUE_ESTIMATE = 500 # Average characters per issue
99
+ COMMENT_CONTINUATION_OVERHEAD_CHARS = 200 # Overhead for continuation markers
100
+ FORMATTING_SAFETY_BUFFER = 100 # Safety buffer for formatting calculations
101
+
102
+ # ============================================================================
103
+ # Console Display Settings
104
+ # ============================================================================
105
+
106
+ # Panel width for formatted console output
107
+ CONSOLE_PANEL_WIDTH = 100
108
+
109
+ # Rich console color styles
110
+ CONSOLE_HEADER_COLOR = "bright_blue"
111
+
112
+ # ============================================================================
113
+ # Cache and Timeout Settings
114
+ # ============================================================================
115
+
116
+ # Cache TTL (Time To Live) - 7 days
117
+ DEFAULT_CACHE_TTL_HOURS = 168 # 7 days in hours
118
+ DEFAULT_CACHE_TTL_SECONDS = 604800 # 7 days in seconds (168 * 3600)
119
+
120
+ # HTTP request timeout in seconds
121
+ DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
122
+
123
+ # Time conversion constants
124
+ SECONDS_PER_HOUR = 3600
125
+
126
+ # ============================================================================
127
+ # AWS Documentation URLs
128
+ # ============================================================================
129
+
130
+ # AWS Service Authorization Reference (for finding valid actions, resources, and condition keys)
131
+ AWS_SERVICE_AUTH_REF_URL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
@@ -39,8 +39,17 @@ class ConsoleFormatter(OutputFormatter):
39
39
  color = kwargs.get("color", True)
40
40
 
41
41
  # Capture the output from print_console_report
42
+ from iam_validator.utils import get_terminal_width
43
+
42
44
  string_buffer = StringIO()
43
- console = Console(file=string_buffer, force_terminal=color, width=120)
45
+ # Get terminal width for proper table column spacing
46
+ terminal_width = get_terminal_width()
47
+ console = Console(
48
+ file=string_buffer,
49
+ force_terminal=color,
50
+ width=terminal_width,
51
+ legacy_windows=False,
52
+ )
44
53
 
45
54
  # Create a generator instance with our custom console
46
55
  generator = ReportGenerator()
@@ -4,6 +4,7 @@ import csv
4
4
  import io
5
5
  from typing import Any
6
6
 
7
+ from iam_validator.core import constants
7
8
  from iam_validator.core.formatters.base import OutputFormatter
8
9
  from iam_validator.core.models import ValidationReport
9
10
 
@@ -58,7 +59,7 @@ class CSVFormatter(OutputFormatter):
58
59
  1
59
60
  for r in report.results
60
61
  for i in r.issues
61
- if i.severity in ("error", "critical", "high")
62
+ if i.severity in constants.HIGH_SEVERITY_LEVELS
62
63
  )
63
64
  warnings = sum(
64
65
  1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
@@ -10,6 +10,7 @@ from rich.text import Text
10
10
  from rich.tree import Tree
11
11
 
12
12
  from iam_validator.__version__ import __version__
13
+ from iam_validator.core import constants
13
14
  from iam_validator.core.formatters.base import OutputFormatter
14
15
  from iam_validator.core.models import PolicyValidationResult, ValidationReport
15
16
 
@@ -50,8 +51,14 @@ class EnhancedFormatter(OutputFormatter):
50
51
  show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
51
52
 
52
53
  # Use StringIO to capture Rich console output
54
+ from iam_validator.utils import get_terminal_width
55
+
53
56
  string_buffer = StringIO()
54
- console = Console(file=string_buffer, force_terminal=color, width=120)
57
+ # Get terminal width for proper text wrapping
58
+ terminal_width = get_terminal_width()
59
+ console = Console(
60
+ file=string_buffer, force_terminal=color, width=terminal_width, legacy_windows=False
61
+ )
55
62
 
56
63
  # Header with title
57
64
  console.print()
@@ -60,7 +67,14 @@ class EnhancedFormatter(OutputFormatter):
60
67
  style="bold cyan",
61
68
  justify="center",
62
69
  )
63
- console.print(Panel(title, border_style="bright_blue", padding=(1, 0)))
70
+ console.print(
71
+ Panel(
72
+ title,
73
+ border_style=constants.CONSOLE_HEADER_COLOR,
74
+ padding=(1, 0),
75
+ width=constants.CONSOLE_PANEL_WIDTH,
76
+ )
77
+ )
64
78
  console.print()
65
79
 
66
80
  # Executive Summary with progress bars (optional)
@@ -73,7 +87,7 @@ class EnhancedFormatter(OutputFormatter):
73
87
  self._print_severity_breakdown(console, report)
74
88
 
75
89
  console.print()
76
- console.print(Rule(title="[bold]Detailed Results", style="bright_blue"))
90
+ console.print(Rule(title="[bold]Detailed Results", style=constants.CONSOLE_HEADER_COLOR))
77
91
  console.print()
78
92
 
79
93
  # Detailed results using tree structure
@@ -140,8 +154,9 @@ class EnhancedFormatter(OutputFormatter):
140
154
  Panel(
141
155
  metrics_table,
142
156
  title="📊 Executive Summary",
143
- border_style="bright_blue",
157
+ border_style=constants.CONSOLE_HEADER_COLOR,
144
158
  padding=(1, 2),
159
+ width=constants.CONSOLE_PANEL_WIDTH,
145
160
  )
146
161
  )
147
162
 
@@ -225,7 +240,12 @@ class EnhancedFormatter(OutputFormatter):
225
240
  )
226
241
 
227
242
  console.print(
228
- Panel(severity_table, title="🎯 Issue Severity Breakdown", border_style="bright_blue")
243
+ Panel(
244
+ severity_table,
245
+ title="🎯 Issue Severity Breakdown",
246
+ border_style=constants.CONSOLE_HEADER_COLOR,
247
+ width=constants.CONSOLE_PANEL_WIDTH,
248
+ )
229
249
  )
230
250
 
231
251
  def _format_policy_result_modern(
@@ -247,7 +267,7 @@ class EnhancedFormatter(OutputFormatter):
247
267
  elif result.is_valid and result.issues:
248
268
  # Valid IAM policy but has security findings
249
269
  # Check severity to determine the appropriate status
250
- has_critical = any(i.severity in ("error", "critical", "high") for i in result.issues)
270
+ has_critical = any(i.severity in constants.HIGH_SEVERITY_LEVELS for i in result.issues)
251
271
  if has_critical:
252
272
  icon = "⚠️"
253
273
  color = "red"
@@ -386,6 +406,13 @@ class EnhancedFormatter(OutputFormatter):
386
406
  suggestion_text.append(issue.suggestion, style="italic yellow")
387
407
  msg_node.add(suggestion_text)
388
408
 
409
+ # Example (if present, show with indentation)
410
+ if issue.example:
411
+ msg_node.add(Text("Example:", style="bold cyan"))
412
+ # Show example code with syntax highlighting
413
+ example_text = Text(issue.example, style="dim")
414
+ msg_node.add(example_text)
415
+
389
416
  def _print_final_status(self, console: Console, report: ValidationReport) -> None:
390
417
  """Print final status panel."""
391
418
  if report.invalid_policies == 0 and report.total_issues == 0:
@@ -400,7 +427,7 @@ class EnhancedFormatter(OutputFormatter):
400
427
  # Valid IAM policies but may have security findings
401
428
  # Check if there are critical/high security issues
402
429
  has_critical = any(
403
- i.severity in ("error", "critical", "high")
430
+ i.severity in constants.HIGH_SEVERITY_LEVELS
404
431
  for r in report.results
405
432
  for i in r.issues
406
433
  )
@@ -437,4 +464,11 @@ class EnhancedFormatter(OutputFormatter):
437
464
  final_text.append("\n\n")
438
465
  final_text.append(message)
439
466
 
440
- console.print(Panel(final_text, border_style=border_color, padding=(1, 2)))
467
+ console.print(
468
+ Panel(
469
+ final_text,
470
+ border_style=border_color,
471
+ padding=(1, 2),
472
+ width=constants.CONSOLE_PANEL_WIDTH,
473
+ )
474
+ )
@@ -1,5 +1,6 @@
1
1
  """Markdown formatter - placeholder for existing functionality."""
2
2
 
3
+ from iam_validator.core import constants
3
4
  from iam_validator.core.formatters.base import OutputFormatter
4
5
  from iam_validator.core.models import ValidationReport
5
6
 
@@ -34,7 +35,7 @@ class MarkdownFormatter(OutputFormatter):
34
35
  1
35
36
  for r in report.results
36
37
  for i in r.issues
37
- if i.severity in ("error", "critical", "high")
38
+ if i.severity in constants.HIGH_SEVERITY_LEVELS
38
39
  )
39
40
  warnings = sum(
40
41
  1 for r in report.results for i in r.issues if i.severity in ("warning", "medium")
@@ -8,6 +8,8 @@ from typing import Any, ClassVar, Literal
8
8
 
9
9
  from pydantic import BaseModel, ConfigDict, Field
10
10
 
11
+ from iam_validator.core import constants
12
+
11
13
  # Policy Type Constants
12
14
  PolicyType = Literal[
13
15
  "IDENTITY_POLICY",
@@ -89,9 +91,8 @@ class ServiceDetail(BaseModel):
89
91
  resources_list: list[ResourceType] = Field(default_factory=list, alias="Resources")
90
92
  condition_keys_list: list[ConditionKey] = Field(default_factory=list, alias="ConditionKeys")
91
93
 
92
- def model_post_init(self, __context: Any) -> None:
94
+ def model_post_init(self, __context: Any, /) -> None:
93
95
  """Convert lists to dictionaries for easier lookup."""
94
- del __context # Unused
95
96
  # Convert actions list to dict
96
97
  self.actions = {action.name: action for action in self.actions_list}
97
98
  # Convert resources list to dict
@@ -104,7 +105,7 @@ class ServiceDetail(BaseModel):
104
105
  class Statement(BaseModel):
105
106
  """IAM policy statement."""
106
107
 
107
- model_config = ConfigDict(populate_by_name=True)
108
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
108
109
 
109
110
  sid: str | None = Field(default=None, alias="Sid")
110
111
  effect: str = Field(alias="Effect")
@@ -134,7 +135,7 @@ class Statement(BaseModel):
134
135
  class IAMPolicy(BaseModel):
135
136
  """IAM policy document."""
136
137
 
137
- model_config = ConfigDict(populate_by_name=True)
138
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
138
139
 
139
140
  version: str = Field(alias="Version")
140
141
  statement: list[Statement] = Field(alias="Statement")
@@ -161,6 +162,7 @@ class ValidationIssue(BaseModel):
161
162
  resource: str | None = None
162
163
  condition_key: str | None = None
163
164
  suggestion: str | None = None
165
+ example: str | None = None # Code example (JSON/YAML) - formatted separately for GitHub
164
166
  line_number: int | None = None # Line number in the policy file (if available)
165
167
 
166
168
  # Severity level constants (ClassVar to avoid Pydantic treating them as fields)
@@ -225,8 +227,8 @@ class ValidationIssue(BaseModel):
225
227
 
226
228
  # Add identifier for bot comment cleanup (HTML comment - not visible to users)
227
229
  if include_identifier:
228
- parts.append("<!-- iam-policy-validator-review -->\n")
229
- parts.append("🤖 **IAM Policy Validator**\n")
230
+ parts.append(f"{constants.REVIEW_IDENTIFIER}\n")
231
+ parts.append(f"{constants.BOT_IDENTIFIER}\n")
230
232
 
231
233
  # Build statement context for better navigation
232
234
  statement_context = f"Statement[{self.statement_index}]"
@@ -241,7 +243,9 @@ class ValidationIssue(BaseModel):
241
243
  parts.append(self.message)
242
244
 
243
245
  # Put additional details in collapsible section if there are any
244
- has_details = bool(self.action or self.resource or self.condition_key or self.suggestion)
246
+ has_details = bool(
247
+ self.action or self.resource or self.condition_key or self.suggestion or self.example
248
+ )
245
249
 
246
250
  if has_details:
247
251
  parts.append("")
@@ -266,6 +270,14 @@ class ValidationIssue(BaseModel):
266
270
  parts.append("**💡 Suggested Fix:**")
267
271
  parts.append("")
268
272
  parts.append(self.suggestion)
273
+ parts.append("")
274
+
275
+ # Add example if present (formatted as JSON code block for GitHub)
276
+ if self.example:
277
+ parts.append("**Example:**")
278
+ parts.append("```json")
279
+ parts.append(self.example)
280
+ parts.append("```")
269
281
 
270
282
  parts.append("")
271
283
  parts.append("</details>")
@@ -298,6 +310,9 @@ class ValidationReport(BaseModel):
298
310
  validity_issues: int = 0 # Count of IAM validity issues (error/warning/info)
299
311
  security_issues: int = 0 # Count of security issues (critical/high/medium/low)
300
312
  results: list[PolicyValidationResult] = Field(default_factory=list)
313
+ parsing_errors: list[tuple[str, str]] = Field(
314
+ default_factory=list
315
+ ) # (file_path, error_message)
301
316
 
302
317
  def get_summary(self) -> str:
303
318
  """Generate a human-readable summary."""