Skip to content

Commit f0ec429

Browse files
authored
Add support for handling of snippet code (#1054)
1 parent da09d8d commit f0ec429

File tree

9 files changed

+620
-6
lines changed

9 files changed

+620
-6
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ require (
1010
github.com/jfrog/build-info-go v1.13.1-0.20260216093441-40a4dc563294
1111
github.com/jfrog/froggit-go v1.21.0
1212
github.com/jfrog/gofrog v1.7.6
13-
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260227101327-7478579b5f25
1413
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260218080258-3bf55ed18973
15-
github.com/jfrog/jfrog-cli-security v1.26.2
14+
github.com/jfrog/jfrog-cli-security v1.26.3
1615
github.com/jfrog/jfrog-client-go v1.55.1-0.20260225080504-17057750d47b
1716
github.com/owenrumney/go-sarif/v3 v3.2.3
1817
github.com/stretchr/testify v1.11.1
@@ -65,6 +64,7 @@ require (
6564
github.com/jedib0t/go-pretty/v6 v6.7.5 // indirect
6665
github.com/jfrog/archiver/v3 v3.6.1 // indirect
6766
github.com/jfrog/jfrog-apps-config v1.0.1 // indirect
67+
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260227101327-7478579b5f25 // indirect
6868
github.com/kevinburke/ssh_config v1.2.0 // indirect
6969
github.com/klauspost/compress v1.18.1 // indirect
7070
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
@@ -128,7 +128,7 @@ require (
128128
gopkg.in/yaml.v3 v3.0.1 // indirect
129129
)
130130

131-
replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security v1.26.3-0.20260302131045-5f088ae1204c
131+
// replace github.com/jfrog/jfrog-cli-security => github.com/jfrog/jfrog-cli-security dev
132132

133133
// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 dev
134134

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260227101327-7478579b5f25 h1:o
148148
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260227101327-7478579b5f25/go.mod h1:P9ZywyTQzp+WsNmeb4IiMQOdVb++eQUD5oXd18LRVj8=
149149
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260218080258-3bf55ed18973 h1:fOlWUGkCuujnIcE3166gpTdvicwv1wAZhLrfbm+f6rY=
150150
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260218080258-3bf55ed18973/go.mod h1:GDveG1xAoiM12JlSx8RE0OcJ6Ov+xcmpmGv84we3pMA=
151-
github.com/jfrog/jfrog-cli-security v1.26.3-0.20260302131045-5f088ae1204c h1:XL7RC5H7HW+wRoUzM04pDFQzWr+VOWMJNDMl8fHto94=
152-
github.com/jfrog/jfrog-cli-security v1.26.3-0.20260302131045-5f088ae1204c/go.mod h1:PSf39yzsu1qp1lXJ3QOSBOw4dzqLW2r/6Q616lY+x/o=
151+
github.com/jfrog/jfrog-cli-security v1.26.3 h1:991m5HZrFxR8GOg5ALxTGxih73+wTPmLvlLG0VaXDxk=
152+
github.com/jfrog/jfrog-cli-security v1.26.3/go.mod h1:eZLjW37Z6f1DbeKCsL+NnYSm41hQnV1wV6NpLfIOwLw=
153153
github.com/jfrog/jfrog-client-go v1.55.1-0.20260225080504-17057750d47b h1:mSxcMTXtnrYMVhCGk7ui2ERh6yLoUVUQhXaNwd3FhL8=
154154
github.com/jfrog/jfrog-client-go v1.55.1-0.20260225080504-17057750d47b/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U=
155155
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
3+
---
4+
## 📜 Public Code Snippet Violation
5+
6+
---
7+
8+
The highlighted snippet was copied from:
9+
[https://github.com/example/repo/blob/main/file.go#L10-L30](https://github.com/example/repo/blob/main/file.go#L10-L30)
10+
| Severity | License | Watch Name | Policies |
11+
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
12+
| High | MIT | watch1 | policy1 |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
## 📜 Public Code Snippet Violation
3+
4+
The highlighted snippet was copied from:
5+
[https://github.com/example/repo/blob/main/file.go#L10-L30](https://github.com/example/repo/blob/main/file.go#L10-L30)
6+
<div align='center'>
7+
8+
| Severity | License | Watch Name | Policies |
9+
| :---------------------: | :-----------------------------------: | :-----------------------------------: | :-----------------------------------: |
10+
| ![high](https://raw.githubusercontent.com/jfrog/frogbot/master/resources/v2/applicableHighSeverity.png)<br> High | MIT | watch1 | policy1 |
11+
12+
</div>

utils/comment.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"regexp"
78
"sort"
9+
"strconv"
810

911
"github.com/jfrog/froggit-go/vcsclient"
12+
"github.com/jfrog/gofrog/datastructures"
1013
"github.com/jfrog/jfrog-cli-security/utils/formats"
1114
"github.com/jfrog/jfrog-cli-security/utils/results"
1215
"github.com/jfrog/jfrog-client-go/utils/log"
@@ -28,7 +31,9 @@ const (
2831
IacComment ReviewCommentType = "Iac"
2932
SastComment ReviewCommentType = "Sast"
3033
SecretComment ReviewCommentType = "Secrets"
34+
SnippetComment ReviewCommentType = "Snippet"
3135

36+
snippetVersionMarker = "snippet"
3237
commentRemovalErrorMsg = "An error occurred while attempting to remove older Frogbot pull request comments:"
3338
)
3439

@@ -186,6 +191,9 @@ func getFrogbotComments(existingComments []vcsclient.CommentInfo) (reviewComment
186191

187192
func getNewReviewComments(repo *Repository, issues *issues.ScansIssuesCollection) (commentsToAdd []ReviewComment) {
188193
writer := repo.OutputWriter
194+
195+
commentsToAdd = append(commentsToAdd, generateSnippetReviewComment(issues, writer)...)
196+
189197
// CVE Applicable Evidence review comments
190198
for _, applicableEvidences := range issues.GetApplicableEvidences() {
191199
commentsToAdd = append(commentsToAdd, generateReviewComment(ApplicableComment, applicableEvidences.Evidence.Location, generateApplicabilityReviewContent(applicableEvidences, writer)))
@@ -206,6 +214,7 @@ func getNewReviewComments(repo *Repository, issues *issues.ScansIssuesCollection
206214
commentsToAdd = append(commentsToAdd, generateReviewComment(SastComment, similarSastIssues.Location, generateSourceCodeReviewContent(SastComment, true, writer, similarSastIssues.issues...)))
207215
}
208216
}
217+
209218
// Secrets review comments
210219
if !repo.FrogbotConfig.ShowSecretsAsPrComment {
211220
return
@@ -218,9 +227,99 @@ func getNewReviewComments(repo *Repository, issues *issues.ScansIssuesCollection
218227
commentsToAdd = append(commentsToAdd, generateReviewComment(SecretComment, similarSecretsIssues.Location, generateSourceCodeReviewContent(SecretComment, true, writer, similarSecretsIssues.issues...)))
219228
}
220229
}
230+
221231
return
222232
}
223233

234+
func generateSnippetReviewComment(issues *issues.ScansIssuesCollection, writer outputwriter.OutputWriter) (commentsToAdd []ReviewComment) {
235+
type key struct {
236+
File string
237+
Line int
238+
}
239+
240+
locToOrigin := make(map[key]*datastructures.Set[string])
241+
licensesBySnippet := make(map[key][]formats.LicenseViolationRow)
242+
for _, lic := range issues.LicensesViolations {
243+
if lic.ImpactedDependencyVersion != snippetVersionMarker {
244+
continue
245+
}
246+
// Extract license violation information that are related to a snippet
247+
for _, ipath := range lic.ImpactPaths {
248+
if len(ipath) == 0 {
249+
continue
250+
}
251+
252+
// Leaf node of the impact path is the snippet location
253+
snippet := ipath[len(ipath)-1]
254+
255+
// Map evidence to snippet location
256+
for _, evidence := range snippet.Evidences {
257+
k := key{File: evidence.File, Line: evidence.StartLine}
258+
licensesBySnippet[k] = append(licensesBySnippet[k], lic)
259+
if locToOrigin[k] == nil {
260+
locToOrigin[k] = datastructures.MakeSet[string]()
261+
}
262+
locToOrigin[k].AddElements(evidence.ExternalReferences...)
263+
}
264+
}
265+
}
266+
// Sort snippet locations by file and line
267+
sortedKeys := make([]key, 0, len(licensesBySnippet))
268+
for k := range licensesBySnippet {
269+
sortedKeys = append(sortedKeys, k)
270+
}
271+
sort.Slice(sortedKeys, func(i, j int) bool {
272+
if sortedKeys[i].File != sortedKeys[j].File {
273+
return sortedKeys[i].File < sortedKeys[j].File
274+
}
275+
return sortedKeys[i].Line < sortedKeys[j].Line
276+
})
277+
// Generate review comments for each snippet location
278+
for _, loc := range sortedKeys {
279+
licenses := licensesBySnippet[loc]
280+
refSlice := locToOrigin[loc].ToSlice()
281+
commentsToAdd = append(commentsToAdd, generateReviewComment(
282+
SnippetComment,
283+
formats.Location{
284+
File: loc.File,
285+
StartLine: loc.Line,
286+
EndLine: loc.Line + snippetLineDeltaFromRef(refSlice),
287+
},
288+
generateComponentReviewContent(
289+
SnippetComment,
290+
true,
291+
writer,
292+
licenses,
293+
refSlice),
294+
))
295+
}
296+
return
297+
}
298+
299+
var snippetLineRangeRe = regexp.MustCompile(`#L(\d+)-L(\d+)$`)
300+
301+
const defaultSnippetLineDelta = 20
302+
303+
// snippetLineDeltaFromRef parses a GitHub-style line range fragment (#L<start>-L<end>)
304+
// from the first matching external reference URL and returns end−start (the delta to
305+
// add to a start line to compute the end line).
306+
// Falls back to defaultSnippetLineDelta when no URL contains a parseable range.
307+
func snippetLineDeltaFromRef(refs []string) int {
308+
for _, ref := range refs {
309+
m := snippetLineRangeRe.FindStringSubmatch(ref)
310+
if len(m) != 3 {
311+
continue
312+
}
313+
start, err1 := strconv.Atoi(m[1])
314+
end, err2 := strconv.Atoi(m[2])
315+
if err1 != nil || err2 != nil || end <= start {
316+
continue
317+
}
318+
return end - start
319+
}
320+
return defaultSnippetLineDelta
321+
}
322+
224323
type jasCommentIssues struct {
225324
// The location of the issue that the comment will be added to.
226325
formats.Location
@@ -284,6 +383,24 @@ func generateSourceCodeReviewContent(commentType ReviewCommentType, violation bo
284383
return
285384
}
286385

386+
func generateComponentReviewContent(
387+
commentType ReviewCommentType,
388+
violation bool,
389+
writer outputwriter.OutputWriter,
390+
licenses []formats.LicenseViolationRow,
391+
externalReferences []string,
392+
) (content string) {
393+
if commentType == SnippetComment {
394+
return outputwriter.GenerateReviewCommentContent(outputwriter.SnippetReviewContent(
395+
violation,
396+
writer,
397+
licenses,
398+
externalReferences,
399+
), writer)
400+
}
401+
return
402+
}
403+
287404
func createPullRequestDiff(location formats.Location) vcsclient.PullRequestDiff {
288405
return vcsclient.PullRequestDiff{
289406
OriginalFilePath: location.File,

0 commit comments

Comments
 (0)