Skip to content

Commit 0a7b856

Browse files
committed
Merge branch 'main' of github.com:aquasecurity/trivy into fix-nested-jar-digest
# Conflicts: # pkg/dependency/parser/java/jar/parse.go # pkg/fanal/analyzer/language/java/jar/jar.go # pkg/fanal/analyzer/language/java/jar/jar_test.go
2 parents d4f8304 + b8a1ccd commit 0a7b856

22 files changed

Lines changed: 1188 additions & 573 deletions

File tree

docs/guide/coverage/language/java.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,15 @@ To find information about these JARs[^2], the same logic is used as for the base
3838
`table` format only contains the name of root JAR[^2] . To get the full path to inner JARs[^2] use the `json` format.
3939

4040
### Licenses
41-
Trivy detects licenses declared in the `<licenses>` block of the embedded `META-INF/maven/<groupId>/<artifactId>/pom.xml` and attaches them to the matching package.
41+
Trivy detects licenses for a JAR[^2] from two sources, in this order:
4242

43-
Coverage is limited: many JARs declare a license only in a parent POM (which is not expanded in the embedded `pom.xml`) or ship no Maven descriptor at all.
43+
1. **Embedded POM** — the `<licenses>` block of the embedded `META-INF/maven/<groupId>/<artifactId>/pom.xml`, matched to the package by `groupId:artifactId`.
44+
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.
45+
46+
Notes and limitations:
47+
48+
- Coverage is limited: many JARs[^2] declare a license only in a parent POM (which is not expanded in the embedded `pom.xml`) or ship no Maven descriptor at all.
49+
- A single license file may bundle the texts of third-party components (e.g. Spring or Tomcat artifacts), so a package can be reported with several licenses found in that file, not only its own.
4450

4551
## pom.xml
4652
Trivy parses your `pom.xml` file and tries to find files with dependencies from these local locations.

go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ require (
3737
github.com/bmatcuk/doublestar/v4 v4.10.0
3838
github.com/cenkalti/backoff/v6 v6.0.1
3939
github.com/cheggaaa/pb/v3 v3.1.7
40-
github.com/containerd/containerd/v2 v2.3.1
40+
github.com/containerd/containerd/v2 v2.3.2
4141
github.com/containerd/platforms v1.0.0-rc.4
4242
github.com/distribution/reference v0.6.0
4343
github.com/docker/cli v29.5.2+incompatible
@@ -113,12 +113,12 @@ require (
113113
github.com/zclconf/go-cty v1.18.1
114114
github.com/zclconf/go-cty-yaml v1.2.0
115115
go.etcd.io/bbolt v1.4.3
116-
golang.org/x/crypto v0.52.0
116+
golang.org/x/crypto v0.53.0
117117
golang.org/x/mod v0.36.0
118118
golang.org/x/net v0.55.0
119-
golang.org/x/sync v0.20.0
120-
golang.org/x/term v0.43.0
121-
golang.org/x/text v0.37.0
119+
golang.org/x/sync v0.21.0
120+
golang.org/x/term v0.44.0
121+
golang.org/x/text v0.38.0
122122
golang.org/x/tools v0.45.0
123123
golang.org/x/vuln v1.3.0
124124
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
@@ -463,7 +463,7 @@ require (
463463
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
464464
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
465465
golang.org/x/oauth2 v0.36.0 // indirect
466-
golang.org/x/sys v0.45.0 // indirect
466+
golang.org/x/sys v0.46.0 // indirect
467467
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
468468
golang.org/x/time v0.15.0 // indirect
469469
google.golang.org/api v0.274.0 // indirect

go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6
302302
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
303303
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
304304
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
305-
github.com/containerd/containerd/v2 v2.3.1 h1:4dVXBdlvotRBlaP2TmNbY/EGc06KJrMDDUqQdxX/HOk=
306-
github.com/containerd/containerd/v2 v2.3.1/go.mod h1:xVoxGPWZBwwph8DF2IbDhriLKdHfjdpO0b3wFP9wQ1I=
305+
github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM=
306+
github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
307307
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
308308
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
309309
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -1306,8 +1306,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
13061306
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
13071307
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
13081308
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
1309-
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
1310-
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
1309+
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
1310+
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
13111311
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
13121312
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
13131313
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
@@ -1346,8 +1346,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
13461346
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
13471347
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
13481348
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
1349-
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
1350-
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
1349+
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
1350+
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
13511351
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13521352
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13531353
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1380,22 +1380,22 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
13801380
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13811381
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13821382
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1383-
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
1384-
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
1383+
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
1384+
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
13851385
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
13861386
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
13871387
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
13881388
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
1389-
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
1390-
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
1389+
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
1390+
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
13911391
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
13921392
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
13931393
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
13941394
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
13951395
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
13961396
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
1397-
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
1398-
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
1397+
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
1398+
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
13991399
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
14001400
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
14011401
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

integration/testdata/spring4shell-jre11.json.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@
257257
"PkgPath": "usr/local/tomcat/webapps/helloworld.war/WEB-INF/lib/spring-beans-5.3.15.jar",
258258
"PkgIdentifier": {
259259
"PURL": "pkg:maven/org.springframework/spring-beans@5.3.15",
260-
"UID": "382ba53322e3ac2a"
260+
"UID": "59399c43172e6917"
261261
},
262262
"InstalledVersion": "5.3.15",
263263
"FixedVersion": "5.3.18",

integration/testdata/spring4shell-jre8.json.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@
257257
"PkgPath": "usr/local/tomcat/webapps/helloworld.war/WEB-INF/lib/spring-beans-5.3.15.jar",
258258
"PkgIdentifier": {
259259
"PURL": "pkg:maven/org.springframework/spring-beans@5.3.15",
260-
"UID": "382ba53322e3ac2a"
260+
"UID": "59399c43172e6917"
261261
},
262262
"InstalledVersion": "5.3.15",
263263
"FixedVersion": "5.3.18",

pkg/dependency/parser/java/jar/export_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ package jar
55
var (
66
EmbeddedPomGAV = embeddedPomGAV
77
DecodePomLicenses = decodePomLicenses
8+
IsJarLicenseFile = isJarLicenseFile
89
)

pkg/dependency/parser/java/jar/parse.go

Lines changed: 125 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/aquasecurity/trivy/pkg/digest"
2222
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
23+
"github.com/aquasecurity/trivy/pkg/licensing"
2324
"github.com/aquasecurity/trivy/pkg/log"
2425
xio "github.com/aquasecurity/trivy/pkg/x/io"
2526
xos "github.com/aquasecurity/trivy/pkg/x/os"
@@ -36,11 +37,12 @@ type Client interface {
3637
}
3738

3839
type Parser struct {
39-
logger *log.Logger
40-
rootFilePath string
41-
offline bool
42-
checksum bool
43-
size int64
40+
logger *log.Logger
41+
rootFilePath string
42+
offline bool
43+
checksum bool
44+
size int64
45+
licenseConfidenceLevel float64
4446

4547
client Client
4648
}
@@ -73,6 +75,12 @@ func WithSize(size int64) Option {
7375
}
7476
}
7577

78+
func WithLicenseClassifierConfidenceLevel(level float64) Option {
79+
return func(p *Parser) {
80+
p.licenseConfidenceLevel = level
81+
}
82+
}
83+
7684
func NewParser(c Client, opts ...Option) *Parser {
7785
p := &Parser{
7886
logger: log.WithPrefix("jar"),
@@ -120,15 +128,15 @@ func (p *Parser) parsePackages(filePath string, size int64, r xio.ReadSeekerAt)
120128
// e.g. spring-core-5.3.4-SNAPSHOT.jar => sprint-core, 5.3.4-SNAPSHOT
121129
fileProps := parseFileName(filePath)
122130

123-
pkgs, m, foundPomProps, err := p.traverseZip(size, r, fileProps)
131+
pkgs, m, foundPomProps, licenseFile, err := p.traverseZip(size, r, fileProps)
124132
if err != nil {
125133
return nil, nil, xerrors.Errorf("zip error: %w", err)
126134
}
127135

128136
// If pom.properties is found, it should be preferred than MANIFEST.MF.
129137
// Otherwise, resolve the artifact of the jar itself from MANIFEST.MF / SHA-1 / file name.
130138
// Such an artifact has no embedded pom.xml (maven-archiver writes pom.xml and
131-
// pom.properties together), so it carries no license.
139+
// pom.properties together), so it carries no pom.xml license.
132140
if !foundPomProps {
133141
pkg, found, err := p.resolveArtifact(r, m, fileProps)
134142
if err != nil {
@@ -139,6 +147,10 @@ func (p *Parser) parsePackages(filePath string, size int64, r xio.ReadSeekerAt)
139147
}
140148
}
141149

150+
// Classify and attach the LICENSE file now that the jar's own artifact is resolved
151+
// (it may have been added above from MANIFEST.MF / SHA-1 / file name).
152+
p.attachFileLicenses(pkgs, fileProps.FilePath, licenseFile)
153+
142154
return pkgs, nil, nil
143155
}
144156

@@ -219,40 +231,44 @@ func fillArchiveDigest(pkgs []ftypes.Package, r xio.ReadSeekerAt) error {
219231
}
220232

221233
func (p *Parser) traverseZip(size int64, r xio.ReadSeekerAt, fileProps Properties) (
222-
[]ftypes.Package, manifest, bool, error) {
234+
[]ftypes.Package, manifest, bool, *zip.File, error) {
223235
var pkgs []ftypes.Package
224236
var m manifest
225237
var foundPomProps bool
238+
var licenseFiles []*zip.File
226239

227240
// Licenses declared in embedded META-INF/maven/<g>/<a>/pom.xml, keyed by "groupID:artifactID".
228241
// The path carries no version, so packages are matched by G:A after the loop
229242
// (file order in the zip is not guaranteed).
230-
licenses := make(map[string][]string)
243+
pomLicenses := make(map[string][]string)
231244

232245
zr, err := zip.NewReader(r, size)
233246
if err != nil {
234-
return nil, manifest{}, false, xerrors.Errorf("zip error: %w", err)
247+
return nil, manifest{}, false, nil, xerrors.Errorf("zip error: %w", err)
235248
}
236249

237250
for _, fileInJar := range zr.File {
238-
// Collect licenses declared in the embedded META-INF/maven/<g>/<a>/pom.xml.
239-
if groupID, artifactID, ok := embeddedPomGAV(fileInJar.Name); ok {
251+
switch {
252+
case filepath.Base(fileInJar.Name) == "pom.xml":
253+
// Collect licenses declared in the embedded META-INF/maven/<g>/<a>/pom.xml.
254+
groupID, artifactID, ok := embeddedPomGAV(fileInJar.Name)
255+
if !ok {
256+
break
257+
}
240258
names, err := parsePomLicenses(fileInJar)
241259
if err != nil {
242260
p.logger.Debug("Failed to parse licenses", log.String("file", fileInJar.Name), log.Err(err))
243-
continue
261+
break
244262
}
245263
if len(names) > 0 {
246-
licenses[packageName(groupID, artifactID)] = names
264+
pomLicenses[packageName(groupID, artifactID)] = names
247265
}
248-
continue
249-
}
250-
251-
switch {
266+
case isJarLicenseFile(fileInJar.Name):
267+
licenseFiles = append(licenseFiles, fileInJar)
252268
case filepath.Base(fileInJar.Name) == "pom.properties":
253269
props, err := parsePomProperties(fileInJar, fileProps.FilePath)
254270
if err != nil {
255-
return nil, manifest{}, false, xerrors.Errorf("failed to parse %s: %w", fileInJar.Name, err)
271+
return nil, manifest{}, false, nil, xerrors.Errorf("failed to parse %s: %w", fileInJar.Name, err)
256272
}
257273
// Validation of props to avoid getting packages with empty Name/Version
258274
if props.Valid() {
@@ -266,7 +282,7 @@ func (p *Parser) traverseZip(size int64, r xio.ReadSeekerAt, fileProps Propertie
266282
case filepath.Base(fileInJar.Name) == "MANIFEST.MF":
267283
m, err = parseManifest(fileInJar)
268284
if err != nil {
269-
return nil, manifest{}, false, xerrors.Errorf("failed to parse MANIFEST.MF: %w", err)
285+
return nil, manifest{}, false, nil, xerrors.Errorf("failed to parse MANIFEST.MF: %w", err)
270286
}
271287
case isArtifact(fileInJar.Name):
272288
innerPkgs, _, err := p.parseInnerJar(fileInJar, fileProps.FilePath) // TODO process inner deps
@@ -279,18 +295,67 @@ func (p *Parser) traverseZip(size int64, r xio.ReadSeekerAt, fileProps Propertie
279295
}
280296

281297
// Attach licenses from embedded pom.xml, matched by "groupID:artifactID".
298+
attachPomLicenses(pkgs, pomLicenses)
299+
300+
var licenseFile *zip.File
301+
if len(licenseFiles) == 1 {
302+
licenseFile = licenseFiles[0]
303+
}
304+
305+
return pkgs, m, foundPomProps, licenseFile, nil
306+
}
307+
308+
// attachPomLicenses attaches licenses declared in embedded pom.xml files to packages,
309+
// matched by "groupID:artifactID". Packages that already have a license (e.g. set by a
310+
// nested jar from its own pom.xml) are left untouched.
311+
func attachPomLicenses(pkgs []ftypes.Package, pomLicenses map[string][]string) {
282312
for i := range pkgs {
283313
pkg := &pkgs[i]
284-
// Keep licenses already set by a nested jar from its own pom.xml.
285314
if len(pkg.Licenses) > 0 {
286315
continue
287316
}
288-
if names, ok := licenses[pkg.Name]; ok {
317+
if names, ok := pomLicenses[pkg.Name]; ok {
289318
pkg.Licenses = names
290319
}
291320
}
321+
}
322+
323+
// attachFileLicenses classifies the LICENSE file packed in a jar and attaches it to the
324+
// jar's own package, but only when the owner is unambiguous: a single LICENSE file, a
325+
// single package belonging to this jar, and no license from its pom.xml yet.
326+
func (p *Parser) attachFileLicenses(pkgs []ftypes.Package, filePath string, licenseFile *zip.File) {
327+
if licenseFile == nil {
328+
return
329+
}
330+
331+
var pkg *ftypes.Package
292332

293-
return pkgs, m, foundPomProps, nil
333+
for i := range pkgs {
334+
if pkgs[i].FilePath != filePath {
335+
continue
336+
}
337+
if pkg != nil {
338+
return // more than one package belongs to this jar
339+
}
340+
pkg = &pkgs[i]
341+
}
342+
343+
if pkg == nil {
344+
return // no package belongs to this jar
345+
}
346+
347+
if len(pkg.Licenses) > 0 {
348+
return
349+
}
350+
351+
names, err := p.classifyPackedLicense(licenseFile)
352+
if err != nil {
353+
p.logger.Debug("Failed to classify license file", log.FilePath(licenseFile.Name), log.Err(err))
354+
return
355+
}
356+
if len(names) > 0 {
357+
pkg.Licenses = names
358+
}
294359
}
295360

296361
func (p *Parser) parseInnerJar(zf *zip.File, rootPath string) ([]ftypes.Package, []ftypes.Dependency, error) {
@@ -463,6 +528,43 @@ func decodePomLicenses(r io.Reader) ([]string, error) {
463528
return names, nil
464529
}
465530

531+
// isJarLicenseFile reports whether a zip entry is a license file eligible for
532+
// classification: located at the jar root or directly under META-INF/ (not in a subdirectory),
533+
// with a base name whose stem is license/licence/copyright (e.g. LICENSE, LICENSE.txt).
534+
// Vendored licenses use prefixed names (e.g. FastDoubleParser-LICENSE) or nested
535+
// paths, so they are intentionally excluded.
536+
func isJarLicenseFile(name string) bool {
537+
dir := path.Dir(name)
538+
if dir != "." && dir != "META-INF" {
539+
return false
540+
}
541+
base := path.Base(name)
542+
if isArtifact(base) {
543+
return false // e.g. license.jar is a nested archive, not a license file
544+
}
545+
stem := strings.TrimSuffix(base, path.Ext(base))
546+
return licensing.LicenseFileNames.Contains(stem)
547+
}
548+
549+
// classifyPackedLicense classifies a LICENSE file packed in a jar and returns the
550+
// detected license names.
551+
func (p *Parser) classifyPackedLicense(f *zip.File) ([]string, error) {
552+
file, err := f.Open()
553+
if err != nil {
554+
return nil, xerrors.Errorf("unable to open %s: %w", f.Name, err)
555+
}
556+
defer file.Close()
557+
558+
lf, err := licensing.Classify(f.Name, file, p.licenseConfidenceLevel)
559+
if err != nil {
560+
return nil, xerrors.Errorf("license classification error: %w", err)
561+
}
562+
if lf == nil {
563+
return nil, nil
564+
}
565+
return lf.Findings.Names(), nil
566+
}
567+
466568
type manifest struct {
467569
implementationVersion string
468570
implementationTitle string

0 commit comments

Comments
 (0)