iam-policy-validator 1.2.0__py3-none-any.whl → 1.3.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.

Potentially problematic release.


This version of iam-policy-validator might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -1,6 +1,6 @@
1
1
  iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=DXwy7NFu0sFYGUDCtWhx6tznod1u86TF0qT4bEvP8xQ,206
3
+ iam_validator/__version__.py,sha256=BOzo0kDxoue17MkZOqACxqP9TwbfCJhkzZuMsC5TMac,206
4
4
  iam_validator/checks/__init__.py,sha256=eKTPgiZ1i3zvyP6OdKgLx9s3u69onITMYifmJPJwZgM,968
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
6
6
  iam_validator/checks/action_resource_constraint.py,sha256=p-gP7S9QYR6M7vffrnJY6LOlMUTn0kpEbrxQ8pTY5rs,6031
@@ -14,10 +14,11 @@ iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIe
14
14
  iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
15
15
  iam_validator/checks/utils/sensitive_action_matcher.py,sha256=VlTpgjMnympYa28kOdm6xRIUL2P87rOvm1O2NdnjtVI,8900
16
16
  iam_validator/checks/utils/wildcard_expansion.py,sha256=V3V_KRpapOzPBhpUObJjGHoMhvCH90QvDxppeEHIG_U,3152
17
- iam_validator/commands/__init__.py,sha256=lF0fSUukLSxTAvhjg-0P79YMseYwihIr_tmQYbfNgcY,425
17
+ iam_validator/commands/__init__.py,sha256=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
18
18
  iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
19
19
  iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
20
20
  iam_validator/commands/cache.py,sha256=NHfbIDWI8tj-3o-4fIZJQS-Vvd9bxIH3Lk6kBtNuiUU,14212
21
+ iam_validator/commands/download_services.py,sha256=anRcobOuhkiEmHpwW_AJb1e2ifgkgYAO2-b9-JBrBcg,9152
21
22
  iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
22
23
  iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
23
24
  iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
@@ -46,8 +47,8 @@ iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkS
46
47
  iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
47
48
  iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
48
49
  iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
49
- iam_policy_validator-1.2.0.dist-info/METADATA,sha256=4b0zMRoJwnyeuwQJzR8eyIrZLVB_aFzhwomz0nybwZk,29136
50
- iam_policy_validator-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
51
- iam_policy_validator-1.2.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
52
- iam_policy_validator-1.2.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
53
- iam_policy_validator-1.2.0.dist-info/RECORD,,
50
+ iam_policy_validator-1.3.0.dist-info/METADATA,sha256=FOWdp3xcENWJmZJtZ8lQlg53Bd6-8v9RpdBLKVvEY3Q,29136
51
+ iam_policy_validator-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
+ iam_policy_validator-1.3.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
53
+ iam_policy_validator-1.3.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
54
+ iam_policy_validator-1.3.0.dist-info/RECORD,,
@@ -3,5 +3,5 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.2.0"
6
+ __version__ = "1.3.0"
7
7
  __version_info__ = tuple(int(part) for part in __version__.split("."))
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .analyze import AnalyzeCommand
4
4
  from .cache import CacheCommand
5
+ from .download_services import DownloadServicesCommand
5
6
  from .post_to_pr import PostToPRCommand
6
7
  from .validate import ValidateCommand
7
8
 
@@ -11,6 +12,14 @@ ALL_COMMANDS = [
11
12
  PostToPRCommand(),
12
13
  AnalyzeCommand(),
13
14
  CacheCommand(),
15
+ DownloadServicesCommand(),
14
16
  ]
15
17
 
16
- __all__ = ["ValidateCommand", "PostToPRCommand", "AnalyzeCommand", "CacheCommand", "ALL_COMMANDS"]
18
+ __all__ = [
19
+ "ValidateCommand",
20
+ "PostToPRCommand",
21
+ "AnalyzeCommand",
22
+ "CacheCommand",
23
+ "DownloadServicesCommand",
24
+ "ALL_COMMANDS",
25
+ ]
@@ -0,0 +1,260 @@
1
+ """Download AWS service definitions command."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import logging
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ import httpx
11
+ from rich.console import Console
12
+ from rich.progress import (
13
+ BarColumn,
14
+ Progress,
15
+ TaskID,
16
+ TextColumn,
17
+ TimeRemainingColumn,
18
+ )
19
+
20
+ from iam_validator.commands.base import Command
21
+
22
+ logger = logging.getLogger(__name__)
23
+ console = Console()
24
+
25
+ BASE_URL = "https://servicereference.us-east-1.amazonaws.com/"
26
+ DEFAULT_OUTPUT_DIR = Path("aws_services")
27
+
28
+
29
+ class DownloadServicesCommand(Command):
30
+ """Download all AWS service definition JSON files."""
31
+
32
+ @property
33
+ def name(self) -> str:
34
+ return "sync-services"
35
+
36
+ @property
37
+ def help(self) -> str:
38
+ return "Sync/download all AWS service definitions for offline use"
39
+
40
+ @property
41
+ def epilog(self) -> str:
42
+ return """
43
+ Examples:
44
+ # Sync all AWS service definitions to default directory (aws_services/)
45
+ iam-validator sync-services
46
+
47
+ # Sync to a custom directory
48
+ iam-validator sync-services --output-dir /path/to/backup
49
+
50
+ # Limit concurrent downloads
51
+ iam-validator sync-services --max-concurrent 5
52
+
53
+ # Enable verbose output
54
+ iam-validator sync-services --log-level debug
55
+
56
+ Directory structure:
57
+ aws_services/
58
+ _manifest.json # Metadata about the download
59
+ _services.json # List of all services
60
+ s3.json # Individual service definitions
61
+ ec2.json
62
+ iam.json
63
+ ...
64
+
65
+ This command is useful for:
66
+ - Creating offline backups of AWS service definitions
67
+ - Avoiding API rate limiting during development
68
+ - Ensuring consistent service definitions across environments
69
+ """
70
+
71
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
72
+ """Add sync-services command arguments."""
73
+ parser.add_argument(
74
+ "--output-dir",
75
+ type=Path,
76
+ default=DEFAULT_OUTPUT_DIR,
77
+ help=f"Output directory for downloaded files (default: {DEFAULT_OUTPUT_DIR})",
78
+ )
79
+
80
+ parser.add_argument(
81
+ "--max-concurrent",
82
+ type=int,
83
+ default=10,
84
+ help="Maximum number of concurrent downloads (default: 10)",
85
+ )
86
+
87
+ async def execute(self, args: argparse.Namespace) -> int:
88
+ """Execute the sync-services command."""
89
+ output_dir = args.output_dir
90
+ max_concurrent = args.max_concurrent
91
+
92
+ try:
93
+ await self._download_all_services(output_dir, max_concurrent)
94
+ return 0
95
+ except Exception as e:
96
+ console.print(f"[red]Error:[/red] {e}")
97
+ logger.error(f"Download failed: {e}", exc_info=True)
98
+ return 1
99
+
100
+ async def _download_services_list(self, client: httpx.AsyncClient) -> list[dict]:
101
+ """Download the list of all AWS services.
102
+
103
+ Args:
104
+ client: HTTP client for making requests
105
+
106
+ Returns:
107
+ List of service info dictionaries
108
+ """
109
+ console.print(f"[cyan]Fetching services list from {BASE_URL}...[/cyan]")
110
+
111
+ try:
112
+ response = await client.get(BASE_URL, timeout=30.0)
113
+ response.raise_for_status()
114
+ services = response.json()
115
+
116
+ console.print(f"[green]✓[/green] Found {len(services)} AWS services")
117
+ return services
118
+ except Exception as e:
119
+ logger.error(f"Failed to fetch services list: {e}")
120
+ raise
121
+
122
+ async def _download_service_detail(
123
+ self,
124
+ client: httpx.AsyncClient,
125
+ service_name: str,
126
+ service_url: str,
127
+ semaphore: asyncio.Semaphore,
128
+ progress: Progress,
129
+ task_id: TaskID,
130
+ ) -> tuple[str, dict | None]:
131
+ """Download detailed JSON for a single service.
132
+
133
+ Args:
134
+ client: HTTP client for making requests
135
+ service_name: Name of the service
136
+ service_url: URL to fetch service details
137
+ semaphore: Semaphore to limit concurrent requests
138
+ progress: Progress bar instance
139
+ task_id: Progress task ID
140
+
141
+ Returns:
142
+ Tuple of (service_name, service_data) or (service_name, None) if failed
143
+ """
144
+ async with semaphore:
145
+ try:
146
+ logger.debug(f"Downloading {service_name}...")
147
+ response = await client.get(service_url, timeout=30.0)
148
+ response.raise_for_status()
149
+ data = response.json()
150
+ logger.debug(f"✓ Downloaded {service_name}")
151
+ progress.update(task_id, advance=1)
152
+ return service_name, data
153
+ except Exception as e:
154
+ logger.error(f"✗ Failed to download {service_name}: {e}")
155
+ progress.update(task_id, advance=1)
156
+ return service_name, None
157
+
158
+ async def _download_all_services(self, output_dir: Path, max_concurrent: int = 10) -> None:
159
+ """Download all AWS service definitions.
160
+
161
+ Args:
162
+ output_dir: Directory to save the downloaded files
163
+ max_concurrent: Maximum number of concurrent downloads
164
+ """
165
+ # Create output directory
166
+ output_dir.mkdir(parents=True, exist_ok=True)
167
+ console.print(f"[cyan]Output directory:[/cyan] {output_dir.absolute()}\n")
168
+
169
+ # Create HTTP client with connection pooling
170
+ async with httpx.AsyncClient(
171
+ limits=httpx.Limits(max_connections=max_concurrent, max_keepalive_connections=5),
172
+ timeout=httpx.Timeout(30.0),
173
+ ) as client:
174
+ # Download services list
175
+ services = await self._download_services_list(client)
176
+
177
+ # Save services list (underscore prefix for easy discovery at top of directory)
178
+ services_file = output_dir / "_services.json"
179
+ with open(services_file, "w") as f:
180
+ json.dump(services, f, indent=2)
181
+ console.print(f"[green]✓[/green] Saved services list to {services_file}\n")
182
+
183
+ # Download all service details with rate limiting and progress bar
184
+ semaphore = asyncio.Semaphore(max_concurrent)
185
+ tasks = []
186
+
187
+ # Set up progress bar
188
+ with Progress(
189
+ TextColumn("[progress.description]{task.description}"),
190
+ BarColumn(),
191
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
192
+ TextColumn("({task.completed}/{task.total})"),
193
+ TimeRemainingColumn(),
194
+ console=console,
195
+ ) as progress:
196
+ task_id = progress.add_task(
197
+ "[cyan]Downloading service definitions...", total=len(services)
198
+ )
199
+
200
+ for item in services:
201
+ service_name = item.get("service")
202
+ service_url = item.get("url")
203
+
204
+ if service_name and service_url:
205
+ task = self._download_service_detail(
206
+ client, service_name, service_url, semaphore, progress, task_id
207
+ )
208
+ tasks.append(task)
209
+
210
+ # Download all services concurrently
211
+ results = await asyncio.gather(*tasks)
212
+
213
+ # Save individual service files
214
+ successful = 0
215
+ failed = 0
216
+
217
+ console.print("\n[cyan]Saving service definitions...[/cyan]")
218
+
219
+ for service_name, data in results:
220
+ if data is not None:
221
+ # Normalize filename (lowercase, safe characters)
222
+ filename = f"{service_name.lower().replace(' ', '_')}.json"
223
+ service_file = output_dir / filename
224
+
225
+ with open(service_file, "w") as f:
226
+ json.dump(data, f, indent=2)
227
+
228
+ successful += 1
229
+ else:
230
+ failed += 1
231
+
232
+ # Create manifest with metadata
233
+ manifest = {
234
+ "download_date": datetime.now(timezone.utc).isoformat(),
235
+ "total_services": len(services),
236
+ "successful_downloads": successful,
237
+ "failed_downloads": failed,
238
+ "base_url": BASE_URL,
239
+ }
240
+
241
+ manifest_file = output_dir / "_manifest.json"
242
+ with open(manifest_file, "w") as f:
243
+ json.dump(manifest, f, indent=2)
244
+
245
+ # Print summary
246
+ console.print(f"\n{'=' * 60}")
247
+ console.print("[bold cyan]Download Summary:[/bold cyan]")
248
+ console.print(f" Total services: {len(services)}")
249
+ console.print(f" [green]Successful:[/green] {successful}")
250
+ if failed > 0:
251
+ console.print(f" [red]Failed:[/red] {failed}")
252
+ console.print(f" Output directory: {output_dir.absolute()}")
253
+ console.print(f" Manifest: {manifest_file}")
254
+ console.print(f"{'=' * 60}")
255
+
256
+ if failed > 0:
257
+ console.print(
258
+ "\n[yellow]Warning:[/yellow] Some services failed to download. "
259
+ "Check the logs for details."
260
+ )