iam-policy-validator 1.3.1__py3-none-any.whl → 1.5.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.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +164 -19
- iam_policy_validator-1.5.0.dist-info/RECORD +67 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +15 -3
- iam_validator/checks/action_condition_enforcement.py +1 -6
- iam_validator/checks/condition_key_validation.py +21 -1
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/policy_size.py +1 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/sensitive_action.py +178 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/sid_uniqueness.py +45 -7
- iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/post_to_pr.py +7 -0
- iam_validator/commands/validate.py +204 -16
- iam_validator/core/aws_fetcher.py +25 -12
- iam_validator/core/check_registry.py +25 -21
- iam_validator/core/config/__init__.py +83 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/condition_requirements.py +535 -0
- iam_validator/core/config/defaults.py +390 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +133 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/config_loader.py +29 -9
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- iam_validator/core/models.py +13 -3
- iam_validator/core/policy_checks.py +39 -6
- iam_validator/core/pr_commenter.py +30 -9
- iam_policy_validator-1.3.1.dist-info/RECORD +0 -54
- iam_validator/checks/security_best_practices.py +0 -535
- iam_validator/core/defaults.py +0 -366
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,20 +9,15 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
from rich.console import Console
|
|
12
|
-
from rich.progress import
|
|
13
|
-
BarColumn,
|
|
14
|
-
Progress,
|
|
15
|
-
TaskID,
|
|
16
|
-
TextColumn,
|
|
17
|
-
TimeRemainingColumn,
|
|
18
|
-
)
|
|
12
|
+
from rich.progress import BarColumn, Progress, TaskID, TextColumn, TimeRemainingColumn
|
|
19
13
|
|
|
20
14
|
from iam_validator.commands.base import Command
|
|
15
|
+
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
21
16
|
|
|
22
17
|
logger = logging.getLogger(__name__)
|
|
23
18
|
console = Console()
|
|
24
19
|
|
|
25
|
-
BASE_URL =
|
|
20
|
+
BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
|
|
26
21
|
DEFAULT_OUTPUT_DIR = Path("aws_services")
|
|
27
22
|
|
|
28
23
|
|
|
@@ -68,12 +68,19 @@ Examples:
|
|
|
68
68
|
help="Don't add summary comment",
|
|
69
69
|
)
|
|
70
70
|
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--config",
|
|
73
|
+
"-c",
|
|
74
|
+
help="Path to configuration file (for fail_on_severity setting)",
|
|
75
|
+
)
|
|
76
|
+
|
|
71
77
|
async def execute(self, args: argparse.Namespace) -> int:
|
|
72
78
|
"""Execute the post-to-pr command."""
|
|
73
79
|
success = await post_report_to_pr(
|
|
74
80
|
args.report,
|
|
75
81
|
create_review=args.create_review,
|
|
76
82
|
add_summary=args.add_summary,
|
|
83
|
+
config_path=getattr(args, "config", None),
|
|
77
84
|
)
|
|
78
85
|
|
|
79
86
|
return 0 if success else 1
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
from typing import cast
|
|
6
7
|
|
|
7
8
|
from iam_validator.commands.base import Command
|
|
9
|
+
from iam_validator.core.models import PolicyType, ValidationReport
|
|
8
10
|
from iam_validator.core.policy_checks import validate_policies
|
|
9
11
|
from iam_validator.core.policy_loader import PolicyLoader
|
|
10
12
|
from iam_validator.core.report import ReportGenerator
|
|
@@ -41,8 +43,20 @@ Examples:
|
|
|
41
43
|
# Generate JSON output
|
|
42
44
|
iam-validator validate --path ./policies/ --format json --output report.json
|
|
43
45
|
|
|
44
|
-
#
|
|
45
|
-
iam-validator validate --path ./policies/ --
|
|
46
|
+
# Validate resource policies (S3 bucket policies, SNS topics, etc.)
|
|
47
|
+
iam-validator validate --path ./bucket-policies/ --policy-type RESOURCE_POLICY
|
|
48
|
+
|
|
49
|
+
# GitHub integration - all options (PR comment + review comments + job summary)
|
|
50
|
+
iam-validator validate --path ./policies/ --github-comment --github-review --github-summary
|
|
51
|
+
|
|
52
|
+
# Only line-specific review comments (clean, minimal)
|
|
53
|
+
iam-validator validate --path ./policies/ --github-review
|
|
54
|
+
|
|
55
|
+
# Only PR summary comment
|
|
56
|
+
iam-validator validate --path ./policies/ --github-comment
|
|
57
|
+
|
|
58
|
+
# Only GitHub Actions job summary
|
|
59
|
+
iam-validator validate --path ./policies/ --github-summary
|
|
46
60
|
"""
|
|
47
61
|
|
|
48
62
|
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
@@ -82,16 +96,37 @@ Examples:
|
|
|
82
96
|
help="Fail validation if warnings are found (default: only fail on errors)",
|
|
83
97
|
)
|
|
84
98
|
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--policy-type",
|
|
101
|
+
"-t",
|
|
102
|
+
choices=[
|
|
103
|
+
"IDENTITY_POLICY",
|
|
104
|
+
"RESOURCE_POLICY",
|
|
105
|
+
"SERVICE_CONTROL_POLICY",
|
|
106
|
+
"RESOURCE_CONTROL_POLICY",
|
|
107
|
+
],
|
|
108
|
+
default="IDENTITY_POLICY",
|
|
109
|
+
help="Type of IAM policy being validated (default: IDENTITY_POLICY). "
|
|
110
|
+
"Enables policy-type-specific validation (e.g., requiring Principal for resource policies, "
|
|
111
|
+
"strict RCP requirements for resource control policies)",
|
|
112
|
+
)
|
|
113
|
+
|
|
85
114
|
parser.add_argument(
|
|
86
115
|
"--github-comment",
|
|
87
116
|
action="store_true",
|
|
88
|
-
help="Post
|
|
117
|
+
help="Post summary comment to PR conversation",
|
|
89
118
|
)
|
|
90
119
|
|
|
91
120
|
parser.add_argument(
|
|
92
121
|
"--github-review",
|
|
93
122
|
action="store_true",
|
|
94
|
-
help="Create line-specific review comments on PR
|
|
123
|
+
help="Create line-specific review comments on PR files",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--github-summary",
|
|
128
|
+
action="store_true",
|
|
129
|
+
help="Write summary to GitHub Actions job summary (visible in Actions tab)",
|
|
95
130
|
)
|
|
96
131
|
|
|
97
132
|
parser.add_argument(
|
|
@@ -131,6 +166,18 @@ Examples:
|
|
|
131
166
|
help="Number of policies to process per batch (default: 10, only with --stream)",
|
|
132
167
|
)
|
|
133
168
|
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--no-summary",
|
|
171
|
+
action="store_true",
|
|
172
|
+
help="Hide Executive Summary section in enhanced format output",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--no-severity-breakdown",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="Hide Issue Severity Breakdown section in enhanced format output",
|
|
179
|
+
)
|
|
180
|
+
|
|
134
181
|
async def execute(self, args: argparse.Namespace) -> int:
|
|
135
182
|
"""Execute the validate command."""
|
|
136
183
|
# Check if streaming mode is enabled
|
|
@@ -165,11 +212,13 @@ Examples:
|
|
|
165
212
|
use_registry = not getattr(args, "no_registry", False)
|
|
166
213
|
config_path = getattr(args, "config", None)
|
|
167
214
|
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
215
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
168
216
|
results = await validate_policies(
|
|
169
217
|
policies,
|
|
170
218
|
config_path=config_path,
|
|
171
219
|
use_registry=use_registry,
|
|
172
220
|
custom_checks_dir=custom_checks_dir,
|
|
221
|
+
policy_type=policy_type,
|
|
173
222
|
)
|
|
174
223
|
|
|
175
224
|
# Generate report
|
|
@@ -192,7 +241,14 @@ Examples:
|
|
|
192
241
|
print(generator.generate_github_comment(report))
|
|
193
242
|
else:
|
|
194
243
|
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
195
|
-
|
|
244
|
+
# Pass options for enhanced format
|
|
245
|
+
format_options = {}
|
|
246
|
+
if args.format == "enhanced":
|
|
247
|
+
format_options["show_summary"] = not getattr(args, "no_summary", False)
|
|
248
|
+
format_options["show_severity_breakdown"] = not getattr(
|
|
249
|
+
args, "no_severity_breakdown", False
|
|
250
|
+
)
|
|
251
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
196
252
|
if args.output:
|
|
197
253
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
198
254
|
f.write(output_content)
|
|
@@ -201,19 +257,28 @@ Examples:
|
|
|
201
257
|
print(output_content)
|
|
202
258
|
|
|
203
259
|
# Post to GitHub if configured
|
|
204
|
-
if args.github_comment:
|
|
260
|
+
if args.github_comment or getattr(args, "github_review", False):
|
|
261
|
+
from iam_validator.core.config_loader import ConfigLoader
|
|
205
262
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
206
263
|
|
|
264
|
+
# Load config to get fail_on_severity setting
|
|
265
|
+
config = ConfigLoader.load_config(config_path)
|
|
266
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
267
|
+
|
|
207
268
|
async with GitHubIntegration() as github:
|
|
208
|
-
commenter = PRCommenter(github)
|
|
269
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
209
270
|
success = await commenter.post_findings_to_pr(
|
|
210
271
|
report,
|
|
211
|
-
create_review=getattr(args, "github_review",
|
|
212
|
-
add_summary_comment=
|
|
272
|
+
create_review=getattr(args, "github_review", False),
|
|
273
|
+
add_summary_comment=args.github_comment,
|
|
213
274
|
)
|
|
214
275
|
if not success:
|
|
215
276
|
logging.error("Failed to post to GitHub PR")
|
|
216
277
|
|
|
278
|
+
# Write to GitHub Actions job summary if configured
|
|
279
|
+
if getattr(args, "github_summary", False):
|
|
280
|
+
self._write_github_actions_summary(report)
|
|
281
|
+
|
|
217
282
|
# Return exit code based on validation results
|
|
218
283
|
if args.fail_on_warnings:
|
|
219
284
|
return 0 if report.total_issues == 0 else 1
|
|
@@ -234,12 +299,13 @@ Examples:
|
|
|
234
299
|
use_registry = not getattr(args, "no_registry", False)
|
|
235
300
|
config_path = getattr(args, "config", None)
|
|
236
301
|
custom_checks_dir = getattr(args, "custom_checks_dir", None)
|
|
302
|
+
policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
|
|
237
303
|
|
|
238
304
|
all_results = []
|
|
239
305
|
total_processed = 0
|
|
240
306
|
|
|
241
307
|
# Clean up old review comments at the start (before posting any new ones)
|
|
242
|
-
if
|
|
308
|
+
if getattr(args, "github_review", False):
|
|
243
309
|
await self._cleanup_old_comments()
|
|
244
310
|
|
|
245
311
|
logging.info(f"Starting streaming validation from {len(args.paths)} path(s)")
|
|
@@ -257,6 +323,7 @@ Examples:
|
|
|
257
323
|
config_path=config_path,
|
|
258
324
|
use_registry=use_registry,
|
|
259
325
|
custom_checks_dir=custom_checks_dir,
|
|
326
|
+
policy_type=policy_type,
|
|
260
327
|
)
|
|
261
328
|
|
|
262
329
|
if results:
|
|
@@ -272,7 +339,7 @@ Examples:
|
|
|
272
339
|
# Note: validation_success tracks overall status
|
|
273
340
|
|
|
274
341
|
# Post to GitHub immediately for this file (progressive PR comments)
|
|
275
|
-
if
|
|
342
|
+
if getattr(args, "github_review", False):
|
|
276
343
|
await self._post_file_review(result, args)
|
|
277
344
|
|
|
278
345
|
if total_processed == 0:
|
|
@@ -300,7 +367,14 @@ Examples:
|
|
|
300
367
|
print(generator.generate_github_comment(report))
|
|
301
368
|
else:
|
|
302
369
|
# Use formatter registry for other formats (enhanced, html, csv, sarif)
|
|
303
|
-
|
|
370
|
+
# Pass options for enhanced format
|
|
371
|
+
format_options = {}
|
|
372
|
+
if args.format == "enhanced":
|
|
373
|
+
format_options["show_summary"] = not getattr(args, "no_summary", False)
|
|
374
|
+
format_options["show_severity_breakdown"] = not getattr(
|
|
375
|
+
args, "no_severity_breakdown", False
|
|
376
|
+
)
|
|
377
|
+
output_content = generator.format_report(report, args.format, **format_options)
|
|
304
378
|
if args.output:
|
|
305
379
|
with open(args.output, "w", encoding="utf-8") as f:
|
|
306
380
|
f.write(output_content)
|
|
@@ -308,20 +382,29 @@ Examples:
|
|
|
308
382
|
else:
|
|
309
383
|
print(output_content)
|
|
310
384
|
|
|
311
|
-
# Post summary comment to GitHub
|
|
385
|
+
# Post summary comment to GitHub (if requested and not already posted per-file reviews)
|
|
312
386
|
if args.github_comment:
|
|
387
|
+
from iam_validator.core.config_loader import ConfigLoader
|
|
313
388
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
314
389
|
|
|
390
|
+
# Load config to get fail_on_severity setting
|
|
391
|
+
config = ConfigLoader.load_config(config_path)
|
|
392
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
393
|
+
|
|
315
394
|
async with GitHubIntegration() as github:
|
|
316
|
-
commenter = PRCommenter(github)
|
|
395
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
317
396
|
success = await commenter.post_findings_to_pr(
|
|
318
397
|
report,
|
|
319
|
-
create_review=False, # Already posted per-file reviews
|
|
398
|
+
create_review=False, # Already posted per-file reviews in streaming mode
|
|
320
399
|
add_summary_comment=True,
|
|
321
400
|
)
|
|
322
401
|
if not success:
|
|
323
402
|
logging.error("Failed to post summary to GitHub PR")
|
|
324
403
|
|
|
404
|
+
# Write to GitHub Actions job summary if configured
|
|
405
|
+
if getattr(args, "github_summary", False):
|
|
406
|
+
self._write_github_actions_summary(report)
|
|
407
|
+
|
|
325
408
|
# Return exit code based on validation results
|
|
326
409
|
if args.fail_on_warnings:
|
|
327
410
|
return 0 if report.total_issues == 0 else 1
|
|
@@ -353,15 +436,23 @@ Examples:
|
|
|
353
436
|
This provides progressive feedback in PRs as files are processed.
|
|
354
437
|
"""
|
|
355
438
|
try:
|
|
439
|
+
from iam_validator.core.config_loader import ConfigLoader
|
|
356
440
|
from iam_validator.core.pr_commenter import PRCommenter
|
|
357
441
|
|
|
358
442
|
async with GitHubIntegration() as github:
|
|
359
443
|
if not github.is_configured():
|
|
360
444
|
return
|
|
361
445
|
|
|
446
|
+
# Load config to get fail_on_severity setting
|
|
447
|
+
config_path = getattr(args, "config", None)
|
|
448
|
+
config = ConfigLoader.load_config(config_path)
|
|
449
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
450
|
+
|
|
362
451
|
# In streaming mode, don't cleanup comments (we want to keep earlier files)
|
|
363
452
|
# Cleanup will happen once at the end
|
|
364
|
-
commenter = PRCommenter(
|
|
453
|
+
commenter = PRCommenter(
|
|
454
|
+
github, cleanup_old_comments=False, fail_on_severities=fail_on_severities
|
|
455
|
+
)
|
|
365
456
|
|
|
366
457
|
# Create a mini-report for just this file
|
|
367
458
|
generator = ReportGenerator()
|
|
@@ -375,3 +466,100 @@ Examples:
|
|
|
375
466
|
)
|
|
376
467
|
except Exception as e:
|
|
377
468
|
logging.warning(f"Failed to post review for {result.policy_file}: {e}")
|
|
469
|
+
|
|
470
|
+
def _write_github_actions_summary(self, report: ValidationReport) -> None:
|
|
471
|
+
"""Write a high-level summary to GitHub Actions job summary.
|
|
472
|
+
|
|
473
|
+
This appears in the Actions tab and provides a quick overview without all details.
|
|
474
|
+
Uses GITHUB_STEP_SUMMARY environment variable.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
report: Validation report to summarize
|
|
478
|
+
"""
|
|
479
|
+
summary_file = os.getenv("GITHUB_STEP_SUMMARY")
|
|
480
|
+
if not summary_file:
|
|
481
|
+
logging.warning(
|
|
482
|
+
"--github-summary specified but GITHUB_STEP_SUMMARY env var not found. "
|
|
483
|
+
"This feature only works in GitHub Actions."
|
|
484
|
+
)
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
# Generate high-level summary (no detailed issue list)
|
|
489
|
+
summary_parts = []
|
|
490
|
+
|
|
491
|
+
# Header with status
|
|
492
|
+
if report.total_issues == 0:
|
|
493
|
+
summary_parts.append("# ✅ IAM Policy Validation - Passed")
|
|
494
|
+
elif report.invalid_policies > 0:
|
|
495
|
+
summary_parts.append("# ❌ IAM Policy Validation - Failed")
|
|
496
|
+
else:
|
|
497
|
+
summary_parts.append("# ⚠️ IAM Policy Validation - Security Issues Found")
|
|
498
|
+
|
|
499
|
+
summary_parts.append("")
|
|
500
|
+
|
|
501
|
+
# Summary table
|
|
502
|
+
summary_parts.append("## Summary")
|
|
503
|
+
summary_parts.append("")
|
|
504
|
+
summary_parts.append("| Metric | Count |")
|
|
505
|
+
summary_parts.append("|--------|-------|")
|
|
506
|
+
summary_parts.append(f"| Total Policies | {report.total_policies} |")
|
|
507
|
+
summary_parts.append(f"| Valid Policies | {report.valid_policies} |")
|
|
508
|
+
summary_parts.append(f"| Invalid Policies | {report.invalid_policies} |")
|
|
509
|
+
summary_parts.append(
|
|
510
|
+
f"| Policies with Security Issues | {report.policies_with_security_issues} |"
|
|
511
|
+
)
|
|
512
|
+
summary_parts.append(f"| **Total Issues** | **{report.total_issues}** |")
|
|
513
|
+
|
|
514
|
+
# Issue breakdown by severity if there are issues
|
|
515
|
+
if report.total_issues > 0:
|
|
516
|
+
summary_parts.append("")
|
|
517
|
+
summary_parts.append("## Issues by Severity")
|
|
518
|
+
summary_parts.append("")
|
|
519
|
+
|
|
520
|
+
# Count issues by severity
|
|
521
|
+
severity_counts: dict[str, int] = {}
|
|
522
|
+
for result in report.results:
|
|
523
|
+
for issue in result.issues:
|
|
524
|
+
severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1
|
|
525
|
+
|
|
526
|
+
# Sort by severity rank (highest first)
|
|
527
|
+
from iam_validator.core.models import ValidationIssue
|
|
528
|
+
|
|
529
|
+
sorted_severities = sorted(
|
|
530
|
+
severity_counts.items(),
|
|
531
|
+
key=lambda x: ValidationIssue.SEVERITY_RANK.get(x[0], 0),
|
|
532
|
+
reverse=True,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
summary_parts.append("| Severity | Count |")
|
|
536
|
+
summary_parts.append("|----------|-------|")
|
|
537
|
+
for severity, count in sorted_severities:
|
|
538
|
+
emoji = {
|
|
539
|
+
"error": "❌",
|
|
540
|
+
"critical": "🔴",
|
|
541
|
+
"high": "🟠",
|
|
542
|
+
"warning": "⚠️",
|
|
543
|
+
"medium": "🟡",
|
|
544
|
+
"low": "🔵",
|
|
545
|
+
"info": "ℹ️",
|
|
546
|
+
}.get(severity, "•")
|
|
547
|
+
summary_parts.append(f"| {emoji} {severity.upper()} | {count} |")
|
|
548
|
+
|
|
549
|
+
# Add footer with links
|
|
550
|
+
summary_parts.append("")
|
|
551
|
+
summary_parts.append("---")
|
|
552
|
+
summary_parts.append("")
|
|
553
|
+
summary_parts.append(
|
|
554
|
+
"📝 For detailed findings, check the PR comments or review the workflow logs."
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Write to summary file (append mode)
|
|
558
|
+
with open(summary_file, "a", encoding="utf-8") as f:
|
|
559
|
+
f.write("\n".join(summary_parts))
|
|
560
|
+
f.write("\n")
|
|
561
|
+
|
|
562
|
+
logging.info("Wrote summary to GitHub Actions job summary")
|
|
563
|
+
|
|
564
|
+
except Exception as e:
|
|
565
|
+
logging.warning(f"Failed to write GitHub Actions summary: {e}")
|
|
@@ -33,6 +33,7 @@ from typing import Any
|
|
|
33
33
|
|
|
34
34
|
import httpx
|
|
35
35
|
|
|
36
|
+
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
36
37
|
from iam_validator.core.models import ServiceDetail, ServiceInfo
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
@@ -121,7 +122,7 @@ class CompiledPatterns:
|
|
|
121
122
|
class AWSServiceFetcher:
|
|
122
123
|
"""Fetches AWS service information from the AWS service reference API with enhanced performance features."""
|
|
123
124
|
|
|
124
|
-
BASE_URL =
|
|
125
|
+
BASE_URL = AWS_SERVICE_REFERENCE_BASE_URL
|
|
125
126
|
|
|
126
127
|
# Common AWS services to pre-fetch
|
|
127
128
|
# All other services will be fetched on-demand (lazy loading if found in policies)
|
|
@@ -797,8 +798,15 @@ class AWSServiceFetcher:
|
|
|
797
798
|
|
|
798
799
|
async def validate_condition_key(
|
|
799
800
|
self, action: str, condition_key: str
|
|
800
|
-
) -> tuple[bool, str | None]:
|
|
801
|
-
"""Validate condition key with optimized caching.
|
|
801
|
+
) -> tuple[bool, str | None, str | None]:
|
|
802
|
+
"""Validate condition key with optimized caching.
|
|
803
|
+
|
|
804
|
+
Returns:
|
|
805
|
+
Tuple of (is_valid, error_message, warning_message)
|
|
806
|
+
- is_valid: True if key is valid (even with warning)
|
|
807
|
+
- error_message: Error message if invalid (is_valid=False)
|
|
808
|
+
- warning_message: Warning message if valid but not recommended
|
|
809
|
+
"""
|
|
802
810
|
try:
|
|
803
811
|
from iam_validator.core.aws_global_conditions import get_global_conditions
|
|
804
812
|
|
|
@@ -814,6 +822,7 @@ class AWSServiceFetcher:
|
|
|
814
822
|
return (
|
|
815
823
|
False,
|
|
816
824
|
f"Invalid AWS global condition key: '{condition_key}'.",
|
|
825
|
+
None,
|
|
817
826
|
)
|
|
818
827
|
|
|
819
828
|
# Fetch service detail (cached)
|
|
@@ -821,7 +830,7 @@ class AWSServiceFetcher:
|
|
|
821
830
|
|
|
822
831
|
# Check service-specific condition keys
|
|
823
832
|
if condition_key in service_detail.condition_keys:
|
|
824
|
-
return True, None
|
|
833
|
+
return True, None, None
|
|
825
834
|
|
|
826
835
|
# Check action-specific condition keys
|
|
827
836
|
if action_name in service_detail.actions:
|
|
@@ -830,29 +839,33 @@ class AWSServiceFetcher:
|
|
|
830
839
|
action_detail.action_condition_keys
|
|
831
840
|
and condition_key in action_detail.action_condition_keys
|
|
832
841
|
):
|
|
833
|
-
return True, None
|
|
842
|
+
return True, None, None
|
|
834
843
|
|
|
835
844
|
# If it's a global key but the action has specific condition keys defined,
|
|
836
|
-
#
|
|
845
|
+
# AWS allows it but the key may not be available in every request context
|
|
837
846
|
if is_global_key and action_detail.action_condition_keys is not None:
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
f"
|
|
841
|
-
f"
|
|
847
|
+
warning_msg = (
|
|
848
|
+
f"Global condition key '{condition_key}' is used with action '{action}'. "
|
|
849
|
+
f"While global condition keys can be used across all AWS services, "
|
|
850
|
+
f"the key may not be available in every request context. "
|
|
851
|
+
f"Verify that '{condition_key}' is available for this specific action's request context. "
|
|
852
|
+
f"Consider using '*IfExists' operators (e.g., StringEqualsIfExists) if the key might be missing."
|
|
842
853
|
)
|
|
854
|
+
return True, None, warning_msg
|
|
843
855
|
|
|
844
856
|
# If it's a global key and action doesn't define specific keys, allow it
|
|
845
857
|
if is_global_key:
|
|
846
|
-
return True, None
|
|
858
|
+
return True, None, None
|
|
847
859
|
|
|
848
860
|
return (
|
|
849
861
|
False,
|
|
850
862
|
f"Condition key '{condition_key}' is not valid for action '{action}'",
|
|
863
|
+
None,
|
|
851
864
|
)
|
|
852
865
|
|
|
853
866
|
except Exception as e:
|
|
854
867
|
logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
|
|
855
|
-
return False, f"Failed to validate condition key: {str(e)}"
|
|
868
|
+
return False, f"Failed to validate condition key: {str(e)}", None
|
|
856
869
|
|
|
857
870
|
async def clear_caches(self) -> None:
|
|
858
871
|
"""Clear all caches (memory and disk)."""
|
|
@@ -98,6 +98,7 @@ class PolicyCheck(ABC):
|
|
|
98
98
|
policy_file: str,
|
|
99
99
|
fetcher: AWSServiceFetcher,
|
|
100
100
|
config: CheckConfig,
|
|
101
|
+
**kwargs,
|
|
101
102
|
) -> list[ValidationIssue]:
|
|
102
103
|
"""
|
|
103
104
|
Execute the check on the entire policy (optional method).
|
|
@@ -113,6 +114,7 @@ class PolicyCheck(ABC):
|
|
|
113
114
|
policy_file: Path to the policy file (for context/reporting)
|
|
114
115
|
fetcher: AWS service fetcher for validation against AWS APIs
|
|
115
116
|
config: Configuration for this check instance
|
|
117
|
+
**kwargs: Additional context (policy_type, etc.)
|
|
116
118
|
|
|
117
119
|
Returns:
|
|
118
120
|
List of ValidationIssue objects found by this check
|
|
@@ -353,6 +355,7 @@ class CheckRegistry:
|
|
|
353
355
|
policy: "IAMPolicy",
|
|
354
356
|
policy_file: str,
|
|
355
357
|
fetcher: AWSServiceFetcher,
|
|
358
|
+
policy_type: str = "IDENTITY_POLICY",
|
|
356
359
|
) -> list[ValidationIssue]:
|
|
357
360
|
"""
|
|
358
361
|
Execute all enabled policy-level checks.
|
|
@@ -364,6 +367,7 @@ class CheckRegistry:
|
|
|
364
367
|
policy: The complete IAM policy to validate
|
|
365
368
|
policy_file: Path to the policy file (for context/reporting)
|
|
366
369
|
fetcher: AWS service fetcher for API calls
|
|
370
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
367
371
|
|
|
368
372
|
Returns:
|
|
369
373
|
List of all ValidationIssue objects from all policy-level checks
|
|
@@ -383,7 +387,9 @@ class CheckRegistry:
|
|
|
383
387
|
config = self.get_config(check.check_id)
|
|
384
388
|
if config:
|
|
385
389
|
try:
|
|
386
|
-
issues = await check.execute_policy(
|
|
390
|
+
issues = await check.execute_policy(
|
|
391
|
+
policy, policy_file, fetcher, config, policy_type=policy_type
|
|
392
|
+
)
|
|
387
393
|
all_issues.extend(issues)
|
|
388
394
|
except Exception as e:
|
|
389
395
|
print(f"Warning: Check '{check.check_id}' failed: {e}")
|
|
@@ -394,7 +400,9 @@ class CheckRegistry:
|
|
|
394
400
|
for check in policy_level_checks:
|
|
395
401
|
config = self.get_config(check.check_id)
|
|
396
402
|
if config:
|
|
397
|
-
task = check.execute_policy(
|
|
403
|
+
task = check.execute_policy(
|
|
404
|
+
policy, policy_file, fetcher, config, policy_type=policy_type
|
|
405
|
+
)
|
|
398
406
|
tasks.append(task)
|
|
399
407
|
|
|
400
408
|
# Wait for all checks to complete
|
|
@@ -432,25 +440,21 @@ def create_default_registry(
|
|
|
432
440
|
|
|
433
441
|
if include_builtin_checks:
|
|
434
442
|
# Import and register built-in checks
|
|
435
|
-
from iam_validator
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
registry.register(
|
|
447
|
-
registry.register(
|
|
448
|
-
registry.register(
|
|
449
|
-
registry.register(
|
|
450
|
-
registry.register(ActionConditionEnforcementCheck())
|
|
451
|
-
registry.register(ActionResourceConstraintCheck())
|
|
452
|
-
registry.register(SidUniquenessCheck())
|
|
453
|
-
registry.register(PolicySizeCheck())
|
|
443
|
+
from iam_validator import checks
|
|
444
|
+
|
|
445
|
+
registry.register(checks.ActionValidationCheck())
|
|
446
|
+
registry.register(checks.ConditionKeyValidationCheck())
|
|
447
|
+
registry.register(checks.ResourceValidationCheck())
|
|
448
|
+
registry.register(checks.WildcardActionCheck())
|
|
449
|
+
registry.register(checks.WildcardResourceCheck())
|
|
450
|
+
registry.register(checks.FullWildcardCheck())
|
|
451
|
+
registry.register(checks.ServiceWildcardCheck())
|
|
452
|
+
registry.register(checks.SensitiveActionCheck())
|
|
453
|
+
registry.register(checks.ActionConditionEnforcementCheck())
|
|
454
|
+
registry.register(checks.ActionResourceConstraintCheck())
|
|
455
|
+
registry.register(checks.SidUniquenessCheck())
|
|
456
|
+
registry.register(checks.PolicySizeCheck())
|
|
457
|
+
registry.register(checks.PrincipalValidationCheck())
|
|
454
458
|
|
|
455
459
|
# Note: SID uniqueness check is registered above but its actual execution
|
|
456
460
|
# happens at the policy level in _validate_policy_with_registry() since it
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core configuration modules for IAM Policy Validator.
|
|
3
|
+
|
|
4
|
+
This package contains default configuration data used by validators, organized into
|
|
5
|
+
logical modules for better maintainability and performance.
|
|
6
|
+
|
|
7
|
+
All configuration is defined as Python code (not YAML/JSON) for:
|
|
8
|
+
- Faster loading (no parsing overhead)
|
|
9
|
+
- Better PyPI packaging (no data files to manage)
|
|
10
|
+
- Type hints and IDE support
|
|
11
|
+
- Compiled to .pyc for even faster imports
|
|
12
|
+
|
|
13
|
+
Performance benefits:
|
|
14
|
+
- 5-10x faster than YAML parsing
|
|
15
|
+
- Zero runtime parsing overhead
|
|
16
|
+
- Lazy loading support
|
|
17
|
+
- O(1) frozenset lookups
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from iam_validator.core.config.aws_api import (
|
|
21
|
+
AWS_SERVICE_REFERENCE_BASE_URL,
|
|
22
|
+
get_service_reference_url,
|
|
23
|
+
)
|
|
24
|
+
from iam_validator.core.config.condition_requirements import (
|
|
25
|
+
ALL_CONDITION_REQUIREMENTS,
|
|
26
|
+
DEFAULT_CONDITION_REQUIREMENTS,
|
|
27
|
+
get_all_requirement_names,
|
|
28
|
+
get_default_requirements,
|
|
29
|
+
get_requirement,
|
|
30
|
+
get_requirements_by_names,
|
|
31
|
+
get_requirements_by_severity,
|
|
32
|
+
)
|
|
33
|
+
from iam_validator.core.config.defaults import DEFAULT_CONFIG
|
|
34
|
+
from iam_validator.core.config.principal_requirements import (
|
|
35
|
+
ALL_PRINCIPAL_REQUIREMENTS,
|
|
36
|
+
DEFAULT_ENABLED_REQUIREMENTS,
|
|
37
|
+
get_all_principal_requirement_names,
|
|
38
|
+
get_default_principal_requirements,
|
|
39
|
+
get_principal_requirement,
|
|
40
|
+
get_principal_requirements_by_names,
|
|
41
|
+
get_principal_requirements_by_severity,
|
|
42
|
+
)
|
|
43
|
+
from iam_validator.core.config.sensitive_actions import (
|
|
44
|
+
DEFAULT_SENSITIVE_ACTIONS,
|
|
45
|
+
get_sensitive_actions,
|
|
46
|
+
)
|
|
47
|
+
from iam_validator.core.config.service_principals import DEFAULT_SERVICE_PRINCIPALS
|
|
48
|
+
from iam_validator.core.config.wildcards import (
|
|
49
|
+
DEFAULT_ALLOWED_WILDCARDS,
|
|
50
|
+
DEFAULT_SERVICE_WILDCARDS,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# Default configuration
|
|
55
|
+
"DEFAULT_CONFIG",
|
|
56
|
+
# AWS API endpoints
|
|
57
|
+
"AWS_SERVICE_REFERENCE_BASE_URL",
|
|
58
|
+
"get_service_reference_url",
|
|
59
|
+
# Sensitive actions
|
|
60
|
+
"DEFAULT_SENSITIVE_ACTIONS",
|
|
61
|
+
"get_sensitive_actions",
|
|
62
|
+
# Condition requirements (for actions)
|
|
63
|
+
"DEFAULT_CONDITION_REQUIREMENTS",
|
|
64
|
+
"ALL_CONDITION_REQUIREMENTS",
|
|
65
|
+
"get_default_requirements",
|
|
66
|
+
"get_requirement",
|
|
67
|
+
"get_all_requirement_names",
|
|
68
|
+
"get_requirements_by_names",
|
|
69
|
+
"get_requirements_by_severity",
|
|
70
|
+
# Principal requirements (for principals)
|
|
71
|
+
"ALL_PRINCIPAL_REQUIREMENTS",
|
|
72
|
+
"DEFAULT_ENABLED_REQUIREMENTS",
|
|
73
|
+
"get_default_principal_requirements",
|
|
74
|
+
"get_principal_requirement",
|
|
75
|
+
"get_all_principal_requirement_names",
|
|
76
|
+
"get_principal_requirements_by_names",
|
|
77
|
+
"get_principal_requirements_by_severity",
|
|
78
|
+
# Wildcards
|
|
79
|
+
"DEFAULT_ALLOWED_WILDCARDS",
|
|
80
|
+
"DEFAULT_SERVICE_WILDCARDS",
|
|
81
|
+
# Service principals
|
|
82
|
+
"DEFAULT_SERVICE_PRINCIPALS",
|
|
83
|
+
]
|