iam-policy-validator 1.2.0__py3-none-any.whl → 1.3.1__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.1
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
@@ -651,7 +651,9 @@ Use as a library in your Python applications:
651
651
 
652
652
  ```python
653
653
  import asyncio
654
- from iam_validator.core import PolicyLoader, validate_policies, ReportGenerator
654
+ from iam_validator.core.policy_loader import PolicyLoader
655
+ from iam_validator.core.policy_checks import validate_policies
656
+ from iam_validator.core.report import ReportGenerator
655
657
 
656
658
  async def main():
657
659
  # Load policies
@@ -669,6 +671,10 @@ async def main():
669
671
  asyncio.run(main())
670
672
  ```
671
673
 
674
+ **📚 For comprehensive Python library documentation, see:**
675
+ - **[Python Library Usage Guide](docs/python-library-usage.md)** - Complete guide with examples
676
+ - **[Library Examples](examples/library-usage/)** - Runnable code examples
677
+
672
678
  ## Validation Checks
673
679
 
674
680
  ### 1. Action Validation
@@ -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=hbgDe5p_vG5JrspHS61bAQLyKxbRMqbUDzeKUVq_gmo,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,21 +14,22 @@ 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
24
25
  iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
25
26
  iam_validator/core/access_analyzer_report.py,sha256=IrQVszlhFfQ6WykYLpig7TU3hf8dnQTegPDsOvHjR5Q,24873
26
- iam_validator/core/aws_fetcher.py,sha256=6W4ixYEMx4Y5bx9rCB65CDqZh7iUVANAvhFVHu0MOKQ,32654
27
+ iam_validator/core/aws_fetcher.py,sha256=0rG7qi3Lz6ulU6pDL0nZ6sklgSAS5pwo0ViykDspRt8,33382
27
28
  iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
28
29
  iam_validator/core/check_registry.py,sha256=wxqaF2t_3lWgT6x7_PnnZ8XGjHKUxUk72UlmdYBLFyo,15679
29
30
  iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
30
31
  iam_validator/core/config_loader.py,sha256=Pq2rd6LJtEZET0ZeW4hEZS2ZRLC5gNRsKbtLyIsT21I,16516
31
- iam_validator/core/defaults.py,sha256=tp8MPrFicRvI0dp8yH95MzJ9tC33n0N92aUC3HMkmYc,13289
32
+ iam_validator/core/defaults.py,sha256=brGPx0_8zmsMNddYryMKbcoIh8VJq2mdXZdGDItAsQs,13251
32
33
  iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
33
34
  iam_validator/core/policy_checks.py,sha256=pMlZ2XkuqppVOUZq__e8w_yGoy7lIHjAB5RiTXwJo4Q,25114
34
35
  iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
@@ -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.1.dist-info/METADATA,sha256=NNF1fvnG9g8pGMopQ71yn5rHtWnRIVMBUGPEeNLX9jI,29465
51
+ iam_policy_validator-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
+ iam_policy_validator-1.3.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
53
+ iam_policy_validator-1.3.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
54
+ iam_policy_validator-1.3.1.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.1"
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
+ )
@@ -804,11 +804,12 @@ class AWSServiceFetcher:
804
804
 
805
805
  service_prefix, action_name = self.parse_action(action)
806
806
 
807
- # Check global conditions first (fast)
807
+ # Check if it's a global condition key
808
+ is_global_key = False
808
809
  if condition_key.startswith("aws:"):
809
810
  global_conditions = get_global_conditions()
810
811
  if global_conditions.is_valid_global_key(condition_key):
811
- return True, None
812
+ is_global_key = True
812
813
  else:
813
814
  return (
814
815
  False,
@@ -831,6 +832,19 @@ class AWSServiceFetcher:
831
832
  ):
832
833
  return True, None
833
834
 
835
+ # If it's a global key but the action has specific condition keys defined,
836
+ # check if the global key is explicitly listed in the action's supported keys
837
+ if is_global_key and action_detail.action_condition_keys is not None:
838
+ return (
839
+ False,
840
+ f"Condition key '{condition_key}' is not supported by action '{action}'. "
841
+ f"This action has a specific set of supported condition keys.",
842
+ )
843
+
844
+ # If it's a global key and action doesn't define specific keys, allow it
845
+ if is_global_key:
846
+ return True, None
847
+
834
848
  return (
835
849
  False,
836
850
  f"Condition key '{condition_key}' is not valid for action '{action}'",
@@ -300,7 +300,7 @@ With specific values:
300
300
  ],
301
301
  },
302
302
  {
303
- "actions": ["s3:PutObject", "s3:DeleteObject", "s3:CreateBucket"],
303
+ "actions": ["s3:PutObject"],
304
304
  "severity": "medium",
305
305
  "required_conditions": [
306
306
  {