iam-policy-validator 1.8.0__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 (45) hide show
  1. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +106 -1
  2. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/RECORD +45 -37
  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 +12 -0
  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/constants.py +17 -0
  39. iam_validator/core/policy_checks.py +7 -3
  40. iam_validator/sdk/__init__.py +1 -1
  41. iam_validator/sdk/context.py +1 -1
  42. iam_validator/sdk/helpers.py +1 -1
  43. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
  44. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
  45. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  """Sensitive action check - detects sensitive actions without IAM conditions."""
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, ClassVar
4
4
 
5
5
  from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
6
6
  from iam_validator.checks.utils.sensitive_action_matcher import (
@@ -8,7 +8,7 @@ from iam_validator.checks.utils.sensitive_action_matcher import (
8
8
  check_sensitive_actions,
9
9
  )
10
10
  from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
11
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
11
+ from iam_validator.core.aws_service import AWSServiceFetcher
12
12
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
13
13
  from iam_validator.core.config.sensitive_actions import get_category_for_action
14
14
  from iam_validator.core.models import Statement, ValidationIssue
@@ -20,17 +20,9 @@ if TYPE_CHECKING:
20
20
  class SensitiveActionCheck(PolicyCheck):
21
21
  """Checks for sensitive actions without IAM conditions to limit their use."""
22
22
 
23
- @property
24
- def check_id(self) -> str:
25
- return "sensitive_action"
26
-
27
- @property
28
- def description(self) -> str:
29
- return "Checks for sensitive actions without conditions"
30
-
31
- @property
32
- def default_severity(self) -> str:
33
- return "medium"
23
+ check_id: ClassVar[str] = "sensitive_action"
24
+ description: ClassVar[str] = "Checks for sensitive actions without conditions"
25
+ default_severity: ClassVar[str] = "medium"
34
26
 
35
27
  def _get_severity_for_action(self, action: str, config: CheckConfig) -> str:
36
28
  """
@@ -86,6 +78,7 @@ class SensitiveActionCheck(PolicyCheck):
86
78
  return (
87
79
  "Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
88
80
  "• Match principal tags to resource tags (`aws:PrincipalTag/<tag-name>` = `aws:ResourceTag/<tag-name>`)\n"
81
+ "• Match organization principal tags to resource tags (`aws:PrincipalOrgID` = `aws:ResourceOrgID`)\n"
89
82
  "• Require MFA (`aws:MultiFactorAuthPresent` = `true`)\n"
90
83
  "• Restrict by IP (`aws:SourceIp`) or VPC (`aws:SourceVpc`)",
91
84
  '"Condition": {\n'
@@ -125,14 +118,14 @@ class SensitiveActionCheck(PolicyCheck):
125
118
  if len(matched_actions) == 1:
126
119
  message_template = config.config.get(
127
120
  "message_single",
128
- "Sensitive action '{action}' should have conditions to limit when it can be used",
121
+ "Sensitive action `{action}` should have conditions to limit when it can be used",
129
122
  )
130
123
  message = message_template.format(action=matched_actions[0])
131
124
  else:
132
125
  action_list = "', '".join(matched_actions)
133
126
  message_template = config.config.get(
134
127
  "message_multiple",
135
- "Sensitive actions '{actions}' should have conditions to limit when they can be used",
128
+ "Sensitive actions `{actions}` should have conditions to limit when they can be used",
136
129
  )
137
130
  message = message_template.format(actions=action_list)
138
131
 
@@ -1,6 +1,8 @@
1
1
  """Service wildcard check - detects service-level wildcards like 'iam:*', 's3:*'."""
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 ServiceWildcardCheck(PolicyCheck):
9
11
  """Checks for service-level wildcards (e.g., 'iam:*', 's3:*') which grant all permissions for a service."""
10
12
 
11
- @property
12
- def check_id(self) -> str:
13
- return "service_wildcard"
14
-
15
- @property
16
- def description(self) -> str:
17
- return "Checks for service-level wildcards (e.g., 'iam:*', 's3:*')"
18
-
19
- @property
20
- def default_severity(self) -> str:
21
- return "high"
13
+ check_id: ClassVar[str] = "service_wildcard"
14
+ description: ClassVar[str] = "Checks for service-level wildcards (e.g., 'iam:*', 's3:*')"
15
+ default_severity: ClassVar[str] = "high"
22
16
 
23
17
  async def execute(
24
18
  self,
@@ -6,7 +6,9 @@ Based on AWS IAM best practices:
6
6
  https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html
7
7
  """
8
8
 
9
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
9
+ from typing import ClassVar
10
+
11
+ from iam_validator.core.aws_service import AWSServiceFetcher
10
12
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
11
13
  from iam_validator.core.condition_validators import (
12
14
  is_multivalued_context_key,
@@ -18,20 +20,11 @@ from iam_validator.core.models import Statement, ValidationIssue
18
20
  class SetOperatorValidationCheck(PolicyCheck):
19
21
  """Check for proper usage of ForAllValues and ForAnyValue set operators."""
20
22
 
21
- @property
22
- def check_id(self) -> str:
23
- """Unique identifier for this check."""
24
- return "set_operator_validation"
25
-
26
- @property
27
- def description(self) -> str:
28
- """Description of what this check does."""
29
- return "Validates proper usage of ForAllValues and ForAnyValue set operators"
30
-
31
- @property
32
- def default_severity(self) -> str:
33
- """Default severity level for issues found by this check."""
34
- return "error"
23
+ check_id: ClassVar[str] = "set_operator_validation"
24
+ description: ClassVar[str] = (
25
+ "Validates proper usage of ForAllValues and ForAnyValue set operators"
26
+ )
27
+ default_severity: ClassVar[str] = "error"
35
28
 
36
29
  async def execute(
37
30
  self,
@@ -120,8 +113,8 @@ class SetOperatorValidationCheck(PolicyCheck):
120
113
  ValidationIssue(
121
114
  severity="warning",
122
115
  message=(
123
- f"Security risk: ForAllValues with Allow effect on `{condition_key}` "
124
- f"should include a Null condition check. Without it, requests with missing "
116
+ f"Security risk: `ForAllValues` with `Allow` effect on `{condition_key}` "
117
+ f"should include a `Null` condition check. Without it, requests with missing "
125
118
  f'`{condition_key}` will be granted access. Add: `"Null": {{"{condition_key}": "false"}}`. '
126
119
  f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
127
120
  ),
@@ -141,7 +134,7 @@ class SetOperatorValidationCheck(PolicyCheck):
141
134
  severity="warning",
142
135
  message=(
143
136
  f"Unpredictable behavior: `ForAnyValue` with `Deny` effect on `{condition_key}` "
144
- f"should include a Null condition check. Without it, requests with missing "
137
+ f"should include a `Null` condition check. Without it, requests with missing "
145
138
  f"`{condition_key}` will evaluate to `No match` instead of denying access. "
146
139
  f'Add: `"Null": {{"{condition_key}": "false"}}`. '
147
140
  f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
@@ -13,10 +13,11 @@ statement, examining all statements in the policy to find duplicates and format
13
13
 
14
14
  import re
15
15
  from collections import Counter
16
+ from typing import ClassVar
16
17
 
17
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
18
+ from iam_validator.core.aws_service import AWSServiceFetcher
18
19
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
19
- from iam_validator.core.models import IAMPolicy, Statement, ValidationIssue
20
+ from iam_validator.core.models import IAMPolicy, ValidationIssue
20
21
 
21
22
 
22
23
  def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[ValidationIssue]:
@@ -111,42 +112,11 @@ class SidUniquenessCheck(PolicyCheck):
111
112
  It only runs once when processing the first statement to avoid duplicate work.
112
113
  """
113
114
 
114
- @property
115
- def check_id(self) -> str:
116
- return "sid_uniqueness"
117
-
118
- @property
119
- def description(self) -> str:
120
- return "Validates that Statement IDs (Sids) are unique and follow AWS naming requirements (no spaces)"
121
-
122
- @property
123
- def default_severity(self) -> str:
124
- return "warning"
125
-
126
- async def execute(
127
- self,
128
- statement: Statement,
129
- statement_idx: int,
130
- fetcher: AWSServiceFetcher,
131
- config: CheckConfig,
132
- ) -> list[ValidationIssue]:
133
- """Execute the SID uniqueness check at statement level.
134
-
135
- This is a policy-level check, so statement-level execution returns empty.
136
- The actual check runs in execute_policy() which has access to all statements.
137
-
138
- Args:
139
- statement: The IAM policy statement (unused)
140
- statement_idx: Index of the statement in the policy (unused)
141
- fetcher: AWS service fetcher (unused for this check)
142
- config: Configuration for this check instance (unused)
143
-
144
- Returns:
145
- Empty list (actual check runs in execute_policy())
146
- """
147
- del statement, statement_idx, fetcher, config # Unused
148
- # This is a policy-level check - execution happens in execute_policy()
149
- return []
115
+ check_id: ClassVar[str] = "sid_uniqueness"
116
+ description: ClassVar[str] = (
117
+ "Validates that Statement IDs (Sids) are unique and follow AWS naming requirements (no spaces)"
118
+ )
119
+ default_severity: ClassVar[str] = "warning"
150
120
 
151
121
  async def execute_policy(
152
122
  self,
@@ -34,9 +34,9 @@ This check is DISABLED by default. Enable it for trust policy validation:
34
34
  """
35
35
 
36
36
  import re
37
- from typing import TYPE_CHECKING, Any
37
+ from typing import TYPE_CHECKING, Any, ClassVar
38
38
 
39
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
39
+ from iam_validator.core.aws_service import AWSServiceFetcher
40
40
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
41
41
  from iam_validator.core.models import Statement, ValidationIssue
42
42
 
@@ -79,17 +79,11 @@ class TrustPolicyValidationCheck(PolicyCheck):
79
79
  },
80
80
  }
81
81
 
82
- @property
83
- def check_id(self) -> str:
84
- return "trust_policy_validation"
85
-
86
- @property
87
- def description(self) -> str:
88
- return "Validates trust policies for role assumption security and action-principal coupling"
89
-
90
- @property
91
- def default_severity(self) -> str:
92
- return "high"
82
+ check_id: ClassVar[str] = "trust_policy_validation"
83
+ description: ClassVar[str] = (
84
+ "Validates trust policies for role assumption security and action-principal coupling"
85
+ )
86
+ default_severity: ClassVar[str] = "high"
93
87
 
94
88
  async def execute(
95
89
  self,
@@ -256,7 +250,7 @@ class TrustPolicyValidationCheck(PolicyCheck):
256
250
  ValidationIssue(
257
251
  severity=self.get_severity(config),
258
252
  issue_type="invalid_principal_type_for_assume_action",
259
- message=f"Action `{action}` should not use Principal type `{principal_type}`. "
253
+ message=f"Action `{action}` should not use `Principal` type `{principal_type}`. "
260
254
  f"Expected principal types: {allowed_list}",
261
255
  statement_index=statement_idx,
262
256
  statement_sid=statement.sid,
@@ -7,7 +7,7 @@ to their actual action names using the AWS Service Reference API.
7
7
  import re
8
8
  from functools import lru_cache
9
9
 
10
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
10
+ from iam_validator.core.aws_service import AWSServiceFetcher
11
11
 
12
12
 
13
13
  # Global cache for compiled wildcard patterns (shared across checks)
@@ -1,6 +1,8 @@
1
1
  """Wildcard action check - detects Action: '*' in IAM policies."""
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 WildcardActionCheck(PolicyCheck):
9
11
  """Checks for wildcard actions (Action: '*') which grant all permissions."""
10
12
 
11
- @property
12
- def check_id(self) -> str:
13
- return "wildcard_action"
14
-
15
- @property
16
- def description(self) -> str:
17
- return "Checks for wildcard actions (*)"
18
-
19
- @property
20
- def default_severity(self) -> str:
21
- return "medium"
13
+ check_id: ClassVar[str] = "wildcard_action"
14
+ description: ClassVar[str] = "Checks for wildcard actions (*)"
15
+ default_severity: ClassVar[str] = "medium"
22
16
 
23
17
  async def execute(
24
18
  self,
@@ -1,7 +1,9 @@
1
1
  """Wildcard resource check - detects Resource: '*' in IAM policies."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
4
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
6
+ from iam_validator.core.aws_service import AWSServiceFetcher
5
7
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
6
8
  from iam_validator.core.models import Statement, ValidationIssue
7
9
 
@@ -9,17 +11,9 @@ from iam_validator.core.models import Statement, ValidationIssue
9
11
  class WildcardResourceCheck(PolicyCheck):
10
12
  """Checks for wildcard resources (Resource: '*') which grant access to all resources."""
11
13
 
12
- @property
13
- def check_id(self) -> str:
14
- return "wildcard_resource"
15
-
16
- @property
17
- def description(self) -> str:
18
- return "Checks for wildcard resources (*)"
19
-
20
- @property
21
- def default_severity(self) -> str:
22
- return "medium"
14
+ check_id: ClassVar[str] = "wildcard_resource"
15
+ description: ClassVar[str] = "Checks for wildcard resources (*)"
16
+ default_severity: ClassVar[str] = "medium"
23
17
 
24
18
  async def execute(
25
19
  self,
@@ -8,7 +8,8 @@ from rich.console import Console
8
8
  from rich.table import Table
9
9
 
10
10
  from iam_validator.commands.base import Command
11
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
11
+ from iam_validator.core.aws_service import AWSServiceFetcher
12
+ from iam_validator.core.aws_service.storage import ServiceFileStorage
12
13
  from iam_validator.core.config.config_loader import ConfigLoader
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -130,7 +131,7 @@ Examples:
130
131
  cache_ttl_seconds = cache_ttl_hours * 3600
131
132
 
132
133
  # Get cache directory (even if caching is disabled, for info purposes)
133
- cache_dir = AWSServiceFetcher._get_cache_directory(cache_directory)
134
+ cache_dir = ServiceFileStorage.get_cache_directory(cache_directory)
134
135
 
135
136
  action = args.cache_action
136
137
 
@@ -226,7 +227,7 @@ Examples:
226
227
  services.append({"name": name, "size": size, "file": f.name, "mtime": mtime})
227
228
 
228
229
  # Sort by service name
229
- services.sort(key=lambda x: x["name"])
230
+ services.sort(key=lambda x: str(x["name"]))
230
231
 
231
232
  if output_format == "table":
232
233
  self._print_services_table(services)
@@ -44,6 +44,9 @@ Examples:
44
44
  # Use custom checks from a directory
45
45
  iam-validator validate --path ./policies/ --custom-checks-dir ./my-checks
46
46
 
47
+ # Use offline mode with pre-downloaded AWS service definitions
48
+ iam-validator validate --path ./policies/ --aws-services-dir ./aws_services
49
+
47
50
  # Generate JSON output
48
51
  iam-validator validate --path ./policies/ --format json --output report.json
49
52
 
@@ -163,6 +166,13 @@ Examples:
163
166
  help="Path to directory containing custom checks for auto-discovery",
164
167
  )
165
168
 
169
+ parser.add_argument(
170
+ "--aws-services-dir",
171
+ help="Path to directory containing pre-downloaded AWS service definitions "
172
+ "(enables offline mode, avoids API rate limiting). "
173
+ "Use 'iam-validator download-services' to create this directory.",
174
+ )
175
+
166
176
  parser.add_argument(
167
177
  "--stream",
168
178
  action="store_true",
@@ -242,12 +252,14 @@ Examples:
242
252
  # Validate policies
243
253
  config_path = getattr(args, "config", None)
244
254
  custom_checks_dir = getattr(args, "custom_checks_dir", None)
255
+ aws_services_dir = getattr(args, "aws_services_dir", None)
245
256
  policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
246
257
  results = await validate_policies(
247
258
  policies,
248
259
  config_path=config_path,
249
260
  custom_checks_dir=custom_checks_dir,
250
261
  policy_type=policy_type,
262
+ aws_services_dir=aws_services_dir,
251
263
  )
252
264
 
253
265
  # Generate report (include parsing errors if any)
@@ -1,6 +1,6 @@
1
1
  """Core validation modules."""
2
2
 
3
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
3
+ from iam_validator.core.aws_service import AWSServiceFetcher
4
4
  from iam_validator.core.policy_checks import validate_policies
5
5
  from iam_validator.core.policy_loader import PolicyLoader
6
6
  from iam_validator.core.report import ReportGenerator