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