Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.


╭─────────────────────────────────────────────────────────╮
Expand All @@ -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.


---
52 changes: 51 additions & 1 deletion internal/presenters/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a quick look at the CLI screenshot here it looks like some of the text is bold, coloured etc., e.g. in this line everything from the bullet point to the colon is white and the text after is grey. Do we need to mirror this here? (The formatting might be happening elsewhere so I might be missing something)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how easy/hard it is to do and whether we even want it, just wondering out of curiosity

Copy link
Contributor Author

@snyk-will snyk-will Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I don't think that's too hard to add, however, it looks like the other fields (reachability, risk score etc.) are not in bold so I don't want to have this as the only field that stands out. @paulrosca-snyk do you have any insight? I can see you working on reachability rendering atm

}
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 {
Expand Down Expand Up @@ -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
Expand Down
221 changes: 220 additions & 1 deletion internal/presenters/presenter_unified_finding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
}
6 changes: 6 additions & 0 deletions internal/presenters/templates/unified_finding.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
Reachability: {{ $reachability }}
{{- end }}

{{- if isLicenseFinding . }}
{{- with (getLicenseInstructions .) }}
Legal instructions:{{ . }}
{{- end }}
{{- end }}

{{end}}

{{- define "details" -}}
Expand Down