iam-policy-validator 1.14.0__py3-none-any.whl → 1.14.2__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.0
3
+ Version: 1.14.2
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=eIxDZehy-dzipjFQ_cuL4L6Az43FtqJUcub1D57guag,374
3
+ iam_validator/__version__.py,sha256=aKdF7mPOKj0H8xIHMqcgUIdAMplJ350c2A8EWCObeRY,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,7 +53,7 @@ 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=BMTovWROjaxmhaNg-9emUGNFF_FGtrwYmCKvioh7x5M,32448
56
+ iam_validator/core/pr_commenter.py,sha256=hDUzn0eQJ3wlNSVbhMCOm2dlOhbS3Pohf8ZdeUYRlCk,32580
57
57
  iam_validator/core/report.py,sha256=uMhUYv-8mNoTMZzD0F2buSQTxr4YIRh8UMZjvFq9tmc,37312
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
@@ -83,7 +83,7 @@ iam_validator/core/formatters/enhanced.py,sha256=GD7RIAL1hLDAsypCKECwDMGslAx2AaM
83
83
  iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
84
84
  iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
85
85
  iam_validator/core/formatters/markdown.py,sha256=dk4STeY-tOEZsVrlmolIEqZvWYP9JhRtygxxNA49DEE,2293
86
- iam_validator/core/formatters/sarif.py,sha256=O3pn7whqFq5xxk-tuoqSb2k4Fk5ai_A2SKX_ph8GLV4,10469
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
88
  iam_validator/integrations/github_integration.py,sha256=2pOjTVsLMymx-wU31Ly7JqODgNXf5U7lvteVqBpaRgE,67913
89
89
  iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
@@ -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.0.dist-info/METADATA,sha256=t_Q87WWncrJAQtMbwuNY9V1-vlav2anJLV55PVoDrlM,34456
103
- iam_policy_validator-1.14.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
- iam_policy_validator-1.14.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
105
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
106
- iam_policy_validator-1.14.0.dist-info/RECORD,,
102
+ iam_policy_validator-1.14.2.dist-info/METADATA,sha256=63ruVMh-1wI_vzVi2Elo6UC6LlmTbyZ7q1vr_-9n_rg,34456
103
+ iam_policy_validator-1.14.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
104
+ iam_policy_validator-1.14.2.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
105
+ iam_policy_validator-1.14.2.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
106
+ iam_policy_validator-1.14.2.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.0"
6
+ __version__ = "1.14.2"
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("."))
@@ -1,4 +1,15 @@
1
- """SARIF (Static Analysis Results Interchange Format) formatter for GitHub integration."""
1
+ """SARIF (Static Analysis Results Interchange Format) formatter for GitHub integration.
2
+
3
+ This formatter produces SARIF 2.1.0 output that integrates with GitHub Code Scanning,
4
+ providing rich issue details including:
5
+ - Risk explanations and remediation guidance
6
+ - Suggested fixes and code examples
7
+ - Links to documentation
8
+ - Affected policy fields (action, resource, condition)
9
+
10
+ The output appears in GitHub's Security tab as code scanning alerts with inline
11
+ annotations on affected lines.
12
+ """
2
13
 
3
14
  import json
4
15
  from datetime import datetime, timezone
@@ -9,7 +20,14 @@ from iam_validator.core.models import ValidationIssue, ValidationReport
9
20
 
10
21
 
11
22
  class SARIFFormatter(OutputFormatter):
12
- """Formats validation results in SARIF format for GitHub code scanning."""
23
+ """Formats validation results in SARIF format for GitHub code scanning.
24
+
25
+ Produces rich SARIF output with:
26
+ - Dynamic rule definitions based on check IDs
27
+ - Full issue context (risk, remediation, examples)
28
+ - Suggested fixes as SARIF fix objects
29
+ - Related locations for affected fields
30
+ """
13
31
 
14
32
  @property
15
33
  def format_id(self) -> str:
@@ -55,6 +73,11 @@ class SARIFFormatter(OutputFormatter):
55
73
  "low": "note",
56
74
  }
57
75
 
76
+ # Collect all unique check_ids from issues for dynamic rule generation
77
+ all_issues: list[ValidationIssue] = []
78
+ for policy_result in report.results:
79
+ all_issues.extend(policy_result.issues)
80
+
58
81
  # Create SARIF structure
59
82
  sarif = {
60
83
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
@@ -66,7 +89,7 @@ class SARIFFormatter(OutputFormatter):
66
89
  "name": "IAM Validator",
67
90
  "version": tool_version,
68
91
  "informationUri": "https://github.com/boogy/iam-validator",
69
- "rules": self._create_rules(),
92
+ "rules": self._create_rules_from_issues(all_issues),
70
93
  }
71
94
  },
72
95
  "results": self._create_results(report, severity_map),
@@ -83,90 +106,190 @@ class SARIFFormatter(OutputFormatter):
83
106
 
84
107
  return sarif
85
108
 
86
- def _create_rules(self) -> list[dict[str, Any]]:
87
- """Create SARIF rules definitions."""
88
- return [
89
- {
90
- "id": "invalid-action",
91
- "shortDescription": {"text": "Invalid IAM Action"},
92
- "fullDescription": {"text": "The specified IAM action does not exist in AWS"},
93
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html",
94
- "defaultConfiguration": {"level": "error"},
95
- },
96
- {
97
- "id": "invalid-condition-key",
98
- "shortDescription": {"text": "Invalid Condition Key"},
99
- "fullDescription": {
100
- "text": "The specified condition key is not valid for this action"
101
- },
102
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
103
- "defaultConfiguration": {"level": "error"},
104
- },
105
- {
106
- "id": "invalid-resource",
107
- "shortDescription": {"text": "Invalid Resource ARN"},
108
- "fullDescription": {"text": "The resource ARN format is invalid"},
109
- "helpUri": "https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html",
110
- "defaultConfiguration": {"level": "error"},
111
- },
112
- {
113
- "id": "duplicate-sid",
114
- "shortDescription": {"text": "Duplicate Statement ID"},
115
- "fullDescription": {
116
- "text": "Multiple statements use the same Statement ID (Sid), which can cause confusion"
117
- },
118
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html",
119
- "defaultConfiguration": {"level": "error"},
120
- },
121
- {
122
- "id": "overly-permissive",
123
- "shortDescription": {"text": "Overly Permissive Policy"},
124
- "fullDescription": {
125
- "text": "Policy grants overly broad permissions using wildcards in actions or resources"
126
- },
127
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
128
- "defaultConfiguration": {"level": "warning"},
129
- },
130
- {
131
- "id": "missing-condition",
132
- "shortDescription": {"text": "Missing Condition Restrictions"},
133
- "fullDescription": {
134
- "text": "Sensitive actions should include condition restrictions to limit when they can be used"
135
- },
136
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-policy-conditions",
137
- "defaultConfiguration": {"level": "warning"},
138
- },
139
- {
140
- "id": "missing-required-condition",
141
- "shortDescription": {"text": "Missing Required Condition"},
142
- "fullDescription": {
143
- "text": "Specific actions require certain conditions to prevent privilege escalation or security issues"
144
- },
145
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html",
146
- "defaultConfiguration": {"level": "error"},
147
- },
148
- {
149
- "id": "invalid-principal",
150
- "shortDescription": {"text": "Invalid Principal"},
151
- "fullDescription": {
152
- "text": "The specified principal is invalid or improperly formatted"
153
- },
154
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html",
155
- "defaultConfiguration": {"level": "error"},
156
- },
157
- {
158
- "id": "general-issue",
159
- "shortDescription": {"text": "IAM Policy Issue"},
160
- "fullDescription": {"text": "General IAM policy validation issue"},
161
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html",
162
- "defaultConfiguration": {"level": "warning"},
163
- },
164
- ]
109
+ def _create_rules_from_issues(self, issues: list[ValidationIssue]) -> list[dict[str, Any]]:
110
+ """Create SARIF rules dynamically from actual issues found.
111
+
112
+ This generates rules based on the check_id and issue_type of actual findings,
113
+ ensuring all rules referenced by results are defined.
114
+
115
+ Args:
116
+ issues: List of all validation issues
117
+
118
+ Returns:
119
+ List of SARIF rule definitions
120
+ """
121
+ # Track unique rules by check_id (or issue_type as fallback)
122
+ rules_map: dict[str, dict[str, Any]] = {}
123
+
124
+ # Severity to SARIF level mapping
125
+ severity_to_level = {
126
+ "error": "error",
127
+ "critical": "error",
128
+ "high": "error",
129
+ "warning": "warning",
130
+ "medium": "warning",
131
+ "info": "note",
132
+ "low": "note",
133
+ }
134
+
135
+ for issue in issues:
136
+ rule_id = self._get_rule_id(issue)
137
+
138
+ # Skip if already defined
139
+ if rule_id in rules_map:
140
+ continue
141
+
142
+ # Build rule from issue metadata
143
+ rule: dict[str, Any] = {
144
+ "id": rule_id,
145
+ "shortDescription": {"text": self._get_rule_short_description(issue)},
146
+ "fullDescription": {"text": self._get_rule_full_description(issue)},
147
+ "defaultConfiguration": {"level": severity_to_level.get(issue.severity, "warning")},
148
+ }
149
+
150
+ # Add help URI if available
151
+ if issue.documentation_url:
152
+ rule["helpUri"] = issue.documentation_url
153
+ else:
154
+ # Use default AWS docs based on issue type
155
+ rule["helpUri"] = self._get_default_help_uri(issue)
156
+
157
+ # Add rich help text with risk explanation and remediation
158
+ help_text = self._build_help_markdown(issue)
159
+ if help_text:
160
+ rule["help"] = {"text": help_text, "markdown": help_text}
161
+
162
+ rules_map[rule_id] = rule
163
+
164
+ # Return rules sorted by ID for consistent output
165
+ return list(sorted(rules_map.values(), key=lambda r: r["id"]))
166
+
167
+ def _get_rule_short_description(self, issue: ValidationIssue) -> str:
168
+ """Get a short description for the rule based on check_id or issue_type."""
169
+ # Map check_id to human-readable short descriptions
170
+ descriptions = {
171
+ "action_validation": "Invalid IAM Action",
172
+ "condition_key_validation": "Invalid Condition Key",
173
+ "condition_type_mismatch": "Condition Type Mismatch",
174
+ "resource_validation": "Invalid Resource ARN",
175
+ "sid_uniqueness": "Duplicate Statement ID",
176
+ "policy_size": "Policy Size Exceeded",
177
+ "policy_structure": "Invalid Policy Structure",
178
+ "set_operator_validation": "Invalid Set Operator",
179
+ "mfa_condition_check": "Missing MFA Condition",
180
+ "principal_validation": "Invalid Principal",
181
+ "policy_type_validation": "Policy Type Mismatch",
182
+ "action_resource_matching": "Action-Resource Mismatch",
183
+ "trust_policy_validation": "Trust Policy Issue",
184
+ "wildcard_action": "Wildcard Action",
185
+ "wildcard_resource": "Wildcard Resource",
186
+ "full_wildcard": "Full Wildcard Permission",
187
+ "service_wildcard": "Service-Wide Wildcard",
188
+ "sensitive_action": "Sensitive Action",
189
+ "action_condition_enforcement": "Missing Required Condition",
190
+ }
191
+
192
+ if issue.check_id and issue.check_id in descriptions:
193
+ return descriptions[issue.check_id]
194
+
195
+ # Fall back to formatting issue_type
196
+ return issue.issue_type.replace("_", " ").title()
197
+
198
+ def _get_rule_full_description(self, issue: ValidationIssue) -> str:
199
+ """Get a full description for the rule, using risk_explanation if available."""
200
+ if issue.risk_explanation:
201
+ return issue.risk_explanation
202
+
203
+ # Default descriptions based on check_id
204
+ descriptions = {
205
+ "action_validation": "The specified IAM action does not exist in AWS or is incorrectly formatted.",
206
+ "condition_key_validation": "The specified condition key is not valid for this action or service.",
207
+ "condition_type_mismatch": "The condition operator does not match the expected type for the condition key.",
208
+ "resource_validation": "The resource ARN format is invalid or does not match the expected pattern.",
209
+ "sid_uniqueness": "Multiple statements use the same Statement ID (Sid), which can cause confusion and policy conflicts.",
210
+ "policy_size": "The policy exceeds AWS size limits and may fail to apply.",
211
+ "policy_structure": "The policy structure is invalid and will be rejected by AWS.",
212
+ "wildcard_action": "Using wildcard (*) in actions grants broader permissions than necessary, violating least privilege.",
213
+ "wildcard_resource": "Using wildcard (*) in resources allows actions on all resources of that type.",
214
+ "full_wildcard": "Using Action: '*' with Resource: '*' grants full administrative access.",
215
+ "service_wildcard": "Using service:* grants all permissions for a service, which is overly permissive.",
216
+ "sensitive_action": "This action can modify security-critical resources and should be carefully restricted.",
217
+ "action_condition_enforcement": "Sensitive actions require specific conditions to prevent security issues.",
218
+ }
219
+
220
+ if issue.check_id and issue.check_id in descriptions:
221
+ return descriptions[issue.check_id]
222
+
223
+ return issue.message
224
+
225
+ def _get_default_help_uri(self, issue: ValidationIssue) -> str:
226
+ """Get default AWS documentation URL based on issue type."""
227
+ uri_map = {
228
+ "action_validation": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html",
229
+ "condition_key_validation": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
230
+ "resource_validation": "https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html",
231
+ "sid_uniqueness": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html",
232
+ "principal_validation": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html",
233
+ "wildcard_action": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
234
+ "wildcard_resource": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
235
+ "sensitive_action": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-policy-conditions",
236
+ }
237
+
238
+ if issue.check_id and issue.check_id in uri_map:
239
+ return uri_map[issue.check_id]
240
+
241
+ return "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html"
242
+
243
+ def _build_help_markdown(self, issue: ValidationIssue) -> str:
244
+ """Build markdown help text with remediation guidance.
245
+
246
+ Args:
247
+ issue: The validation issue
248
+
249
+ Returns:
250
+ Markdown-formatted help text
251
+ """
252
+ parts: list[str] = []
253
+
254
+ # Add risk explanation
255
+ if issue.risk_explanation:
256
+ parts.append(f"**Why this matters:** {issue.risk_explanation}")
257
+ parts.append("")
258
+
259
+ # Add remediation steps
260
+ if issue.remediation_steps:
261
+ parts.append("**How to fix:**")
262
+ for i, step in enumerate(issue.remediation_steps, 1):
263
+ parts.append(f"{i}. {step}")
264
+ parts.append("")
265
+
266
+ # Add suggestion
267
+ if issue.suggestion:
268
+ parts.append(f"**Suggestion:** {issue.suggestion}")
269
+ parts.append("")
270
+
271
+ # Add example
272
+ if issue.example:
273
+ parts.append("**Example:**")
274
+ parts.append("```json")
275
+ parts.append(issue.example)
276
+ parts.append("```")
277
+
278
+ return "\n".join(parts) if parts else ""
165
279
 
166
280
  def _create_results(
167
281
  self, report: ValidationReport, severity_map: dict[str, str]
168
282
  ) -> list[dict[str, Any]]:
169
- """Create SARIF results from validation issues."""
283
+ """Create SARIF results from validation issues with full context.
284
+
285
+ Each result includes:
286
+ - Rule reference and severity level
287
+ - Full message with risk explanation
288
+ - Location with line number
289
+ - Related locations for affected fields
290
+ - Fix suggestions with examples
291
+ - Properties with additional metadata
292
+ """
170
293
  results = []
171
294
 
172
295
  for policy_result in report.results:
@@ -177,7 +300,7 @@ class SARIFFormatter(OutputFormatter):
177
300
  result = {
178
301
  "ruleId": self._get_rule_id(issue),
179
302
  "level": severity_map.get(issue.severity, "note"),
180
- "message": {"text": issue.message},
303
+ "message": {"text": self._build_result_message(issue)},
181
304
  "locations": [
182
305
  {
183
306
  "physicalLocation": {
@@ -195,57 +318,176 @@ class SARIFFormatter(OutputFormatter):
195
318
  }
196
319
 
197
320
  # Add fix suggestions if available
198
- if issue.suggestion:
199
- result["fixes"] = [
200
- {
201
- "description": {"text": issue.suggestion},
202
- }
203
- ]
321
+ fixes = self._build_fixes(issue)
322
+ if fixes:
323
+ result["fixes"] = fixes
324
+
325
+ # Add related locations for affected fields
326
+ related = self._build_related_locations(issue, policy_result.policy_file)
327
+ if related:
328
+ result["relatedLocations"] = related
329
+
330
+ # Add properties with additional metadata
331
+ properties = self._build_properties(issue)
332
+ if properties:
333
+ result["properties"] = properties
204
334
 
205
335
  results.append(result)
206
336
 
207
337
  return results
208
338
 
339
+ def _build_result_message(self, issue: ValidationIssue) -> str:
340
+ """Build a comprehensive result message including context.
341
+
342
+ Args:
343
+ issue: The validation issue
344
+
345
+ Returns:
346
+ Formatted message string
347
+ """
348
+ parts = [issue.message]
349
+
350
+ # Add risk explanation if present
351
+ if issue.risk_explanation:
352
+ parts.append(f"\n\nWhy this matters: {issue.risk_explanation}")
353
+
354
+ # Add affected fields context
355
+ affected = []
356
+ if issue.action:
357
+ affected.append(f"Action: {issue.action}")
358
+ if issue.resource:
359
+ affected.append(f"Resource: {issue.resource}")
360
+ if issue.condition_key:
361
+ affected.append(f"Condition Key: {issue.condition_key}")
362
+
363
+ if affected:
364
+ parts.append(f"\n\nAffected: {', '.join(affected)}")
365
+
366
+ return "".join(parts)
367
+
368
+ def _build_fixes(self, issue: ValidationIssue) -> list[dict[str, Any]]:
369
+ """Build SARIF fix objects from issue suggestions.
370
+
371
+ Args:
372
+ issue: The validation issue
373
+
374
+ Returns:
375
+ List of SARIF fix objects
376
+ """
377
+ fixes = []
378
+
379
+ # Add suggestion as a fix
380
+ if issue.suggestion:
381
+ fix: dict[str, Any] = {"description": {"text": issue.suggestion}}
382
+
383
+ # If we have an example, include it as replacement text
384
+ if issue.example:
385
+ fix["description"]["text"] += f"\n\nExample:\n{issue.example}"
386
+
387
+ fixes.append(fix)
388
+
389
+ # Add remediation steps as a separate fix entry
390
+ if issue.remediation_steps:
391
+ remediation_text = "How to fix:\n" + "\n".join(
392
+ f"{i}. {step}" for i, step in enumerate(issue.remediation_steps, 1)
393
+ )
394
+ fixes.append({"description": {"text": remediation_text}})
395
+
396
+ return fixes
397
+
398
+ def _build_related_locations(
399
+ self, issue: ValidationIssue, policy_file: str
400
+ ) -> list[dict[str, Any]]:
401
+ """Build related locations for affected fields.
402
+
403
+ Args:
404
+ issue: The validation issue
405
+ policy_file: Path to the policy file
406
+
407
+ Returns:
408
+ List of SARIF related location objects
409
+ """
410
+ related = []
411
+
412
+ # Add statement context
413
+ if issue.statement_sid:
414
+ related.append(
415
+ {
416
+ "id": 0,
417
+ "message": {"text": f"Statement: {issue.statement_sid}"},
418
+ "physicalLocation": {
419
+ "artifactLocation": {"uri": policy_file, "uriBaseId": "SRCROOT"},
420
+ "region": {"startLine": issue.line_number or 1},
421
+ },
422
+ }
423
+ )
424
+
425
+ return related
426
+
427
+ def _build_properties(self, issue: ValidationIssue) -> dict[str, Any]:
428
+ """Build SARIF properties with additional metadata.
429
+
430
+ Args:
431
+ issue: The validation issue
432
+
433
+ Returns:
434
+ Dictionary of custom properties
435
+ """
436
+ properties: dict[str, Any] = {}
437
+
438
+ # Add check ID
439
+ if issue.check_id:
440
+ properties["checkId"] = issue.check_id
441
+
442
+ # Add issue type
443
+ properties["issueType"] = issue.issue_type
444
+
445
+ # Add statement info
446
+ properties["statementIndex"] = issue.statement_index
447
+ if issue.statement_sid:
448
+ properties["statementSid"] = issue.statement_sid
449
+
450
+ # Add severity category
451
+ if issue.is_security_severity():
452
+ properties["severityCategory"] = "security"
453
+ else:
454
+ properties["severityCategory"] = "validity"
455
+
456
+ # Add affected fields
457
+ if issue.action:
458
+ properties["action"] = issue.action
459
+ if issue.resource:
460
+ properties["resource"] = issue.resource
461
+ if issue.condition_key:
462
+ properties["conditionKey"] = issue.condition_key
463
+ if issue.field_name:
464
+ properties["fieldName"] = issue.field_name
465
+
466
+ # Add documentation URL
467
+ if issue.documentation_url:
468
+ properties["documentationUrl"] = issue.documentation_url
469
+
470
+ # Add remediation steps as array
471
+ if issue.remediation_steps:
472
+ properties["remediationSteps"] = issue.remediation_steps
473
+
474
+ return properties
475
+
209
476
  def _get_rule_id(self, issue: ValidationIssue) -> str:
210
477
  """Map issue to SARIF rule ID.
211
478
 
212
- Uses the issue_type field directly, converting underscores to hyphens
213
- for SARIF rule ID format. Falls back to heuristic matching for unknown types.
479
+ Uses check_id as the primary identifier (matches dynamically generated rules).
480
+ Falls back to issue_type if check_id is not available.
481
+
482
+ Args:
483
+ issue: The validation issue
484
+
485
+ Returns:
486
+ SARIF rule ID string
214
487
  """
215
- # Map common issue types directly
216
- issue_type_map = {
217
- "invalid_action": "invalid-action",
218
- "invalid_condition_key": "invalid-condition-key",
219
- "invalid_resource": "invalid-resource",
220
- "duplicate_sid": "duplicate-sid",
221
- "overly_permissive": "overly-permissive",
222
- "missing_condition": "missing-condition",
223
- "missing_required_condition": "missing-required-condition",
224
- "invalid_principal": "invalid-principal",
225
- }
488
+ # Prefer check_id as it's more specific and matches the check that raised it
489
+ if issue.check_id:
490
+ return issue.check_id
226
491
 
227
- # Try direct mapping from issue_type
228
- if issue.issue_type in issue_type_map:
229
- return issue_type_map[issue.issue_type]
230
-
231
- # Fallback: heuristic matching based on message
232
- message_lower = issue.message.lower()
233
-
234
- if "action" in message_lower and "not found" in message_lower:
235
- return "invalid-action"
236
- elif "condition key" in message_lower:
237
- return "invalid-condition-key"
238
- elif "duplicate" in message_lower and "sid" in message_lower:
239
- return "duplicate-sid"
240
- elif "wildcard" in message_lower or "overly permissive" in message_lower:
241
- return "overly-permissive"
242
- elif "missing" in message_lower and "condition" in message_lower:
243
- if "required" in message_lower:
244
- return "missing-required-condition"
245
- return "missing-condition"
246
- elif "principal" in message_lower:
247
- return "invalid-principal"
248
- elif "resource" in message_lower or "arn" in message_lower:
249
- return "invalid-resource"
250
- else:
251
- return "general-issue"
492
+ # Fall back to issue_type
493
+ return issue.issue_type
@@ -398,12 +398,15 @@ class PRCommenter:
398
398
  logger.info("No inline comments to post (after diff filtering)")
399
399
  # Still run cleanup to delete any stale comments from resolved findings
400
400
  # (unless skip_cleanup is set for streaming mode)
401
+ # Use APPROVE event to dismiss any previous REQUEST_CHANGES review
401
402
  if validated_files and self.cleanup_old_comments:
402
- logger.debug("Running cleanup for stale comments from resolved findings...")
403
+ logger.debug(
404
+ "Running cleanup for stale comments and approving PR (no blocking issues)..."
405
+ )
403
406
  await self.github.update_or_create_review_comments(
404
407
  comments=[],
405
408
  body="",
406
- event=ReviewEvent.COMMENT,
409
+ event=ReviewEvent.APPROVE,
407
410
  identifier=self.REVIEW_IDENTIFIER,
408
411
  validated_files=validated_files,
409
412
  skip_cleanup=False, # Explicitly run cleanup
@@ -421,7 +424,7 @@ class PRCommenter:
421
424
  for issue in result.issues
422
425
  )
423
426
 
424
- event = ReviewEvent.REQUEST_CHANGES if has_blocking_issues else ReviewEvent.COMMENT
427
+ event = ReviewEvent.REQUEST_CHANGES if has_blocking_issues else ReviewEvent.APPROVE
425
428
  logger.info(
426
429
  f"Creating PR review with {len(inline_comments)} comments, event: {event.value}"
427
430
  )