iam-policy-validator 1.13.1__py3-none-any.whl → 1.14.1__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.
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/RECORD +45 -39
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +6 -0
- iam_validator/checks/action_resource_matching.py +12 -12
- iam_validator/checks/action_validation.py +1 -0
- iam_validator/checks/condition_key_validation.py +2 -0
- iam_validator/checks/condition_type_mismatch.py +3 -0
- iam_validator/checks/full_wildcard.py +1 -0
- iam_validator/checks/mfa_condition_check.py +2 -0
- iam_validator/checks/policy_structure.py +9 -0
- iam_validator/checks/policy_type_validation.py +11 -0
- iam_validator/checks/principal_validation.py +5 -0
- iam_validator/checks/resource_validation.py +4 -0
- iam_validator/checks/sensitive_action.py +1 -0
- iam_validator/checks/service_wildcard.py +6 -3
- iam_validator/checks/set_operator_validation.py +3 -0
- iam_validator/checks/sid_uniqueness.py +2 -0
- iam_validator/checks/trust_policy_validation.py +3 -0
- iam_validator/checks/utils/__init__.py +16 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/wildcard_action.py +1 -0
- iam_validator/checks/wildcard_resource.py +231 -4
- iam_validator/commands/analyze.py +19 -1
- iam_validator/commands/completion.py +6 -2
- iam_validator/commands/validate.py +231 -12
- iam_validator/core/aws_service/fetcher.py +21 -9
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/config_loader.py +199 -0
- iam_validator/core/config/defaults.py +25 -0
- iam_validator/core/constants.py +1 -0
- iam_validator/core/diff_parser.py +8 -4
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/sarif.py +370 -128
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/models.py +54 -4
- iam_validator/core/policy_loader.py +313 -4
- iam_validator/core/pr_commenter.py +223 -22
- iam_validator/core/report.py +22 -6
- iam_validator/integrations/github_integration.py +881 -123
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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.
|
|
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
|
|
87
|
-
"""Create SARIF rules
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
213
|
-
|
|
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
|
-
#
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
#
|
|
228
|
-
|
|
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
|