@@ -18,6 +18,7 @@ package oss
1818
1919import (
2020 "fmt"
21+ "net/url"
2122 "os"
2223 "strings"
2324
@@ -209,3 +210,208 @@ func (i *ossIssue) getUpgradedPathParts() (string, string, error) {
209210 depVersion := rootDependencyUpgrade [len (rootDependencyUpgrade )- 1 ]
210211 return depName , depVersion , nil
211212}
213+
214+ // AddCodeActionsFromOssIssueData generates code actions from OssIssueData directly
215+ // This avoids the need to convert OssIssueData back to ossIssue for unified flow issues
216+ func AddCodeActionsFromOssIssueData (
217+ ossData snyk.OssIssueData ,
218+ issueId string ,
219+ learnService learn.Service ,
220+ ep error_reporting.ErrorReporter ,
221+ affectedFilePath types.FilePath ,
222+ issueDepNode * ast.Node ,
223+ ) (actions []types.CodeAction ) {
224+ c := config .CurrentConfig ()
225+ if issueDepNode == nil {
226+ c .Logger ().Debug ().Str ("issue" , issueId ).Msg ("skipping adding code action, as issueDepNode is empty" )
227+ return actions
228+ }
229+
230+ // let's see if we can offer a quickfix here
231+ // value has the version information, so if it's empty, we'll need to look at the parent
232+ var quickFixAction types.CodeAction
233+ if issueDepNode .Tree != nil && issueDepNode .Value == "" {
234+ fixNode := issueDepNode .LinkedParentDependencyNode
235+ if fixNode != nil {
236+ quickFixAction = AddQuickFixActionFromOssIssueData (ossData , types .FilePath (fixNode .Tree .Document ), getRangeFromNode (fixNode ), []byte (fixNode .Tree .Root .Value ), true )
237+ }
238+ } else {
239+ quickFixAction = AddQuickFixActionFromOssIssueData (ossData , affectedFilePath , getRangeFromNode (issueDepNode ), nil , false )
240+ }
241+ if quickFixAction != nil {
242+ actions = append (actions , quickFixAction )
243+ }
244+
245+ if c .IsSnykOpenBrowserActionEnabled () {
246+ title := fmt .Sprintf ("Open description of '%s affecting package %s' in browser (Snyk)" , ossData .Title , ossData .PackageName )
247+ issueURL := createIssueURLFromId (issueId )
248+ command := & types.CommandData {
249+ Title : title ,
250+ CommandId : types .OpenBrowserCommand ,
251+ Arguments : []any {issueURL .String ()},
252+ }
253+
254+ action , err := snyk .NewCodeAction (title , nil , command )
255+ if err != nil {
256+ c .Logger ().Err (err ).Msgf ("could not create code action %s" , title )
257+ } else {
258+ actions = append (actions , action )
259+ }
260+ }
261+
262+ codeAction := AddSnykLearnActionFromOssIssueData (ossData , issueId , learnService , ep )
263+ if codeAction != nil {
264+ actions = append (actions , codeAction )
265+ }
266+
267+ return actions
268+ }
269+
270+ // AddSnykLearnActionFromOssIssueData creates a Snyk Learn code action from OssIssueData
271+ func AddSnykLearnActionFromOssIssueData (ossData snyk.OssIssueData , issueId string , learnService learn.Service , ep error_reporting.ErrorReporter ) (action types.CodeAction ) {
272+ if config .CurrentConfig ().IsSnykLearnCodeActionsEnabled () {
273+ lesson , err := learnService .GetLesson (ossData .PackageManager , issueId , ossData .Identifiers .CWE , ossData .Identifiers .CVE , types .DependencyVulnerability )
274+ if err != nil {
275+ msg := "failed to get lesson"
276+ config .CurrentConfig ().Logger ().Err (err ).Msg (msg )
277+ ep .CaptureError (errors .WithMessage (err , msg ))
278+ return nil
279+ }
280+
281+ if lesson != nil && lesson .Url != "" {
282+ title := fmt .Sprintf ("Learn more about %s (Snyk)" , ossData .Title )
283+ action = & snyk.CodeAction {
284+ Title : title ,
285+ OriginalTitle : title ,
286+ Command : & types.CommandData {
287+ Title : title ,
288+ CommandId : types .OpenBrowserCommand ,
289+ Arguments : []any {lesson .Url },
290+ },
291+ }
292+ config .CurrentConfig ().Logger ().Debug ().Str ("method" , "oss.AddSnykLearnActionFromOssIssueData" ).Msgf ("Learn action: %v" , action )
293+ }
294+ }
295+ return action
296+ }
297+
298+ // AddQuickFixActionFromOssIssueData creates a quick-fix code action from OssIssueData
299+ func AddQuickFixActionFromOssIssueData (ossData snyk.OssIssueData , affectedFilePath types.FilePath , issueRange types.Range , fileContent []byte , addFileNameToFixTitle bool ) types.CodeAction {
300+ logger := config .CurrentConfig ().Logger ().With ().Str ("method" , "oss.AddQuickFixActionFromOssIssueData" ).Logger ()
301+ if ! config .CurrentConfig ().IsSnykOSSQuickFixCodeActionsEnabled () {
302+ return nil
303+ }
304+ logger .Debug ().Msg ("create deferred quickfix code action" )
305+ filePathString := string (affectedFilePath )
306+ quickfixEdit := getQuickfixEditFromOssIssueData (ossData , affectedFilePath )
307+ if quickfixEdit == "" {
308+ return nil
309+ }
310+ upgradeMessage := "⚡️ Upgrade to " + quickfixEdit
311+ if addFileNameToFixTitle {
312+ upgradeMessage += " [ in file: " + filePathString + " ]"
313+ }
314+ autofixEditCallback := func () * types.WorkspaceEdit {
315+ edit := & types.WorkspaceEdit {}
316+ var err error
317+ if fileContent == nil {
318+ fileContent , err = os .ReadFile (filePathString )
319+ if err != nil {
320+ logger .Error ().Err (err ).Str ("file" , filePathString ).Msg ("could not open file" )
321+ return edit
322+ }
323+ }
324+
325+ singleTextEdit := types.TextEdit {
326+ Range : issueRange ,
327+ NewText : quickfixEdit ,
328+ }
329+ edit .Changes = make (map [string ][]types.TextEdit )
330+ edit .Changes [filePathString ] = []types.TextEdit {singleTextEdit }
331+ return edit
332+ }
333+
334+ // our grouping key for oss quickfixes is the dependency name
335+ groupingKey , groupingValue , err := getUpgradedPathPartsFromOssIssueData (ossData )
336+ if err != nil {
337+ logger .Warn ().Err (err ).Msg ("could not get the upgrade path, so cannot add quickfix." )
338+ return nil
339+ }
340+
341+ action , err := snyk .NewDeferredCodeAction (upgradeMessage , & autofixEditCallback , nil , types .Key (groupingKey ), groupingValue )
342+ if err != nil {
343+ logger .Error ().Msg ("failed to create deferred quickfix code action" )
344+ return nil
345+ }
346+ return & action
347+ }
348+
349+ // getQuickfixEditFromOssIssueData generates the quickfix edit text from OssIssueData
350+ func getQuickfixEditFromOssIssueData (ossData snyk.OssIssueData , affectedFilePath types.FilePath ) string {
351+ logger := config .CurrentConfig ().Logger ().With ().Str ("method" , "oss.getQuickfixEditFromOssIssueData" ).Logger ()
352+ hasUpgradePath := len (ossData .UpgradePath ) > 1
353+ if ! hasUpgradePath {
354+ return ""
355+ }
356+
357+ // UpgradePath[0] is the upgrade for the package that was scanned
358+ // UpgradePath[1] is the upgrade for the root dependency
359+ depName , depVersion , err := getUpgradedPathPartsFromOssIssueData (ossData )
360+ if err != nil {
361+ logger .Warn ().Err (err ).Msg ("could not get the upgrade path, so cannot add quickfix." )
362+ return ""
363+ }
364+ if len (ossData .UpgradePath ) > 1 && len (ossData .From ) > 1 {
365+ logger .Debug ().Msgf ("comparing %s with %s" , ossData .UpgradePath [1 ], ossData .From [1 ])
366+ // from[1] contains the package that caused this issue
367+ normalizedCurrentVersion := strings .Split (ossData .From [1 ], "@" )[1 ]
368+ if semver .Compare ("v" + depVersion , "v" + normalizedCurrentVersion ) == 0 {
369+ logger .Warn ().Msg ("proposed upgrade version is the same version as the current, not adding quickfix" )
370+ return ""
371+ }
372+ }
373+ if ossData .PackageManager == "npm" || ossData .PackageManager == "yarn" || ossData .PackageManager == "yarn-workspace" {
374+ return fmt .Sprintf ("\" %s\" : \" %s\" " , depName , depVersion )
375+ } else if ossData .PackageManager == "maven" {
376+ depNameSplit := strings .Split (depName , ":" )
377+ depName = depNameSplit [len (depNameSplit )- 1 ]
378+ // TODO: remove once https://snyksec.atlassian.net/browse/OSM-1775 is fixed
379+ if strings .Contains (string (affectedFilePath ), "build.gradle" ) {
380+ return fmt .Sprintf ("%s:%s" , depName , depVersion )
381+ }
382+ return depVersion
383+ } else if ossData .PackageManager == "gradle" {
384+ depNameSplit := strings .Split (depName , ":" )
385+ depName = depNameSplit [len (depNameSplit )- 1 ]
386+ return fmt .Sprintf ("%s:%s" , depName , depVersion )
387+ }
388+ if ossData .PackageManager == "gomodules" {
389+ return fmt .Sprintf ("v%s" , depVersion )
390+ }
391+
392+ return ""
393+ }
394+
395+ // getUpgradedPathPartsFromOssIssueData extracts dependency name and version from OssIssueData
396+ func getUpgradedPathPartsFromOssIssueData (ossData snyk.OssIssueData ) (string , string , error ) {
397+ if len (ossData .UpgradePath ) < 2 {
398+ return "" , "" , errors .New ("upgrade path too short" )
399+ }
400+ s , ok := ossData .UpgradePath [1 ].(string )
401+ if ! ok {
402+ return "" , "" , errors .New ("invalid upgrade path, could not cast to string" )
403+ }
404+ rootDependencyUpgrade := strings .Split (s , "@" )
405+ depName := strings .Join (rootDependencyUpgrade [:len (rootDependencyUpgrade )- 1 ], "@" )
406+ depVersion := rootDependencyUpgrade [len (rootDependencyUpgrade )- 1 ]
407+ return depName , depVersion , nil
408+ }
409+
410+ // createIssueURLFromId creates an issue URL from an issue ID
411+ func createIssueURLFromId (issueId string ) * url.URL {
412+ parse , err := url .Parse ("https://snyk.io/vuln/" + issueId )
413+ if err != nil {
414+ config .CurrentConfig ().Logger ().Err (err ).Msg ("Unable to create issue link for issue:" + issueId )
415+ }
416+ return parse
417+ }
0 commit comments