From cac74799861e3e391c42df10f3ba54b87009dfb6 Mon Sep 17 00:00:00 2001 From: Eran Turgeman <81029514+eranturgeman@users.noreply.github.com> Date: Sun, 24 Mar 2024 10:53:18 +0200 Subject: [PATCH] Additions to support Pnpm in frogbot (#41) --- commands/audit/sca/pnpm/pnpm.go | 44 ++++++++++++++++++++++++---- commands/audit/sca/pnpm/pnpm_test.go | 25 ++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/commands/audit/sca/pnpm/pnpm.go b/commands/audit/sca/pnpm/pnpm.go index b254baee..39dedce5 100644 --- a/commands/audit/sca/pnpm/pnpm.go +++ b/commands/audit/sca/pnpm/pnpm.go @@ -3,6 +3,7 @@ package pnpm import ( "encoding/json" "errors" + "fmt" "os/exec" "path/filepath" @@ -18,6 +19,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" + biutils "github.com/jfrog/build-info-go/utils" coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) @@ -46,10 +48,21 @@ func BuildDependencyTree(params utils.AuditParams) (dependencyTrees []*xrayUtils return } // Build - if err = installProjectIfNeeded(pnpmExecPath, currentDir); errorutils.CheckError(err) != nil { + var dirForDependenciesCalculation string + if dirForDependenciesCalculation, err = installProjectIfNeeded(pnpmExecPath, currentDir); errorutils.CheckError(err) != nil { return } - return calculateDependencies(pnpmExecPath, currentDir, params) + + if dirForDependenciesCalculation == "" { + // If we didn't execute 'install' dirForDependenciesCalculation contains an empty value and the dependencies calculation should be performed on the original cloned dir + dirForDependenciesCalculation = currentDir + } else { + // If tempDirForDependenciesCalculation contains a non-empty value, it means we created a temporary directory during the execution of 'install' command, and it needs to removed at the end + defer func() { + err = errors.Join(err, biutils.RemoveTempDir(dirForDependenciesCalculation)) + }() + } + return calculateDependencies(pnpmExecPath, dirForDependenciesCalculation, params) } func getPnpmExecPath() (pnpmExecPath string, err error) { @@ -76,8 +89,10 @@ func getPnpmCmd(pnpmExecPath, workingDir, cmd string, args ...string) *io.Comman return command } -// Install is required when "pnpm-lock.yaml" lock file or "node_modules/.pnpm" directory not exists. -func installProjectIfNeeded(pnpmExecPath, workingDir string) (err error) { +// Installation is necessary when either the "pnpm-lock.yaml" lock file or the "node_modules/.pnpm" directory does not exist. +// If install is needed, we duplicate the project to a temporary directory and conduct the 'install' operation on the duplicate, to ensure that the original clone does not retain the node_modules directory if it didn't exist previously. +// Upon 'install' the path to the duplicate directory will be returned. +func installProjectIfNeeded(pnpmExecPath, workingDir string) (dirForDependenciesCalculation string, err error) { lockFileExists, err := fileutils.IsFileExists(filepath.Join(workingDir, "pnpm-lock.yaml"), false) if err != nil { return @@ -86,9 +101,26 @@ func installProjectIfNeeded(pnpmExecPath, workingDir string) (err error) { if err != nil || (lockFileExists && pnpmDirExists) { return } - // Install is needed + // Install is needed and will be performed on a copy of the cloned dir log.Debug("Installing Pnpm project:", workingDir) - return getPnpmCmd(pnpmExecPath, workingDir, "install", npm.IgnoreScriptsFlag).GetCmd().Run() + dirForDependenciesCalculation, err = fileutils.CreateTempDir() + if err != nil { + err = fmt.Errorf("failed to create a temporary dir: %w", err) + return + } + defer func() { + // If an error occurs for any reason, we proceed to delete the temporary directory. + if err != nil { + err = errors.Join(err, fileutils.RemoveTempDir(dirForDependenciesCalculation)) + } + }() + err = biutils.CopyDir(workingDir, dirForDependenciesCalculation, true, nil) + if err != nil { + err = fmt.Errorf("failed copying project to temp dir: %w", err) + return + } + err = getPnpmCmd(pnpmExecPath, dirForDependenciesCalculation, "install", npm.IgnoreScriptsFlag).GetCmd().Run() + return } // Run 'pnpm ls ...' command (project must be installed) and parse the returned result to create a dependencies trees for the projects. diff --git a/commands/audit/sca/pnpm/pnpm_test.go b/commands/audit/sca/pnpm/pnpm_test.go index 7ae28bf0..e04ba84f 100644 --- a/commands/audit/sca/pnpm/pnpm_test.go +++ b/commands/audit/sca/pnpm/pnpm_test.go @@ -1,6 +1,8 @@ package pnpm import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "path/filepath" "testing" @@ -84,3 +86,26 @@ func TestBuildDependencyTree(t *testing.T) { }) } } + +func TestInstallProjectIfNeeded(t *testing.T) { + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "npm", "npm-no-lock")) + defer cleanUp() + + currentDir, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + + pnpmExecPath, err := getPnpmExecPath() + assert.NoError(t, err) + + dirForDependenciesCalculation, err := installProjectIfNeeded(pnpmExecPath, currentDir) + assert.NoError(t, err) + assert.NotEmpty(t, dirForDependenciesCalculation) + + nodeModulesExist, err := fileutils.IsDirExists(filepath.Join(dirForDependenciesCalculation, "node_modules"), false) + assert.NoError(t, err) + assert.True(t, nodeModulesExist) + + nodeModulesExist, err = fileutils.IsDirExists(filepath.Join(currentDir, "node_modules"), false) + assert.NoError(t, err) + assert.False(t, nodeModulesExist) +}