diff --git a/infrastructure/code/code_html.go b/infrastructure/code/code_html.go index 8aad75650..ec5b29a66 100644 --- a/infrastructure/code/code_html.go +++ b/infrastructure/code/code_html.go @@ -27,10 +27,9 @@ import ( "strings" "time" - "github.com/gomarkdown/markdown" - "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/html" "github.com/snyk/snyk-ls/internal/product" ) @@ -66,8 +65,8 @@ var globalTemplate *template.Template func init() { funcMap := template.FuncMap{ "repoName": getRepoName, - "trimCWEPrefix": trimCWEPrefix, - "idxMinusOne": idxMinusOne, + "trimCWEPrefix": html.TrimCWEPrefix, + "idxMinusOne": html.IdxMinusOne, } var err error @@ -92,9 +91,9 @@ func getCodeDetailsHtml(issue snyk.Issue) string { "IssueTitle": additionalData.Title, "IssueMessage": additionalData.Message, "IssueType": getIssueType(additionalData), - "SeverityIcon": getSeverityIconSvg(issue), + "SeverityIcon": html.GetSeverityIconSvg(issue), "CWEs": issue.CWEs, - "IssueOverview": markdownToHTML(additionalData.Text), + "IssueOverview": html.MarkdownToHTML(additionalData.Text), "IsIgnored": issue.IsIgnored, "DataFlow": additionalData.DataFlow, "DataFlowTable": prepareDataFlowTable(additionalData), @@ -105,7 +104,7 @@ func getCodeDetailsHtml(issue snyk.Issue) string { "PriorityScore": additionalData.PriorityScore, "SnykWebUrl": config.CurrentConfig().SnykUi(), "LessonUrl": issue.LessonUrl, - "LessonIcon": getLessonIconSvg(), + "LessonIcon": html.GetLessonIconSvg(), "IgnoreLineAction": getLineToIgnoreAction(issue), "HasAIFix": additionalData.HasAIFix, "ExternalIcon": getExternalIconSvg(), @@ -132,23 +131,10 @@ func getCodeDetailsHtml(issue snyk.Issue) string { return html.String() } -func markdownToHTML(md string) template.HTML { - html := markdown.ToHTML([]byte(md), nil, nil) - return template.HTML(html) -} - func getLineToIgnoreAction(issue snyk.Issue) int { return issue.Range.Start.Line + 1 } -func idxMinusOne(n int) int { - return n - 1 -} - -func trimCWEPrefix(cwe string) string { - return strings.TrimPrefix(cwe, "CWE-") -} - func prepareIgnoreDetailsRow(ignoreDetails *snyk.IgnoreDetails) []IgnoreDetail { return []IgnoreDetail{ {"Category", parseCategory(ignoreDetails.Category)}, @@ -279,33 +265,6 @@ func getExternalIconSvg() template.HTML { `) } -func getSeverityIconSvg(issue snyk.Issue) template.HTML { - switch issue.Severity { - case snyk.Critical: - return template.HTML(` - - - `) - case snyk.High: - return template.HTML(` - - - `) - case snyk.Medium: - return template.HTML(` - - - `) - case snyk.Low: - return template.HTML(` - - - `) - default: - return `` - } -} - func getGitHubIconSvg() template.HTML { return template.HTML(` `) } -func getLessonIconSvg() template.HTML { - return template.HTML(` - - - `) -} - func getScanAnimationSvg() template.HTML { return template.HTML(` diff --git a/infrastructure/oss/issue_html.go b/infrastructure/oss/issue_html.go index 198741cdf..67f839266 100644 --- a/infrastructure/oss/issue_html.go +++ b/infrastructure/oss/issue_html.go @@ -17,145 +17,171 @@ package oss import ( + "bytes" _ "embed" "fmt" + "html/template" "strings" "github.com/gomarkdown/markdown" - "golang.org/x/exp/maps" "github.com/snyk/snyk-ls/application/config" "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/html" + "github.com/snyk/snyk-ls/internal/product" ) //go:embed template/details.html var detailsHtmlTemplate string -func replaceVariableInHtml(html string, variableName string, variableValue string) string { - return strings.ReplaceAll(html, fmt.Sprintf("${%s}", variableName), variableValue) -} +var globalTemplate *template.Template -func getIdentifiers(id string, issue snyk.OssIssueData) string { - identifierList := []string{""} +func init() { + funcMap := template.FuncMap{ + "trimCWEPrefix": html.TrimCWEPrefix, + "idxMinusOne": html.IdxMinusOne, + "join": join, + } - issueTypeString := "Vulnerability" - if len(issue.License) > 0 { - issueTypeString = "License" + var err error + globalTemplate, err = template.New(string(product.ProductOpenSource)).Funcs(funcMap).Parse(detailsHtmlTemplate) + if err != nil { + config.CurrentConfig().Logger().Error().Msgf("Failed to parse details template: %s", err) } +} + +func join(sep string, s []string) string { + return strings.Join(s, sep) +} - for _, id := range issue.Identifiers.CVE { - url := "https://cve.mitre.org/cgi-bin/cvename.cgi?name=" + id - htmlAnchor := fmt.Sprintf("%s", url, id) - identifierList = append(identifierList, htmlAnchor) +func getDetailsHtml(issue snyk.Issue) string { + additionalData, ok := issue.AdditionalData.(snyk.OssIssueData) + if !ok { + config.CurrentConfig().Logger().Error().Msg("Failed to cast additional data to OssIssueData") + return "" } + overview := markdown.ToHTML([]byte(additionalData.Description), nil, nil) - for _, id := range issue.Identifiers.CWE { - linkId := strings.ReplaceAll(strings.ToUpper(id), "CWE-", "") - htmlAnchor := fmt.Sprintf("%s", linkId, id) - identifierList = append(identifierList, htmlAnchor) + data := map[string]interface{}{ + "IssueId": issue.ID, + "IssueName": additionalData.Name, + "IssueTitle": additionalData.Title, + "IssueType": getIssueType(additionalData), + "SeverityText": issue.Severity.String(), + "SeverityIcon": html.GetSeverityIconSvg(issue), + "VulnerableModule": additionalData.Name, + "IssueOverview": html.MarkdownToHTML(string(overview)), + "CVEs": additionalData.Identifiers.CVE, + "CWEs": additionalData.Identifiers.CWE, + "CvssScore": fmt.Sprintf("%.1f", additionalData.CvssScore), + "ExploitMaturity": getExploitMaturity(additionalData), + "IntroducedThroughs": getIntroducedThroughs(additionalData), + "LessonUrl": additionalData.Lesson, + "LessonIcon": html.GetLessonIconSvg(), + "FixedIn": additionalData.FixedIn, + "DetailedPaths": getDetailedPaths(additionalData), } - if issue.CvssScore > 0 { - htmlAnchor := fmt.Sprintf("CVSS %.1f", issue.CvssScore) - identifierList = append(identifierList, htmlAnchor) + var html bytes.Buffer + if err := globalTemplate.Execute(&html, data); err != nil { + config.CurrentConfig().Logger().Error().Msgf("Failed to execute main details template: %v", err) + return "" } - htmlAnchor := fmt.Sprintf("%s", id, strings.ToUpper(id)) - identifierList = append(identifierList, htmlAnchor) + return html.String() +} + +func getIssueType(issue snyk.OssIssueData) string { + if len(issue.License) > 0 { + return "License" + } - return fmt.Sprintf("%s %s", issueTypeString, strings.Join(identifierList, " ")) + return "Vulnerability" } func getExploitMaturity(issue snyk.OssIssueData) string { if len(issue.Exploit) > 0 { - return fmt.Sprintf("
Exploit maturity
"+ - "
%s
", issue.Exploit) + return issue.Exploit } else { return "" } } -func getIntroducedBy(issue snyk.OssIssueData) string { - m := make(map[string]string) +type IntroducedThrough struct { + SnykUI string + PackageManager string + Module string +} + +func getIntroducedThroughs(issue snyk.OssIssueData) []IntroducedThrough { + introducedThroughs := []IntroducedThrough{} + snykUi := config.CurrentConfig().SnykUi() if len(issue.From) > 0 { for _, v := range issue.MatchingIssues { if len(v.From) > 1 { - module := v.From[1] - htmlAnchor := getVulnHtmlAnchor(issue.PackageManager, module) - m[module] = htmlAnchor + introducedThroughs = append(introducedThroughs, IntroducedThrough{ + SnykUI: snykUi, + PackageManager: issue.PackageManager, + Module: v.From[1], + }) } } - - return fmt.Sprintf("
Introduced through
"+ - "
%s
", strings.Join(maps.Values(m), ", ")) - } else { - return "" } + return introducedThroughs } -func getVulnHtmlAnchor(packageManager string, module string) string { - snykUi := config.CurrentConfig().SnykUi() - return fmt.Sprintf("%s", snykUi, packageManager, module) +type DetailedPath struct { + From []string + Remediation string } -func getLearnLink(issue snyk.OssIssueData) string { - if issue.Lesson == "" { - return "" - } +func getDetailedPaths(issue snyk.OssIssueData) []DetailedPath { + var detailedPaths = make([]DetailedPath, len(issue.MatchingIssues)) - return fmt.Sprintf("Learn about this vulnerability", - issue.Lesson) -} + for i, matchingIssue := range issue.MatchingIssues { + remediationAdvice := getRemediationAdvice(matchingIssue) -func getFixedIn(issue snyk.OssIssueData) string { - if len(issue.FixedIn) == 0 { - return "Not fixed" + detailedPaths[i] = DetailedPath{ + From: matchingIssue.From, + Remediation: remediationAdvice, + } } - - result := "%s@%v" - return fmt.Sprintf(result, issue.Name, strings.Join(issue.FixedIn, ", ")) + return detailedPaths } -func getDetailedPaths(issue snyk.OssIssueData) string { - detailedPathHtml := "" - - for _, matchingIssue := range issue.MatchingIssues { - remediationAdvice := matchingIssue.Remediation - introducedThrough := strings.Join(matchingIssue.From, " > ") - - detailedPathHtml += fmt.Sprintf(`
-
Introduced through
-
%s
-
-
-
Remediation
-
%s
-
`, introducedThrough, remediationAdvice) - } +func getRemediationAdvice(issue snyk.OssIssueData) string { + hasUpgradePath := len(issue.UpgradePath) > 1 + isOutdated := hasUpgradePath && issue.UpgradePath[1] == issue.From[1] + remediationAdvice := "No remediation advice available" + upgradeMessage := "" + if issue.IsUpgradable || issue.IsPatchable { + if hasUpgradePath { + upgradeMessage = "Upgrade to " + issue.UpgradePath[1].(string) + } - return detailedPathHtml + if isOutdated { + if issue.IsPatchable { + remediationAdvice = upgradeMessage + } else { + remediationAdvice = getOutdatedDependencyMessage(issue) + } + } else { + remediationAdvice = upgradeMessage + } + } + return remediationAdvice } -func getDetailsHtml(issue snyk.Issue) string { - additionalData, ok := issue.AdditionalData.(snyk.OssIssueData) - if !ok { - config.CurrentConfig().Logger().Error().Msg("Failed to cast additional data to OssIssueData") - return "" - } - overview := markdown.ToHTML([]byte(additionalData.Description), nil, nil) +func getOutdatedDependencyMessage(issue snyk.OssIssueData) string { + remediationAdvice := fmt.Sprintf("Your dependencies are out of date, "+ + "otherwise you would be using a newer %s than %s@%s. ", issue.Name, issue.Name, issue.Version) - html := replaceVariableInHtml(detailsHtmlTemplate, "issueId", issue.ID) - html = replaceVariableInHtml(html, "issueTitle", additionalData.Title) - html = replaceVariableInHtml(html, "severityText", issue.Severity.String()) - html = replaceVariableInHtml(html, "vulnerableModule", additionalData.Name) - html = replaceVariableInHtml(html, "overview", string(overview)) - html = replaceVariableInHtml(html, "identifiers", getIdentifiers(issue.ID, additionalData)) - html = replaceVariableInHtml(html, "exploitMaturity", getExploitMaturity(additionalData)) - html = replaceVariableInHtml(html, "introducedThrough", getIntroducedBy(additionalData)) - html = replaceVariableInHtml(html, "learnLink", getLearnLink(additionalData)) - html = replaceVariableInHtml(html, "fixedIn", getFixedIn(additionalData)) - html = replaceVariableInHtml(html, "detailedPaths", getDetailedPaths(additionalData)) - - return html + if issue.PackageManager == "npm" || issue.PackageManager == "yarn" || issue.PackageManager == "yarn-workspace" { + remediationAdvice += "Try relocking your lockfile or deleting node_modules and reinstalling" + + " your dependencies. If the problem persists, one of your dependencies may be bundling outdated modules." + } else { + remediationAdvice += "Try reinstalling your dependencies. If the problem persists, one of your dependencies may be bundling outdated modules." + } + return remediationAdvice } diff --git a/infrastructure/oss/issue_html_test.go b/infrastructure/oss/issue_html_test.go index ed7571e79..db710ee0e 100644 --- a/infrastructure/oss/issue_html_test.go +++ b/infrastructure/oss/issue_html_test.go @@ -31,7 +31,7 @@ import ( func Test_OssDetailsPanel_html_noLearn(t *testing.T) { _ = testutil.UnitTest(t) - expectedVariables := []string{"${headerEnd}", "${cspSource}", "${nonce}", "${severityIcon}", "${learnIcon}"} + expectedVariables := []string{"${headerEnd}", "${cspSource}", "${ideStyle}", "${nonce}"} slices.Sort(expectedVariables) issueAdditionalData := snyk.OssIssueData{ @@ -72,8 +72,8 @@ func Test_OssDetailsPanel_html_noLearn(t *testing.T) { assert.True(t, strings.Contains(issueDetailsPanelHtml, issue.ID)) assert.True(t, strings.Contains(issueDetailsPanelHtml, issueAdditionalData.Title)) assert.True(t, strings.Contains(issueDetailsPanelHtml, issue.Severity.String())) - assert.True(t, strings.Contains(issueDetailsPanelHtml, strings.Join(issueAdditionalData.From, " > "))) - assert.True(t, strings.Contains(issueDetailsPanelHtml, strings.Join(issue2.From, " > "))) + assert.True(t, strings.Contains(issueDetailsPanelHtml, strings.Join(issueAdditionalData.From, " > "))) + assert.True(t, strings.Contains(issueDetailsPanelHtml, strings.Join(issue2.From, " > "))) assert.True(t, strings.Contains(issueDetailsPanelHtml, "
  • list
  • ")) assert.False(t, strings.Contains(issueDetailsPanelHtml, "Learn about this vulnerability")) } diff --git a/infrastructure/oss/template/details.html b/infrastructure/oss/template/details.html index cda3bbbef..c04a833dc 100644 --- a/infrastructure/oss/template/details.html +++ b/infrastructure/oss/template/details.html @@ -19,7 +19,9 @@ - + + + ${headerEnd} + + ${ideStyle} +
    - ${severityText} + {{.SeverityIcon}}
    -
    ${issueTitle}
    -
    ${identifiers}
    -
    - learnIcon - ${learnLink} +
    {{.IssueTitle}}
    +
    + {{.IssueType}} + {{if gt (len .CVEs) 0}} + + {{range $index, $cve := .CVEs}} + {{$cve}} + {{if ne $index (idxMinusOne (len $.CVEs))}}{{end}} + {{end}} + {{end}} + + {{if gt (len .CWEs) 0}} + + {{range $index, $cwe := .CWEs}} + {{$cwe}} + {{if ne $index (idxMinusOne (len $.CWEs))}}{{end}} + {{end}} + {{end}} + + {{if gt (len .CvssScore) 0}} + + CVSS {{.CvssScore}} + {{end}} + + + {{.IssueId}}
    + {{ if .LessonUrl }} +
    + {{.LessonIcon}} + Learn about this vulnerability +
    + {{end}}
    Vulnerable module
    -
    ${vulnerableModule}
    +
    {{.VulnerableModule}}
    +
    + {{range .IntroducedThroughs}} +
    +
    Introduced through
    +
    - ${introducedThrough} + {{end}}
    Fixed in
    -
    ${fixedIn}
    +
    + {{ if eq (len .FixedIn) 0 }} + Not fixed + {{else}} + {{.IssueName}}@{{ join ", " .FixedIn }} + {{end}} +
    +
    + {{ if .ExploitMaturity }} +
    +
    Exploit maturity
    +
    {{.ExploitMaturity}}
    - ${exploitMaturity} + {{end}}

    Detailed paths

    -
    ${detailedPaths}
    +
    + {{range .DetailedPaths}} +
    +
    Introduced through
    +
    {{join " > " .From}}
    +
    +
    +
    Remediation
    +
    {{.Remediation}}
    +
    + {{end}} +
    -
    ${overview}
    +
    + {{.IssueOverview}} +