Skip to content

Commit 27373e5

Browse files
Merge pull request #47 from snyk/feat/group-licenses
feat: DGP-789 - group text output by Open Issues and License issues
2 parents bf0d0d5 + 0da1e9b commit 27373e5

File tree

5 files changed

+527
-55
lines changed

5 files changed

+527
-55
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[TestUnifiedFindingPresenter_CliOutput/snapshot_test_of_human-readable_output - 1]
2+
3+
Testing ...
4+
5+
Security 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 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+
╰─────────────────────────────────────────────────────────╯
35+
💡 Tip
36+
37+
To view ignored issues, use the --include-ignores option.
38+
39+
40+
---

internal/presenters/funcs.go

Lines changed: 143 additions & 17 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

@@ -247,9 +248,12 @@ func isOpenFinding() func(obj any) bool {
247248
if !ok {
248249
return false
249250
}
250-
// A finding is considered "open" if it has no suppression information.
251-
// A rejected suppression is not represented as a status, but by the absence of a suppression object.
252-
return finding.Attributes.Suppression == nil
251+
// Treat findings as open unless they are explicitly ignored.
252+
// Pending ignore approvals and other statuses remain visible as open issues.
253+
if finding.Attributes == nil || finding.Attributes.Suppression == nil {
254+
return true
255+
}
256+
return finding.Attributes.Suppression.Status != testapi.SuppressionStatusIgnored
253257
}
254258
}
255259

@@ -275,14 +279,50 @@ func isIgnoredFinding() func(obj any) bool {
275279
}
276280
}
277281

282+
// isLicenseFinding returns true if the finding is a license finding.
283+
func isLicenseFinding(finding testapi.FindingData) bool {
284+
if finding.Attributes != nil {
285+
for _, problem := range finding.Attributes.Problems {
286+
disc, err := problem.Discriminator()
287+
if err == nil && disc == string(testapi.SnykLicense) {
288+
return true
289+
}
290+
}
291+
}
292+
return false
293+
}
294+
295+
// isLicenseFindingFilter returns a filter function that checks if a finding is a license finding.
296+
func isLicenseFindingFilter() func(obj any) bool {
297+
return func(obj any) bool {
298+
finding, ok := obj.(testapi.FindingData)
299+
if !ok {
300+
return false
301+
}
302+
return isLicenseFinding(finding)
303+
}
304+
}
305+
306+
// isNotLicenseFindingFilter returns a function that checks if a finding is not a license finding.
307+
func isNotLicenseFindingFilter() func(obj any) bool {
308+
return func(obj any) bool {
309+
finding, ok := obj.(testapi.FindingData)
310+
if !ok {
311+
return true
312+
}
313+
isLicense := isLicenseFinding(finding)
314+
return !isLicense
315+
}
316+
}
317+
278318
// hasSuppression checks if a finding has any suppression.
279319
func hasSuppression(finding testapi.FindingData) bool {
280-
if finding.Attributes.Suppression == nil {
320+
if finding.Attributes == nil || finding.Attributes.Suppression == nil {
281321
return false
282322
}
283323

284-
// If a suppression object exists, the finding is considered suppressed (either ignored or pending).
285-
return true
324+
// Treat as suppressed unless the suppression status is "other" (treating as rejected).
325+
return finding.Attributes.Suppression.Status != testapi.SuppressionStatusOther
286326
}
287327

288328
// getCliTemplateFuncMap returns the template function map for CLI rendering.
@@ -299,7 +339,8 @@ func getCliTemplateFuncMap(tmpl *template.Template) template.FuncMap {
299339
fnMap["divider"] = RenderDivider
300340
fnMap["title"] = RenderTitle
301341
fnMap["renderToString"] = renderTemplateToString(tmpl)
302-
fnMap["isLicenseFinding"] = isLicenseFinding
342+
fnMap["isLicenseFindingFilter"] = isLicenseFindingFilter
343+
fnMap["isNotLicenseFindingFilter"] = isNotLicenseFindingFilter
303344
fnMap["isOpenFinding"] = isOpenFinding
304345
fnMap["isPendingFinding"] = isPendingFinding
305346
fnMap["isIgnoredFinding"] = isIgnoredFinding
@@ -366,24 +407,109 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
366407
defaultMap["formatDatetime"] = formatDatetime
367408
defaultMap["getSourceLocation"] = getSourceLocation
368409
defaultMap["getFindingId"] = getFindingID
369-
defaultMap["hasPrefix"] = strings.HasPrefix
370410
defaultMap["isLicenseFinding"] = isLicenseFinding
411+
defaultMap["hasPrefix"] = strings.HasPrefix
371412
defaultMap["constructDisplayPath"] = constructDisplayPath(config)
413+
defaultMap["filterByIssueType"] = filterByIssueType
414+
defaultMap["getSummaryResultsByIssueType"] = getSummaryResultsByIssueType
415+
defaultMap["getIssueCountsTotal"] = getIssueCountsTotal
416+
defaultMap["getIssueCountsOpen"] = getIssueCountsOpen
417+
defaultMap["getIssueCountsIgnored"] = getIssueCountsIgnored
372418

373419
return defaultMap
374420
}
375421

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

389515
// reverse reverses the order of elements in a slice.

0 commit comments

Comments
 (0)