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: refactor the HTML to use html/template and to inject styling for other IDEs [IDE-326] #564

Merged
merged 4 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 6 additions & 54 deletions infrastructure/code/code_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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(),
Expand All @@ -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)},
Expand Down Expand Up @@ -279,33 +265,6 @@ func getExternalIconSvg() template.HTML {
</svg>`)
}

func getSeverityIconSvg(issue snyk.Issue) template.HTML {
switch issue.Severity {
case snyk.Critical:
return template.HTML(`<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
<rect width="16" height="16" rx="2" fill="#AB1A1A"/>
<path d="M9.975 9.64h2.011a3.603 3.603 0 0 1-.545 1.743 3.24 3.24 0 0 1-1.338 1.19c-.57.284-1.256.427-2.06.427-.627 0-1.19-.107-1.688-.32a3.594 3.594 0 0 1-1.278-.936 4.158 4.158 0 0 1-.801-1.47C4.092 9.7 4 9.057 4 8.345v-.675c0-.712.094-1.356.283-1.93a4.255 4.255 0 0 1 .82-1.476 3.657 3.657 0 0 1 1.286-.936A4.114 4.114 0 0 1 8.057 3c.817 0 1.505.147 2.066.44.565.295 1.002.7 1.312 1.217.314.516.502 1.104.565 1.763H9.982c-.023-.392-.101-.723-.236-.995a1.331 1.331 0 0 0-.612-.621c-.27-.143-.628-.214-1.077-.214-.336 0-.63.062-.881.187a1.632 1.632 0 0 0-.633.568c-.17.254-.298.574-.383.962a6.61 6.61 0 0 0-.121 1.349v.688c0 .503.038.946.114 1.33.076.378.193.699.35.961.161.259.368.454.619.588.256.13.563.194.922.194.421 0 .769-.067 1.043-.2a1.39 1.39 0 0 0 .625-.595c.148-.263.236-.59.263-.982Z" fill="#fff"/>
</svg>`)
case snyk.High:
return template.HTML(`<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
<rect width="16" height="16" rx="2" fill="#CE5019"/>
<path d="M10.5 7v2h-5V7h5ZM6 3v10H4V3h2Zm6 0v10h-2V3h2Z" fill="#fff"/>
</svg>`)
case snyk.Medium:
return template.HTML(`<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
<rect width="16" height="16" rx="2" fill="#D68000"/>
<path d="M3 3h2l2.997 7.607L11 3h2L9 13H7L3 3Zm0 0h2v10l-2-.001V3.001Zm8 0h2V13h-2V3Z" fill="#fff"/>
</svg>`)
case snyk.Low:
return template.HTML(`<svg fill="none" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
<rect width="16" height="16" rx="2" fill="#88879E"/>
<path d="M11 11v2H6.705v-2H11ZM7 3v10H5V3h2Z" fill="#fff"/>
</svg>`)
default:
return ``
}
}

func getGitHubIconSvg() template.HTML {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering, if caching/memory will become an issue with 1000s of issues if we embed the svg like that. Do you know how Chrome handles this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean we might run into trouble ebecause we would be using a lot of memory if we cache the same SVG multiple times?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this and at the very beginning of this migration, when we were just supporting IntelliJ and writing the HTML and CSS from scratch, we embedded everything in one single HTML file, trading DRY for velocity.

Now that the project has grown and VSCode and IntelliJ are loading CSS rules anyway, I think we could start considering removing the CSS and SVGs from the Language Server and moving them to the IDEs as separate assets.

Benefits:

  1. Reduced Redundancy: Each vulnerability doesn't need to embed the same CSS and SVGs.
  2. Improved Performance: Smaller HTML payloads and better caching of static assets.

WDYT @teodora-sandu @bastiandoetsch

return template.HTML(`<svg class="tab-item-icon" width="18" height="16" viewBox="0 0 98 96" xmlns="http://www.w3.org/2000/svg">
<path
Expand All @@ -316,13 +275,6 @@ func getGitHubIconSvg() template.HTML {
</svg>`)
}

func getLessonIconSvg() template.HTML {
return template.HTML(`<svg width="17" height="14" viewBox="0 0 17 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.25 0L0 4.5L3 6.135V10.635L8.25 13.5L13.5 10.635V6.135L15 5.3175V10.5H16.5V4.5L8.25 0ZM13.365 4.5L8.25 7.29L3.135 4.5L8.25 1.71L13.365 4.5ZM12 9.75L8.25 11.79L4.5 9.75V6.9525L8.25 9L12 6.9525V9.75Z" fill="#888"/>
</svg>
`)
}

func getScanAnimationSvg() template.HTML {
return template.HTML(`<svg id="scan-animation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 248 204" shape-rendering="geometricPrecision">
<defs>
Expand Down
202 changes: 114 additions & 88 deletions infrastructure/oss/issue_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("<a href='%s'>%s</a>", 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("<a href='https://cwe.mitre.org/data/definitions/%s.html'>%s</a>", 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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're at it - should we just update it to CVSSv4? ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! But I didn't change it in the LS message type, since that would affect our IDEs being able to read this value

"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("<span>CVSS %.1f</span>", 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("<a href='https://snyk.io/vuln/%s'>%s</a>", 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, "<span class='delimiter'> </span> "))
return "Vulnerability"
}

func getExploitMaturity(issue snyk.OssIssueData) string {
if len(issue.Exploit) > 0 {
return fmt.Sprintf("<div class='summary-item maturity'><div class='label font-light'>Exploit maturity</div>"+
"<div class='content'>%s</div></div>", issue.Exploit)
return issue.Exploit
} else {
return ""
}
}
Comment on lines 102 to 108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can remove the else?

func getExploitMaturity(issue snyk.OssIssueData) string {
	if len(issue.Exploit) > 0 {
		return issue.Exploit
	}
	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("<div class='summary-item introduced-through'><div class='label font-light'>Introduced through</div>"+
"<div class='content'>%s</div></div>", strings.Join(maps.Values(m), ", "))
} else {
return ""
}
return introducedThroughs
}

func getVulnHtmlAnchor(packageManager string, module string) string {
snykUi := config.CurrentConfig().SnykUi()
return fmt.Sprintf("<a href='%s/test/%s'>%s</a>", 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("<a class='learn--link' id='learn--link' href='%s'>Learn about this vulnerability</a>",
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(`<div class="summary-item path">
<div class="label font-light">Introduced through</div>
<div class="content">%s</div>
</div>
<div class="summary-item remediation">
<div class="label font-light">Remediation</div>
<div class="content">%s</div>
</div>`, 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
}
6 changes: 3 additions & 3 deletions infrastructure/oss/issue_html_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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, " &gt; ")))
assert.True(t, strings.Contains(issueDetailsPanelHtml, strings.Join(issue2.From, " &gt; ")))
assert.True(t, strings.Contains(issueDetailsPanelHtml, "<li>list</li>"))
assert.False(t, strings.Contains(issueDetailsPanelHtml, "Learn about this vulnerability"))
}
Expand Down
Loading
Loading