iam-policy-validator 1.4.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.

Files changed (56) hide show
  1. iam_policy_validator-1.4.0.dist-info/METADATA +1022 -0
  2. iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
  3. iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.4.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 +7 -0
  9. iam_validator/checks/__init__.py +27 -0
  10. iam_validator/checks/action_condition_enforcement.py +727 -0
  11. iam_validator/checks/action_resource_constraint.py +151 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +70 -0
  14. iam_validator/checks/policy_size.py +151 -0
  15. iam_validator/checks/policy_type_validation.py +299 -0
  16. iam_validator/checks/principal_validation.py +282 -0
  17. iam_validator/checks/resource_validation.py +108 -0
  18. iam_validator/checks/security_best_practices.py +536 -0
  19. iam_validator/checks/sid_uniqueness.py +170 -0
  20. iam_validator/checks/utils/__init__.py +1 -0
  21. iam_validator/checks/utils/policy_level_checks.py +143 -0
  22. iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
  23. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  24. iam_validator/commands/__init__.py +25 -0
  25. iam_validator/commands/analyze.py +434 -0
  26. iam_validator/commands/base.py +48 -0
  27. iam_validator/commands/cache.py +392 -0
  28. iam_validator/commands/download_services.py +260 -0
  29. iam_validator/commands/post_to_pr.py +86 -0
  30. iam_validator/commands/validate.py +539 -0
  31. iam_validator/core/__init__.py +14 -0
  32. iam_validator/core/access_analyzer.py +666 -0
  33. iam_validator/core/access_analyzer_report.py +643 -0
  34. iam_validator/core/aws_fetcher.py +880 -0
  35. iam_validator/core/aws_global_conditions.py +137 -0
  36. iam_validator/core/check_registry.py +469 -0
  37. iam_validator/core/cli.py +134 -0
  38. iam_validator/core/config_loader.py +452 -0
  39. iam_validator/core/defaults.py +393 -0
  40. iam_validator/core/formatters/__init__.py +27 -0
  41. iam_validator/core/formatters/base.py +147 -0
  42. iam_validator/core/formatters/console.py +59 -0
  43. iam_validator/core/formatters/csv.py +170 -0
  44. iam_validator/core/formatters/enhanced.py +434 -0
  45. iam_validator/core/formatters/html.py +672 -0
  46. iam_validator/core/formatters/json.py +33 -0
  47. iam_validator/core/formatters/markdown.py +63 -0
  48. iam_validator/core/formatters/sarif.py +187 -0
  49. iam_validator/core/models.py +298 -0
  50. iam_validator/core/policy_checks.py +656 -0
  51. iam_validator/core/policy_loader.py +396 -0
  52. iam_validator/core/pr_commenter.py +338 -0
  53. iam_validator/core/report.py +859 -0
  54. iam_validator/integrations/__init__.py +28 -0
  55. iam_validator/integrations/github_integration.py +795 -0
  56. iam_validator/integrations/ms_teams.py +442 -0
@@ -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
@@ -0,0 +1,539 @@
1
+ """Validate command for IAM Policy Validator."""
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ from typing import cast
7
+
8
+ from iam_validator.commands.base import Command
9
+ from iam_validator.core.models import PolicyType, ValidationReport
10
+ from iam_validator.core.policy_checks import validate_policies
11
+ from iam_validator.core.policy_loader import PolicyLoader
12
+ from iam_validator.core.report import ReportGenerator
13
+ from iam_validator.integrations.github_integration import GitHubIntegration
14
+
15
+
16
+ class ValidateCommand(Command):
17
+ """Command to validate IAM policies."""
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ return "validate"
22
+
23
+ @property
24
+ def help(self) -> str:
25
+ return "Validate IAM policies"
26
+
27
+ @property
28
+ def epilog(self) -> str:
29
+ return """
30
+ Examples:
31
+ # Validate a single policy file
32
+ iam-validator validate --path policy.json
33
+
34
+ # Validate all policies in a directory
35
+ iam-validator validate --path ./policies/
36
+
37
+ # Validate multiple paths (files and directories)
38
+ iam-validator validate --path policy1.json --path ./policies/ --path ./more-policies/
39
+
40
+ # Use custom checks from a directory
41
+ iam-validator validate --path ./policies/ --custom-checks-dir ./my-checks
42
+
43
+ # Generate JSON output
44
+ iam-validator validate --path ./policies/ --format json --output report.json
45
+
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
60
+ """
61
+
62
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
63
+ """Add validate command arguments."""
64
+ parser.add_argument(
65
+ "--path",
66
+ "-p",
67
+ required=True,
68
+ action="append",
69
+ dest="paths",
70
+ help="Path to IAM policy file or directory (can be specified multiple times)",
71
+ )
72
+
73
+ parser.add_argument(
74
+ "--format",
75
+ "-f",
76
+ choices=["console", "enhanced", "json", "markdown", "html", "csv", "sarif"],
77
+ default="console",
78
+ help="Output format (default: console). Use 'enhanced' for modern visual output with Rich library",
79
+ )
80
+
81
+ parser.add_argument(
82
+ "--output",
83
+ "-o",
84
+ help="Output file path (for json/markdown/html/csv/sarif formats)",
85
+ )
86
+
87
+ parser.add_argument(
88
+ "--no-recursive",
89
+ action="store_true",
90
+ help="Don't recursively search directories",
91
+ )
92
+
93
+ parser.add_argument(
94
+ "--fail-on-warnings",
95
+ action="store_true",
96
+ help="Fail validation if warnings are found (default: only fail on errors)",
97
+ )
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
+
114
+ parser.add_argument(
115
+ "--github-comment",
116
+ action="store_true",
117
+ help="Post summary comment to PR conversation",
118
+ )
119
+
120
+ parser.add_argument(
121
+ "--github-review",
122
+ action="store_true",
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)",
130
+ )
131
+
132
+ parser.add_argument(
133
+ "--verbose",
134
+ "-v",
135
+ action="store_true",
136
+ help="Enable verbose logging",
137
+ )
138
+
139
+ parser.add_argument(
140
+ "--config",
141
+ "-c",
142
+ help="Path to configuration file (default: auto-discover iam-validator.yaml)",
143
+ )
144
+
145
+ parser.add_argument(
146
+ "--custom-checks-dir",
147
+ help="Path to directory containing custom checks for auto-discovery",
148
+ )
149
+
150
+ parser.add_argument(
151
+ "--no-registry",
152
+ action="store_true",
153
+ help="Use legacy validation (disable check registry system)",
154
+ )
155
+
156
+ parser.add_argument(
157
+ "--stream",
158
+ action="store_true",
159
+ help="Process files one-by-one (memory efficient, progressive feedback)",
160
+ )
161
+
162
+ parser.add_argument(
163
+ "--batch-size",
164
+ type=int,
165
+ default=10,
166
+ help="Number of policies to process per batch (default: 10, only with --stream)",
167
+ )
168
+
169
+ async def execute(self, args: argparse.Namespace) -> int:
170
+ """Execute the validate command."""
171
+ # Check if streaming mode is enabled
172
+ use_stream = getattr(args, "stream", False)
173
+
174
+ # Auto-enable streaming for CI environments or large policy sets
175
+ # to provide progressive feedback
176
+ if not use_stream and os.getenv("CI"):
177
+ logging.info(
178
+ "CI environment detected, enabling streaming mode for progressive feedback"
179
+ )
180
+ use_stream = True
181
+
182
+ if use_stream:
183
+ return await self._execute_streaming(args)
184
+ else:
185
+ return await self._execute_batch(args)
186
+
187
+ async def _execute_batch(self, args: argparse.Namespace) -> int:
188
+ """Execute validation by loading all policies at once (original behavior)."""
189
+ # Load policies from all specified paths
190
+ loader = PolicyLoader()
191
+ policies = loader.load_from_paths(args.paths, recursive=not args.no_recursive)
192
+
193
+ if not policies:
194
+ logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
195
+ return 1
196
+
197
+ logging.info(f"Loaded {len(policies)} policies from {len(args.paths)} path(s)")
198
+
199
+ # Validate policies
200
+ use_registry = not getattr(args, "no_registry", False)
201
+ config_path = getattr(args, "config", None)
202
+ custom_checks_dir = getattr(args, "custom_checks_dir", None)
203
+ policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
204
+ results = await validate_policies(
205
+ policies,
206
+ config_path=config_path,
207
+ use_registry=use_registry,
208
+ custom_checks_dir=custom_checks_dir,
209
+ policy_type=policy_type,
210
+ )
211
+
212
+ # Generate report
213
+ generator = ReportGenerator()
214
+ report = generator.generate_report(results)
215
+
216
+ # Output results
217
+ if args.format is None:
218
+ # Default: use classic console output (direct Rich printing)
219
+ generator.print_console_report(report)
220
+ elif args.format == "json":
221
+ if args.output:
222
+ generator.save_json_report(report, args.output)
223
+ else:
224
+ print(generator.generate_json_report(report))
225
+ elif args.format == "markdown":
226
+ if args.output:
227
+ generator.save_markdown_report(report, args.output)
228
+ else:
229
+ print(generator.generate_github_comment(report))
230
+ else:
231
+ # Use formatter registry for other formats (enhanced, html, csv, sarif)
232
+ output_content = generator.format_report(report, args.format)
233
+ if args.output:
234
+ with open(args.output, "w", encoding="utf-8") as f:
235
+ f.write(output_content)
236
+ logging.info(f"Saved {args.format.upper()} report to {args.output}")
237
+ else:
238
+ print(output_content)
239
+
240
+ # Post to GitHub if configured
241
+ if args.github_comment or getattr(args, "github_review", False):
242
+ from iam_validator.core.config_loader import ConfigLoader
243
+ from iam_validator.core.pr_commenter import PRCommenter
244
+
245
+ # Load config to get fail_on_severity setting
246
+ config = ConfigLoader.load_config(config_path)
247
+ fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
248
+
249
+ async with GitHubIntegration() as github:
250
+ commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
251
+ success = await commenter.post_findings_to_pr(
252
+ report,
253
+ create_review=getattr(args, "github_review", False),
254
+ add_summary_comment=args.github_comment,
255
+ )
256
+ if not success:
257
+ logging.error("Failed to post to GitHub PR")
258
+
259
+ # Write to GitHub Actions job summary if configured
260
+ if getattr(args, "github_summary", False):
261
+ self._write_github_actions_summary(report)
262
+
263
+ # Return exit code based on validation results
264
+ if args.fail_on_warnings:
265
+ return 0 if report.total_issues == 0 else 1
266
+ else:
267
+ return 0 if report.invalid_policies == 0 else 1
268
+
269
+ async def _execute_streaming(self, args: argparse.Namespace) -> int:
270
+ """Execute validation by streaming policies one-by-one.
271
+
272
+ This provides:
273
+ - Lower memory usage
274
+ - Progressive feedback (see results as they come)
275
+ - Partial results if errors occur
276
+ - Better for CI/CD pipelines
277
+ """
278
+ loader = PolicyLoader()
279
+ generator = ReportGenerator()
280
+ use_registry = not getattr(args, "no_registry", False)
281
+ config_path = getattr(args, "config", None)
282
+ custom_checks_dir = getattr(args, "custom_checks_dir", None)
283
+ policy_type = cast(PolicyType, getattr(args, "policy_type", "IDENTITY_POLICY"))
284
+
285
+ all_results = []
286
+ total_processed = 0
287
+
288
+ # Clean up old review comments at the start (before posting any new ones)
289
+ if getattr(args, "github_review", False):
290
+ await self._cleanup_old_comments()
291
+
292
+ logging.info(f"Starting streaming validation from {len(args.paths)} path(s)")
293
+
294
+ # Process policies one at a time
295
+ for file_path, policy in loader.stream_from_paths(
296
+ args.paths, recursive=not args.no_recursive
297
+ ):
298
+ total_processed += 1
299
+ logging.info(f"[{total_processed}] Processing: {file_path}")
300
+
301
+ # Validate single policy
302
+ results = await validate_policies(
303
+ [(file_path, policy)],
304
+ config_path=config_path,
305
+ use_registry=use_registry,
306
+ custom_checks_dir=custom_checks_dir,
307
+ policy_type=policy_type,
308
+ )
309
+
310
+ if results:
311
+ result = results[0]
312
+ all_results.append(result)
313
+
314
+ # Print immediate feedback for this file
315
+ if args.format == "console":
316
+ if result.is_valid:
317
+ logging.info(f" ✓ {file_path}: Valid")
318
+ else:
319
+ logging.warning(f" ✗ {file_path}: {len(result.issues)} issue(s) found")
320
+ # Note: validation_success tracks overall status
321
+
322
+ # Post to GitHub immediately for this file (progressive PR comments)
323
+ if getattr(args, "github_review", False):
324
+ await self._post_file_review(result, args)
325
+
326
+ if total_processed == 0:
327
+ logging.error(f"No valid IAM policies found in: {', '.join(args.paths)}")
328
+ return 1
329
+
330
+ logging.info(f"\nCompleted validation of {total_processed} policies")
331
+
332
+ # Generate final summary report
333
+ report = generator.generate_report(all_results)
334
+
335
+ # Output final results
336
+ if args.format == "console":
337
+ # Classic console output (direct Rich printing from report.py)
338
+ generator.print_console_report(report)
339
+ elif args.format == "json":
340
+ if args.output:
341
+ generator.save_json_report(report, args.output)
342
+ else:
343
+ print(generator.generate_json_report(report))
344
+ elif args.format == "markdown":
345
+ if args.output:
346
+ generator.save_markdown_report(report, args.output)
347
+ else:
348
+ print(generator.generate_github_comment(report))
349
+ else:
350
+ # Use formatter registry for other formats (enhanced, html, csv, sarif)
351
+ output_content = generator.format_report(report, args.format)
352
+ if args.output:
353
+ with open(args.output, "w", encoding="utf-8") as f:
354
+ f.write(output_content)
355
+ logging.info(f"Saved {args.format.upper()} report to {args.output}")
356
+ else:
357
+ print(output_content)
358
+
359
+ # Post summary comment to GitHub (if requested and not already posted per-file reviews)
360
+ if args.github_comment:
361
+ from iam_validator.core.config_loader import ConfigLoader
362
+ from iam_validator.core.pr_commenter import PRCommenter
363
+
364
+ # Load config to get fail_on_severity setting
365
+ config = ConfigLoader.load_config(config_path)
366
+ fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
367
+
368
+ async with GitHubIntegration() as github:
369
+ commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
370
+ success = await commenter.post_findings_to_pr(
371
+ report,
372
+ create_review=False, # Already posted per-file reviews in streaming mode
373
+ add_summary_comment=True,
374
+ )
375
+ if not success:
376
+ logging.error("Failed to post summary to GitHub PR")
377
+
378
+ # Write to GitHub Actions job summary if configured
379
+ if getattr(args, "github_summary", False):
380
+ self._write_github_actions_summary(report)
381
+
382
+ # Return exit code based on validation results
383
+ if args.fail_on_warnings:
384
+ return 0 if report.total_issues == 0 else 1
385
+ else:
386
+ return 0 if report.invalid_policies == 0 else 1
387
+
388
+ async def _cleanup_old_comments(self) -> None:
389
+ """Clean up old bot review comments from previous validation runs.
390
+
391
+ This ensures the PR stays clean without duplicate/stale comments.
392
+ """
393
+ try:
394
+ from iam_validator.core.pr_commenter import PRCommenter
395
+
396
+ async with GitHubIntegration() as github:
397
+ if not github.is_configured():
398
+ return
399
+
400
+ logging.info("Cleaning up old review comments from previous runs...")
401
+ deleted = await github.cleanup_bot_review_comments(PRCommenter.REVIEW_IDENTIFIER)
402
+ if deleted > 0:
403
+ logging.info(f"Removed {deleted} old comment(s)")
404
+ except Exception as e:
405
+ logging.warning(f"Failed to cleanup old comments: {e}")
406
+
407
+ async def _post_file_review(self, result, args: argparse.Namespace) -> None:
408
+ """Post review comments for a single file immediately.
409
+
410
+ This provides progressive feedback in PRs as files are processed.
411
+ """
412
+ try:
413
+ from iam_validator.core.config_loader import ConfigLoader
414
+ from iam_validator.core.pr_commenter import PRCommenter
415
+
416
+ async with GitHubIntegration() as github:
417
+ if not github.is_configured():
418
+ return
419
+
420
+ # Load config to get fail_on_severity setting
421
+ config_path = getattr(args, "config", None)
422
+ config = ConfigLoader.load_config(config_path)
423
+ fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
424
+
425
+ # In streaming mode, don't cleanup comments (we want to keep earlier files)
426
+ # Cleanup will happen once at the end
427
+ commenter = PRCommenter(
428
+ github, cleanup_old_comments=False, fail_on_severities=fail_on_severities
429
+ )
430
+
431
+ # Create a mini-report for just this file
432
+ generator = ReportGenerator()
433
+ mini_report = generator.generate_report([result])
434
+
435
+ # Post line-specific comments
436
+ await commenter.post_findings_to_pr(
437
+ mini_report,
438
+ create_review=True,
439
+ add_summary_comment=False, # Summary comes later
440
+ )
441
+ except Exception as e:
442
+ logging.warning(f"Failed to post review for {result.policy_file}: {e}")
443
+
444
+ def _write_github_actions_summary(self, report: ValidationReport) -> None:
445
+ """Write a high-level summary to GitHub Actions job summary.
446
+
447
+ This appears in the Actions tab and provides a quick overview without all details.
448
+ Uses GITHUB_STEP_SUMMARY environment variable.
449
+
450
+ Args:
451
+ report: Validation report to summarize
452
+ """
453
+ summary_file = os.getenv("GITHUB_STEP_SUMMARY")
454
+ if not summary_file:
455
+ logging.warning(
456
+ "--github-summary specified but GITHUB_STEP_SUMMARY env var not found. "
457
+ "This feature only works in GitHub Actions."
458
+ )
459
+ return
460
+
461
+ try:
462
+ # Generate high-level summary (no detailed issue list)
463
+ summary_parts = []
464
+
465
+ # Header with status
466
+ if report.total_issues == 0:
467
+ summary_parts.append("# ✅ IAM Policy Validation - Passed")
468
+ elif report.invalid_policies > 0:
469
+ summary_parts.append("# ❌ IAM Policy Validation - Failed")
470
+ else:
471
+ summary_parts.append("# ⚠️ IAM Policy Validation - Security Issues Found")
472
+
473
+ summary_parts.append("")
474
+
475
+ # Summary table
476
+ summary_parts.append("## Summary")
477
+ summary_parts.append("")
478
+ summary_parts.append("| Metric | Count |")
479
+ summary_parts.append("|--------|-------|")
480
+ summary_parts.append(f"| Total Policies | {report.total_policies} |")
481
+ summary_parts.append(f"| Valid Policies | {report.valid_policies} |")
482
+ summary_parts.append(f"| Invalid Policies | {report.invalid_policies} |")
483
+ summary_parts.append(
484
+ f"| Policies with Security Issues | {report.policies_with_security_issues} |"
485
+ )
486
+ summary_parts.append(f"| **Total Issues** | **{report.total_issues}** |")
487
+
488
+ # Issue breakdown by severity if there are issues
489
+ if report.total_issues > 0:
490
+ summary_parts.append("")
491
+ summary_parts.append("## Issues by Severity")
492
+ summary_parts.append("")
493
+
494
+ # Count issues by severity
495
+ severity_counts: dict[str, int] = {}
496
+ for result in report.results:
497
+ for issue in result.issues:
498
+ severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1
499
+
500
+ # Sort by severity rank (highest first)
501
+ from iam_validator.core.models import ValidationIssue
502
+
503
+ sorted_severities = sorted(
504
+ severity_counts.items(),
505
+ key=lambda x: ValidationIssue.SEVERITY_RANK.get(x[0], 0),
506
+ reverse=True,
507
+ )
508
+
509
+ summary_parts.append("| Severity | Count |")
510
+ summary_parts.append("|----------|-------|")
511
+ for severity, count in sorted_severities:
512
+ emoji = {
513
+ "error": "❌",
514
+ "critical": "🔴",
515
+ "high": "🟠",
516
+ "warning": "⚠️",
517
+ "medium": "🟡",
518
+ "low": "🔵",
519
+ "info": "ℹ️",
520
+ }.get(severity, "•")
521
+ summary_parts.append(f"| {emoji} {severity.upper()} | {count} |")
522
+
523
+ # Add footer with links
524
+ summary_parts.append("")
525
+ summary_parts.append("---")
526
+ summary_parts.append("")
527
+ summary_parts.append(
528
+ "📝 For detailed findings, check the PR comments or review the workflow logs."
529
+ )
530
+
531
+ # Write to summary file (append mode)
532
+ with open(summary_file, "a", encoding="utf-8") as f:
533
+ f.write("\n".join(summary_parts))
534
+ f.write("\n")
535
+
536
+ logging.info("Wrote summary to GitHub Actions job summary")
537
+
538
+ except Exception as e:
539
+ logging.warning(f"Failed to write GitHub Actions summary: {e}")
@@ -0,0 +1,14 @@
1
+ """Core validation modules."""
2
+
3
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
4
+ from iam_validator.core.policy_checks import PolicyValidator, validate_policies
5
+ from iam_validator.core.policy_loader import PolicyLoader
6
+ from iam_validator.core.report import ReportGenerator
7
+
8
+ __all__ = [
9
+ "AWSServiceFetcher",
10
+ "PolicyValidator",
11
+ "validate_policies",
12
+ "PolicyLoader",
13
+ "ReportGenerator",
14
+ ]