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.
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +89 -60
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/RECORD +40 -25
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +9 -3
- iam_validator/checks/action_condition_enforcement.py +164 -2
- iam_validator/checks/action_resource_matching.py +424 -0
- iam_validator/checks/condition_key_validation.py +3 -1
- iam_validator/checks/condition_type_mismatch.py +259 -0
- iam_validator/checks/mfa_condition_check.py +112 -0
- iam_validator/checks/sensitive_action.py +78 -6
- iam_validator/checks/set_operator_validation.py +157 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +35 -1
- iam_validator/commands/cache.py +1 -1
- iam_validator/commands/validate.py +44 -11
- iam_validator/core/aws_fetcher.py +89 -52
- iam_validator/core/check_registry.py +165 -21
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +13 -15
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +104 -0
- iam_validator/core/config/condition_requirements.py +5 -385
- iam_validator/core/{config_loader.py → config/config_loader.py} +3 -0
- iam_validator/core/config/defaults.py +187 -54
- iam_validator/core/config/sensitive_actions.py +620 -81
- iam_validator/core/models.py +14 -1
- iam_validator/core/policy_checks.py +4 -4
- iam_validator/core/pr_commenter.py +1 -1
- iam_validator/sdk/__init__.py +187 -0
- iam_validator/sdk/arn_matching.py +274 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +425 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +31 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +206 -0
- iam_validator/checks/action_resource_constraint.py +0 -151
- iam_validator/core/aws_global_conditions.py +0 -137
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
iam_validator/core/models.py
CHANGED
|
@@ -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
|
-
|
|
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)
|