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.
- iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,255 @@
|
|
|
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 BarColumn, Progress, TaskID, TextColumn, TimeRemainingColumn
|
|
13
|
+
|
|
14
|
+
from iam_validator.commands.base import Command
|
|
15
|
+
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
|
|
21
|
+
DEFAULT_OUTPUT_DIR = Path("aws_services")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DownloadServicesCommand(Command):
|
|
25
|
+
"""Download all AWS service definition JSON files."""
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return "sync-services"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def help(self) -> str:
|
|
33
|
+
return "Sync/download all AWS service definitions for offline use"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def epilog(self) -> str:
|
|
37
|
+
return """
|
|
38
|
+
Examples:
|
|
39
|
+
# Sync all AWS service definitions to default directory (aws_services/)
|
|
40
|
+
iam-validator sync-services
|
|
41
|
+
|
|
42
|
+
# Sync to a custom directory
|
|
43
|
+
iam-validator sync-services --output-dir /path/to/backup
|
|
44
|
+
|
|
45
|
+
# Limit concurrent downloads
|
|
46
|
+
iam-validator sync-services --max-concurrent 5
|
|
47
|
+
|
|
48
|
+
# Enable verbose output
|
|
49
|
+
iam-validator sync-services --log-level debug
|
|
50
|
+
|
|
51
|
+
Directory structure:
|
|
52
|
+
aws_services/
|
|
53
|
+
_manifest.json # Metadata about the download
|
|
54
|
+
_services.json # List of all services
|
|
55
|
+
s3.json # Individual service definitions
|
|
56
|
+
ec2.json
|
|
57
|
+
iam.json
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
This command is useful for:
|
|
61
|
+
- Creating offline backups of AWS service definitions
|
|
62
|
+
- Avoiding API rate limiting during development
|
|
63
|
+
- Ensuring consistent service definitions across environments
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
67
|
+
"""Add sync-services command arguments."""
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--output-dir",
|
|
70
|
+
type=Path,
|
|
71
|
+
default=DEFAULT_OUTPUT_DIR,
|
|
72
|
+
help=f"Output directory for downloaded files (default: {DEFAULT_OUTPUT_DIR})",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--max-concurrent",
|
|
77
|
+
type=int,
|
|
78
|
+
default=10,
|
|
79
|
+
help="Maximum number of concurrent downloads (default: 10)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
83
|
+
"""Execute the sync-services command."""
|
|
84
|
+
output_dir = args.output_dir
|
|
85
|
+
max_concurrent = args.max_concurrent
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
await self._download_all_services(output_dir, max_concurrent)
|
|
89
|
+
return 0
|
|
90
|
+
except Exception as e:
|
|
91
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
92
|
+
logger.error(f"Download failed: {e}", exc_info=True)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
async def _download_services_list(self, client: httpx.AsyncClient) -> list[dict]:
|
|
96
|
+
"""Download the list of all AWS services.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
client: HTTP client for making requests
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of service info dictionaries
|
|
103
|
+
"""
|
|
104
|
+
console.print(f"[cyan]Fetching services list from {BASE_URL}...[/cyan]")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
response = await client.get(BASE_URL, timeout=30.0)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
services = response.json()
|
|
110
|
+
|
|
111
|
+
console.print(f"[green]✓[/green] Found {len(services)} AWS services")
|
|
112
|
+
return services
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to fetch services list: {e}")
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
async def _download_service_detail(
|
|
118
|
+
self,
|
|
119
|
+
client: httpx.AsyncClient,
|
|
120
|
+
service_name: str,
|
|
121
|
+
service_url: str,
|
|
122
|
+
semaphore: asyncio.Semaphore,
|
|
123
|
+
progress: Progress,
|
|
124
|
+
task_id: TaskID,
|
|
125
|
+
) -> tuple[str, dict | None]:
|
|
126
|
+
"""Download detailed JSON for a single service.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
client: HTTP client for making requests
|
|
130
|
+
service_name: Name of the service
|
|
131
|
+
service_url: URL to fetch service details
|
|
132
|
+
semaphore: Semaphore to limit concurrent requests
|
|
133
|
+
progress: Progress bar instance
|
|
134
|
+
task_id: Progress task ID
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Tuple of (service_name, service_data) or (service_name, None) if failed
|
|
138
|
+
"""
|
|
139
|
+
async with semaphore:
|
|
140
|
+
try:
|
|
141
|
+
logger.debug(f"Downloading {service_name}...")
|
|
142
|
+
response = await client.get(service_url, timeout=30.0)
|
|
143
|
+
response.raise_for_status()
|
|
144
|
+
data = response.json()
|
|
145
|
+
logger.debug(f"✓ Downloaded {service_name}")
|
|
146
|
+
progress.update(task_id, advance=1)
|
|
147
|
+
return service_name, data
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"✗ Failed to download {service_name}: {e}")
|
|
150
|
+
progress.update(task_id, advance=1)
|
|
151
|
+
return service_name, None
|
|
152
|
+
|
|
153
|
+
async def _download_all_services(self, output_dir: Path, max_concurrent: int = 10) -> None:
|
|
154
|
+
"""Download all AWS service definitions.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
output_dir: Directory to save the downloaded files
|
|
158
|
+
max_concurrent: Maximum number of concurrent downloads
|
|
159
|
+
"""
|
|
160
|
+
# Create output directory
|
|
161
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
console.print(f"[cyan]Output directory:[/cyan] {output_dir.absolute()}\n")
|
|
163
|
+
|
|
164
|
+
# Create HTTP client with connection pooling
|
|
165
|
+
async with httpx.AsyncClient(
|
|
166
|
+
limits=httpx.Limits(max_connections=max_concurrent, max_keepalive_connections=5),
|
|
167
|
+
timeout=httpx.Timeout(30.0),
|
|
168
|
+
) as client:
|
|
169
|
+
# Download services list
|
|
170
|
+
services = await self._download_services_list(client)
|
|
171
|
+
|
|
172
|
+
# Save services list (underscore prefix for easy discovery at top of directory)
|
|
173
|
+
services_file = output_dir / "_services.json"
|
|
174
|
+
with open(services_file, "w") as f:
|
|
175
|
+
json.dump(services, f, indent=2)
|
|
176
|
+
console.print(f"[green]✓[/green] Saved services list to {services_file}\n")
|
|
177
|
+
|
|
178
|
+
# Download all service details with rate limiting and progress bar
|
|
179
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
180
|
+
tasks = []
|
|
181
|
+
|
|
182
|
+
# Set up progress bar
|
|
183
|
+
with Progress(
|
|
184
|
+
TextColumn("[progress.description]{task.description}"),
|
|
185
|
+
BarColumn(),
|
|
186
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
187
|
+
TextColumn("({task.completed}/{task.total})"),
|
|
188
|
+
TimeRemainingColumn(),
|
|
189
|
+
console=console,
|
|
190
|
+
) as progress:
|
|
191
|
+
task_id = progress.add_task(
|
|
192
|
+
"[cyan]Downloading service definitions...", total=len(services)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for item in services:
|
|
196
|
+
service_name = item.get("service")
|
|
197
|
+
service_url = item.get("url")
|
|
198
|
+
|
|
199
|
+
if service_name and service_url:
|
|
200
|
+
task = self._download_service_detail(
|
|
201
|
+
client, service_name, service_url, semaphore, progress, task_id
|
|
202
|
+
)
|
|
203
|
+
tasks.append(task)
|
|
204
|
+
|
|
205
|
+
# Download all services concurrently
|
|
206
|
+
results = await asyncio.gather(*tasks)
|
|
207
|
+
|
|
208
|
+
# Save individual service files
|
|
209
|
+
successful = 0
|
|
210
|
+
failed = 0
|
|
211
|
+
|
|
212
|
+
console.print("\n[cyan]Saving service definitions...[/cyan]")
|
|
213
|
+
|
|
214
|
+
for service_name, data in results:
|
|
215
|
+
if data is not None:
|
|
216
|
+
# Normalize filename (lowercase, safe characters)
|
|
217
|
+
filename = f"{service_name.lower().replace(' ', '_')}.json"
|
|
218
|
+
service_file = output_dir / filename
|
|
219
|
+
|
|
220
|
+
with open(service_file, "w") as f:
|
|
221
|
+
json.dump(data, f, indent=2)
|
|
222
|
+
|
|
223
|
+
successful += 1
|
|
224
|
+
else:
|
|
225
|
+
failed += 1
|
|
226
|
+
|
|
227
|
+
# Create manifest with metadata
|
|
228
|
+
manifest = {
|
|
229
|
+
"download_date": datetime.now(timezone.utc).isoformat(),
|
|
230
|
+
"total_services": len(services),
|
|
231
|
+
"successful_downloads": successful,
|
|
232
|
+
"failed_downloads": failed,
|
|
233
|
+
"base_url": BASE_URL,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
manifest_file = output_dir / "_manifest.json"
|
|
237
|
+
with open(manifest_file, "w") as f:
|
|
238
|
+
json.dump(manifest, f, indent=2)
|
|
239
|
+
|
|
240
|
+
# Print summary
|
|
241
|
+
console.print(f"\n{'=' * 60}")
|
|
242
|
+
console.print("[bold cyan]Download Summary:[/bold cyan]")
|
|
243
|
+
console.print(f" Total services: {len(services)}")
|
|
244
|
+
console.print(f" [green]Successful:[/green] {successful}")
|
|
245
|
+
if failed > 0:
|
|
246
|
+
console.print(f" [red]Failed:[/red] {failed}")
|
|
247
|
+
console.print(f" Output directory: {output_dir.absolute()}")
|
|
248
|
+
console.print(f" Manifest: {manifest_file}")
|
|
249
|
+
console.print(f"{'=' * 60}")
|
|
250
|
+
|
|
251
|
+
if failed > 0:
|
|
252
|
+
console.print(
|
|
253
|
+
"\n[yellow]Warning:[/yellow] Some services failed to download. "
|
|
254
|
+
"Check the logs for details."
|
|
255
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Post-to-PR command for IAM Policy Validator."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from iam_validator.commands.base import Command
|
|
6
|
+
from iam_validator.core.pr_commenter import post_report_to_pr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PostToPRCommand(Command):
|
|
10
|
+
"""Command to post a validation report to a GitHub PR."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "post-to-pr"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def help(self) -> str:
|
|
18
|
+
return "Post a JSON report to a GitHub PR"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def epilog(self) -> str:
|
|
22
|
+
return """
|
|
23
|
+
Examples:
|
|
24
|
+
# Post report with line comments
|
|
25
|
+
iam-validator post-to-pr --report report.json
|
|
26
|
+
|
|
27
|
+
# Post only summary comment
|
|
28
|
+
iam-validator post-to-pr --report report.json --no-review
|
|
29
|
+
|
|
30
|
+
# Post only line comments (no summary)
|
|
31
|
+
iam-validator post-to-pr --report report.json --no-summary
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
35
|
+
"""Add post-to-pr command arguments."""
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--report",
|
|
38
|
+
"-r",
|
|
39
|
+
required=True,
|
|
40
|
+
help="Path to JSON report file",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--create-review",
|
|
45
|
+
action="store_true",
|
|
46
|
+
default=True,
|
|
47
|
+
help="Create line-specific review comments (default: True)",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--no-review",
|
|
52
|
+
action="store_false",
|
|
53
|
+
dest="create_review",
|
|
54
|
+
help="Don't create line-specific review comments",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--add-summary",
|
|
59
|
+
action="store_true",
|
|
60
|
+
default=True,
|
|
61
|
+
help="Add summary comment (default: True)",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--no-summary",
|
|
66
|
+
action="store_false",
|
|
67
|
+
dest="add_summary",
|
|
68
|
+
help="Don't add summary comment",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--config",
|
|
73
|
+
"-c",
|
|
74
|
+
help="Path to configuration file (for fail_on_severity setting)",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
78
|
+
"""Execute the post-to-pr command."""
|
|
79
|
+
success = await post_report_to_pr(
|
|
80
|
+
args.report,
|
|
81
|
+
create_review=args.create_review,
|
|
82
|
+
add_summary=args.add_summary,
|
|
83
|
+
config_path=getattr(args, "config", None),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return 0 if success else 1
|