-
Notifications
You must be signed in to change notification settings - Fork 74
/
comment.go
273 lines (246 loc) · 10.8 KB
/
comment.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
package utils
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"github.com/jfrog/frogbot/v2/utils/outputwriter"
"github.com/jfrog/froggit-go/vcsclient"
"github.com/jfrog/jfrog-cli-security/formats"
"github.com/jfrog/jfrog-client-go/utils/log"
)
type ReviewCommentType string
type ReviewComment struct {
Location formats.Location
Type ReviewCommentType
CommentInfo vcsclient.PullRequestComment
}
const (
ApplicableComment ReviewCommentType = "Applicable"
IacComment ReviewCommentType = "Iac"
SastComment ReviewCommentType = "Sast"
RescanRequestComment = "rescan"
commentRemovalErrorMsg = "An error occurred while attempting to remove older Frogbot pull request comments:"
)
func HandlePullRequestCommentsAfterScan(issues *IssuesCollection, repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) {
if !repo.Params.AvoidPreviousPrCommentsDeletion {
// The removal of comments may fail for various reasons,
// such as concurrent scanning of pull requests and attempts
// to delete comments that have already been removed in a different process.
// Since this task is not mandatory for a Frogbot run,
// we will not cause a Frogbot run to fail but will instead log the error.
log.Debug("Looking for an existing Frogbot pull request comment. Deleting it if it exists...")
if e := DeletePullRequestComments(repo, client, pullRequestID); e != nil {
log.Error(fmt.Sprintf("%s:\n%v", commentRemovalErrorMsg, e))
}
}
// Add summary (SCA, license) scan comment
for _, comment := range generatePullRequestSummaryComment(issues, repo.OutputWriter) {
if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, comment, pullRequestID); err != nil {
err = errors.New("couldn't add pull request comment: " + err.Error())
return
}
}
// Handle review comments at the pull request
if err = addReviewComments(repo, pullRequestID, client, issues); err != nil {
err = errors.New("couldn't add pull request review comments: " + err.Error())
return
}
return
}
func DeletePullRequestComments(repo *Repository, client vcsclient.VcsClient, pullRequestID int) (err error) {
// Delete previous PR regular comments, if exists (not related to location of a change)
err = DeleteExistingPullRequestComments(repo, client)
// Delete previous PR review comments, if exists (related to location of a change)
return errors.Join(err, DeleteExistingPullRequestReviewComments(repo, pullRequestID, client))
}
// Delete existing pull request regular comments (Summary, Fallback review comments)
func DeleteExistingPullRequestComments(repository *Repository, client vcsclient.VcsClient) error {
prDetails := repository.PullRequestDetails
comments, err := GetSortedPullRequestComments(client, prDetails.Target.Owner, prDetails.Target.Repository, int(prDetails.ID))
if err != nil {
return fmt.Errorf(
"failed to get comments. the following details were used in order to fetch the comments: <%s/%s> pull request #%d. the error received: %s",
repository.RepoOwner, repository.RepoName, int(repository.PullRequestDetails.ID), err.Error())
}
commentsToDelete := getFrogbotComments(repository.OutputWriter, comments)
// Delete
if len(commentsToDelete) > 0 {
for _, commentToDelete := range commentsToDelete {
if err = client.DeletePullRequestComment(context.Background(), prDetails.Target.Owner, prDetails.Target.Repository, int(prDetails.ID), int(commentToDelete.ID)); err != nil {
return err
}
}
}
return err
}
func GenerateFixPullRequestDetails(vulnerabilities []formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) (description string, extraComments []string) {
content := outputwriter.GetPRSummaryContent(outputwriter.VulnerabilitiesContent(vulnerabilities, writer), true, false, writer)
if len(content) == 1 {
// Limit is not reached, use the entire content as the description
description = content[0]
return
}
// Limit is reached (at least 2 content), use the first as the description and the rest as extra comments
for i, comment := range content {
if i == 0 {
description = comment
} else {
extraComments = append(extraComments, comment)
}
}
return
}
func generatePullRequestSummaryComment(issuesCollection *IssuesCollection, writer outputwriter.OutputWriter) []string {
if !issuesCollection.IssuesExists() {
return outputwriter.GetPRSummaryContent([]string{}, false, true, writer)
}
content := []string{}
if vulnerabilitiesContent := outputwriter.VulnerabilitiesContent(issuesCollection.Vulnerabilities, writer); len(vulnerabilitiesContent) > 0 {
content = append(content, vulnerabilitiesContent...)
}
if licensesContent := outputwriter.LicensesContent(issuesCollection.Licenses, writer); len(licensesContent) > 0 {
content = append(content, licensesContent)
}
return outputwriter.GetPRSummaryContent(content, true, true, writer)
}
func IsFrogbotRescanComment(comment string) bool {
return strings.Contains(strings.ToLower(comment), RescanRequestComment)
}
func GetSortedPullRequestComments(client vcsclient.VcsClient, repoOwner, repoName string, prID int) ([]vcsclient.CommentInfo, error) {
pullRequestsComments, err := client.ListPullRequestComments(context.Background(), repoOwner, repoName, prID)
if err != nil {
return nil, err
}
// Sort the comment according to time created, the newest comment should be the first one.
sort.Slice(pullRequestsComments, func(i, j int) bool {
return pullRequestsComments[i].Created.After(pullRequestsComments[j].Created)
})
return pullRequestsComments, nil
}
func addReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient, issues *IssuesCollection) (err error) {
commentsToAdd := getNewReviewComments(repo, issues)
if len(commentsToAdd) == 0 {
return
}
// Add review comments for the given data
for _, comment := range commentsToAdd {
log.Debug("creating a review comment for", comment.Type, comment.Location.File, comment.Location.StartLine, comment.Location.StartColumn)
if e := client.AddPullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, comment.CommentInfo); e != nil {
log.Debug("couldn't add pull request review comment, fallback to regular comment: " + e.Error())
if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, outputwriter.GetFallbackReviewCommentContent(comment.CommentInfo.Content, comment.Location, repo.OutputWriter), pullRequestID); err != nil {
err = errors.New("couldn't add pull request comment, fallback to comment: " + err.Error())
return
}
}
}
return
}
// Delete existing pull request review comments (Applicable, Sast, Iac)
func DeleteExistingPullRequestReviewComments(repo *Repository, pullRequestID int, client vcsclient.VcsClient) (err error) {
// Get all review comments in PR
var existingComments []vcsclient.CommentInfo
if existingComments, err = client.ListPullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID); err != nil {
err = errors.New("couldn't list existing review comments: " + err.Error())
return
}
// Delete old review comments
if len(existingComments) > 0 {
if err = client.DeletePullRequestReviewComments(context.Background(), repo.RepoOwner, repo.RepoName, pullRequestID, getFrogbotComments(repo.OutputWriter, existingComments)...); err != nil {
err = errors.New("couldn't delete pull request review comment: " + err.Error())
return
}
}
return
}
func getFrogbotComments(writer outputwriter.OutputWriter, existingComments []vcsclient.CommentInfo) (reviewComments []vcsclient.CommentInfo) {
for _, comment := range existingComments {
if outputwriter.IsFrogbotComment(comment.Content) || outputwriter.IsFrogbotSummaryComment(writer, comment.Content) {
log.Debug("Deleting comment id:", comment.ID)
reviewComments = append(reviewComments, comment)
}
}
return
}
func getNewReviewComments(repo *Repository, issues *IssuesCollection) (commentsToAdd []ReviewComment) {
writer := repo.OutputWriter
for _, vulnerability := range issues.Vulnerabilities {
for _, cve := range vulnerability.Cves {
if cve.Applicability != nil {
for _, evidence := range cve.Applicability.Evidence {
commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, evidence.Location, generateApplicabilityReviewContent(evidence, cve, vulnerability, writer)))
}
}
}
}
for _, iac := range issues.Iacs {
commentsToAdd = append(commentsToAdd, generateReviewComment(IacComment, iac.Location, generateSourceCodeReviewContent(IacComment, iac, writer)))
}
for _, sast := range issues.Sast {
commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, sast.Location, generateSourceCodeReviewContent(SastComment, sast, writer)))
}
return
}
func generateReviewComment(commentType ReviewCommentType, location formats.Location, content string) (comment ReviewComment) {
return ReviewComment{
Location: location,
CommentInfo: vcsclient.PullRequestComment{
CommentInfo: vcsclient.CommentInfo{
Content: content,
},
PullRequestDiff: createPullRequestDiff(location),
},
Type: commentType,
}
}
func generateApplicabilityReviewContent(issue formats.Evidence, relatedCve formats.CveRow, relatedVulnerability formats.VulnerabilityOrViolationRow, writer outputwriter.OutputWriter) string {
remediation := ""
if relatedVulnerability.JfrogResearchInformation != nil {
remediation = relatedVulnerability.JfrogResearchInformation.Remediation
}
return outputwriter.GenerateReviewCommentContent(outputwriter.ApplicableCveReviewContent(
relatedVulnerability.Severity,
issue.Reason,
relatedCve.Applicability.ScannerDescription,
relatedCve.Id,
relatedVulnerability.Summary,
fmt.Sprintf("%s:%s", relatedVulnerability.ImpactedDependencyName, relatedVulnerability.ImpactedDependencyVersion),
remediation,
writer,
), writer)
}
func generateSourceCodeReviewContent(commentType ReviewCommentType, issue formats.SourceCodeRow, writer outputwriter.OutputWriter) (content string) {
switch commentType {
case IacComment:
return outputwriter.GenerateReviewCommentContent(outputwriter.IacReviewContent(
issue.Severity,
issue.Finding,
issue.ScannerDescription,
writer,
), writer)
case SastComment:
return outputwriter.GenerateReviewCommentContent(outputwriter.SastReviewContent(
issue.Severity,
issue.Finding,
issue.ScannerDescription,
issue.CodeFlow,
writer,
), writer)
}
return
}
func createPullRequestDiff(location formats.Location) vcsclient.PullRequestDiff {
return vcsclient.PullRequestDiff{
OriginalFilePath: location.File,
OriginalStartLine: location.StartLine,
OriginalEndLine: location.EndLine,
OriginalStartColumn: location.StartColumn,
OriginalEndColumn: location.EndColumn,
NewFilePath: location.File,
NewStartLine: location.StartLine,
NewEndLine: location.EndLine,
NewStartColumn: location.StartColumn,
NewEndColumn: location.EndColumn,
}
}