44 "archive/zip"
55 "bufio"
66 "context"
7- "crypto/sha1" // nolint:gosec
8- "encoding/hex"
97 "encoding/xml"
108 "errors"
119 "io"
@@ -20,6 +18,7 @@ import (
2018 "golang.org/x/net/html/charset"
2119 "golang.org/x/xerrors"
2220
21+ "github.com/aquasecurity/trivy/pkg/digest"
2322 ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
2423 "github.com/aquasecurity/trivy/pkg/licensing"
2524 "github.com/aquasecurity/trivy/pkg/log"
@@ -41,6 +40,7 @@ type Parser struct {
4140 logger * log.Logger
4241 rootFilePath string
4342 offline bool
43+ checksum bool
4444 size int64
4545 licenseConfidenceLevel float64
4646
@@ -61,6 +61,14 @@ func WithOffline(offline bool) Option {
6161 }
6262}
6363
64+ // WithChecksum enables calculation of the SHA-1 digest for every archive
65+ // (not only the ones that are looked up by SHA-1) and saving it to Package.Digest.
66+ func WithChecksum (checksum bool ) Option {
67+ return func (p * Parser ) {
68+ p .checksum = checksum
69+ }
70+ }
71+
6472func WithSize (size int64 ) Option {
6573 return func (p * Parser ) {
6674 p .size = size
@@ -95,6 +103,25 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
95103}
96104
97105func (p * Parser ) parseArtifact (filePath string , size int64 , r xio.ReadSeekerAt ) ([]ftypes.Package , []ftypes.Dependency , error ) {
106+ pkgs , deps , err := p .parsePackages (filePath , size , r )
107+ if err != nil {
108+ return nil , nil , err
109+ }
110+
111+ // When a checksum is requested, every package must carry the digest of its
112+ // own file. Packages from nested archives (and the one resolved by
113+ // searchBySHA1) already have it, so fill in this archive's digest only for
114+ // the packages that are still missing one.
115+ if p .checksum {
116+ if err := fillArchiveDigest (pkgs , r ); err != nil {
117+ return nil , nil , xerrors .Errorf ("unable to set digest for %s: %w" , filePath , err )
118+ }
119+ }
120+
121+ return pkgs , deps , nil
122+ }
123+
124+ func (p * Parser ) parsePackages (filePath string , size int64 , r xio.ReadSeekerAt ) ([]ftypes.Package , []ftypes.Dependency , error ) {
98125 p .logger .Debug ("Parsing Java artifacts..." , log .FilePath (filePath ))
99126
100127 // Try to extract artifactId and version from the file name
@@ -111,12 +138,12 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
111138 // Such an artifact has no embedded pom.xml (maven-archiver writes pom.xml and
112139 // pom.properties together), so it carries no pom.xml license.
113140 if ! foundPomProps {
114- props , found , err := p .resolveArtifact (r , m , fileProps )
141+ pkg , found , err := p .resolveArtifact (r , m , fileProps )
115142 if err != nil {
116143 return nil , nil , err
117144 }
118145 if found {
119- pkgs = append (pkgs , props . Package () )
146+ pkgs = append (pkgs , pkg )
120147 }
121148 }
122149
@@ -129,41 +156,41 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
129156
130157// resolveArtifact determines the artifact of the jar itself when pom.properties is absent,
131158// trying MANIFEST.MF, then Maven Central by SHA-1, then a heuristic search by file name.
132- func (p * Parser ) resolveArtifact (r xio.ReadSeekerAt , m manifest , fileProps Properties ) (Properties , bool , error ) {
159+ func (p * Parser ) resolveArtifact (r xio.ReadSeekerAt , m manifest , fileProps Properties ) (ftypes. Package , bool , error ) {
133160 fileName := filepath .Base (fileProps .FilePath )
134161
135162 manifestProps := m .properties (fileProps .FilePath )
136163 if p .offline {
137164 // In offline mode, we will not check if the artifact information is correct.
138165 if ! manifestProps .Valid () {
139166 p .logger .Debug ("Unable to identify POM in offline mode" , log .String ("file" , fileName ))
140- return Properties {}, false , nil
167+ return ftypes. Package {}, false , nil
141168 }
142- return manifestProps , true , nil
169+ return manifestProps . Package () , true , nil
143170 }
144171
145172 if manifestProps .Valid () {
146173 // Even if MANIFEST.MF is found, the groupId and artifactId might not be valid.
147174 // We have to make sure that the artifact exists actually.
148175 if ok , _ := p .client .Exists (manifestProps .GroupID , manifestProps .ArtifactID ); ok {
149176 // If groupId and artifactId are valid, they will be returned.
150- return manifestProps , true , nil
177+ return manifestProps . Package () , true , nil
151178 }
152179 }
153180
154181 // If groupId and artifactId are not found, call Maven Central's search API with SHA-1 digest.
155- props , err := p .searchBySHA1 (r , fileProps .FilePath )
182+ pkg , err := p .searchBySHA1 (r , fileProps .FilePath )
156183 if err == nil {
157- return props , true , nil
184+ return pkg , true , nil
158185 } else if ! errors .Is (err , ArtifactNotFoundErr ) {
159- return Properties {}, false , xerrors .Errorf ("failed to search by SHA1: %w" , err )
186+ return ftypes. Package {}, false , xerrors .Errorf ("failed to search by SHA1: %w" , err )
160187 }
161188
162189 p .logger .Debug ("No such POM in the central repositories" , log .String ("file" , fileName ))
163190
164191 // Return when artifactId or version from the file name are empty
165192 if fileProps .ArtifactID == "" || fileProps .Version == "" {
166- return Properties {}, false , nil
193+ return ftypes. Package {}, false , nil
167194 }
168195
169196 // Try to search groupId by artifactId via sonatype API
@@ -172,12 +199,42 @@ func (p *Parser) resolveArtifact(r xio.ReadSeekerAt, m manifest, fileProps Prope
172199 if err == nil {
173200 p .logger .Debug ("POM was determined in a heuristic way" , log .String ("file" , fileName ),
174201 log .String ("artifact" , fileProps .String ()))
175- return fileProps , true , nil
202+ return fileProps . Package () , true , nil
176203 } else if ! errors .Is (err , ArtifactNotFoundErr ) {
177- return Properties {}, false , xerrors .Errorf ("failed to search by artifact id: %w" , err )
204+ return ftypes. Package {}, false , xerrors .Errorf ("failed to search by artifact id: %w" , err )
178205 }
179206
180- return Properties {}, false , nil
207+ return ftypes.Package {}, false , nil
208+ }
209+
210+ // fillArchiveDigest sets the SHA-1 digest of the archive (r) on every package
211+ // that does not have a digest yet. The digest is calculated lazily, so the
212+ // archive is not read when all packages already carry their own digest.
213+ //
214+ // Packages that have no file of their own — e.g. dependencies flattened into a
215+ // shaded/uber JAR, which only leave a bundled pom.properties behind — all share
216+ // this archive's digest. That is consistent with their FilePath, which is also
217+ // the enclosing archive, so the digest stays aligned with the file it refers to.
218+ func fillArchiveDigest (pkgs []ftypes.Package , r xio.ReadSeekerAt ) error {
219+ var d digest.Digest
220+ for i := range pkgs {
221+ if pkgs [i ].Digest != "" {
222+ continue
223+ }
224+ // Compute the archive digest at most once and reuse it afterwards.
225+ // An empty d means it has not been calculated yet.
226+ if d == "" {
227+ if _ , err := r .Seek (0 , io .SeekStart ); err != nil {
228+ return xerrors .Errorf ("file seek error: %w" , err )
229+ }
230+ var err error
231+ if d , err = digest .CalcSHA1 (r ); err != nil {
232+ return xerrors .Errorf ("unable to calculate SHA-1: %w" , err )
233+ }
234+ }
235+ pkgs [i ].Digest = d
236+ }
237+ return nil
181238}
182239
183240func (p * Parser ) traverseZip (size int64 , r xio.ReadSeekerAt , fileProps Properties ) (
@@ -345,22 +402,28 @@ func (p *Parser) parseInnerJar(zf *zip.File, rootPath string) ([]ftypes.Package,
345402 return innerPkgs , innerDeps , nil
346403}
347404
348- func (p * Parser ) searchBySHA1 (r io.ReadSeeker , filePath string ) (Properties , error ) {
405+ func (p * Parser ) searchBySHA1 (r io.ReadSeeker , filePath string ) (ftypes. Package , error ) {
349406 if _ , err := r .Seek (0 , io .SeekStart ); err != nil {
350- return Properties {}, xerrors .Errorf ("file seek error: %w" , err )
407+ return ftypes. Package {}, xerrors .Errorf ("file seek error: %w" , err )
351408 }
352-
353- h := sha1 .New () // nolint:gosec
354- if _ , err := io .Copy (h , r ); err != nil {
355- return Properties {}, xerrors .Errorf ("unable to calculate SHA-1: %w" , err )
409+ d , err := digest .CalcSHA1 (r )
410+ if err != nil {
411+ return ftypes.Package {}, xerrors .Errorf ("unable to calculate SHA-1: %w" , err )
356412 }
357- s := hex . EncodeToString ( h . Sum ( nil ))
358- prop , err := p .client .SearchBySHA1 (s )
413+
414+ prop , err := p .client .SearchBySHA1 (d . Encoded () )
359415 if err != nil {
360- return Properties {}, err
416+ return ftypes. Package {}, err
361417 }
362418 prop .FilePath = filePath
363- return prop , nil
419+
420+ pkg := prop .Package ()
421+ // searchBySHA1 has already calculated the archive's SHA-1, so stamp it on the
422+ // resolved package to avoid recalculating it in fillArchiveDigest.
423+ if p .checksum {
424+ pkg .Digest = d
425+ }
426+ return pkg , nil
364427}
365428
366429func isArtifact (name string ) bool {
0 commit comments