iam-policy-validator 1.8.0__py3-none-any.whl → 1.10.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 (49) hide show
  1. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/METADATA +106 -1
  2. iam_policy_validator-1.10.0.dist-info/RECORD +96 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/action_condition_enforcement.py +504 -190
  6. iam_validator/checks/action_resource_matching.py +8 -15
  7. iam_validator/checks/action_validation.py +6 -12
  8. iam_validator/checks/condition_key_validation.py +6 -12
  9. iam_validator/checks/condition_type_mismatch.py +9 -16
  10. iam_validator/checks/full_wildcard.py +9 -13
  11. iam_validator/checks/mfa_condition_check.py +8 -17
  12. iam_validator/checks/policy_size.py +6 -39
  13. iam_validator/checks/policy_structure.py +10 -40
  14. iam_validator/checks/policy_type_validation.py +18 -19
  15. iam_validator/checks/principal_validation.py +11 -20
  16. iam_validator/checks/resource_validation.py +5 -12
  17. iam_validator/checks/sensitive_action.py +8 -15
  18. iam_validator/checks/service_wildcard.py +6 -12
  19. iam_validator/checks/set_operator_validation.py +11 -18
  20. iam_validator/checks/sid_uniqueness.py +8 -38
  21. iam_validator/checks/trust_policy_validation.py +8 -14
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +6 -12
  24. iam_validator/checks/wildcard_resource.py +6 -12
  25. iam_validator/commands/cache.py +4 -3
  26. iam_validator/commands/validate.py +26 -4
  27. iam_validator/core/__init__.py +1 -1
  28. iam_validator/core/aws_fetcher.py +24 -1030
  29. iam_validator/core/aws_service/__init__.py +21 -0
  30. iam_validator/core/aws_service/cache.py +108 -0
  31. iam_validator/core/aws_service/client.py +205 -0
  32. iam_validator/core/aws_service/fetcher.py +612 -0
  33. iam_validator/core/aws_service/parsers.py +149 -0
  34. iam_validator/core/aws_service/patterns.py +51 -0
  35. iam_validator/core/aws_service/storage.py +291 -0
  36. iam_validator/core/aws_service/validators.py +379 -0
  37. iam_validator/core/check_registry.py +82 -14
  38. iam_validator/core/config/defaults.py +10 -0
  39. iam_validator/core/constants.py +17 -0
  40. iam_validator/core/label_manager.py +197 -0
  41. iam_validator/core/policy_checks.py +7 -3
  42. iam_validator/core/pr_commenter.py +34 -7
  43. iam_validator/sdk/__init__.py +1 -1
  44. iam_validator/sdk/context.py +1 -1
  45. iam_validator/sdk/helpers.py +1 -1
  46. iam_policy_validator-1.8.0.dist-info/RECORD +0 -87
  47. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/WHEEL +0 -0
  48. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/entry_points.txt +0 -0
  49. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -22,8 +22,9 @@ Example:
22
22
  """
23
23
 
24
24
  import re
25
+ from typing import ClassVar
25
26
 
26
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
27
+ from iam_validator.core.aws_service import AWSServiceFetcher
27
28
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
28
29
  from iam_validator.core.models import Statement, ValidationIssue
29
30
  from iam_validator.sdk.arn_matching import (
@@ -42,17 +43,9 @@ class ActionResourceMatchingCheck(PolicyCheck):
42
43
  work as intended because the resource ARNs don't match what the action requires.
43
44
  """
44
45
 
45
- @property
46
- def check_id(self) -> str:
47
- return "action_resource_matching"
48
-
49
- @property
50
- def description(self) -> str:
51
- return "Validates that resources match required types for actions"
52
-
53
- @property
54
- def default_severity(self) -> str:
55
- return "medium" # Security issue, not IAM validity error
46
+ check_id: ClassVar[str] = "action_resource_matching"
47
+ description: ClassVar[str] = "Validates that resources match required types for actions"
48
+ default_severity: ClassVar[str] = "medium" # Security issue, not IAM validity error
56
49
 
57
50
  async def execute(
58
51
  self,
@@ -139,7 +132,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
139
132
  statement_sid=statement_sid,
140
133
  line_number=line_number,
141
134
  config=config,
142
- reason=f'Action {action} can only use Resource: "*"',
135
+ reason=f'Action `{action}` can only use `Resource: "*"`',
143
136
  )
144
137
  )
145
138
  continue
@@ -293,9 +286,9 @@ class ActionResourceMatchingCheck(PolicyCheck):
293
286
  # Special case: Wildcard resource
294
287
  if required_format == "*":
295
288
  return (
296
- f'Action `{action}` can only use Resource: `"*"` (wildcard).\n'
289
+ f'Action `{action}` can only use `Resource: "*"` (wildcard).\n'
297
290
  f" This action does not support resource-level permissions.\n"
298
- f' Example: "Resource": `"*"`'
291
+ f' Example: `"Resource": "*"`'
299
292
  )
300
293
 
301
294
  # Build service-specific suggestion with proper markdown formatting
@@ -9,7 +9,9 @@ validations. However, it can be useful in environments where Access Analyzer is
9
9
  not available or for pre-deployment policy validation to catch errors early.
10
10
  """
11
11
 
12
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
+ from typing import ClassVar
13
+
14
+ from iam_validator.core.aws_service import AWSServiceFetcher
13
15
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
14
16
  from iam_validator.core.models import Statement, ValidationIssue
15
17
 
@@ -17,17 +19,9 @@ from iam_validator.core.models import Statement, ValidationIssue
17
19
  class ActionValidationCheck(PolicyCheck):
18
20
  """Validates that IAM actions exist in AWS services."""
19
21
 
20
- @property
21
- def check_id(self) -> str:
22
- return "action_validation"
23
-
24
- @property
25
- def description(self) -> str:
26
- return "Validates that actions exist in AWS service definitions"
27
-
28
- @property
29
- def default_severity(self) -> str:
30
- return "error"
22
+ check_id: ClassVar[str] = "action_validation"
23
+ description: ClassVar[str] = "Validates that actions exist in AWS service definitions"
24
+ default_severity: ClassVar[str] = "error"
31
25
 
32
26
  async def execute(
33
27
  self,
@@ -1,6 +1,8 @@
1
1
  """Condition key validation check - validates condition keys against AWS definitions."""
2
2
 
3
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
3
+ from typing import ClassVar
4
+
5
+ from iam_validator.core.aws_service import AWSServiceFetcher
4
6
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
5
7
  from iam_validator.core.models import Statement, ValidationIssue
6
8
 
@@ -8,17 +10,9 @@ from iam_validator.core.models import Statement, ValidationIssue
8
10
  class ConditionKeyValidationCheck(PolicyCheck):
9
11
  """Validates condition keys against AWS service definitions and global keys."""
10
12
 
11
- @property
12
- def check_id(self) -> str:
13
- return "condition_key_validation"
14
-
15
- @property
16
- def description(self) -> str:
17
- return "Validates condition keys against AWS service definitions"
18
-
19
- @property
20
- def default_severity(self) -> str:
21
- return "error" # Invalid condition keys are IAM policy errors
13
+ check_id: ClassVar[str] = "condition_key_validation"
14
+ description: ClassVar[str] = "Validates condition keys against AWS service definitions"
15
+ default_severity: ClassVar[str] = "error" # Invalid condition keys are IAM policy errors
22
16
 
23
17
  async def execute(
24
18
  self,
@@ -3,7 +3,9 @@
3
3
  Validates that condition operators match the expected types for condition keys and values.
4
4
  """
5
5
 
6
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
6
+ from typing import ClassVar
7
+
8
+ from iam_validator.core.aws_service import AWSServiceFetcher
7
9
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
8
10
  from iam_validator.core.condition_validators import (
9
11
  normalize_operator,
@@ -16,20 +18,11 @@ from iam_validator.core.models import Statement, ValidationIssue
16
18
  class ConditionTypeMismatchCheck(PolicyCheck):
17
19
  """Check for type mismatches between operators, keys, and values."""
18
20
 
19
- @property
20
- def check_id(self) -> str:
21
- """Unique identifier for this check."""
22
- return "condition_type_mismatch"
23
-
24
- @property
25
- def description(self) -> str:
26
- """Description of what this check does."""
27
- return "Validates condition operator types match key types and value formats"
28
-
29
- @property
30
- def default_severity(self) -> str:
31
- """Default severity level for issues found by this check."""
32
- return "error"
21
+ check_id: ClassVar[str] = "condition_type_mismatch"
22
+ description: ClassVar[str] = (
23
+ "Validates condition operator types match key types and value formats"
24
+ )
25
+ default_severity: ClassVar[str] = "error"
33
26
 
34
27
  async def execute(
35
28
  self,
@@ -109,7 +102,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
109
102
  message=(
110
103
  f"Type mismatch (usable but not recommended): Operator `{operator}` expects "
111
104
  f"`{operator_type}` values, but condition key `{condition_key}` is type `{key_type}`. "
112
- f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
105
+ f"Consider using an ARN-specific operator like `ArnEquals` or `ArnLike` instead."
113
106
  ),
114
107
  statement_sid=statement_sid,
115
108
  statement_index=statement_idx,
@@ -1,6 +1,8 @@
1
1
  """Full wildcard check - detects Action: '*' AND Resource: '*' together (critical security risk)."""
2
2
 
3
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
3
+ from typing import ClassVar
4
+
5
+ from iam_validator.core.aws_service import AWSServiceFetcher
4
6
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
5
7
  from iam_validator.core.models import Statement, ValidationIssue
6
8
 
@@ -8,17 +10,11 @@ from iam_validator.core.models import Statement, ValidationIssue
8
10
  class FullWildcardCheck(PolicyCheck):
9
11
  """Checks for both Action: '*' AND Resource: '*' which grants full administrative access."""
10
12
 
11
- @property
12
- def check_id(self) -> str:
13
- return "full_wildcard"
14
-
15
- @property
16
- def description(self) -> str:
17
- return "Checks for both action and resource wildcards together (critical risk)"
18
-
19
- @property
20
- def default_severity(self) -> str:
21
- return "critical"
13
+ check_id: ClassVar[str] = "full_wildcard"
14
+ description: ClassVar[str] = (
15
+ "Checks for both action and resource wildcards together (critical risk)"
16
+ )
17
+ default_severity: ClassVar[str] = "critical"
22
18
 
23
19
  async def execute(
24
20
  self,
@@ -41,7 +37,7 @@ class FullWildcardCheck(PolicyCheck):
41
37
  if "*" in actions and "*" in resources:
42
38
  message = config.config.get(
43
39
  "message",
44
- "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
40
+ "Statement allows all actions on all resources - **CRITICAL SECURITY RISK**",
45
41
  )
46
42
  suggestion = config.config.get(
47
43
  "suggestion",
@@ -3,6 +3,8 @@
3
3
  Detects dangerous MFA-related condition patterns that may not enforce MFA as intended.
4
4
  """
5
5
 
6
+ from typing import ClassVar
7
+
6
8
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
9
  from iam_validator.core.models import Statement, ValidationIssue
8
10
 
@@ -10,20 +12,9 @@ from iam_validator.core.models import Statement, ValidationIssue
10
12
  class MFAConditionCheck(PolicyCheck):
11
13
  """Check for MFA condition anti-patterns."""
12
14
 
13
- @property
14
- def check_id(self) -> str:
15
- """Unique identifier for this check."""
16
- return "mfa_condition_antipattern"
17
-
18
- @property
19
- def description(self) -> str:
20
- """Description of what this check does."""
21
- return "Detects dangerous MFA-related condition patterns"
22
-
23
- @property
24
- def default_severity(self) -> str:
25
- """Default severity level for issues found by this check."""
26
- return "warning"
15
+ check_id: ClassVar[str] = "mfa_condition_antipattern"
16
+ description: ClassVar[str] = "Detects dangerous MFA-related condition patterns"
17
+ default_severity: ClassVar[str] = "warning"
27
18
 
28
19
  async def execute(
29
20
  self, statement: Statement, statement_idx: int, fetcher, config: CheckConfig
@@ -73,8 +64,8 @@ class MFAConditionCheck(PolicyCheck):
73
64
  "**Dangerous MFA condition pattern detected.** "
74
65
  'Using `{"Bool": {"aws:MultiFactorAuthPresent": "false"}}` does not enforce MFA '
75
66
  "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."
67
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an `Allow` statement, '
68
+ "or use `BoolIfExists` in a `Deny` statement."
78
69
  ),
79
70
  statement_sid=statement_sid,
80
71
  statement_index=statement_idx,
@@ -100,7 +91,7 @@ class MFAConditionCheck(PolicyCheck):
100
91
  "**Dangerous MFA condition pattern detected.** "
101
92
  'Using `{"Null": {"aws:MultiFactorAuthPresent": "false"}}` only checks if the key exists, '
102
93
  "not whether MFA was actually used. This does not enforce MFA. "
103
- 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement instead.'
94
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an `Allow` statement instead.'
104
95
  ),
105
96
  statement_sid=statement_sid,
106
97
  statement_index=statement_idx,
@@ -12,12 +12,12 @@ Note: AWS does not count whitespace when calculating policy size.
12
12
 
13
13
  import json
14
14
  import re
15
- from typing import TYPE_CHECKING
15
+ from typing import TYPE_CHECKING, ClassVar
16
16
 
17
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
17
+ from iam_validator.core.aws_service import AWSServiceFetcher
18
18
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
19
19
  from iam_validator.core.constants import AWS_POLICY_SIZE_LIMITS
20
- from iam_validator.core.models import Statement, ValidationIssue
20
+ from iam_validator.core.models import ValidationIssue
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from iam_validator.core.models import IAMPolicy
@@ -29,42 +29,9 @@ class PolicySizeCheck(PolicyCheck):
29
29
  # AWS IAM policy size limits (loaded from constants module)
30
30
  DEFAULT_LIMITS = AWS_POLICY_SIZE_LIMITS
31
31
 
32
- @property
33
- def check_id(self) -> str:
34
- return "policy_size"
35
-
36
- @property
37
- def description(self) -> str:
38
- return "Validates that IAM policies don't exceed AWS size limits"
39
-
40
- @property
41
- def default_severity(self) -> str:
42
- return "error"
43
-
44
- async def execute(
45
- self,
46
- statement: Statement,
47
- statement_idx: int,
48
- fetcher: AWSServiceFetcher,
49
- config: CheckConfig,
50
- ) -> list[ValidationIssue]:
51
- """Execute the policy size check at statement level.
52
-
53
- This is a policy-level check, so statement-level execution returns empty.
54
- The actual check runs in execute_policy() which has access to the full policy.
55
-
56
- Args:
57
- statement: The IAM policy statement (unused)
58
- statement_idx: Index of the statement in the policy (unused)
59
- fetcher: AWS service fetcher (unused for this check)
60
- config: Configuration for this check instance (unused)
61
-
62
- Returns:
63
- Empty list (actual check runs in execute_policy())
64
- """
65
- del statement, statement_idx, fetcher, config # Unused
66
- # This is a policy-level check - execution happens in execute_policy()
67
- return []
32
+ check_id: ClassVar[str] = "policy_size"
33
+ description: ClassVar[str] = "Validates that IAM policies don't exceed AWS size limits"
34
+ default_severity: ClassVar[str] = "error"
68
35
 
69
36
  async def execute_policy(
70
37
  self,
@@ -20,11 +20,11 @@ By detecting these issues early, we can:
20
20
  """
21
21
 
22
22
  import re
23
- from typing import Any
23
+ from typing import Any, ClassVar
24
24
 
25
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
25
+ from iam_validator.core.aws_service import AWSServiceFetcher
26
26
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
27
- from iam_validator.core.models import IAMPolicy, PolicyType, Statement, ValidationIssue
27
+ from iam_validator.core.models import IAMPolicy, PolicyType, ValidationIssue
28
28
 
29
29
  # Valid statement fields according to AWS IAM policy grammar
30
30
  # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
@@ -350,7 +350,7 @@ def validate_statement_structure(
350
350
  statement_index=statement_idx,
351
351
  issue_type="missing_effect",
352
352
  message="`Statement` is missing the required `Effect` field",
353
- suggestion="Add an Effect field with value `Allow` or `Deny`",
353
+ suggestion="Add an `Effect` field with value `Allow` or `Deny`",
354
354
  example='"Effect": "Allow"',
355
355
  )
356
356
  )
@@ -362,7 +362,7 @@ def validate_statement_structure(
362
362
  statement_index=statement_idx,
363
363
  issue_type="invalid_effect",
364
364
  message=f"Invalid `Effect` value: `{statement_dict['Effect']}`. Must be `Allow` or `Deny`",
365
- suggestion="Change Effect to either `Allow` or `Deny`",
365
+ suggestion="Change `Effect` to either `Allow` or `Deny`",
366
366
  example='"Effect": "Allow"',
367
367
  )
368
368
  )
@@ -488,41 +488,11 @@ class PolicyStructureCheck(PolicyCheck):
488
488
  issues early.
489
489
  """
490
490
 
491
- @property
492
- def check_id(self) -> str:
493
- return "policy_structure"
494
-
495
- @property
496
- def description(self) -> str:
497
- return "Validates fundamental IAM policy structure (required fields, field conflicts, valid values)"
498
-
499
- @property
500
- def default_severity(self) -> str:
501
- return "error"
502
-
503
- async def execute(
504
- self,
505
- statement: Statement,
506
- statement_idx: int,
507
- fetcher: AWSServiceFetcher,
508
- config: CheckConfig,
509
- ) -> list[ValidationIssue]:
510
- """Execute at statement level.
511
-
512
- This is a policy-level check, so statement-level execution returns empty.
513
- The actual check runs in execute_policy() which has access to all statements.
514
-
515
- Args:
516
- statement: The IAM policy statement (unused)
517
- statement_idx: Index of the statement in the policy (unused)
518
- fetcher: AWS service fetcher (unused)
519
- config: Check configuration (unused)
520
-
521
- Returns:
522
- Empty list (actual check runs in execute_policy())
523
- """
524
- del statement, statement_idx, fetcher, config # Unused
525
- return []
491
+ check_id: ClassVar[str] = "policy_structure"
492
+ description: ClassVar[str] = (
493
+ "Validates fundamental IAM policy structure (required fields, field conflicts, valid values)"
494
+ )
495
+ default_severity: ClassVar[str] = "error"
526
496
 
527
497
  async def execute_policy(
528
498
  self,
@@ -11,6 +11,7 @@ This check runs automatically based on:
11
11
  2. Auto-detection: If any statement has a Principal, provides helpful guidance
12
12
  """
13
13
 
14
+ from iam_validator.core.constants import RCP_SUPPORTED_SERVICES
14
15
  from iam_validator.core.models import IAMPolicy, ValidationIssue
15
16
 
16
17
 
@@ -47,12 +48,12 @@ async def execute_policy(
47
48
  if is_trust_policy(policy):
48
49
  hint_msg = (
49
50
  "Policy contains assume role actions - this is a TRUST POLICY. "
50
- "Use --policy-type TRUST_POLICY for proper validation (suppresses missing Resource warnings, "
51
+ "Use `--policy-type TRUST_POLICY` for proper validation (suppresses missing Resource warnings, "
51
52
  "enables trust-specific validation)"
52
53
  )
53
54
  suggestion_msg = "iam-validator validate --path <file> --policy-type TRUST_POLICY"
54
55
  else:
55
- hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use --policy-type RESOURCE_POLICY"
56
+ hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use `--policy-type RESOURCE_POLICY`"
56
57
  suggestion_msg = "iam-validator validate --path <file> --policy-type RESOURCE_POLICY"
57
58
 
58
59
  issues.append(
@@ -164,8 +165,8 @@ async def execute_policy(
164
165
 
165
166
  # Resource Control Policies (RCPs) have very strict requirements
166
167
  elif policy_type == "RESOURCE_CONTROL_POLICY":
167
- # RCP supported services (as of 2025)
168
- rcp_supported_services = {"s3", "sts", "sqs", "secretsmanager", "kms"}
168
+ # Use the centralized list of RCP supported services from constants
169
+ rcp_supported_services = RCP_SUPPORTED_SERVICES
169
170
 
170
171
  for idx, statement in enumerate(policy.statement):
171
172
  # 1. Effect MUST be Deny (only RCPFullAWSAccess can use Allow)
@@ -174,13 +175,13 @@ async def execute_policy(
174
175
  ValidationIssue(
175
176
  severity="error",
176
177
  issue_type="invalid_rcp_effect",
177
- message="Resource Control Policy statement must have Effect: Deny. "
178
- "For RCPs that you create, the Effect value must be 'Deny'. "
179
- "Only the AWS-managed RCPFullAWSAccess policy can use 'Allow'.",
178
+ message="Resource Control Policy statement must have `Effect: Deny`. "
179
+ "For RCPs that you create, the `Effect` value must be `Deny`. "
180
+ "Only the AWS-managed `RCPFullAWSAccess` policy can use `Allow`.",
180
181
  statement_index=idx,
181
182
  statement_sid=statement.sid,
182
183
  line_number=statement.line_number,
183
- suggestion='Change the Effect to "Deny" for this RCP statement.',
184
+ suggestion="Change the `Effect` to `Deny` for this RCP statement.",
184
185
  )
185
186
  )
186
187
 
@@ -194,13 +195,12 @@ async def execute_policy(
194
195
  severity="error",
195
196
  issue_type="invalid_rcp_not_principal",
196
197
  message="Resource Control Policy must not contain `NotPrincipal` element. "
197
- "RCPs only support Principal with value '*'. Use Condition elements "
198
+ "RCPs only support `Principal` with value `*`. Use `Condition` elements "
198
199
  "to restrict specific principals.",
199
200
  statement_index=idx,
200
201
  statement_sid=statement.sid,
201
202
  line_number=statement.line_number,
202
- suggestion="Remove NotPrincipal and use Principal: '*' with Condition "
203
- "elements to restrict access.",
203
+ suggestion='Remove `NotPrincipal` and use `Principal: "*"` with `Condition` elements to restrict access.',
204
204
  )
205
205
  )
206
206
  elif not has_principal:
@@ -208,8 +208,8 @@ async def execute_policy(
208
208
  ValidationIssue(
209
209
  severity="error",
210
210
  issue_type="missing_rcp_principal",
211
- message="Resource Control Policy statement must have `Principal`: '*'. "
212
- "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` '
213
213
  "elements to restrict specific principals.",
214
214
  statement_index=idx,
215
215
  statement_sid=statement.sid,
@@ -257,7 +257,7 @@ async def execute_policy(
257
257
  statement_sid=statement.sid,
258
258
  line_number=statement.line_number,
259
259
  suggestion="Replace `*` with service-specific actions from supported "
260
- f"services: {', '.join(sorted(rcp_supported_services))}",
260
+ f"services: {', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
261
261
  )
262
262
  )
263
263
  else:
@@ -275,13 +275,13 @@ async def execute_policy(
275
275
  severity="error",
276
276
  issue_type="unsupported_rcp_service",
277
277
  message=f"Resource Control Policy contains actions from unsupported services: "
278
- f"{', '.join(unsupported_actions)}. RCPs only support these services: "
279
- f"{', '.join(sorted(rcp_supported_services))}",
278
+ f"{', '.join(f'`{a}`' for a in unsupported_actions)}. RCPs only support these services: "
279
+ f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
280
280
  statement_index=idx,
281
281
  statement_sid=statement.sid,
282
282
  line_number=statement.line_number,
283
283
  suggestion=f"Use only actions from supported RCP services: "
284
- f"{', '.join(sorted(rcp_supported_services))}",
284
+ f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
285
285
  )
286
286
  )
287
287
 
@@ -296,8 +296,7 @@ async def execute_policy(
296
296
  statement_index=idx,
297
297
  statement_sid=statement.sid,
298
298
  line_number=statement.line_number,
299
- suggestion="Replace `NotAction` with `Action` element listing the specific "
300
- "actions to deny.",
299
+ suggestion="Replace `NotAction` with `Action` element listing the specific actions to deny.",
301
300
  )
302
301
  )
303
302
 
@@ -35,9 +35,9 @@ Supports: any_of, all_of, none_of, and expected_value (single value or list)
35
35
  """
36
36
 
37
37
  import fnmatch
38
- from typing import Any
38
+ from typing import Any, ClassVar
39
39
 
40
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
40
+ from iam_validator.core.aws_service import AWSServiceFetcher
41
41
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
42
42
  from iam_validator.core.config.service_principals import is_aws_service_principal
43
43
  from iam_validator.core.models import Statement, ValidationIssue
@@ -46,17 +46,11 @@ from iam_validator.core.models import Statement, ValidationIssue
46
46
  class PrincipalValidationCheck(PolicyCheck):
47
47
  """Validates Principal elements in resource policies."""
48
48
 
49
- @property
50
- def check_id(self) -> str:
51
- return "principal_validation"
52
-
53
- @property
54
- def description(self) -> str:
55
- return "Validates Principal elements in resource policies for security best practices"
56
-
57
- @property
58
- def default_severity(self) -> str:
59
- return "high"
49
+ check_id: ClassVar[str] = "principal_validation"
50
+ description: ClassVar[str] = (
51
+ "Validates Principal elements in resource policies for security best practices"
52
+ )
53
+ default_severity: ClassVar[str] = "high"
60
54
 
61
55
  async def execute(
62
56
  self,
@@ -122,7 +116,7 @@ class PrincipalValidationCheck(PolicyCheck):
122
116
  severity=self.get_severity(config),
123
117
  issue_type="unauthorized_principal",
124
118
  message=f"`Principal` not in allowed list: `{principal}`. "
125
- f"Only principals in the `allowed_principals` whitelist are permitted.",
119
+ f"Only principals in the `allowed_principals` allow-list are permitted.",
126
120
  statement_index=statement_idx,
127
121
  statement_sid=statement.sid,
128
122
  line_number=statement.line_number,
@@ -401,7 +395,6 @@ class PrincipalValidationCheck(PolicyCheck):
401
395
  # Create a combined error for any_of
402
396
  condition_keys = [cond.get("condition_key", "unknown") for cond in any_of]
403
397
  severity = requirement.get("severity", self.get_severity(config))
404
- matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
405
398
  issues.append(
406
399
  ValidationIssue(
407
400
  severity=severity,
@@ -409,7 +402,7 @@ class PrincipalValidationCheck(PolicyCheck):
409
402
  statement_index=statement_idx,
410
403
  issue_type="missing_principal_condition_any_of",
411
404
  message=(
412
- f"`Principal`s `{matching_principals_str}` require at least ONE of these conditions: "
405
+ f"`Principal`s `{', '.join(f'`{p}`' for p in matching_principals)}` require at least ONE of these conditions: "
413
406
  f"{', '.join(f'`{c}`' for c in condition_keys)}"
414
407
  ),
415
408
  suggestion=self._build_any_of_suggestion(any_of),
@@ -566,14 +559,12 @@ class PrincipalValidationCheck(PolicyCheck):
566
559
  condition_key, description, example, expected_value, operator
567
560
  )
568
561
 
569
- matching_principals_formatted = ", ".join(f"`{p}`" for p in matching_principals)
570
-
571
562
  return ValidationIssue(
572
563
  severity=severity,
573
564
  statement_sid=statement.sid,
574
565
  statement_index=statement_idx,
575
566
  issue_type="missing_principal_condition",
576
- message=f"{message_prefix} Principal(s) {matching_principals_formatted} require condition `{condition_key}`",
567
+ message=f"{message_prefix} Principal(s) {', '.join(f'`{p}`' for p in matching_principals)} require condition `{condition_key}`",
577
568
  suggestion=suggestion_text,
578
569
  example=example_code,
579
570
  line_number=statement.line_number,
@@ -599,7 +590,7 @@ class PrincipalValidationCheck(PolicyCheck):
599
590
  Returns:
600
591
  Tuple of (suggestion_text, example_code)
601
592
  """
602
- suggestion = description if description else f"Add condition: {condition_key}"
593
+ suggestion = description if description else f"Add condition: `{condition_key}`"
603
594
 
604
595
  # Build example based on condition key type
605
596
  if example:
@@ -1,8 +1,9 @@
1
1
  """Resource validation check - validates ARN formats."""
2
2
 
3
3
  import re
4
+ from typing import ClassVar
4
5
 
5
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
6
+ from iam_validator.core.aws_service import AWSServiceFetcher
6
7
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
7
8
  from iam_validator.core.constants import DEFAULT_ARN_VALIDATION_PATTERN, MAX_ARN_LENGTH
8
9
  from iam_validator.core.models import Statement, ValidationIssue
@@ -15,17 +16,9 @@ from iam_validator.sdk.arn_matching import (
15
16
  class ResourceValidationCheck(PolicyCheck):
16
17
  """Validates ARN format for resources."""
17
18
 
18
- @property
19
- def check_id(self) -> str:
20
- return "resource_validation"
21
-
22
- @property
23
- def description(self) -> str:
24
- return "Validates ARN format for resources"
25
-
26
- @property
27
- def default_severity(self) -> str:
28
- return "error"
19
+ check_id: ClassVar[str] = "resource_validation"
20
+ description: ClassVar[str] = "Validates ARN format for resources"
21
+ default_severity: ClassVar[str] = "error"
29
22
 
30
23
  async def execute(
31
24
  self,