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