Skip to content

Commit b9415a3

Browse files
authored
fix(java): correctly propagate repositories from upper POMs to dependencies (#10077)
1 parent 31c4780 commit b9415a3

File tree

12 files changed

+445
-27
lines changed

12 files changed

+445
-27
lines changed

pkg/dependency/parser/java/pom/artifact.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type artifact struct {
3737
// We need to store the file paths for root or module artifacts.
3838
// For other artifacts, it will be empty.
3939
RootFilePath string
40+
41+
// Repositories got from current POM, upper-level POMs and parent POMs.
42+
Repositories []repository
4043
}
4144

4245
func newArtifact(groupID, artifactID, version string, licenses []string, props map[string]string) artifact {

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,8 @@ func (p *Parser) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]ftypes.Packag
136136
// Cache root POM
137137
p.cache.put(result.artifact, result)
138138

139-
rootArt := root.artifact()
139+
rootArt := result.artifact
140140
rootArt.Relationship = ftypes.RelationshipRoot
141-
rootArt.RootFilePath = p.rootPath
142141

143142
return p.parseRoot(ctx, rootArt, set.New[string]())
144143
}
@@ -228,7 +227,7 @@ func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.S
228227

229228
// Parse, cache, and enqueue modules.
230229
for _, relativePath := range result.modules {
231-
moduleArtifact, err := p.parseModule(ctx, result.filePath, relativePath)
230+
moduleArtifact, err := p.parseModule(ctx, result.filePath, relativePath, root.Repositories)
232231
if err != nil {
233232
p.logger.Debug("Unable to parse the module",
234233
log.FilePath(result.filePath), log.Err(err))
@@ -307,7 +306,7 @@ func depVersion(depName string, uniqArtifacts map[string]artifact) string {
307306
return ""
308307
}
309308

310-
func (p *Parser) parseModule(ctx context.Context, currentPath, relativePath string) (artifact, error) {
309+
func (p *Parser) parseModule(ctx context.Context, currentPath, relativePath string, repos []repository) (artifact, error) {
311310
// modulePath: "root/" + "module/" => "root/module"
312311
module, err := p.openRelativePom(currentPath, relativePath)
313312
if err != nil {
@@ -316,6 +315,7 @@ func (p *Parser) parseModule(ctx context.Context, currentPath, relativePath stri
316315

317316
result, err := p.analyze(ctx, module, analysisOptions{
318317
rootFilePath: module.filePath,
318+
repositories: repos,
319319
})
320320
if err != nil {
321321
return artifact{}, xerrors.Errorf("analyze error: %w", err)
@@ -347,17 +347,19 @@ func (p *Parser) resolve(ctx context.Context, art artifact, rootDepManagement []
347347

348348
p.logger.Debug("Resolving...", log.String("group_id", art.GroupID),
349349
log.String("artifact_id", art.ArtifactID), log.String("version", art.Version.String()))
350-
pomContent, err := p.tryRepository(ctx, art.GroupID, art.ArtifactID, art.Version.String())
350+
pomContent, err := p.tryRepository(ctx, art.GroupID, art.ArtifactID, art.Version.String(), art.Repositories)
351351
if err != nil {
352352
if shouldReturnError(err) {
353353
return analysisResult{}, err
354354
}
355355
p.logger.Debug("Repository error", log.Err(err))
356356
}
357+
357358
result, err := p.analyze(ctx, pomContent, analysisOptions{
358359
exclusions: art.Exclusions,
359360
depManagement: rootDepManagement,
360361
rootFilePath: art.RootFilePath,
362+
repositories: art.Repositories,
361363
})
362364
if err != nil {
363365
return analysisResult{}, xerrors.Errorf("analyze error: %w", err)
@@ -380,6 +382,7 @@ type analysisOptions struct {
380382
exclusions set.Set[string]
381383
depManagement []pomDependency // from the root POM
382384
rootFilePath string // File path of the root POM or module POM
385+
repositories []repository // Repositories inherited from parent
383386
}
384387

385388
func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (analysisResult, error) {
@@ -389,14 +392,16 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
389392
if opts.exclusions == nil {
390393
opts.exclusions = set.New[string]()
391394
}
392-
// Update remoteRepositories
395+
396+
// Get repositories from current POM and merge with inherited ones
393397
pomRepos := pom.repositories(p.servers)
394-
p.remoteRepos.pom = lo.UniqBy(append(pomRepos, p.remoteRepos.pom...), func(r repository) url.URL {
398+
opts.repositories = lo.UniqBy(append(pomRepos, opts.repositories...), func(r repository) url.URL {
395399
return r.url
396400
})
397401

398402
// Resolve parent POM
399-
if err := p.resolveParent(ctx, pom); err != nil {
403+
// TODO handle repos from parents
404+
if err := p.resolveParent(ctx, pom, opts.repositories); err != nil {
400405
return analysisResult{}, xerrors.Errorf("pom resolve error: %w", err)
401406
}
402407

@@ -411,6 +416,7 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
411416

412417
art := pom.artifact()
413418
art.RootFilePath = opts.rootFilePath
419+
art.Repositories = opts.repositories
414420

415421
return analysisResult{
416422
filePath: pom.filePath,
@@ -423,13 +429,13 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
423429
}
424430

425431
// resolveParent resolves its parent POMs and inherits properties, dependencies, and dependencyManagement.
426-
func (p *Parser) resolveParent(ctx context.Context, pom *pom) error {
432+
func (p *Parser) resolveParent(ctx context.Context, pom *pom, pomRepos []repository) error {
427433
if pom.nil() {
428434
return nil
429435
}
430436

431437
// Parse parent POM
432-
parent, err := p.parseParent(ctx, pom.filePath, pom.content.Parent)
438+
parent, err := p.parseParent(ctx, pom.filePath, pom.content.Parent, pomRepos)
433439
if err != nil {
434440
return xerrors.Errorf("parent error: %w", err)
435441
}
@@ -576,7 +582,7 @@ func excludeDep(exclusions set.Set[string], art artifact) bool {
576582
return false
577583
}
578584

579-
func (p *Parser) parseParent(ctx context.Context, currentPath string, parent pomParent) (*pom, error) {
585+
func (p *Parser) parseParent(ctx context.Context, currentPath string, parent pomParent, pomRepos []repository) (*pom, error) {
580586
// Pass nil properties so that variables in <parent> are not evaluated.
581587
target := newArtifact(parent.GroupId, parent.ArtifactId, parent.Version, nil, nil)
582588
// if version is property (e.g. ${revision}) - we still need to parse this pom
@@ -588,7 +594,7 @@ func (p *Parser) parseParent(ctx context.Context, currentPath string, parent pom
588594
logger.Debug("Start parent")
589595
defer logger.Debug("Exit parent")
590596

591-
parentPOM, err := p.retrieveParent(ctx, currentPath, parent.RelativePath, target)
597+
parentPOM, err := p.retrieveParent(ctx, currentPath, parent.RelativePath, target, pomRepos)
592598
if err != nil {
593599
if shouldReturnError(err) {
594600
return nil, err
@@ -597,34 +603,34 @@ func (p *Parser) parseParent(ctx context.Context, currentPath string, parent pom
597603
return &pom{content: &pomXML{}}, nil
598604
}
599605

600-
if err = p.resolveParent(ctx, parentPOM); err != nil {
606+
if err = p.resolveParent(ctx, parentPOM, pomRepos); err != nil {
601607
return nil, xerrors.Errorf("parent pom resolve error: %w", err)
602608
}
603609

604610
return parentPOM, nil
605611
}
606612

607-
func (p *Parser) retrieveParent(ctx context.Context, currentPath, relativePath string, target artifact) (*pom, error) {
613+
func (p *Parser) retrieveParent(ctx context.Context, currentPath, relativePath string, target artifact, pomRepos []repository) (*pom, error) {
608614
var errs error
609615

610616
// Try relativePath
611617
if relativePath != "" {
612-
pom, err := p.tryRelativePath(ctx, target, currentPath, relativePath)
618+
pom, err := p.tryRelativePath(ctx, target, currentPath, relativePath, pomRepos)
613619
if err == nil {
614620
return pom, nil
615621
}
616622
errs = multierror.Append(errs, err)
617623
}
618624

619625
// If not found, search the parent director
620-
pom, err := p.tryRelativePath(ctx, target, currentPath, "../pom.xml")
626+
pom, err := p.tryRelativePath(ctx, target, currentPath, "../pom.xml", pomRepos)
621627
if err == nil {
622628
return pom, nil
623629
}
624630
errs = multierror.Append(errs, err)
625631

626632
// If not found, search local/remote remoteRepositories
627-
pom, err = p.tryRepository(ctx, target.GroupID, target.ArtifactID, target.Version.String())
633+
pom, err = p.tryRepository(ctx, target.GroupID, target.ArtifactID, target.Version.String(), pomRepos)
628634
if err == nil {
629635
return pom, nil
630636
}
@@ -634,7 +640,7 @@ func (p *Parser) retrieveParent(ctx context.Context, currentPath, relativePath s
634640
return nil, errs
635641
}
636642

637-
func (p *Parser) tryRelativePath(ctx context.Context, parentArtifact artifact, currentPath, relativePath string) (*pom, error) {
643+
func (p *Parser) tryRelativePath(ctx context.Context, parentArtifact artifact, currentPath, relativePath string, pomRepos []repository) (*pom, error) {
638644
parsedPOM, err := p.openRelativePom(currentPath, relativePath)
639645
if err != nil {
640646
return nil, err
@@ -649,7 +655,7 @@ func (p *Parser) tryRelativePath(ctx context.Context, parentArtifact artifact, c
649655
if parsedPOM.artifact().ArtifactID != parentArtifact.ArtifactID {
650656
return nil, xerrors.New("'parent.relativePath' points at wrong local POM")
651657
}
652-
if err := p.resolveParent(ctx, parsedPOM); err != nil {
658+
if err := p.resolveParent(ctx, parsedPOM, pomRepos); err != nil {
653659
return nil, xerrors.Errorf("analyze error: %w", err)
654660
}
655661

@@ -699,7 +705,7 @@ func (p *Parser) openPom(filePath string) (*pom, error) {
699705
}, nil
700706
}
701707

702-
func (p *Parser) tryRepository(ctx context.Context, groupID, artifactID, version string) (*pom, error) {
708+
func (p *Parser) tryRepository(ctx context.Context, groupID, artifactID, version string, pomRepos []repository) (*pom, error) {
703709
if version == "" {
704710
return nil, xerrors.Errorf("Version missing for %s:%s", groupID, artifactID)
705711
}
@@ -717,7 +723,7 @@ func (p *Parser) tryRepository(ctx context.Context, groupID, artifactID, version
717723
}
718724

719725
// Search remote remoteRepositories
720-
loaded, err = p.fetchPOMFromRemoteRepositories(ctx, paths, isSnapshot(version))
726+
loaded, err = p.fetchPOMFromRemoteRepositories(ctx, paths, isSnapshot(version), pomRepos)
721727
if err == nil {
722728
return loaded, nil
723729
// We should return error if it's not "not found" error
@@ -735,7 +741,7 @@ func (p *Parser) loadPOMFromLocalRepository(paths []string) (*pom, error) {
735741
return p.openPom(localPath)
736742
}
737743

738-
func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []string, snapshot bool) (*pom, error) {
744+
func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []string, snapshot bool, pomRepos []repository) (*pom, error) {
739745
// Do not try fetching pom.xml from remote repositories in offline mode
740746
if p.offline {
741747
p.logger.Debug("Fetching the remote pom.xml is skipped")
@@ -744,9 +750,9 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
744750

745751
// Try all remoteRepositories by following order:
746752
// 1. remoteRepositories from settings.xml
747-
// 2. remoteRepositories from pom.xml
753+
// 2. remoteRepositories from pom.xml (passed as parameter)
748754
// 3. default remoteRepository (Maven Central for Release repository)
749-
for _, repo := range slices.Concat(p.remoteRepos.settings, p.remoteRepos.pom, []repository{p.remoteRepos.defaultRepo}) {
755+
for _, repo := range slices.Concat(p.remoteRepos.settings, pomRepos, []repository{p.remoteRepos.defaultRepo}) {
750756
// Skip Release only repositories for snapshot artifacts and vice versa
751757
if snapshot && !repo.snapshotEnabled || !snapshot && !repo.releaseEnabled {
752758
continue

pkg/dependency/parser/java/pom/parse_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package pom_test
22

33
import (
4+
"bytes"
45
"fmt"
6+
"io/fs"
57
"net/http"
68
"net/http/httptest"
79
"os"
810
"path/filepath"
911
"testing"
1012

13+
"github.com/samber/lo"
1114
"github.com/stretchr/testify/assert"
1215
"github.com/stretchr/testify/require"
16+
"golang.org/x/tools/txtar"
1317

1418
"github.com/aquasecurity/trivy/pkg/dependency/parser/java/pom"
1519
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
@@ -2488,3 +2492,118 @@ func TestPom_Parse(t *testing.T) {
24882492
})
24892493
}
24902494
}
2495+
2496+
// TestPom_Parse_Remote_Repos verifies that POM files are fetched from the correct repositories.
2497+
// The verification is done through the license field, as it's the only way to deterministically
2498+
// identify which repository an artifact came from.
2499+
func TestPom_Parse_Remote_Repos(t *testing.T) {
2500+
const rootRepoPlaceholder = "REPO_ROOT_URL"
2501+
2502+
tests := []struct {
2503+
name string
2504+
inputFile string
2505+
rootRepoTxtar string // txtar file for root repository
2506+
repos map[string]string // key: repository URL placeholder, value: txtar file path
2507+
wantPackages map[string]string
2508+
}{
2509+
{
2510+
name: "different repos for different dependencies",
2511+
inputFile: filepath.Join("testdata", "different-repos-for-different-poms", "pom.xml"),
2512+
rootRepoTxtar: filepath.Join("testdata", "different-repos-for-different-poms", "repo-root-artifacts.txtar"),
2513+
repos: map[string]string{
2514+
"REPO1_URL": filepath.Join("testdata", "different-repos-for-different-poms", "repo1-artifacts.txtar"),
2515+
"REPO2_URL": filepath.Join("testdata", "different-repos-for-different-poms", "repo2-artifacts.txtar"),
2516+
},
2517+
wantPackages: map[string]string{
2518+
"org.example:example-api:1.7.30::2cbe1ca4": "License from repo1",
2519+
"org.example:example-api2:1.0.0::f8958ec7": "License from repo2",
2520+
"org.example:example-api3:1.0.0::887fc940": "License from reporoot",
2521+
},
2522+
},
2523+
{
2524+
name: "root POM with module inherits repository",
2525+
inputFile: filepath.Join("testdata", "repo-from-root-for-dep-from-module", "pom.xml"),
2526+
rootRepoTxtar: filepath.Join("testdata", "repo-from-root-for-dep-from-module", "repo-artifacts.txtar"),
2527+
repos: nil,
2528+
wantPackages: map[string]string{
2529+
"org.example:example-api:1.0.0::4a790a84": "License from root repo",
2530+
},
2531+
},
2532+
}
2533+
2534+
for _, tt := range tests {
2535+
t.Run(tt.name, func(t *testing.T) {
2536+
// Setup remote repositories from txtar files
2537+
repoURLs := make(map[string]string)
2538+
for placeholder, txtarPath := range tt.repos {
2539+
repoURL := setupTxtarRepository(t, txtarPath, nil)
2540+
repoURLs[placeholder] = repoURL
2541+
}
2542+
2543+
// Setup root repository with replacements for repo placeholders
2544+
rootRepoURL := setupTxtarRepository(t, tt.rootRepoTxtar, repoURLs)
2545+
2546+
// Prepare input POM file with root repository URL
2547+
pomContent := applyReplacements(t, tt.inputFile, map[string]string{
2548+
rootRepoPlaceholder: rootRepoURL,
2549+
})
2550+
2551+
// Parse the POM
2552+
parser := pom.NewParser(tt.inputFile)
2553+
pkgs, _, err := parser.Parse(t.Context(), bytes.NewReader(pomContent))
2554+
require.NoError(t, err)
2555+
2556+
// Verify expected packages
2557+
for pkgID, wantLicense := range tt.wantPackages {
2558+
p, found := lo.Find(pkgs, func(pkg ftypes.Package) bool {
2559+
return pkg.ID == pkgID
2560+
})
2561+
require.True(t, found, "package %s not found", pkgID)
2562+
require.NotEmpty(t, p.Licenses, "package %s has no licenses", pkgID)
2563+
require.Equal(t, wantLicense, p.Licenses[0])
2564+
}
2565+
})
2566+
}
2567+
}
2568+
2569+
// applyReplacements reads a file, applies replacements, and returns the modified content.
2570+
func applyReplacements(t *testing.T, filePath string, replacements map[string]string) []byte {
2571+
t.Helper()
2572+
2573+
content, err := os.ReadFile(filePath)
2574+
require.NoError(t, err)
2575+
2576+
for oldURL, newURL := range replacements {
2577+
content = bytes.ReplaceAll(content, []byte(oldURL), []byte(newURL))
2578+
}
2579+
2580+
return content
2581+
}
2582+
2583+
// txtarWithReposReplace reads a txtar file, applies repository URL replacements,
2584+
// and returns the result as a fs.FS.
2585+
func txtarWithReposReplace(t *testing.T, txtarPath string, reposReplacements map[string]string) fs.FS {
2586+
t.Helper()
2587+
2588+
content := applyReplacements(t, txtarPath, reposReplacements)
2589+
2590+
archive := txtar.Parse(content)
2591+
2592+
fsys, err := txtar.FS(archive)
2593+
require.NoError(t, err)
2594+
2595+
return fsys
2596+
}
2597+
2598+
// setupTxtarRepository reads a txtar file, applies repository URL replacements,
2599+
// starts an HTTP test server with the files, and returns the server URL.
2600+
func setupTxtarRepository(t *testing.T, txtarPath string, reposReplacements map[string]string) string {
2601+
t.Helper()
2602+
2603+
fsys := txtarWithReposReplace(t, txtarPath, reposReplacements)
2604+
2605+
ts := httptest.NewServer(http.FileServer(http.FS(fsys)))
2606+
t.Cleanup(ts.Close)
2607+
2608+
return ts.URL
2609+
}

0 commit comments

Comments
 (0)