Skip to content

Commit 10c3d34

Browse files
authored
Merge branch 'aquasecurity:main' into deps/upgr-go-github-67
2 parents b18743b + 878fdb4 commit 10c3d34

11 files changed

Lines changed: 383 additions & 299 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pkg/iac/ @nikpivkin
1616

1717
# Helm chart
1818
helm/trivy/ @afdesk
19+
# Chart.yaml is bumped automatically by CI, so trivy team may approve it.
20+
helm/trivy/Chart.yaml @aquasecurity/trivy
1921

2022
# Kubernetes scanning
2123
pkg/k8s/ @afdesk

go.mod

Lines changed: 70 additions & 69 deletions
Large diffs are not rendered by default.

go.sum

Lines changed: 176 additions & 192 deletions
Large diffs are not rendered by default.

helm/trivy/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
apiVersion: v2
22
name: trivy
3-
version: 0.23.2
4-
appVersion: 0.71.2
3+
version: 0.24.0
4+
appVersion: 0.72.0
55
description: Trivy helm chart
66
keywords:
77
- scanner

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

Lines changed: 88 additions & 25 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
"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+
6472
func 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

97105
func (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

183240
func (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

366429
func isArtifact(name string) bool {

pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ func buildRunInstruction(s string) string {
147147
}
148148

149149
func buildHealthcheckInstruction(health *v1.HealthConfig) string {
150+
// Config.Healthcheck can be null even when a HEALTHCHECK history line exists
151+
// if the image was re-committed by a tool that normalizes HEALTHCHECK NONE to null.
152+
if health == nil {
153+
return "HEALTHCHECK NONE"
154+
}
150155
var interval, timeout, startPeriod, retries, command string
151156
if health.Interval != 0 {
152157
interval = fmt.Sprintf("--interval=%s ", health.Interval)

pkg/fanal/analyzer/imgconf/dockerfile/dockerfile_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,17 @@ HEALTHCHECK NONE
458458
ENTRYPOINT ["/bin/sh"]
459459
`,
460460
},
461+
{
462+
name: "healthcheck history without resolved config",
463+
input: &v1.ConfigFile{
464+
History: []v1.History{
465+
{
466+
CreatedBy: "HEALTHCHECK NONE",
467+
},
468+
},
469+
},
470+
expected: "HEALTHCHECK NONE\n",
471+
},
461472
{
462473
name: "legacy env format",
463474
input: &v1.ConfigFile{

pkg/fanal/analyzer/language/analyze.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package language
22

33
import (
4+
"cmp"
45
"context"
56
"io"
67

@@ -95,14 +96,15 @@ func toApplication(fileType types.LangType, filePath, libFilePath string, r xio.
9596
deps[dep.ID] = dep.DependsOn
9697
}
9798

98-
for i, pkg := range pkgs {
99+
for i := range pkgs {
100+
pkg := &pkgs[i]
99101
// This file path is populated for virtual file paths within archives, such as nested JAR files.
100-
if pkg.FilePath == "" {
101-
pkgs[i].FilePath = libFilePath
102-
}
103-
pkgs[i].DependsOn = deps[pkg.ID]
104-
pkgs[i].Digest = d
105-
pkgs[i].Indirect = isIndirect(pkg.Relationship) // For backward compatibility
102+
pkg.FilePath = cmp.Or(pkg.FilePath, libFilePath)
103+
pkg.DependsOn = deps[pkg.ID]
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+
pkg.Digest = cmp.Or(pkg.Digest, d)
107+
pkg.Indirect = isIndirect(pkg.Relationship) // For backward compatibility
106108
}
107109

108110
return &types.Application{

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ func (a *javaLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.Po
6060
// It will be called on each JAR file
6161
onFile := func(path string, info fs.FileInfo, r xio.ReadSeekerAt) (*types.Application, error) {
6262
p := jar.NewParser(client, jar.WithSize(info.Size()), jar.WithFilePath(path),
63+
jar.WithChecksum(input.Options.FileChecksum),
6364
jar.WithLicenseClassifierConfidenceLevel(a.licenseClassifierConfidenceLevel))
64-
return language.ParsePackage(ctx, types.Jar, path, r, p, input.Options.FileChecksum)
65+
// The jar parser calculates a per-file digest for every (possibly nested)
66+
// artifact itself, so language.ParsePackage must not overwrite them with
67+
// the enclosing archive's digest.
68+
return language.ParsePackage(ctx, types.Jar, path, r, p, false)
6569
}
6670

6771
var apps []types.Application

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func Test_javaLibraryAnalyzer_Analyze(t *testing.T) {
8282
},
8383
},
8484
{
85+
// The PAR itself (resolved via its pom.properties) and the nested
86+
// `jackson-core` JAR must each carry the digest of their own file.
8587
name: "happy path (PAR file)",
8688
inputFile: "testdata/test.par",
8789
includeChecksum: true,
@@ -91,20 +93,29 @@ func Test_javaLibraryAnalyzer_Analyze(t *testing.T) {
9193
Type: types.Jar,
9294
FilePath: "testdata/test.par",
9395
Packages: types.Packages{
96+
{
97+
Name: "com.example:par-app",
98+
FilePath: "testdata/test.par",
99+
Version: "3.0.0",
100+
Digest: "sha1:bdce3e13cc5d39960ab7f7644ae24d82becd2ba2",
101+
},
94102
{
95103
Name: "com.fasterxml.jackson.core:jackson-core",
96104
FilePath: "testdata/test.par/lib/jackson-core-2.9.10.jar",
97105
Version: "2.9.10",
98-
Digest: "sha1:d40913470259cfba6dcc90f96bcaa9bcff1b72e0",
106+
Digest: "sha1:66b715dec9dd8b0f39f3296e67e05913bf422d0c",
99107
},
100108
},
101109
},
102110
},
103111
},
104112
},
105113
{
106-
name: "happy path (package found in trivy-java-db by sha1)",
107-
inputFile: "testdata/test.jar",
114+
// The package is resolved by SHA-1 in trivy-java-db, and that SHA-1 is
115+
// also stored as the package digest.
116+
name: "happy path (package found in trivy-java-db by sha1)",
117+
inputFile: "testdata/test.jar",
118+
includeChecksum: true,
108119
want: &analyzer.AnalysisResult{
109120
Applications: []types.Application{
110121
{
@@ -115,6 +126,7 @@ func Test_javaLibraryAnalyzer_Analyze(t *testing.T) {
115126
Name: "org.apache.tomcat.embed:tomcat-embed-websocket",
116127
FilePath: "testdata/test.jar",
117128
Version: "9.0.65",
129+
Digest: "sha1:bd70dfeb39cc83c6934be24fa377b21e541dbe76",
118130
Licenses: []string{"Apache-2.0"},
119131
},
120132
},

0 commit comments

Comments
 (0)