From 0afb3290f2de91357597874a9e9959c43185753f Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 16 Oct 2024 10:46:00 +0200 Subject: [PATCH] feat: load issue HTML on demand (#703) --- README.md | 10 +- application/server/server.go | 1 + domain/ide/command/clear_cache.go | 3 + domain/ide/command/command_factory.go | 2 + .../ide/command/generate_issue_description.go | 95 +++++++++++++++++++ domain/ide/converter/converter.go | 69 +++++--------- infrastructure/code/code.go | 2 - infrastructure/code/code_html.go | 54 ++++++++--- infrastructure/code/code_html_test.go | 33 ++++--- infrastructure/code/code_test.go | 6 +- infrastructure/iac/iac.go | 8 -- infrastructure/iac/iac_html.go | 24 ++--- infrastructure/iac/iac_html_test.go | 4 +- infrastructure/iac/iac_test.go | 19 ++-- infrastructure/oss/cli_scanner.go | 9 +- infrastructure/oss/issue.go | 20 +--- .../oss/{issue_html.go => oss_html.go} | 34 ++++--- .../{issue_html_test.go => oss_html_test.go} | 36 ++++--- internal/types/command.go | 34 +++---- internal/types/lsp.go | 57 +++++------ 20 files changed, 308 insertions(+), 212 deletions(-) create mode 100644 domain/ide/command/generate_issue_description.go rename infrastructure/oss/{issue_html.go => oss_html.go} (86%) rename infrastructure/oss/{issue_html_test.go => oss_html_test.go} (89%) diff --git a/README.md b/README.md index 813d899c4..56ba0d986 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,6 @@ Right now the language server supports the following actions: }, } ], - "exampleCommitFixes": [ - { - "commit": "commit", - "diff": "diff" - } - ], "cwe": "cwe", "isSecurityType": true } @@ -412,6 +406,10 @@ Right now the language server supports the following actions: - args: - `folderUri` string, - `cacheType` `persisted` or `inMemory` +- `Generate Issue Description` Generates issue description in HTML. + - command: `snyk.generateIssueDescription` + - args: + - `issueId` string ## Installation diff --git a/application/server/server.go b/application/server/server.go index 298db85fd..6f0977782 100644 --- a/application/server/server.go +++ b/application/server/server.go @@ -297,6 +297,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func { types.CodeFixDiffsCommand, types.ExecuteCLICommand, types.ClearCacheCommand, + types.GenerateIssueDescriptionCommand, }, }, }, diff --git a/domain/ide/command/clear_cache.go b/domain/ide/command/clear_cache.go index 5dfc12b7c..710d3f48f 100644 --- a/domain/ide/command/clear_cache.go +++ b/domain/ide/command/clear_cache.go @@ -75,6 +75,9 @@ func (cmd *clearCache) purgeInMemoryCache(logger *zerolog.Logger, folderUri *lsp } logger.Info().Msgf("deleting in-memory cache for folder %s", folder.Path()) folder.Clear() + if config.CurrentConfig().IsAutoScanEnabled() { + folder.ScanFolder(context.Background()) + } } } diff --git a/domain/ide/command/command_factory.go b/domain/ide/command/command_factory.go index 9cb833465..6db65d673 100644 --- a/domain/ide/command/command_factory.go +++ b/domain/ide/command/command_factory.go @@ -91,6 +91,8 @@ func CreateFromCommandData( return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil case types.ClearCacheCommand: return &clearCache{command: commandData}, nil + case types.GenerateIssueDescriptionCommand: + return &generateIssueDescription{command: commandData, issueProvider: issueProvider}, nil } return nil, fmt.Errorf("unknown command %v", commandData) diff --git a/domain/ide/command/generate_issue_description.go b/domain/ide/command/generate_issue_description.go new file mode 100644 index 000000000..a3ccf8623 --- /dev/null +++ b/domain/ide/command/generate_issue_description.go @@ -0,0 +1,95 @@ +/* + * © 2024 Snyk Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package command + +import ( + "context" + "errors" + "github.com/rs/zerolog" + "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/infrastructure/iac" + "github.com/snyk/snyk-ls/infrastructure/oss" + "github.com/snyk/snyk-ls/internal/product" + "github.com/snyk/snyk-ls/internal/types" +) + +type generateIssueDescription struct { + command types.CommandData + issueProvider snyk.IssueProvider +} + +func (cmd *generateIssueDescription) Command() types.CommandData { + return cmd.command +} + +func (cmd *generateIssueDescription) Execute(_ context.Context) (any, error) { + c := config.CurrentConfig() + logger := c.Logger().With().Str("method", "generateIssueDescription.Execute").Logger() + args := cmd.command.Arguments + + issueId, ok := args[0].(string) + if !ok { + return nil, errors.New("failed to parse issue id") + } + + issue := cmd.issueProvider.Issue(issueId) + if issue.ID == "" { + return nil, errors.New("failed to find issue") + } + + if issue.Product == product.ProductInfrastructureAsCode { + return getIacHtml(c, logger, issue) + } else if issue.Product == product.ProductCode { + return getCodeHtml(c, logger, issue) + } else if issue.Product == product.ProductOpenSource { + return getOssHtml(c, logger, issue) + } + + return nil, nil +} + +func getOssHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) { + htmlRender, err := oss.NewHtmlRenderer(c) + if err != nil { + logger.Err(err).Msg("Cannot create Oss HTML render") + return "", err + } + html := htmlRender.GetDetailsHtml(issue) + return html, nil +} + +func getCodeHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) { + htmlRender, err := code.NewHtmlRenderer(c) + if err != nil { + logger.Err(err).Msg("Cannot create Code HTML render") + return "", err + } + html := htmlRender.GetDetailsHtml(issue) + return html, nil +} + +func getIacHtml(c *config.Config, logger zerolog.Logger, issue snyk.Issue) (string, error) { + htmlRender, err := iac.NewHtmlRenderer(c) + if err != nil { + logger.Err(err).Msg("Cannot create IaC HTML render") + return "", err + } + html := htmlRender.GetDetailsHtml(issue) + return html, nil +} diff --git a/domain/ide/converter/converter.go b/domain/ide/converter/converter.go index 09cdfdc47..5a363c7c8 100644 --- a/domain/ide/converter/converter.go +++ b/domain/ide/converter/converter.go @@ -235,7 +235,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue { IsUpgradable: matchingIssue.IsUpgradable, ProjectName: matchingIssue.ProjectName, DisplayTargetFile: matchingIssue.DisplayTargetFile, - Details: matchingIssue.Details, } } @@ -272,7 +271,6 @@ func getOssIssue(issue snyk.Issue) types.ScanIssue { IsUpgradable: additionalData.IsUpgradable, ProjectName: additionalData.ProjectName, DisplayTargetFile: additionalData.DisplayTargetFile, - Details: additionalData.Details, MatchingIssues: matchingIssues, Lesson: additionalData.Lesson, }, @@ -297,16 +295,15 @@ func getIacIssue(issue snyk.Issue) types.ScanIssue { IsNew: issue.IsNew, FilterableIssueType: additionalData.GetFilterableIssueType(), AdditionalData: types.IacIssueData{ - Key: additionalData.Key, - PublicId: additionalData.PublicId, - Documentation: additionalData.Documentation, - LineNumber: additionalData.LineNumber, - Issue: additionalData.Issue, - Impact: additionalData.Impact, - Resolve: additionalData.Resolve, - Path: additionalData.Path, - References: additionalData.References, - CustomUIContent: additionalData.CustomUIContent, + Key: additionalData.Key, + PublicId: additionalData.PublicId, + Documentation: additionalData.Documentation, + LineNumber: additionalData.LineNumber, + Issue: additionalData.Issue, + Impact: additionalData.Impact, + Resolve: additionalData.Resolve, + Path: additionalData.Path, + References: additionalData.References, }, } @@ -319,22 +316,6 @@ func getCodeIssue(issue snyk.Issue) types.ScanIssue { return types.ScanIssue{} } - exampleCommitFixes := make([]types.ExampleCommitFix, 0, len(additionalData.ExampleCommitFixes)) - for i := range additionalData.ExampleCommitFixes { - lines := make([]types.CommitChangeLine, 0, len(additionalData.ExampleCommitFixes[i].Lines)) - for j := range additionalData.ExampleCommitFixes[i].Lines { - lines = append(lines, types.CommitChangeLine{ - Line: additionalData.ExampleCommitFixes[i].Lines[j].Line, - LineNumber: additionalData.ExampleCommitFixes[i].Lines[j].LineNumber, - LineChange: additionalData.ExampleCommitFixes[i].Lines[j].LineChange, - }) - } - exampleCommitFixes = append(exampleCommitFixes, types.ExampleCommitFix{ - CommitURL: additionalData.ExampleCommitFixes[i].CommitURL, - Lines: lines, - }) - } - markers := make([]types.Marker, 0, len(additionalData.Markers)) for _, marker := range additionalData.Markers { positions := make([]types.MarkerPosition, 0) @@ -374,23 +355,21 @@ func getCodeIssue(issue snyk.Issue) types.ScanIssue { IsNew: issue.IsNew, FilterableIssueType: additionalData.GetFilterableIssueType(), AdditionalData: types.CodeIssueData{ - Key: additionalData.Key, - Message: additionalData.Message, - Rule: additionalData.Rule, - RuleId: additionalData.RuleId, - RepoDatasetSize: additionalData.RepoDatasetSize, - ExampleCommitFixes: exampleCommitFixes, - CWE: additionalData.CWE, - IsSecurityType: additionalData.IsSecurityType, - Text: additionalData.Text, - Cols: additionalData.Cols, - Rows: additionalData.Rows, - PriorityScore: additionalData.PriorityScore, - Markers: markers, - LeadURL: "", - HasAIFix: additionalData.HasAIFix, - DataFlow: dataFlow, - Details: additionalData.Details, + Key: additionalData.Key, + Message: additionalData.Message, + Rule: additionalData.Rule, + RuleId: additionalData.RuleId, + RepoDatasetSize: additionalData.RepoDatasetSize, + CWE: additionalData.CWE, + IsSecurityType: additionalData.IsSecurityType, + Text: additionalData.Text, + Cols: additionalData.Cols, + Rows: additionalData.Rows, + PriorityScore: additionalData.PriorityScore, + Markers: markers, + LeadURL: "", + HasAIFix: additionalData.HasAIFix, + DataFlow: dataFlow, }, } if scanIssue.IsIgnored { diff --git a/infrastructure/code/code.go b/infrastructure/code/code.go index 759bf3926..5a25b430a 100644 --- a/infrastructure/code/code.go +++ b/infrastructure/code/code.go @@ -264,8 +264,6 @@ func (sc *Scanner) enhanceIssuesDetails(issues []snyk.Issue, folderPath string) } else if lesson != nil && lesson.Url != "" { issue.LessonUrl = lesson.Url } - - issueData.Details = getCodeDetailsHtml(*issue, folderPath) issue.AdditionalData = issueData } } diff --git a/infrastructure/code/code_html.go b/infrastructure/code/code_html.go index 0ca90e1dc..8a7f6597f 100644 --- a/infrastructure/code/code_html.go +++ b/infrastructure/code/code_html.go @@ -21,6 +21,9 @@ import ( _ "embed" "encoding/json" "fmt" + "github.com/rs/zerolog" + "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/internal/uri" "html/template" "path/filepath" "regexp" @@ -60,32 +63,53 @@ type ExampleCommit struct { //go:embed template/details.html var detailsHtmlTemplate string -var globalTemplate *template.Template +type HtmlRenderer struct { + c *config.Config + globalTemplate *template.Template +} -func init() { +func NewHtmlRenderer(c *config.Config) (*HtmlRenderer, error) { funcMap := template.FuncMap{ "repoName": getRepoName, "trimCWEPrefix": html.TrimCWEPrefix, "idxMinusOne": html.IdxMinusOne, } - var err error - globalTemplate, err = template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate) + globalTemplate, err := template.New(string(product.ProductCode)).Funcs(funcMap).Parse(detailsHtmlTemplate) if err != nil { - config.CurrentConfig().Logger().Error().Msgf("Failed to parse details template: %s", err) + c.Logger().Error().Msgf("Failed to parse details template: %s", err) + return nil, err } + + return &HtmlRenderer{ + c: c, + globalTemplate: globalTemplate, + }, nil } -func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string { - c := config.CurrentConfig() +func determineFolderPath(filePath string) string { + ws := workspace.Get() + if ws == nil { + return "" + } + for _, folder := range ws.Folders() { + folderPath := folder.Path() + if uri.FolderContains(folderPath, filePath) { + return folderPath + } + } + return "" +} + +func (renderer *HtmlRenderer) GetDetailsHtml(issue snyk.Issue) string { additionalData, ok := issue.AdditionalData.(snyk.CodeIssueData) if !ok { - c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData") + renderer.c.Logger().Error().Msg("Failed to cast additional data to CodeIssueData") return "" } - + folderPath := determineFolderPath(issue.AffectedFilePath) exampleCommits := prepareExampleCommits(additionalData.ExampleCommitFixes) - commitFixes := parseExampleCommitsToTemplateJS(exampleCommits) + commitFixes := parseExampleCommitsToTemplateJS(exampleCommits, renderer.c.Logger()) data := map[string]interface{}{ "IssueTitle": additionalData.Title, @@ -102,7 +126,7 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string { "ExampleCommitFixes": exampleCommits, "CommitFixes": commitFixes, "PriorityScore": additionalData.PriorityScore, - "SnykWebUrl": config.CurrentConfig().SnykUI(), + "SnykWebUrl": renderer.c.SnykUI(), "LessonUrl": issue.LessonUrl, "LessonIcon": html.LessonIcon(), "IgnoreLineAction": getLineToIgnoreAction(issue), @@ -126,8 +150,8 @@ func getCodeDetailsHtml(issue snyk.Issue, folderPath string) string { } var buffer bytes.Buffer - if err := globalTemplate.Execute(&buffer, data); err != nil { - c.Logger().Error().Msgf("Failed to execute main details template: %v", err) + if err := renderer.globalTemplate.Execute(&buffer, data); err != nil { + renderer.c.Logger().Error().Msgf("Failed to execute main details template: %v", err) return "" } @@ -215,10 +239,10 @@ func prepareExampleCommits(fixes []snyk.ExampleCommitFix) []ExampleCommit { return fixData } -func parseExampleCommitsToTemplateJS(fixes []ExampleCommit) template.JS { +func parseExampleCommitsToTemplateJS(fixes []ExampleCommit, logger *zerolog.Logger) template.JS { jsonFixes, err := json.Marshal(fixes) if err != nil { - config.CurrentConfig().Logger().Error().Msgf("Failed to marshal example commit fixes: %v", err) + logger.Error().Msgf("Failed to marshal example commit fixes: %v", err) return "" } return template.JS(jsonFixes) diff --git a/infrastructure/code/code_html_test.go b/infrastructure/code/code_html_test.go index 94bfd7a68..33c95a871 100644 --- a/infrastructure/code/code_html_test.go +++ b/infrastructure/code/code_html_test.go @@ -29,7 +29,7 @@ import ( ) func Test_Code_Html_getCodeDetailsHtml(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) dataFlow := getDataFlowElements() fixes := getFixes() @@ -52,7 +52,9 @@ func Test_Code_Html_getCodeDetailsHtml(t *testing.T) { } // invoke method under test - codePanelHtml := getCodeDetailsHtml(issue, "./repos/") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + codePanelHtml := htmlRenderer.GetDetailsHtml(issue) // assert injectable style assert.Contains(t, codePanelHtml, "${ideStyle}") @@ -79,7 +81,7 @@ func Test_Code_Html_getCodeDetailsHtml(t *testing.T) { // assert Fixes section assert.Contains(t, codePanelHtml, ` id="ai-fix-wrapper" class="hidden">`) assert.Contains(t, codePanelHtml, ` id="no-ai-fix-wrapper" class="">`) - assert.Contains(t, codePanelHtml, ``) expectedFixesDescription := fmt.Sprintf(`This type of vulnerability was fixed in %d open source projects.`, repoCount) assert.Regexp(t, regexp.MustCompile(expectedFixesDescription), codePanelHtml) @@ -91,7 +93,7 @@ func Test_Code_Html_getCodeDetailsHtml(t *testing.T) { } func Test_Code_Html_getCodeDetailsHtml_withAIfix(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) dataFlow := getDataFlowElements() fixes := getFixes() @@ -115,17 +117,18 @@ func Test_Code_Html_getCodeDetailsHtml_withAIfix(t *testing.T) { } // invoke method under test - codePanelHtml := getCodeDetailsHtml(issue, "./repos/") - + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + codePanelHtml := htmlRenderer.GetDetailsHtml(issue) // assert Fixes section assert.Contains(t, codePanelHtml, ` id="ai-fix-wrapper" class="">`) assert.Contains(t, codePanelHtml, `✨ Generate AI fix`) assert.Contains(t, codePanelHtml, ` id="no-ai-fix-wrapper" class="hidden">`) - assert.Contains(t, codePanelHtml, ` folder-path="./repos/"`) + assert.Contains(t, codePanelHtml, ` folder-path=""`) } func Test_Code_Html_getCodeDetailsHtml_ignored(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) dataFlow := getDataFlowElements() fixes := getFixes() @@ -155,7 +158,9 @@ func Test_Code_Html_getCodeDetailsHtml_ignored(t *testing.T) { } // invoke method under test - codePanelHtml := getCodeDetailsHtml(issue, "") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + codePanelHtml := htmlRenderer.GetDetailsHtml(issue) // assert Header section assert.Contains(t, codePanelHtml, "Priority score: 0") @@ -173,7 +178,7 @@ func Test_Code_Html_getCodeDetailsHtml_ignored(t *testing.T) { } func Test_Code_Html_getCodeDetailsHtml_ignored_expired(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) issue := snyk.Issue{ ID: "scala/DontUsePrintStackTrace", @@ -192,7 +197,9 @@ func Test_Code_Html_getCodeDetailsHtml_ignored_expired(t *testing.T) { } // invoke method under test - codePanelHtml := getCodeDetailsHtml(issue, "") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + codePanelHtml := htmlRenderer.GetDetailsHtml(issue) // assert Ignore Details section // Asserting an expired date to prevent the test from breaking in the future as the current date changes @@ -232,7 +239,9 @@ func Test_Code_Html_getCodeDetailsHtml_ignored_customEndpoint(t *testing.T) { } // invoke method under test - codePanelHtml := getCodeDetailsHtml(issue, "") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + codePanelHtml := htmlRenderer.GetDetailsHtml(issue) // assert Ignore Details section - Ignore link must be the custom endpoint assert.Contains(t, codePanelHtml, customEndpoint) diff --git a/infrastructure/code/code_test.go b/infrastructure/code/code_test.go index 3cfb7e2e4..1f797921f 100644 --- a/infrastructure/code/code_test.go +++ b/infrastructure/code/code_test.go @@ -598,10 +598,12 @@ func Test_enhanceIssuesDetails(t *testing.T) { // Act scanner.enhanceIssuesDetails(issues, "") - + htmlRenderer, err := NewHtmlRenderer(c) + assert.Nil(t, err) + html := htmlRenderer.GetDetailsHtml(issues[0]) // Assert assert.Equal(t, expectedLessonUrl, issues[0].LessonUrl) - assert.Contains(t, issues[0].AdditionalData.(snyk.CodeIssueData).Details, `href="https://learn.snyk.io/lesson/no-rate-limiting/?loc=ide"`) + assert.Contains(t, html, `href="https://learn.snyk.io/lesson/no-rate-limiting/?loc=ide"`) } func setupIgnoreWorkspace(t *testing.T) (tempDir string, ignoredFilePath string, notIgnoredFilePath string) { diff --git a/infrastructure/iac/iac.go b/infrastructure/iac/iac.go index 368c03dfe..55445bd1d 100644 --- a/infrastructure/iac/iac.go +++ b/infrastructure/iac/iac.go @@ -392,14 +392,6 @@ func (iac *Scanner) toIssue(affectedFilePath string, issue iacIssue, fileContent fingerprint := utils.CalculateFingerprintFromAdditionalData(result) result.SetFingerPrint(fingerprint) - - htmlRender, err := NewIacHtmlRenderer(iac.c) - if err != nil { - iac.c.Logger().Err(err).Msg("Cannot create IaC HTML render") - return snyk.Issue{}, err - } - - additionalData.CustomUIContent = htmlRender.getCustomUIContent(result) result.AdditionalData = additionalData return result, nil diff --git a/infrastructure/iac/iac_html.go b/infrastructure/iac/iac_html.go index 619d154b0..6a58b82e0 100644 --- a/infrastructure/iac/iac_html.go +++ b/infrastructure/iac/iac_html.go @@ -28,9 +28,9 @@ import ( "github.com/snyk/snyk-ls/internal/product" ) -type IacHtmlRenderer struct { - Config *config.Config - GlobalTemplate *template.Template +type HtmlRenderer struct { + c *config.Config + globalTemplate *template.Template } type TemplateData struct { @@ -49,16 +49,16 @@ var detailsHtmlTemplate string //go:embed template/styles.css var stylesCSS string -func NewIacHtmlRenderer(cfg *config.Config) (*IacHtmlRenderer, error) { +func NewHtmlRenderer(c *config.Config) (*HtmlRenderer, error) { tmp, err := template.New(string(product.ProductInfrastructureAsCode)).Parse(detailsHtmlTemplate) if err != nil { - cfg.Logger().Error().Msgf("Failed to parse IaC template: %s", err) + c.Logger().Error().Msgf("Failed to parse IaC template: %s", err) return nil, err } - return &IacHtmlRenderer{ - Config: cfg, - GlobalTemplate: tmp, + return &HtmlRenderer{ + c: c, + globalTemplate: tmp, }, nil } @@ -67,12 +67,12 @@ func getStyles() template.CSS { } // Function to get the rendered HTML with issue details and CSS -func (service *IacHtmlRenderer) getCustomUIContent(issue snyk.Issue) string { +func (service *HtmlRenderer) GetDetailsHtml(issue snyk.Issue) string { var htmlTemplate bytes.Buffer nonce, err := html.GenerateSecurityNonce() if err != nil { - service.Config.Logger().Warn().Msgf("Failed to generate nonce: %s", err) + service.c.Logger().Warn().Msgf("Failed to generate nonce: %s", err) return "" } @@ -86,9 +86,9 @@ func (service *IacHtmlRenderer) getCustomUIContent(issue snyk.Issue) string { Nonce: template.HTML(nonce), } - err = service.GlobalTemplate.Execute(&htmlTemplate, data) + err = service.globalTemplate.Execute(&htmlTemplate, data) if err != nil { - service.Config.Logger().Error().Msgf("Failed to execute IaC template: %s", err) + service.c.Logger().Error().Msgf("Failed to execute IaC template: %s", err) } return htmlTemplate.String() diff --git a/infrastructure/iac/iac_html_test.go b/infrastructure/iac/iac_html_test.go index 995707870..785b47b60 100644 --- a/infrastructure/iac/iac_html_test.go +++ b/infrastructure/iac/iac_html_test.go @@ -14,8 +14,8 @@ func Test_IaC_Html_getIacHtml(t *testing.T) { cfg := &config.Config{} // Initialize the IaC service - service, _ := NewIacHtmlRenderer(cfg) - iacPanelHtml := service.getCustomUIContent(createIacIssueSample()) + service, _ := NewHtmlRenderer(cfg) + iacPanelHtml := service.GetDetailsHtml(createIacIssueSample()) // assert assert.Contains(t, iacPanelHtml, "", "HTML should contain the doctype declaration") diff --git a/infrastructure/iac/iac_test.go b/infrastructure/iac/iac_test.go index 102e3a2ea..73469d859 100644 --- a/infrastructure/iac/iac_test.go +++ b/infrastructure/iac/iac_test.go @@ -156,9 +156,13 @@ func Test_createIssueDataForCustomUI_SuccessfullyParses(t *testing.T) { assert.Equal(t, expectedAdditionalData.Resolve, actualAdditionalData.Resolve) assert.Equal(t, expectedAdditionalData.References, actualAdditionalData.References) - assert.NotEmpty(t, actualAdditionalData.CustomUIContent, "Details field should not be empty") - assert.Contains(t, actualAdditionalData.CustomUIContent, "", "Details should contain HTML doctype declaration") - assert.Contains(t, actualAdditionalData.CustomUIContent, "PublicID", "Details should contain the PublicID") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + html := htmlRenderer.GetDetailsHtml(issue) + + assert.NotEmpty(t, html, "Details field should not be empty") + assert.Contains(t, html, "", "Details should contain HTML doctype declaration") + assert.Contains(t, html, "PublicID", "Details should contain the PublicID") } func Test_toIssue_issueHasHtmlTemplate(t *testing.T) { @@ -170,9 +174,12 @@ func Test_toIssue_issueHasHtmlTemplate(t *testing.T) { assert.NoError(t, err) // Assert the Details field contains the HTML template and expected content - additionalData := issue.AdditionalData.(snyk.IaCIssueData) - assert.NotEmpty(t, additionalData.CustomUIContent, "HTML Details should not be empty") - assert.Contains(t, additionalData.CustomUIContent, "PublicID", "HTML should contain the PublicID") + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + html := htmlRenderer.GetDetailsHtml(issue) + + assert.NotEmpty(t, html, "HTML Details should not be empty") + assert.Contains(t, html, "PublicID", "HTML should contain the PublicID") } func Test_getIssueId(t *testing.T) { diff --git a/infrastructure/oss/cli_scanner.go b/infrastructure/oss/cli_scanner.go index 5c249d3d5..619364ffa 100644 --- a/infrastructure/oss/cli_scanner.go +++ b/infrastructure/oss/cli_scanner.go @@ -490,14 +490,7 @@ func (cliScanner *CLIScanner) retrieveIssues( // we are updating the cli scanner maps/attributes in parallel, so we need to lock cliScanner.mutex.Lock() defer cliScanner.mutex.Unlock() - issues := convertScanResultToIssues( - res, - targetFilePath, - fileContent, - cliScanner.learnService, - cliScanner.errorReporter, - cliScanner.packageIssueCache, - ) + issues := convertScanResultToIssues(res, targetFilePath, fileContent, cliScanner.learnService, cliScanner.errorReporter, cliScanner.packageIssueCache) // repopulate cliScanner.addVulnerabilityCountsToCache(issues) diff --git a/infrastructure/oss/issue.go b/infrastructure/oss/issue.go index b2c72e8c0..9f40818c3 100644 --- a/infrastructure/oss/issue.go +++ b/infrastructure/oss/issue.go @@ -40,15 +40,7 @@ var issuesSeverity = map[string]snyk.Severity{ "medium": snyk.Medium, } -func toIssue( - affectedFilePath string, - issue ossIssue, - scanResult *scanResult, - issueRange snyk.Range, - learnService learn.Service, - ep error_reporting.ErrorReporter, - fileContent []byte, -) snyk.Issue { +func toIssue(affectedFilePath string, issue ossIssue, scanResult *scanResult, issueRange snyk.Range, learnService learn.Service, ep error_reporting.ErrorReporter, fileContent []byte) snyk.Issue { // this needs to be first so that the lesson from Snyk Learn is added codeActions := issue.AddCodeActions(learnService, ep, affectedFilePath, issueRange, fileContent) @@ -114,7 +106,6 @@ func toIssue( CVEs: issue.Identifiers.CVE, AdditionalData: additionalData, } - additionalData.Details = getDetailsHtml(d) d.AdditionalData = additionalData fingerprint := utils.CalculateFingerprintFromAdditionalData(d) d.SetFingerPrint(fingerprint) @@ -122,14 +113,7 @@ func toIssue( return d } -func convertScanResultToIssues( - res *scanResult, - targetFilePath string, - fileContent []byte, - ls learn.Service, - ep error_reporting.ErrorReporter, - packageIssueCache map[string][]snyk.Issue, -) []snyk.Issue { +func convertScanResultToIssues(res *scanResult, targetFilePath string, fileContent []byte, ls learn.Service, ep error_reporting.ErrorReporter, packageIssueCache map[string][]snyk.Issue) []snyk.Issue { var issues []snyk.Issue duplicateCheckMap := map[string]bool{} diff --git a/infrastructure/oss/issue_html.go b/infrastructure/oss/oss_html.go similarity index 86% rename from infrastructure/oss/issue_html.go rename to infrastructure/oss/oss_html.go index ccf033e82..25f1c9568 100644 --- a/infrastructure/oss/issue_html.go +++ b/infrastructure/oss/oss_html.go @@ -34,30 +34,37 @@ import ( //go:embed template/details.html var detailsHtmlTemplate string -var globalTemplate *template.Template +type HtmlRenderer struct { + c *config.Config + globalTemplate *template.Template +} -func init() { +func NewHtmlRenderer(c *config.Config) (*HtmlRenderer, error) { funcMap := template.FuncMap{ "trimCWEPrefix": html.TrimCWEPrefix, "idxMinusOne": html.IdxMinusOne, "join": join, } - - var err error - globalTemplate, err = template.New(string(product.ProductOpenSource)).Funcs(funcMap).Parse(detailsHtmlTemplate) + 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) + c.Logger().Error().Msgf("Failed to parse details template: %s", err) + return nil, err } + + return &HtmlRenderer{ + c: c, + globalTemplate: globalTemplate, + }, nil } func join(sep string, s []string) string { return strings.Join(s, sep) } -func getDetailsHtml(issue snyk.Issue) string { +func (renderer *HtmlRenderer) 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") + renderer.c.Logger().Error().Msg("Failed to cast additional data to OssIssueData") return "" } @@ -79,7 +86,7 @@ func getDetailsHtml(issue snyk.Issue) string { "CVSSv3": template.URL(additionalData.CVSSv3), "CvssScore": fmt.Sprintf("%.1f", additionalData.CvssScore), "ExploitMaturity": getExploitMaturity(additionalData), - "IntroducedThroughs": getIntroducedThroughs(additionalData), + "IntroducedThroughs": getIntroducedThroughs(additionalData, renderer.c.SnykUI()), "LessonUrl": additionalData.Lesson, "LessonIcon": html.LessonIcon(), "FixedIn": additionalData.FixedIn, @@ -89,8 +96,8 @@ func getDetailsHtml(issue snyk.Issue) string { } var htmlBuffer bytes.Buffer - if err := globalTemplate.Execute(&htmlBuffer, data); err != nil { - config.CurrentConfig().Logger().Error().Msgf("Failed to execute main details template: %v", err) + if err := renderer.globalTemplate.Execute(&htmlBuffer, data); err != nil { + renderer.c.Logger().Error().Msgf("Failed to execute main details template: %v", err) return "" } @@ -152,15 +159,14 @@ type IntroducedThrough struct { Module string } -func getIntroducedThroughs(issue snyk.OssIssueData) []IntroducedThrough { +func getIntroducedThroughs(issue snyk.OssIssueData, snykUI string) []IntroducedThrough { var introducedThroughs []IntroducedThrough - snykUi := config.CurrentConfig().SnykUI() if len(issue.From) > 0 { for _, v := range issue.MatchingIssues { if len(v.From) > 1 { introducedThroughs = append(introducedThroughs, IntroducedThrough{ - SnykUI: snykUi, + SnykUI: snykUI, PackageManager: issue.PackageManager, Module: v.From[1], }) diff --git a/infrastructure/oss/issue_html_test.go b/infrastructure/oss/oss_html_test.go similarity index 89% rename from infrastructure/oss/issue_html_test.go rename to infrastructure/oss/oss_html_test.go index 5447b45b7..808d88410 100644 --- a/infrastructure/oss/issue_html_test.go +++ b/infrastructure/oss/oss_html_test.go @@ -30,7 +30,7 @@ import ( ) func Test_OssDetailsPanel_html_noLearn(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) expectedVariables := []string{"${headerEnd}", "${cspSource}", "${ideStyle}", "${nonce}"} slices.Sort(expectedVariables) @@ -58,7 +58,9 @@ func Test_OssDetailsPanel_html_noLearn(t *testing.T) { } // invoke methode under test - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) // compare reg := regexp.MustCompile(`\$\{\w+\}`) @@ -79,7 +81,7 @@ func Test_OssDetailsPanel_html_noLearn(t *testing.T) { } func Test_OssDetailsPanel_html_withLearn(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) issueAdditionalData := snyk.OssIssueData{ Title: "myTitle", @@ -98,7 +100,9 @@ func Test_OssDetailsPanel_html_withLearn(t *testing.T) { issueAdditionalData.MatchingIssues = append(issueAdditionalData.MatchingIssues, issueAdditionalData) // invoke methode under test - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) assert.True(t, strings.Contains(issueDetailsPanelHtml, "Learn about this vulnerability")) } @@ -130,13 +134,15 @@ func Test_OssDetailsPanel_html_withLearn_withCustomEndpoint(t *testing.T) { issueAdditionalData.MatchingIssues = append(issueAdditionalData.MatchingIssues, issueAdditionalData) - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) - assert.True(t, strings.Contains(issueDetailsPanelHtml, customEndpoint)) + assert.Truef(t, strings.Contains(issueDetailsPanelHtml, customEndpoint), issueDetailsPanelHtml) } func Test_OssDetailsPanel_html_moreDetailedPaths(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) expectedVariables := []string{"${headerEnd}", "${cspSource}", "${ideStyle}", "${nonce}"} slices.Sort(expectedVariables) @@ -180,7 +186,9 @@ func Test_OssDetailsPanel_html_moreDetailedPaths(t *testing.T) { } // invoke methode under test - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) // compare reg := regexp.MustCompile(`\$\{\w+\}`) @@ -203,7 +211,7 @@ func Test_OssDetailsPanel_html_moreDetailedPaths(t *testing.T) { } func Test_OssDetailsPanel_html_withAnnotationsPolicy(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) // Arrange issueAdditionalData := snyk.OssIssueData{ @@ -226,7 +234,9 @@ func Test_OssDetailsPanel_html_withAnnotationsPolicy(t *testing.T) { } // Act - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) // Assert assert.True(t, strings.Contains(issueDetailsPanelHtml, "User note")) @@ -234,7 +244,7 @@ func Test_OssDetailsPanel_html_withAnnotationsPolicy(t *testing.T) { } func Test_OssDetailsPanel_html_withSeverityChangePolicy(t *testing.T) { - _ = testutil.UnitTest(t) + c := testutil.UnitTest(t) // Arrange issueAdditionalData := snyk.OssIssueData{ @@ -258,7 +268,9 @@ func Test_OssDetailsPanel_html_withSeverityChangePolicy(t *testing.T) { } // Act - issueDetailsPanelHtml := getDetailsHtml(issue) + htmlRenderer, err := NewHtmlRenderer(c) + assert.NoError(t, err) + issueDetailsPanelHtml := htmlRenderer.GetDetailsHtml(issue) // Assert assert.True(t, strings.Contains(issueDetailsPanelHtml, "A policy has affected the severity of this issue. It was originally critical severity")) diff --git a/internal/types/command.go b/internal/types/command.go index 1643fb9ff..00b474208 100644 --- a/internal/types/command.go +++ b/internal/types/command.go @@ -24,23 +24,23 @@ import ( ) const ( - NavigateToRangeCommand = "snyk.navigateToRange" - WorkspaceScanCommand = "snyk.workspace.scan" - WorkspaceFolderScanCommand = "snyk.workspaceFolder.scan" - OpenBrowserCommand = "snyk.openBrowser" - LoginCommand = "snyk.login" - CopyAuthLinkCommand = "snyk.copyAuthLink" - LogoutCommand = "snyk.logout" - TrustWorkspaceFoldersCommand = "snyk.trustWorkspaceFolders" - OpenLearnLesson = "snyk.openLearnLesson" - GetLearnLesson = "snyk.getLearnLesson" - GetSettingsSastEnabled = "snyk.getSettingsSastEnabled" - GetFeatureFlagStatus = "snyk.getFeatureFlagStatus" - GetActiveUserCommand = "snyk.getActiveUser" - ReportAnalyticsCommand = "snyk.reportAnalytics" - ExecuteCLICommand = "snyk.executeCLI" - ClearCacheCommand = "snyk.clearCache" - + NavigateToRangeCommand = "snyk.navigateToRange" + WorkspaceScanCommand = "snyk.workspace.scan" + WorkspaceFolderScanCommand = "snyk.workspaceFolder.scan" + OpenBrowserCommand = "snyk.openBrowser" + LoginCommand = "snyk.login" + CopyAuthLinkCommand = "snyk.copyAuthLink" + LogoutCommand = "snyk.logout" + TrustWorkspaceFoldersCommand = "snyk.trustWorkspaceFolders" + OpenLearnLesson = "snyk.openLearnLesson" + GetLearnLesson = "snyk.getLearnLesson" + GetSettingsSastEnabled = "snyk.getSettingsSastEnabled" + GetFeatureFlagStatus = "snyk.getFeatureFlagStatus" + GetActiveUserCommand = "snyk.getActiveUser" + ReportAnalyticsCommand = "snyk.reportAnalytics" + ExecuteCLICommand = "snyk.executeCLI" + ClearCacheCommand = "snyk.clearCache" + GenerateIssueDescriptionCommand = "snyk.generateIssueDescription" // Snyk Code specific commands CodeFixCommand = "snyk.code.fix" CodeSubmitFixFeedback = "snyk.code.submitFixFeedback" diff --git a/internal/types/lsp.go b/internal/types/lsp.go index c4dda6e5c..f21fa154b 100644 --- a/internal/types/lsp.go +++ b/internal/types/lsp.go @@ -1118,7 +1118,6 @@ type OssIssueData struct { IsUpgradable bool `json:"isUpgradable"` ProjectName string `json:"projectName"` DisplayTargetFile string `json:"displayTargetFile"` - Details string `json:"details,omitempty"` MatchingIssues []OssIssueData `json:"matchingIssues"` Lesson string `json:"lessonUrl,omitempty"` } @@ -1136,32 +1135,25 @@ type DataflowElement struct { } type CodeIssueData struct { - Key string `json:"key,omitempty"` - Message string `json:"message"` - LeadURL string `json:"leadURL,omitempty"` - Rule string `json:"rule"` - RuleId string `json:"ruleId"` - RepoDatasetSize int `json:"repoDatasetSize"` - ExampleCommitFixes []ExampleCommitFix `json:"exampleCommitFixes"` - CWE []string `json:"cwe"` - Text string `json:"text"` - Markers []Marker `json:"markers,omitempty"` - Cols Point `json:"cols"` - Rows Point `json:"rows"` - IsSecurityType bool `json:"isSecurityType"` - PriorityScore int `json:"priorityScore"` - HasAIFix bool `json:"hasAIFix"` - DataFlow []DataflowElement `json:"dataFlow,omitempty"` - Details string `json:"details,omitempty"` + Key string `json:"key,omitempty"` + Message string `json:"message"` + LeadURL string `json:"leadURL,omitempty"` + Rule string `json:"rule"` + RuleId string `json:"ruleId"` + RepoDatasetSize int `json:"repoDatasetSize"` + CWE []string `json:"cwe"` + Text string `json:"text"` + Markers []Marker `json:"markers,omitempty"` + Cols Point `json:"cols"` + Rows Point `json:"rows"` + IsSecurityType bool `json:"isSecurityType"` + PriorityScore int `json:"priorityScore"` + HasAIFix bool `json:"hasAIFix"` + DataFlow []DataflowElement `json:"dataFlow,omitempty"` } type Point = [2]int -type ExampleCommitFix struct { - CommitURL string `json:"commitURL"` - Lines []CommitChangeLine `json:"lines"` -} - type CommitChangeLine struct { Line string `json:"line"` LineNumber int `json:"lineNumber"` @@ -1188,14 +1180,13 @@ type CodeActionOptions struct { } type IacIssueData struct { - Key string `json:"key,omitempty"` - PublicId string `json:"publicId"` - Documentation string `json:"documentation"` - LineNumber int `json:"lineNumber"` - Issue string `json:"issue"` - Impact string `json:"impact"` - Resolve string `json:"resolve,omitempty"` - Path []string `json:"path"` - References []string `json:"references,omitempty"` - CustomUIContent string `json:"customUIContent,omitempty"` + Key string `json:"key,omitempty"` + PublicId string `json:"publicId"` + Documentation string `json:"documentation"` + LineNumber int `json:"lineNumber"` + Issue string `json:"issue"` + Impact string `json:"impact"` + Resolve string `json:"resolve,omitempty"` + Path []string `json:"path"` + References []string `json:"references,omitempty"` }