diff --git a/internal/presenters/__snapshots__/presenter_unified_finding_test.snap b/internal/presenters/__snapshots__/presenter_unified_finding_test.snap new file mode 100755 index 0000000..d029d37 --- /dev/null +++ b/internal/presenters/__snapshots__/presenter_unified_finding_test.snap @@ -0,0 +1,36 @@ +[TestUnifiedFindingPresenter_CliOutput/snapshot_test_of_human-readable_output - 1] + +Testing ... + +Open issues: 1 + + ✗ [HIGH] High severity vulnerability + Finding ID: SNYK-JS-VM2-5537100 + Info: https://snyk.io/vuln/SNYK-JS-VM2-5537100 + Risk Score: 780 + + +License issues: 1 + + ✗ [MEDIUM] LGPL-3.0 license + Finding ID: snyk:lic:npm:web3-core:LGPL-3.0 + Info: https://snyk.io/vuln/snyk:lic:npm:web3-core:LGPL-3.0 + + +╭────────────────────────────────────────────────────────────────╮ +│ Test Summary │ +│ │ +│ Organization: │ +│ Test type: open-source │ +│ Project path: │ +│ │ +│ Total issues: 2 │ +│ Ignored issues: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │ +│ Open issues: 2 [ 0 CRITICAL 1 HIGH 1 MEDIUM 0 LOW ] │ +╰────────────────────────────────────────────────────────────────╯ +💡 Tip + + To view ignored issues, use the --include-ignores option. + + +--- diff --git a/internal/presenters/funcs.go b/internal/presenters/funcs.go index e3e54ed..06c537f 100644 --- a/internal/presenters/funcs.go +++ b/internal/presenters/funcs.go @@ -275,6 +275,42 @@ func isIgnoredFinding() func(obj any) bool { } } +// isLicenseFinding returns true if the finding is a license finding. +func isLicenseFinding(finding testapi.FindingData) bool { + if finding.Attributes != nil { + for _, problem := range finding.Attributes.Problems { + disc, err := problem.Discriminator() + if err == nil && disc == string(testapi.SnykLicense) { + return true + } + } + } + return false +} + +// isLicenseFindingFilter returns a filter function that checks if a finding is a license finding. +func isLicenseFindingFilter() func(obj any) bool { + return func(obj any) bool { + finding, ok := obj.(testapi.FindingData) + if !ok { + return false + } + return isLicenseFinding(finding) + } +} + +// isNotLicenseFindingFilter returns a function that checks if a finding is not a license finding. +func isNotLicenseFindingFilter() func(obj any) bool { + return func(obj any) bool { + finding, ok := obj.(testapi.FindingData) + if !ok { + return true + } + isLicense := isLicenseFinding(finding) + return !isLicense + } +} + // hasSuppression checks if a finding has any suppression. func hasSuppression(finding testapi.FindingData) bool { if finding.Attributes.Suppression == nil { @@ -299,7 +335,8 @@ func getCliTemplateFuncMap(tmpl *template.Template) template.FuncMap { fnMap["divider"] = RenderDivider fnMap["title"] = RenderTitle fnMap["renderToString"] = renderTemplateToString(tmpl) - fnMap["isLicenseFinding"] = isLicenseFinding + fnMap["isLicenseFindingFilter"] = isLicenseFindingFilter + fnMap["isNotLicenseFindingFilter"] = isNotLicenseFindingFilter fnMap["isOpenFinding"] = isOpenFinding fnMap["isPendingFinding"] = isPendingFinding fnMap["isIgnoredFinding"] = isIgnoredFinding @@ -366,26 +403,13 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf defaultMap["formatDatetime"] = formatDatetime defaultMap["getSourceLocation"] = getSourceLocation defaultMap["getFindingId"] = getFindingID - defaultMap["hasPrefix"] = strings.HasPrefix defaultMap["isLicenseFinding"] = isLicenseFinding + defaultMap["hasPrefix"] = strings.HasPrefix defaultMap["constructDisplayPath"] = constructDisplayPath(config) return defaultMap } -// isLicenseFinding returns true if the finding is a license finding. -func isLicenseFinding(finding testapi.FindingData) bool { - if finding.Attributes != nil { - for _, problem := range finding.Attributes.Problems { - disc, err := problem.Discriminator() - if err == nil && disc == string(testapi.SnykLicense) { - return true - } - } - } - return false -} - // reverse reverses the order of elements in a slice. func reverse(v interface{}) []interface{} { l, err := mustReverse(v) diff --git a/internal/presenters/presenter_unified_finding_test.go b/internal/presenters/presenter_unified_finding_test.go index 525659d..b10f986 100644 --- a/internal/presenters/presenter_unified_finding_test.go +++ b/internal/presenters/presenter_unified_finding_test.go @@ -4,7 +4,10 @@ import ( "bytes" "testing" + "github.com/charmbracelet/lipgloss" + "github.com/gkampitakis/go-snaps/snaps" "github.com/google/uuid" + "github.com/muesli/termenv" "github.com/snyk/go-application-framework/pkg/apiclients/testapi" "github.com/snyk/go-application-framework/pkg/configuration" "github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas" @@ -32,7 +35,7 @@ func TestJsonWriter(t *testing.T) { assert.Equal(t, expected, buffer.String()) }) - t.Run("Don't strip whitespaces while writing", func(t *testing.T) { + t.Run("don't strip whitespaces while writing", func(t *testing.T) { buffer := &bytes.Buffer{} writerUnderTest := presenters.NewJSONWriter(buffer, false) @@ -78,7 +81,9 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) { projectResult := &presenters.UnifiedProjectResult{ Findings: []testapi.FindingData{licenseFinding}, Summary: &json_schemas.TestSummary{ - SeverityOrderAsc: []string{"critical", "high", "medium", "low", "none"}, + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, Results: []json_schemas.TestSummaryResult{ { Severity: "medium", @@ -128,7 +133,9 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) { projectResult := &presenters.UnifiedProjectResult{ Findings: []testapi.FindingData{vulnFinding}, Summary: &json_schemas.TestSummary{ - SeverityOrderAsc: []string{"critical", "high", "medium", "low", "none"}, + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, Results: []json_schemas.TestSummaryResult{ { Severity: "high", @@ -152,4 +159,93 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) { output := buffer.String() assert.Contains(t, output, "Risk Score: 780") }) + + t.Run("snapshot test of human-readable output", func(t *testing.T) { + // setup + config := configuration.New() + buffer := &bytes.Buffer{} + lipgloss.SetColorProfile(termenv.Ascii) + + riskScore := uint16(780) + problemID := "SNYK-JS-VM2-5537100" + vulnFinding := testapi.FindingData{ + Id: util.Ptr(uuid.MustParse("22222222-2222-2222-2222-222222222222")), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "High severity vulnerability", + Risk: testapi.Risk{ + RiskScore: &testapi.RiskScore{ + Value: riskScore, + }, + }, + Rating: testapi.Rating{ + Severity: testapi.Severity("high"), + }, + Problems: func() []testapi.Problem { + var p testapi.Problem + err := p.FromSnykVulnProblem(testapi.SnykVulnProblem{ + Id: problemID, + Source: testapi.SnykVuln, + }) + assert.NoError(t, err) + return []testapi.Problem{p} + }(), + }, + } + + licProblemID := "snyk:lic:npm:web3-core:LGPL-3.0" + licenseFinding := testapi.FindingData{ + Id: util.Ptr(uuid.MustParse("33333333-3333-3333-3333-333333333333")), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "LGPL-3.0 license", + Rating: testapi.Rating{ + Severity: testapi.Severity("medium"), + }, + Problems: func() []testapi.Problem { + var p testapi.Problem + err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{ + Id: licProblemID, + License: string(testapi.SnykLicense), + }) + assert.NoError(t, err) + return []testapi.Problem{p} + }(), + }, + } + + projectResult := &presenters.UnifiedProjectResult{ + Findings: []testapi.FindingData{vulnFinding, licenseFinding}, + Summary: &json_schemas.TestSummary{ + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, + Results: []json_schemas.TestSummaryResult{ + { + Severity: "high", + Open: 1, + Total: 1, + }, + { + Severity: "medium", + Open: 1, + Total: 1, + }, + }, + }, + } + + presenter := presenters.NewUnifiedFindingsRenderer( + []*presenters.UnifiedProjectResult{projectResult}, + config, + buffer, + ) + + // execute + err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType) + + // assert + assert.NoError(t, err) + snaps.MatchSnapshot(t, buffer.String()) + }) } diff --git a/internal/presenters/templates/unified_finding.tmpl b/internal/presenters/templates/unified_finding.tmpl index ca674a9..0c80dca 100644 --- a/internal/presenters/templates/unified_finding.tmpl +++ b/internal/presenters/templates/unified_finding.tmpl @@ -9,14 +9,16 @@ {{- with (getIntroducedThrough .) }} Introduced through: {{ . }} {{- end }} + {{- if not (isLicenseFinding .) }} - {{- $riskScore := getFieldValueFrom . "Attributes.Risk.RiskScore.Value" -}} + {{- $riskScore := getFieldValueFrom . "Attributes.Risk.RiskScore.Value" }} {{- if $riskScore }} Risk Score: {{ $riskScore }} {{- else }} Risk Score: N/A {{- end }} {{- end }} + {{- $reachability := getReachability . -}} {{- if ne $reachability "N/A" }} Reachability: {{ $reachability }} @@ -26,13 +28,17 @@ {{- define "details" -}} {{- $sortedFindings := .Findings | sortFindingBy "Attributes.Rating.Severity" .Summary.SeverityOrderAsc }} - {{- $openFindings := $sortedFindings | filterFinding (isOpenFinding) }} + {{- $allOpenFindings := $sortedFindings | filterFinding (isOpenFinding) }} + {{- $openFindings := $allOpenFindings | filterFinding (isNotLicenseFindingFilter) }} + {{- $licenseFindings := $allOpenFindings | filterFinding (isLicenseFindingFilter) }} {{- $pendingIgnoreFindings := $sortedFindings | filterFinding (isPendingFinding) }} {{- $ignoredFindings := $sortedFindings | filterFinding (isIgnoredFinding) }} {{- $hasOpenFindings := gt (len $openFindings) 0 }} + {{- $hasLicenseFindings := gt (len $licenseFindings) 0 }} {{- $hasPendingIgnoreFindings := gt (len $pendingIgnoreFindings) 0 }} {{- $hasIgnoredFindings := gt (len $ignoredFindings) 0 }} - {{- if $hasOpenFindings }}{{ "Open Issues" | title }} + + {{- if $hasOpenFindings }}{{ printf "Open issues: %d" (len $openFindings) | title }} {{- range $finding := $openFindings }} {{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}} {{- $title := getFieldValueFrom $finding "Attributes.Title" -}} @@ -40,6 +46,15 @@ {{- template "finding_details" $finding -}} {{- end }} {{- end }} + + {{- if $hasLicenseFindings }}{{ printf "License issues: %d" (len $licenseFindings) | title }} + {{- range $finding := $licenseFindings }} + {{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}} + {{- $title := getFieldValueFrom $finding "Attributes.Title" -}} + {{- printf " ✗ %s %s" (printf "[%s]" ($severity | toUpperCase) | renderInSeverityColor) ($title | bold) -}} + {{- template "finding_details" $finding -}} + {{- end }} + {{- end }} {{- if $hasPendingIgnoreFindings }} {{- range $finding := $pendingIgnoreFindings }} {{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}}