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.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. 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