diff --git a/.github/workflows/embedded-jar-test.yml b/.github/workflows/embedded-jar-test.yml
new file mode 100644
index 000000000..a2ff93497
--- /dev/null
+++ b/.github/workflows/embedded-jar-test.yml
@@ -0,0 +1,29 @@
+# This test verifies that gradle-dep-tree.jar and maven-dep-tree.jar are kept up-to-date with the version specified in buildscripts/download-jars.js.
+# It accomplishes this by downloading the JARs and executing a "git diff" command.
+# In case there are any differences detected, the test will result in failure.
+name: Embedded Jars Tests
+on:
+ push:
+ branches:
+ - '**'
+ tags-ignore:
+ - '**'
+ pull_request:
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ env:
+ GOPROXY: direct
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download JARs
+ run: buildscripts/download-jars.sh
+
+ - name: Check Diff
+ run: git diff --exit-code
+
+ - name: Log if Failure
+ run: echo "::warning::Please run ./buildscripts/download-jars to use compatible Maven and Gradle dependency tree JARs."
+ if: ${{ failure() }}
+
diff --git a/buildscripts/download-jars.sh b/buildscripts/download-jars.sh
new file mode 100755
index 000000000..28c4d7d22
--- /dev/null
+++ b/buildscripts/download-jars.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+# Please use this script to download the JAR files for maven-dep-tree and gradle-dep-tree into the directory utils/java/.
+# These JARs allow us to build Maven and Gradle dependency trees efficiently and without compilation.
+# Learn more about them here:
+# https://github.com/jfrog/gradle-dep-tree
+# https://github.com/jfrog/maven-dep-tree
+
+# Once you have updated the versions mentioned below, please execute this script from the root directory of the jfrog-cli-core to ensure the JAR files are updated.
+GRADLE_DEP_TREE_VERSION="3.0.2"
+# Changing this version also requires a change in mavenDepTreeVersion within utils/java/mvn.go.
+MAVEN_DEP_TREE_VERSION="1.1.0"
+
+curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/gradle-dep-tree/${GRADLE_DEP_TREE_VERSION}/gradle-dep-tree-${GRADLE_DEP_TREE_VERSION}.jar -o commands/audit/sca/java/resources/gradle-dep-tree.jar
+curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/maven-dep-tree/${MAVEN_DEP_TREE_VERSION}/maven-dep-tree-${MAVEN_DEP_TREE_VERSION}.jar -o commands/audit/sca/java/resources/maven-dep-tree.jar
diff --git a/commands/audit/sca/java/deptreemanager.go b/commands/audit/sca/java/deptreemanager.go
new file mode 100644
index 000000000..e323fad1e
--- /dev/null
+++ b/commands/audit/sca/java/deptreemanager.go
@@ -0,0 +1,124 @@
+package java
+
+import (
+ "encoding/json"
+ "os"
+ "strings"
+
+ "github.com/jfrog/jfrog-cli-core/v2/utils/config"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/xray"
+ "github.com/jfrog/jfrog-client-go/utils/errorutils"
+ xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
+)
+
+const (
+ GavPackageTypeIdentifier = "gav://"
+)
+
+func BuildDependencyTree(depTreeParams DepTreeParams, tech coreutils.Technology) ([]*xrayUtils.GraphNode, map[string][]string, error) {
+ if tech == coreutils.Maven {
+ return buildMavenDependencyTree(&depTreeParams)
+ }
+ return buildGradleDependencyTree(&depTreeParams)
+}
+
+type DepTreeParams struct {
+ UseWrapper bool
+ Server *config.ServerDetails
+ DepsRepo string
+ IsMavenDepTreeInstalled bool
+ IsCurationCmd bool
+ CurationCacheFolder string
+}
+
+type DepTreeManager struct {
+ server *config.ServerDetails
+ depsRepo string
+ useWrapper bool
+}
+
+func NewDepTreeManager(params *DepTreeParams) DepTreeManager {
+ return DepTreeManager{useWrapper: params.UseWrapper, depsRepo: params.DepsRepo, server: params.Server}
+}
+
+// The structure of a dependency tree of a module in a Gradle/Maven project, as created by the gradle-dep-tree and maven-dep-tree plugins.
+type moduleDepTree struct {
+ Root string `json:"root"`
+ Nodes map[string]xray.DepTreeNode `json:"nodes"`
+}
+
+// Reads the output files of the gradle-dep-tree and maven-dep-tree plugins and returns them as a slice of GraphNodes.
+// It takes the output of the plugin's run (which is a byte representation of a list of paths of the output files, separated by newlines) as input.
+func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNode, uniqueDepsMap map[string][]string, err error) {
+ modules, err := parseDepTreeFiles(outputFilePaths)
+ if err != nil {
+ return
+ }
+ uniqueDepsMap = map[string][]string{}
+ for _, module := range modules {
+ moduleTree, moduleUniqueDeps := GetModuleTreeAndDependencies(module)
+ depsGraph = append(depsGraph, moduleTree)
+ for depToAdd, depTypes := range moduleUniqueDeps {
+ uniqueDepsMap[depToAdd] = depTypes
+ }
+ }
+ return
+}
+
+// Returns a dependency tree and a flat list of the module's dependencies for the given module
+func GetModuleTreeAndDependencies(module *moduleDepTree) (*xrayUtils.GraphNode, map[string][]string) {
+ moduleTreeMap := make(map[string]xray.DepTreeNode)
+ moduleDeps := module.Nodes
+ for depName, dependency := range moduleDeps {
+ dependencyId := GavPackageTypeIdentifier + depName
+ var childrenList []string
+ for _, childName := range dependency.Children {
+ childId := GavPackageTypeIdentifier + childName
+ childrenList = append(childrenList, childId)
+ }
+ moduleTreeMap[dependencyId] = xray.DepTreeNode{
+ Types: dependency.Types,
+ Children: childrenList,
+ }
+ }
+ return xray.BuildXrayDependencyTree(moduleTreeMap, GavPackageTypeIdentifier+module.Root)
+}
+
+func parseDepTreeFiles(jsonFilePaths string) ([]*moduleDepTree, error) {
+ outputFilePaths := strings.Split(strings.TrimSpace(jsonFilePaths), "\n")
+ var modules []*moduleDepTree
+ for _, path := range outputFilePaths {
+ results, err := parseDepTreeFile(path)
+ if err != nil {
+ return nil, err
+ }
+ modules = append(modules, results)
+ }
+ return modules, nil
+}
+
+func parseDepTreeFile(path string) (results *moduleDepTree, err error) {
+ depTreeJson, err := os.ReadFile(strings.TrimSpace(path))
+ if errorutils.CheckError(err) != nil {
+ return
+ }
+ results = &moduleDepTree{}
+ err = errorutils.CheckError(json.Unmarshal(depTreeJson, &results))
+ return
+}
+
+func getArtifactoryAuthFromServer(server *config.ServerDetails) (string, string, error) {
+ username, password, err := server.GetAuthenticationCredentials()
+ if err != nil {
+ return "", "", err
+ }
+ if username == "" {
+ return "", "", errorutils.CheckErrorf("a username is required for authenticating with Artifactory")
+ }
+ return username, password, nil
+}
+
+func (dtm *DepTreeManager) GetDepsRepo() string {
+ return dtm.depsRepo
+}
diff --git a/commands/audit/sca/java/deptreemanager_test.go b/commands/audit/sca/java/deptreemanager_test.go
new file mode 100644
index 000000000..827ccac40
--- /dev/null
+++ b/commands/audit/sca/java/deptreemanager_test.go
@@ -0,0 +1,66 @@
+package java
+
+import (
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetGradleGraphFromDepTree(t *testing.T) {
+ // Create and change directory to test workspace
+ tempDirPath, cleanUp := tests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "gradle", "gradle"))
+ defer cleanUp()
+ assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700))
+ expectedTree := map[string]map[string]string{
+ "org.jfrog.example.gradle:shared:1.0": {},
+ "org.jfrog.example.gradle:" + filepath.Base(tempDirPath) + ":1.0": {},
+ "org.jfrog.example.gradle:services:1.0": {},
+ "org.jfrog.example.gradle:webservice:1.0": {
+ "junit:junit:4.11": "",
+ "commons-io:commons-io:1.2": "",
+ "org.apache.wicket:wicket:1.3.7": "",
+ "org.jfrog.example.gradle:shared:1.0": "",
+ "org.jfrog.example.gradle:api:1.0": "",
+ "commons-lang:commons-lang:2.4": "",
+ "commons-collections:commons-collections:3.2": "",
+ },
+ "org.jfrog.example.gradle:api:1.0": {
+ "org.apache.wicket:wicket:1.3.7": "",
+ "org.jfrog.example.gradle:shared:1.0": "",
+ "commons-lang:commons-lang:2.4": "",
+ },
+ }
+ expectedUniqueDeps := []string{
+ "junit:junit:4.11",
+ "org.jfrog.example.gradle:webservice:1.0",
+ "org.jfrog.example.gradle:api:1.0",
+ "org.jfrog.example.gradle:" + filepath.Base(tempDirPath) + ":1.0",
+ "commons-io:commons-io:1.2",
+ "org.apache.wicket:wicket:1.3.7",
+ "org.jfrog.example.gradle:shared:1.0",
+ "org.jfrog.example.gradle:api:1.0",
+ "commons-collections:commons-collections:3.2",
+ "commons-lang:commons-lang:2.4",
+ "org.hamcrest:hamcrest-core:1.3",
+ "org.slf4j:slf4j-api:1.4.2",
+ }
+
+ manager := &gradleDepTreeManager{DepTreeManager{}}
+ outputFileContent, err := manager.runGradleDepTree()
+ assert.NoError(t, err)
+ depTree, uniqueDeps, err := getGraphFromDepTree(outputFileContent)
+ assert.NoError(t, err)
+ reflect.DeepEqual(uniqueDeps, expectedUniqueDeps)
+
+ for _, dependency := range depTree {
+ dependencyId := strings.TrimPrefix(dependency.Id, GavPackageTypeIdentifier)
+ depChild, exists := expectedTree[dependencyId]
+ assert.True(t, exists)
+ assert.Equal(t, len(depChild), len(dependency.Nodes))
+ }
+}
diff --git a/commands/audit/sca/java/gradle.go b/commands/audit/sca/java/gradle.go
new file mode 100644
index 000000000..57ceff8bd
--- /dev/null
+++ b/commands/audit/sca/java/gradle.go
@@ -0,0 +1,199 @@
+package java
+
+import (
+ _ "embed"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/jfrog/build-info-go/build"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/config"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
+ "github.com/jfrog/jfrog-client-go/utils/errorutils"
+ "github.com/jfrog/jfrog-client-go/utils/io/fileutils"
+ "github.com/jfrog/jfrog-client-go/utils/log"
+ xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
+)
+
+const (
+ remoteDepTreePath = "artifactory/oss-release-local"
+ gradlew = "gradlew"
+ gradleDepTreeJarFile = "gradle-dep-tree.jar"
+ gradleDepTreeInitFile = "gradledeptree.init"
+ gradleDepTreeOutputFile = "gradledeptree.out"
+ gradleDepTreeInitScript = `initscript {
+ repositories { %s
+ mavenCentral()
+ }
+ dependencies {
+ classpath files('%s')
+ }
+}
+
+allprojects {
+ repositories { %s
+ }
+ apply plugin: com.jfrog.GradleDepTree
+}`
+ artifactoryRepository = `
+ maven {
+ url "%s/%s"
+ credentials {
+ username = '%s'
+ password = '%s'
+ }
+ }`
+)
+
+//go:embed resources/gradle-dep-tree.jar
+var gradleDepTreeJar []byte
+
+type gradleDepTreeManager struct {
+ DepTreeManager
+}
+
+func buildGradleDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string][]string, err error) {
+ manager := &gradleDepTreeManager{DepTreeManager: NewDepTreeManager(params)}
+ outputFileContent, err := manager.runGradleDepTree()
+ if err != nil {
+ return
+ }
+ dependencyTree, uniqueDeps, err = getGraphFromDepTree(outputFileContent)
+ return
+}
+
+func (gdt *gradleDepTreeManager) runGradleDepTree() (string, error) {
+ // Create the script file in the repository
+ depTreeDir, err := gdt.createDepTreeScriptAndGetDir()
+ if err != nil {
+ return "", err
+ }
+ defer func() {
+ err = errors.Join(err, fileutils.RemoveTempDir(depTreeDir))
+ }()
+
+ if gdt.useWrapper {
+ gdt.useWrapper, err = isGradleWrapperExist()
+ if err != nil {
+ return "", err
+ }
+ }
+
+ output, err := gdt.execGradleDepTree(depTreeDir)
+ if err != nil {
+ return "", err
+ }
+ return string(output), nil
+}
+
+func (gdt *gradleDepTreeManager) createDepTreeScriptAndGetDir() (tmpDir string, err error) {
+ tmpDir, err = fileutils.CreateTempDir()
+ if err != nil {
+ return
+ }
+ var releasesRepo string
+ releasesRepo, gdt.depsRepo, err = getRemoteRepos(gdt.depsRepo, gdt.server)
+ if err != nil {
+ return
+ }
+ gradleDepTreeJarPath := filepath.Join(tmpDir, gradleDepTreeJarFile)
+ if err = errorutils.CheckError(os.WriteFile(gradleDepTreeJarPath, gradleDepTreeJar, 0600)); err != nil {
+ return
+ }
+ gradleDepTreeJarPath = ioutils.DoubleWinPathSeparator(gradleDepTreeJarPath)
+
+ depTreeInitScript := fmt.Sprintf(gradleDepTreeInitScript, releasesRepo, gradleDepTreeJarPath, gdt.depsRepo)
+ return tmpDir, errorutils.CheckError(os.WriteFile(filepath.Join(tmpDir, gradleDepTreeInitFile), []byte(depTreeInitScript), 0666))
+}
+
+// getRemoteRepos constructs the sections of Artifactory's remote repositories in the gradle-dep-tree init script.
+// depsRemoteRepo - name of the remote repository that proxies the relevant registry, e.g. maven central.
+// server - the Artifactory server details on which the repositories reside in.
+// Returns the constructed sections.
+func getRemoteRepos(depsRepo string, server *config.ServerDetails) (string, string, error) {
+ constructedReleasesRepo, err := constructReleasesRemoteRepo()
+ if err != nil {
+ return "", "", err
+ }
+
+ constructedDepsRepo, err := getDepTreeArtifactoryRepository(depsRepo, server)
+ if err != nil {
+ return "", "", err
+ }
+ return constructedReleasesRepo, constructedDepsRepo, nil
+}
+
+func constructReleasesRemoteRepo() (string, error) {
+ // Try to retrieve the serverID and remote repository that proxies https://releases.jfrog.io, from the environment variable
+ serverId, repoName, err := coreutils.GetServerIdAndRepo(coreutils.ReleasesRemoteEnv)
+ if err != nil || serverId == "" || repoName == "" {
+ return "", err
+ }
+
+ releasesServer, err := config.GetSpecificConfig(serverId, false, true)
+ if err != nil {
+ return "", err
+ }
+
+ releasesPath := fmt.Sprintf("%s/%s", repoName, remoteDepTreePath)
+ log.Debug("The `"+gradleDepTreeJarFile+"` will be resolved from", repoName)
+ return getDepTreeArtifactoryRepository(releasesPath, releasesServer)
+}
+
+func (gdt *gradleDepTreeManager) execGradleDepTree(depTreeDir string) (outputFileContent []byte, err error) {
+ gradleExecPath, err := build.GetGradleExecPath(gdt.useWrapper)
+ if err != nil {
+ err = errorutils.CheckError(err)
+ return
+ }
+
+ outputFilePath := filepath.Join(depTreeDir, gradleDepTreeOutputFile)
+ tasks := []string{
+ "clean",
+ "generateDepTrees", "-I", filepath.Join(depTreeDir, gradleDepTreeInitFile),
+ "-q",
+ fmt.Sprintf("-Dcom.jfrog.depsTreeOutputFile=%s", outputFilePath),
+ "-Dcom.jfrog.includeAllBuildFiles=true"}
+ log.Info("Running gradle deps tree command:", gradleExecPath, strings.Join(tasks, " "))
+ if output, err := exec.Command(gradleExecPath, tasks...).CombinedOutput(); err != nil {
+ return nil, errorutils.CheckErrorf("error running gradle-dep-tree: %s\n%s", err.Error(), string(output))
+ }
+ defer func() {
+ err = errors.Join(err, errorutils.CheckError(os.Remove(outputFilePath)))
+ }()
+
+ outputFileContent, err = os.ReadFile(outputFilePath)
+ err = errorutils.CheckError(err)
+ return
+}
+
+func getDepTreeArtifactoryRepository(remoteRepo string, server *config.ServerDetails) (string, error) {
+ if remoteRepo == "" || server.IsEmpty() {
+ return "", nil
+ }
+ username, password, err := getArtifactoryAuthFromServer(server)
+ if err != nil {
+ return "", err
+ }
+
+ log.Debug("The project dependencies will be resolved from", server.ArtifactoryUrl, "from the", remoteRepo, "repository")
+ return fmt.Sprintf(artifactoryRepository,
+ strings.TrimSuffix(server.ArtifactoryUrl, "/"),
+ remoteRepo,
+ username,
+ password), nil
+}
+
+// This function assumes that the Gradle wrapper is in the root directory.
+// The --project-dir option of Gradle won't work in this case.
+func isGradleWrapperExist() (bool, error) {
+ wrapperName := gradlew
+ if coreutils.IsWindows() {
+ wrapperName += ".bat"
+ }
+ return fileutils.IsFileExists(wrapperName, false)
+}
diff --git a/commands/audit/sca/java/gradle_test.go b/commands/audit/sca/java/gradle_test.go
new file mode 100644
index 000000000..77920f937
--- /dev/null
+++ b/commands/audit/sca/java/gradle_test.go
@@ -0,0 +1,230 @@
+package java
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/jfrog/jfrog-cli-core/v2/utils/config"
+ testsutils "github.com/jfrog/jfrog-cli-core/v2/utils/config/tests"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// #nosec G101 -- Dummy token for tests
+// jfrog-ignore
+const dummyToken = "eyJ2ZXIiOiIyIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYiLCJraWQiOiJIcnU2VHctZk1yOTV3dy12TDNjV3ZBVjJ3Qm9FSHpHdGlwUEFwOE1JdDljIn0.eyJzdWIiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3XC91c2Vyc1wvYWRtaW4iLCJzY3AiOiJtZW1iZXItb2YtZ3JvdXBzOnJlYWRlcnMgYXBpOioiLCJhdWQiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3IiwiaXNzIjoiamZydEAwMWMzZ2ZmaGcyZTh3NjE0OWUzYTJxMHc5NyIsImV4cCI6MTU1NjAzNzc2NSwiaWF0IjoxNTU2MDM0MTY1LCJqdGkiOiI1M2FlMzgyMy05NGM3LTQ0OGItOGExOC1iZGVhNDBiZjFlMjAifQ.Bp3sdvppvRxysMlLgqT48nRIHXISj9sJUCXrm7pp8evJGZW1S9hFuK1olPmcSybk2HNzdzoMcwhUmdUzAssiQkQvqd_HanRcfFbrHeg5l1fUQ397ECES-r5xK18SYtG1VR7LNTVzhJqkmRd3jzqfmIK2hKWpEgPfm8DRz3j4GGtDRxhb3oaVsT2tSSi_VfT3Ry74tzmO0GcCvmBE2oh58kUZ4QfEsalgZ8IpYHTxovsgDx_M7ujOSZx_hzpz-iy268-OkrU22PQPCfBmlbEKeEUStUO9n0pj4l1ODL31AGARyJRy46w4yzhw7Fk5P336WmDMXYs5LAX2XxPFNLvNzA"
+
+const expectedInitScriptWithRepos = `initscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath files('%s')
+ }
+}
+
+allprojects {
+ repositories {
+ maven {
+ url "https://myartifactory.com/artifactory/deps-repo"
+ credentials {
+ username = 'admin'
+ password = '%s'
+ }
+ }
+ }
+ apply plugin: com.jfrog.GradleDepTree
+}`
+
+func TestGradleTreesWithoutConfig(t *testing.T) {
+ // Create and change directory to test workspace
+ tempDirPath, cleanUp := tests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "gradle", "gradle"))
+ defer cleanUp()
+ assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700))
+
+ // Run getModulesDependencyTrees
+ modulesDependencyTrees, uniqueDeps, err := buildGradleDependencyTree(&DepTreeParams{})
+ if assert.NoError(t, err) && assert.NotNil(t, modulesDependencyTrees) {
+ assert.Len(t, uniqueDeps, 12)
+ assert.Len(t, modulesDependencyTrees, 5)
+ // Check module
+ module := tests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.example.gradle:webservice:1.0")
+ assert.Len(t, module.Nodes, 7)
+
+ // Check direct dependency
+ directDependency := tests.GetAndAssertNode(t, module.Nodes, "junit:junit:4.11")
+ assert.Len(t, directDependency.Nodes, 1)
+
+ // Check transitive dependency
+ tests.GetAndAssertNode(t, directDependency.Nodes, "org.hamcrest:hamcrest-core:1.3")
+ }
+}
+
+func TestGradleTreesWithConfig(t *testing.T) {
+ // Create and change directory to test workspace
+ tempDirPath, cleanUp := tests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "gradle", "gradle-example-config"))
+ defer cleanUp()
+ assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700))
+
+ // Run getModulesDependencyTrees
+ modulesDependencyTrees, uniqueDeps, err := buildGradleDependencyTree(&DepTreeParams{UseWrapper: true})
+ if assert.NoError(t, err) && assert.NotNil(t, modulesDependencyTrees) {
+ assert.Len(t, modulesDependencyTrees, 5)
+ assert.Len(t, uniqueDeps, 11)
+ // Check module
+ module := tests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test.gradle.publish:api:1.0-SNAPSHOT")
+ assert.Len(t, module.Nodes, 4)
+
+ // Check direct dependency
+ directDependency := tests.GetAndAssertNode(t, module.Nodes, "commons-lang:commons-lang:2.4")
+ assert.Len(t, directDependency.Nodes, 1)
+
+ // Check transitive dependency
+ tests.GetAndAssertNode(t, directDependency.Nodes, "commons-io:commons-io:1.2")
+ }
+}
+
+func TestIsGradleWrapperExist(t *testing.T) {
+ // Check Gradle wrapper doesn't exist
+ isWrapperExist, err := isGradleWrapperExist()
+ assert.False(t, isWrapperExist)
+ assert.NoError(t, err)
+
+ // Check Gradle wrapper exist
+ _, cleanUp := tests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "gradle", "gradle"))
+ defer cleanUp()
+ isWrapperExist, err = isGradleWrapperExist()
+ assert.NoError(t, err)
+ assert.True(t, isWrapperExist)
+}
+
+func TestGetDepTreeArtifactoryRepository(t *testing.T) {
+ tests := []struct {
+ name string
+ remoteRepo string
+ server *config.ServerDetails
+ expectedUrl string
+ expectedErr string
+ }{
+ {
+ name: "WithAccessToken",
+ remoteRepo: "my-remote-repo",
+ server: &config.ServerDetails{
+ Url: "https://myartifactory.com",
+ // jfrog-ignore
+ AccessToken: dummyToken,
+ },
+ // jfrog-ignore
+ expectedUrl: "\n\t\tmaven {\n\t\t\turl \"/my-remote-repo\"\n\t\t\tcredentials {\n\t\t\t\tusername = 'admin'\n\t\t\t\tpassword = 'eyJ2ZXIiOiIyIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYiLCJraWQiOiJIcnU2VHctZk1yOTV3dy12TDNjV3ZBVjJ3Qm9FSHpHdGlwUEFwOE1JdDljIn0.eyJzdWIiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3XC91c2Vyc1wvYWRtaW4iLCJzY3AiOiJtZW1iZXItb2YtZ3JvdXBzOnJlYWRlcnMgYXBpOioiLCJhdWQiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3IiwiaXNzIjoiamZydEAwMWMzZ2ZmaGcyZTh3NjE0OWUzYTJxMHc5NyIsImV4cCI6MTU1NjAzNzc2NSwiaWF0IjoxNTU2MDM0MTY1LCJqdGkiOiI1M2FlMzgyMy05NGM3LTQ0OGItOGExOC1iZGVhNDBiZjFlMjAifQ.Bp3sdvppvRxysMlLgqT48nRIHXISj9sJUCXrm7pp8evJGZW1S9hFuK1olPmcSybk2HNzdzoMcwhUmdUzAssiQkQvqd_HanRcfFbrHeg5l1fUQ397ECES-r5xK18SYtG1VR7LNTVzhJqkmRd3jzqfmIK2hKWpEgPfm8DRz3j4GGtDRxhb3oaVsT2tSSi_VfT3Ry74tzmO0GcCvmBE2oh58kUZ4QfEsalgZ8IpYHTxovsgDx_M7ujOSZx_hzpz-iy268-OkrU22PQPCfBmlbEKeEUStUO9n0pj4l1ODL31AGARyJRy46w4yzhw7Fk5P336WmDMXYs5LAX2XxPFNLvNzA'\n\t\t\t}\n\t\t}",
+ expectedErr: "",
+ },
+ {
+ name: "WithUsernameAndPassword",
+ remoteRepo: "my-remote-repo",
+ server: &config.ServerDetails{
+ Url: "https://myartifactory.com",
+ User: "my-username",
+ Password: "my-password",
+ },
+ expectedUrl: "\n\t\tmaven {\n\t\t\turl \"/my-remote-repo\"\n\t\t\tcredentials {\n\t\t\t\tusername = 'my-username'\n\t\t\t\tpassword = 'my-password'\n\t\t\t}\n\t\t}",
+ expectedErr: "",
+ },
+ {
+ name: "MissingCredentials",
+ remoteRepo: "my-remote-repo",
+ server: &config.ServerDetails{
+ Url: "https://myartifactory.com",
+ },
+ expectedUrl: "",
+ expectedErr: "either username/password or access token must be set for https://myartifactory.com",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ url, err := getDepTreeArtifactoryRepository(test.remoteRepo, test.server)
+ if err != nil {
+ assert.Equal(t, test.expectedErr, err.Error())
+ } else {
+ assert.Equal(t, test.expectedUrl, url)
+ }
+ })
+ }
+}
+
+func TestCreateDepTreeScript(t *testing.T) {
+ manager := &gradleDepTreeManager{DepTreeManager: DepTreeManager{}}
+ tmpDir, err := manager.createDepTreeScriptAndGetDir()
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, os.Remove(filepath.Join(tmpDir, gradleDepTreeInitFile)))
+ }()
+ content, err := os.ReadFile(filepath.Join(tmpDir, gradleDepTreeInitFile))
+ assert.NoError(t, err)
+ gradleDepTreeJarPath := ioutils.DoubleWinPathSeparator(filepath.Join(tmpDir, gradleDepTreeJarFile))
+ assert.Equal(t, fmt.Sprintf(gradleDepTreeInitScript, "", gradleDepTreeJarPath, ""), string(content))
+}
+
+func TestCreateDepTreeScriptWithRepositories(t *testing.T) {
+ manager := &gradleDepTreeManager{DepTreeManager: DepTreeManager{}}
+ manager.depsRepo = "deps-repo"
+ manager.server = &config.ServerDetails{
+ Url: "https://myartifactory.com/",
+ ArtifactoryUrl: "https://myartifactory.com/artifactory",
+ // jfrog-ignore
+ AccessToken: dummyToken,
+ }
+ tmpDir, err := manager.createDepTreeScriptAndGetDir()
+ assert.NoError(t, err)
+ defer func() {
+ assert.NoError(t, os.Remove(filepath.Join(tmpDir, gradleDepTreeInitFile)))
+ }()
+
+ content, err := os.ReadFile(filepath.Join(tmpDir, gradleDepTreeInitFile))
+ assert.NoError(t, err)
+ gradleDepTreeJarPath := ioutils.DoubleWinPathSeparator(filepath.Join(tmpDir, gradleDepTreeJarFile))
+ // jfrog-ignore
+ assert.Equal(t, fmt.Sprintf(expectedInitScriptWithRepos, gradleDepTreeJarPath, dummyToken), string(content))
+}
+
+func TestConstructReleasesRemoteRepo(t *testing.T) {
+ cleanUp := testsutils.CreateTempEnv(t, false)
+ serverDetails := &config.ServerDetails{
+ ServerId: "test",
+ ArtifactoryUrl: "https://domain.com/artifactory",
+ User: "user",
+ Password: "pass",
+ }
+ err := config.SaveServersConf([]*config.ServerDetails{serverDetails})
+ assert.NoError(t, err)
+ defer cleanUp()
+ testCases := []struct {
+ envVar string
+ expectedRepo string
+ expectedErr error
+ }{
+ {envVar: "", expectedRepo: "", expectedErr: nil},
+ {envVar: "test/repo1", expectedRepo: "\n\t\tmaven {\n\t\t\turl \"https://domain.com/artifactory/repo1/artifactory/oss-release-local\"\n\t\t\tcredentials {\n\t\t\t\tusername = 'user'\n\t\t\t\tpassword = 'pass'\n\t\t\t}\n\t\t}", expectedErr: nil},
+ {envVar: "notexist/repo1", expectedRepo: "", expectedErr: errors.New("Server ID 'notexist' does not exist.")},
+ }
+
+ for _, tc := range testCases {
+ // Set the environment variable for this test case
+ func() {
+ assert.NoError(t, os.Setenv(coreutils.ReleasesRemoteEnv, tc.envVar))
+ defer func() {
+ // Reset the environment variable after each test case
+ assert.NoError(t, os.Unsetenv(coreutils.ReleasesRemoteEnv))
+ }()
+ actualRepo, actualErr := constructReleasesRemoteRepo()
+ assert.Equal(t, tc.expectedRepo, actualRepo)
+ assert.Equal(t, tc.expectedErr, actualErr)
+ }()
+ }
+}
diff --git a/commands/audit/sca/java/mvn.go b/commands/audit/sca/java/mvn.go
new file mode 100644
index 000000000..76514fb11
--- /dev/null
+++ b/commands/audit/sca/java/mvn.go
@@ -0,0 +1,265 @@
+package java
+
+import (
+ _ "embed"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils"
+ "github.com/jfrog/jfrog-client-go/utils/errorutils"
+ "github.com/jfrog/jfrog-client-go/utils/io/fileutils"
+ "github.com/jfrog/jfrog-client-go/utils/log"
+ xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
+)
+
+const (
+ mavenDepTreeJarFile = "maven-dep-tree.jar"
+ mavenDepTreeOutputFile = "mavendeptree.out"
+ // Changing this version also requires a change in MAVEN_DEP_TREE_VERSION within buildscripts/download_jars.sh
+ mavenDepTreeVersion = "1.1.0"
+ settingsXmlFile = "settings.xml"
+)
+
+var mavenConfigPath = filepath.Join(".mvn", "maven.config")
+
+type MavenDepTreeCmd string
+
+const (
+ Projects MavenDepTreeCmd = "projects"
+ Tree MavenDepTreeCmd = "tree"
+)
+
+//go:embed resources/settings.xml
+var settingsXmlTemplate string
+
+//go:embed resources/maven-dep-tree.jar
+var mavenDepTreeJar []byte
+
+type MavenDepTreeManager struct {
+ DepTreeManager
+ isInstalled bool
+ // this flag its curation command, it will set dedicated cache and download url.
+ isCurationCmd bool
+ // path to the curation dedicated cache
+ curationCacheFolder string
+ cmdName MavenDepTreeCmd
+ settingsXmlPath string
+}
+
+func NewMavenDepTreeManager(params *DepTreeParams, cmdName MavenDepTreeCmd) *MavenDepTreeManager {
+ depTreeManager := NewDepTreeManager(&DepTreeParams{
+ Server: params.Server,
+ DepsRepo: params.DepsRepo,
+ })
+ return &MavenDepTreeManager{
+ DepTreeManager: depTreeManager,
+ isInstalled: params.IsMavenDepTreeInstalled,
+ cmdName: cmdName,
+ isCurationCmd: params.IsCurationCmd,
+ curationCacheFolder: params.CurationCacheFolder,
+ }
+}
+
+func buildMavenDependencyTree(params *DepTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps map[string][]string, err error) {
+ manager := NewMavenDepTreeManager(params, Tree)
+ outputFilePaths, clearMavenDepTreeRun, err := manager.RunMavenDepTree()
+ if err != nil {
+ if clearMavenDepTreeRun != nil {
+ err = errors.Join(err, clearMavenDepTreeRun())
+ }
+ return
+ }
+ defer func() {
+ err = errors.Join(err, clearMavenDepTreeRun())
+ }()
+ dependencyTree, uniqueDeps, err = getGraphFromDepTree(outputFilePaths)
+ return
+}
+
+// Runs maven-dep-tree according to cmdName. Returns the plugin output along with a function pointer to revert the plugin side effects.
+// If a non-nil clearMavenDepTreeRun pointer is returnes it means we had no error during the entire function execution
+func (mdt *MavenDepTreeManager) RunMavenDepTree() (depTreeOutput string, clearMavenDepTreeRun func() error, err error) {
+ // depTreeExecDir is a temp directory for all the files that are required for the maven-dep-tree run
+ depTreeExecDir, clearMavenDepTreeRun, err := mdt.CreateTempDirWithSettingsXmlIfNeeded()
+ if err != nil {
+ return
+ }
+ if err = mdt.installMavenDepTreePlugin(depTreeExecDir); err != nil {
+ return
+ }
+
+ depTreeOutput, err = mdt.execMavenDepTree(depTreeExecDir)
+ if err != nil {
+ return
+ }
+ return
+}
+
+func (mdt *MavenDepTreeManager) installMavenDepTreePlugin(depTreeExecDir string) error {
+ if mdt.isInstalled {
+ return nil
+ }
+ mavenDepTreeJarPath := filepath.Join(depTreeExecDir, mavenDepTreeJarFile)
+ if err := errorutils.CheckError(os.WriteFile(mavenDepTreeJarPath, mavenDepTreeJar, 0666)); err != nil {
+ return err
+ }
+ goals := GetMavenPluginInstallationGoals(mavenDepTreeJarPath)
+ _, err := mdt.RunMvnCmd(goals)
+ return err
+}
+
+func GetMavenPluginInstallationGoals(pluginPath string) []string {
+ return []string{"org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file", "-Dfile=" + pluginPath, "-B"}
+}
+
+func (mdt *MavenDepTreeManager) execMavenDepTree(depTreeExecDir string) (string, error) {
+ if mdt.cmdName == Tree {
+ return mdt.runTreeCmd(depTreeExecDir)
+ }
+ return mdt.runProjectsCmd()
+}
+
+func (mdt *MavenDepTreeManager) runTreeCmd(depTreeExecDir string) (string, error) {
+ mavenDepTreePath := filepath.Join(depTreeExecDir, mavenDepTreeOutputFile)
+ goals := []string{"com.jfrog:maven-dep-tree:" + mavenDepTreeVersion + ":" + string(Tree), "-DdepsTreeOutputFile=" + mavenDepTreePath, "-B"}
+ if mdt.isCurationCmd {
+ goals = append(goals, "-Dmaven.repo.local="+mdt.curationCacheFolder)
+ }
+ if _, err := mdt.RunMvnCmd(goals); err != nil {
+ return "", err
+ }
+
+ mavenDepTreeOutput, err := os.ReadFile(mavenDepTreePath)
+ if err != nil {
+ return "", errorutils.CheckError(err)
+ }
+ return string(mavenDepTreeOutput), nil
+}
+
+func (mdt *MavenDepTreeManager) runProjectsCmd() (string, error) {
+ goals := []string{"com.jfrog:maven-dep-tree:" + mavenDepTreeVersion + ":" + string(Projects), "-q"}
+ output, err := mdt.RunMvnCmd(goals)
+ if err != nil {
+ return "", err
+ }
+ return string(output), nil
+}
+
+func (mdt *MavenDepTreeManager) RunMvnCmd(goals []string) (cmdOutput []byte, err error) {
+ restoreMavenConfig, err := removeMavenConfig()
+ if err != nil {
+ return
+ }
+
+ defer func() {
+ if restoreMavenConfig != nil {
+ err = errors.Join(err, restoreMavenConfig())
+ }
+ }()
+
+ if mdt.settingsXmlPath != "" {
+ goals = append(goals, "-s", mdt.settingsXmlPath)
+ }
+
+ //#nosec G204
+ cmdOutput, err = exec.Command("mvn", goals...).CombinedOutput()
+ if err != nil {
+ stringOutput := string(cmdOutput)
+ if len(cmdOutput) > 0 {
+ log.Info(stringOutput)
+ }
+ if msg := mdt.suspectCurationBlockedError(stringOutput); msg != "" {
+ err = fmt.Errorf("failed running command 'mvn %s\n\n%s", strings.Join(goals, " "), msg)
+ } else {
+ err = fmt.Errorf("failed running command 'mvn %s': %s", strings.Join(goals, " "), err.Error())
+ }
+ }
+ return
+}
+
+func (mdt *MavenDepTreeManager) GetSettingsXmlPath() string {
+ return mdt.settingsXmlPath
+}
+
+func (mdt *MavenDepTreeManager) SetSettingsXmlPath(settingsXmlPath string) {
+ mdt.settingsXmlPath = settingsXmlPath
+}
+
+func removeMavenConfig() (func() error, error) {
+ mavenConfigExists, err := fileutils.IsFileExists(mavenConfigPath, false)
+ if err != nil {
+ return nil, err
+ }
+ if !mavenConfigExists {
+ return nil, nil
+ }
+ restoreMavenConfig, err := ioutils.BackupFile(mavenConfigPath, "maven.config.bkp")
+ if err != nil {
+ return nil, err
+ }
+ err = os.Remove(mavenConfigPath)
+ if err != nil {
+ err = errorutils.CheckErrorf("failed to remove %s while building the maven dependencies tree. Error received:\n%s", mavenConfigPath, err.Error())
+ }
+ return restoreMavenConfig, err
+}
+
+// Creates a new settings.xml file configured with the provided server and repository from the current MavenDepTreeManager instance.
+// The settings.xml will be written to the given path.
+func (mdt *MavenDepTreeManager) createSettingsXmlWithConfiguredArtifactory(settingsXmlPath string) error {
+ username, password, err := getArtifactoryAuthFromServer(mdt.server)
+ if err != nil {
+ return err
+ }
+ endPoint := mdt.depsRepo
+ if mdt.isCurationCmd {
+ endPoint = path.Join("api/curation/audit", endPoint)
+ }
+ remoteRepositoryFullPath, err := url.JoinPath(mdt.server.ArtifactoryUrl, endPoint)
+ if err != nil {
+ return err
+ }
+ mdt.settingsXmlPath = filepath.Join(settingsXmlPath, settingsXmlFile)
+ settingsXmlContent := fmt.Sprintf(settingsXmlTemplate, username, password, remoteRepositoryFullPath)
+
+ return errorutils.CheckError(os.WriteFile(mdt.settingsXmlPath, []byte(settingsXmlContent), 0600))
+}
+
+// Creates a temporary directory.
+// If Artifactory resolution repo is provided, a settings.xml file with the provided server and repository will be created inside the temprary directory.
+func (mdt *MavenDepTreeManager) CreateTempDirWithSettingsXmlIfNeeded() (tempDirPath string, clearMavenDepTreeRun func() error, err error) {
+ tempDirPath, err = fileutils.CreateTempDir()
+ if err != nil {
+ return
+ }
+
+ clearMavenDepTreeRun = func() error { return fileutils.RemoveTempDir(tempDirPath) }
+
+ // Create a settings.xml file that sets the dependency resolution from the given server and repository
+ if mdt.depsRepo != "" {
+ err = mdt.createSettingsXmlWithConfiguredArtifactory(tempDirPath)
+ }
+ if err != nil {
+ err = errors.Join(err, clearMavenDepTreeRun())
+ clearMavenDepTreeRun = nil
+ }
+ return
+}
+
+// In case mvn tree fails on 403 or 500 it can be related to packages blocked by curation.
+// For this use case to succeed, pass through should be enabled in the curated repos
+func (mdt *MavenDepTreeManager) suspectCurationBlockedError(cmdOutput string) (msgToUser string) {
+ if !mdt.isCurationCmd {
+ return
+ }
+ if strings.Contains(cmdOutput, "status code: 403") || strings.Contains(cmdOutput, "status code: 500") {
+ msgToUser = "Failed to get dependencies tree for maven project, Please verify pass-through enabled on the curated repos"
+ }
+ return msgToUser
+}
diff --git a/commands/audit/sca/java/mvn_test.go b/commands/audit/sca/java/mvn_test.go
new file mode 100644
index 000000000..f211879e4
--- /dev/null
+++ b/commands/audit/sca/java/mvn_test.go
@@ -0,0 +1,380 @@
+package java
+
+import (
+ "github.com/jfrog/build-info-go/utils"
+ "github.com/jfrog/jfrog-cli-core/v2/utils/config"
+ coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests"
+ "github.com/jfrog/jfrog-client-go/utils/io/fileutils"
+ "github.com/jfrog/jfrog-client-go/utils/tests"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/exp/maps"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+const (
+ //#nosec G101 - dummy token for testing
+ settingsXmlWithUsernameAndPassword = `
+
+
+
+ artifactory
+ testUser
+ testPass
+
+
+
+
+ artifactory
+ https://myartifactory.com/artifactory/testRepo
+ *
+
+
+`
+ //#nosec G101 - dummy token for testing
+ settingsXmlWithUsernameAndPasswordAndCurationDedicatedAPi = `
+
+
+
+ artifactory
+ testUser
+ testPass
+
+
+
+
+ artifactory
+ https://myartifactory.com/artifactory/api/curation/audit/testRepo
+ *
+
+
+`
+ //#nosec G101 - dummy token for testing
+ settingsXmlWithUsernameAndToken = `
+
+
+
+ artifactory
+ testUser
+ eyJ2ZXIiOiIyIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYiLCJraWQiOiJIcnU2VHctZk1yOTV3dy12TDNjV3ZBVjJ3Qm9FSHpHdGlwUEFwOE1JdDljIn0.eyJzdWIiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3XC91c2Vyc1wvYWRtaW4iLCJzY3AiOiJtZW1iZXItb2YtZ3JvdXBzOnJlYWRlcnMgYXBpOioiLCJhdWQiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3IiwiaXNzIjoiamZydEAwMWMzZ2ZmaGcyZTh3NjE0OWUzYTJxMHc5NyIsImV4cCI6MTU1NjAzNzc2NSwiaWF0IjoxNTU2MDM0MTY1LCJqdGkiOiI1M2FlMzgyMy05NGM3LTQ0OGItOGExOC1iZGVhNDBiZjFlMjAifQ.Bp3sdvppvRxysMlLgqT48nRIHXISj9sJUCXrm7pp8evJGZW1S9hFuK1olPmcSybk2HNzdzoMcwhUmdUzAssiQkQvqd_HanRcfFbrHeg5l1fUQ397ECES-r5xK18SYtG1VR7LNTVzhJqkmRd3jzqfmIK2hKWpEgPfm8DRz3j4GGtDRxhb3oaVsT2tSSi_VfT3Ry74tzmO0GcCvmBE2oh58kUZ4QfEsalgZ8IpYHTxovsgDx_M7ujOSZx_hzpz-iy268-OkrU22PQPCfBmlbEKeEUStUO9n0pj4l1ODL31AGARyJRy46w4yzhw7Fk5P336WmDMXYs5LAX2XxPFNLvNzA
+
+
+
+
+ artifactory
+ https://myartifactory.com/artifactory/testRepo
+ *
+
+
+`
+ //#nosec G101 - dummy token for testing
+ settingsXmlWithAccessToken = `
+
+
+
+ artifactory
+ admin
+ eyJ2ZXIiOiIyIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYiLCJraWQiOiJIcnU2VHctZk1yOTV3dy12TDNjV3ZBVjJ3Qm9FSHpHdGlwUEFwOE1JdDljIn0.eyJzdWIiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3XC91c2Vyc1wvYWRtaW4iLCJzY3AiOiJtZW1iZXItb2YtZ3JvdXBzOnJlYWRlcnMgYXBpOioiLCJhdWQiOiJqZnJ0QDAxYzNnZmZoZzJlOHc2MTQ5ZTNhMnEwdzk3IiwiaXNzIjoiamZydEAwMWMzZ2ZmaGcyZTh3NjE0OWUzYTJxMHc5NyIsImV4cCI6MTU1NjAzNzc2NSwiaWF0IjoxNTU2MDM0MTY1LCJqdGkiOiI1M2FlMzgyMy05NGM3LTQ0OGItOGExOC1iZGVhNDBiZjFlMjAifQ.Bp3sdvppvRxysMlLgqT48nRIHXISj9sJUCXrm7pp8evJGZW1S9hFuK1olPmcSybk2HNzdzoMcwhUmdUzAssiQkQvqd_HanRcfFbrHeg5l1fUQ397ECES-r5xK18SYtG1VR7LNTVzhJqkmRd3jzqfmIK2hKWpEgPfm8DRz3j4GGtDRxhb3oaVsT2tSSi_VfT3Ry74tzmO0GcCvmBE2oh58kUZ4QfEsalgZ8IpYHTxovsgDx_M7ujOSZx_hzpz-iy268-OkrU22PQPCfBmlbEKeEUStUO9n0pj4l1ODL31AGARyJRy46w4yzhw7Fk5P336WmDMXYs5LAX2XxPFNLvNzA
+
+
+
+
+ artifactory
+ https://myartifactory.com/artifactory/testRepo
+ *
+
+
+`
+)
+
+func TestMavenTreesMultiModule(t *testing.T) {
+ // Create and change directory to test workspace
+ _, cleanUp := coreTests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "maven", "maven-example"))
+ defer cleanUp()
+
+ expectedUniqueDeps := []string{
+ GavPackageTypeIdentifier + "javax.mail:mail:1.4",
+ GavPackageTypeIdentifier + "org.testng:testng:5.9",
+ GavPackageTypeIdentifier + "javax.servlet:servlet-api:2.5",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi3:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi2:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "junit:junit:3.8.1",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi1:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "commons-io:commons-io:1.4",
+ GavPackageTypeIdentifier + "org.apache.commons:commons-email:1.1",
+ GavPackageTypeIdentifier + "javax.activation:activation:1.1",
+ GavPackageTypeIdentifier + "hsqldb:hsqldb:1.8.0.10",
+ }
+ // Run getModulesDependencyTrees
+ modulesDependencyTrees, uniqueDeps, err := buildMavenDependencyTree(&DepTreeParams{})
+ if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) {
+ assert.ElementsMatch(t, maps.Keys(uniqueDeps), expectedUniqueDeps, "First is actual, Second is Expected")
+ // Check root module
+ multi := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT")
+ if assert.NotNil(t, multi) {
+ assert.Len(t, multi.Nodes, 1)
+ // Check multi1 with a transitive dependency
+ multi1 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT")
+ assert.Len(t, multi1.Nodes, 4)
+ commonsEmail := coreTests.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1")
+ assert.Len(t, commonsEmail.Nodes, 2)
+
+ // Check multi2 and multi3
+ multi2 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT")
+ assert.Len(t, multi2.Nodes, 1)
+ multi3 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT")
+ assert.Len(t, multi3.Nodes, 4)
+ }
+ }
+}
+
+func TestMavenWrapperTrees(t *testing.T) {
+ // Create and change directory to test workspace
+ _, cleanUp := coreTests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "maven", "maven-example-with-wrapper"))
+ err := os.Chmod("mvnw", 0700)
+ defer cleanUp()
+ assert.NoError(t, err)
+ expectedUniqueDeps := []string{
+ GavPackageTypeIdentifier + "org.jfrog.test:multi1:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.codehaus.plexus:plexus-utils:1.5.1",
+ GavPackageTypeIdentifier + "org.springframework:spring-beans:2.5.6",
+ GavPackageTypeIdentifier + "commons-logging:commons-logging:1.1.1",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi3:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.apache.commons:commons-email:1.1",
+ GavPackageTypeIdentifier + "org.springframework:spring-aop:2.5.6",
+ GavPackageTypeIdentifier + "org.springframework:spring-core:2.5.6",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.jfrog.test:multi2:3.7-SNAPSHOT",
+ GavPackageTypeIdentifier + "org.testng:testng:5.9",
+ GavPackageTypeIdentifier + "hsqldb:hsqldb:1.8.0.10",
+ GavPackageTypeIdentifier + "junit:junit:3.8.1",
+ GavPackageTypeIdentifier + "javax.activation:activation:1.1",
+ GavPackageTypeIdentifier + "javax.mail:mail:1.4",
+ GavPackageTypeIdentifier + "aopalliance:aopalliance:1.0",
+ GavPackageTypeIdentifier + "commons-io:commons-io:1.4",
+ GavPackageTypeIdentifier + "javax.servlet.jsp:jsp-api:2.1",
+ GavPackageTypeIdentifier + "javax.servlet:servlet-api:2.5",
+ }
+
+ modulesDependencyTrees, uniqueDeps, err := buildMavenDependencyTree(&DepTreeParams{})
+ if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) {
+ assert.ElementsMatch(t, maps.Keys(uniqueDeps), expectedUniqueDeps, "First is actual, Second is Expected")
+ // Check root module
+ multi := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT")
+ if assert.NotNil(t, multi) {
+ assert.Len(t, multi.Nodes, 1)
+ // Check multi1 with a transitive dependency
+ multi1 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT")
+ assert.Len(t, multi1.Nodes, 7)
+ commonsEmail := coreTests.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1")
+ assert.Len(t, commonsEmail.Nodes, 2)
+ // Check multi2 and multi3
+ multi2 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT")
+ assert.Len(t, multi2.Nodes, 1)
+ multi3 := coreTests.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT")
+ assert.Len(t, multi3.Nodes, 4)
+ }
+ }
+}
+
+func TestMavenWrapperTreesTypes(t *testing.T) {
+ // Create and change directory to test workspace
+ _, cleanUp := coreTests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "maven", "maven-example-with-many-types"))
+ defer cleanUp()
+ tree, uniqueDeps, err := buildMavenDependencyTree(&DepTreeParams{})
+ require.NoError(t, err)
+ // dependency of pom type
+ depWithPomType := uniqueDeps["gav://org.webjars:lodash:4.17.21"]
+ assert.NotEmpty(t, depWithPomType)
+ assert.Equal(t, depWithPomType[0], "pom")
+ existInTreePom := false
+ for _, node := range tree[0].Nodes {
+ if node.Id == "gav://org.webjars:lodash:4.17.21" {
+ nodeTypes := *node.Types
+ assert.Equal(t, nodeTypes[0], "pom")
+ existInTreePom = true
+ }
+ }
+ assert.True(t, existInTreePom)
+
+ // dependency of jar type
+ depWithJarType := uniqueDeps["gav://junit:junit:4.11"]
+ assert.NotEmpty(t, depWithJarType)
+ assert.Equal(t, depWithJarType[0], "jar")
+ existInTreeJar := false
+ for _, node := range tree[0].Nodes {
+ if node.Id == "gav://junit:junit:4.11" {
+ nodeTypes := *node.Types
+ assert.Equal(t, nodeTypes[0], "jar")
+ existInTreeJar = true
+ }
+ }
+ assert.True(t, existInTreeJar)
+}
+
+func TestDepTreeWithDedicatedCache(t *testing.T) {
+ // Create and change directory to test workspace
+ _, cleanUp := coreTests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "maven", "maven-example-with-wrapper"))
+ err := os.Chmod("mvnw", 0700)
+ defer cleanUp()
+ assert.NoError(t, err)
+ tempDir := t.TempDir()
+ defer assert.NoError(t, utils.RemoveTempDir(tempDir))
+ manager := NewMavenDepTreeManager(&DepTreeParams{IsCurationCmd: true, CurationCacheFolder: tempDir}, Tree)
+ _, err = manager.runTreeCmd(tempDir)
+ require.NoError(t, err)
+ // validate one of the jars exist in the dedicated cache for curation
+ fileExist, err := utils.IsFileExists(filepath.Join(tempDir, "org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar"), false)
+ require.NoError(t, err)
+ assert.True(t, fileExist)
+}
+
+func TestGetMavenPluginInstallationArgs(t *testing.T) {
+ args := GetMavenPluginInstallationGoals("testPlugin")
+ assert.Equal(t, "org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file", args[0])
+ assert.Equal(t, "-Dfile=testPlugin", args[1])
+}
+
+func TestCreateSettingsXmlWithConfiguredArtifactory(t *testing.T) {
+ // Test case for successful creation of settings.xml.
+ mdt := MavenDepTreeManager{
+ DepTreeManager: DepTreeManager{
+ server: &config.ServerDetails{
+ ArtifactoryUrl: "https://myartifactory.com/artifactory",
+ User: "testUser",
+ Password: "testPass",
+ },
+ depsRepo: "testRepo",
+ },
+ }
+ // Create a temporary directory for testing and settings.xml creation
+ tempDir := t.TempDir()
+ err := mdt.createSettingsXmlWithConfiguredArtifactory(tempDir)
+ assert.NoError(t, err)
+
+ // Verify settings.xml file creation with username and password
+ settingsXmlPath := filepath.Join(tempDir, "settings.xml")
+ actualContent, err := os.ReadFile(settingsXmlPath)
+ actualContent = []byte(strings.ReplaceAll(string(actualContent), "\r\n", "\n"))
+ assert.NoError(t, err)
+ assert.Equal(t, settingsXmlWithUsernameAndPassword, string(actualContent))
+
+ // check curation command write a dedicated api for curation.
+ mdt.isCurationCmd = true
+ err = mdt.createSettingsXmlWithConfiguredArtifactory(tempDir)
+ require.NoError(t, err)
+ actualContent, err = os.ReadFile(settingsXmlPath)
+ actualContent = []byte(strings.ReplaceAll(string(actualContent), "\r\n", "\n"))
+ assert.NoError(t, err)
+ assert.Equal(t, settingsXmlWithUsernameAndPasswordAndCurationDedicatedAPi, string(actualContent))
+ mdt.isCurationCmd = false
+
+ mdt.server.Password = ""
+ // jfrog-ignore
+ mdt.server.AccessToken = dummyToken
+ err = mdt.createSettingsXmlWithConfiguredArtifactory(tempDir)
+ assert.NoError(t, err)
+
+ // Verify settings.xml file creation with username and access token
+ actualContent, err = os.ReadFile(settingsXmlPath)
+ actualContent = []byte(strings.ReplaceAll(string(actualContent), "\r\n", "\n"))
+ assert.NoError(t, err)
+ assert.Equal(t, settingsXmlWithUsernameAndToken, string(actualContent))
+
+ mdt.server.User = ""
+ err = mdt.createSettingsXmlWithConfiguredArtifactory(tempDir)
+ assert.NoError(t, err)
+
+ // Verify settings.xml file creation with access token only
+ actualContent, err = os.ReadFile(settingsXmlPath)
+ actualContent = []byte(strings.ReplaceAll(string(actualContent), "\r\n", "\n"))
+ assert.NoError(t, err)
+ assert.Equal(t, settingsXmlWithAccessToken, string(actualContent))
+}
+
+func TestRunProjectsCmd(t *testing.T) {
+ // Create and change directory to test workspace
+ _, cleanUp := coreTests.CreateTestWorkspace(t, filepath.Join("..", "..", "..", "..", "tests", "testdata", "projects", "package-managers", "maven", "maven-example"))
+ defer cleanUp()
+ mvnDepTreeManager := NewMavenDepTreeManager(&DepTreeParams{}, Projects)
+ output, clearMavenDepTreeRun, err := mvnDepTreeManager.RunMavenDepTree()
+ assert.NoError(t, err)
+ assert.NotNil(t, clearMavenDepTreeRun)
+
+ pomPathOccurrences := strings.Count(output, "pomPath")
+ assert.Equal(t, 4, pomPathOccurrences)
+ assert.NoError(t, clearMavenDepTreeRun())
+}
+
+func TestRemoveMavenConfig(t *testing.T) {
+ tmpDir := t.TempDir()
+ currentDir, err := os.Getwd()
+ assert.NoError(t, err)
+ restoreDir := tests.ChangeDirWithCallback(t, currentDir, tmpDir)
+ defer restoreDir()
+
+ // No maven.config exists
+ restoreFunc, err := removeMavenConfig()
+ assert.Nil(t, restoreFunc)
+ assert.Nil(t, err)
+
+ // Create maven.config
+ err = fileutils.CreateDirIfNotExist(".mvn")
+ assert.NoError(t, err)
+ file, err := os.Create(mavenConfigPath)
+ assert.NoError(t, err)
+ err = file.Close()
+ assert.NoError(t, err)
+ restoreFunc, err = removeMavenConfig()
+ assert.NoError(t, err)
+ assert.NoFileExists(t, mavenConfigPath)
+ err = restoreFunc()
+ assert.NoError(t, err)
+ assert.FileExists(t, mavenConfigPath)
+}
+
+func TestMavenDepTreeManager_suspectCurationBlockedError(t *testing.T) {
+ errPrefix := "[ERROR] Failed to execute goal on project my-app: Could not resolve dependencies for project com.mycompany.app:my-app:jar:1.0-SNAPSHOT: Failed to " +
+ "collect dependencies at junit:junit:jar:3.8.1: Failed to read artifact descriptor for junit:junit:jar:3.8.1: " +
+ "The following artifacts could not be resolved: junit:junit:pom:3.8.1 (absent): Could not transfer artifact junit:junit:pom:3.8.1 " +
+ "from/to artifactory (http://test:8046/artifactory/api/curation/audit/maven-remote):"
+ tests := []struct {
+ name string
+ wantMsgToUser string
+ input string
+ }{
+ {
+ name: "failed on 403",
+ wantMsgToUser: "Please verify pass-through enabled on the curated repos",
+ input: errPrefix + "status code: 403, reason phrase: Forbidden (403)",
+ },
+ {
+ name: "failed on 500",
+ wantMsgToUser: "Please verify pass-through enabled on the curated repos",
+ input: errPrefix + " status code: 500, reason phrase: Internal Server Error (500)",
+ },
+ {
+ name: "not 403 or 500",
+ wantMsgToUser: "",
+ input: errPrefix + " status code: 400, reason phrase: Forbidden (400)",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mdt := &MavenDepTreeManager{}
+ assert.Contains(t, tt.wantMsgToUser, mdt.suspectCurationBlockedError(tt.input))
+ })
+ }
+}
diff --git a/commands/audit/sca/java/resources/gradle-dep-tree.jar b/commands/audit/sca/java/resources/gradle-dep-tree.jar
new file mode 100644
index 000000000..b855f258b
Binary files /dev/null and b/commands/audit/sca/java/resources/gradle-dep-tree.jar differ
diff --git a/commands/audit/sca/java/resources/maven-dep-tree.jar b/commands/audit/sca/java/resources/maven-dep-tree.jar
new file mode 100644
index 000000000..f8d7ff406
Binary files /dev/null and b/commands/audit/sca/java/resources/maven-dep-tree.jar differ
diff --git a/commands/audit/sca/java/resources/settings.xml b/commands/audit/sca/java/resources/settings.xml
new file mode 100644
index 000000000..9dce691f8
--- /dev/null
+++ b/commands/audit/sca/java/resources/settings.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ artifactory
+ %s
+ %s
+
+
+
+
+ artifactory
+ %s
+ *
+
+
+
\ No newline at end of file
diff --git a/commands/audit/scarunner.go b/commands/audit/scarunner.go
index d514d0492..881f424ef 100644
--- a/commands/audit/scarunner.go
+++ b/commands/audit/scarunner.go
@@ -13,9 +13,9 @@ import (
"github.com/jfrog/jfrog-cli-core/v2/common/project"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
- "github.com/jfrog/jfrog-cli-core/v2/utils/java"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca"
_go "github.com/jfrog/jfrog-cli-security/commands/audit/sca/go"
+ "github.com/jfrog/jfrog-cli-security/commands/audit/sca/java"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/npm"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/nuget"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/pnpm"
diff --git a/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml b/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml
new file mode 100644
index 000000000..6d2096c96
--- /dev/null
+++ b/tests/testdata/projects/package-managers/maven/maven-example-with-many-types/pom.xml
@@ -0,0 +1,53 @@
+
+ 4.0.0
+
+ org.jfrog
+ cli-test
+ 1.0
+ jar
+
+ cli-test
+ http://maven.apache.org
+
+
+ UTF-8
+ 1.8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.1
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
+
+ junit
+ junit
+ 4.11
+ test
+
+
+ commons-io
+ commons-io
+ 1.2
+ test
+
+
+ org.webjars
+ lodash
+ 4.17.21
+ pom
+
+
+
\ No newline at end of file