Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: send OSS scan issues #388

Merged
merged 9 commits into from
Oct 20, 2023
45 changes: 45 additions & 0 deletions application/server/notification/scan_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package notification

import (
"errors"
"strconv"

"github.com/snyk/snyk-ls/domain/ide/notification"
"github.com/snyk/snyk-ls/domain/snyk"
Expand All @@ -12,6 +13,7 @@ import (
var enabledProducts = map[product.Product]bool{
product.ProductCode: true,
product.ProductInfrastructureAsCode: true,
product.ProductOpenSource: true,
}

type scanNotifier struct {
Expand Down Expand Up @@ -77,6 +79,8 @@ func (n *scanNotifier) sendSuccess(pr product.Product, folderPath string, issues
scanIssues = n.appendIacIssues(scanIssues, folderPath, issues)
} else if pr == product.ProductCode {
scanIssues = n.appendCodeIssues(scanIssues, folderPath, issues)
} else if pr == product.ProductOpenSource {
scanIssues = n.appendOssIssues(scanIssues, folderPath, issues)
}

n.notifier.Send(
Expand All @@ -89,6 +93,47 @@ func (n *scanNotifier) sendSuccess(pr product.Product, folderPath string, issues
)
}

func (n *scanNotifier) appendOssIssues(scanIssues []lsp.ScanIssue, folderPath string, issues []snyk.Issue) []lsp.ScanIssue {
for _, issue := range issues {
additionalData, ok := issue.AdditionalData.(snyk.OssIssueData)
if !ok {
continue // skip non-oss issues
}

scanIssues = append(scanIssues, lsp.ScanIssue{
Id: additionalData.Key,
Title: additionalData.Title,
Severity: issue.Severity.String(),
FilePath: issue.AffectedFilePath,
AdditionalData: lsp.OssIssueData{
License: additionalData.License,
Identifiers: lsp.OssIdentifiers{
CWE: issue.CWEs,
CVE: issue.CVEs,
},
Description: additionalData.Description,
Language: additionalData.Language,
PackageManager: additionalData.PackageManager,
PackageName: additionalData.PackageName,
Name: additionalData.Name,
Version: additionalData.Version,
Exploit: additionalData.Exploit,
CVSSv3: additionalData.CVSSv3,
CvssScore: strconv.FormatFloat(additionalData.CvssScore, 'f', 2, 64), // convert float64 to string with 2 decimal places
FixedIn: additionalData.FixedIn,
From: additionalData.From,
UpgradePath: additionalData.UpgradePath,
IsPatchable: additionalData.IsPatchable,
IsUpgradable: additionalData.IsUpgradable,
ProjectName: additionalData.ProjectName,
DisplayTargetFile: additionalData.DisplayTargetFile,
},
})
}

return scanIssues
}

func (n *scanNotifier) appendIacIssues(scanIssues []lsp.ScanIssue, folderPath string, issues []snyk.Issue) []lsp.ScanIssue {
for _, issue := range issues {
additionalData, ok := issue.AdditionalData.(snyk.IaCIssueData)
Expand Down
114 changes: 113 additions & 1 deletion application/server/notification/scan_notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,118 @@ func Test_SendSuccess_SendsForAllEnabledProducts(t *testing.T) {
}
}

func Test_SendSuccess_SendsForOpenSource(t *testing.T) {
testutil.UnitTest(t)

mockNotifier := notification.NewMockNotifier()
scanNotifier, _ := notification2.NewScanNotifier(mockNotifier)

const folderPath = "/test/oss/folderPath"

expectedUIScanIssue := []lsp2.ScanIssue{
{
Id: "OSS Key",
Title: "OSS Title",
Severity: "critical",
FilePath: "ossAffectedFilePath",
AdditionalData: lsp2.OssIssueData{
License: "OSS License",
Identifiers: lsp2.OssIdentifiers{
CWE: []string{"CWE-184"},
CVE: []string{"CVE-2023-45133"},
},
Description: "OSS Description",
Language: "js",
PackageManager: "OSS PackageManager",
PackageName: "OSS PackageName",
Name: "OSS Name",
Version: "OSS Version",
Exploit: "OSS Exploit",
CVSSv3: "OSS CVSSv3",
CvssScore: "9.90",
FixedIn: []string{},
From: []string{"babel/[email protected]"},
UpgradePath: []any{
true,
"[email protected]",
},
IsPatchable: false,
IsUpgradable: false,
ProjectName: "OSS ProjectName",
DisplayTargetFile: "OSS DisplayTargetFile",
},
},
}

issues := []snyk.Issue{
{ // OSS issue
ID: "SNYK-JS-BABELTRAVERSE-5962463",
Severity: snyk.Critical,
IssueType: 1,
Range: snyk.Range{
Start: snyk.Position{
Line: 1,
Character: 1,
},
End: snyk.Position{
Line: 1,
Character: 2,
},
},
Message: "Incomplete List of Disallowed Inputs",
FormattedMessage: "Incomplete List of Disallowed Inputs",
AffectedFilePath: "ossAffectedFilePath",
Product: product.ProductOpenSource,
References: []snyk.Reference{},
IssueDescriptionURL: &url.URL{},
CodeActions: []snyk.CodeAction{},
CodelensCommands: []snyk.CommandData{},
Ecosystem: "OSS Ecosystem",
CWEs: []string{"CWE-184"},
CVEs: []string{"CVE-2023-45133"},
AdditionalData: snyk.OssIssueData{
Key: "OSS Key",
Title: "OSS Title",
Name: "OSS Name",
LineNumber: 1,
Description: "OSS Description",
References: []snyk.Reference{},
Version: "OSS Version",
License: "OSS License",
PackageManager: "OSS PackageManager",
PackageName: "OSS PackageName",
From: []string{"babel/[email protected]"},
FixedIn: []string{},
UpgradePath: []any{
true,
"[email protected]",
},
IsUpgradable: false,
CVSSv3: "OSS CVSSv3",
CvssScore: 9.9,
Exploit: "OSS Exploit",
IsPatchable: false,
ProjectName: "OSS ProjectName",
DisplayTargetFile: "OSS DisplayTargetFile",
Language: "js",
},
},
}

// Act - run the test
scanNotifier.SendSuccess(product.ProductOpenSource, folderPath, issues)

// Assert - check that there are messages sent
assert.NotEmpty(t, mockNotifier.SentMessages())

// Assert - check the messages matches the expected message for each product
for _, msg := range mockNotifier.SentMessages() {
actualUIOssIssue := msg.(lsp2.SnykScanParams).Issues
assert.Equal(t, expectedUIScanIssue, actualUIOssIssue)
return
}
}

func Test_SendSuccess_SendsForSnykCode(t *testing.T) {
testutil.UnitTest(t)

Expand Down Expand Up @@ -380,7 +492,7 @@ func Test_SendInProgress_SendsForAllEnabledProducts(t *testing.T) {
scanNotifier.SendInProgress("/test/folderPath")

// Assert
assert.Equal(t, 2, len(mockNotifier.SentMessages()))
assert.Equal(t, 3, len(mockNotifier.SentMessages()))
}

func containsMatchingMessage(t *testing.T,
Expand Down
24 changes: 24 additions & 0 deletions domain/snyk/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ type MarkerPosition struct {
File string `json:"file"`
}

type OssIssueData struct {
Key string `json:"key"`
Title string `json:"title"`
Name string `json:"name"`
LineNumber int `json:"lineNumber"`
Description string `json:"description"`
References []Reference `json:"references,omitempty"`
Version string `json:"version"`
License string `json:"license,omitempty"`
PackageManager string `json:"packageManager"`
PackageName string `json:"packageName"`
From []string `json:"from"`
FixedIn []string `json:"fixedIn,omitempty"`
UpgradePath []any `json:"upgradePath,omitempty"`
IsUpgradable bool `json:"isUpgradable,omitempty"`
CVSSv3 string `json:"CVSSv3,omitempty"`
CvssScore float64 `json:"cvssScore,omitempty"`
Exploit string `json:"exploit,omitempty"`
IsPatchable bool `json:"isPatchable"`
ProjectName string `json:"projectName"`
DisplayTargetFile string `json:"displayTargetFile"`
Language string `json:"language"`
}

type IaCIssueData struct {
// Unique key identifying an issue in the whole result set
Key string `json:"key"`
Expand Down
58 changes: 57 additions & 1 deletion infrastructure/oss/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
package oss

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/gomarkdown/markdown"
Expand Down Expand Up @@ -162,6 +165,7 @@ func (i *ossIssue) ToIssueSeverity() snyk.Severity {
func toIssue(
affectedFilePath string,
issue ossIssue,
scanResult *scanResult,
issueRange snyk.Range,
learnService learn.Service,
ep error_reporting.ErrorReporter,
Expand Down Expand Up @@ -204,6 +208,53 @@ func toIssue(
Ecosystem: issue.PackageManager,
CWEs: issue.Identifiers.CWE,
CVEs: issue.Identifiers.CVE,
AdditionalData: issue.toAdditionalData(affectedFilePath, scanResult),
}
}

func (o ossIssue) toAdditionalData(filepath string, scanResult *scanResult) snyk.OssIssueData {
var additionalData snyk.OssIssueData
additionalData.Key = getIssueKey(filepath, o)
additionalData.Title = o.Title
additionalData.Name = o.Name
additionalData.LineNumber = o.LineNumber
additionalData.Description = o.Description
additionalData.References = o.toReferences()
additionalData.Version = o.Version
additionalData.License = o.License
additionalData.PackageManager = o.PackageManager
additionalData.PackageName = o.PackageName
additionalData.From = o.From
additionalData.FixedIn = o.FixedIn
additionalData.UpgradePath = o.UpgradePath
additionalData.IsUpgradable = o.IsUpgradable
additionalData.CVSSv3 = o.CVSSv3
additionalData.CvssScore = o.CvssScore
additionalData.Exploit = o.Exploit
additionalData.IsPatchable = o.IsPatchable
additionalData.ProjectName = scanResult.ProjectName
additionalData.DisplayTargetFile = scanResult.DisplayTargetFile
additionalData.Language = o.Language

return additionalData
}

func (o ossIssue) toReferences() []snyk.Reference {
var references []snyk.Reference
for _, ref := range o.References {
references = append(references, ref.toReference())
}
return references
}

func (r reference) toReference() snyk.Reference {
url, err := url.Parse(string(r.Url))
if err != nil {
log.Err(err).Msg("Unable to parse reference url: " + string(r.Url))
}
return snyk.Reference{
Url: url,
Title: r.Title,
}
}

Expand All @@ -226,10 +277,15 @@ func convertScanResultToIssues(
continue
}
issueRange := findRange(issue, path, fileContent)
snykIssue := toIssue(path, issue, issueRange, ls, ep)
snykIssue := toIssue(path, issue, res, issueRange, ls, ep)
packageIssueCache[packageKey] = append(packageIssueCache[packageKey], snykIssue)
issues = append(issues, snykIssue)
duplicateCheckMap[duplicateKey] = true
}
return issues
}

func getIssueKey(affectedFilePath string, issue ossIssue) string {
id := sha256.Sum256([]byte(affectedFilePath + strconv.Itoa(issue.LineNumber) + issue.Id))
return hex.EncodeToString(id[:16])
}
2 changes: 1 addition & 1 deletion infrastructure/oss/oss_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func Test_toIssue_LearnParameterConversion(t *testing.T) {
learnService: getLearnMock(t),
}

issue := toIssue("testPath", ossIssue, snyk.Range{}, scanner.learnService, scanner.errorReporter)
issue := toIssue("testPath", ossIssue, &scanResult{}, snyk.Range{}, scanner.learnService, scanner.errorReporter)

assert.Equal(t, ossIssue.Id, issue.ID)
assert.Equal(t, ossIssue.Identifiers.CWE, issue.CWEs)
Expand Down
6 changes: 6 additions & 0 deletions infrastructure/oss/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ type ossIssue struct {
FixedIn []string `json:"fixedIn,omitempty"`
UpgradePath []any `json:"upgradePath,omitempty"`
IsUpgradable bool `json:"isUpgradable,omitempty"`
CVSSv3 string `json:"CVSSv3,omitempty"`
CvssScore float64 `json:"cvssScore,omitempty"`
Exploit string `json:"exploit,omitempty"`
IsPatchable bool `json:"isPatchable"`
License string `json:"license,omitempty"`
Language string `json:"language,omitempty"`
}

type licensesPolicy struct {
Expand Down
27 changes: 27 additions & 0 deletions internal/lsp/message_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,33 @@ type ScanIssue struct { // TODO - convert this to a generic type
AdditionalData any `json:"additionalData,omitempty"`
}

// Snyk Open Source
type OssIssueData struct {
License string `json:"license,omitempty"`
Identifiers OssIdentifiers `json:"identifiers,omitempty"`
Description string `json:"description"`
Language string `json:"language"`
PackageManager string `json:"packageManager"`
PackageName string `json:"packageName"`
Name string `json:"name"`
Version string `json:"version"`
Exploit string `json:"exploit,omitempty"`
CVSSv3 string `json:"CVSSv3,omitempty"`
CvssScore string `json:"cvssScore,omitempty"`
FixedIn []string `json:"fixedIn,omitempty"`
From []string `json:"from"`
UpgradePath []any `json:"upgradePath"`
IsPatchable bool `json:"isPatchable"`
IsUpgradable bool `json:"isUpgradable"`
ProjectName string `json:"projectName"`
DisplayTargetFile string `json:"displayTargetFile"`
}

type OssIdentifiers struct {
CWE []string `json:"CWE,omitempty"`
CVE []string `json:"CVE,omitempty"`
}

type CodeIssueData struct {
Message string `json:"message"`
LeadURL string `json:"leadURL,omitempty"`
Expand Down