Skip to content

Commit 1911b9e

Browse files
committed
fix(java): set per-file digest for nested JARs
Nested JAR/WAR/EAR archives (e.g. libraries inside a Spring Boot repackaged JAR) were reported with the SHA-1 digest of the enclosing archive instead of their own file digest, because the digest was calculated once per scanned file and assigned to every package. The jar parser now computes the SHA-1 of each (possibly nested) archive and stamps it onto the packages that belong to it, so every component carries the digest of its own file.
1 parent 0aff3fd commit 1911b9e

6 files changed

Lines changed: 129 additions & 18 deletions

File tree

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

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"archive/zip"
55
"bufio"
66
"context"
7-
"crypto/sha1" // nolint:gosec
8-
"encoding/hex"
97
"errors"
108
"io"
119
"os"
@@ -18,6 +16,7 @@ import (
1816
mavenversion "github.com/masahiro331/go-mvn-version"
1917
"golang.org/x/xerrors"
2018

19+
"github.com/aquasecurity/trivy/pkg/digest"
2120
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
2221
"github.com/aquasecurity/trivy/pkg/log"
2322
xio "github.com/aquasecurity/trivy/pkg/x/io"
@@ -38,6 +37,7 @@ type Parser struct {
3837
logger *log.Logger
3938
rootFilePath string
4039
offline bool
40+
checksum bool
4141
size int64
4242

4343
client Client
@@ -57,6 +57,14 @@ func WithOffline(offline bool) Option {
5757
}
5858
}
5959

60+
// WithChecksum enables calculation of the SHA-1 digest for every archive
61+
// (not only the ones that are looked up by SHA-1) and saving it to Package.Digest.
62+
func WithChecksum(checksum bool) Option {
63+
return func(p *Parser) {
64+
p.checksum = checksum
65+
}
66+
}
67+
6068
func WithSize(size int64) Option {
6169
return func(p *Parser) {
6270
p.size = size
@@ -85,6 +93,25 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
8593
}
8694

8795
func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
96+
pkgs, deps, err := p.parsePackages(filePath, size, r)
97+
if err != nil {
98+
return nil, nil, err
99+
}
100+
101+
// When a checksum is requested, every package must carry the digest of its
102+
// own file. Packages from nested archives (and the one resolved by
103+
// searchBySHA1) already have it, so fill in this archive's digest only for
104+
// the packages that are still missing one.
105+
if p.checksum {
106+
if err := fillArchiveDigest(pkgs, r); err != nil {
107+
return nil, nil, xerrors.Errorf("unable to set digest for %s: %w", filePath, err)
108+
}
109+
}
110+
111+
return pkgs, deps, nil
112+
}
113+
114+
func (p *Parser) parsePackages(filePath string, size int64, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
88115
p.logger.Debug("Parsing Java artifacts...", log.FilePath(filePath))
89116

90117
// Try to extract artifactId and version from the file name
@@ -122,9 +149,9 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
122149
}
123150

124151
// If groupId and artifactId are not found, call Maven Central's search API with SHA-1 digest.
125-
props, err := p.searchBySHA1(r, filePath)
152+
pkg, err := p.searchBySHA1(r, filePath)
126153
if err == nil {
127-
return append(pkgs, props.Package()), nil, nil
154+
return append(pkgs, pkg), nil, nil
128155
} else if !errors.Is(err, ArtifactNotFoundErr) {
129156
return nil, nil, xerrors.Errorf("failed to search by SHA1: %w", err)
130157
}
@@ -150,6 +177,29 @@ func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt)
150177
return pkgs, nil, nil
151178
}
152179

180+
// fillArchiveDigest sets the SHA-1 digest of the archive (r) on every package
181+
// that does not have a digest yet. The digest is calculated lazily, so the
182+
// archive is not read when all packages already carry their own digest.
183+
func fillArchiveDigest(pkgs []ftypes.Package, r xio.ReadSeekerAt) error {
184+
var d digest.Digest
185+
for i := range pkgs {
186+
if pkgs[i].Digest != "" {
187+
continue
188+
}
189+
if d == "" {
190+
if _, err := r.Seek(0, io.SeekStart); err != nil {
191+
return xerrors.Errorf("file seek error: %w", err)
192+
}
193+
var err error
194+
if d, err = digest.CalcSHA1(r); err != nil {
195+
return xerrors.Errorf("unable to calculate SHA-1: %w", err)
196+
}
197+
}
198+
pkgs[i].Digest = d
199+
}
200+
return nil
201+
}
202+
153203
func (p *Parser) traverseZip(filePath string, size int64, r xio.ReadSeekerAt, fileProps Properties) (
154204
[]ftypes.Package, manifest, bool, error) {
155205
var pkgs []ftypes.Package
@@ -231,22 +281,28 @@ func (p *Parser) parseInnerJar(zf *zip.File, rootPath string) ([]ftypes.Package,
231281
return innerPkgs, innerDeps, nil
232282
}
233283

234-
func (p *Parser) searchBySHA1(r io.ReadSeeker, filePath string) (Properties, error) {
284+
func (p *Parser) searchBySHA1(r io.ReadSeeker, filePath string) (ftypes.Package, error) {
235285
if _, err := r.Seek(0, io.SeekStart); err != nil {
236-
return Properties{}, xerrors.Errorf("file seek error: %w", err)
286+
return ftypes.Package{}, xerrors.Errorf("file seek error: %w", err)
237287
}
238-
239-
h := sha1.New() // nolint:gosec
240-
if _, err := io.Copy(h, r); err != nil {
241-
return Properties{}, xerrors.Errorf("unable to calculate SHA-1: %w", err)
288+
d, err := digest.CalcSHA1(r)
289+
if err != nil {
290+
return ftypes.Package{}, xerrors.Errorf("unable to calculate SHA-1: %w", err)
242291
}
243-
s := hex.EncodeToString(h.Sum(nil))
244-
prop, err := p.client.SearchBySHA1(s)
292+
293+
prop, err := p.client.SearchBySHA1(d.Encoded())
245294
if err != nil {
246-
return Properties{}, err
295+
return ftypes.Package{}, err
247296
}
248297
prop.FilePath = filePath
249-
return prop, nil
298+
299+
pkg := prop.Package()
300+
// searchBySHA1 has already calculated the archive's SHA-1, so stamp it on the
301+
// resolved package to avoid recalculating it in fillArchiveDigest.
302+
if p.checksum {
303+
pkg.Digest = d
304+
}
305+
return pkg, nil
250306
}
251307

252308
func isArtifact(name string) bool {

pkg/fanal/analyzer/language/analyze.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ func toApplication(fileType types.LangType, filePath, libFilePath string, r xio.
101101
pkgs[i].FilePath = libFilePath
102102
}
103103
pkgs[i].DependsOn = deps[pkg.ID]
104-
pkgs[i].Digest = d
104+
// Save the digest only if it has not been calculated for the package yet.
105+
// For example, the digest for JAR files is calculated inside the parser.
106+
if pkgs[i].Digest == "" {
107+
pkgs[i].Digest = d
108+
}
105109
pkgs[i].Indirect = isIndirect(pkg.Relationship) // For backward compatibility
106110
}
107111

pkg/fanal/analyzer/language/java/jar/jar.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,12 @@ func (a *javaLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.Po
5757

5858
// It will be called on each JAR file
5959
onFile := func(path string, info fs.FileInfo, r xio.ReadSeekerAt) (*types.Application, error) {
60-
p := jar.NewParser(client, jar.WithSize(info.Size()), jar.WithFilePath(path))
61-
return language.ParsePackage(ctx, types.Jar, path, r, p, input.Options.FileChecksum)
60+
p := jar.NewParser(client, jar.WithSize(info.Size()), jar.WithFilePath(path),
61+
jar.WithChecksum(input.Options.FileChecksum))
62+
// The jar parser calculates a per-file digest for every (possibly nested)
63+
// artifact itself, so language.ParsePackage must not overwrite them with
64+
// the enclosing archive's digest.
65+
return language.ParsePackage(ctx, types.Jar, path, r, p, false)
6266
}
6367

6468
var apps []types.Application

pkg/fanal/analyzer/language/java/jar/jar_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,47 @@ func Test_javaLibraryAnalyzer_Analyze(t *testing.T) {
7979
},
8080
},
8181
{
82+
// The top-level WAR and each inner JAR must have the digest of
83+
// their own file, not the digest of the enclosing WAR.
84+
// `repackaged-app` is the WAR itself (resolved via its pom.properties),
85+
// `inner-lib-1.2.3.jar` is resolved via its pom.properties,
86+
// `tomcat-embed-websocket-9.0.65.jar` is resolved by SHA-1 in trivy-java-db.
87+
// cf. https://github.com/aquasecurity/trivy/discussions/10847
88+
name: "happy path (repackaged WAR with checksum)",
89+
inputFile: "testdata/repackaged.war",
90+
includeChecksum: true,
91+
want: &analyzer.AnalysisResult{
92+
Applications: []types.Application{
93+
{
94+
Type: types.Jar,
95+
FilePath: "testdata/repackaged.war",
96+
Packages: types.Packages{
97+
{
98+
Name: "com.example:repackaged-app",
99+
FilePath: "testdata/repackaged.war",
100+
Version: "2.0.0",
101+
Digest: "sha1:5ea8e9906f162e5287e788cf464ec96a8a815bcc",
102+
},
103+
{
104+
Name: "com.example:inner-lib",
105+
FilePath: "testdata/repackaged.war/WEB-INF/lib/inner-lib-1.2.3.jar",
106+
Version: "1.2.3",
107+
Digest: "sha1:a22f48b888dd3b7ed9f6fea047e968479eac736f",
108+
},
109+
{
110+
Name: "org.apache.tomcat.embed:tomcat-embed-websocket",
111+
FilePath: "testdata/repackaged.war/WEB-INF/lib/tomcat-embed-websocket-9.0.65.jar",
112+
Version: "9.0.65",
113+
Digest: "sha1:bd70dfeb39cc83c6934be24fa377b21e541dbe76",
114+
},
115+
},
116+
},
117+
},
118+
},
119+
},
120+
{
121+
// The PAR itself (resolved via its pom.properties) and the nested
122+
// `jackson-core` JAR must each carry the digest of their own file.
82123
name: "happy path (PAR file)",
83124
inputFile: "testdata/test.par",
84125
includeChecksum: true,
@@ -88,11 +129,17 @@ func Test_javaLibraryAnalyzer_Analyze(t *testing.T) {
88129
Type: types.Jar,
89130
FilePath: "testdata/test.par",
90131
Packages: types.Packages{
132+
{
133+
Name: "com.example:par-app",
134+
FilePath: "testdata/test.par",
135+
Version: "3.0.0",
136+
Digest: "sha1:bdce3e13cc5d39960ab7f7644ae24d82becd2ba2",
137+
},
91138
{
92139
Name: "com.fasterxml.jackson.core:jackson-core",
93140
FilePath: "testdata/test.par/lib/jackson-core-2.9.10.jar",
94141
Version: "2.9.10",
95-
Digest: "sha1:d40913470259cfba6dcc90f96bcaa9bcff1b72e0",
142+
Digest: "sha1:66b715dec9dd8b0f39f3296e67e05913bf422d0c",
96143
},
97144
},
98145
},
245 KB
Binary file not shown.
477 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)