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,641 @@
1
+ """AWS Service Fetcher - Main orchestrator for AWS service data retrieval.
2
+
3
+ This module provides the main AWSServiceFetcher class that coordinates between
4
+ HTTP client, caching, storage, parsing, and validation components.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from iam_validator.core import constants
13
+ from iam_validator.core.aws_service.cache import ServiceCacheManager
14
+ from iam_validator.core.aws_service.client import AWSServiceClient
15
+ from iam_validator.core.aws_service.parsers import ServiceParser
16
+ from iam_validator.core.aws_service.storage import ServiceFileStorage
17
+ from iam_validator.core.aws_service.validators import (
18
+ ConditionKeyValidationResult,
19
+ ServiceValidator,
20
+ )
21
+ from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
22
+ from iam_validator.core.models import ServiceDetail, ServiceInfo
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AWSServiceFetcher:
28
+ """Fetches and validates AWS service information with caching.
29
+
30
+ This is the main entry point for AWS service data operations.
31
+ Coordinates between HTTP client, caching, storage, and validation.
32
+
33
+ Features:
34
+ - Multi-layer caching (memory LRU + disk with TTL)
35
+ - Service pre-fetching for common AWS services
36
+ - Request batching and coalescing
37
+ - Offline mode support with local AWS service files
38
+ - HTTP/2 connection pooling
39
+ - Automatic retry with exponential backoff
40
+
41
+ Example:
42
+ >>> async with AWSServiceFetcher() as fetcher:
43
+ ... # Fetch service list
44
+ ... services = await fetcher.fetch_services()
45
+ ...
46
+ ... # Fetch specific service details
47
+ ... s3_service = await fetcher.fetch_service_by_name("s3")
48
+ ...
49
+ ... # Validate actions
50
+ ... is_valid, error, is_wildcard = await fetcher.validate_action("s3:GetObject")
51
+ """
52
+
53
+ BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
54
+
55
+ # Common AWS services to pre-fetch
56
+ # All other services will be fetched on-demand (lazy loading if found in policies)
57
+ COMMON_SERVICES = [
58
+ "acm",
59
+ "apigateway",
60
+ "autoscaling",
61
+ "backup",
62
+ "batch",
63
+ "bedrock",
64
+ "cloudformation",
65
+ "cloudfront",
66
+ "cloudtrail",
67
+ "cloudwatch",
68
+ "config",
69
+ "dynamodb",
70
+ "ec2-instance-connect",
71
+ "ec2",
72
+ "ecr",
73
+ "ecs",
74
+ "eks",
75
+ "elasticache",
76
+ "elasticloadbalancing",
77
+ "events",
78
+ "firehose",
79
+ "glacier",
80
+ "glue",
81
+ "guardduty",
82
+ "iam",
83
+ "imagebuilder",
84
+ "inspector2",
85
+ "kinesis",
86
+ "kms",
87
+ "lambda",
88
+ "logs",
89
+ "rds",
90
+ "route53",
91
+ "s3",
92
+ "scheduler",
93
+ "secretsmanager",
94
+ "securityhub",
95
+ "sns",
96
+ "sqs",
97
+ "sts",
98
+ "support",
99
+ "waf",
100
+ "wafv2",
101
+ ]
102
+
103
+ # Default concurrency limits
104
+ DEFAULT_MAX_CONCURRENT_REQUESTS = 10
105
+
106
+ def __init__(
107
+ self,
108
+ timeout: float = constants.DEFAULT_HTTP_TIMEOUT_SECONDS,
109
+ retries: int = 3,
110
+ enable_cache: bool = True,
111
+ cache_ttl: int = constants.DEFAULT_CACHE_TTL_SECONDS,
112
+ memory_cache_size: int = 256,
113
+ connection_pool_size: int = 50,
114
+ keepalive_connections: int = 20,
115
+ prefetch_common: bool = True,
116
+ cache_dir: Path | str | None = None,
117
+ aws_services_dir: Path | str | None = None,
118
+ max_concurrent_requests: int = DEFAULT_MAX_CONCURRENT_REQUESTS,
119
+ ):
120
+ """Initialize AWS service fetcher.
121
+
122
+ Args:
123
+ timeout: Request timeout in seconds
124
+ retries: Number of retries for failed requests
125
+ enable_cache: Enable persistent disk caching
126
+ cache_ttl: Cache time-to-live in seconds
127
+ memory_cache_size: Size of in-memory LRU cache
128
+ connection_pool_size: HTTP connection pool size
129
+ keepalive_connections: Number of keepalive connections
130
+ prefetch_common: Prefetch common AWS services
131
+ cache_dir: Custom cache directory path
132
+ aws_services_dir: Directory containing pre-downloaded AWS service JSON files.
133
+ When set, the fetcher will load services from local files
134
+ instead of making API calls. Directory should contain:
135
+ - _services.json: List of all services
136
+ - {service}.json: Individual service files (e.g., s3.json)
137
+ max_concurrent_requests: Maximum number of concurrent HTTP requests (default: 10)
138
+ """
139
+ self.prefetch_common = prefetch_common
140
+ self.aws_services_dir = Path(aws_services_dir) if aws_services_dir else None
141
+ self._prefetched_services: set[str] = set()
142
+ # Semaphore for limiting concurrent requests
143
+ self._request_semaphore = asyncio.Semaphore(max_concurrent_requests)
144
+
145
+ # Initialize storage component
146
+ self._storage = ServiceFileStorage(
147
+ cache_dir=cache_dir,
148
+ aws_services_dir=aws_services_dir,
149
+ cache_ttl=cache_ttl,
150
+ enable_cache=enable_cache,
151
+ )
152
+
153
+ # Initialize cache manager
154
+ self._cache = ServiceCacheManager(
155
+ memory_cache_size=memory_cache_size,
156
+ cache_ttl=cache_ttl,
157
+ storage=self._storage if enable_cache else None,
158
+ )
159
+
160
+ # Initialize HTTP client
161
+ self._client = AWSServiceClient(
162
+ base_url=self.BASE_URL,
163
+ timeout=timeout,
164
+ retries=retries,
165
+ connection_pool_size=connection_pool_size,
166
+ keepalive_connections=keepalive_connections,
167
+ )
168
+
169
+ # Initialize parser and validator
170
+ self._parser = ServiceParser()
171
+ self._validator = ServiceValidator(parser=self._parser)
172
+
173
+ async def __aenter__(self) -> "AWSServiceFetcher":
174
+ """Async context manager entry."""
175
+ await self._client.__aenter__()
176
+
177
+ # Pre-fetch common services if enabled
178
+ if self.prefetch_common:
179
+ await self._prefetch_common_services()
180
+
181
+ return self
182
+
183
+ async def __aexit__(
184
+ self,
185
+ exc_type: type[BaseException] | None,
186
+ exc_val: BaseException | None,
187
+ exc_tb: Any,
188
+ ) -> None:
189
+ """Async context manager exit."""
190
+ await self._client.__aexit__(exc_type, exc_val, exc_tb)
191
+
192
+ async def _prefetch_common_services(self) -> None:
193
+ """Pre-fetch commonly used AWS services for better performance."""
194
+ logger.info(f"Pre-fetching {len(self.COMMON_SERVICES)} common AWS services...")
195
+
196
+ # First, fetch the services list once to populate the cache
197
+ # This prevents all concurrent calls from fetching the same list
198
+ await self.fetch_services()
199
+
200
+ async def fetch_service(name: str) -> None:
201
+ try:
202
+ await self.fetch_service_by_name(name)
203
+ self._prefetched_services.add(name)
204
+ except Exception as e: # pylint: disable=broad-exception-caught
205
+ logger.warning(f"Failed to prefetch service {name}: {e}")
206
+
207
+ # Fetch in batches to avoid overwhelming the API
208
+ batch_size = 5
209
+ for i in range(0, len(self.COMMON_SERVICES), batch_size):
210
+ batch = self.COMMON_SERVICES[i : i + batch_size]
211
+ await asyncio.gather(*[fetch_service(name) for name in batch])
212
+
213
+ logger.info(f"Pre-fetched {len(self._prefetched_services)} services successfully")
214
+
215
+ async def fetch_services(self) -> list[ServiceInfo]:
216
+ """Fetch list of AWS services with caching.
217
+
218
+ When aws_services_dir is set, loads from local _services.json file.
219
+ Otherwise, fetches from AWS API.
220
+
221
+ Returns:
222
+ List of ServiceInfo objects
223
+
224
+ Example:
225
+ >>> async with AWSServiceFetcher() as fetcher:
226
+ ... services = await fetcher.fetch_services()
227
+ ... print(f"Found {len(services)} AWS services")
228
+ """
229
+ # Check if we have the parsed services list in cache
230
+ services_cache_key = "parsed_services_list"
231
+ cached_services = await self._cache.get(services_cache_key)
232
+ if cached_services is not None and isinstance(cached_services, list):
233
+ logger.debug(f"Retrieved {len(cached_services)} services from parsed cache")
234
+ return cached_services
235
+
236
+ # Load from local file if aws_services_dir is set
237
+ if self.aws_services_dir:
238
+ loaded_services = self._storage.load_services_from_file()
239
+ # Cache the loaded services
240
+ await self._cache.set(services_cache_key, loaded_services)
241
+ return loaded_services
242
+
243
+ # Not in parsed cache, check disk cache then fetch from API
244
+ data = await self._cache.get(
245
+ f"raw:{self.BASE_URL}", url=self.BASE_URL, base_url=self.BASE_URL
246
+ )
247
+ if data is None:
248
+ data = await self._client.fetch(self.BASE_URL)
249
+ # Cache the raw data
250
+ await self._cache.set(
251
+ f"raw:{self.BASE_URL}", data, url=self.BASE_URL, base_url=self.BASE_URL
252
+ )
253
+
254
+ if not isinstance(data, list):
255
+ raise ValueError("Expected list of services from root endpoint")
256
+
257
+ services: list[ServiceInfo] = []
258
+ for item in data:
259
+ if isinstance(item, dict):
260
+ service = item.get("service")
261
+ url = item.get("url")
262
+ if service and url:
263
+ services.append(ServiceInfo(service=str(service), url=str(url)))
264
+
265
+ # Cache the parsed services list (memory only)
266
+ await self._cache.set(services_cache_key, services)
267
+
268
+ # Log only on first fetch (when parsed cache was empty)
269
+ logger.info(f"Fetched and parsed {len(services)} services from AWS API")
270
+ return services
271
+
272
+ async def fetch_service_by_name(self, service_name: str) -> ServiceDetail:
273
+ """Fetch service detail with optimized caching.
274
+
275
+ When aws_services_dir is set, loads from local {service}.json file.
276
+ Otherwise, fetches from AWS API.
277
+
278
+ Args:
279
+ service_name: Name of the service (case-insensitive, e.g., "s3", "iam")
280
+
281
+ Returns:
282
+ ServiceDetail object with full service definition
283
+
284
+ Raises:
285
+ ValueError: If service is not found
286
+
287
+ Example:
288
+ >>> async with AWSServiceFetcher() as fetcher:
289
+ ... s3_service = await fetcher.fetch_service_by_name("s3")
290
+ ... print(f"S3 has {len(s3_service.actions)} actions")
291
+ """
292
+ # Normalize service name
293
+ service_name_lower = service_name.lower()
294
+
295
+ # Check memory cache with service name as key
296
+ cache_key = f"service:{service_name_lower}"
297
+ cached_detail = await self._cache.get(cache_key)
298
+ if isinstance(cached_detail, ServiceDetail):
299
+ logger.debug(f"Memory cache hit for service {service_name}")
300
+ return cached_detail
301
+
302
+ # Load from local file if aws_services_dir is set
303
+ if self.aws_services_dir:
304
+ try:
305
+ service_detail = self._storage.load_service_from_file(service_name_lower)
306
+ # Cache the loaded service
307
+ await self._cache.set(cache_key, service_detail)
308
+ return service_detail
309
+ except FileNotFoundError:
310
+ # Try to find the service in services.json to get proper name
311
+ services = await self.fetch_services()
312
+ for service in services:
313
+ if service.service.lower() == service_name_lower:
314
+ # Try with the exact service name from services.json
315
+ try:
316
+ service_detail = self._storage.load_service_from_file(service.service)
317
+ await self._cache.set(cache_key, service_detail)
318
+ return service_detail
319
+ except FileNotFoundError:
320
+ pass
321
+ raise ValueError(
322
+ f"Service `{service_name}` not found in {self.aws_services_dir}"
323
+ ) from FileNotFoundError
324
+
325
+ # Fetch service list and find URL from API
326
+ services = await self.fetch_services()
327
+
328
+ for service in services:
329
+ if service.service.lower() == service_name_lower:
330
+ # Check disk cache first, then fetch from API
331
+ data = await self._cache.get(
332
+ f"raw:{service.url}", url=service.url, base_url=self.BASE_URL
333
+ )
334
+ if data is None:
335
+ # Fetch service detail from API
336
+ data = await self._client.fetch(service.url)
337
+ # Cache the raw data
338
+ await self._cache.set(
339
+ f"raw:{service.url}", data, url=service.url, base_url=self.BASE_URL
340
+ )
341
+
342
+ # Validate and parse
343
+ service_detail = ServiceDetail.model_validate(data)
344
+
345
+ # Cache with service name as key (memory only)
346
+ await self._cache.set(cache_key, service_detail)
347
+
348
+ return service_detail
349
+
350
+ raise ValueError(f"Service `{service_name}` not found")
351
+
352
+ async def fetch_multiple_services(self, service_names: list[str]) -> dict[str, ServiceDetail]:
353
+ """Fetch multiple services concurrently with controlled parallelism.
354
+
355
+ Uses a semaphore to limit concurrent requests and prevent overwhelming
356
+ the AWS service reference API.
357
+
358
+ Args:
359
+ service_names: List of service names to fetch
360
+
361
+ Returns:
362
+ Dictionary mapping service names to ServiceDetail objects
363
+
364
+ Raises:
365
+ Exception: If any service fetch fails
366
+
367
+ Example:
368
+ >>> async with AWSServiceFetcher() as fetcher:
369
+ ... services = await fetcher.fetch_multiple_services(["s3", "iam", "ec2"])
370
+ ... print(f"Fetched {len(services)} services")
371
+ """
372
+
373
+ async def fetch_single(name: str) -> tuple[str, ServiceDetail]:
374
+ # Use semaphore to limit concurrent requests
375
+ async with self._request_semaphore:
376
+ try:
377
+ detail = await self.fetch_service_by_name(name)
378
+ return name, detail
379
+ except Exception as e: # pylint: disable=broad-exception-caught
380
+ logger.error(f"Failed to fetch service {name}: {e}")
381
+ raise
382
+
383
+ # Fetch all services concurrently (semaphore controls parallelism)
384
+ tasks = [fetch_single(name) for name in service_names]
385
+ results = await asyncio.gather(*tasks, return_exceptions=True)
386
+
387
+ services: dict[str, ServiceDetail] = {}
388
+ for i, result in enumerate(results):
389
+ if isinstance(result, Exception):
390
+ logger.error(f"Failed to fetch service {service_names[i]}: {result}")
391
+ raise result
392
+ if isinstance(result, tuple):
393
+ name, detail = result
394
+ services[name] = detail
395
+
396
+ return services
397
+
398
+ # --- Validation Methods (delegate to validator) ---
399
+
400
+ async def validate_action(
401
+ self,
402
+ action: str,
403
+ allow_wildcards: bool = True,
404
+ ) -> tuple[bool, str | None, bool]:
405
+ """Validate IAM action.
406
+
407
+ Args:
408
+ action: Full action string (e.g., "s3:GetObject", "iam:CreateUser")
409
+ allow_wildcards: Whether to allow wildcard actions
410
+
411
+ Returns:
412
+ Tuple of (is_valid, error_message, is_wildcard)
413
+
414
+ Example:
415
+ >>> async with AWSServiceFetcher() as fetcher:
416
+ ... is_valid, error, is_wildcard = await fetcher.validate_action("s3:GetObject")
417
+ ... if not is_valid:
418
+ ... print(f"Invalid action: {error}")
419
+ """
420
+ service_prefix, _ = self._parser.parse_action(action)
421
+ service_detail = await self.fetch_service_by_name(service_prefix)
422
+ return await self._validator.validate_action(action, service_detail, allow_wildcards)
423
+
424
+ def validate_arn(self, arn: str) -> tuple[bool, str | None]:
425
+ """Validate ARN format.
426
+
427
+ Args:
428
+ arn: ARN string to validate
429
+
430
+ Returns:
431
+ Tuple of (is_valid, error_message)
432
+
433
+ Example:
434
+ >>> fetcher = AWSServiceFetcher()
435
+ >>> is_valid, error = fetcher.validate_arn("arn:aws:s3:::my-bucket/*")
436
+ >>> if not is_valid:
437
+ ... print(f"Invalid ARN: {error}")
438
+ """
439
+ return self._parser.validate_arn_format(arn)
440
+
441
+ async def validate_condition_key(
442
+ self,
443
+ action: str,
444
+ condition_key: str,
445
+ resources: list[str] | None = None,
446
+ ) -> ConditionKeyValidationResult:
447
+ """Validate condition key.
448
+
449
+ Args:
450
+ action: IAM action (e.g., "s3:GetObject")
451
+ condition_key: Condition key to validate (e.g., "s3:prefix")
452
+ resources: Optional list of resource ARNs
453
+
454
+ Returns:
455
+ ConditionKeyValidationResult with validation details
456
+
457
+ Example:
458
+ >>> async with AWSServiceFetcher() as fetcher:
459
+ ... result = await fetcher.validate_condition_key("s3:GetObject", "s3:prefix")
460
+ ... if not result.is_valid:
461
+ ... print(f"Invalid condition key: {result.error_message}")
462
+ """
463
+ service_prefix, _ = self._parser.parse_action(action)
464
+ service_detail = await self.fetch_service_by_name(service_prefix)
465
+ return await self._validator.validate_condition_key(
466
+ action, condition_key, service_detail, resources
467
+ )
468
+
469
+ # --- Parsing Methods (delegate to parser) ---
470
+
471
+ def parse_action(self, action: str) -> tuple[str, str]:
472
+ """Parse action into service and action name.
473
+
474
+ Args:
475
+ action: Full action string (e.g., "s3:GetObject")
476
+
477
+ Returns:
478
+ Tuple of (service_prefix, action_name)
479
+
480
+ Example:
481
+ >>> fetcher = AWSServiceFetcher()
482
+ >>> service, action_name = fetcher.parse_action("s3:GetObject")
483
+ >>> print(f"Service: {service}, Action: {action_name}")
484
+ """
485
+ return self._parser.parse_action(action)
486
+
487
+ def match_wildcard_action(
488
+ self,
489
+ pattern: str,
490
+ actions: list[str],
491
+ ) -> tuple[bool, list[str]]:
492
+ """Match wildcard pattern against actions.
493
+
494
+ Args:
495
+ pattern: Action pattern with wildcards (e.g., "Get*", "*Object")
496
+ actions: List of action names to match against
497
+
498
+ Returns:
499
+ Tuple of (has_matches, list_of_matched_actions)
500
+
501
+ Example:
502
+ >>> fetcher = AWSServiceFetcher()
503
+ >>> actions = ["GetObject", "PutObject", "DeleteObject"]
504
+ >>> has_matches, matched = fetcher.match_wildcard_action("Get*", actions)
505
+ >>> print(f"Matched: {matched}")
506
+ """
507
+ return self._parser.match_wildcard_action(pattern, actions)
508
+
509
+ # --- Helper Methods ---
510
+
511
+ async def get_resources_for_action(self, action: str) -> list[dict[str, Any]]:
512
+ """Get resource types for action.
513
+
514
+ Args:
515
+ action: Full action name (e.g., "s3:GetObject")
516
+
517
+ Returns:
518
+ List of resource dictionaries from AWS API
519
+
520
+ Example:
521
+ >>> async with AWSServiceFetcher() as fetcher:
522
+ ... resources = await fetcher.get_resources_for_action("s3:GetObject")
523
+ ... print(f"Action operates on {len(resources)} resource types")
524
+ """
525
+ service_prefix, _ = self._parser.parse_action(action)
526
+ service_detail = await self.fetch_service_by_name(service_prefix)
527
+ return self._validator.get_resources_for_action(action, service_detail)
528
+
529
+ async def get_arn_formats_for_action(self, action: str) -> list[str]:
530
+ """Get ARN formats for action.
531
+
532
+ Args:
533
+ action: Full action name (e.g., "s3:GetObject")
534
+
535
+ Returns:
536
+ List of ARN format strings
537
+
538
+ Example:
539
+ >>> async with AWSServiceFetcher() as fetcher:
540
+ ... arns = await fetcher.get_arn_formats_for_action("s3:GetObject")
541
+ ... for arn in arns:
542
+ ... print(f"ARN format: {arn}")
543
+ """
544
+ service_prefix, _ = self._parser.parse_action(action)
545
+ service_detail = await self.fetch_service_by_name(service_prefix)
546
+ return self._validator.get_arn_formats_for_action(action, service_detail)
547
+
548
+ async def get_all_actions_for_service(self, service: str) -> list[str]:
549
+ """Get all actions for service.
550
+
551
+ Args:
552
+ service: Service prefix (e.g., "s3", "iam", "ec2")
553
+
554
+ Returns:
555
+ Sorted list of action names (without service prefix)
556
+
557
+ Example:
558
+ >>> async with AWSServiceFetcher() as fetcher:
559
+ ... actions = await fetcher.get_all_actions_for_service("s3")
560
+ ... print(f"S3 has {len(actions)} actions")
561
+ """
562
+ service_detail = await self.fetch_service_by_name(service)
563
+ return sorted(service_detail.actions.keys())
564
+
565
+ async def expand_wildcard_action(self, action_pattern: str) -> list[str]:
566
+ """Expand wildcard action to full list.
567
+
568
+ Args:
569
+ action_pattern: Action with wildcards (e.g., "iam:Create*", "s3:*Object")
570
+
571
+ Returns:
572
+ Sorted list of fully-qualified actions matching the pattern
573
+
574
+ Example:
575
+ >>> async with AWSServiceFetcher() as fetcher:
576
+ ... actions = await fetcher.expand_wildcard_action("iam:Create*")
577
+ ... print(f"Pattern matches {len(actions)} actions")
578
+ """
579
+ if action_pattern in ("*", "*:*"):
580
+ return ["*"]
581
+
582
+ service_prefix, _ = self._parser.parse_action(action_pattern)
583
+ service_detail = await self.fetch_service_by_name(service_prefix)
584
+ available = list(service_detail.actions.keys())
585
+ return self._parser.expand_wildcard_to_actions(action_pattern, available, service_prefix)
586
+
587
+ # --- Cache Management ---
588
+
589
+ async def clear_caches(self) -> None:
590
+ """Clear all caches (memory and disk).
591
+
592
+ Example:
593
+ >>> async with AWSServiceFetcher() as fetcher:
594
+ ... await fetcher.clear_caches()
595
+ """
596
+ await self._cache.clear()
597
+
598
+ def set_cache_directory(self, cache_dir: Path | str) -> None:
599
+ """Set a new cache directory path dynamically.
600
+
601
+ This method allows library users to change the cache location at runtime.
602
+ Useful for applications that need to control where cache files are stored.
603
+
604
+ Args:
605
+ cache_dir: New cache directory path
606
+
607
+ Example:
608
+ >>> async with AWSServiceFetcher() as fetcher:
609
+ ... fetcher.set_cache_directory("/tmp/my-custom-cache")
610
+ ... # Future cache operations will use the new directory
611
+ """
612
+ self._storage.set_cache_directory(cache_dir)
613
+
614
+ def get_cache_directory(self) -> Path:
615
+ """Get the current cache directory path.
616
+
617
+ Returns:
618
+ Current cache directory as Path object
619
+
620
+ Example:
621
+ >>> async with AWSServiceFetcher() as fetcher:
622
+ ... cache_path = fetcher.get_cache_directory()
623
+ ... print(f"Cache location: {cache_path}")
624
+ """
625
+ return self._storage.cache_directory
626
+
627
+ def get_stats(self) -> dict[str, Any]:
628
+ """Get fetcher statistics for monitoring.
629
+
630
+ Returns:
631
+ Dictionary with cache and prefetch statistics
632
+
633
+ Example:
634
+ >>> async with AWSServiceFetcher() as fetcher:
635
+ ... stats = fetcher.get_stats()
636
+ ... print(f"Prefetched {stats['prefetched_services']} services")
637
+ """
638
+ return {
639
+ "prefetched_services": len(self._prefetched_services),
640
+ **self._cache.get_stats(),
641
+ }