66 "context"
77 "crypto/sha1" // nolint:gosec
88 "encoding/hex"
9+ "encoding/xml"
910 "errors"
1011 "io"
1112 "os"
@@ -16,6 +17,7 @@ import (
1617 "strings"
1718
1819 mavenversion "github.com/masahiro331/go-mvn-version"
20+ "golang.org/x/net/html/charset"
1921 "golang.org/x/xerrors"
2022
2123 ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
@@ -89,51 +91,77 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
8991
9092 // Try to extract artifactId and version from the file name
9193 // e.g. spring-core-5.3.4-SNAPSHOT.jar => sprint-core, 5.3.4-SNAPSHOT
92- fileName := filepath .Base (filePath )
9394 fileProps := parseFileName (filePath )
9495
95- pkgs , m , foundPomProps , err := p .traverseZip (filePath , size , r , fileProps )
96+ pkgs , m , foundPomProps , licenses , err := p .traverseZip (filePath , size , r , fileProps )
9697 if err != nil {
9798 return nil , nil , xerrors .Errorf ("zip error: %w" , err )
9899 }
99100
100101 // If pom.properties is found, it should be preferred than MANIFEST.MF.
101- if foundPomProps {
102- return pkgs , nil , nil
102+ // Otherwise, resolve the artifact of the jar itself from MANIFEST.MF / SHA-1 / file name.
103+ if ! foundPomProps {
104+ props , found , err := p .resolveArtifact (filePath , r , m , fileProps )
105+ if err != nil {
106+ return nil , nil , err
107+ }
108+ if found {
109+ pkgs = append (pkgs , props .Package ())
110+ }
103111 }
104112
113+ // Attach licenses from embedded pom.xml, matched by "groupID:artifactID".
114+ for i := range pkgs {
115+ pkg := & pkgs [i ]
116+ // Keep licenses already set by a nested jar from its own pom.xml.
117+ if len (pkg .Licenses ) > 0 {
118+ continue
119+ }
120+ if names , ok := licenses [pkg .Name ]; ok {
121+ pkg .Licenses = names
122+ }
123+ }
124+
125+ return pkgs , nil , nil
126+ }
127+
128+ // resolveArtifact determines the artifact of the jar itself when pom.properties is absent,
129+ // trying MANIFEST.MF, then Maven Central by SHA-1, then a heuristic search by file name.
130+ func (p * Parser ) resolveArtifact (filePath string , r xio.ReadSeekerAt , m manifest , fileProps Properties ) (Properties , bool , error ) {
131+ fileName := filepath .Base (filePath )
132+
105133 manifestProps := m .properties (filePath )
106134 if p .offline {
107135 // In offline mode, we will not check if the artifact information is correct.
108136 if ! manifestProps .Valid () {
109137 p .logger .Debug ("Unable to identify POM in offline mode" , log .String ("file" , fileName ))
110- return pkgs , nil , nil
138+ return Properties {}, false , nil
111139 }
112- return append ( pkgs , manifestProps . Package ()), nil , nil
140+ return manifestProps , true , nil
113141 }
114142
115143 if manifestProps .Valid () {
116144 // Even if MANIFEST.MF is found, the groupId and artifactId might not be valid.
117145 // We have to make sure that the artifact exists actually.
118146 if ok , _ := p .client .Exists (manifestProps .GroupID , manifestProps .ArtifactID ); ok {
119147 // If groupId and artifactId are valid, they will be returned.
120- return append ( pkgs , manifestProps . Package ()), nil , nil
148+ return manifestProps , true , nil
121149 }
122150 }
123151
124152 // If groupId and artifactId are not found, call Maven Central's search API with SHA-1 digest.
125153 props , err := p .searchBySHA1 (r , filePath )
126154 if err == nil {
127- return append ( pkgs , props . Package ()), nil , nil
155+ return props , true , nil
128156 } else if ! errors .Is (err , ArtifactNotFoundErr ) {
129- return nil , nil , xerrors .Errorf ("failed to search by SHA1: %w" , err )
157+ return Properties {}, false , xerrors .Errorf ("failed to search by SHA1: %w" , err )
130158 }
131159
132160 p .logger .Debug ("No such POM in the central repositories" , log .String ("file" , fileName ))
133161
134162 // Return when artifactId or version from the file name are empty
135163 if fileProps .ArtifactID == "" || fileProps .Version == "" {
136- return pkgs , nil , nil
164+ return Properties {}, false , nil
137165 }
138166
139167 // Try to search groupId by artifactId via sonatype API
@@ -142,31 +170,49 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
142170 if err == nil {
143171 p .logger .Debug ("POM was determined in a heuristic way" , log .String ("file" , fileName ),
144172 log .String ("artifact" , fileProps .String ()))
145- pkgs = append ( pkgs , fileProps . Package ())
173+ return fileProps , true , nil
146174 } else if ! errors .Is (err , ArtifactNotFoundErr ) {
147- return nil , nil , xerrors .Errorf ("failed to search by artifact id: %w" , err )
175+ return Properties {}, false , xerrors .Errorf ("failed to search by artifact id: %w" , err )
148176 }
149177
150- return pkgs , nil , nil
178+ return Properties {}, false , nil
151179}
152180
153181func (p * Parser ) traverseZip (filePath string , size int64 , r xio.ReadSeekerAt , fileProps Properties ) (
154- []ftypes.Package , manifest , bool , error ) {
182+ []ftypes.Package , manifest , bool , map [ string ][] string , error ) {
155183 var pkgs []ftypes.Package
156184 var m manifest
157185 var foundPomProps bool
158186
187+ // Licenses declared in embedded META-INF/maven/<g>/<a>/pom.xml, keyed by "groupID:artifactID".
188+ // The path carries no version, so packages are matched by G:A after the loop
189+ // (file order in the zip is not guaranteed).
190+ licenses := make (map [string ][]string )
191+
159192 zr , err := zip .NewReader (r , size )
160193 if err != nil {
161- return nil , manifest {}, false , xerrors .Errorf ("zip error: %w" , err )
194+ return nil , manifest {}, false , nil , xerrors .Errorf ("zip error: %w" , err )
162195 }
163196
164197 for _ , fileInJar := range zr .File {
198+ // Collect licenses declared in the embedded META-INF/maven/<g>/<a>/pom.xml.
199+ if groupID , artifactID , ok := embeddedPomGAV (fileInJar .Name ); ok {
200+ names , err := parsePomLicenses (fileInJar )
201+ if err != nil {
202+ p .logger .Debug ("Failed to parse licenses" , log .String ("file" , fileInJar .Name ), log .Err (err ))
203+ continue
204+ }
205+ if len (names ) > 0 {
206+ licenses [packageName (groupID , artifactID )] = names
207+ }
208+ continue
209+ }
210+
165211 switch {
166212 case filepath .Base (fileInJar .Name ) == "pom.properties" :
167213 props , err := parsePomProperties (fileInJar , filePath )
168214 if err != nil {
169- return nil , manifest {}, false , xerrors .Errorf ("failed to parse %s: %w" , fileInJar .Name , err )
215+ return nil , manifest {}, false , nil , xerrors .Errorf ("failed to parse %s: %w" , fileInJar .Name , err )
170216 }
171217 // Validation of props to avoid getting packages with empty Name/Version
172218 if props .Valid () {
@@ -180,7 +226,7 @@ func (p *Parser) traverseZip(filePath string, size int64, r xio.ReadSeekerAt, fi
180226 case filepath .Base (fileInJar .Name ) == "MANIFEST.MF" :
181227 m , err = parseManifest (fileInJar )
182228 if err != nil {
183- return nil , manifest {}, false , xerrors .Errorf ("failed to parse MANIFEST.MF: %w" , err )
229+ return nil , manifest {}, false , nil , xerrors .Errorf ("failed to parse MANIFEST.MF: %w" , err )
184230 }
185231 case isArtifact (fileInJar .Name ):
186232 innerPkgs , _ , err := p .parseInnerJar (fileInJar , filePath ) // TODO process inner deps
@@ -191,7 +237,7 @@ func (p *Parser) traverseZip(filePath string, size int64, r xio.ReadSeekerAt, fi
191237 pkgs = append (pkgs , innerPkgs ... )
192238 }
193239 }
194- return pkgs , m , foundPomProps , nil
240+ return pkgs , m , foundPomProps , licenses , nil
195241}
196242
197243func (p * Parser ) parseInnerJar (zf * zip.File , rootPath string ) ([]ftypes.Package , []ftypes.Dependency , error ) {
@@ -300,6 +346,64 @@ func parsePomProperties(f *zip.File, filePath string) (Properties, error) {
300346 return p , nil
301347}
302348
349+ // embeddedPom is a minimal view of an embedded pom.xml: only the license names are needed.
350+ type embeddedPom struct {
351+ Licenses struct {
352+ License []struct {
353+ Name string `xml:"name"`
354+ } `xml:"license"`
355+ } `xml:"licenses"`
356+ }
357+
358+ // embeddedPomGAV extracts groupId and artifactId from a path of the form
359+ // META-INF/maven/<groupId>/<artifactId>/pom.xml. The version is not part of the path.
360+ // ok is false when the path is not a Maven descriptor pom.xml.
361+ func embeddedPomGAV (name string ) (groupID , artifactID string , ok bool ) {
362+ rel , found := strings .CutPrefix (name , "META-INF/maven/" )
363+ if ! found {
364+ return "" , "" , false
365+ }
366+ rel , found = strings .CutSuffix (rel , "/pom.xml" )
367+ if ! found {
368+ return "" , "" , false
369+ }
370+ groupID , artifactID , found = strings .Cut (rel , "/" )
371+ if ! found || groupID == "" || artifactID == "" {
372+ return "" , "" , false
373+ }
374+ return groupID , artifactID , true
375+ }
376+
377+ // parsePomLicenses returns the raw <license><name> values from an embedded pom.xml.
378+ func parsePomLicenses (f * zip.File ) ([]string , error ) {
379+ file , err := f .Open ()
380+ if err != nil {
381+ return nil , xerrors .Errorf ("unable to open %s: %w" , f .Name , err )
382+ }
383+ defer file .Close ()
384+
385+ return decodePomLicenses (file )
386+ }
387+
388+ // decodePomLicenses decodes a pom.xml and returns the raw <license><name> values.
389+ // Names are kept as-is; normalization happens downstream.
390+ func decodePomLicenses (r io.Reader ) ([]string , error ) {
391+ var pom embeddedPom
392+ decoder := xml .NewDecoder (r )
393+ decoder .CharsetReader = charset .NewReaderLabel
394+ if err := decoder .Decode (& pom ); err != nil {
395+ return nil , xerrors .Errorf ("xml decode error: %w" , err )
396+ }
397+
398+ var names []string
399+ for _ , lic := range pom .Licenses .License {
400+ if name := strings .TrimSpace (lic .Name ); name != "" {
401+ names = append (names , name )
402+ }
403+ }
404+ return names , nil
405+ }
406+
303407type manifest struct {
304408 implementationVersion string
305409 implementationTitle string
0 commit comments