Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ec428df
test: add module tests from #7879
DmitriyLewen Nov 27, 2025
71cec98
feat: use gav + path for pkg.ID
DmitriyLewen Nov 27, 2025
d3f4405
test: update tests
DmitriyLewen Nov 27, 2025
96a4159
mage test:updateGolden
DmitriyLewen Nov 27, 2025
d23481d
refactor(pom): check RootFilePath in cache key
DmitriyLewen Dec 3, 2025
15af332
refactor(pom): use RootFilePath for all artifacts:
DmitriyLewen Dec 3, 2025
d0edab9
tests: update tests:
DmitriyLewen Dec 3, 2025
0c42925
fix: add pkgID for vulns filtering
DmitriyLewen Dec 3, 2025
643cbeb
fix(report): use human-readable IDs in dep tree
DmitriyLewen Dec 4, 2025
9a2270b
test: update other tests with pom files
DmitriyLewen Dec 4, 2025
c9cda67
mage lint:fix
DmitriyLewen Dec 4, 2025
c111673
fix: use filepath.ToSlash for hashing to avoid differences between Wi…
DmitriyLewen Dec 8, 2025
6b8ceaa
refactor: update error message
DmitriyLewen Jan 14, 2026
e168939
fix: remove extra quotes
DmitriyLewen Jan 14, 2026
1be9285
refactor: use GAV::digest as ID
DmitriyLewen Jan 14, 2026
f4a5a14
revert: use origin ID for dep tree
DmitriyLewen Jan 14, 2026
62f5bd5
test: update test/testcases
DmitriyLewen Jan 14, 2026
22d7f4a
Merge branch 'main' of github.com:DmitriyLewen/trivy into refactor/po…
DmitriyLewen Jan 14, 2026
88e2f7c
test(pom): simplify help functions
DmitriyLewen Jan 14, 2026
c846294
test: update broken tests
DmitriyLewen Jan 14, 2026
3adddfd
fix(filter): sort by pkgIDs
DmitriyLewen Jan 14, 2026
dec5adf
fix: linter error
DmitriyLewen Jan 15, 2026
9d46a38
Merge branch 'main' of github.com:DmitriyLewen/trivy into refactor/po…
DmitriyLewen Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration/testdata/pom-cyclonedx.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"properties": [
{
"name": "aquasecurity:trivy:PkgID",
"value": "com.example:log4shell:1.0-SNAPSHOT"
"value": "b21b31f8c0d5705a"
},
{
"name": "aquasecurity:trivy:PkgType",
Expand All @@ -75,7 +75,7 @@
"properties": [
{
"name": "aquasecurity:trivy:PkgID",
"value": "com.fasterxml.jackson.core:jackson-databind:2.9.1"
"value": "6a9e9547e78d4dc9"
},
{
"name": "aquasecurity:trivy:PkgType",
Expand Down
12 changes: 6 additions & 6 deletions integration/testdata/pom.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2020-9548",
"PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1",
"PkgID": "6a9e9547e78d4dc9",
"PkgName": "com.fasterxml.jackson.core:jackson-databind",
"PkgIdentifier": {
"PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1",
"UID": "9c69fdeffb7ee6d4"
"UID": "48ebcb1614e7d3ef"
},
"InstalledVersion": "2.9.1",
"FixedVersion": "2.9.10.4",
Expand All @@ -28,7 +28,7 @@
"Name": "GitHub Security Advisory Maven",
"URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven"
},
"Fingerprint": "sha256:68e4f9ccb3a897341f76048401e88a22f2d9251a88eef44abdf7b9c2af70f2e4",
"Fingerprint": "sha256:dcaf35f205265d8f4f11729d3c3b08ff29cc1116c98de314e9566eb73406c9dd",
"Title": "jackson-databind: Serialization gadgets in anteros-core",
"Description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).",
"Severity": "CRITICAL",
Expand Down Expand Up @@ -78,11 +78,11 @@
},
{
"VulnerabilityID": "CVE-2021-20190",
"PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1",
"PkgID": "6a9e9547e78d4dc9",
"PkgName": "com.fasterxml.jackson.core:jackson-databind",
"PkgIdentifier": {
"PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1",
"UID": "9c69fdeffb7ee6d4"
"UID": "48ebcb1614e7d3ef"
},
"InstalledVersion": "2.9.1",
"FixedVersion": "2.9.10.7",
Expand All @@ -94,7 +94,7 @@
"Name": "GitLab Advisory Database Community",
"URL": "https://gitlab.com/gitlab-org/advisories-community"
},
"Fingerprint": "sha256:b7e077da6366be5eebc967119b377ac75bf9c1f0b0fb63f07ee1cfdec931506e",
"Fingerprint": "sha256:cced2c1615093a3d56693e9c36f69a540dd897c3a9d5e552944292bfb72cd1ee",
"Title": "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing",
"Description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.",
"Severity": "HIGH",
Expand Down
5 changes: 5 additions & 0 deletions pkg/dependency/parser/java/pom/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type artifact struct {
Relationship ftypes.Relationship

Locations ftypes.Locations

// For correctly calculation package ID (hash),
// We need to store the file paths for root or module artifacts.
// For other artifacts, it will be empty.
RootFilePath string
}

func newArtifact(groupID, artifactID, version string, licenses []string, props map[string]string) artifact {
Expand Down
2 changes: 1 addition & 1 deletion pkg/dependency/parser/java/pom/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ func (c pomCache) get(art artifact) *analysisResult {
}

func (c pomCache) key(art artifact) string {
return fmt.Sprintf("%s:%s", art.Name(), art.Version)
return fmt.Sprintf("%s:%s:%s", art.Name(), art.Version, art.RootFilePath)
}
49 changes: 39 additions & 10 deletions pkg/dependency/parser/java/pom/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import (
"path/filepath"
"slices"
"sort"
"strconv"
"strings"

"github.com/hashicorp/go-multierror"
"github.com/mitchellh/hashstructure/v2"
"github.com/samber/lo"
"golang.org/x/net/html/charset"
"golang.org/x/xerrors"
Expand Down Expand Up @@ -123,7 +125,9 @@ func (p *Parser) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]ftypes.Packag
}

// Analyze root POM
result, err := p.analyze(ctx, root, analysisOptions{})
result, err := p.analyze(ctx, root, analysisOptions{
rootFilePath: p.rootPath,
})
if err != nil {
return nil, nil, xerrors.Errorf("analyze error (%s): %w", p.rootPath, err)
}
Expand All @@ -133,12 +137,17 @@ func (p *Parser) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]ftypes.Packag

rootArt := root.artifact()
rootArt.Relationship = ftypes.RelationshipRoot
rootArt.RootFilePath = p.rootPath

return p.parseRoot(ctx, rootArt, set.New[string]())
}

// nolint: gocyclo
func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.Set[string]) ([]ftypes.Package, []ftypes.Dependency, error) {
if root.RootFilePath == "" {
return nil, nil, xerrors.New("root file path should be filled")
}

// Prepare a queue for dependencies
queue := newArtifactQueue()

Expand All @@ -161,10 +170,11 @@ func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.S
// Modules should be handled separately so that they can have independent dependencies.
// It means multi-module allows for duplicate dependencies.
if art.Module {
if uniqModules.Contains(art.String()) {
id := packageID(art.Name(), art.Version.String(), art.RootFilePath)
if uniqModules.Contains(id) {
continue
}
uniqModules.Append(art.String())
uniqModules.Append(id)

modulePkgs, moduleDeps, err := p.parseRoot(ctx, art, uniqModules)
if err != nil {
Expand Down Expand Up @@ -245,14 +255,14 @@ func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.S
dependsOn := lo.Map(result.dependencies, func(a artifact, _ int) string {
return a.Name()
})
uniqDeps[packageID(art.Name(), art.Version.String())] = dependsOn
uniqDeps[packageID(art.Name(), art.Version.String(), root.RootFilePath)] = dependsOn
}
}

// Convert to []ftypes.Package and []ftypes.Dependency
for name, art := range uniqArtifacts {
pkg := ftypes.Package{
ID: packageID(name, art.Version.String()),
ID: packageID(name, art.Version.String(), root.RootFilePath),
Name: name,
Version: art.Version.String(),
Licenses: art.Licenses,
Expand All @@ -264,7 +274,7 @@ func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.S
// Convert dependency names into dependency IDs
dependsOn := lo.FilterMap(uniqDeps[pkg.ID], func(dependOnName string, _ int) (string, bool) {
ver := depVersion(dependOnName, uniqArtifacts)
return packageID(dependOnName, ver), ver != ""
return packageID(dependOnName, ver, root.RootFilePath), ver != ""
})

// `mvn` shows modules separately from the root package and does not show module nesting.
Expand Down Expand Up @@ -303,13 +313,16 @@ func (p *Parser) parseModule(ctx context.Context, currentPath, relativePath stri
return artifact{}, xerrors.Errorf("unable to open the relative path: %w", err)
}

result, err := p.analyze(ctx, module, analysisOptions{})
result, err := p.analyze(ctx, module, analysisOptions{
rootFilePath: module.filePath,
})
if err != nil {
return artifact{}, xerrors.Errorf("analyze error: %w", err)
}

moduleArtifact := module.artifact()
moduleArtifact.Module = true
moduleArtifact.RootFilePath = module.filePath
moduleArtifact.Relationship = ftypes.RelationshipWorkspace

p.cache.put(moduleArtifact, result)
Expand Down Expand Up @@ -343,6 +356,7 @@ func (p *Parser) resolve(ctx context.Context, art artifact, rootDepManagement []
result, err := p.analyze(ctx, pomContent, analysisOptions{
exclusions: art.Exclusions,
depManagement: rootDepManagement,
rootFilePath: art.RootFilePath,
})
if err != nil {
return analysisResult{}, xerrors.Errorf("analyze error: %w", err)
Expand All @@ -364,6 +378,7 @@ type analysisResult struct {
type analysisOptions struct {
exclusions set.Set[string]
depManagement []pomDependency // from the root POM
rootFilePath string // File path of the root POM or module POM
}

func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (analysisResult, error) {
Expand Down Expand Up @@ -393,9 +408,12 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
}
deps = p.filterDependencies(deps, opts.exclusions)

art := pom.artifact()
art.RootFilePath = opts.rootFilePath

return analysisResult{
filePath: pom.filePath,
artifact: pom.artifact(),
artifact: art,
dependencies: deps,
dependencyManagement: depManagement,
properties: props,
Expand Down Expand Up @@ -875,8 +893,19 @@ func parseMavenMetadata(r io.Reader) (*Metadata, error) {
return parsed, nil
}

func packageID(name, version string) string {
return dependency.ID(ftypes.Pom, name, version)
func packageID(name, version, pomFilePath string) string {
v := map[string]any{
"gav": dependency.ID(ftypes.Pom, name, version),
"path": filepath.ToSlash(pomFilePath),
}
h, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
})
if err != nil {
log.Warn("Failed to calculate the pom.xml hash", log.String("name", name), log.String("version", version), log.Err(err))
}
return strconv.FormatUint(h, 16)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about including GAV in the ID for readability? Something like com.example:log4shell:1.0-SNAPSHOT::a302c021 (GAV + 8-char hash suffix).

Suggested change
func packageID(name, version, pomFilePath string) string {
v := map[string]any{
"gav": dependency.ID(ftypes.Pom, name, version),
"path": filepath.ToSlash(pomFilePath),
}
h, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
})
if err != nil {
log.Warn("Failed to calculate the pom.xml hash", log.String("name", name), log.String("version", version), log.Err(err))
}
return strconv.FormatUint(h, 16)
func packageID(name, version, pomFilePath string) string {
gav := dependency.ID(ftypes.Pom, name, version)
v := map[string]any{
"gav": gav,
"path": filepath.ToSlash(pomFilePath),
}
h, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
})
if err != nil {
log.Warn("Failed to calculate hash", log.Err(err))
return gav // fallback to GAV only
}
// Append 8-character hash suffix
return fmt.Sprintf("%s::%s", gav, strconv.FormatUint(h, 16)[:8])
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good idea!
Updated in 1be9285

}

// cf. https://github.com/apache/maven/blob/259404701402230299fe05ee889ecdf1c9dae816/maven-artifact/src/main/java/org/apache/maven/artifact/DefaultArtifact.java#L482-L486
Expand Down
Loading