Skip to content

Commit

Permalink
done tests
Browse files Browse the repository at this point in the history
  • Loading branch information
attiasas committed Feb 13, 2024
1 parent b169f7f commit e06b87a
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 49 deletions.
26 changes: 26 additions & 0 deletions audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 4 additions & 2 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
Mvn = "mvn"
Gradle = "gradle"
Npm = "npm"
Pnpm = "pnpm"
Yarn = "yarn"
Nuget = "nuget"
Go = "go"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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."),
Expand Down
112 changes: 65 additions & 47 deletions commands/audit/sca/pnpm/pnpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
85 changes: 85 additions & 0 deletions commands/audit/sca/pnpm/pnpm_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}

0 comments on commit e06b87a

Please sign in to comment.