iam-policy-validator 1.14.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.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -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 +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convenience functions for common validation scenarios.
|
|
3
|
+
|
|
4
|
+
This module provides high-level, easy-to-use functions for common IAM policy
|
|
5
|
+
validation tasks without requiring deep knowledge of the internal API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from iam_validator.core.config.config_loader import ValidatorConfig
|
|
11
|
+
from iam_validator.core.models import PolicyValidationResult, ValidationIssue
|
|
12
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
13
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def validate_file(
|
|
17
|
+
file_path: str | Path,
|
|
18
|
+
config_path: str | None = None,
|
|
19
|
+
config: ValidatorConfig | None = None,
|
|
20
|
+
) -> PolicyValidationResult:
|
|
21
|
+
"""
|
|
22
|
+
Validate a single IAM policy file.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
file_path: Path to the policy file (JSON or YAML)
|
|
26
|
+
config_path: Optional path to configuration file
|
|
27
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
PolicyValidationResult for the policy
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> result = await validate_file("policy.json")
|
|
34
|
+
>>> if result.is_valid:
|
|
35
|
+
... print("Policy is valid!")
|
|
36
|
+
>>> else:
|
|
37
|
+
... for issue in result.issues:
|
|
38
|
+
... print(f"{issue.severity}: {issue.message}")
|
|
39
|
+
"""
|
|
40
|
+
loader = PolicyLoader()
|
|
41
|
+
policies = loader.load_from_path(str(file_path))
|
|
42
|
+
|
|
43
|
+
if not policies:
|
|
44
|
+
raise ValueError(f"No IAM policies found in {file_path}")
|
|
45
|
+
|
|
46
|
+
results = await validate_policies(
|
|
47
|
+
policies,
|
|
48
|
+
config_path=config_path,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
results[0]
|
|
53
|
+
if results
|
|
54
|
+
else PolicyValidationResult(
|
|
55
|
+
policy_file=str(file_path),
|
|
56
|
+
is_valid=False,
|
|
57
|
+
issues=[],
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def validate_directory(
|
|
63
|
+
dir_path: str | Path,
|
|
64
|
+
config_path: str | None = None,
|
|
65
|
+
config: ValidatorConfig | None = None,
|
|
66
|
+
recursive: bool = True,
|
|
67
|
+
) -> list[PolicyValidationResult]:
|
|
68
|
+
"""
|
|
69
|
+
Validate all IAM policies in a directory.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
dir_path: Path to directory containing policy files
|
|
73
|
+
config_path: Optional path to configuration file
|
|
74
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
75
|
+
recursive: Whether to search subdirectories (default: True)
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of PolicyValidationResults for all policies found
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> results = await validate_directory("./policies")
|
|
82
|
+
>>> valid_count = sum(1 for r in results if r.is_valid)
|
|
83
|
+
>>> print(f"{valid_count}/{len(results)} policies are valid")
|
|
84
|
+
"""
|
|
85
|
+
loader = PolicyLoader()
|
|
86
|
+
policies = loader.load_from_path(str(dir_path))
|
|
87
|
+
|
|
88
|
+
if not policies:
|
|
89
|
+
raise ValueError(f"No IAM policies found in {dir_path}")
|
|
90
|
+
|
|
91
|
+
return await validate_policies(
|
|
92
|
+
policies,
|
|
93
|
+
config_path=config_path,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def validate_json(
|
|
98
|
+
policy_json: dict,
|
|
99
|
+
policy_name: str = "inline-policy",
|
|
100
|
+
config_path: str | None = None,
|
|
101
|
+
config: ValidatorConfig | None = None,
|
|
102
|
+
) -> PolicyValidationResult:
|
|
103
|
+
"""
|
|
104
|
+
Validate an IAM policy from a Python dictionary.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
policy_json: IAM policy as a Python dict
|
|
108
|
+
policy_name: Name to identify this policy in results
|
|
109
|
+
config_path: Optional path to configuration file
|
|
110
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
PolicyValidationResult for the policy
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> policy = {
|
|
117
|
+
... "Version": "2012-10-17",
|
|
118
|
+
... "Statement": [{
|
|
119
|
+
... "Effect": "Allow",
|
|
120
|
+
... "Action": "s3:GetObject",
|
|
121
|
+
... "Resource": "arn:aws:s3:::my-bucket/*"
|
|
122
|
+
... }]
|
|
123
|
+
... }
|
|
124
|
+
>>> result = await validate_json(policy)
|
|
125
|
+
>>> print(f"Valid: {result.is_valid}")
|
|
126
|
+
"""
|
|
127
|
+
from iam_validator.core.models import IAMPolicy
|
|
128
|
+
|
|
129
|
+
# Parse the dict into an IAMPolicy
|
|
130
|
+
policy = IAMPolicy(**policy_json)
|
|
131
|
+
|
|
132
|
+
results = await validate_policies(
|
|
133
|
+
[(policy_name, policy)],
|
|
134
|
+
config_path=config_path,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
results[0]
|
|
139
|
+
if results
|
|
140
|
+
else PolicyValidationResult(
|
|
141
|
+
policy_file=policy_name,
|
|
142
|
+
is_valid=False,
|
|
143
|
+
issues=[],
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def quick_validate(
|
|
149
|
+
policy: str | Path | dict,
|
|
150
|
+
config_path: str | None = None,
|
|
151
|
+
config: ValidatorConfig | None = None,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Quick validation returning just True/False.
|
|
155
|
+
|
|
156
|
+
Automatically detects whether input is a file path, directory, or dict.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
policy: File path, directory path, or policy dict
|
|
160
|
+
config_path: Optional path to configuration file
|
|
161
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if all policies are valid, False otherwise
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
>>> if await quick_validate("policy.json"):
|
|
168
|
+
... print("Policy is valid!")
|
|
169
|
+
>>> else:
|
|
170
|
+
... print("Policy has issues")
|
|
171
|
+
"""
|
|
172
|
+
# If dict, validate as JSON
|
|
173
|
+
if isinstance(policy, dict):
|
|
174
|
+
result = await validate_json(policy, config_path=config_path)
|
|
175
|
+
return result.is_valid
|
|
176
|
+
|
|
177
|
+
# Convert to Path for easier handling
|
|
178
|
+
policy_path = Path(policy)
|
|
179
|
+
|
|
180
|
+
if not policy_path.exists():
|
|
181
|
+
raise FileNotFoundError(f"Path does not exist: {policy}")
|
|
182
|
+
|
|
183
|
+
# If directory, validate all files in it
|
|
184
|
+
if policy_path.is_dir():
|
|
185
|
+
results = await validate_directory(policy_path, config_path=config_path)
|
|
186
|
+
return all(r.is_valid for r in results)
|
|
187
|
+
|
|
188
|
+
# Otherwise, validate single file
|
|
189
|
+
result = await validate_file(policy_path, config_path=config_path)
|
|
190
|
+
return result.is_valid
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def get_issues(
|
|
194
|
+
policy: str | Path | dict,
|
|
195
|
+
min_severity: str = "medium",
|
|
196
|
+
config_path: str | None = None,
|
|
197
|
+
config: ValidatorConfig | None = None,
|
|
198
|
+
) -> list[ValidationIssue]:
|
|
199
|
+
"""
|
|
200
|
+
Get just the issues from validation, filtered by severity.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
policy: File path, directory path, or policy dict
|
|
204
|
+
min_severity: Minimum severity to include (critical, high, medium, low, info)
|
|
205
|
+
config_path: Optional path to configuration file
|
|
206
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of ValidationIssues meeting the severity threshold
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> issues = await get_issues("policy.json", min_severity="high")
|
|
213
|
+
>>> for issue in issues:
|
|
214
|
+
... print(f"{issue.severity}: {issue.message}")
|
|
215
|
+
"""
|
|
216
|
+
# Severity ranking for filtering
|
|
217
|
+
severity_rank = {
|
|
218
|
+
"critical": 5,
|
|
219
|
+
"high": 4,
|
|
220
|
+
"medium": 3,
|
|
221
|
+
"low": 2,
|
|
222
|
+
"info": 1,
|
|
223
|
+
"warning": 3, # Treat warning as medium
|
|
224
|
+
"error": 4, # Treat error as high
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
min_rank = severity_rank.get(min_severity.lower(), 0)
|
|
228
|
+
|
|
229
|
+
# Get validation results
|
|
230
|
+
if isinstance(policy, dict):
|
|
231
|
+
result = await validate_json(policy, config_path=config_path)
|
|
232
|
+
results = [result]
|
|
233
|
+
else:
|
|
234
|
+
policy_path = Path(policy)
|
|
235
|
+
if policy_path.is_dir():
|
|
236
|
+
results = await validate_directory(policy_path, config_path=config_path)
|
|
237
|
+
else:
|
|
238
|
+
result = await validate_file(policy_path, config_path=config_path)
|
|
239
|
+
results = [result]
|
|
240
|
+
|
|
241
|
+
# Collect and filter issues
|
|
242
|
+
all_issues = []
|
|
243
|
+
for result in results:
|
|
244
|
+
for issue in result.issues:
|
|
245
|
+
issue_rank = severity_rank.get(issue.severity.lower(), 0)
|
|
246
|
+
if issue_rank >= min_rank:
|
|
247
|
+
all_issues.append(issue)
|
|
248
|
+
|
|
249
|
+
return all_issues
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def count_issues_by_severity(
|
|
253
|
+
policy: str | Path | dict,
|
|
254
|
+
config_path: str | None = None,
|
|
255
|
+
config: ValidatorConfig | None = None,
|
|
256
|
+
) -> dict[str, int]:
|
|
257
|
+
"""
|
|
258
|
+
Count issues grouped by severity level.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
policy: File path, directory path, or policy dict
|
|
262
|
+
config_path: Optional path to configuration file
|
|
263
|
+
config: Optional ValidatorConfig object (overrides config_path)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary mapping severity levels to counts
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> counts = await count_issues_by_severity("./policies")
|
|
270
|
+
>>> print(f"Critical: {counts.get('critical', 0)}")
|
|
271
|
+
>>> print(f"High: {counts.get('high', 0)}")
|
|
272
|
+
>>> print(f"Medium: {counts.get('medium', 0)}")
|
|
273
|
+
"""
|
|
274
|
+
# Get all issues (no filtering)
|
|
275
|
+
all_issues = await get_issues(policy, min_severity="info", config_path=config_path)
|
|
276
|
+
|
|
277
|
+
# Count by severity
|
|
278
|
+
counts: dict[str, int] = {}
|
|
279
|
+
for issue in all_issues:
|
|
280
|
+
severity = issue.severity.lower()
|
|
281
|
+
counts[severity] = counts.get(severity, 0) + 1
|
|
282
|
+
|
|
283
|
+
return counts
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
- terminal.py: Terminal width detection utilities
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from iam_validator.utils.cache import LRUCache
|
|
17
|
+
from iam_validator.utils.regex import (
|
|
18
|
+
cached_pattern,
|
|
19
|
+
clear_pattern_cache,
|
|
20
|
+
compile_and_cache,
|
|
21
|
+
get_cached_pattern,
|
|
22
|
+
)
|
|
23
|
+
from iam_validator.utils.terminal import get_terminal_width
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Cache utilities
|
|
27
|
+
"LRUCache",
|
|
28
|
+
# Regex utilities
|
|
29
|
+
"cached_pattern",
|
|
30
|
+
"compile_and_cache",
|
|
31
|
+
"get_cached_pattern",
|
|
32
|
+
"clear_pattern_cache",
|
|
33
|
+
# Terminal utilities
|
|
34
|
+
"get_terminal_width",
|
|
35
|
+
]
|
|
@@ -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,205 @@
|
|
|
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
|
+
|
|
17
|
+
|
|
18
|
+
def cached_pattern(
|
|
19
|
+
flags: int = 0,
|
|
20
|
+
maxsize: int = 128,
|
|
21
|
+
) -> Callable[[Callable[[], str]], Callable[[], re.Pattern]]:
|
|
22
|
+
r"""Decorator that caches compiled regex patterns.
|
|
23
|
+
|
|
24
|
+
This decorator transforms a function that returns a regex pattern string
|
|
25
|
+
into a function that returns a compiled regex Pattern object. The compilation
|
|
26
|
+
is cached, so subsequent calls return the same compiled pattern without
|
|
27
|
+
re-compilation overhead.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
flags: Regex compilation flags (e.g., re.IGNORECASE, re.MULTILINE)
|
|
31
|
+
maxsize: Maximum cache size for LRU eviction (default: 128)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Decorator function
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> @cached_pattern(flags=re.IGNORECASE)
|
|
38
|
+
... def email_pattern():
|
|
39
|
+
... return r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
40
|
+
...
|
|
41
|
+
>>> pattern = email_pattern() # Compiles and caches
|
|
42
|
+
>>> pattern2 = email_pattern() # Returns cached pattern (same object)
|
|
43
|
+
>>> pattern is pattern2
|
|
44
|
+
True
|
|
45
|
+
>>> pattern.match("user@example.com")
|
|
46
|
+
<re.Match object; span=(0, 17), match='user@example.com'>
|
|
47
|
+
|
|
48
|
+
Example with ARN pattern:
|
|
49
|
+
>>> @cached_pattern()
|
|
50
|
+
... def arn_pattern():
|
|
51
|
+
... return r'^arn:aws:iam::[0-9]{12}:role/.*$'
|
|
52
|
+
...
|
|
53
|
+
>>> arn = arn_pattern()
|
|
54
|
+
>>> arn.match("arn:aws:iam::123456789012:role/MyRole")
|
|
55
|
+
<re.Match object; ...>
|
|
56
|
+
|
|
57
|
+
Performance:
|
|
58
|
+
First call: ~10-50μs (pattern compilation)
|
|
59
|
+
Cached calls: ~0.1-0.5μs (cache lookup) → 20-100x faster
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def decorator(func: Callable[[], str]) -> Callable[[], re.Pattern]:
|
|
63
|
+
# Use a cache per function to avoid key collisions
|
|
64
|
+
cache = {}
|
|
65
|
+
|
|
66
|
+
@wraps(func)
|
|
67
|
+
def wrapper() -> re.Pattern:
|
|
68
|
+
# Use function name as cache key (since each decorated function
|
|
69
|
+
# returns the same pattern string)
|
|
70
|
+
cache_key = func.__name__
|
|
71
|
+
|
|
72
|
+
if cache_key not in cache:
|
|
73
|
+
pattern_str = func()
|
|
74
|
+
cache[cache_key] = re.compile(pattern_str, flags)
|
|
75
|
+
|
|
76
|
+
return cache[cache_key]
|
|
77
|
+
|
|
78
|
+
# Store pattern string as attribute for introspection
|
|
79
|
+
wrapper.pattern_string = func # type: ignore
|
|
80
|
+
|
|
81
|
+
return wrapper
|
|
82
|
+
|
|
83
|
+
return decorator
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def compile_and_cache(pattern: str, flags: int = 0, maxsize: int = 512) -> re.Pattern:
|
|
87
|
+
"""Compile a regex pattern with automatic caching.
|
|
88
|
+
|
|
89
|
+
This is a functional interface (not a decorator) that compiles and caches
|
|
90
|
+
regex patterns. Useful for dynamic patterns or one-off compilations.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
pattern: Regex pattern string
|
|
94
|
+
flags: Regex compilation flags (e.g., re.IGNORECASE)
|
|
95
|
+
maxsize: Maximum cache size for LRU eviction
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Compiled Pattern object
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> pattern1 = compile_and_cache(r'\\d+', re.IGNORECASE)
|
|
102
|
+
>>> pattern2 = compile_and_cache(r'\\d+', re.IGNORECASE)
|
|
103
|
+
>>> pattern1 is pattern2 # Same pattern, same flags -> cached
|
|
104
|
+
True
|
|
105
|
+
|
|
106
|
+
>>> # Different flags -> different cached entry
|
|
107
|
+
>>> pattern3 = compile_and_cache(r'\\d+', re.MULTILINE)
|
|
108
|
+
>>> pattern1 is pattern3
|
|
109
|
+
False
|
|
110
|
+
|
|
111
|
+
Note:
|
|
112
|
+
This uses a module-level cache shared across all calls. For function-specific
|
|
113
|
+
caching, use the @cached_pattern decorator instead.
|
|
114
|
+
"""
|
|
115
|
+
from functools import lru_cache
|
|
116
|
+
|
|
117
|
+
@lru_cache(maxsize=maxsize)
|
|
118
|
+
def _compile(pattern_str: str, flags: int) -> re.Pattern:
|
|
119
|
+
return re.compile(pattern_str, flags)
|
|
120
|
+
|
|
121
|
+
return _compile(pattern, flags)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Singleton instance for shared pattern compilation
|
|
125
|
+
_pattern_cache: dict[tuple[str, int], re.Pattern] = {}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_cached_pattern(pattern: str, flags: int = 0) -> re.Pattern:
|
|
129
|
+
"""Get a compiled pattern from the shared cache.
|
|
130
|
+
|
|
131
|
+
This provides a simple, stateless way to get cached patterns without
|
|
132
|
+
decorators or function calls. Uses a module-level cache.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
pattern: Regex pattern string
|
|
136
|
+
flags: Regex compilation flags
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Compiled Pattern object (cached)
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> pattern = get_cached_pattern(r'^arn:aws:.*$', re.IGNORECASE)
|
|
143
|
+
>>> pattern.match("arn:aws:s3:::bucket")
|
|
144
|
+
<re.Match object; ...>
|
|
145
|
+
|
|
146
|
+
Thread Safety:
|
|
147
|
+
This function is NOT thread-safe. For concurrent use, use
|
|
148
|
+
compile_and_cache() which uses functools.lru_cache (thread-safe).
|
|
149
|
+
"""
|
|
150
|
+
cache_key = (pattern, flags)
|
|
151
|
+
|
|
152
|
+
if cache_key not in _pattern_cache:
|
|
153
|
+
_pattern_cache[cache_key] = re.compile(pattern, flags)
|
|
154
|
+
|
|
155
|
+
return _pattern_cache[cache_key]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def clear_pattern_cache() -> None:
|
|
159
|
+
"""Clear the shared pattern cache.
|
|
160
|
+
|
|
161
|
+
Useful for testing or memory management.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> get_cached_pattern(r'test')
|
|
165
|
+
>>> len(_pattern_cache)
|
|
166
|
+
1
|
|
167
|
+
>>> clear_pattern_cache()
|
|
168
|
+
>>> len(_pattern_cache)
|
|
169
|
+
0
|
|
170
|
+
"""
|
|
171
|
+
_pattern_cache.clear()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Pre-defined common patterns for IAM validation
|
|
175
|
+
# These are compiled once and reused throughout the application
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@cached_pattern()
|
|
179
|
+
def wildcard_pattern():
|
|
180
|
+
"""Pattern for detecting wildcards (*) in strings."""
|
|
181
|
+
return r"\*"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@cached_pattern()
|
|
185
|
+
def partial_wildcard_pattern():
|
|
186
|
+
"""Pattern for detecting partial wildcards (e.g., 's3:Get*')."""
|
|
187
|
+
return r"^[^*]+\*$"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@cached_pattern()
|
|
191
|
+
def arn_base_pattern():
|
|
192
|
+
"""Basic ARN structure pattern."""
|
|
193
|
+
return r"^arn:[^:]*:[^:]*:[^:]*:[^:]*:.*$"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@cached_pattern()
|
|
197
|
+
def aws_account_id_pattern():
|
|
198
|
+
"""AWS account ID pattern (12 digits)."""
|
|
199
|
+
return r"^[0-9]{12}$"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@cached_pattern(flags=re.IGNORECASE)
|
|
203
|
+
def action_pattern():
|
|
204
|
+
"""IAM action pattern (service:Action format)."""
|
|
205
|
+
return r"^[a-z0-9-]+:[a-zA-Z0-9*]+$"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Terminal utilities for console output formatting."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_terminal_width(min_width: int = 80, max_width: int = 150, fallback: int = 100) -> int:
|
|
7
|
+
"""Get the current terminal width with reasonable bounds.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
min_width: Minimum width to return (default: 80)
|
|
11
|
+
max_width: Maximum width to return (default: 150)
|
|
12
|
+
fallback: Fallback width if detection fails (default: 100)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Terminal width within the specified bounds
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
19
|
+
# Ensure width is within reasonable bounds
|
|
20
|
+
return max(min(terminal_width, max_width), min_width)
|
|
21
|
+
except Exception:
|
|
22
|
+
return fallback
|