diff --git a/internal/presenters/__snapshots__/presenter_unified_finding_test.snap b/internal/presenters/__snapshots__/presenter_unified_finding_test.snap index 278b60c..dcae50e 100755 --- a/internal/presenters/__snapshots__/presenter_unified_finding_test.snap +++ b/internal/presenters/__snapshots__/presenter_unified_finding_test.snap @@ -15,6 +15,8 @@ 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 + Legal instructions: + ○ for LGPL-3.0: This license requires source code disclosure when modified. ╭─────────────────────────────────────────────────────────╮ @@ -38,3 +40,64 @@ License issues: 1 --- + +[TestUnifiedFindingPresenter_CliOutput/snapshot_test_with_multiple_license_instructions - 1] + +Testing ... + +License issues: 1 + + ✗ [HIGH] GPL-3.0 OR MIT license + Finding ID: snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT + Info: https://snyk.io/vuln/snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT + Legal instructions: + ○ for GPL-3.0: Strong copyleft license. Requires source code disclosure for modifications. + ○ for MIT: Permissive license. Must include original copyright notice. + + +╭─────────────────────────────────────────────────────────╮ +│ Test Summary │ +│ │ +│ Organization: │ +│ Test type: open-source │ +│ Project path: │ +│ │ +│ Total license issues: 1 │ +│ Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │ +│ Open : 1 [ 0 CRITICAL 1 HIGH 0 MEDIUM 0 LOW ] │ +╰─────────────────────────────────────────────────────────╯ +💡 Tip + + To view ignored issues, use the --include-ignores option. + + +--- + +[TestUnifiedFindingPresenter_CliOutput/snapshot_test_with_license_without_instructions - 1] + +Testing ... + +License issues: 1 + + ✗ [MEDIUM] Apache-2.0 license + Finding ID: snyk:lic:npm:test-pkg:Apache-2.0 + Info: https://snyk.io/vuln/snyk:lic:npm:test-pkg:Apache-2.0 + + +╭─────────────────────────────────────────────────────────╮ +│ Test Summary │ +│ │ +│ Organization: │ +│ Test type: open-source │ +│ Project path: │ +│ │ +│ Total license issues: 1 │ +│ Ignored: 0 [ 0 CRITICAL 0 HIGH 0 MEDIUM 0 LOW ] │ +│ Open : 1 [ 0 CRITICAL 0 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 3c10c0f..2816410 100644 --- a/internal/presenters/funcs.go +++ b/internal/presenters/funcs.go @@ -15,7 +15,10 @@ import ( "github.com/snyk/go-application-framework/pkg/runtimeinfo" ) -const notApplicable = "N/A" +const ( + notApplicable = "N/A" + bulletPoint = "○" +) // add returns the sum of two integers. func add(a, b int) int { @@ -311,6 +314,52 @@ func isLicenseFinding(finding testapi.FindingData) bool { return false } +// getLicenseInstructions returns license instructions for a license finding. +func getLicenseInstructions(finding testapi.FindingData) string { + if finding.Attributes == nil { + return "" + } + + for _, problem := range finding.Attributes.Problems { + disc, err := problem.Discriminator() + if err != nil { + continue + } + + if disc != string(testapi.SnykLicense) { + continue + } + + p, err := problem.AsSnykLicenseProblem() + if err != nil { + continue + } + + if len(p.Instructions) == 0 { + continue + } + + instructions := buildInstructionsList(p.Instructions) + if len(instructions) > 0 { + return "\n" + strings.Join(instructions, "\n") + } + } + return "" +} + +// buildInstructionsList formats license instructions prefixing with a bullet point and license name. +func buildInstructionsList(instructionsList []testapi.SnykvulndbLicenseInstructions) []string { + instructions := make([]string, 0, len(instructionsList)) + + for _, inst := range instructionsList { + if inst.Content == "" { + continue + } + instructions = append(instructions, fmt.Sprintf(" %s for %s: %s", bulletPoint, inst.License, inst.Content)) + } + return instructions +} + // 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 { @@ -427,6 +476,7 @@ func getDefaultTemplateFuncMap(config configuration.Configuration, ri runtimeinf defaultMap["getSourceLocation"] = getSourceLocation defaultMap["getFindingId"] = getFindingID defaultMap["isLicenseFinding"] = isLicenseFinding + defaultMap["getLicenseInstructions"] = getLicenseInstructions defaultMap["hasPrefix"] = strings.HasPrefix defaultMap["constructDisplayPath"] = constructDisplayPath(config) defaultMap["filterByIssueType"] = filterByIssueType diff --git a/internal/presenters/presenter_unified_finding_test.go b/internal/presenters/presenter_unified_finding_test.go index c4e27e8..5048bb5 100644 --- a/internal/presenters/presenter_unified_finding_test.go +++ b/internal/presenters/presenter_unified_finding_test.go @@ -206,7 +206,13 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) { var p testapi.Problem err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{ Id: licProblemID, - License: string(testapi.SnykLicense), + License: "LGPL-3.0", + Instructions: []testapi.SnykvulndbLicenseInstructions{ + { + License: "LGPL-3.0", + Content: "This license requires source code disclosure when modified.", + }, + }, }) assert.NoError(t, err) return []testapi.Problem{p} @@ -249,6 +255,123 @@ func TestUnifiedFindingPresenter_CliOutput(t *testing.T) { snaps.MatchSnapshot(t, buffer.String()) }) + t.Run("snapshot test with multiple license instructions", func(t *testing.T) { + config := configuration.New() + buffer := &bytes.Buffer{} + lipgloss.SetColorProfile(termenv.Ascii) + + // Create a dual-licensed package with instructions for each license + dualLicenseFinding := testapi.FindingData{ + Id: util.Ptr(uuid.MustParse("44444444-4444-4444-4444-444444444444")), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "GPL-3.0 OR MIT license", + Rating: testapi.Rating{ + Severity: testapi.Severity("high"), + }, + Problems: func() []testapi.Problem { + var p testapi.Problem + err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{ + Id: "snyk:lic:npm:dual-pkg:GPL-3.0-OR-MIT", + License: "GPL-3.0 OR MIT", + Instructions: []testapi.SnykvulndbLicenseInstructions{ + { + License: "GPL-3.0", + Content: "Strong copyleft license. Requires source code disclosure for modifications.", + }, + { + License: "MIT", + Content: "Permissive license. Must include original copyright notice.", + }, + }, + }) + assert.NoError(t, err) + return []testapi.Problem{p} + }(), + }, + } + + projectResult := &presenters.UnifiedProjectResult{ + Findings: []testapi.FindingData{dualLicenseFinding}, + 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, + }, + }, + }, + } + + presenter := presenters.NewUnifiedFindingsRenderer( + []*presenters.UnifiedProjectResult{projectResult}, + config, + buffer, + ) + + err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType) + assert.NoError(t, err) + snaps.MatchSnapshot(t, buffer.String()) + }) + + t.Run("snapshot test with license without instructions", func(t *testing.T) { + config := configuration.New() + buffer := &bytes.Buffer{} + lipgloss.SetColorProfile(termenv.Ascii) + + // Create a license finding without instructions + licenseFinding := testapi.FindingData{ + Id: util.Ptr(uuid.MustParse("55555555-5555-5555-5555-555555555555")), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "Apache-2.0 license", + Rating: testapi.Rating{ + Severity: testapi.Severity("medium"), + }, + Problems: func() []testapi.Problem { + var p testapi.Problem + err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{ + Id: "snyk:lic:npm:test-pkg:Apache-2.0", + License: "Apache-2.0", + Instructions: []testapi.SnykvulndbLicenseInstructions{}, // No instructions + }) + assert.NoError(t, err) + return []testapi.Problem{p} + }(), + }, + } + + projectResult := &presenters.UnifiedProjectResult{ + Findings: []testapi.FindingData{licenseFinding}, + Summary: &json_schemas.TestSummary{ + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, + Results: []json_schemas.TestSummaryResult{ + { + Severity: "medium", + Open: 1, + Total: 1, + }, + }, + }, + } + + presenter := presenters.NewUnifiedFindingsRenderer( + []*presenters.UnifiedProjectResult{projectResult}, + config, + buffer, + ) + + err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType) + assert.NoError(t, err) + snaps.MatchSnapshot(t, buffer.String()) + }) + // summary shows security only when there are vulnerability findings and no license findings t.Run("summary shows only security when no license issues", func(t *testing.T) { config := configuration.New() @@ -437,3 +560,99 @@ func TestUnifiedFindingPresenter_Ignored_ShownInIgnoredSectionWithBang(t *testin // Ignored entries appear with ! and IGNORED label assert.Contains(t, out, " ! [IGNORED] [MEDIUM] Ignored Suppression Finding") } + +// verifies that license instructions appear in output. +func TestUnifiedFindingPresenter_LicenseInstructions(t *testing.T) { + config := configuration.New() + buffer := &bytes.Buffer{} + lipgloss.SetColorProfile(termenv.Ascii) + + licProblem := testapi.SnykLicenseProblem{ + Id: "snyk:lic:npm:web3-core:LGPL-3.0", + License: "LGPL-3.0", + Instructions: []testapi.SnykvulndbLicenseInstructions{ + { + License: "LGPL-3.0", + Content: "This license requires you to disclose source code changes.", + }, + }, + } + + var p testapi.Problem + err := p.FromSnykLicenseProblem(licProblem) + assert.NoError(t, err) + + licenseFinding := testapi.FindingData{ + Id: util.Ptr(uuid.New()), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "LGPL-3.0 license", + Rating: testapi.Rating{Severity: testapi.Severity("medium")}, + Problems: []testapi.Problem{p}, + }, + } + + projectResult := &presenters.UnifiedProjectResult{ + Findings: []testapi.FindingData{licenseFinding}, + Summary: &json_schemas.TestSummary{ + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, + Results: []json_schemas.TestSummaryResult{{Severity: "medium", Open: 1, Total: 1}}, + }, + } + + presenter := presenters.NewUnifiedFindingsRenderer([]*presenters.UnifiedProjectResult{projectResult}, config, buffer) + err = presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType) + assert.NoError(t, err) + + out := buffer.String() + assert.Contains(t, out, "Legal instructions:") + assert.Contains(t, out, "○ for LGPL-3.0: This license requires you to disclose source code changes.") +} + +// verifies that license findings without instructions don't show the instructions field. +func TestUnifiedFindingPresenter_LicenseWithoutInstructions(t *testing.T) { + config := configuration.New() + buffer := &bytes.Buffer{} + lipgloss.SetColorProfile(termenv.Ascii) + + licenseFinding := testapi.FindingData{ + Id: util.Ptr(uuid.New()), + Type: util.Ptr(testapi.Findings), + Attributes: &testapi.FindingAttributes{ + Title: "MIT license", + Rating: testapi.Rating{ + Severity: testapi.Severity("low"), + }, + Problems: func() []testapi.Problem { + var p testapi.Problem + err := p.FromSnykLicenseProblem(testapi.SnykLicenseProblem{ + Id: "snyk:lic:npm:test-pkg:MIT", + License: "MIT", + Instructions: []testapi.SnykvulndbLicenseInstructions{}, + }) + assert.NoError(t, err) + return []testapi.Problem{p} + }(), + }, + } + + projectResult := &presenters.UnifiedProjectResult{ + Findings: []testapi.FindingData{licenseFinding}, + Summary: &json_schemas.TestSummary{ + Type: "open-source", + Path: "test/path", + SeverityOrderAsc: []string{"low", "medium", "high", "critical"}, + Results: []json_schemas.TestSummaryResult{{Severity: "low", Open: 1, Total: 1}}, + }, + } + + presenter := presenters.NewUnifiedFindingsRenderer([]*presenters.UnifiedProjectResult{projectResult}, config, buffer) + err := presenter.RenderTemplate(presenters.DefaultTemplateFiles, presenters.DefaultMimeType) + assert.NoError(t, err) + + out := buffer.String() + assert.NotContains(t, out, "Legal instructions:") + assert.Contains(t, out, "MIT license") +} diff --git a/internal/presenters/templates/unified_finding.tmpl b/internal/presenters/templates/unified_finding.tmpl index 51dd3c4..c295d56 100644 --- a/internal/presenters/templates/unified_finding.tmpl +++ b/internal/presenters/templates/unified_finding.tmpl @@ -24,6 +24,12 @@ Reachability: {{ $reachability }} {{- end }} + {{- if isLicenseFinding . }} + {{- with (getLicenseInstructions .) }} + Legal instructions:{{ . }} + {{- end }} + {{- end }} + {{end}} {{- define "details" -}}