Skip to content

Commit ab586e7

Browse files
committed
feat: [DGP-789] group issues as "Security issues" and "License issues," and only if there are non-zero issues in that group
- Example test summary │ Total security issues: 1 │ │ Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │ │ Open : 1 [ 0 CRITICAL 1 HIGH 0 MEDIUM 0 LOW ] │ │ │ │ Total license issues: 1 │ │ Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │ │ Open : 1 [ 0 CRITICAL 0 HIGH 1 MEDIUM 0 LOW ] │ - adds tests for licenses without security issues, and vice versa
1 parent e7951ce commit ab586e7

File tree

4 files changed

+251
-37
lines changed

4 files changed

+251
-37
lines changed

internal/presenters/__snapshots__/presenter_unified_finding_test.snap

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Testing ...
44

5-
Open issues: 1
5+
Security issues: 1
66

77
✗ [HIGH] High severity vulnerability
88
Finding ID: SNYK-JS-VM2-5537100
@@ -17,17 +17,21 @@ License issues: 1
1717
Info: https://snyk.io/vuln/snyk:lic:npm:web3-core:LGPL-3.0
1818

1919

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-
╰────────────────────────────────────────────────────────────────╯
20+
╭─────────────────────────────────────────────────────────╮
21+
Test Summary
22+
│ │
23+
Organization: │
24+
Test type: open-source
25+
Project path: │
26+
│ │
27+
Total security issues: 1
28+
Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │
29+
Open : 1 [ 0 CRITICAL 1 HIGH 0 MEDIUM 0 LOW ] │
30+
│ │
31+
Total license issues: 1
32+
Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │
33+
Open : 1 [ 0 CRITICAL 0 HIGH 1 MEDIUM 0 LOW ] │
34+
╰─────────────────────────────────────────────────────────╯
3135
💡 Tip
3236

3337
To view ignored issues, use the --include-ignores option.

internal/presenters/funcs.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
1313
"github.com/snyk/go-application-framework/pkg/configuration"
14+
"github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas"
1415
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
1516
)
1617

@@ -406,10 +407,108 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
406407
defaultMap["isLicenseFinding"] = isLicenseFinding
407408
defaultMap["hasPrefix"] = strings.HasPrefix
408409
defaultMap["constructDisplayPath"] = constructDisplayPath(config)
410+
defaultMap["filterByIssueType"] = filterByIssueType
411+
defaultMap["getSummaryResultsByIssueType"] = getSummaryResultsByIssueType
412+
defaultMap["getIssueCountsTotal"] = getIssueCountsTotal
413+
defaultMap["getIssueCountsOpen"] = getIssueCountsOpen
414+
defaultMap["getIssueCountsIgnored"] = getIssueCountsIgnored
409415

410416
return defaultMap
411417
}
412418

419+
func getIssueCountsTotal(results []json_schemas.TestSummaryResult) (total int) {
420+
for _, res := range results {
421+
total += res.Total
422+
}
423+
return total
424+
}
425+
426+
func getIssueCountsOpen(results []json_schemas.TestSummaryResult) (open int) {
427+
for _, res := range results {
428+
open += res.Open
429+
}
430+
return open
431+
}
432+
433+
func getIssueCountsIgnored(results []json_schemas.TestSummaryResult) (ignored int) {
434+
for _, res := range results {
435+
ignored += res.Ignored
436+
}
437+
return ignored
438+
}
439+
440+
// filterByIssueType filters a list of finding summary results by issue type.
441+
func filterByIssueType(issueType string, summary *json_schemas.TestSummary) []json_schemas.TestSummaryResult {
442+
if summary.Type == issueType {
443+
return summary.Results
444+
}
445+
return []json_schemas.TestSummaryResult{}
446+
}
447+
448+
// getSummaryResultsByIssueType computes summary results for a specific issue type from findings.
449+
// issueType can be "vulnerability" or "license".
450+
func getSummaryResultsByIssueType(issueType string, findings []testapi.FindingData, orderAsc []string) []json_schemas.TestSummaryResult {
451+
if len(findings) == 0 {
452+
return []json_schemas.TestSummaryResult{}
453+
}
454+
455+
// Prepare counters by severity
456+
totalBySeverity := map[string]int{}
457+
openBySeverity := map[string]int{}
458+
ignoredBySeverity := map[string]int{}
459+
460+
for _, f := range findings {
461+
// Determine category membership
462+
isLicense := isLicenseFinding(f)
463+
if issueType == "license" && !isLicense {
464+
continue
465+
}
466+
if issueType == "vulnerability" && isLicense {
467+
continue
468+
}
469+
470+
severity := getFieldValueFrom(f, "Attributes.Rating.Severity")
471+
if severity == "" {
472+
// Skip if we cannot determine severity
473+
continue
474+
}
475+
476+
totalBySeverity[severity]++
477+
478+
// Determine suppression state
479+
isIgnored := false
480+
isOpen := true
481+
if f.Attributes != nil && f.Attributes.Suppression != nil {
482+
isOpen = false
483+
isIgnored = f.Attributes.Suppression.Status == testapi.SuppressionStatusIgnored
484+
}
485+
486+
if isOpen {
487+
openBySeverity[severity]++
488+
}
489+
if isIgnored {
490+
ignoredBySeverity[severity]++
491+
}
492+
}
493+
494+
// Build results in the provided order, but only include severities that appeared
495+
results := make([]json_schemas.TestSummaryResult, 0, len(totalBySeverity))
496+
for _, sev := range orderAsc {
497+
total := totalBySeverity[sev]
498+
if total == 0 {
499+
continue
500+
}
501+
results = append(results, json_schemas.TestSummaryResult{
502+
Severity: sev,
503+
Total: total,
504+
Open: openBySeverity[sev],
505+
Ignored: ignoredBySeverity[sev],
506+
})
507+
}
508+
509+
return results
510+
}
511+
413512
// reverse reverses the order of elements in a slice.
414513
func reverse(v interface{}) []interface{} {
415514
l, err := mustReverse(v)

internal/presenters/presenter_unified_finding_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,86 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
248248
assert.NoError(t, err)
249249
snaps.MatchSnapshot(t, buffer.String())
250250
})
251+
252+
// summary shows security only when there are vulnerability findings and no license findings
253+
t.Run("summary shows only security when no license issues", func(t *testing.T) {
254+
config := configuration.New()
255+
buffer := &bytes.Buffer{}
256+
257+
vulnFinding := testapi.FindingData{
258+
Id: util.Ptr(uuid.New()),
259+
Type: util.Ptr(testapi.Findings),
260+
Attributes: &testapi.FindingAttributes{
261+
Title: "High severity vulnerability",
262+
Rating: testapi.Rating{Severity: testapi.Severity("high")},
263+
},
264+
}
265+
266+
projectResult := &presenters.UnifiedProjectResult{
267+
Findings: []testapi.FindingData{vulnFinding},
268+
Summary: &json_schemas.TestSummary{
269+
Type: "open-source",
270+
Path: "test/path",
271+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
272+
Results: []json_schemas.TestSummaryResult{{Severity: "high", Open: 1, Total: 1}},
273+
},
274+
}
275+
276+
presenter := presenters.NewUnifiedFindingsRenderer(
277+
[]*presenters.UnifiedProjectResult{projectResult},
278+
config,
279+
buffer,
280+
)
281+
282+
err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType)
283+
assert.NoError(t, err)
284+
285+
out := buffer.String()
286+
assert.Contains(t, out, "Total security issues: 1")
287+
assert.NotContains(t, out, "Total license issues")
288+
})
289+
290+
// summary shows license only when there are license findings and no vulnerability findings
291+
t.Run("summary shows only license when no security issues", func(t *testing.T) {
292+
config := configuration.New()
293+
buffer := &bytes.Buffer{}
294+
295+
problemID := uuid.New().String()
296+
licenseFinding := testapi.FindingData{
297+
Id: util.Ptr(uuid.New()),
298+
Type: util.Ptr(testapi.Findings),
299+
Attributes: &testapi.FindingAttributes{
300+
Title: "LGPL-3.0 license",
301+
Rating: testapi.Rating{Severity: testapi.Severity("medium")},
302+
Problems: func() []testapi.Problem {
303+
var p testapi.Problem
304+
_ = p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{Id: problemID, License: string(testapi.SnykLicense)})
305+
return []testapi.Problem{p}
306+
}(),
307+
},
308+
}
309+
310+
projectResult := &presenters.UnifiedProjectResult{
311+
Findings: []testapi.FindingData{licenseFinding},
312+
Summary: &json_schemas.TestSummary{
313+
Type: "open-source",
314+
Path: "test/path",
315+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
316+
Results: []json_schemas.TestSummaryResult{{Severity: "medium", Open: 1, Total: 1}},
317+
},
318+
}
319+
320+
presenter := presenters.NewUnifiedFindingsRenderer(
321+
[]*presenters.UnifiedProjectResult{projectResult},
322+
config,
323+
buffer,
324+
)
325+
326+
err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType)
327+
assert.NoError(t, err)
328+
329+
out := buffer.String()
330+
assert.Contains(t, out, "Total license issues: 1")
331+
assert.NotContains(t, out, "Total security issues")
332+
})
251333
}

internal/presenters/templates/unified_finding.tmpl

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
{{- $hasPendingIgnoreFindings := gt (len $pendingIgnoreFindings) 0 }}
3939
{{- $hasIgnoredFindings := gt (len $ignoredFindings) 0 }}
4040

41-
{{- if $hasOpenFindings }}{{ printf "Open issues: %d" (len $openFindings) | title }}
41+
{{- if $hasOpenFindings }}{{ printf "Security issues: %d" (len $openFindings) | title }}
4242
{{- range $finding := $openFindings }}
4343
{{- $severity := getFieldValueFrom $finding "Attributes.Rating.Severity" -}}
4444
{{- $title := getFieldValueFrom $finding "Attributes.Title" -}}
@@ -91,35 +91,64 @@
9191
Test type: {{ if eq .Summary.Type "sast" }}Static code analysis{{else}}{{ .Summary.Type }}{{ end}}
9292
Project path: {{ getValueFromConfig "targetDirectory" }}
9393

94-
{{- $total := 0 }}{{- $open := 0 }}{{- $ignored := 0 }}
95-
{{- range $res := .Summary.Results }}
96-
{{- $total = add $total $res.Total }}
97-
{{- $open = add $open $res.Open }}
98-
{{- $ignored = add $ignored $res.Ignored }}
94+
{{- $vulnResults := getSummaryResultsByIssueType "vulnerability" .Findings .Summary.SeverityOrderAsc }}
95+
{{- $licenseResults := getSummaryResultsByIssueType "license" .Findings .Summary.SeverityOrderAsc }}
96+
97+
{{- if gt (len $vulnResults) 0 }}
98+
99+
Total security issues: {{ getIssueCountsTotal $vulnResults }}
100+
{{- $total := getIssueCountsTotal $vulnResults }}
101+
{{- $open := getIssueCountsOpen $vulnResults }}
102+
{{- $ignored := getIssueCountsIgnored $vulnResults }}
103+
Ignored: {{ $ignored }} [
104+
{{- range $severity := .Summary.SeverityOrderAsc | reverse }}
105+
{{- $count := 0 }}
106+
{{- range $res := $vulnResults }}
107+
{{- if eq $res.Severity $severity }}
108+
{{- $count = $res.Ignored }}
109+
{{- end }}
110+
{{- end }}
111+
{{- print " " $count " " $severity " " | toUpperCase | renderInSeverityColor }}
112+
{{- end}}]
113+
Open : {{ $open }} [
114+
{{- range $severity := .Summary.SeverityOrderAsc | reverse }}
115+
{{- $count := 0 }}
116+
{{- range $res := $vulnResults }}
117+
{{- if eq $res.Severity $severity }}
118+
{{- $count = $res.Open }}
119+
{{- end }}
120+
{{- end }}
121+
{{- print " " $count " " $severity " " | toUpperCase | renderInSeverityColor }}
122+
{{- end}}]
99123
{{- end }}
100124

101-
Total issues: {{ $total }}
102-
{{- if gt $total 0}}
103-
Ignored issues: {{ print $ignored | bold }} [
125+
{{- if gt (len $licenseResults) 0 }}
126+
127+
Total license issues: {{ getIssueCountsTotal $licenseResults }}
128+
{{- $total := getIssueCountsTotal $licenseResults }}
129+
{{- $open := getIssueCountsOpen $licenseResults }}
130+
{{- $ignored := getIssueCountsIgnored $licenseResults }}
131+
Ignored: {{ $ignored }} [
104132
{{- range $severity := .Summary.SeverityOrderAsc | reverse }}
105-
{{- $countFound := 0 }}
106-
{{- range $res := $.Summary.Results }}
107-
{{- if eq $res.Severity $severity }}
108-
{{- $countFound = $res.Ignored }}
109-
{{- end }}
110-
{{- end}}
111-
{{- print " " $countFound " " $severity " " | toUpperCase | renderInSeverityColor }}
133+
{{- $count := 0 }}
134+
{{- range $res := $licenseResults }}
135+
{{- if eq $res.Severity $severity }}
136+
{{- $count = $res.Ignored }}
137+
{{- end }}
138+
{{- end }}
139+
{{- print " " $count " " $severity " " | toUpperCase | renderInSeverityColor }}
112140
{{- end}}]
113-
Open issues: {{ print $open | bold }} [
141+
Open : {{ $open }} [
114142
{{- range $severity := .Summary.SeverityOrderAsc | reverse }}
115-
{{- $countFound := 0 }}
116-
{{- range $res := $.Summary.Results }}
117-
{{- if eq $res.Severity $severity }}
118-
{{- $countFound = $res.Open }}
119-
{{- end }}
120-
{{- end}}
121-
{{- print " " $countFound " " $severity " " | toUpperCase | renderInSeverityColor }}
122-
{{- end}}]{{- end}}
143+
{{- $count := 0 }}
144+
{{- range $res := $licenseResults }}
145+
{{- if eq $res.Severity $severity }}
146+
{{- $count = $res.Open }}
147+
{{- end }}
148+
{{- end }}
149+
{{- print " " $count " " $severity " " | toUpperCase | renderInSeverityColor }}
150+
{{- end}}]
151+
{{- end }}
123152
{{- end }} {{/* end summary */}}
124153

125154
{{- define "main" }}

0 commit comments

Comments
 (0)