Skip to content

Commit 303557a

Browse files
committed
feat: license instructions to human readable output
1 parent 08f6e2a commit 303557a

File tree

4 files changed

+213
-2
lines changed

4 files changed

+213
-2
lines changed

internal/presenters/__snapshots__/presenter_unified_finding_test.snap

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ License issues: 1
1515
✗ [MEDIUM] LGPL-3.0 license
1616
Finding ID: snyk:lic:npm:web3-core:LGPL-3.0
1717
Info: https://snyk.io/vuln/snyk:lic:npm:web3-core:LGPL-3.0
18+
Instructions: LGPL-3.0: This license requires source code disclosure when modified.
1819

1920

2021
╭─────────────────────────────────────────────────────────╮
@@ -38,3 +39,34 @@ License issues: 1
3839

3940

4041
---
42+
43+
[TestUnifiedFindingPresenter_CliOutput/snapshot_test_with_multiple_license_instructions - 1]
44+
45+
Testing ...
46+
47+
License issues: 1
48+
49+
✗ [HIGH] GPL-3.0 OR MIT license
50+
Finding ID: snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT
51+
Info: https://snyk.io/vuln/snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT
52+
Instructions: GPL-3.0: Strong copyleft license. Requires source code disclosure for modifications.
53+
MIT: Permissive license. Must include original copyright notice.
54+
55+
56+
╭─────────────────────────────────────────────────────────╮
57+
Test Summary
58+
│ │
59+
Organization: │
60+
Test type: open-source
61+
Project path: │
62+
│ │
63+
Total license issues: 1
64+
Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │
65+
Open : 1 [ 0 CRITICAL 1 HIGH 0 MEDIUM 0 LOW ] │
66+
╰─────────────────────────────────────────────────────────╯
67+
💡 Tip
68+
69+
To view ignored issues, use the --include-ignores option.
70+
71+
72+
---

internal/presenters/funcs.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ import (
1515
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
1616
)
1717

18-
const notApplicable = "N/A"
18+
const (
19+
notApplicable = "N/A"
20+
// Template field indentation: 3 spaces + "Instructions: " = 17 spaces for continuation.
21+
instructionsContinuationIndent = " "
22+
)
1923

2024
// add returns the sum of two integers.
2125
func add(a, b int) int {
@@ -311,6 +315,55 @@ func isLicenseFinding(finding testapi.FindingData) bool {
311315
return false
312316
}
313317

318+
// getLicenseInstructions returns license instructions for a license finding.
319+
// For packages with multiple licenses, instructions are formatted as "License: instruction"
320+
// on separate lines.
321+
func getLicenseInstructions(finding testapi.FindingData) string {
322+
if finding.Attributes == nil {
323+
return ""
324+
}
325+
326+
for _, problem := range finding.Attributes.Problems {
327+
disc, err := problem.Discriminator()
328+
if err != nil {
329+
continue
330+
}
331+
332+
if disc != string(testapi.SnykLicense) {
333+
continue
334+
}
335+
336+
p, err := problem.AsSnykLicenseProblem()
337+
if err != nil {
338+
continue
339+
}
340+
341+
if len(p.Instructions) == 0 {
342+
continue
343+
}
344+
345+
instructions := buildInstructionsList(p.Instructions)
346+
if len(instructions) > 0 {
347+
return strings.Join(instructions, "\n"+instructionsContinuationIndent)
348+
}
349+
}
350+
return ""
351+
}
352+
353+
// buildInstructionsList formats license instructions for display.
354+
// Each instruction is prefixed with its license name for clarity.
355+
func buildInstructionsList(instructionsList []testapi.SnykvulndbLicenseInstructions) []string {
356+
instructions := make([]string, 0, len(instructionsList))
357+
358+
for _, inst := range instructionsList {
359+
if inst.Content == "" {
360+
continue
361+
}
362+
instructions = append(instructions, fmt.Sprintf("%s: %s", inst.License, inst.Content))
363+
}
364+
return instructions
365+
}
366+
314367
// isLicenseFindingFilter returns a filter function that checks if a finding is a license finding.
315368
func isLicenseFindingFilter() func(obj any) bool {
316369
return func(obj any) bool {
@@ -427,6 +480,7 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf
427480
defaultMap["getSourceLocation"] = getSourceLocation
428481
defaultMap["getFindingId"] = getFindingID
429482
defaultMap["isLicenseFinding"] = isLicenseFinding
483+
defaultMap["getLicenseInstructions"] = getLicenseInstructions
430484
defaultMap["hasPrefix"] = strings.HasPrefix
431485
defaultMap["constructDisplayPath"] = constructDisplayPath(config)
432486
defaultMap["filterByIssueType"] = filterByIssueType

internal/presenters/presenter_unified_finding_test.go

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,13 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
206206
var p testapi.Problem
207207
err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{
208208
Id: licProblemID,
209-
License: string(testapi.SnykLicense),
209+
License: "LGPL-3.0",
210+
Instructions: []testapi.SnykvulndbLicenseInstructions{
211+
{
212+
License: "LGPL-3.0",
213+
Content: "This license requires source code disclosure when modified.",
214+
},
215+
},
210216
})
211217
assert.NoError(t, err)
212218
return []testapi.Problem{p}
@@ -249,6 +255,69 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) {
249255
snaps.MatchSnapshot(t, buffer.String())
250256
})
251257

258+
t.Run("snapshot test with multiple license instructions", func(t *testing.T) {
259+
config := configuration.New()
260+
buffer := &bytes.Buffer{}
261+
lipgloss.SetColorProfile(termenv.Ascii)
262+
263+
// Create a dual-licensed package with instructions for each license
264+
dualLicenseFinding := testapi.FindingData{
265+
Id: util.Ptr(uuid.MustParse("44444444-4444-4444-4444-444444444444")),
266+
Type: util.Ptr(testapi.Findings),
267+
Attributes: &testapi.FindingAttributes{
268+
Title: "GPL-3.0 OR MIT license",
269+
Rating: testapi.Rating{
270+
Severity: testapi.Severity("high"),
271+
},
272+
Problems: func() []testapi.Problem {
273+
var p testapi.Problem
274+
err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{
275+
Id: "snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT",
276+
License: "GPL-3.0 OR MIT",
277+
Instructions: []testapi.SnykvulndbLicenseInstructions{
278+
{
279+
License: "GPL-3.0",
280+
Content: "Strong copyleft license. Requires source code disclosure for modifications.",
281+
},
282+
{
283+
License: "MIT",
284+
Content: "Permissive license. Must include original copyright notice.",
285+
},
286+
},
287+
})
288+
assert.NoError(t, err)
289+
return []testapi.Problem{p}
290+
}(),
291+
},
292+
}
293+
294+
projectResult := &presenters.UnifiedProjectResult{
295+
Findings: []testapi.FindingData{dualLicenseFinding},
296+
Summary: &json_schemas.TestSummary{
297+
Type: "open-source",
298+
Path: "test/path",
299+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
300+
Results: []json_schemas.TestSummaryResult{
301+
{
302+
Severity: "high",
303+
Open: 1,
304+
Total: 1,
305+
},
306+
},
307+
},
308+
}
309+
310+
presenter := presenters.NewUnifiedFindingsRenderer(
311+
[]*presenters.UnifiedProjectResult{projectResult},
312+
config,
313+
buffer,
314+
)
315+
316+
err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType)
317+
assert.NoError(t, err)
318+
snaps.MatchSnapshot(t, buffer.String())
319+
})
320+
252321
// summary shows security only when there are vulnerability findings and no license findings
253322
t.Run("summary shows only security when no license issues", func(t *testing.T) {
254323
config := configuration.New()
@@ -437,3 +506,53 @@ func TestUnifiedFindingPresenter_Ignored_ShownInIgnoredSectionWithBang(t *testin
437506
// Ignored entries appear with ! and IGNORED label
438507
assert.Contains(t, out, " ! [IGNORED] [MEDIUM] Ignored Suppression Finding")
439508
}
509+
510+
// TestUnifiedFindingPresenter_LicenseInstructions verifies that license instructions appear in output.
511+
func TestUnifiedFindingPresenter_LicenseInstructions(t *testing.T) {
512+
config := configuration.New()
513+
buffer := &bytes.Buffer{}
514+
lipgloss.SetColorProfile(termenv.Ascii)
515+
516+
licProblem := testapi.SnykLicenseProblem{
517+
Id: "snyk:lic:npm:web3-core:LGPL-3.0",
518+
License: "LGPL-3.0",
519+
Instructions: []testapi.SnykvulndbLicenseInstructions{
520+
{
521+
License: "LGPL-3.0",
522+
Content: "This license requires you to disclose source code changes.",
523+
},
524+
},
525+
}
526+
527+
var p testapi.Problem
528+
err := p.FromSnykLicenseProblem(licProblem)
529+
assert.NoError(t, err)
530+
531+
licenseFinding := testapi.FindingData{
532+
Id: util.Ptr(uuid.New()),
533+
Type: util.Ptr(testapi.Findings),
534+
Attributes: &testapi.FindingAttributes{
535+
Title: "LGPL-3.0 license",
536+
Rating: testapi.Rating{Severity: testapi.Severity("medium")},
537+
Problems: []testapi.Problem{p},
538+
},
539+
}
540+
541+
projectResult := &presenters.UnifiedProjectResult{
542+
Findings: []testapi.FindingData{licenseFinding},
543+
Summary: &json_schemas.TestSummary{
544+
Type: "open-source",
545+
Path: "test/path",
546+
SeverityOrderAsc: []string{"low", "medium", "high", "critical"},
547+
Results: []json_schemas.TestSummaryResult{{Severity: "medium", Open: 1, Total: 1}},
548+
},
549+
}
550+
551+
presenter := presenters.NewUnifiedFindingsRenderer([]*presenters.UnifiedProjectResult{projectResult}, config, buffer)
552+
err = presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType)
553+
assert.NoError(t, err)
554+
555+
out := buffer.String()
556+
assert.Contains(t, out, "Instructions:")
557+
assert.Contains(t, out, "This license requires you to disclose source code changes.")
558+
}

internal/presenters/templates/unified_finding.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
Reachability: {{ $reachability }}
2525
{{- end }}
2626

27+
{{- if isLicenseFinding . }}
28+
{{- with (getLicenseInstructions .) }}
29+
Instructions: {{ . }}
30+
{{- end }}
31+
{{- end }}
32+
2733
{{end}}
2834

2935
{{- define "details" -}}

0 commit comments

Comments
 (0)