diff --git a/pkg/dependency/parser/java/pom/parse.go b/pkg/dependency/parser/java/pom/parse.go
index 408a0bfd0bc6..ec935b37988c 100644
--- a/pkg/dependency/parser/java/pom/parse.go
+++ b/pkg/dependency/parser/java/pom/parse.go
@@ -53,6 +53,7 @@ type parser struct {
localRepository string
remoteRepositories []string
offline bool
+ servers []Server
}
func NewParser(filePath string, opts ...option) types.Parser {
@@ -78,6 +79,7 @@ func NewParser(filePath string, opts ...option) types.Parser {
localRepository: localRepository,
remoteRepositories: o.remoteRepos,
offline: o.offline,
+ servers: s.Servers,
}
}
@@ -312,7 +314,7 @@ func (p *parser) analyze(pom *pom, opts analysisOptions) (analysisResult, error)
}
// Update remoteRepositories
- p.remoteRepositories = utils.UniqueStrings(append(p.remoteRepositories, pom.repositories()...))
+ p.remoteRepositories = utils.UniqueStrings(append(pom.repositories(p.servers), p.remoteRepositories...))
// Parent
parent, err := p.parseParent(pom.filePath, pom.content.Parent)
@@ -586,6 +588,10 @@ func (p *parser) openPom(filePath string) (*pom, error) {
}, nil
}
func (p *parser) tryRepository(groupID, artifactID, version string) (*pom, error) {
+ if version == "" {
+ return nil, xerrors.Errorf("Version missing for %s:%s", groupID, artifactID)
+ }
+
// Generate a proper path to the pom.xml
// e.g. com.fasterxml.jackson.core, jackson-annotations, 2.10.0
// => com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0.pom
@@ -644,8 +650,20 @@ func fetchPOMFromRemoteRepository(repo string, paths []string) (*pom, error) {
paths = append([]string{repoURL.Path}, paths...)
repoURL.Path = path.Join(paths...)
- resp, err := http.Get(repoURL.String())
+ client := &http.Client{}
+ req, err := http.NewRequest("GET", repoURL.String(), http.NoBody)
+ if err != nil {
+ log.Logger.Debugf("Request failed for %s%s", repoURL.Host, repoURL.Path)
+ return nil, nil
+ }
+ if repoURL.User != nil {
+ password, _ := repoURL.User.Password()
+ req.SetBasicAuth(repoURL.User.Username(), password)
+ }
+
+ resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
+ log.Logger.Debugf("Failed to fetch from %s%s", repoURL.Host, repoURL.Path)
return nil, nil
}
defer resp.Body.Close()
diff --git a/pkg/dependency/parser/java/pom/parse_test.go b/pkg/dependency/parser/java/pom/parse_test.go
index 837306e9482d..7c7410bd9834 100644
--- a/pkg/dependency/parser/java/pom/parse_test.go
+++ b/pkg/dependency/parser/java/pom/parse_test.go
@@ -1226,7 +1226,7 @@ func TestPom_Parse(t *testing.T) {
var remoteRepos []string
if tt.local {
// for local repository
- t.Setenv("MAVEN_HOME", "testdata")
+ t.Setenv("MAVEN_HOME", "testdata/settings/global")
} else {
// for remote repository
h := http.FileServer(http.Dir(filepath.Join("testdata", "repository")))
diff --git a/pkg/dependency/parser/java/pom/pom.go b/pkg/dependency/parser/java/pom/pom.go
index 7fd7204cbac3..e041cb10fddd 100644
--- a/pkg/dependency/parser/java/pom/pom.go
+++ b/pkg/dependency/parser/java/pom/pom.go
@@ -5,12 +5,14 @@ import (
"fmt"
"io"
"maps"
+ "net/url"
"reflect"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
+ "github.com/aquasecurity/trivy/pkg/dependency/parser/log"
"github.com/aquasecurity/trivy/pkg/dependency/parser/types"
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
)
@@ -113,12 +115,31 @@ func (p pom) licenses() []string {
})
}
-func (p pom) repositories() []string {
+func (p pom) repositories(servers []Server) []string {
var urls []string
for _, rep := range p.content.Repositories.Repository {
- if rep.Releases.Enabled != "false" {
- urls = append(urls, rep.URL)
+ // Add only enabled repositories
+ if rep.Releases.Enabled == "false" && rep.Snapshots.Enabled == "false" {
+ continue
+ }
+
+ repoURL, err := url.Parse(rep.URL)
+ if err != nil {
+ log.Logger.Debugf("Unable to parse remote repository url: %s", 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
+ }
}
+
+ log.Logger.Debugf("Adding repository %s: %s", rep.ID, rep.URL)
+ urls = append(urls, repoURL.String())
}
return urls
}
@@ -139,23 +160,7 @@ type pomXML struct {
Dependencies pomDependencies `xml:"dependencies"`
} `xml:"dependencyManagement"`
Dependencies pomDependencies `xml:"dependencies"`
- Repositories struct {
- Text string `xml:",chardata"`
- Repository []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"`
- } `xml:"repository"`
- } `xml:"repositories"`
+ Repositories pomRepositories `xml:"repositories"`
}
type pomParent struct {
@@ -350,3 +355,23 @@ func findDep(name string, depManagement []pomDependency) (pomDependency, bool) {
return item.Name() == name
})
}
+
+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"`
+}
diff --git a/pkg/dependency/parser/java/pom/settings.go b/pkg/dependency/parser/java/pom/settings.go
index 7237875db98f..42153155706a 100644
--- a/pkg/dependency/parser/java/pom/settings.go
+++ b/pkg/dependency/parser/java/pom/settings.go
@@ -8,24 +8,59 @@ import (
"golang.org/x/net/html/charset"
)
+type Server struct {
+ ID string `xml:"id"`
+ Username string `xml:"username"`
+ Password string `xml:"password"`
+}
+
type settings struct {
- LocalRepository string `xml:"localRepository"`
+ LocalRepository string `xml:"localRepository"`
+ Servers []Server `xml:"servers>server"`
+}
+
+// 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
+ }
+ }
+ return false
}
func readSettings() settings {
+ s := settings{}
+
userSettingsPath := filepath.Join(os.Getenv("HOME"), ".m2", "settings.xml")
userSettings, err := openSettings(userSettingsPath)
- if err == nil && userSettings.LocalRepository != "" {
- return userSettings
+ if err == nil {
+ s = userSettings
}
- globalSettingsPath := filepath.Join(os.Getenv("MAVEN_HOME"), "conf", "settings.xml")
+ // Some package managers use this path by default
+ mavenHome := "/usr/share/maven"
+ if mHome := os.Getenv("MAVEN_HOME"); mHome != "" {
+ mavenHome = mHome
+ }
+ globalSettingsPath := filepath.Join(mavenHome, "conf", "settings.xml")
globalSettings, err := openSettings(globalSettingsPath)
- if err == nil && globalSettings.LocalRepository != "" {
- return globalSettings
+ if err == nil {
+ // We need to merge global and user settings. User settings being dominant.
+ // https://maven.apache.org/settings.html#quick-overview
+ 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)
+ }
+ }
}
- return settings{}
+ return s
}
func openSettings(filePath string) (settings, error) {
diff --git a/pkg/dependency/parser/java/pom/settings_test.go b/pkg/dependency/parser/java/pom/settings_test.go
new file mode 100644
index 000000000000..0d4941a18f97
--- /dev/null
+++ b/pkg/dependency/parser/java/pom/settings_test.go
@@ -0,0 +1,136 @@
+package pom
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_ReadSettings(t *testing.T) {
+ tests := []struct {
+ name string
+ envs map[string]string
+ wantSettings settings
+ }{
+ {
+ name: "happy path with only global settings",
+ envs: map[string]string{
+ "HOME": "",
+ "MAVEN_HOME": filepath.Join("testdata", "settings", "global"),
+ },
+ wantSettings: settings{
+ LocalRepository: "testdata/repository",
+ Servers: []Server{
+ {
+ ID: "global-server",
+ },
+ {
+ ID: "server-with-credentials",
+ Username: "test-user",
+ Password: "test-password-from-global",
+ },
+ {
+ ID: "server-with-name-only",
+ Username: "test-user-only",
+ },
+ },
+ },
+ },
+ {
+ name: "happy path with only user settings",
+ envs: map[string]string{
+ "HOME": filepath.Join("testdata", "settings", "user"),
+ "MAVEN_HOME": "NOT_EXISTING_PATH",
+ },
+ wantSettings: settings{
+ LocalRepository: "testdata/user/repository",
+ Servers: []Server{
+ {
+ ID: "user-server",
+ },
+ {
+ ID: "server-with-credentials",
+ Username: "test-user",
+ Password: "test-password",
+ },
+ {
+ ID: "server-with-name-only",
+ Username: "test-user-only",
+ },
+ },
+ },
+ },
+ {
+ // $ 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:
+ //
+ //
+ //
+ // /root/testdata/user/repository
+ //
+ //
+ // user-server
+ //
+ //
+ // test-user
+ // ***
+ // server-with-credentials
+ //
+ //
+ // test-user-only
+ // server-with-name-only
+ //
+ //
+ // global-server
+ //
+ //
+ //
+ name: "happy path with global and user settings",
+ envs: map[string]string{
+ "HOME": filepath.Join("testdata", "settings", "user"),
+ "MAVEN_HOME": filepath.Join("testdata", "settings", "global"),
+ },
+ wantSettings: settings{
+ LocalRepository: "testdata/user/repository",
+ Servers: []Server{
+ {
+ ID: "user-server",
+ },
+ {
+ ID: "server-with-credentials",
+ Username: "test-user",
+ Password: "test-password",
+ },
+ {
+ ID: "server-with-name-only",
+ Username: "test-user-only",
+ },
+ {
+ ID: "global-server",
+ },
+ },
+ },
+ },
+ {
+ name: "without settings",
+ envs: map[string]string{
+ "HOME": "",
+ "MAVEN_HOME": "NOT_EXISTING_PATH",
+ },
+ wantSettings: settings{},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ for env, settingsDir := range tt.envs {
+ t.Setenv(env, settingsDir)
+ }
+
+ gotSettings := readSettings()
+ require.Equal(t, tt.wantSettings, gotSettings)
+ })
+ }
+}
diff --git a/pkg/dependency/parser/java/pom/testdata/conf/settings.xml b/pkg/dependency/parser/java/pom/testdata/conf/settings.xml
deleted file mode 100644
index 133a101b1bbd..000000000000
--- a/pkg/dependency/parser/java/pom/testdata/conf/settings.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
-
-
-
- testdata/repository
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml b/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml
new file mode 100644
index 000000000000..791406988b03
--- /dev/null
+++ b/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml
@@ -0,0 +1,21 @@
+
+
+ testdata/repository
+
+
+
+ global-server
+
+
+ server-with-credentials
+ test-user
+ test-password-from-global
+
+
+ server-with-name-only
+ test-user-only
+
+
+
diff --git a/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml b/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml
new file mode 100755
index 000000000000..aa720b72c82d
--- /dev/null
+++ b/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml
@@ -0,0 +1,21 @@
+
+
+ testdata/user/repository
+
+
+
+ user-server
+
+
+ server-with-credentials
+ test-user
+ test-password
+
+
+ server-with-name-only
+ test-user-only
+
+
+