@@ -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
187192func 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+
224323type 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+
287404func createPullRequestDiff (location formats.Location ) vcsclient.PullRequestDiff {
288405 return vcsclient.PullRequestDiff {
289406 OriginalFilePath : location .File ,
0 commit comments