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
5 changes: 3 additions & 2 deletions docs/guide/coverage/language/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ To find information about these JARs[^2], the same logic is used as for the base
`table` format only contains the name of root JAR[^2] . To get the full path to inner JARs[^2] use the `json` format.

### Licenses
Trivy detects licenses for a JAR[^2] from two sources, in this order:
Trivy detects licenses for a JAR[^2] from three sources, in this order:

1. **Embedded POM** — the `<licenses>` block of the embedded `META-INF/maven/<groupId>/<artifactId>/pom.xml`, matched to the package by `groupId:artifactId`.
2. **License files** — `LICENSE`, `LICENCE` or `COPYRIGHT` files (including variants like `LICENSE.txt`) located at the JAR[^2] root or directly under `META-INF/`. Their content is classified with the [license classifier](../../scanner/license.md). A license file carries no `groupId:artifactId`, so it is attached only when the JAR[^2] contains a single artifact; in uber/shaded JARs[^2] (multiple artifacts) the owner is ambiguous and such files are skipped.
2. **Jenkins plugin manifest** — `Plugin-License-Name` attributes in `META-INF/MANIFEST.MF`, including suffixed variants such as `Plugin-License-Name-2`.
3. **License files** — `LICENSE`, `LICENCE` or `COPYRIGHT` files (including variants like `LICENSE.txt`) located at the JAR[^2] root or directly under `META-INF/`. Their content is classified with the [license classifier](../../scanner/license.md). A license file carries no `groupId:artifactId`, so it is attached only when the JAR[^2] contains a single artifact; in uber/shaded JARs[^2] (multiple artifacts) the owner is ambiguous and such files are skipped.

Notes and limitations:

Expand Down
7 changes: 4 additions & 3 deletions pkg/dependency/parser/java/jar/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package jar
// Bridge to expose jar parser internals to tests in the jar_test package.

var (
EmbeddedPomGAV = embeddedPomGAV
DecodePomLicenses = decodePomLicenses
IsJarLicenseFile = isJarLicenseFile
EmbeddedPomGAV = embeddedPomGAV
DecodePomLicenses = decodePomLicenses
IsJarLicenseFile = isJarLicenseFile
ParsePluginLicenseName = parsePluginLicenseName
)
77 changes: 66 additions & 11 deletions pkg/dependency/parser/java/jar/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ func (p *Parser) parsePackages(filePath string, size int64, r xio.ReadSeekerAt)
}
}

// Classify and attach the LICENSE file now that the jar's own artifact is resolved
// (it may have been added above from MANIFEST.MF / SHA-1 / file name).
// Attach license sources that depend on the jar's own artifact after it has
// been resolved (it may have been added above from MANIFEST.MF / SHA-1 / file
// name). Manifest attributes are cheap to parse, so try them before falling
// back to classifying packed LICENSE files.
attachManifestLicenses(pkgs, fileProps.FilePath, m.licenseNames())
p.attachFileLicenses(pkgs, fileProps.FilePath, licenseFile)

return pkgs, nil, nil
Expand Down Expand Up @@ -327,31 +330,53 @@ func attachPomLicenses(pkgs []ftypes.Package, pomLicenses map[string][]string) {
}
}

// attachFileLicenses classifies the LICENSE file packed in a jar and attaches it to the
// jar's own package, but only when the owner is unambiguous: a single LICENSE file, a
// single package belonging to this jar, and no license from its pom.xml yet.
func (p *Parser) attachFileLicenses(pkgs []ftypes.Package, filePath string, licenseFile *zip.File) {
if licenseFile == nil {
// attachManifestLicenses attaches Jenkins plugin licenses declared in MANIFEST.MF
// to the jar's own package when a single unambiguous package belongs to this jar.
func attachManifestLicenses(pkgs []ftypes.Package, filePath string, licenses []string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A lot of duplicate code.
You can create a common function for attachFileLicenses() and attachManifestLicenses().
something like that:

 // ownJarPackage returns the single package that belongs to filePath and still
  // needs a license, or nil when there is none, the owner already has a license,
  // or the owner is ambiguous (multiple packages share the jar's file path, e.g.
  // an uber jar).
  func ownJarPackage(pkgs []ftypes.Package, filePath string) *ftypes.Package {
        var pkg *ftypes.Package
        for i := range pkgs {
                if pkgs[i].FilePath != filePath {
                        continue
                }
                if pkg != nil {
                        return nil // more than one package belongs to this jar
                }
                pkg = &pkgs[i]
        }
        if pkg == nil || len(pkg.Licenses) > 0 {
                return nil
        }
        return pkg
  }

if len(licenses) == 0 {
return
}

pkg := ownJarPackage(pkgs, filePath)
if pkg == nil {
return
}

pkg.Licenses = licenses
}

// ownJarPackage returns the single package that belongs to filePath and still
// needs a license, or nil when there is none, the owner already has a license,
// or the owner is ambiguous.
func ownJarPackage(pkgs []ftypes.Package, filePath string) *ftypes.Package {
var pkg *ftypes.Package

for i := range pkgs {
if pkgs[i].FilePath != filePath {
continue
}
if pkg != nil {
return // more than one package belongs to this jar
return nil // more than one package belongs to this jar
}
pkg = &pkgs[i]
}

if pkg == nil {
return // no package belongs to this jar
if pkg == nil || len(pkg.Licenses) > 0 {
return nil
}
return pkg
}

if len(pkg.Licenses) > 0 {
// attachFileLicenses classifies the LICENSE file packed in a jar and attaches it to the
// jar's own package, but only when the owner is unambiguous: a single LICENSE file, a
// single package belonging to this jar, and no license from its pom.xml yet.
func (p *Parser) attachFileLicenses(pkgs []ftypes.Package, filePath string, licenseFile *zip.File) {
if licenseFile == nil {
return
}

pkg := ownJarPackage(pkgs, filePath)
if pkg == nil {
return
}

Expand Down Expand Up @@ -583,6 +608,7 @@ type manifest struct {
bundleName string
bundleVersion string
bundleSymbolicName string
pluginLicenseNames []string
}

func parseManifest(f *zip.File) (manifest, error) {
Expand Down Expand Up @@ -626,6 +652,8 @@ func parseManifest(f *zip.File) (manifest, error) {
m.bundleName = strings.TrimPrefix(line, "Bundle-Name:")
case strings.HasPrefix(line, "Bundle-SymbolicName:"):
m.bundleSymbolicName = strings.TrimPrefix(line, "Bundle-SymbolicName:")
case strings.HasPrefix(line, "Plugin-License-Name"):
m.pluginLicenseNames = append(m.pluginLicenseNames, line)
}
}

Expand All @@ -635,6 +663,33 @@ func parseManifest(f *zip.File) (manifest, error) {
return m, nil
}

// parsePluginLicenseName extracts the license name from a single Jenkins
// Plugin-License-Name[-N] manifest line, or an empty string when the line is not
// such an attribute or carries no value.
func parsePluginLicenseName(line string) string {
key, value, ok := strings.Cut(line, ":")
if !ok {
return ""
}
if key != "Plugin-License-Name" {
if _, ok = strings.CutPrefix(key, "Plugin-License-Name-"); !ok {
return ""
}
}
return strings.TrimSpace(value)
}

// licenseNames returns the license names declared in the manifest.
func (m manifest) licenseNames() []string {
var names []string
for _, line := range m.pluginLicenseNames {
if name := parsePluginLicenseName(line); name != "" {
names = append(names, name)
}
}
return names
}

func (m manifest) properties(filePath string) Properties {
groupID, err := m.determineGroupID()
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions pkg/dependency/parser/java/jar/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ var (
FilePath: "testdata/multi-license-1.0.0.jar",
},
}

// Manually created: a Jenkins plugin with license information in MANIFEST.MF.
wantJenkinsPlugin = []ftypes.Package{
{
Name: "com.example:jenkins-plugin",
Version: "1.0.0",
FilePath: "testdata/license-from-jenkins-plugin.jar",
Licenses: []string{
"Apache License, Version 2.0",
"MIT License",
},
},
}
)

type apiResponse struct {
Expand Down Expand Up @@ -313,6 +326,12 @@ func TestParse(t *testing.T) {
file: "testdata/multi-license-1.0.0.jar",
want: wantMultiLicense,
},
{
name: "jenkins plugin manifest license",
file: "testdata/license-from-jenkins-plugin.jar",
offline: true,
want: wantJenkinsPlugin,
},
}

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -386,6 +405,47 @@ func TestParse(t *testing.T) {
}
}

func TestParsePluginLicenseName(t *testing.T) {
tests := []struct {
name string
line string
want string
}{
{
name: "plugin license name",
line: "Plugin-License-Name: Apache License, Version 2.0",
want: "Apache License, Version 2.0",
},
{
name: "suffixed plugin license name",
line: "Plugin-License-Name-2: MIT License",
want: "MIT License",
},
{
name: "trims license name",
line: "Plugin-License-Name: MIT License ",
want: "MIT License",
},
{
name: "empty license name",
line: "Plugin-License-Name: ",
want: "",
},
{
name: "non license line",
line: "Plugin-License-Url: https://opensource.org/licenses/MIT",
want: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := jar.ParsePluginLicenseName(tt.line)
assert.Equal(t, tt.want, got)
})
}
}

func TestEmbeddedPomGAV(t *testing.T) {
tests := []struct {
name string
Expand Down
Binary file not shown.
Loading