Skip to content

Commit e7951ce

Browse files
committed
feat: DGP-789 - group text output by Open Issues and License issues
1 parent b8a7882 commit e7951ce

File tree

4 files changed

+192
-21
lines changed

4 files changed

+192
-21
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[TestUnifiedFindingPresenter_CliOutput/snapshot_test_of_human-readable_output - 1]
2+
3+
Testing ...
4+
5+
Open issues: 1
6+
7+
✗ [HIGH] High severity vulnerability
8+
Finding ID: SNYK-JS-VM2-5537100
9+
Info: https://snyk.io/vuln/SNYK-JS-VM2-5537100
10+
Risk Score: 780
11+
12+
13+
License issues: 1
14+
15+
✗ [MEDIUM] LGPL-3.0 license
16+
Finding ID: snyk:lic:npm:web3-core:LGPL-3.0
17+
Info: https://snyk.io/vuln/snyk:lic:npm:web3-core:LGPL-3.0
18+
19+
20+
╭────────────────────────────────────────────────────────────────╮
21+
Test Summary
22+
│ │
23+
Organization: │
24+
Test type: open-source
25+
Project path: │
26+
│ │
27+
Total issues: 2
28+
Ignored issues: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │
29+
Open issues: 2 [ 0 CRITICAL 1 HIGH 1 MEDIUM 0 LOW ] │
30+
╰────────────────────────────────────────────────────────────────╯
31+
💡 Tip
32+
33+
To view ignored issues, use the --include-ignores option.
34+
35+
36+
---

internal/presenters/funcs.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,42 @@ func isIgnoredFinding() func(obj any) bool {
275275
}
276276
}
277277

278+
// isLicenseFinding returns true if the finding is a license finding.
279+
func isLicenseFinding(finding testapi.FindingData) bool {
280+
if finding.Attributes != nil {
281+
for _, problem := range finding.Attributes.Problems {
282+
disc, err := problem.Discriminator()
283+
if err == nil && disc == string(testapi.SnykLicense) {
284+
return true
285+
}
286+
}
287+
}
288+
return false
289+
}
290+
291+
// isLicenseFindingFilter returns a filter function that checks if a finding is a license finding.
292+
func isLicenseFindingFilter() func(obj any) bool {
293+
return func(obj any) bool {
294+
finding, ok := obj.(testapi.FindingData)
295+
if !ok {
296+
return false
297+
}
298+
return isLicenseFinding(finding)
299+
}
300+
}
301+
302+
// isNotLicenseFindingFilter returns a function that checks if a finding is not a license finding.
303+
func isNotLicenseFindingFilter() func(obj any) bool {
304+
return func(obj any) bool {
305+
finding, ok := obj.(testapi.FindingData)
306+
if !ok {
307+
return true
308+
}
309+
isLicense := isLicenseFinding(finding)
310+
return !isLicense
311+
}
312+
}
313+
278314
// hasSuppression checks if a finding has any suppression.
279315
func hasSuppression(finding testapi.FindingData) bool {
280316
if finding.Attributes.Suppression == nil {
@@ -299,7 +335,8 @@ func getCliTemplateFuncMap(tmpl *template.Template) template.FuncMap {
299335
fnMap["divider"] = RenderDivider
300336
fnMap["title"] = RenderTitle
301337
fnMap["renderToString"] = renderTemplateToString(tmpl)
302-
fnMap["isLicenseFinding"] = isLicenseFinding
338+
fnMap["isLicenseFindingFilter"] = isLicenseFindingFilter
339+
fnMap["isNotLicenseFindingFilter"] = isNotLicenseFindingFilter
303340
fnMap["isOpenFinding"] = isOpenFinding
304341
fnMap["isPendingFinding"] = isPendingFinding
305342
fnMap["isIgnoredFinding"] = isIgnoredFinding
@@ -366,26 +403,13 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
366403
defaultMap["formatDatetime"] = formatDatetime
367404
defaultMap["getSourceLocation"] = getSourceLocation
368405
defaultMap["getFindingId"] = getFindingID
369-
defaultMap["hasPrefix"] = strings.HasPrefix
370406
defaultMap["isLicenseFinding"] = isLicenseFinding
407+
defaultMap["hasPrefix"] = strings.HasPrefix
371408
defaultMap["constructDisplayPath"] = constructDisplayPath(config)
372409

373410
return defaultMap
374411
}
375412

376-
// isLicenseFinding returns true if the finding is a license finding.
377-
func isLicenseFinding(finding testapi.FindingData) bool {
378-
if finding.Attributes != nil {
379-
for _, problem := range finding.Attributes.Problems {
380-
disc, err := problem.Discriminator()
381-
if err == nil && disc == string(testapi.SnykLicense) {
382-
return true
383-
}
384-
}
385-
}
386-
return false
387-
}
388-
389413
// reverse reverses the order of elements in a slice.
390414
func reverse(v interface{}) []interface{} {
391415
l, err := mustReverse(v)

internal/presenters/presenter_unified_finding_test.go

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"bytes"
55
"testing"
66

7+
"github.com/charmbracelet/lipgloss"
8+
"github.com/gkampitakis/go-snaps/snaps"
79
"github.com/google/uuid"
10+
"github.com/muesli/termenv"
811
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
912
"github.com/snyk/go-application-framework/pkg/configuration"
1013
"github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas"
@@ -32,7 +35,7 @@ func TestJsonWriter(t *testing.T) {
3235
assert.Equal(t, expected, buffer.String())
3336
})
3437

35-
t.Run("Don't strip whitespaces while writing", func(t *testing.T) {
38+
t.Run("don't strip whitespaces while writing", func(t *testing.T) {
3639
buffer := &bytes.Buffer{}
3740
writerUnderTest := presenters.NewJSONWriter(buffer, false)
3841

@@ -78,7 +81,9 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
7881
projectResult := &presenters.UnifiedProjectResult{
7982
Findings: []testapi.FindingData{licenseFinding},
8083
Summary: &json_schemas.TestSummary{
81-
SeverityOrderAsc: []string{"critical", "high", "medium", "low", "none"},
84+
Type: "open-source",
85+
Path: "test/path",
86+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
8287
Results: []json_schemas.TestSummaryResult{
8388
{
8489
Severity: "medium",
@@ -128,7 +133,9 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
128133
projectResult := &presenters.UnifiedProjectResult{
129134
Findings: []testapi.FindingData{vulnFinding},
130135
Summary: &json_schemas.TestSummary{
131-
SeverityOrderAsc: []string{"critical", "high", "medium", "low", "none"},
136+
Type: "open-source",
137+
Path: "test/path",
138+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
132139
Results: []json_schemas.TestSummaryResult{
133140
{
134141
Severity: "high",
@@ -152,4 +159,93 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
152159
output := buffer.String()
153160
assert.Contains(t, output, "Risk Score: 780")
154161
})
162+
163+
t.Run("snapshot test of human-readable output", func(t *testing.T) {
164+
// setup
165+
config := configuration.New()
166+
buffer := &bytes.Buffer{}
167+
lipgloss.SetColorProfile(termenv.Ascii)
168+
169+
riskScore := uint16(780)
170+
problemID := "SNYK-JS-VM2-5537100"
171+
vulnFinding := testapi.FindingData{
172+
Id: util.Ptr(uuid.MustParse("22222222-2222-2222-2222-222222222222")),
173+
Type: util.Ptr(testapi.Findings),
174+
Attributes: &testapi.FindingAttributes{
175+
Title: "High severity vulnerability",
176+
Risk: testapi.Risk{
177+
RiskScore: &testapi.RiskScore{
178+
Value: riskScore,
179+
},
180+
},
181+
Rating: testapi.Rating{
182+
Severity: testapi.Severity("high"),
183+
},
184+
Problems: func() []testapi.Problem {
185+
var p testapi.Problem
186+
err := p.FromSnykVulnProblem(testapi.SnykVulnProblem{
187+
Id: problemID,
188+
Source: testapi.SnykVuln,
189+
})
190+
assert.NoError(t, err)
191+
return []testapi.Problem{p}
192+
}(),
193+
},
194+
}
195+
196+
licProblemID := "snyk:lic:npm:web3-core:LGPL-3.0"
197+
licenseFinding := testapi.FindingData{
198+
Id: util.Ptr(uuid.MustParse("33333333-3333-3333-3333-333333333333")),
199+
Type: util.Ptr(testapi.Findings),
200+
Attributes: &testapi.FindingAttributes{
201+
Title: "LGPL-3.0 license",
202+
Rating: testapi.Rating{
203+
Severity: testapi.Severity("medium"),
204+
},
205+
Problems: func() []testapi.Problem {
206+
var p testapi.Problem
207+
err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{
208+
Id: licProblemID,
209+
License: string(testapi.SnykLicense),
210+
})
211+
assert.NoError(t, err)
212+
return []testapi.Problem{p}
213+
}(),
214+
},
215+
}
216+
217+
projectResult := &presenters.UnifiedProjectResult{
218+
Findings: []testapi.FindingData{vulnFinding, licenseFinding},
219+
Summary: &json_schemas.TestSummary{
220+
Type: "open-source",
221+
Path: "test/path",
222+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
223+
Results: []json_schemas.TestSummaryResult{
224+
{
225+
Severity: "high",
226+
Open: 1,
227+
Total: 1,
228+
},
229+
{
230+
Severity: "medium",
231+
Open: 1,
232+
Total: 1,
233+
},
234+
},
235+
},
236+
}
237+
238+
presenter := presenters.NewUnifiedFindingsRenderer(
239+
[]*presenters.UnifiedProjectResult{projectResult},
240+
config,
241+
buffer,
242+
)
243+
244+
// execute
245+
err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType)
246+
247+
// assert
248+
assert.NoError(t, err)
249+
snaps.MatchSnapshot(t, buffer.String())
250+
})
155251
}

internal/presenters/templates/unified_finding.tmpl

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
{{- with (getIntroducedThrough .) }}
1010
Introduced through: {{ . }}
1111
{{- end }}
12+
1213
{{- if not (isLicenseFinding .) }}
13-
{{- $riskScore := getFieldValueFrom . "Attributes.Risk.RiskScore.Value" -}}
14+
{{- $riskScore := getFieldValueFrom . "Attributes.Risk.RiskScore.Value" }}
1415
{{- if $riskScore }}
1516
Risk Score: {{ $riskScore }}
1617
{{- else }}
1718
Risk Score: N/A
1819
{{- end }}
1920
{{- end }}
21+
2022
{{- $reachability := getReachability . -}}
2123
{{- if ne $reachability "N/A" }}
2224
Reachability: {{ $reachability }}
@@ -26,20 +28,33 @@
2628

2729
{{- define "details" -}}
2830
{{- $sortedFindings := .Findings | sortFindingBy "Attributes.Rating.Severity" .Summary.SeverityOrderAsc }}
29-
{{- $openFindings := $sortedFindings | filterFinding (isOpenFinding) }}
31+
{{- $allOpenFindings := $sortedFindings | filterFinding (isOpenFinding) }}
32+
{{- $openFindings := $allOpenFindings | filterFinding (isNotLicenseFindingFilter) }}
33+
{{- $licenseFindings := $allOpenFindings | filterFinding (isLicenseFindingFilter) }}
3034
{{- $pendingIgnoreFindings := $sortedFindings | filterFinding (isPendingFinding) }}
3135
{{- $ignoredFindings := $sortedFindings | filterFinding (isIgnoredFinding) }}
3236
{{- $hasOpenFindings := gt (len $openFindings) 0 }}
37+
{{- $hasLicenseFindings := gt (len $licenseFindings) 0 }}
3338
{{- $hasPendingIgnoreFindings := gt (len $pendingIgnoreFindings) 0 }}
3439
{{- $hasIgnoredFindings := gt (len $ignoredFindings) 0 }}
35-
{{- if $hasOpenFindings }}{{ "Open Issues" | title }}
40+
41+
{{- if $hasOpenFindings }}{{ printf "Open issues: %d" (len $openFindings) | title }}
3642
{{- range $finding := $openFindings }}
3743
{{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}}
3844
{{- $title := getFieldValueFrom $finding "Attributes.Title" -}}
3945
{{- printf " ✗ %s %s" (printf "[%s]" ($severity | toUpperCase) | renderInSeverityColor) ($title | bold) -}}
4046
{{- template "finding_details" $finding -}}
4147
{{- end }}
4248
{{- end }}
49+
50+
{{- if $hasLicenseFindings }}{{ printf "License issues: %d" (len $licenseFindings) | title }}
51+
{{- range $finding := $licenseFindings }}
52+
{{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}}
53+
{{- $title := getFieldValueFrom $finding "Attributes.Title" -}}
54+
{{- printf " ✗ %s %s" (printf "[%s]" ($severity | toUpperCase) | renderInSeverityColor) ($title | bold) -}}
55+
{{- template "finding_details" $finding -}}
56+
{{- end }}
57+
{{- end }}
4358
{{- if $hasPendingIgnoreFindings }}
4459
{{- range $finding := $pendingIgnoreFindings }}
4560
{{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}}

0 commit comments

Comments
 (0)