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
@@ -0,0 +1,31 @@
1
+ """Utility modules for IAM Policy Validator.
2
+
3
+ This package contains reusable utility classes and functions that have
4
+ NO dependencies on IAM-specific logic. These utilities are generic and
5
+ could be used in any Python project.
6
+
7
+ For IAM-specific utilities (that depend on CheckConfig, AWSServiceFetcher, etc.),
8
+ see iam_validator.checks.utils instead.
9
+
10
+ Organization:
11
+ - cache.py: Generic caching implementations (LRUCache with TTL)
12
+ - regex.py: Regex pattern caching and compilation utilities
13
+ """
14
+
15
+ from iam_validator.utils.cache import LRUCache
16
+ from iam_validator.utils.regex import (
17
+ cached_pattern,
18
+ clear_pattern_cache,
19
+ compile_and_cache,
20
+ get_cached_pattern,
21
+ )
22
+
23
+ __all__ = [
24
+ # Cache utilities
25
+ "LRUCache",
26
+ # Regex utilities
27
+ "cached_pattern",
28
+ "compile_and_cache",
29
+ "get_cached_pattern",
30
+ "clear_pattern_cache",
31
+ ]
@@ -0,0 +1,105 @@
1
+ """Caching utilities for IAM Policy Validator.
2
+
3
+ This module provides reusable caching implementations with TTL support.
4
+ """
5
+
6
+ import asyncio
7
+ import time
8
+ from collections import OrderedDict
9
+ from typing import Any
10
+
11
+
12
+ class LRUCache:
13
+ """Thread-safe LRU (Least Recently Used) cache implementation with TTL support.
14
+
15
+ This cache automatically expires items after a specified time-to-live (TTL)
16
+ and evicts the least recently used items when the cache reaches maximum size.
17
+
18
+ Features:
19
+ - Async-safe with lock protection
20
+ - Automatic TTL-based expiration
21
+ - LRU eviction when at capacity
22
+ - O(1) get and set operations
23
+
24
+ Example:
25
+ >>> cache = LRUCache(maxsize=100, ttl=3600)
26
+ >>> await cache.set("key", "value")
27
+ >>> value = await cache.get("key")
28
+ >>> await cache.clear()
29
+
30
+ Args:
31
+ maxsize: Maximum number of items in cache (default: 128)
32
+ ttl: Time to live in seconds (default: 3600 = 1 hour)
33
+ """
34
+
35
+ def __init__(self, maxsize: int = 128, ttl: int = 3600):
36
+ """Initialize LRU cache.
37
+
38
+ Args:
39
+ maxsize: Maximum number of items in cache
40
+ ttl: Time to live in seconds (default: 1 hour)
41
+ """
42
+ self.cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
43
+ self.maxsize = maxsize
44
+ self.ttl = ttl
45
+ self._lock = asyncio.Lock()
46
+
47
+ async def get(self, key: str) -> Any | None:
48
+ """Get item from cache if not expired.
49
+
50
+ Args:
51
+ key: Cache key to retrieve
52
+
53
+ Returns:
54
+ Cached value if found and not expired, None otherwise
55
+
56
+ Note:
57
+ Successfully retrieved items are moved to the end (marked as most recently used).
58
+ """
59
+ async with self._lock:
60
+ if key in self.cache:
61
+ value, timestamp = self.cache[key]
62
+ if time.time() - timestamp < self.ttl:
63
+ # Move to end (most recently used)
64
+ self.cache.move_to_end(key)
65
+ return value
66
+ else:
67
+ # Expired, remove it
68
+ del self.cache[key]
69
+ return None
70
+
71
+ async def set(self, key: str, value: Any) -> None:
72
+ """Set item in cache with current timestamp.
73
+
74
+ Args:
75
+ key: Cache key
76
+ value: Value to cache
77
+
78
+ Note:
79
+ If cache is at capacity, the least recently used item will be evicted.
80
+ """
81
+ async with self._lock:
82
+ if key in self.cache:
83
+ # Move to end if exists
84
+ self.cache.move_to_end(key)
85
+ elif len(self.cache) >= self.maxsize:
86
+ # Remove least recently used (first item)
87
+ self.cache.popitem(last=False)
88
+
89
+ self.cache[key] = (value, time.time())
90
+
91
+ async def clear(self) -> None:
92
+ """Clear the entire cache.
93
+
94
+ Removes all cached items.
95
+ """
96
+ async with self._lock:
97
+ self.cache.clear()
98
+
99
+ def __len__(self) -> int:
100
+ """Return the current number of items in cache."""
101
+ return len(self.cache)
102
+
103
+ def __contains__(self, key: str) -> bool:
104
+ """Check if key exists in cache (does not check expiration)."""
105
+ return key in self.cache
@@ -0,0 +1,206 @@
1
+ """Generic regex pattern caching utilities.
2
+
3
+ This module provides decorators and utilities for efficiently caching compiled
4
+ regex patterns. Compiling regex patterns is expensive, so caching them provides
5
+ significant performance improvements when patterns are reused.
6
+
7
+ Performance benefits:
8
+ - 10-30x faster than re-compiling patterns on each use
9
+ - O(1) lookup for cached patterns via functools.lru_cache
10
+ - Automatic memory management with LRU eviction
11
+ """
12
+
13
+ import re
14
+ from collections.abc import Callable
15
+ from functools import wraps
16
+ from re import Pattern
17
+
18
+
19
+ def cached_pattern(
20
+ flags: int = 0,
21
+ maxsize: int = 128,
22
+ ) -> Callable[[Callable[[], str]], Callable[[], Pattern]]:
23
+ r"""Decorator that caches compiled regex patterns.
24
+
25
+ This decorator transforms a function that returns a regex pattern string
26
+ into a function that returns a compiled regex Pattern object. The compilation
27
+ is cached, so subsequent calls return the same compiled pattern without
28
+ re-compilation overhead.
29
+
30
+ Args:
31
+ flags: Regex compilation flags (e.g., re.IGNORECASE, re.MULTILINE)
32
+ maxsize: Maximum cache size for LRU eviction (default: 128)
33
+
34
+ Returns:
35
+ Decorator function
36
+
37
+ Example:
38
+ >>> @cached_pattern(flags=re.IGNORECASE)
39
+ ... def email_pattern():
40
+ ... return r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
41
+ ...
42
+ >>> pattern = email_pattern() # Compiles and caches
43
+ >>> pattern2 = email_pattern() # Returns cached pattern (same object)
44
+ >>> pattern is pattern2
45
+ True
46
+ >>> pattern.match("user@example.com")
47
+ <re.Match object; span=(0, 17), match='user@example.com'>
48
+
49
+ Example with ARN pattern:
50
+ >>> @cached_pattern()
51
+ ... def arn_pattern():
52
+ ... return r'^arn:aws:iam::[0-9]{12}:role/.*$'
53
+ ...
54
+ >>> arn = arn_pattern()
55
+ >>> arn.match("arn:aws:iam::123456789012:role/MyRole")
56
+ <re.Match object; ...>
57
+
58
+ Performance:
59
+ First call: ~10-50μs (pattern compilation)
60
+ Cached calls: ~0.1-0.5μs (cache lookup) → 20-100x faster
61
+ """
62
+
63
+ def decorator(func: Callable[[], str]) -> Callable[[], Pattern]:
64
+ # Use a cache per function to avoid key collisions
65
+ cache = {}
66
+
67
+ @wraps(func)
68
+ def wrapper() -> Pattern:
69
+ # Use function name as cache key (since each decorated function
70
+ # returns the same pattern string)
71
+ cache_key = func.__name__
72
+
73
+ if cache_key not in cache:
74
+ pattern_str = func()
75
+ cache[cache_key] = re.compile(pattern_str, flags)
76
+
77
+ return cache[cache_key]
78
+
79
+ # Store pattern string as attribute for introspection
80
+ wrapper.pattern_string = func # type: ignore
81
+
82
+ return wrapper
83
+
84
+ return decorator
85
+
86
+
87
+ def compile_and_cache(pattern: str, flags: int = 0, maxsize: int = 512) -> Pattern:
88
+ """Compile a regex pattern with automatic caching.
89
+
90
+ This is a functional interface (not a decorator) that compiles and caches
91
+ regex patterns. Useful for dynamic patterns or one-off compilations.
92
+
93
+ Args:
94
+ pattern: Regex pattern string
95
+ flags: Regex compilation flags (e.g., re.IGNORECASE)
96
+ maxsize: Maximum cache size for LRU eviction
97
+
98
+ Returns:
99
+ Compiled Pattern object
100
+
101
+ Example:
102
+ >>> pattern1 = compile_and_cache(r'\\d+', re.IGNORECASE)
103
+ >>> pattern2 = compile_and_cache(r'\\d+', re.IGNORECASE)
104
+ >>> pattern1 is pattern2 # Same pattern, same flags -> cached
105
+ True
106
+
107
+ >>> # Different flags -> different cached entry
108
+ >>> pattern3 = compile_and_cache(r'\\d+', re.MULTILINE)
109
+ >>> pattern1 is pattern3
110
+ False
111
+
112
+ Note:
113
+ This uses a module-level cache shared across all calls. For function-specific
114
+ caching, use the @cached_pattern decorator instead.
115
+ """
116
+ from functools import lru_cache
117
+
118
+ @lru_cache(maxsize=maxsize)
119
+ def _compile(pattern_str: str, flags: int) -> Pattern:
120
+ return re.compile(pattern_str, flags)
121
+
122
+ return _compile(pattern, flags)
123
+
124
+
125
+ # Singleton instance for shared pattern compilation
126
+ _pattern_cache: dict[tuple[str, int], Pattern] = {}
127
+
128
+
129
+ def get_cached_pattern(pattern: str, flags: int = 0) -> Pattern:
130
+ """Get a compiled pattern from the shared cache.
131
+
132
+ This provides a simple, stateless way to get cached patterns without
133
+ decorators or function calls. Uses a module-level cache.
134
+
135
+ Args:
136
+ pattern: Regex pattern string
137
+ flags: Regex compilation flags
138
+
139
+ Returns:
140
+ Compiled Pattern object (cached)
141
+
142
+ Example:
143
+ >>> pattern = get_cached_pattern(r'^arn:aws:.*$', re.IGNORECASE)
144
+ >>> pattern.match("arn:aws:s3:::bucket")
145
+ <re.Match object; ...>
146
+
147
+ Thread Safety:
148
+ This function is NOT thread-safe. For concurrent use, use
149
+ compile_and_cache() which uses functools.lru_cache (thread-safe).
150
+ """
151
+ cache_key = (pattern, flags)
152
+
153
+ if cache_key not in _pattern_cache:
154
+ _pattern_cache[cache_key] = re.compile(pattern, flags)
155
+
156
+ return _pattern_cache[cache_key]
157
+
158
+
159
+ def clear_pattern_cache() -> None:
160
+ """Clear the shared pattern cache.
161
+
162
+ Useful for testing or memory management.
163
+
164
+ Example:
165
+ >>> get_cached_pattern(r'test')
166
+ >>> len(_pattern_cache)
167
+ 1
168
+ >>> clear_pattern_cache()
169
+ >>> len(_pattern_cache)
170
+ 0
171
+ """
172
+ _pattern_cache.clear()
173
+
174
+
175
+ # Pre-defined common patterns for IAM validation
176
+ # These are compiled once and reused throughout the application
177
+
178
+
179
+ @cached_pattern()
180
+ def wildcard_pattern():
181
+ """Pattern for detecting wildcards (*) in strings."""
182
+ return r"\*"
183
+
184
+
185
+ @cached_pattern()
186
+ def partial_wildcard_pattern():
187
+ """Pattern for detecting partial wildcards (e.g., 's3:Get*')."""
188
+ return r"^[^*]+\*$"
189
+
190
+
191
+ @cached_pattern()
192
+ def arn_base_pattern():
193
+ """Basic ARN structure pattern."""
194
+ return r"^arn:[^:]*:[^:]*:[^:]*:[^:]*:.*$"
195
+
196
+
197
+ @cached_pattern()
198
+ def aws_account_id_pattern():
199
+ """AWS account ID pattern (12 digits)."""
200
+ return r"^[0-9]{12}$"
201
+
202
+
203
+ @cached_pattern(flags=re.IGNORECASE)
204
+ def action_pattern():
205
+ """IAM action pattern (service:Action format)."""
206
+ return r"^[a-z0-9-]+:[a-zA-Z0-9*]+$"
@@ -1,151 +0,0 @@
1
- """Action resource constraint check - validates resource constraints for actions.
2
-
3
- This check ensures that:
4
- - Actions WITHOUT required resource types (empty or missing Resources field in AWS API)
5
- MUST use Resource: "*" because they are account-level operations
6
-
7
- Examples of actions without required resources:
8
- - s3:ListAllMyBuckets (lists all buckets in account)
9
- - iam:ListRoles (lists all roles in account)
10
- - ec2:DescribeInstances (describes instances across all regions)
11
-
12
- These actions cannot target specific resources because they operate at the account level.
13
- """
14
-
15
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
16
- from iam_validator.core.check_registry import CheckConfig, PolicyCheck
17
- from iam_validator.core.models import Statement, ValidationIssue
18
-
19
-
20
- class ActionResourceConstraintCheck(PolicyCheck):
21
- """Validates resource constraints based on action requirements.
22
- This check ensures that actions without required resource types use Resource: "*".
23
-
24
- Examples of such actions include s3:ListAllMyBuckets, iam:ListRoles, etc.
25
- """
26
-
27
- @property
28
- def check_id(self) -> str:
29
- return "action_resource_constraint"
30
-
31
- @property
32
- def description(self) -> str:
33
- return "Validates that actions without required resource types use Resource: '*'"
34
-
35
- @property
36
- def default_severity(self) -> str:
37
- return "error"
38
-
39
- async def execute(
40
- self,
41
- statement: Statement,
42
- statement_idx: int,
43
- fetcher: AWSServiceFetcher,
44
- config: CheckConfig,
45
- ) -> list[ValidationIssue]:
46
- """Execute action resource constraint validation on a statement."""
47
- issues = []
48
-
49
- # Only check Allow statements
50
- if statement.effect != "Allow":
51
- return issues
52
-
53
- # Get actions and resources from statement
54
- actions = statement.get_actions()
55
- resources = statement.get_resources()
56
- statement_sid = statement.sid
57
- line_number = statement.line_number
58
-
59
- # Skip if no actions or wildcard action
60
- if not actions or "*" in actions:
61
- return issues
62
-
63
- # Skip if already using wildcard resource (this is correct for these actions)
64
- if "*" in resources:
65
- return issues
66
-
67
- # Check each action for resource requirements
68
- actions_without_required_resources = []
69
-
70
- for action in actions:
71
- # Skip wildcard actions
72
- if "*" in action:
73
- continue
74
-
75
- try:
76
- # Parse action to get service and action name
77
- service_prefix, action_name = fetcher.parse_action(action)
78
-
79
- # Fetch service detail to check resource requirements
80
- service_detail = await fetcher.fetch_service_by_name(service_prefix)
81
-
82
- # Check if action exists
83
- if action_name not in service_detail.actions:
84
- # Action doesn't exist - skip (will be caught by action_validation_check)
85
- continue
86
-
87
- action_detail = service_detail.actions[action_name]
88
-
89
- # Check if action has NO required resources (empty or missing Resources field)
90
- has_no_required_resources = (
91
- not action_detail.resources or len(action_detail.resources) == 0
92
- )
93
-
94
- if has_no_required_resources:
95
- actions_without_required_resources.append(action)
96
-
97
- except ValueError:
98
- # Invalid action format - skip (will be caught by action_validation_check)
99
- continue
100
- except Exception:
101
- # Service not found or other error - skip
102
- continue
103
-
104
- # If we found actions without required resources, report the issue
105
- if actions_without_required_resources:
106
- # Get a sample of the resources to show in error message
107
- resource_sample = resources[:3] if len(resources) > 3 else resources
108
- resource_display = ", ".join(f'"{r}"' for r in resource_sample)
109
- if len(resources) > 3:
110
- resource_display += f", ... ({len(resources) - 3} more)"
111
-
112
- # Format action list
113
- action_list = ", ".join(f'"{a}"' for a in actions_without_required_resources)
114
-
115
- # Determine message based on how many actions are affected
116
- if len(actions_without_required_resources) == 1:
117
- message = (
118
- f"Action {action_list} does not operate on specific resources "
119
- f'and requires Resource: "*"'
120
- )
121
- suggestion = (
122
- f"Action {action_list} is an account-level operation that cannot target "
123
- 'specific resources. Move this action to a separate statement with Resource: "*", '
124
- "and keep resource-specific actions in another statement with their specific ARNs"
125
- )
126
- else:
127
- message = (
128
- f"Actions {action_list} do not operate on specific resources "
129
- f'and require Resource: "*"'
130
- )
131
- suggestion = (
132
- "These actions are account-level operations that cannot target "
133
- 'specific resources. Move these actions to a dedicated statement with Resource: "*", '
134
- "and keep resource-specific actions in separate statements with their specific ARNs"
135
- )
136
-
137
- issues.append(
138
- ValidationIssue(
139
- severity=self.get_severity(config),
140
- statement_sid=statement_sid,
141
- statement_index=statement_idx,
142
- issue_type="invalid_resource_constraint",
143
- message=message,
144
- action=action_list,
145
- resource=resource_display,
146
- suggestion=suggestion,
147
- line_number=line_number,
148
- )
149
- )
150
-
151
- return issues
@@ -1,137 +0,0 @@
1
- """
2
- AWS Global Condition Keys Management.
3
-
4
- Provides access to the list of valid AWS global condition keys
5
- that can be used across all AWS services.
6
-
7
- Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
8
- Last updated: 2025-01-17
9
- """
10
-
11
- import re
12
- from typing import Any
13
-
14
- # AWS Global Condition Keys
15
- # These condition keys are available for use in IAM policies across all AWS services
16
- AWS_GLOBAL_CONDITION_KEYS = {
17
- # Properties of the Principal
18
- "aws:PrincipalArn", # ARN of the principal making the request
19
- "aws:PrincipalAccount", # Account to which the requesting principal belongs
20
- "aws:PrincipalOrgPaths", # AWS Organizations path for the principal
21
- "aws:PrincipalOrgID", # Organization identifier of the principal
22
- "aws:PrincipalIsAWSService", # Checks if call is made directly by AWS service principal
23
- "aws:PrincipalServiceName", # Service principal name making the request
24
- "aws:PrincipalServiceNamesList", # List of all service principal names
25
- "aws:PrincipalType", # Type of principal making the request
26
- "aws:userid", # Principal identifier of the requester
27
- "aws:username", # User name of the requester
28
- # Properties of a Role Session
29
- "aws:AssumedRoot", # Checks if request used AssumeRoot for privileged access
30
- "aws:FederatedProvider", # Principal's issuing identity provider
31
- "aws:TokenIssueTime", # When temporary security credentials were issued
32
- "aws:MultiFactorAuthAge", # Seconds since MFA authorization
33
- "aws:MultiFactorAuthPresent", # Whether MFA was used for temporary credentials
34
- "aws:ChatbotSourceArn", # Source chat configuration ARN
35
- "aws:Ec2InstanceSourceVpc", # VPC where EC2 IAM role credentials were delivered
36
- "aws:Ec2InstanceSourcePrivateIPv4", # Private IPv4 of EC2 instance
37
- "aws:SourceIdentity", # Source identity set when assuming a role
38
- "ec2:RoleDelivery", # Instance metadata service version
39
- # Network Properties
40
- "aws:SourceIp", # Requester's IP address (IPv4/IPv6)
41
- "aws:SourceVpc", # VPC through which request travels
42
- "aws:SourceVpce", # VPC endpoint identifier
43
- "aws:VpceAccount", # AWS account owning the VPC endpoint
44
- "aws:VpceOrgID", # Organization ID of VPC endpoint owner
45
- "aws:VpceOrgPaths", # AWS Organizations path of VPC endpoint
46
- "aws:VpcSourceIp", # IP address from VPC endpoint request
47
- # Resource Properties
48
- "aws:ResourceAccount", # Resource owner's AWS account ID
49
- "aws:ResourceOrgID", # Organization ID of resource owner
50
- "aws:ResourceOrgPaths", # AWS Organizations path of resource
51
- # Request Properties
52
- "aws:CurrentTime", # Current date and time
53
- "aws:EpochTime", # Request timestamp in epoch format
54
- "aws:referer", # HTTP referer header value (note: lowercase 'r')
55
- "aws:Referer", # HTTP referer header value (alternate capitalization)
56
- "aws:RequestedRegion", # AWS Region for the request
57
- "aws:TagKeys", # Tag keys present in request
58
- "aws:SecureTransport", # Whether HTTPS was used
59
- "aws:SourceAccount", # Account making the request
60
- "aws:SourceArn", # ARN of request source
61
- "aws:SourceOrgID", # Organization ID of request source
62
- "aws:SourceOrgPaths", # Organization paths of request source
63
- "aws:UserAgent", # HTTP user agent string
64
- # Cross-Service Keys
65
- "aws:CalledVia", # Services called in request chain
66
- "aws:CalledViaFirst", # First service in call chain
67
- "aws:CalledViaLast", # Last service in call chain
68
- "aws:ViaAWSService", # Whether AWS service made the request
69
- }
70
-
71
- # Patterns that should be recognized (wildcards and tag-based keys)
72
- # These allow things like aws:RequestTag/Department or aws:PrincipalTag/Environment
73
- AWS_CONDITION_KEY_PATTERNS = [
74
- {
75
- "pattern": r"^aws:RequestTag/[a-zA-Z0-9+\-=._:/@]+$",
76
- "description": "Tag keys in the request (for tag-based access control)",
77
- },
78
- {
79
- "pattern": r"^aws:ResourceTag/[a-zA-Z0-9+\-=._:/@]+$",
80
- "description": "Tags on the resource being accessed",
81
- },
82
- {
83
- "pattern": r"^aws:PrincipalTag/[a-zA-Z0-9+\-=._:/@]+$",
84
- "description": "Tags attached to the principal making the request",
85
- },
86
- ]
87
-
88
-
89
- class AWSGlobalConditions:
90
- """Manages AWS global condition keys."""
91
-
92
- def __init__(self):
93
- """Initialize with global condition keys."""
94
- self._global_keys: set[str] = AWS_GLOBAL_CONDITION_KEYS.copy()
95
- self._patterns: list[dict[str, Any]] = AWS_CONDITION_KEY_PATTERNS.copy()
96
-
97
- def is_valid_global_key(self, condition_key: str) -> bool:
98
- """
99
- Check if a condition key is a valid AWS global condition key.
100
-
101
- Args:
102
- condition_key: The condition key to validate (e.g., "aws:SourceIp")
103
-
104
- Returns:
105
- True if valid global condition key, False otherwise
106
- """
107
- # Check exact matches first
108
- if condition_key in self._global_keys:
109
- return True
110
-
111
- # Check patterns (for tags and wildcards)
112
- for pattern_config in self._patterns:
113
- pattern = pattern_config["pattern"]
114
- if re.match(pattern, condition_key):
115
- return True
116
-
117
- return False
118
-
119
- def get_all_keys(self) -> set[str]:
120
- """Get all explicit global condition keys."""
121
- return self._global_keys.copy()
122
-
123
- def get_patterns(self) -> list[dict[str, Any]]:
124
- """Get all condition key patterns."""
125
- return self._patterns.copy()
126
-
127
-
128
- # Singleton instance
129
- _global_conditions_instance = None
130
-
131
-
132
- def get_global_conditions() -> AWSGlobalConditions:
133
- """Get singleton instance of AWSGlobalConditions."""
134
- global _global_conditions_instance
135
- if _global_conditions_instance is None:
136
- _global_conditions_instance = AWSGlobalConditions()
137
- return _global_conditions_instance