iam-policy-validator 1.7.1__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 (34) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/METADATA +1 -2
  2. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +34 -33
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/action_condition_enforcement.py +20 -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/wildcard_action.py +2 -8
  15. iam_validator/checks/wildcard_resource.py +2 -8
  16. iam_validator/commands/validate.py +2 -2
  17. iam_validator/core/aws_fetcher.py +115 -22
  18. iam_validator/core/config/config_loader.py +1 -2
  19. iam_validator/core/config/defaults.py +16 -7
  20. iam_validator/core/constants.py +57 -0
  21. iam_validator/core/formatters/console.py +10 -1
  22. iam_validator/core/formatters/csv.py +2 -1
  23. iam_validator/core/formatters/enhanced.py +42 -8
  24. iam_validator/core/formatters/markdown.py +2 -1
  25. iam_validator/core/models.py +22 -7
  26. iam_validator/core/policy_checks.py +5 -4
  27. iam_validator/core/policy_loader.py +71 -14
  28. iam_validator/core/report.py +65 -24
  29. iam_validator/integrations/github_integration.py +4 -5
  30. iam_validator/utils/__init__.py +4 -0
  31. iam_validator/utils/terminal.py +22 -0
  32. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
  33. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
  34. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
  )
@@ -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:
@@ -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"