iam-policy-validator 1.14.3__py3-none-any.whl → 1.14.5__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.5
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=8ouM89pP7JLVFY6dwrTsOuZeWcu_xuQ3YwT7-1g9xn8,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
@@ -49,12 +49,12 @@ iam_validator/core/finding_fingerprint.py,sha256=NJIlu8NhdenWbLS7ww8LyWFasJgpKWN
49
49
  iam_validator/core/ignore_patterns.py,sha256=pZqDJBtkbck-85QK5eFPM5ZOPEKs3McRh3avqiCT5z0,10398
50
50
  iam_validator/core/ignore_processor.py,sha256=zgWfS-4BU4c_W6VxUxHIHorMtB5XzB410wZ3bbzVgH8,10686
51
51
  iam_validator/core/ignored_findings.py,sha256=b4PySz46so1rGKNt4prg2dkysHPfTJP4wsHYorVn1FA,12756
52
- iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf0TSUfc,7502
52
+ iam_validator/core/label_manager.py,sha256=qKQ60shsW8yJELkHgd9rXgzLW9oKErPd4hFTTQkHjbI,8776
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=IZu2FQqzw73U_8ugTUq197ECLqk9mRCQpTWXPu5qk0k,35490
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
@@ -85,7 +85,7 @@ iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFV
85
85
  iam_validator/core/formatters/markdown.py,sha256=dk4STeY-tOEZsVrlmolIEqZvWYP9JhRtygxxNA49DEE,2293
86
86
  iam_validator/core/formatters/sarif.py,sha256=03MHSyuZm9FlzaPeWg7wH-UTzzCDhSy6vMPrFpFNkS8,18884
87
87
  iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
88
- iam_validator/integrations/github_integration.py,sha256=OZjVFkeEK0PYerqHFOuc0tFtTMmo78JhbqZgFduzq-8,67949
88
+ iam_validator/integrations/github_integration.py,sha256=IKhJW_v_lGZiuyPN_xWULzv2YBbaXHn8zBfaOdUm28g,69054
89
89
  iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
90
90
  iam_validator/sdk/__init__.py,sha256=AZLnfdn3A9AWb0pMhsbu3GAOAzt6rV7Fi3E3d9_3ZdI,6388
91
91
  iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urchEFDA,12778
@@ -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.5.dist-info/METADATA,sha256=h6M6__GqJW5fWPtV0cEDqZ4sK259K5ulz68Jgt6COQE,34456
103
+ iam_policy_validator-1.14.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
+ iam_policy_validator-1.14.5.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
105
+ iam_policy_validator-1.14.5.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
106
+ iam_policy_validator-1.14.5.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.5"
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("."))
@@ -6,10 +6,11 @@ When those severities are not found, it removes the labels if present.
6
6
  """
7
7
 
8
8
  import logging
9
+ from collections.abc import Callable
9
10
  from typing import TYPE_CHECKING
10
11
 
11
12
  if TYPE_CHECKING:
12
- from iam_validator.core.models import PolicyValidationResult, ValidationReport
13
+ from iam_validator.core.models import PolicyValidationResult, ValidationIssue, ValidationReport
13
14
  from iam_validator.integrations.github_integration import GitHubIntegration
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -48,11 +49,17 @@ class LabelManager:
48
49
  """
49
50
  return bool(self.severity_labels) and self.github.is_configured()
50
51
 
51
- def _get_severities_in_results(self, results: list["PolicyValidationResult"]) -> set[str]:
52
+ def _get_severities_in_results(
53
+ self,
54
+ results: list["PolicyValidationResult"],
55
+ is_issue_ignored: Callable[["ValidationIssue", str], bool] | None = None,
56
+ ) -> set[str]:
52
57
  """Extract all severity levels found in validation results.
53
58
 
54
59
  Args:
55
60
  results: List of PolicyValidationResult objects
61
+ is_issue_ignored: Optional callback to check if an issue is ignored.
62
+ Takes (issue, file_path) and returns True if ignored.
56
63
 
57
64
  Returns:
58
65
  Set of severity levels found (e.g., {"error", "critical", "high"})
@@ -60,6 +67,9 @@ class LabelManager:
60
67
  severities = set()
61
68
  for result in results:
62
69
  for issue in result.issues:
70
+ # Skip ignored issues if a filter is provided
71
+ if is_issue_ignored and is_issue_ignored(issue, result.policy_file):
72
+ continue
63
73
  severities.add(issue.severity)
64
74
  return severities
65
75
 
@@ -113,17 +123,22 @@ class LabelManager:
113
123
  return labels_to_remove
114
124
 
115
125
  async def manage_labels_from_results(
116
- self, results: list["PolicyValidationResult"]
126
+ self,
127
+ results: list["PolicyValidationResult"],
128
+ is_issue_ignored: Callable[["ValidationIssue", str], bool] | None = None,
117
129
  ) -> tuple[bool, int, int]:
118
130
  """Manage PR labels based on validation results.
119
131
 
120
132
  This method will:
121
- 1. Determine which severity levels are present in the results
133
+ 1. Determine which severity levels are present in the results (excluding ignored issues)
122
134
  2. Add labels for severities that are found
123
135
  3. Remove labels for severities that are not found
124
136
 
125
137
  Args:
126
138
  results: List of PolicyValidationResult objects
139
+ is_issue_ignored: Optional callback to check if an issue is ignored.
140
+ Takes (issue, file_path) and returns True if ignored.
141
+ Ignored issues are excluded from label determination.
127
142
 
128
143
  Returns:
129
144
  Tuple of (success, labels_added, labels_removed)
@@ -132,8 +147,8 @@ class LabelManager:
132
147
  logger.debug("Label management not enabled (no severity_labels configured)")
133
148
  return (True, 0, 0)
134
149
 
135
- # Get all severities found in results
136
- found_severities = self._get_severities_in_results(results)
150
+ # Get all severities found in results (excluding ignored issues)
151
+ found_severities = self._get_severities_in_results(results, is_issue_ignored)
137
152
  logger.debug(f"Found severities in results: {found_severities}")
138
153
 
139
154
  # Determine which labels to apply/remove
@@ -182,7 +197,11 @@ class LabelManager:
182
197
 
183
198
  return (success, added_count, removed_count)
184
199
 
185
- async def manage_labels_from_report(self, report: "ValidationReport") -> tuple[bool, int, int]:
200
+ async def manage_labels_from_report(
201
+ self,
202
+ report: "ValidationReport",
203
+ is_issue_ignored: Callable[["ValidationIssue", str], bool] | None = None,
204
+ ) -> tuple[bool, int, int]:
186
205
  """Manage PR labels based on validation report.
187
206
 
188
207
  This is a convenience method that extracts results from the report
@@ -190,8 +209,11 @@ class LabelManager:
190
209
 
191
210
  Args:
192
211
  report: ValidationReport object
212
+ is_issue_ignored: Optional callback to check if an issue is ignored.
213
+ Takes (issue, file_path) and returns True if ignored.
214
+ Ignored issues are excluded from label determination.
193
215
 
194
216
  Returns:
195
217
  Tuple of (success, labels_added, labels_removed)
196
218
  """
197
- return await self.manage_labels_from_results(report.results)
219
+ return await self.manage_labels_from_results(report.results, is_issue_ignored)
@@ -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
@@ -174,7 +196,17 @@ class PRCommenter:
174
196
  # Manage PR labels based on severity findings
175
197
  if manage_labels and self.severity_labels:
176
198
  label_manager = LabelManager(self.github, self.severity_labels)
177
- label_success, added, removed = await label_manager.manage_labels_from_report(report)
199
+
200
+ # Create a filter function that uses relative paths for ignored finding lookup
201
+ def is_issue_ignored_for_labels(issue: ValidationIssue, file_path: str) -> bool:
202
+ relative_path = self._make_relative_path(file_path)
203
+ if not relative_path:
204
+ return False
205
+ return self._is_issue_ignored(issue, relative_path)
206
+
207
+ label_success, added, removed = await label_manager.manage_labels_from_report(
208
+ report, is_issue_ignored=is_issue_ignored_for_labels
209
+ )
178
210
 
179
211
  if not label_success:
180
212
  logger.error("Failed to manage PR labels")
@@ -694,7 +726,10 @@ class PRCommenter:
694
726
  )
695
727
 
696
728
  store = IgnoredFindingsStore(self.github)
697
- self._ignored_finding_ids = await store.get_ignored_ids()
729
+ # Load full ignored findings for display in summary
730
+ self._ignored_findings = await store.load()
731
+ # Also get just the IDs for fast lookup
732
+ self._ignored_finding_ids = frozenset(self._ignored_findings.keys())
698
733
  if self._ignored_finding_ids:
699
734
  logger.debug(f"Loaded {len(self._ignored_finding_ids)} ignored finding(s)")
700
735
 
@@ -718,6 +753,36 @@ class PRCommenter:
718
753
  fingerprint = FindingFingerprint.from_issue(issue, file_path)
719
754
  return fingerprint.to_hash() in self._ignored_finding_ids
720
755
 
756
+ def _are_all_blocking_issues_ignored(self, report: ValidationReport) -> bool:
757
+ """Check if all blocking issues (based on fail_on_severities) are ignored.
758
+
759
+ Args:
760
+ report: The validation report
761
+
762
+ Returns:
763
+ True if there are no unignored blocking issues (i.e., all blocking
764
+ issues have been ignored, or there were no blocking issues to begin with)
765
+ """
766
+ if not self._ignored_finding_ids:
767
+ # No ignored findings - check if there are any blocking issues at all
768
+ for result in report.results:
769
+ for issue in result.issues:
770
+ if issue.severity in self.fail_on_severities:
771
+ return False
772
+ return True
773
+
774
+ # Check each blocking issue to see if it's ignored
775
+ for result in report.results:
776
+ relative_path = self._make_relative_path(result.policy_file)
777
+ if not relative_path:
778
+ continue
779
+ for issue in result.issues:
780
+ if issue.severity in self.fail_on_severities:
781
+ if not self._is_issue_ignored(issue, relative_path):
782
+ return False
783
+
784
+ return True
785
+
721
786
 
722
787
  async def post_report_to_pr(
723
788
  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")
@@ -1265,6 +1265,26 @@ class GitHubIntegration:
1265
1265
  f"{created_count} created, {deleted_count} deleted (resolved)"
1266
1266
  )
1267
1267
 
1268
+ # Step 4: If no new comments were created but we need to submit APPROVE/REQUEST_CHANGES,
1269
+ # submit a review without inline comments to update the PR review state.
1270
+ # This is important when all issues are ignored/resolved - we need to dismiss
1271
+ # the previous REQUEST_CHANGES review by submitting an APPROVE review.
1272
+ if not new_comments_for_review and event in (
1273
+ ReviewEvent.APPROVE,
1274
+ ReviewEvent.REQUEST_CHANGES,
1275
+ ):
1276
+ # Only submit if there's a meaningful state change to make
1277
+ # (submitting APPROVE when all issues are resolved/ignored)
1278
+ logger.info(f"Submitting {event.value} review (no inline comments)")
1279
+ success = await self.create_review_with_comments(
1280
+ comments=[],
1281
+ body=body or f"<!-- {identifier} -->\nValidation complete.",
1282
+ event=event,
1283
+ )
1284
+ if not success:
1285
+ logger.warning(f"Failed to submit {event.value} review")
1286
+ # Don't fail the whole operation - comments were managed successfully
1287
+
1268
1288
  return True
1269
1289
 
1270
1290
  def _extract_finding_id(self, body: str) -> str | None: