iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.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 (56) hide show
  1. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +127 -6
  2. iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +5 -3
  6. iam_validator/checks/action_condition_enforcement.py +559 -207
  7. iam_validator/checks/action_resource_matching.py +12 -15
  8. iam_validator/checks/action_validation.py +7 -13
  9. iam_validator/checks/condition_key_validation.py +7 -13
  10. iam_validator/checks/condition_type_mismatch.py +15 -22
  11. iam_validator/checks/full_wildcard.py +9 -13
  12. iam_validator/checks/mfa_condition_check.py +8 -17
  13. iam_validator/checks/policy_size.py +6 -39
  14. iam_validator/checks/policy_structure.py +547 -0
  15. iam_validator/checks/policy_type_validation.py +61 -46
  16. iam_validator/checks/principal_validation.py +71 -148
  17. iam_validator/checks/resource_validation.py +13 -20
  18. iam_validator/checks/sensitive_action.py +15 -18
  19. iam_validator/checks/service_wildcard.py +8 -14
  20. iam_validator/checks/set_operator_validation.py +21 -28
  21. iam_validator/checks/sid_uniqueness.py +16 -42
  22. iam_validator/checks/trust_policy_validation.py +506 -0
  23. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  24. iam_validator/checks/utils/wildcard_expansion.py +2 -2
  25. iam_validator/checks/wildcard_action.py +9 -13
  26. iam_validator/checks/wildcard_resource.py +9 -13
  27. iam_validator/commands/cache.py +4 -3
  28. iam_validator/commands/validate.py +15 -9
  29. iam_validator/core/__init__.py +2 -3
  30. iam_validator/core/access_analyzer.py +1 -1
  31. iam_validator/core/access_analyzer_report.py +2 -2
  32. iam_validator/core/aws_fetcher.py +24 -1028
  33. iam_validator/core/aws_service/__init__.py +21 -0
  34. iam_validator/core/aws_service/cache.py +108 -0
  35. iam_validator/core/aws_service/client.py +205 -0
  36. iam_validator/core/aws_service/fetcher.py +612 -0
  37. iam_validator/core/aws_service/parsers.py +149 -0
  38. iam_validator/core/aws_service/patterns.py +51 -0
  39. iam_validator/core/aws_service/storage.py +291 -0
  40. iam_validator/core/aws_service/validators.py +379 -0
  41. iam_validator/core/check_registry.py +165 -93
  42. iam_validator/core/config/condition_requirements.py +69 -17
  43. iam_validator/core/config/defaults.py +58 -52
  44. iam_validator/core/config/service_principals.py +40 -3
  45. iam_validator/core/constants.py +17 -0
  46. iam_validator/core/ignore_patterns.py +297 -0
  47. iam_validator/core/models.py +15 -5
  48. iam_validator/core/policy_checks.py +38 -475
  49. iam_validator/core/policy_loader.py +27 -4
  50. iam_validator/sdk/__init__.py +1 -1
  51. iam_validator/sdk/context.py +1 -1
  52. iam_validator/sdk/helpers.py +1 -1
  53. iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
  54. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
  55. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
  56. {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.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,
@@ -88,6 +81,10 @@ class ActionResourceMatchingCheck(PolicyCheck):
88
81
  statement_sid = statement.sid
89
82
  line_number = statement.line_number
90
83
 
84
+ # Skip if no resources to validate (e.g., trust policies don't have Resource field)
85
+ if not resources:
86
+ return issues
87
+
91
88
  # Skip if we have a wildcard resource (handled by other checks)
92
89
  if "*" in resources:
93
90
  return issues
@@ -135,7 +132,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
135
132
  statement_sid=statement_sid,
136
133
  line_number=line_number,
137
134
  config=config,
138
- reason=f'Action {action} can only use Resource: "*"',
135
+ reason=f'Action `{action}` can only use `Resource: "*"`',
139
136
  )
140
137
  )
141
138
  continue
@@ -289,9 +286,9 @@ class ActionResourceMatchingCheck(PolicyCheck):
289
286
  # Special case: Wildcard resource
290
287
  if required_format == "*":
291
288
  return (
292
- f'Action `{action}` can only use Resource: "*" (wildcard).\n'
289
+ f'Action `{action}` can only use `Resource: "*"` (wildcard).\n'
293
290
  f" This action does not support resource-level permissions.\n"
294
- f' Example: "Resource": "*"'
291
+ f' Example: `"Resource": "*"`'
295
292
  )
296
293
 
297
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,
@@ -63,7 +57,7 @@ class ActionValidationCheck(PolicyCheck):
63
57
  statement_sid=statement_sid,
64
58
  statement_index=statement_idx,
65
59
  issue_type="invalid_action",
66
- message=error_msg or f"Invalid action: {action}",
60
+ message=error_msg or f"Invalid action: `{action}`",
67
61
  action=action,
68
62
  line_number=line_number,
69
63
  )
@@ -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,
@@ -62,7 +56,7 @@ class ConditionKeyValidationCheck(PolicyCheck):
62
56
  statement_index=statement_idx,
63
57
  issue_type="invalid_condition_key",
64
58
  message=result.error_message
65
- or f"Invalid condition key: {condition_key}",
59
+ or f"Invalid condition key: `{condition_key}`",
66
60
  action=action,
67
61
  condition_key=condition_key,
68
62
  line_number=line_number,
@@ -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,
@@ -72,7 +65,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
72
65
  # Check each condition operator and its keys/values
73
66
  for operator, conditions in statement.condition.items():
74
67
  # Normalize the operator and get its expected type
75
- base_operator, operator_type, set_prefix = normalize_operator(operator)
68
+ base_operator, operator_type, _set_prefix = normalize_operator(operator)
76
69
 
77
70
  if operator_type is None:
78
71
  # Unknown operator - this will be caught by another check
@@ -108,8 +101,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
108
101
  severity="warning",
109
102
  message=(
110
103
  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
- f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
104
+ f"`{operator_type}` values, but condition key `{condition_key}` is type `{key_type}`. "
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,
@@ -123,8 +116,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
123
116
  ValidationIssue(
124
117
  severity=self.get_severity(config),
125
118
  message=(
126
- f"Type mismatch: Operator `{operator}` expects {operator_type} values, "
127
- f"but condition key `{condition_key}` is type {key_type}."
119
+ f"Type mismatch: Operator `{operator}` expects `{operator_type}` values, "
120
+ f"but condition key `{condition_key}` is type `{key_type}`."
128
121
  ),
129
122
  statement_sid=statement_sid,
130
123
  statement_index=statement_idx,
@@ -172,7 +165,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
172
165
  Returns:
173
166
  Type string or None if not found
174
167
  """
175
- from iam_validator.core.config.aws_global_conditions import (
168
+ from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
176
169
  get_global_conditions,
177
170
  )
178
171
 
@@ -229,7 +222,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
229
222
  if condition_key_obj.types:
230
223
  return condition_key_obj.types[0]
231
224
 
232
- except Exception:
225
+ except Exception: # pylint: disable=broad-exception-caught
233
226
  # If we can't look up the action, skip it
234
227
  continue
235
228
 
@@ -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,