mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat(java): add support remote repositories from settings.xml files (#9708)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -29,14 +29,10 @@ import (
|
||||
xio "github.com/aquasecurity/trivy/pkg/x/io"
|
||||
)
|
||||
|
||||
const (
|
||||
centralURL = "https://repo.maven.apache.org/maven2/"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
offline bool
|
||||
releaseRemoteRepos []string
|
||||
snapshotRemoteRepos []string
|
||||
offline bool
|
||||
defaultRepo repository
|
||||
settingsRepos []repository
|
||||
}
|
||||
|
||||
type option func(*options)
|
||||
@@ -47,55 +43,71 @@ func WithOffline(offline bool) option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithReleaseRemoteRepos(repos []string) option {
|
||||
func WithDefaultRepo(repoURL string, releaseEnabled, snapshotEnabled bool) option {
|
||||
return func(opts *options) {
|
||||
opts.releaseRemoteRepos = repos
|
||||
u, _ := url.Parse(repoURL)
|
||||
opts.defaultRepo = repository{
|
||||
url: *u,
|
||||
releaseEnabled: releaseEnabled,
|
||||
snapshotEnabled: snapshotEnabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithSnapshotRemoteRepos(repos []string) option {
|
||||
func WithSettingsRepos(repoURLs []string, releaseEnabled, snapshotEnabled bool) option {
|
||||
return func(opts *options) {
|
||||
opts.snapshotRemoteRepos = repos
|
||||
opts.settingsRepos = lo.Map(repoURLs, func(repoURL string, _ int) repository {
|
||||
u, _ := url.Parse(repoURL)
|
||||
return repository{
|
||||
url: *u,
|
||||
releaseEnabled: releaseEnabled,
|
||||
snapshotEnabled: snapshotEnabled,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
logger *log.Logger
|
||||
rootPath string
|
||||
cache pomCache
|
||||
localRepository string
|
||||
releaseRemoteRepos []string
|
||||
snapshotRemoteRepos []string
|
||||
offline bool
|
||||
servers []Server
|
||||
logger *log.Logger
|
||||
rootPath string
|
||||
cache pomCache
|
||||
localRepository string
|
||||
remoteRepos repositories
|
||||
offline bool
|
||||
servers []Server
|
||||
}
|
||||
|
||||
func NewParser(filePath string, opts ...option) *Parser {
|
||||
o := &options{
|
||||
offline: false,
|
||||
releaseRemoteRepos: []string{centralURL}, // Maven doesn't use central repository for snapshot dependencies
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
offline: false,
|
||||
defaultRepo: mavenCentralRepo,
|
||||
}
|
||||
|
||||
s := readSettings()
|
||||
o.settingsRepos = s.effectiveRepositories()
|
||||
localRepository := s.LocalRepository
|
||||
if localRepository == "" {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
localRepository = filepath.Join(homeDir, ".m2", "repository")
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
remoteRepos := repositories{
|
||||
defaultRepo: o.defaultRepo,
|
||||
settings: o.settingsRepos,
|
||||
}
|
||||
|
||||
return &Parser{
|
||||
logger: log.WithPrefix("pom"),
|
||||
rootPath: filepath.Clean(filePath),
|
||||
cache: newPOMCache(),
|
||||
localRepository: localRepository,
|
||||
releaseRemoteRepos: o.releaseRemoteRepos,
|
||||
snapshotRemoteRepos: o.snapshotRemoteRepos,
|
||||
offline: o.offline,
|
||||
servers: s.Servers,
|
||||
logger: log.WithPrefix("pom"),
|
||||
rootPath: filepath.Clean(filePath),
|
||||
cache: newPOMCache(),
|
||||
localRepository: localRepository,
|
||||
remoteRepos: remoteRepos,
|
||||
offline: o.offline,
|
||||
servers: s.Servers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,9 +374,10 @@ func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (a
|
||||
opts.exclusions = set.New[string]()
|
||||
}
|
||||
// Update remoteRepositories
|
||||
pomReleaseRemoteRepos, pomSnapshotRemoteRepos := pom.repositories(p.servers)
|
||||
p.releaseRemoteRepos = lo.Uniq(append(pomReleaseRemoteRepos, p.releaseRemoteRepos...))
|
||||
p.snapshotRemoteRepos = lo.Uniq(append(pomSnapshotRemoteRepos, p.snapshotRemoteRepos...))
|
||||
pomRepos := pom.repositories(p.servers)
|
||||
p.remoteRepos.pom = lo.UniqBy(append(pomRepos, p.remoteRepos.pom...), func(r repository) url.URL {
|
||||
return r.url
|
||||
})
|
||||
|
||||
// Resolve parent POM
|
||||
if err := p.resolveParent(ctx, pom); err != nil {
|
||||
@@ -710,17 +723,19 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
|
||||
return nil, xerrors.New("offline mode")
|
||||
}
|
||||
|
||||
remoteRepos := p.releaseRemoteRepos
|
||||
// Maven uses only snapshot repos for snapshot artifacts
|
||||
if snapshot {
|
||||
remoteRepos = p.snapshotRemoteRepos
|
||||
}
|
||||
// Try all remoteRepositories by following order:
|
||||
// 1. remoteRepositories from settings.xml
|
||||
// 2. remoteRepositories from pom.xml
|
||||
// 3. default remoteRepository (Maven Central for Release repository)
|
||||
for _, repo := range slices.Concat(p.remoteRepos.settings, p.remoteRepos.pom, []repository{p.remoteRepos.defaultRepo}) {
|
||||
// Skip Release only repositories for snapshot artifacts and vice versa
|
||||
if snapshot && !repo.snapshotEnabled || !snapshot && !repo.releaseEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
// try all remoteRepositories
|
||||
for _, repo := range remoteRepos {
|
||||
repoPaths := slices.Clone(paths) // Clone slice to avoid overwriting last element of `paths`
|
||||
if snapshot {
|
||||
pomFileName, err := p.fetchPomFileNameFromMavenMetadata(ctx, repo, repoPaths)
|
||||
pomFileName, err := p.fetchPomFileNameFromMavenMetadata(ctx, repo.url, repoPaths)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch maven-metadata.xml error: %w", err)
|
||||
}
|
||||
@@ -729,7 +744,7 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
|
||||
repoPaths[len(repoPaths)-1] = pomFileName
|
||||
}
|
||||
}
|
||||
fetched, err := p.fetchPOMFromRemoteRepository(ctx, repo, repoPaths)
|
||||
fetched, err := p.fetchPOMFromRemoteRepository(ctx, repo.url, repoPaths)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("fetch repository error: %w", err)
|
||||
} else if fetched == nil {
|
||||
@@ -740,12 +755,7 @@ func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []str
|
||||
return nil, xerrors.Errorf("the POM was not found in remote remoteRepositories")
|
||||
}
|
||||
|
||||
func (p *Parser) remoteRepoRequest(ctx context.Context, repo string, paths []string) (*http.Request, error) {
|
||||
repoURL, err := url.Parse(repo)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("unable to parse URL: %w", err)
|
||||
}
|
||||
|
||||
func (p *Parser) remoteRepoRequest(ctx context.Context, repoURL url.URL, paths []string) (*http.Request, error) {
|
||||
paths = append([]string{repoURL.Path}, paths...)
|
||||
repoURL.Path = path.Join(paths...)
|
||||
|
||||
@@ -762,14 +772,14 @@ func (p *Parser) remoteRepoRequest(ctx context.Context, repo string, paths []str
|
||||
}
|
||||
|
||||
// fetchPomFileNameFromMavenMetadata fetches `maven-metadata.xml` file to detect file name of pom file.
|
||||
func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo string, paths []string) (string, error) {
|
||||
func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repoURL url.URL, paths []string) (string, error) {
|
||||
// Overwrite pom file name to `maven-metadata.xml`
|
||||
mavenMetadataPaths := slices.Clone(paths[:len(paths)-1]) // Clone slice to avoid shadow overwriting last element of `paths`
|
||||
mavenMetadataPaths = append(mavenMetadataPaths, "maven-metadata.xml")
|
||||
|
||||
req, err := p.remoteRepoRequest(ctx, repo, mavenMetadataPaths)
|
||||
req, err := p.remoteRepoRequest(ctx, repoURL, mavenMetadataPaths)
|
||||
if err != nil {
|
||||
p.logger.Debug("Unable to create request", log.String("repo", repo), log.Err(err))
|
||||
p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -779,14 +789,16 @@ func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo str
|
||||
if shouldReturnError(err) {
|
||||
return "", err
|
||||
}
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Err(err))
|
||||
return "", nil
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Int("statusCode", resp.StatusCode))
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err))
|
||||
return "", nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
mavenMetadata, err := parseMavenMetadata(resp.Body)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("failed to parse maven-metadata.xml file: %w", err)
|
||||
@@ -803,10 +815,10 @@ func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repo str
|
||||
return pomFileName, nil
|
||||
}
|
||||
|
||||
func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repo string, paths []string) (*pom, error) {
|
||||
req, err := p.remoteRepoRequest(ctx, repo, paths)
|
||||
func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repoURL url.URL, paths []string) (*pom, error) {
|
||||
req, err := p.remoteRepoRequest(ctx, repoURL, paths)
|
||||
if err != nil {
|
||||
p.logger.Debug("Unable to create request", log.String("repo", repo), log.Err(err))
|
||||
p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -816,14 +828,16 @@ func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repo string,
|
||||
if shouldReturnError(err) {
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Err(err))
|
||||
return nil, nil
|
||||
} else if resp.StatusCode != http.StatusOK {
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.String()), log.Int("statusCode", resp.StatusCode))
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err))
|
||||
return nil, nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, err := parsePom(resp.Body, false)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to parse the remote POM: %w", err)
|
||||
|
||||
@@ -110,13 +110,14 @@ var (
|
||||
|
||||
func TestPom_Parse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputFile string
|
||||
local bool
|
||||
offline bool
|
||||
want []ftypes.Package
|
||||
wantDeps []ftypes.Dependency
|
||||
wantErr string
|
||||
name string
|
||||
inputFile string
|
||||
local bool
|
||||
enableRepoForSettingsRepo bool // use another repo for repository from settings.xml
|
||||
offline bool
|
||||
want []ftypes.Package
|
||||
wantDeps []ftypes.Dependency
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "local repository",
|
||||
@@ -326,6 +327,55 @@ func TestPom_Parse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple repositories are used",
|
||||
inputFile: filepath.Join("testdata", "happy", "pom.xml"),
|
||||
local: false,
|
||||
enableRepoForSettingsRepo: true,
|
||||
want: []ftypes.Package{
|
||||
{
|
||||
ID: "com.example:happy:1.0.0",
|
||||
Name: "com.example:happy",
|
||||
Version: "1.0.0",
|
||||
Licenses: []string{"BSD-3-Clause"},
|
||||
Relationship: ftypes.RelationshipRoot,
|
||||
},
|
||||
{
|
||||
ID: "org.example:example-api:1.7.30",
|
||||
Name: "org.example:example-api",
|
||||
Version: "1.7.30",
|
||||
Licenses: []string{"Custom License from custom repo"},
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 32,
|
||||
EndLine: 36,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "org.example:example-runtime:1.0.0",
|
||||
Name: "org.example:example-runtime",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 37,
|
||||
EndLine: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantDeps: []ftypes.Dependency{
|
||||
{
|
||||
ID: "com.example:happy:1.0.0",
|
||||
DependsOn: []string{
|
||||
"org.example:example-api:1.7.30",
|
||||
"org.example:example-runtime:1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "inherit parent properties",
|
||||
inputFile: filepath.Join("testdata", "parent-properties", "child", "pom.xml"),
|
||||
@@ -2206,7 +2256,8 @@ func TestPom_Parse(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
var remoteRepos []string
|
||||
var defaultRepo string
|
||||
var settingsRepos []string
|
||||
if tt.local {
|
||||
// for local repository
|
||||
t.Setenv("MAVEN_HOME", "testdata/settings/global")
|
||||
@@ -2214,10 +2265,18 @@ func TestPom_Parse(t *testing.T) {
|
||||
// for remote repository
|
||||
h := http.FileServer(http.Dir(filepath.Join("testdata", "repository")))
|
||||
ts := httptest.NewServer(h)
|
||||
remoteRepos = []string{ts.URL}
|
||||
defaultRepo = ts.URL
|
||||
|
||||
// Enable custom repository to be sure in repository order checking
|
||||
if tt.enableRepoForSettingsRepo {
|
||||
ch := http.FileServer(http.Dir(filepath.Join("testdata", "repository-for-settings-repo")))
|
||||
cts := httptest.NewServer(ch)
|
||||
settingsRepos = []string{cts.URL}
|
||||
}
|
||||
}
|
||||
|
||||
p := pom.NewParser(tt.inputFile, pom.WithReleaseRemoteRepos(remoteRepos), pom.WithSnapshotRemoteRepos(remoteRepos), pom.WithOffline(tt.offline))
|
||||
p := pom.NewParser(tt.inputFile, pom.WithDefaultRepo(defaultRepo, true, true),
|
||||
pom.WithSettingsRepos(settingsRepos, true, false), pom.WithOffline(tt.offline))
|
||||
|
||||
gotPkgs, gotDeps, err := p.Parse(t.Context(), f)
|
||||
if tt.wantErr != "" {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
"github.com/aquasecurity/trivy/pkg/set"
|
||||
"github.com/aquasecurity/trivy/pkg/x/slices"
|
||||
)
|
||||
@@ -123,42 +121,8 @@ func (p *pom) licenses() []string {
|
||||
}))
|
||||
}
|
||||
|
||||
func (p *pom) repositories(servers []Server) ([]string, []string) {
|
||||
logger := log.WithPrefix("pom")
|
||||
var releaseRepos, snapshotRepos []string
|
||||
for _, rep := range p.content.Repositories.Repository {
|
||||
snapshot := rep.Snapshots.Enabled == "true"
|
||||
release := rep.Releases.Enabled == "true"
|
||||
// Add only enabled repositories
|
||||
if !release && !snapshot {
|
||||
continue
|
||||
}
|
||||
|
||||
repoURL, err := url.Parse(rep.URL)
|
||||
if err != nil {
|
||||
logger.Debug("Unable to parse remote repository url", log.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the credentials from settings.xml based on matching server id
|
||||
// with the repository id from pom.xml and use it for accessing the repository url
|
||||
for _, server := range servers {
|
||||
if rep.ID == server.ID && server.Username != "" && server.Password != "" {
|
||||
repoURL.User = url.UserPassword(server.Username, server.Password)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Adding repository", log.String("id", rep.ID), log.String("url", rep.URL))
|
||||
if snapshot {
|
||||
snapshotRepos = append(snapshotRepos, repoURL.String())
|
||||
}
|
||||
if release {
|
||||
releaseRepos = append(releaseRepos, repoURL.String())
|
||||
}
|
||||
}
|
||||
|
||||
return releaseRepos, snapshotRepos
|
||||
func (p *pom) repositories(servers []Server) []repository {
|
||||
return resolvePomRepos(servers, p.content.Repositories)
|
||||
}
|
||||
|
||||
type pomXML struct {
|
||||
@@ -177,7 +141,7 @@ type pomXML struct {
|
||||
Dependencies pomDependencies `xml:"dependencies"`
|
||||
} `xml:"dependencyManagement"`
|
||||
Dependencies pomDependencies `xml:"dependencies"`
|
||||
Repositories pomRepositories `xml:"repositories"`
|
||||
Repositories []pomRepository `xml:"repositories>repository"`
|
||||
}
|
||||
|
||||
type pomParent struct {
|
||||
@@ -384,22 +348,10 @@ func findDep(name string, depManagement []pomDependency) (pomDependency, bool) {
|
||||
})
|
||||
}
|
||||
|
||||
type pomRepositories struct {
|
||||
Text string `xml:",chardata"`
|
||||
Repository []pomRepository `xml:"repository"`
|
||||
}
|
||||
|
||||
type pomRepository struct {
|
||||
Text string `xml:",chardata"`
|
||||
ID string `xml:"id"`
|
||||
Name string `xml:"name"`
|
||||
URL string `xml:"url"`
|
||||
Releases struct {
|
||||
Text string `xml:",chardata"`
|
||||
Enabled string `xml:"enabled"`
|
||||
} `xml:"releases"`
|
||||
Snapshots struct {
|
||||
Text string `xml:",chardata"`
|
||||
Enabled string `xml:"enabled"`
|
||||
} `xml:"snapshots"`
|
||||
ID string `xml:"id"`
|
||||
Name string `xml:"name"`
|
||||
URL string `xml:"url"`
|
||||
ReleasesEnabled string `xml:"releases>enabled"`
|
||||
SnapshotsEnabled string `xml:"snapshots>enabled"`
|
||||
}
|
||||
|
||||
67
pkg/dependency/parser/java/pom/repository.go
Normal file
67
pkg/dependency/parser/java/pom/repository.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package pom
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/log"
|
||||
)
|
||||
|
||||
var centralURL, _ = url.Parse("https://repo.maven.apache.org/maven2/")
|
||||
|
||||
type repository struct {
|
||||
url url.URL
|
||||
releaseEnabled bool
|
||||
snapshotEnabled bool
|
||||
}
|
||||
|
||||
type repositories struct {
|
||||
settings []repository // Repositories from settings.xml files
|
||||
pom []repository // Repositories from pom file and its parents (parent and upper pom files)
|
||||
defaultRepo repository // Default repository - Maven Central for Release, empty for Snapshot
|
||||
}
|
||||
|
||||
var mavenCentralRepo = repository{
|
||||
url: *centralURL,
|
||||
releaseEnabled: true,
|
||||
}
|
||||
|
||||
func resolvePomRepos(servers []Server, pomRepos []pomRepository) []repository {
|
||||
logger := log.WithPrefix("pom")
|
||||
var repos []repository
|
||||
for _, rep := range pomRepos {
|
||||
r := repository{
|
||||
releaseEnabled: rep.ReleasesEnabled == "true",
|
||||
snapshotEnabled: rep.SnapshotsEnabled == "true",
|
||||
}
|
||||
|
||||
// Add only enabled repositories
|
||||
if !r.releaseEnabled && !r.snapshotEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
repoURL, err := url.Parse(rep.URL)
|
||||
if err != nil {
|
||||
var ue *url.Error
|
||||
if errors.As(err, &ue) {
|
||||
err = ue.Unwrap()
|
||||
}
|
||||
logger.Debug("Unable to parse remote repository url", log.String("id", rep.ID), log.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the credentials from settings.xml based on matching server id
|
||||
// with the repository id from pom.xml and use it for accessing the repository url
|
||||
for _, server := range servers {
|
||||
if rep.ID == server.ID && server.Username != "" && server.Password != "" {
|
||||
repoURL.User = url.UserPassword(server.Username, server.Password)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Adding repository", log.String("id", rep.ID), log.String("url", repoURL.Redacted()))
|
||||
r.url = *repoURL
|
||||
repos = append(repos, r)
|
||||
}
|
||||
return repos
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"encoding/xml"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/samber/lo/mutable"
|
||||
"golang.org/x/net/html/charset"
|
||||
)
|
||||
|
||||
@@ -14,20 +17,35 @@ type Server struct {
|
||||
Password string `xml:"password"`
|
||||
}
|
||||
|
||||
type settings struct {
|
||||
LocalRepository string `xml:"localRepository"`
|
||||
Servers []Server `xml:"servers>server"`
|
||||
type Profile struct {
|
||||
ID string `xml:"id"`
|
||||
Repositories []pomRepository `xml:"repositories>repository"`
|
||||
ActiveByDefault bool `xml:"activation>activeByDefault"`
|
||||
}
|
||||
|
||||
// serverFound checks that servers already contain server.
|
||||
// Maven compares servers by ID only.
|
||||
func serverFound(servers []Server, id string) bool {
|
||||
for _, server := range servers {
|
||||
if server.ID == id {
|
||||
return true
|
||||
type settings struct {
|
||||
LocalRepository string `xml:"localRepository"`
|
||||
Servers []Server `xml:"servers>server"`
|
||||
Profiles []Profile `xml:"profiles>profile"`
|
||||
ActiveProfiles []string `xml:"activeProfiles>activeProfile"`
|
||||
}
|
||||
|
||||
func (s settings) effectiveRepositories() []repository {
|
||||
var pomRepos []pomRepository
|
||||
for _, profile := range s.Profiles {
|
||||
if slices.Contains(s.ActiveProfiles, profile.ID) || profile.ActiveByDefault {
|
||||
pomRepos = append(pomRepos, profile.Repositories...)
|
||||
}
|
||||
}
|
||||
return false
|
||||
pomRepos = lo.UniqBy(pomRepos, func(r pomRepository) string {
|
||||
return r.ID
|
||||
})
|
||||
|
||||
// mvn takes repositories from settings in reverse order
|
||||
// cf. https://github.com/aquasecurity/trivy/issues/7807#issuecomment-2541485152
|
||||
mutable.Reverse(pomRepos)
|
||||
|
||||
return resolvePomRepos(s.Servers, pomRepos)
|
||||
}
|
||||
|
||||
func readSettings() settings {
|
||||
@@ -52,12 +70,18 @@ func readSettings() settings {
|
||||
if s.LocalRepository == "" {
|
||||
s.LocalRepository = globalSettings.LocalRepository
|
||||
}
|
||||
// Maven checks user servers first, then global servers
|
||||
for _, server := range globalSettings.Servers {
|
||||
if !serverFound(s.Servers, server.ID) {
|
||||
s.Servers = append(s.Servers, server)
|
||||
}
|
||||
}
|
||||
|
||||
// Maven servers
|
||||
s.Servers = lo.UniqBy(append(s.Servers, globalSettings.Servers...), func(server Server) string {
|
||||
return server.ID
|
||||
})
|
||||
|
||||
// Merge profiles
|
||||
s.Profiles = lo.UniqBy(append(s.Profiles, globalSettings.Profiles...), func(p Profile) string {
|
||||
return p.ID
|
||||
})
|
||||
// Merge active profiles
|
||||
s.ActiveProfiles = lo.Uniq(append(s.ActiveProfiles, globalSettings.ActiveProfiles...))
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -89,4 +113,16 @@ func expandAllEnvPlaceholders(s *settings) {
|
||||
s.Servers[i].Username = evaluateVariable(server.Username, nil, nil)
|
||||
s.Servers[i].Password = evaluateVariable(server.Password, nil, nil)
|
||||
}
|
||||
|
||||
for i, profile := range s.Profiles {
|
||||
s.Profiles[i].ID = evaluateVariable(profile.ID, nil, nil)
|
||||
for j, repo := range profile.Repositories {
|
||||
s.Profiles[i].Repositories[j].ID = evaluateVariable(repo.ID, nil, nil)
|
||||
s.Profiles[i].Repositories[j].Name = evaluateVariable(repo.Name, nil, nil)
|
||||
s.Profiles[i].Repositories[j].URL = evaluateVariable(repo.URL, nil, nil)
|
||||
}
|
||||
}
|
||||
for i, activeProfile := range s.ActiveProfiles {
|
||||
s.ActiveProfiles[i] = evaluateVariable(activeProfile, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pom
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -35,6 +36,39 @@ func Test_ReadSettings(t *testing.T) {
|
||||
Username: "test-user-only",
|
||||
},
|
||||
},
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "mycompany-global",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-internal-releases",
|
||||
URL: "https://mycompany.example.com/repository/internal-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "mycompany-global-releases",
|
||||
URL: "https://mycompany.example.com/repository/global-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
},
|
||||
ActiveByDefault: false,
|
||||
},
|
||||
{
|
||||
ID: "default",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-default-releases",
|
||||
URL: "https://mycompany.example.com/repository/default-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
},
|
||||
ActiveByDefault: true,
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -59,18 +93,41 @@ func Test_ReadSettings(t *testing.T) {
|
||||
Username: "test-user-only",
|
||||
},
|
||||
},
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "mycompany-global",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-releases",
|
||||
URL: "https://mycompany.example.com/repository/user-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "mycompany-user-snapshots",
|
||||
URL: "https://mycompany.example.com/repository/user-snapshots",
|
||||
ReleasesEnabled: "false",
|
||||
SnapshotsEnabled: "true",
|
||||
},
|
||||
},
|
||||
ActiveByDefault: true,
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{
|
||||
"mycompany-global",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// $ mvn help:effective-settings
|
||||
//[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
|
||||
//[INFO] --- maven-help-plugin:3.4.0:effective-settings (default-cli) @ standalone-pom ---
|
||||
//Effective user-specific configuration settings:
|
||||
// [INFO] ------------------< org.apache.maven:standalone-pom >-------------------
|
||||
// [INFO] --- maven-help-plugin:3.4.0:effective-settings (default-cli) @ standalone-pom ---
|
||||
// Effective user-specific configuration settings:
|
||||
//
|
||||
//<?xml version="1.0" encoding="UTF-8"?>
|
||||
//<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
|
||||
// <?xml version="1.0" encoding="UTF-8"?>
|
||||
// <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
|
||||
// <localRepository>/root/testdata/user/repository</localRepository>
|
||||
// <servers>
|
||||
// <servers>
|
||||
// <server>
|
||||
// <id>user-server</id>
|
||||
// </server>
|
||||
@@ -87,7 +144,53 @@ func Test_ReadSettings(t *testing.T) {
|
||||
// <id>global-server</id>
|
||||
// </server>
|
||||
// </servers>
|
||||
//</settings>
|
||||
// <profiles>
|
||||
// <profile>
|
||||
// <activation>
|
||||
// <activeByDefault>true</activeByDefault>
|
||||
// </activation>
|
||||
// <repositories>
|
||||
// <repository>
|
||||
// <releases>
|
||||
// <checksumPolicy>fail</checksumPolicy>
|
||||
// </releases>
|
||||
// <snapshots>
|
||||
// <enabled>false</enabled>
|
||||
// </snapshots>
|
||||
// <id>mycompany-releases</id>
|
||||
// <url>https://mycompany.example.com/repository/user-releases</url>
|
||||
// </repository>
|
||||
// <repository>
|
||||
// <releases>
|
||||
// <enabled>false</enabled>
|
||||
// </releases>
|
||||
// <snapshots />
|
||||
// <id>mycompany-user-snapshots</id>
|
||||
// <url>https://mycompany.example.com/repository/user-snapshots</url>
|
||||
// </repository>
|
||||
// </repositories>
|
||||
// <id>mycompany-global</id>
|
||||
// </profile>
|
||||
// <profile>
|
||||
// <activation>
|
||||
// <activeByDefault>true</activeByDefault>
|
||||
// </activation>
|
||||
// <repositories>
|
||||
// <repository>
|
||||
// <releases />
|
||||
// <snapshots>
|
||||
// <enabled>false</enabled>
|
||||
// </snapshots>
|
||||
// <id>mycompany-default-releases</id>
|
||||
// <url>https://mycompany.example.com/repository/default-releases</url>
|
||||
// </repository>
|
||||
// </repositories>
|
||||
// </profile>
|
||||
// </profiles>
|
||||
// <activeProfiles>
|
||||
// <activeProfile>mycompany-global</activeProfile>
|
||||
// </activeProfiles>
|
||||
// </settings>
|
||||
name: "happy path with global and user settings",
|
||||
envs: map[string]string{
|
||||
"HOME": filepath.Join("testdata", "settings", "user"),
|
||||
@@ -112,6 +215,41 @@ func Test_ReadSettings(t *testing.T) {
|
||||
ID: "global-server",
|
||||
},
|
||||
},
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "mycompany-global",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-releases",
|
||||
URL: "https://mycompany.example.com/repository/user-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "mycompany-user-snapshots",
|
||||
URL: "https://mycompany.example.com/repository/user-snapshots",
|
||||
ReleasesEnabled: "false",
|
||||
SnapshotsEnabled: "true",
|
||||
},
|
||||
},
|
||||
ActiveByDefault: true,
|
||||
},
|
||||
{
|
||||
ID: "default",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-default-releases",
|
||||
URL: "https://mycompany.example.com/repository/default-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
},
|
||||
ActiveByDefault: true,
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{
|
||||
"mycompany-global",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -132,6 +270,9 @@ func Test_ReadSettings(t *testing.T) {
|
||||
"SERVER_ID": "server-id-from-env",
|
||||
"USERNAME": "username-from-env",
|
||||
"PASSWORD": "password-from-env",
|
||||
"PROFILE_ID": "mycompany-global",
|
||||
"REPO_ID": "mycompany-releases",
|
||||
"REPO_URL": "https://mycompany.example.com",
|
||||
},
|
||||
wantSettings: settings{
|
||||
LocalRepository: "part1/part2/.m2/repository",
|
||||
@@ -149,6 +290,22 @@ func Test_ReadSettings(t *testing.T) {
|
||||
Username: "test-user-only",
|
||||
},
|
||||
},
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "mycompany-global",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "mycompany-releases",
|
||||
URL: "https://mycompany.example.com/repository/user-releases",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{
|
||||
"mycompany-global",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -163,3 +320,156 @@ func Test_ReadSettings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_effectiveRepositories(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s settings
|
||||
want []repository
|
||||
}{
|
||||
{
|
||||
name: "single active profile, reversed order",
|
||||
s: settings{
|
||||
Servers: []Server{
|
||||
{
|
||||
ID: "r1",
|
||||
Username: "u",
|
||||
Password: "p",
|
||||
},
|
||||
},
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "p1",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "r1",
|
||||
URL: "https://example.com/repo1",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "r2",
|
||||
URL: "https://example.com/repo2",
|
||||
ReleasesEnabled: "false",
|
||||
SnapshotsEnabled: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{"p1"},
|
||||
},
|
||||
want: []repository{
|
||||
{
|
||||
url: mustParseURL(t, "https://example.com/repo2"),
|
||||
releaseEnabled: false,
|
||||
snapshotEnabled: true,
|
||||
},
|
||||
{
|
||||
url: mustParseURL(t, "https://u:p@example.com/repo1"),
|
||||
releaseEnabled: true,
|
||||
snapshotEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "activeByDefault + activeProfiles with dedup and reverse",
|
||||
s: settings{
|
||||
Servers: nil,
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "p1",
|
||||
ActiveByDefault: true,
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "dup",
|
||||
URL: "https://p1.example.com/dup",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "only-p1",
|
||||
URL: "https://p1.example.com/only",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "p2",
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "dup",
|
||||
URL: "https://p2.example.com/dup",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ActiveProfiles: []string{"p2"},
|
||||
},
|
||||
// Expected order after dedup (keep first occurrence from p1) and reverse:
|
||||
// Input order before reverse: [dup(from p1), only-p1, dup(from p2 - removed by dedup)]
|
||||
// After reverse: [only-p1, dup(from p1)]
|
||||
want: []repository{
|
||||
{
|
||||
url: mustParseURL(t, "https://p1.example.com/only"),
|
||||
releaseEnabled: true,
|
||||
snapshotEnabled: true,
|
||||
},
|
||||
{
|
||||
url: mustParseURL(t, "https://p1.example.com/dup"),
|
||||
releaseEnabled: true,
|
||||
snapshotEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "disabled repositories are ignored",
|
||||
s: settings{
|
||||
Profiles: []Profile{
|
||||
{
|
||||
ID: "p",
|
||||
ActiveByDefault: true,
|
||||
Repositories: []pomRepository{
|
||||
{
|
||||
ID: "disabled",
|
||||
URL: "https://example.com/disabled",
|
||||
ReleasesEnabled: "false",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
{
|
||||
ID: "enabled",
|
||||
URL: "https://example.com/enabled",
|
||||
ReleasesEnabled: "true",
|
||||
SnapshotsEnabled: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []repository{
|
||||
{
|
||||
url: mustParseURL(t, "https://example.com/enabled"),
|
||||
releaseEnabled: true,
|
||||
snapshotEnabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.s.effectiveRepositories()
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mustParseURL parses a URL and panics on error; handy for test literals
|
||||
func mustParseURL(t *testing.T, s string) url.URL {
|
||||
t.Helper()
|
||||
u, err := url.Parse(s)
|
||||
require.NoError(t, err)
|
||||
return *u
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>example-api</artifactId>
|
||||
<version>1.7.30</version>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
<name>Example API Module</name>
|
||||
<description>The example API</description>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Custom License from custom repo</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
</project>
|
||||
@@ -18,4 +18,49 @@
|
||||
<username>test-user-only</username>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>mycompany-global</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>mycompany-internal-releases</id>
|
||||
<url>https://mycompany.example.com/repository/internal-releases</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>mycompany-global-releases</id>
|
||||
<url>https://mycompany.example.com/repository/global-releases</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>default</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>mycompany-default-releases</id>
|
||||
<url>https://mycompany.example.com/repository/default-releases</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
|
||||
@@ -18,4 +18,25 @@
|
||||
<username>test-user-only</username>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>${env.PROFILE_ID}</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>${env.REPO_ID}</id>
|
||||
<url>${env.REPO_URL}/repository/user-releases</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
<checksumPolicy>fail</checksumPolicy>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
<activeProfiles>
|
||||
<activeProfile>${env.PROFILE_ID}</activeProfile>
|
||||
</activeProfiles>
|
||||
</settings>
|
||||
|
||||
@@ -18,4 +18,38 @@
|
||||
<username>test-user-only</username>
|
||||
</server>
|
||||
</servers>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>mycompany-global</id>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>mycompany-releases</id>
|
||||
<url>https://mycompany.example.com/repository/user-releases</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
<checksumPolicy>fail</checksumPolicy>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>mycompany-user-snapshots</id>
|
||||
<url>https://mycompany.example.com/repository/user-snapshots</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
</profile>
|
||||
</profiles>
|
||||
<activeProfiles>
|
||||
<activeProfile>mycompany-global</activeProfile>
|
||||
</activeProfiles>
|
||||
</settings>
|
||||
|
||||
Reference in New Issue
Block a user