-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As we're at it - should we just update it to CVSSv4? ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
WDYT @teodora-sandu @bastiandoetsch