iam-policy-validator 1.14.3__py3-none-any.whl → 1.14.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.14.3
3
+ Version: 1.14.4
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -1,6 +1,6 @@
1
1
  iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=Gm8njL8lXkcG-nPd19SPPxnjvafHZUsjluEm-8FCYvo,374
3
+ iam_validator/__version__.py,sha256=K_xnaUWCmwON5ETAsNuzottXYU-HSz_ojv0wjnlbS0U,374
4
4
  iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=2-XUMbof9tQ7SHZNmAHMkR1DgbOIzY2eFWlp9S9dwLk,60625
6
6
  iam_validator/checks/action_resource_matching.py,sha256=qND0hfDgNoxFEdLWwrxOPVDfdj3k50nzedT2qF7nK7o,19428
@@ -53,8 +53,8 @@ iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf
53
53
  iam_validator/core/models.py,sha256=lXUadIsTpp_j0Vt89Ez7aJkTKs2GD2ty3Ukl2NeY9Zo,15680
54
54
  iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
55
55
  iam_validator/core/policy_loader.py,sha256=iid3mGfDzSXASzKDqbLnrqJHBdVQvvebofVqNImsGKM,29201
56
- iam_validator/core/pr_commenter.py,sha256=hDUzn0eQJ3wlNSVbhMCOm2dlOhbS3Pohf8ZdeUYRlCk,32580
57
- iam_validator/core/report.py,sha256=uMhUYv-8mNoTMZzD0F2buSQTxr4YIRh8UMZjvFq9tmc,37312
56
+ iam_validator/core/pr_commenter.py,sha256=F5ql60E-etGYOIDUSacvlhjsx5E-2hgGqhPbXmYfHqE,35021
57
+ iam_validator/core/report.py,sha256=IEHjNe6v_9nvcGA8_FNbXdG0AoV-yHVjiP1KQKnpEys,41376
58
58
  iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
59
59
  iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
60
60
  iam_validator/core/aws_service/client.py,sha256=Zv7rIpEFdUCDXKGp3migPDkj8L5eZltgrGe64M2t2Ko,7336
@@ -99,8 +99,8 @@ iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYu
99
99
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
100
100
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
101
101
  iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
102
- iam_policy_validator-1.14.3.dist-info/METADATA,sha256=aH2RAojXqwEay7CKD89eRTVCHphTpvyXON4XUdMcRfg,34456
103
- iam_policy_validator-1.14.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
- iam_policy_validator-1.14.3.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
105
- iam_policy_validator-1.14.3.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
106
- iam_policy_validator-1.14.3.dist-info/RECORD,,
102
+ iam_policy_validator-1.14.4.dist-info/METADATA,sha256=YHZ8MSw6dCEyDQmQm5mdbRNcf75MT-zL3n13ipE-zOQ,34456
103
+ iam_policy_validator-1.14.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
+ iam_policy_validator-1.14.4.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
105
+ iam_policy_validator-1.14.4.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
106
+ iam_policy_validator-1.14.4.dist-info/RECORD,,
@@ -3,7 +3,7 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.14.3"
6
+ __version__ = "1.14.4"
7
7
  # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
8
  _version_base = __version__.split("-", maxsplit=1)[0] # Remove pre-release suffix if present
9
9
  __version_info__ = tuple(int(part) for part in _version_base.split("."))
@@ -17,7 +17,7 @@ from iam_validator.core.diff_parser import DiffParser
17
17
  from iam_validator.core.label_manager import LabelManager
18
18
  from iam_validator.core.models import ValidationIssue, ValidationReport
19
19
  from iam_validator.core.policy_loader import PolicyLineMap, PolicyLoader
20
- from iam_validator.core.report import ReportGenerator
20
+ from iam_validator.core.report import IgnoredFindingInfo, ReportGenerator
21
21
  from iam_validator.integrations.github_integration import GitHubIntegration, ReviewEvent
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -96,6 +96,8 @@ class PRCommenter:
96
96
  self._context_issues: list[ContextIssue] = []
97
97
  # Track ignored finding IDs for the current run
98
98
  self._ignored_finding_ids: frozenset[str] = frozenset()
99
+ # Store full ignored findings for display in summary
100
+ self._ignored_findings: dict[str, Any] = {}
99
101
  # Cache for PolicyLineMap per file (for field-level line detection)
100
102
  self._policy_line_maps: dict[str, PolicyLineMap] = {}
101
103
 
@@ -155,8 +157,28 @@ class PRCommenter:
155
157
  generator = ReportGenerator()
156
158
  # Pass ignored count to show in summary
157
159
  ignored_count = len(self._ignored_finding_ids) if self._ignored_finding_ids else 0
160
+
161
+ # Convert ignored findings to IgnoredFindingInfo for display
162
+ ignored_findings_info: list[IgnoredFindingInfo] = []
163
+ if self._ignored_findings:
164
+ for finding in self._ignored_findings.values():
165
+ ignored_findings_info.append(
166
+ IgnoredFindingInfo(
167
+ file_path=finding.file_path,
168
+ issue_type=finding.issue_type,
169
+ ignored_by=finding.ignored_by,
170
+ reason=finding.reason,
171
+ )
172
+ )
173
+
174
+ # Determine if all blocking issues are ignored
175
+ all_blocking_ignored = self._are_all_blocking_issues_ignored(report)
176
+
158
177
  comment_parts = generator.generate_github_comment_parts(
159
- report, ignored_count=ignored_count
178
+ report,
179
+ ignored_count=ignored_count,
180
+ ignored_findings=ignored_findings_info if ignored_findings_info else None,
181
+ all_blocking_ignored=all_blocking_ignored,
160
182
  )
161
183
 
162
184
  # Post all parts using the multipart method
@@ -694,7 +716,10 @@ class PRCommenter:
694
716
  )
695
717
 
696
718
  store = IgnoredFindingsStore(self.github)
697
- self._ignored_finding_ids = await store.get_ignored_ids()
719
+ # Load full ignored findings for display in summary
720
+ self._ignored_findings = await store.load()
721
+ # Also get just the IDs for fast lookup
722
+ self._ignored_finding_ids = frozenset(self._ignored_findings.keys())
698
723
  if self._ignored_finding_ids:
699
724
  logger.debug(f"Loaded {len(self._ignored_finding_ids)} ignored finding(s)")
700
725
 
@@ -718,6 +743,36 @@ class PRCommenter:
718
743
  fingerprint = FindingFingerprint.from_issue(issue, file_path)
719
744
  return fingerprint.to_hash() in self._ignored_finding_ids
720
745
 
746
+ def _are_all_blocking_issues_ignored(self, report: ValidationReport) -> bool:
747
+ """Check if all blocking issues (based on fail_on_severities) are ignored.
748
+
749
+ Args:
750
+ report: The validation report
751
+
752
+ Returns:
753
+ True if there are no unignored blocking issues (i.e., all blocking
754
+ issues have been ignored, or there were no blocking issues to begin with)
755
+ """
756
+ if not self._ignored_finding_ids:
757
+ # No ignored findings - check if there are any blocking issues at all
758
+ for result in report.results:
759
+ for issue in result.issues:
760
+ if issue.severity in self.fail_on_severities:
761
+ return False
762
+ return True
763
+
764
+ # Check each blocking issue to see if it's ignored
765
+ for result in report.results:
766
+ relative_path = self._make_relative_path(result.policy_file)
767
+ if not relative_path:
768
+ continue
769
+ for issue in result.issues:
770
+ if issue.severity in self.fail_on_severities:
771
+ if not self._is_issue_ignored(issue, relative_path):
772
+ return False
773
+
774
+ return True
775
+
721
776
 
722
777
  async def post_report_to_pr(
723
778
  report_file: str,
@@ -5,6 +5,7 @@ including console output, JSON, and GitHub-flavored markdown for PR comments.
5
5
  """
6
6
 
7
7
  import logging
8
+ from dataclasses import dataclass
8
9
 
9
10
  from rich.console import Console
10
11
  from rich.panel import Panel
@@ -29,6 +30,24 @@ from iam_validator.core.models import (
29
30
  ValidationReport,
30
31
  )
31
32
 
33
+
34
+ @dataclass
35
+ class IgnoredFindingInfo:
36
+ """Information about an ignored finding for display in summary.
37
+
38
+ Attributes:
39
+ file_path: Path to the policy file
40
+ issue_type: Type of issue (e.g., "invalid_action")
41
+ ignored_by: Username who ignored the finding
42
+ reason: Optional reason provided by the user
43
+ """
44
+
45
+ file_path: str
46
+ issue_type: str
47
+ ignored_by: str
48
+ reason: str | None = None
49
+
50
+
32
51
  logger = logging.getLogger(__name__)
33
52
 
34
53
 
@@ -239,6 +258,8 @@ class ReportGenerator:
239
258
  report: ValidationReport,
240
259
  max_length_per_part: int = constants.GITHUB_COMMENT_SPLIT_LIMIT,
241
260
  ignored_count: int = 0,
261
+ ignored_findings: list[IgnoredFindingInfo] | None = None,
262
+ all_blocking_ignored: bool = False,
242
263
  ) -> list[str]:
243
264
  """Generate GitHub PR comment(s), splitting into multiple parts if needed.
244
265
 
@@ -246,6 +267,8 @@ class ReportGenerator:
246
267
  report: Validation report
247
268
  max_length_per_part: Maximum character length per comment part (default from GITHUB_COMMENT_SPLIT_LIMIT)
248
269
  ignored_count: Number of findings that were ignored (will be shown in summary)
270
+ ignored_findings: List of ignored finding details for display in summary
271
+ all_blocking_ignored: True if all blocking issues were ignored (shows "Passed" status)
249
272
 
250
273
  Returns:
251
274
  List of comment parts (each under max_length_per_part)
@@ -257,13 +280,19 @@ class ReportGenerator:
257
280
  if estimated_size <= max_length_per_part:
258
281
  # Try single comment
259
282
  single_comment = self.generate_github_comment(
260
- report, max_length=max_length_per_part * 2, ignored_count=ignored_count
283
+ report,
284
+ max_length=max_length_per_part * 2,
285
+ ignored_count=ignored_count,
286
+ ignored_findings=ignored_findings,
287
+ all_blocking_ignored=all_blocking_ignored,
261
288
  )
262
289
  if len(single_comment) <= max_length_per_part:
263
290
  return [single_comment]
264
291
 
265
292
  # Need to split into multiple parts
266
- return self._generate_split_comments(report, max_length_per_part, ignored_count)
293
+ return self._generate_split_comments(
294
+ report, max_length_per_part, ignored_count, ignored_findings, all_blocking_ignored
295
+ )
267
296
 
268
297
  def _estimate_report_size(self, report: ValidationReport) -> int:
269
298
  """Estimate the size of the report in characters.
@@ -280,7 +309,12 @@ class ReportGenerator:
280
309
  )
281
310
 
282
311
  def _generate_split_comments(
283
- self, report: ValidationReport, max_length: int, ignored_count: int = 0
312
+ self,
313
+ report: ValidationReport,
314
+ max_length: int,
315
+ ignored_count: int = 0,
316
+ ignored_findings: list[IgnoredFindingInfo] | None = None,
317
+ all_blocking_ignored: bool = False,
284
318
  ) -> list[str]:
285
319
  """Split a large report into multiple comment parts.
286
320
 
@@ -288,6 +322,8 @@ class ReportGenerator:
288
322
  report: Validation report
289
323
  max_length: Maximum length per part
290
324
  ignored_count: Number of ignored findings to show in summary
325
+ ignored_findings: List of ignored finding details for display
326
+ all_blocking_ignored: True if all blocking issues were ignored
291
327
 
292
328
  Returns:
293
329
  List of comment parts
@@ -295,7 +331,9 @@ class ReportGenerator:
295
331
  parts: list[str] = []
296
332
 
297
333
  # Generate header (will be in first part only)
298
- header_lines = self._generate_header(report, ignored_count)
334
+ header_lines = self._generate_header(
335
+ report, ignored_count, ignored_findings, all_blocking_ignored
336
+ )
299
337
  header_content = "\n".join(header_lines)
300
338
 
301
339
  # Generate footer (will be in all parts)
@@ -388,17 +426,27 @@ class ReportGenerator:
388
426
 
389
427
  return parts
390
428
 
391
- def _generate_header(self, report: ValidationReport, ignored_count: int = 0) -> list[str]:
429
+ def _generate_header(
430
+ self,
431
+ report: ValidationReport,
432
+ ignored_count: int = 0,
433
+ ignored_findings: list[IgnoredFindingInfo] | None = None,
434
+ all_blocking_ignored: bool = False,
435
+ ) -> list[str]:
392
436
  """Generate the comment header with summary.
393
437
 
394
438
  Args:
395
439
  report: Validation report
396
440
  ignored_count: Number of findings that were ignored
441
+ ignored_findings: List of ignored finding details for display
442
+ all_blocking_ignored: True if all blocking issues were ignored (shows "Passed" status)
397
443
  """
398
444
  lines = []
399
445
 
400
446
  # Title with emoji and status badge
401
- if report.invalid_policies == 0:
447
+ # Pass if: no invalid policies, OR all blocking issues were ignored
448
+ is_passing = report.invalid_policies == 0 or all_blocking_ignored
449
+ if is_passing:
402
450
  lines.append("# 🎉 IAM Policy Validation Passed!")
403
451
  status_badge = (
404
452
  "![Status](https://img.shields.io/badge/status-passed-success?style=flat-square)"
@@ -456,6 +504,56 @@ class ReportGenerator:
456
504
  lines.append(f"| 🔵 **Info** | {infos} |")
457
505
  lines.append("")
458
506
 
507
+ # Ignored findings section
508
+ if ignored_findings:
509
+ lines.extend(self._generate_ignored_findings_section(ignored_findings))
510
+
511
+ return lines
512
+
513
+ def _generate_ignored_findings_section(
514
+ self, ignored_findings: list[IgnoredFindingInfo]
515
+ ) -> list[str]:
516
+ """Generate the ignored findings section for the summary comment.
517
+
518
+ Args:
519
+ ignored_findings: List of ignored finding details
520
+
521
+ Returns:
522
+ List of markdown lines for the section
523
+ """
524
+ lines = []
525
+ lines.append("### 🔕 Ignored Findings")
526
+ lines.append("")
527
+ lines.append(
528
+ "> The following findings were ignored by authorized users and are excluded from validation:"
529
+ )
530
+ lines.append("")
531
+
532
+ lines.append("<details>")
533
+ lines.append(f"<summary>View {len(ignored_findings)} ignored finding(s)</summary>")
534
+ lines.append("")
535
+
536
+ lines.append("| File | Issue Type | Ignored By | Reason |")
537
+ lines.append("|------|------------|------------|--------|")
538
+
539
+ for finding in ignored_findings:
540
+ # Truncate file path if too long
541
+ file_display = finding.file_path
542
+ if len(file_display) > 50:
543
+ file_display = "..." + file_display[-47:]
544
+
545
+ reason_display = finding.reason if finding.reason else "-"
546
+ if len(reason_display) > 30:
547
+ reason_display = reason_display[:27] + "..."
548
+
549
+ lines.append(
550
+ f"| `{file_display}` | `{finding.issue_type}` | @{finding.ignored_by} | {reason_display} |"
551
+ )
552
+
553
+ lines.append("")
554
+ lines.append("</details>")
555
+ lines.append("")
556
+
459
557
  return lines
460
558
 
461
559
  def _generate_footer(self) -> str:
@@ -540,6 +638,8 @@ class ReportGenerator:
540
638
  report: ValidationReport,
541
639
  max_length: int = constants.GITHUB_MAX_COMMENT_LENGTH,
542
640
  ignored_count: int = 0,
641
+ ignored_findings: list[IgnoredFindingInfo] | None = None,
642
+ all_blocking_ignored: bool = False,
543
643
  ) -> str:
544
644
  """Generate a GitHub-flavored markdown comment for PR reviews.
545
645
 
@@ -547,6 +647,8 @@ class ReportGenerator:
547
647
  report: Validation report
548
648
  max_length: Maximum character length (default from GITHUB_MAX_COMMENT_LENGTH constant)
549
649
  ignored_count: Number of findings that were ignored (will be shown in summary)
650
+ ignored_findings: List of ignored finding details for display in summary
651
+ all_blocking_ignored: True if all blocking issues were ignored (shows "Passed" status)
550
652
 
551
653
  Returns:
552
654
  Markdown formatted string
@@ -554,8 +656,12 @@ class ReportGenerator:
554
656
  lines = []
555
657
 
556
658
  # Header with emoji and status badge
659
+ # Pass if: no invalid policies, OR all blocking issues were ignored
557
660
  has_parsing_errors = len(report.parsing_errors) > 0
558
- if report.invalid_policies == 0 and not has_parsing_errors:
661
+ is_passing = (
662
+ report.invalid_policies == 0 or all_blocking_ignored
663
+ ) and not has_parsing_errors
664
+ if is_passing:
559
665
  lines.append("# 🎉 IAM Policy Validation Passed!")
560
666
  status_badge = (
561
667
  "![Status](https://img.shields.io/badge/status-passed-success?style=flat-square)"
@@ -613,6 +719,10 @@ class ReportGenerator:
613
719
  lines.append(f"| 🔵 **Info** | {infos} |")
614
720
  lines.append("")
615
721
 
722
+ # Ignored findings section
723
+ if ignored_findings:
724
+ lines.extend(self._generate_ignored_findings_section(ignored_findings))
725
+
616
726
  # Parsing errors section (if any)
617
727
  if report.parsing_errors:
618
728
  lines.append("### ⚠️ Parsing Errors")