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,393 @@
1
+ """Cache management command for IAM Policy Validator."""
2
+
3
+ import argparse
4
+ import logging
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from iam_validator.commands.base import Command
11
+ from iam_validator.core.aws_service import AWSServiceFetcher
12
+ from iam_validator.core.aws_service.storage import ServiceFileStorage
13
+ from iam_validator.core.config.config_loader import ConfigLoader
14
+
15
+ logger = logging.getLogger(__name__)
16
+ console = Console()
17
+
18
+
19
+ class CacheCommand(Command):
20
+ """Manage AWS service definition cache."""
21
+
22
+ @property
23
+ def name(self) -> str:
24
+ return "cache"
25
+
26
+ @property
27
+ def help(self) -> str:
28
+ return "Manage AWS service definition cache"
29
+
30
+ @property
31
+ def epilog(self) -> str:
32
+ return """
33
+ Examples:
34
+ # Show cache information
35
+ iam-validator cache info
36
+
37
+ # List all cached services
38
+ iam-validator cache list
39
+
40
+ # Clear all cached AWS service definitions
41
+ iam-validator cache clear
42
+
43
+ # Refresh cache (clear and pre-fetch common services)
44
+ iam-validator cache refresh
45
+
46
+ # Pre-fetch common AWS services
47
+ iam-validator cache prefetch
48
+
49
+ # Show cache location
50
+ iam-validator cache location
51
+ """
52
+
53
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
54
+ """Add cache command arguments."""
55
+ subparsers = parser.add_subparsers(dest="cache_action", help="Cache action to perform")
56
+
57
+ # Info subcommand
58
+ info_parser = subparsers.add_parser("info", help="Show cache information and statistics")
59
+ info_parser.add_argument(
60
+ "--config",
61
+ type=str,
62
+ help="Path to configuration file",
63
+ )
64
+
65
+ # List subcommand
66
+ list_parser = subparsers.add_parser("list", help="List all cached AWS services")
67
+ list_parser.add_argument(
68
+ "--config",
69
+ type=str,
70
+ help="Path to configuration file",
71
+ )
72
+ list_parser.add_argument(
73
+ "--format",
74
+ choices=["table", "columns", "simple"],
75
+ default="table",
76
+ help="Output format (default: table)",
77
+ )
78
+
79
+ # Clear subcommand
80
+ clear_parser = subparsers.add_parser(
81
+ "clear", help="Clear all cached AWS service definitions"
82
+ )
83
+ clear_parser.add_argument(
84
+ "--config",
85
+ type=str,
86
+ help="Path to configuration file",
87
+ )
88
+
89
+ # Refresh subcommand
90
+ refresh_parser = subparsers.add_parser(
91
+ "refresh", help="Clear cache and pre-fetch common AWS services"
92
+ )
93
+ refresh_parser.add_argument(
94
+ "--config",
95
+ type=str,
96
+ help="Path to configuration file",
97
+ )
98
+
99
+ # Prefetch subcommand
100
+ prefetch_parser = subparsers.add_parser(
101
+ "prefetch", help="Pre-fetch common AWS services (without clearing)"
102
+ )
103
+ prefetch_parser.add_argument(
104
+ "--config",
105
+ type=str,
106
+ help="Path to configuration file",
107
+ )
108
+
109
+ # Location subcommand
110
+ location_parser = subparsers.add_parser("location", help="Show cache directory location")
111
+ location_parser.add_argument(
112
+ "--config",
113
+ type=str,
114
+ help="Path to configuration file",
115
+ )
116
+
117
+ async def execute(self, args: argparse.Namespace) -> int:
118
+ """Execute cache command."""
119
+ if not hasattr(args, "cache_action") or not args.cache_action:
120
+ console.print("[red]Error:[/red] No cache action specified")
121
+ console.print("Use 'iam-validator cache --help' for available actions")
122
+ return 1
123
+
124
+ # Load config to get cache settings
125
+ config_path = getattr(args, "config", None)
126
+ config = ConfigLoader.load_config(explicit_path=config_path, allow_missing=True)
127
+
128
+ cache_enabled = config.get_setting("cache_enabled", True)
129
+ cache_ttl_hours = config.get_setting("cache_ttl_hours", 168)
130
+ cache_directory = config.get_setting("cache_directory", None)
131
+ cache_ttl_seconds = cache_ttl_hours * 3600
132
+
133
+ # Get cache directory (even if caching is disabled, for info purposes)
134
+ cache_dir = ServiceFileStorage.get_cache_directory(cache_directory)
135
+
136
+ action = args.cache_action
137
+
138
+ if action == "info":
139
+ return await self._show_info(cache_dir, cache_enabled, cache_ttl_hours)
140
+ elif action == "list":
141
+ output_format = getattr(args, "format", "table")
142
+ return self._list_cached_services(cache_dir, output_format)
143
+ elif action == "clear":
144
+ return await self._clear_cache(cache_dir, cache_enabled)
145
+ elif action == "refresh":
146
+ return await self._refresh_cache(cache_enabled, cache_ttl_seconds, cache_directory)
147
+ elif action == "prefetch":
148
+ return await self._prefetch_services(cache_enabled, cache_ttl_seconds, cache_directory)
149
+ elif action == "location":
150
+ return self._show_location(cache_dir)
151
+ else:
152
+ console.print(f"[red]Error:[/red] Unknown cache action: {action}")
153
+ return 1
154
+
155
+ async def _show_info(self, cache_dir: Path, cache_enabled: bool, cache_ttl_hours: int) -> int:
156
+ """Show cache information and statistics."""
157
+ table = Table(title="Cache Information")
158
+ table.add_column("Setting", style="cyan", no_wrap=True)
159
+ table.add_column("Value", style="white")
160
+
161
+ # Cache status
162
+ table.add_row(
163
+ "Status", "[green]Enabled[/green]" if cache_enabled else "[red]Disabled[/red]"
164
+ )
165
+
166
+ # Cache location
167
+ table.add_row("Location", str(cache_dir))
168
+
169
+ # Cache exists?
170
+ exists = cache_dir.exists()
171
+ table.add_row("Exists", "[green]Yes[/green]" if exists else "[yellow]No[/yellow]")
172
+
173
+ # Cache TTL
174
+ ttl_days = cache_ttl_hours / 24
175
+ table.add_row("TTL", f"{cache_ttl_hours} hours ({ttl_days:.1f} days)")
176
+
177
+ if exists:
178
+ # Count cached files
179
+ cache_files = list(cache_dir.glob("*.json"))
180
+ table.add_row("Cached Services", str(len(cache_files)))
181
+
182
+ # Calculate cache size
183
+ total_size = sum(f.stat().st_size for f in cache_files)
184
+ size_mb = total_size / (1024 * 1024)
185
+ table.add_row("Cache Size", f"{size_mb:.2f} MB")
186
+
187
+ # Show some cached services
188
+ if cache_files:
189
+ service_names = []
190
+ for f in cache_files[:5]:
191
+ name = f.stem.split("_")[0] if "_" in f.stem else f.stem
192
+ service_names.append(name)
193
+ sample = ", ".join(service_names)
194
+ if len(cache_files) > 5:
195
+ sample += f", ... ({len(cache_files) - 5} more)"
196
+ table.add_row("Sample Services", sample)
197
+
198
+ console.print(table)
199
+ return 0
200
+
201
+ def _list_cached_services(self, cache_dir: Path, output_format: str) -> int:
202
+ """List all cached AWS services."""
203
+ if not cache_dir.exists():
204
+ console.print("[yellow]Cache directory does not exist[/yellow]")
205
+ return 0
206
+
207
+ cache_files = list(cache_dir.glob("*.json"))
208
+
209
+ if not cache_files:
210
+ console.print("[yellow]No services cached yet[/yellow]")
211
+ return 0
212
+
213
+ # Extract service names from filenames
214
+ services = []
215
+ for f in cache_files:
216
+ # Handle both formats: "service_hash.json" and "services_list.json"
217
+ if f.stem == "services_list":
218
+ continue # Skip the services list file
219
+
220
+ # Extract service name (before underscore or full name)
221
+ name = f.stem.split("_")[0] if "_" in f.stem else f.stem
222
+
223
+ # Get file stats
224
+ size = f.stat().st_size
225
+ mtime = f.stat().st_mtime
226
+
227
+ services.append({"name": name, "size": size, "file": f.name, "mtime": mtime})
228
+
229
+ # Sort by service name
230
+ services.sort(key=lambda x: str(x["name"]))
231
+
232
+ if output_format == "table":
233
+ self._print_services_table(services)
234
+ elif output_format == "columns":
235
+ self._print_services_columns(services)
236
+ else: # simple
237
+ self._print_services_simple(services)
238
+
239
+ return 0
240
+
241
+ def _print_services_table(self, services: list[dict]) -> None:
242
+ """Print services in a nice table format."""
243
+ from datetime import datetime
244
+
245
+ table = Table(title=f"Cached AWS Services ({len(services)} total)")
246
+ table.add_column("Service", style="cyan", no_wrap=True)
247
+ table.add_column("Cache File", style="white")
248
+ table.add_column("Size", style="yellow", justify="right")
249
+ table.add_column("Cached", style="green")
250
+
251
+ for svc in services:
252
+ size_kb = svc["size"] / 1024
253
+ cached_time = datetime.fromtimestamp(svc["mtime"]).strftime("%Y-%m-%d %H:%M")
254
+
255
+ table.add_row(svc["name"], svc["file"], f"{size_kb:.1f} KB", cached_time)
256
+
257
+ console.print(table)
258
+
259
+ def _print_services_columns(self, services: list[dict]) -> None:
260
+ """Print services in columns format (like ls)."""
261
+ from rich.columns import Columns
262
+
263
+ console.print(f"[cyan]Cached AWS Services ({len(services)} total):[/cyan]\n")
264
+
265
+ service_names = [f"[green]{svc['name']}[/green]" for svc in services]
266
+ console.print(Columns(service_names, equal=True, expand=False))
267
+
268
+ def _print_services_simple(self, services: list[dict]) -> None:
269
+ """Print services in simple list format."""
270
+ console.print(f"[cyan]Cached AWS Services ({len(services)} total):[/cyan]\n")
271
+
272
+ for svc in services:
273
+ console.print(svc["name"])
274
+
275
+ async def _clear_cache(self, cache_dir: Path, cache_enabled: bool) -> int:
276
+ """Clear all cached AWS service definitions."""
277
+ if not cache_enabled:
278
+ console.print("[yellow]Warning:[/yellow] Cache is disabled in config")
279
+ return 0
280
+
281
+ if not cache_dir.exists():
282
+ console.print("[yellow]Cache directory does not exist, nothing to clear[/yellow]")
283
+ return 0
284
+
285
+ # Count files before deletion
286
+ cache_files = list(cache_dir.glob("*.json"))
287
+ file_count = len(cache_files)
288
+
289
+ if file_count == 0:
290
+ console.print("[yellow]Cache is already empty[/yellow]")
291
+ return 0
292
+
293
+ # Delete cache files
294
+ deleted = 0
295
+ failed = 0
296
+ for cache_file in cache_files:
297
+ try:
298
+ cache_file.unlink()
299
+ deleted += 1
300
+ except Exception as e:
301
+ logger.error(f"Failed to delete {cache_file}: {e}")
302
+ failed += 1
303
+
304
+ if failed == 0:
305
+ console.print(f"[green]✓[/green] Cleared {deleted} cached service definitions")
306
+ else:
307
+ console.print(
308
+ f"[yellow]![/yellow] Cleared {deleted} files, failed to delete {failed} files"
309
+ )
310
+ return 1
311
+
312
+ return 0
313
+
314
+ async def _refresh_cache(
315
+ self, cache_enabled: bool, cache_ttl_seconds: int, cache_directory: str | None
316
+ ) -> int:
317
+ """Clear cache and pre-fetch common services."""
318
+ if not cache_enabled:
319
+ console.print("[red]Error:[/red] Cache is disabled in config")
320
+ console.print("Enable cache by setting 'cache_enabled: true' in your config")
321
+ return 1
322
+
323
+ console.print("[cyan]Refreshing cache...[/cyan]")
324
+
325
+ # Create fetcher and clear cache
326
+ async with AWSServiceFetcher(
327
+ enable_cache=cache_enabled,
328
+ cache_ttl=cache_ttl_seconds,
329
+ cache_dir=cache_directory,
330
+ prefetch_common=False, # Don't prefetch yet, we'll do it after clearing
331
+ ) as fetcher:
332
+ # Clear existing cache
333
+ console.print("Clearing old cache...")
334
+ await fetcher.clear_caches()
335
+
336
+ # Prefetch common services
337
+ console.print("Fetching fresh AWS service definitions...")
338
+ services = await fetcher.fetch_services()
339
+ console.print(f"[green]✓[/green] Fetched list of {len(services)} AWS services")
340
+
341
+ # Prefetch common services
342
+ console.print("Pre-fetching common services...")
343
+ prefetched = 0
344
+ for service_name in fetcher.COMMON_SERVICES:
345
+ try:
346
+ await fetcher.fetch_service_by_name(service_name)
347
+ prefetched += 1
348
+ except Exception as e:
349
+ logger.warning(f"Failed to prefetch {service_name}: {e}")
350
+
351
+ console.print(f"[green]✓[/green] Pre-fetched {prefetched} common services")
352
+
353
+ console.print("[green]✓[/green] Cache refreshed successfully")
354
+ return 0
355
+
356
+ async def _prefetch_services(
357
+ self, cache_enabled: bool, cache_ttl_seconds: int, cache_directory: str | None
358
+ ) -> int:
359
+ """Pre-fetch common AWS services without clearing cache."""
360
+ if not cache_enabled:
361
+ console.print("[red]Error:[/red] Cache is disabled in config")
362
+ console.print("Enable cache by setting 'cache_enabled: true' in your config")
363
+ return 1
364
+
365
+ console.print("[cyan]Pre-fetching common AWS services...[/cyan]")
366
+
367
+ async with AWSServiceFetcher(
368
+ enable_cache=cache_enabled,
369
+ cache_ttl=cache_ttl_seconds,
370
+ cache_dir=cache_directory,
371
+ prefetch_common=True, # Enable prefetching
372
+ ) as fetcher:
373
+ # Prefetching happens in __aenter__, just wait for it
374
+ prefetched = len(fetcher._prefetched_services)
375
+ total = len(fetcher.COMMON_SERVICES)
376
+
377
+ console.print(
378
+ f"[green]✓[/green] Pre-fetched {prefetched}/{total} common services successfully"
379
+ )
380
+
381
+ return 0
382
+
383
+ def _show_location(self, cache_dir: Path) -> int:
384
+ """Show cache directory location."""
385
+ console.print(f"[cyan]Cache directory:[/cyan] {cache_dir}")
386
+
387
+ if cache_dir.exists():
388
+ console.print("[green]✓[/green] Directory exists")
389
+ else:
390
+ console.print("[yellow]![/yellow] Directory does not exist yet")
391
+ console.print("It will be created automatically when caching is used")
392
+
393
+ return 0