iam-policy-validator 1.5.0__py3-none-any.whl → 1.6.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 (42) hide show
  1. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
  2. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +9 -3
  5. iam_validator/checks/action_condition_enforcement.py +164 -2
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +3 -1
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/mfa_condition_check.py +112 -0
  10. iam_validator/checks/sensitive_action.py +78 -6
  11. iam_validator/checks/set_operator_validation.py +157 -0
  12. iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
  13. iam_validator/commands/cache.py +1 -1
  14. iam_validator/commands/validate.py +44 -11
  15. iam_validator/core/aws_fetcher.py +89 -52
  16. iam_validator/core/check_registry.py +165 -21
  17. iam_validator/core/condition_validators.py +626 -0
  18. iam_validator/core/config/__init__.py +13 -15
  19. iam_validator/core/config/aws_global_conditions.py +160 -0
  20. iam_validator/core/config/category_suggestions.py +104 -0
  21. iam_validator/core/config/condition_requirements.py +5 -385
  22. iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
  23. iam_validator/core/config/defaults.py +187 -54
  24. iam_validator/core/config/sensitive_actions.py +620 -81
  25. iam_validator/core/models.py +14 -1
  26. iam_validator/core/policy_checks.py +4 -4
  27. iam_validator/core/pr_commenter.py +1 -1
  28. iam_validator/sdk/__init__.py +187 -0
  29. iam_validator/sdk/arn_matching.py +274 -0
  30. iam_validator/sdk/context.py +222 -0
  31. iam_validator/sdk/exceptions.py +48 -0
  32. iam_validator/sdk/helpers.py +177 -0
  33. iam_validator/sdk/policy_utils.py +425 -0
  34. iam_validator/sdk/shortcuts.py +283 -0
  35. iam_validator/utils/__init__.py +31 -0
  36. iam_validator/utils/cache.py +105 -0
  37. iam_validator/utils/regex.py +206 -0
  38. iam_validator/checks/action_resource_constraint.py +0 -151
  39. iam_validator/core/aws_global_conditions.py +0 -137
  40. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  41. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  42. {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -45,9 +45,22 @@ class ResourceType(BaseModel):
45
45
  model_config = ConfigDict(populate_by_name=True)
46
46
 
47
47
  name: str = Field(alias="Name")
48
- arn_pattern: str | None = Field(default=None, alias="ARNPattern")
48
+ arn_formats: list[str] | None = Field(default=None, alias="ARNFormats")
49
49
  condition_keys: list[str] | None = Field(default_factory=list, alias="ConditionKeys")
50
50
 
51
+ @property
52
+ def arn_pattern(self) -> str | None:
53
+ """
54
+ Get the first ARN format for backwards compatibility.
55
+
56
+ AWS provides ARN formats as an array (ARNFormats), but most code
57
+ just needs a single pattern. This property returns the first one.
58
+
59
+ Returns:
60
+ First ARN format string, or None if no formats are defined
61
+ """
62
+ return self.arn_formats[0] if self.arn_formats else None
63
+
51
64
 
52
65
  class ConditionKey(BaseModel):
53
66
  """Details about an AWS condition key."""
@@ -491,7 +491,7 @@ async def validate_policies(
491
491
  if not use_registry:
492
492
  # Legacy path - use old PolicyValidator
493
493
  # Load config for cache settings even in legacy mode
494
- from iam_validator.core.config_loader import ConfigLoader
494
+ from iam_validator.core.config.config_loader import ConfigLoader
495
495
 
496
496
  config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
497
497
  cache_enabled = config.get_setting("cache_enabled", True)
@@ -519,7 +519,7 @@ async def validate_policies(
519
519
 
520
520
  # New path - use CheckRegistry system
521
521
  from iam_validator.core.check_registry import create_default_registry
522
- from iam_validator.core.config_loader import ConfigLoader
522
+ from iam_validator.core.config.config_loader import ConfigLoader
523
523
 
524
524
  # Load configuration
525
525
  config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
@@ -630,8 +630,8 @@ async def _validate_policy_with_registry(
630
630
 
631
631
  # Execute all statement-level checks for each statement
632
632
  for idx, statement in enumerate(policy.statement):
633
- # Execute all registered checks in parallel
634
- issues = await registry.execute_checks_parallel(statement, idx, fetcher)
633
+ # Execute all registered checks in parallel (with ignore_patterns filtering)
634
+ issues = await registry.execute_checks_parallel(statement, idx, fetcher, policy_file)
635
635
 
636
636
  # Add issues to result
637
637
  result.issues.extend(issues)
@@ -313,7 +313,7 @@ async def post_report_to_pr(
313
313
  report = ValidationReport.model_validate(report_data)
314
314
 
315
315
  # Load config to get fail_on_severity setting
316
- from iam_validator.core.config_loader import ConfigLoader
316
+ from iam_validator.core.config.config_loader import ConfigLoader
317
317
 
318
318
  config = ConfigLoader.load_config(config_path)
319
319
  fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
@@ -0,0 +1,187 @@
1
+ """
2
+ IAM Policy Validator SDK - Public API for library usage.
3
+
4
+ This module provides the complete public API for using IAM Policy Validator
5
+ as a Python library. It exposes both high-level convenience functions and
6
+ low-level components for custom integrations.
7
+
8
+ Quick Start:
9
+ Basic validation:
10
+ >>> from iam_validator.sdk import validate_file
11
+ >>> result = await validate_file("policy.json")
12
+ >>> print(f"Valid: {result.is_valid}")
13
+
14
+ With context manager:
15
+ >>> from iam_validator.sdk import validator
16
+ >>> async with validator() as v:
17
+ ... result = await v.validate_file("policy.json")
18
+ ... v.generate_report([result])
19
+
20
+ Policy manipulation:
21
+ >>> from iam_validator.sdk import parse_policy, get_policy_summary
22
+ >>> policy = parse_policy(policy_json)
23
+ >>> summary = get_policy_summary(policy)
24
+ >>> print(f"Actions: {summary['action_count']}")
25
+
26
+ Custom check development:
27
+ >>> from iam_validator.sdk import PolicyCheck, CheckHelper
28
+ >>> class MyCheck(PolicyCheck):
29
+ ... @property
30
+ ... def check_id(self) -> str:
31
+ ... return "my_check"
32
+ ... async def execute(self, statement, idx, fetcher, config):
33
+ ... helper = CheckHelper(fetcher)
34
+ ... # Use helper.arn_matches(), helper.create_issue(), etc.
35
+ ... return []
36
+ """
37
+
38
+ # === High-level validation functions (shortcuts) ===
39
+ # === AWS utilities ===
40
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
41
+
42
+ # === Core validation components (for advanced usage) ===
43
+ from iam_validator.core.check_registry import CheckRegistry, PolicyCheck
44
+
45
+ # === ValidatorConfiguration ===
46
+ from iam_validator.core.config.config_loader import ValidatorConfig, load_validator_config
47
+
48
+ # === Reporting ===
49
+ from iam_validator.core.formatters.csv import CSVFormatter
50
+ from iam_validator.core.formatters.html import HTMLFormatter
51
+ from iam_validator.core.formatters.json import JSONFormatter
52
+ from iam_validator.core.formatters.markdown import MarkdownFormatter
53
+ from iam_validator.core.formatters.sarif import SARIFFormatter
54
+
55
+ # === Models (for type hints and inspection) ===
56
+ from iam_validator.core.models import (
57
+ IAMPolicy,
58
+ PolicyValidationResult,
59
+ Statement,
60
+ ValidationIssue,
61
+ )
62
+ from iam_validator.core.policy_checks import validate_policies
63
+ from iam_validator.core.policy_loader import PolicyLoader
64
+ from iam_validator.core.report import ReportGenerator
65
+
66
+ # === ARN matching utilities ===
67
+ from iam_validator.sdk.arn_matching import (
68
+ arn_matches,
69
+ arn_strictly_valid,
70
+ convert_aws_pattern_to_wildcard,
71
+ is_glob_match,
72
+ )
73
+
74
+ # === Context managers ===
75
+ from iam_validator.sdk.context import (
76
+ ValidationContext,
77
+ validator,
78
+ validator_from_config,
79
+ )
80
+
81
+ # === Public exceptions ===
82
+ from iam_validator.sdk.exceptions import (
83
+ AWSServiceError,
84
+ ConfigurationError,
85
+ IAMValidatorError,
86
+ InvalidPolicyFormatError,
87
+ PolicyLoadError,
88
+ PolicyValidationError,
89
+ UnsupportedPolicyTypeError,
90
+ )
91
+
92
+ # === Custom check development ===
93
+ from iam_validator.sdk.helpers import CheckHelper, expand_actions
94
+
95
+ # === Policy manipulation utilities ===
96
+ from iam_validator.sdk.policy_utils import (
97
+ extract_actions,
98
+ extract_condition_keys,
99
+ extract_resources,
100
+ find_statements_with_action,
101
+ find_statements_with_resource,
102
+ get_policy_summary,
103
+ has_public_access,
104
+ is_resource_policy,
105
+ merge_policies,
106
+ normalize_policy,
107
+ parse_policy,
108
+ policy_to_dict,
109
+ policy_to_json,
110
+ )
111
+ from iam_validator.sdk.shortcuts import (
112
+ count_issues_by_severity,
113
+ get_issues,
114
+ quick_validate,
115
+ validate_directory,
116
+ validate_file,
117
+ validate_json,
118
+ )
119
+
120
+ __all__ = [
121
+ # === High-level shortcuts ===
122
+ "validate_file",
123
+ "validate_directory",
124
+ "validate_json",
125
+ "quick_validate",
126
+ "get_issues",
127
+ "count_issues_by_severity",
128
+ # === Context managers ===
129
+ "validator",
130
+ "validator_from_config",
131
+ "ValidationContext",
132
+ # === Policy utilities ===
133
+ "parse_policy",
134
+ "normalize_policy",
135
+ "extract_actions",
136
+ "extract_resources",
137
+ "extract_condition_keys",
138
+ "find_statements_with_action",
139
+ "find_statements_with_resource",
140
+ "merge_policies",
141
+ "get_policy_summary",
142
+ "policy_to_json",
143
+ "policy_to_dict",
144
+ "is_resource_policy",
145
+ "has_public_access",
146
+ # === ARN utilities ===
147
+ "arn_matches",
148
+ "arn_strictly_valid",
149
+ "is_glob_match",
150
+ "convert_aws_pattern_to_wildcard",
151
+ # === Custom check development ===
152
+ "PolicyCheck",
153
+ "CheckRegistry",
154
+ "CheckHelper",
155
+ "expand_actions",
156
+ # === Core validation (advanced) ===
157
+ "validate_policies",
158
+ "PolicyLoader",
159
+ # === Reporting ===
160
+ "ReportGenerator",
161
+ "JSONFormatter",
162
+ "HTMLFormatter",
163
+ "CSVFormatter",
164
+ "MarkdownFormatter",
165
+ "SARIFFormatter",
166
+ # === Models ===
167
+ "ValidationIssue",
168
+ "PolicyValidationResult",
169
+ "IAMPolicy",
170
+ "Statement",
171
+ # === ValidatorConfiguration ===
172
+ "ValidatorConfig",
173
+ "load_validator_config",
174
+ # === AWS utilities ===
175
+ "AWSServiceFetcher",
176
+ # === Exceptions ===
177
+ "IAMValidatorError",
178
+ "PolicyLoadError",
179
+ "PolicyValidationError",
180
+ "ConfigurationError",
181
+ "AWSServiceError",
182
+ "InvalidPolicyFormatError",
183
+ "UnsupportedPolicyTypeError",
184
+ ]
185
+
186
+ # SDK version
187
+ __version__ = "0.1.0"
@@ -0,0 +1,274 @@
1
+ """
2
+ ARN pattern matching utilities for IAM policy validation.
3
+
4
+ This module provides functions for matching ARN patterns with glob support.
5
+ Portions of this code are derived from or inspired by Parliament's ARN matching
6
+ implementation.
7
+
8
+ Original work Copyright 2019 Duo Security (BSD 3-Clause License)
9
+ Modifications and additions Copyright 2024 (MIT License)
10
+
11
+ Parliament: https://github.com/duo-labs/parliament
12
+ License: https://github.com/duo-labs/parliament/blob/master/LICENSE
13
+
14
+ The is_glob_match() function is adapted from Parliament's implementation.
15
+ See: https://github.com/duo-labs/parliament/issues/36#issuecomment-574001764
16
+ """
17
+
18
+ import re
19
+
20
+
21
+ def arn_matches(
22
+ arn_pattern: str,
23
+ arn: str,
24
+ resource_type: str | None = None,
25
+ ) -> bool:
26
+ """
27
+ Check if an ARN matches a pattern with glob support.
28
+
29
+ Both the pattern and ARN can contain wildcards (*). This is useful for
30
+ validating that policy resources match the required format for actions.
31
+
32
+ Args:
33
+ arn_pattern: ARN pattern (e.g., from AWS docs), can have wildcards
34
+ arn: ARN from policy, can have wildcards
35
+ resource_type: Optional resource type (e.g., "bucket", "object") for special handling
36
+
37
+ Returns:
38
+ True if ARN could match the pattern
39
+
40
+ Examples:
41
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::bucket/key")
42
+ True
43
+
44
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::bucket")
45
+ False
46
+
47
+ >>> # Both can have wildcards
48
+ >>> arn_matches("arn:*:s3:::*/*", "arn:aws:s3:::*personalize*")
49
+ True # Could match "arn:aws:s3:::personalize/file"
50
+
51
+ >>> # Special case: S3 buckets can't have /
52
+ >>> arn_matches("arn:*:s3:::*", "arn:aws:s3:::bucket/key", resource_type="bucket")
53
+ False
54
+ """
55
+ # Wildcard shortcuts
56
+ if arn_pattern == "*" or arn == "*":
57
+ return True
58
+
59
+ # Special case for S3 buckets - no "/" allowed
60
+ if resource_type and "bucket" in resource_type.lower():
61
+ # Strip variables like ${aws:username} before checking
62
+ arn_without_vars = _strip_variables_from_arn(arn)
63
+ if "/" in arn_without_vars:
64
+ return False
65
+
66
+ # Parse ARN into parts
67
+ pattern_parts = arn_pattern.split(":")
68
+ arn_parts = arn.split(":")
69
+
70
+ # ARN must have at least 6 parts: arn:partition:service:region:account:resource
71
+ if len(pattern_parts) < 6 or len(arn_parts) < 6:
72
+ # Invalid ARN format
73
+ return False
74
+
75
+ # Match first 5 parts (arn:partition:service:region:account)
76
+ for i in range(5):
77
+ pattern_part = pattern_parts[i]
78
+ arn_part = arn_parts[i]
79
+
80
+ # Pattern wildcard matches any non-empty value
81
+ if pattern_part == "*" and arn_part != "":
82
+ continue
83
+ # ARN wildcard matches anything
84
+ elif arn_part == "*":
85
+ continue
86
+ # Exact match
87
+ elif pattern_part == arn_part:
88
+ continue
89
+ else:
90
+ # No match
91
+ return False
92
+
93
+ # Match resource ID (everything after 5th colon)
94
+ pattern_id = ":".join(pattern_parts[5:])
95
+ arn_id = ":".join(arn_parts[5:])
96
+
97
+ # Replace variables like [key] with wildcard
98
+ arn_id = re.sub(r"\[.+?\]", "*", arn_id)
99
+
100
+ return is_glob_match(pattern_id, arn_id)
101
+
102
+
103
+ def arn_strictly_valid(
104
+ arn_pattern: str,
105
+ arn: str,
106
+ resource_type: str | None = None,
107
+ ) -> bool:
108
+ """
109
+ Strictly validate ARN against pattern with resource type checking.
110
+
111
+ This is stricter than arn_matches() and enforces:
112
+ - Resource type must be present and match
113
+ - No wildcards in resource type portion
114
+ - No extra colons in resource ID
115
+
116
+ Args:
117
+ arn_pattern: ARN pattern from AWS service definition
118
+ arn: ARN from policy
119
+ resource_type: Optional resource type for additional validation
120
+
121
+ Returns:
122
+ True if ARN strictly matches the pattern
123
+
124
+ Examples:
125
+ >>> # Valid: has resource type "user"
126
+ >>> arn_strictly_valid("arn:*:iam::*:user/*", "arn:aws:iam::123456789012:user/alice")
127
+ True
128
+
129
+ >>> # Invalid: missing resource type
130
+ >>> arn_strictly_valid("arn:*:iam::*:user/*", "arn:aws:iam::123456789012:u*")
131
+ False
132
+ """
133
+ # First check basic match
134
+ if not arn_matches(arn_pattern, arn, resource_type):
135
+ return False
136
+
137
+ # Parse ARNs
138
+ pattern_parts = arn_pattern.split(":")
139
+ arn_parts = arn.split(":")
140
+
141
+ pattern_id = ":".join(pattern_parts[5:])
142
+ arn_id = ":".join(arn_parts[5:])
143
+
144
+ # Check if pattern has a resource type component
145
+ # Example: "user/alice" has resource type "user"
146
+ # Regex: resource type word followed by : or / (excluding patterns starting with *)
147
+ resource_type_match = re.match(r"(^[^\*][\w-]+)[\/\:](.+)", pattern_id)
148
+
149
+ if resource_type_match and arn_id != "*":
150
+ expected_resource_type = resource_type_match.group(1)
151
+
152
+ # ARN must start with the same resource type
153
+ # Invalid: arn:aws:iam::123456789012:u* (wildcards not allowed in resource type)
154
+ if not arn_id.startswith(expected_resource_type):
155
+ return False
156
+
157
+ # Check for invalid colons in resource ID
158
+ # Strip variables first
159
+ arn_id_without_vars = _strip_variables_from_arn(arn_id)
160
+
161
+ # If ARN has colons but pattern doesn't, it's invalid
162
+ if ":" in arn_id_without_vars and ":" not in pattern_id:
163
+ return False
164
+
165
+ return True
166
+
167
+
168
+ def is_glob_match(s1: str, s2: str) -> bool:
169
+ """
170
+ Recursive glob pattern matching for two strings.
171
+
172
+ Both strings can contain wildcards (*). This implements a recursive
173
+ algorithm that handles all combinations of wildcard positions.
174
+
175
+ Args:
176
+ s1: First string (can contain *)
177
+ s2: Second string (can contain *)
178
+
179
+ Returns:
180
+ True if strings could match
181
+
182
+ Examples:
183
+ >>> is_glob_match("*/*", "*personalize*")
184
+ True
185
+
186
+ >>> is_glob_match("*/*", "mybucket")
187
+ False
188
+
189
+ >>> is_glob_match("*mybucket", "*myotherthing")
190
+ False
191
+
192
+ >>> is_glob_match("test*", "test123")
193
+ True
194
+
195
+ Note:
196
+ This is adapted from Parliament's implementation:
197
+ https://github.com/duo-labs/parliament/issues/36#issuecomment-574001764
198
+ """
199
+ # If strings are equal, TRUE
200
+ if s1 == s2:
201
+ return True
202
+
203
+ # If either string is all wildcards, TRUE
204
+ if (s1 and all(c == "*" for c in s1)) or (s2 and all(c == "*" for c in s2)):
205
+ return True
206
+
207
+ # If either string is empty, FALSE (already handled both empty above)
208
+ if not s1 or not s2:
209
+ return False
210
+
211
+ # At this point, both strings are non-empty
212
+ # If both start with *, TRUE if match first with remainder of second
213
+ # or second with remainder of first
214
+ if s1[0] == s2[0] == "*":
215
+ return is_glob_match(s1[1:], s2) or is_glob_match(s1, s2[1:])
216
+
217
+ # If s1 starts with *, TRUE if remainder of s1 matches any suffix of s2
218
+ if s1[0] == "*":
219
+ return any(is_glob_match(s1[1:], s2[i:]) for i in range(len(s2) + 1))
220
+
221
+ # If s2 starts with *, TRUE if remainder of s2 matches any suffix of s1
222
+ if s2[0] == "*":
223
+ return any(is_glob_match(s1[i:], s2[1:]) for i in range(len(s1) + 1))
224
+
225
+ # TRUE if both have same first character and remainders match
226
+ return s1[0] == s2[0] and is_glob_match(s1[1:], s2[1:])
227
+
228
+
229
+ def _strip_variables_from_arn(arn: str, replace_with: str = "") -> str:
230
+ """
231
+ Strip AWS policy variables from ARN.
232
+
233
+ Examples:
234
+ ${aws:username} → ""
235
+ bucket-${aws:username} → "bucket-"
236
+
237
+ Args:
238
+ arn: ARN string that may contain variables
239
+ replace_with: What to replace variables with (default: empty string)
240
+
241
+ Returns:
242
+ ARN with variables replaced
243
+ """
244
+ # Match ${aws.whatever} or ${aws:whatever}
245
+ return re.sub(r"\$\{aws[\.:][\w\/]+\}", replace_with, arn)
246
+
247
+
248
+ def convert_aws_pattern_to_wildcard(pattern: str) -> str:
249
+ """
250
+ Convert AWS ARN pattern format to wildcard pattern for matching.
251
+
252
+ AWS provides ARN patterns with placeholders like ${Partition}, ${BucketName},
253
+ etc. This function converts them to wildcard (*) patterns that can be used
254
+ with arn_matches() and arn_strictly_valid().
255
+
256
+ Args:
257
+ pattern: ARN pattern from AWS service definition
258
+
259
+ Returns:
260
+ ARN pattern with placeholders replaced by wildcards
261
+
262
+ Examples:
263
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:s3:::${BucketName}/${ObjectName}")
264
+ "arn:*:s3:::*/*"
265
+
266
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:iam::${Account}:user/${UserNameWithPath}")
267
+ "arn:*:iam::*:user/*"
268
+
269
+ >>> convert_aws_pattern_to_wildcard("arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId}")
270
+ "arn:*:ec2:*:*:instance/*"
271
+ """
272
+ # Replace all ${...} placeholders with *
273
+ # Matches ${Partition}, ${BucketName}, ${Account}, etc.
274
+ return re.sub(r"\$\{[^}]+\}", "*", pattern)