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
|
@@ -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
|
|
File without changes
|
{iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.5.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|