From 152e25b3191cde40de11b205a5fcdca97c483c52 Mon Sep 17 00:00:00 2001 From: Teodora Sandu Date: Wed, 26 Jun 2024 14:23:49 +0100 Subject: [PATCH 1/4] feat: add ability to inject style directly --- infrastructure/oss/issue_html_test.go | 2 +- infrastructure/oss/template/details.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/oss/issue_html_test.go b/infrastructure/oss/issue_html_test.go index ed7571e79..2c6260dc4 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{"${ideStyle}", "${cspSource}", "${nonce}", "${severityIcon}", "${learnIcon}"} slices.Sort(expectedVariables) issueAdditionalData := snyk.OssIssueData{ diff --git a/infrastructure/oss/template/details.html b/infrastructure/oss/template/details.html index cda3bbbef..5c10a35b8 100644 --- a/infrastructure/oss/template/details.html +++ b/infrastructure/oss/template/details.html @@ -93,7 +93,7 @@ } - ${headerEnd} + ${ideStyle}
From de69c2d0ab4251cd97b3afe1866368d6f0d605e5 Mon Sep 17 00:00:00 2001 From: Teodora Sandu Date: Wed, 26 Jun 2024 16:16:59 +0100 Subject: [PATCH 2/4] refactor: use html/template --- infrastructure/code/code_html.go | 24 +-- infrastructure/oss/issue_html.go | 212 +++++++++++++---------- infrastructure/oss/issue_html_test.go | 8 +- infrastructure/oss/template/details.html | 92 ++++++++-- 4 files changed, 216 insertions(+), 120 deletions(-) diff --git a/infrastructure/code/code_html.go b/infrastructure/code/code_html.go index 8aad75650..a306ac721 100644 --- a/infrastructure/code/code_html.go +++ b/infrastructure/code/code_html.go @@ -66,8 +66,8 @@ var globalTemplate *template.Template func init() { funcMap := template.FuncMap{ "repoName": getRepoName, - "trimCWEPrefix": trimCWEPrefix, - "idxMinusOne": idxMinusOne, + "trimCWEPrefix": TrimCWEPrefix, + "idxMinusOne": IdxMinusOne, } var err error @@ -92,7 +92,7 @@ func getCodeDetailsHtml(issue snyk.Issue) string { "IssueTitle": additionalData.Title, "IssueMessage": additionalData.Message, "IssueType": getIssueType(additionalData), - "SeverityIcon": getSeverityIconSvg(issue), + "SeverityIcon": GetSeverityIconSvg(issue), "CWEs": issue.CWEs, "IssueOverview": markdownToHTML(additionalData.Text), "IsIgnored": issue.IsIgnored, @@ -105,7 +105,7 @@ func getCodeDetailsHtml(issue snyk.Issue) string { "PriorityScore": additionalData.PriorityScore, "SnykWebUrl": config.CurrentConfig().SnykUi(), "LessonUrl": issue.LessonUrl, - "LessonIcon": getLessonIconSvg(), + "LessonIcon": GetLessonIconSvg(), "IgnoreLineAction": getLineToIgnoreAction(issue), "HasAIFix": additionalData.HasAIFix, "ExternalIcon": getExternalIconSvg(), @@ -141,11 +141,11 @@ func getLineToIgnoreAction(issue snyk.Issue) int { return issue.Range.Start.Line + 1 } -func idxMinusOne(n int) int { +func IdxMinusOne(n int) int { return n - 1 } -func trimCWEPrefix(cwe string) string { +func TrimCWEPrefix(cwe string) string { return strings.TrimPrefix(cwe, "CWE-") } @@ -279,25 +279,25 @@ func getExternalIconSvg() template.HTML { `) } -func getSeverityIconSvg(issue snyk.Issue) template.HTML { +func GetSeverityIconSvg(issue snyk.Issue) template.HTML { switch issue.Severity { case snyk.Critical: - return template.HTML(` + return template.HTML(` `) case snyk.High: - return template.HTML(` + return template.HTML(` `) case snyk.Medium: - return template.HTML(` + return template.HTML(` `) case snyk.Low: - return template.HTML(` + return template.HTML(` `) @@ -316,7 +316,7 @@ func getGitHubIconSvg() template.HTML { `) } -func getLessonIconSvg() template.HTML { +func GetLessonIconSvg() template.HTML { return template.HTML(` diff --git a/infrastructure/oss/issue_html.go b/infrastructure/oss/issue_html.go index 198741cdf..cb6000c18 100644 --- a/infrastructure/oss/issue_html.go +++ b/infrastructure/oss/issue_html.go @@ -17,145 +17,175 @@ package oss import ( + "bytes" _ "embed" "fmt" - "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/infrastructure/code" + "github.com/snyk/snyk-ls/internal/product" + "html/template" + "strings" ) //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": code.TrimCWEPrefix, + "idxMinusOne": code.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": code.GetSeverityIconSvg(issue), + "VulnerableModule": additionalData.Name, + "IssueOverview": markdwonToHTML(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": code.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) +// TODO: common +func markdwonToHTML(md string) template.HTML { + html := markdown.ToHTML([]byte(md), nil, nil) + return template.HTML(html) +} + +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 2c6260dc4..ccfda413a 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{"${ideStyle}", "${cspSource}", "${nonce}", "${severityIcon}", "${learnIcon}"} + expectedVariables := []string{"${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")) } @@ -131,6 +131,6 @@ func Test_OssDetailsPanel_html_withLearn_withCustomEndpoint(t *testing.T) { issueAdditionalData.MatchingIssues = append(issueAdditionalData.MatchingIssues, issueAdditionalData) issueDetailsPanelHtml := getDetailsHtml(issue) - + assert.True(t, strings.Contains(issueDetailsPanelHtml, customEndpoint)) } diff --git a/infrastructure/oss/template/details.html b/infrastructure/oss/template/details.html index 5c10a35b8..39d22666a 100644 --- a/infrastructure/oss/template/details.html +++ b/infrastructure/oss/template/details.html @@ -19,7 +19,9 @@ - + + + ${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}} + | + {{.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}} +