Skip to content

Commit

Permalink
feat: refactor the HTML to use html/template and to inject styling fo…
Browse files Browse the repository at this point in the history
…r other IDEs [IDE-326] (#564)
  • Loading branch information
teodora-sandu authored and ShawkyZ committed Jul 10, 2024
1 parent 4ecba52 commit bb76412
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 158 deletions.
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 {
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),
"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 ""
}
}

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

0 comments on commit bb76412

Please sign in to comment.