From e06b87a9b913ab3b8c49100a37e1c1e4405b53be Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 13 Feb 2024 10:39:58 +0200 Subject: [PATCH] done tests --- audit_test.go | 26 +++++++ cli/docs/flags.go | 6 +- commands/audit/sca/pnpm/pnpm.go | 112 ++++++++++++++++----------- commands/audit/sca/pnpm/pnpm_test.go | 85 ++++++++++++++++++++ 4 files changed, 180 insertions(+), 49 deletions(-) diff --git a/audit_test.go b/audit_test.go index 951f6bc4..51bafd55 100644 --- a/audit_test.go +++ b/audit_test.go @@ -50,6 +50,32 @@ func testXrayAuditNpm(t *testing.T, format string) string { return securityTests.PlatformCli.RunCliCmdWithOutput(t, "audit", "--npm", "--licenses", "--format="+format) } +func TestXrayAuditPnpmJson(t *testing.T) { + output := testXrayAuditPnpm(t, string(format.Json)) + securityTestUtils.VerifyJsonScanResults(t, output, 0, 1, 1) +} + +func TestXrayAuditPnpmSimpleJson(t *testing.T) { + output := testXrayAuditPnpm(t, string(format.SimpleJson)) + securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 1) +} + +func testXrayAuditPnpm(t *testing.T, format string) string { + securityTestUtils.InitSecurityTest(t, scangraph.GraphScanMinXrayVersion) + tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t) + defer createTempDirCallback() + npmProjectPath := filepath.Join(filepath.FromSlash(securityTestUtils.GetTestResourcesPath()), "projects", "package-managers", "npm", "npm-no-lock") + // Copy the npm project from the testdata to a temp dir + assert.NoError(t, biutils.CopyDir(npmProjectPath, tempDirPath, true, nil)) + prevWd := securityTestUtils.ChangeWD(t, tempDirPath) + defer clientTests.ChangeDirAndAssert(t, prevWd) + // Run pnpm install before executing audit + assert.NoError(t, exec.Command("pnpm", "install").Run()) + // Add dummy descriptor file to check that we run only specific audit + addDummyPackageDescriptor(t, true) + return securityTests.PlatformCli.RunCliCmdWithOutput(t, "audit", "--pnpm", "--licenses", "--format="+format) +} + func TestXrayAuditYarnV2Json(t *testing.T) { testXrayAuditYarn(t, "yarn-v2", func() { output := runXrayAuditYarnWithOutput(t, string(format.Json)) diff --git a/cli/docs/flags.go b/cli/docs/flags.go index f14b53b8..8f47c0a4 100644 --- a/cli/docs/flags.go +++ b/cli/docs/flags.go @@ -35,6 +35,7 @@ const ( Mvn = "mvn" Gradle = "gradle" Npm = "npm" + Pnpm = "pnpm" Yarn = "yarn" Nuget = "nuget" Go = "go" @@ -124,7 +125,7 @@ var commandFlags = map[string][]string{ }, Audit: { url, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Licenses, OutputFormat, ExcludeTestDeps, - useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, + useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm, Pnpm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, }, CurationAudit: { CurationOutput, WorkingDirs, CurationThreads, @@ -203,7 +204,8 @@ var flagsMap = map[string]components.Flag{ ), Mvn: components.NewBoolFlag(Mvn, "Set to true to request audit for a Maven project."), Gradle: components.NewBoolFlag(Gradle, "Set to true to request audit for a Gradle project."), - Npm: components.NewBoolFlag(Npm, "Set to true to request audit for an npm project."), + Npm: components.NewBoolFlag(Npm, "Set to true to request audit for a npm project."), + Pnpm: components.NewBoolFlag(Pnpm, "Set to true to request audit for a Pnpm project."), Yarn: components.NewBoolFlag(Yarn, "Set to true to request audit for a Yarn project."), Nuget: components.NewBoolFlag(Nuget, "Set to true to request audit for a .NET project."), Pip: components.NewBoolFlag(Pip, "Set to true to request audit for a Pip project."), diff --git a/commands/audit/sca/pnpm/pnpm.go b/commands/audit/sca/pnpm/pnpm.go index 12d55e82..3ec704cf 100644 --- a/commands/audit/sca/pnpm/pnpm.go +++ b/commands/audit/sca/pnpm/pnpm.go @@ -4,37 +4,36 @@ import ( "encoding/json" "errors" "os/exec" - - // "strings" + "path/filepath" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/gofrog/io" - // "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - // "github.com/jfrog/jfrog-cli-security/commands/audit/sca" "github.com/jfrog/jfrog-cli-security/utils" + "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" coreXray "github.com/jfrog/jfrog-cli-core/v2/utils/xray" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) -type pnpmLsProject struct { - Name string `json:"name"` - Version string `json:"version"` - Dependencies map[string]pnpmLsDependency `json:"dependencies,omitempty"` -} - type pnpmLsDependency struct { From string `json:"from"` Version string `json:"version"` Dependencies map[string]pnpmLsDependency `json:"dependencies,omitempty"` - // binary location - Resolved string `json:"resolved"` +} + +type pnpmLsProject struct { + Name string `json:"name"` + Version string `json:"version"` + Dependencies map[string]pnpmLsDependency `json:"dependencies,omitempty"` + DevDependencies map[string]pnpmLsDependency `json:"devDependencies,omitempty"` } func BuildDependencyTree(params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { + // Prepare currentDir, err := coreutils.GetWorkingDirectory() if err != nil { return @@ -43,55 +42,77 @@ func BuildDependencyTree(params utils.AuditParams) (dependencyTrees []*xrayUtils if err != nil { return } - // Run 'pnpm ls...' command and parse the returned result to create a dependencies map. - projectInfo, err := calculateDependencies(pnpmExecPath, currentDir) - if err != nil { + // Build + if err = installProjectIfNeeded(pnpmExecPath, currentDir); errorutils.CheckError(err) != nil { return } - dependencyTrees, uniqueDeps = parsePnpmDependenciesList(projectInfo) - return + return calculateDependencies(pnpmExecPath, currentDir, params) } -func getPnpmExecPath() (string, error) { - pnpmExecPath, err := exec.LookPath("pnpm") - if err != nil { - return "", err +func getPnpmExecPath() (pnpmExecPath string, err error) { + if pnpmExecPath, err = exec.LookPath("pnpm"); errorutils.CheckError(err) != nil { + return } if pnpmExecPath == "" { - return "", errors.New("could not find the 'pnpm' executable in the system PATH") + err = errors.New("could not find the 'pnpm' executable in the system PATH") + return } log.Debug("Using Pnpm executable:", pnpmExecPath) // Validate pnpm version command version, err := getPnpmCmd(pnpmExecPath, "", "--version").RunWithOutput() - if err != nil { - return "", err + if errorutils.CheckError(err) != nil { + return } log.Debug("Pnpm version:", string(version)) - return pnpmExecPath, nil + return } -// Run 'pnpm ls ...' command and parse the returned result to create a dependencies map of. -func calculateDependencies(executablePath, workingDir string) ([]pnpmLsProject, error) { - npmLsCmdContent, err := getPnpmCmd(executablePath, workingDir, "ls", "--depth", "Infinity", "--json", "--long").RunWithOutput() +func getPnpmCmd(pnpmExecPath, workingDir, cmd string, args ...string) *io.Command { + command := io.NewCommand(pnpmExecPath, cmd, args) + if workingDir != "" { + command.Dir = workingDir + } + 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) { + lockFileExists, err := fileutils.IsFileExists(filepath.Join(workingDir, "pnpm-lock.yaml"), false) + if err != nil { + return + } + pnpmDirExists, err := fileutils.IsDirExists(filepath.Join(workingDir, "node_modules", ".pnpm"), false) + if err != nil || (lockFileExists && pnpmDirExists) { + return + } + // Install is needed + log.Debug("Installing Pnpm project:", workingDir) + return getPnpmCmd(pnpmExecPath, workingDir, "install").GetCmd().Run() +} + +// Run 'pnpm ls ...' command (project must be installed) and parse the returned result to create a dependencies map of. +func calculateDependencies(executablePath, workingDir string, params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { + lsArgs := append([]string{"--depth", "Infinity", "--json", "--long"}, params.Args()...) + npmLsCmdContent, err := getPnpmCmd(executablePath, workingDir, "ls", lsArgs...).RunWithOutput() if err != nil { - return nil, err + return } log.Debug("Pnpm ls command output:\n", string(npmLsCmdContent)) output := &[]pnpmLsProject{} - if err := json.Unmarshal(npmLsCmdContent, output); err != nil { - return nil, err + if err = json.Unmarshal(npmLsCmdContent, output); err != nil { + return } - return *output, nil + dependencyTrees, uniqueDeps = parsePnpmLSContent(*output) + return } -func parsePnpmDependenciesList(projectInfo []pnpmLsProject) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string) { +func parsePnpmLSContent(projectInfo []pnpmLsProject) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string) { uniqueDepsSet := datastructures.MakeSet[string]() for _, project := range projectInfo { - treeMap := createProjectDependenciesTree(project) // Parse the dependencies into Xray dependency tree format - dependencyTree, uniqueProjectDeps := coreXray.BuildXrayDependencyTree(treeMap, getDependencyId(project.Name, project.Version)) + dependencyTree, uniqueProjectDeps := coreXray.BuildXrayDependencyTree(createProjectDependenciesTree(project), getDependencyId(project.Name, project.Version)) + // Add results dependencyTrees = append(dependencyTrees, dependencyTree) - // Add the dependencies to the unique dependencies set uniqueDepsSet.AddElements(uniqueProjectDeps...) } uniqueDeps = uniqueDepsSet.ToSlice() @@ -100,16 +121,21 @@ func parsePnpmDependenciesList(projectInfo []pnpmLsProject) (dependencyTrees []* func createProjectDependenciesTree(project pnpmLsProject) map[string][]string { treeMap := make(map[string][]string) - // Create a map of the project's dependencies directDependencies := []string{} - projectId := getDependencyId(project.Name, project.Version) + // Handle production-dependencies for depName, dependency := range project.Dependencies { directDependency := getDependencyId(depName, dependency.Version) directDependencies = append(directDependencies, directDependency) appendTransitiveDependencies(directDependency, dependency.Dependencies, treeMap) } + // Handle dev-dependencies + for depName, dependency := range project.DevDependencies { + directDependency := getDependencyId(depName, dependency.Version) + directDependencies = append(directDependencies, directDependency) + appendTransitiveDependencies(directDependency, dependency.Dependencies, treeMap) + } if len(directDependencies) > 0 { - treeMap[projectId] = directDependencies + treeMap[getDependencyId(project.Name, project.Version)] = directDependencies } return treeMap } @@ -139,11 +165,3 @@ func appendUniqueChild(children []string, candidateDependency string) []string { } return append(children, candidateDependency) } - -func getPnpmCmd(pnpmExecPath, workingDir, cmd string, args ...string) *io.Command { - command := io.NewCommand(pnpmExecPath, cmd, args) - if workingDir != "" { - command.Dir = workingDir - } - return command -} diff --git a/commands/audit/sca/pnpm/pnpm_test.go b/commands/audit/sca/pnpm/pnpm_test.go index c89791d6..7ae28bf0 100644 --- a/commands/audit/sca/pnpm/pnpm_test.go +++ b/commands/audit/sca/pnpm/pnpm_test.go @@ -1 +1,86 @@ package pnpm + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + + "github.com/jfrog/jfrog-cli-security/commands/audit/sca" + "github.com/jfrog/jfrog-cli-security/utils" +) + +func TestBuildDependencyTree(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "npm", "npm-no-lock")) + defer cleanUp() + + testCases := []struct { + name string + depType string + expectedUniqueDeps []string + expectedTree *xrayUtils.GraphNode + }{ + { + name: "All", + depType: "all", + expectedUniqueDeps: []string{ + "npm://jfrog-cli-tests:v1.0.0", + "npm://xml:1.0.1", + "npm://json:9.0.6", + }, + expectedTree: &xrayUtils.GraphNode{ + Id: "npm://jfrog-cli-tests:v1.0.0", + Nodes: []*xrayUtils.GraphNode{ + {Id: "npm://xml:1.0.1"}, + {Id: "npm://json:9.0.6"}, + }, + }, + }, + { + name: "Prod", + depType: "prodOnly", + expectedUniqueDeps: []string{ + "npm://jfrog-cli-tests:v1.0.0", + "npm://xml:1.0.1", + }, + expectedTree: &xrayUtils.GraphNode{ + Id: "npm://jfrog-cli-tests:v1.0.0", + Nodes: []*xrayUtils.GraphNode{{Id: "npm://xml:1.0.1"}}, + }, + }, + { + name: "Dev", + depType: "devOnly", + expectedUniqueDeps: []string{ + "npm://jfrog-cli-tests:v1.0.0", + "npm://json:9.0.6", + }, + expectedTree: &xrayUtils.GraphNode{ + Id: "npm://jfrog-cli-tests:v1.0.0", + Nodes: []*xrayUtils.GraphNode{{Id: "npm://json:9.0.6"}}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // Build dependency tree + params := &utils.AuditBasicParams{} + rootNode, uniqueDeps, err := BuildDependencyTree(params.SetNpmScope(testCase.depType)) + require.NoError(t, err) + // Validations + assert.ElementsMatch(t, uniqueDeps, testCase.expectedUniqueDeps, "First is actual, Second is Expected") + if assert.Len(t, rootNode, 1) { + assert.Equal(t, rootNode[0].Id, testCase.expectedTree.Id) + if !tests.CompareTree(testCase.expectedTree, rootNode[0]) { + t.Error("expected:", testCase.expectedTree.Nodes, "got:", rootNode[0].Nodes) + } + } + }) + } +}