Skip to content

Commit

Permalink
Control the depth of the pnpm dependency tree (#202)
Browse files Browse the repository at this point in the history
  • Loading branch information
attiasas authored Dec 15, 2024
1 parent 20b7fa1 commit a42170d
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 32 deletions.
27 changes: 15 additions & 12 deletions cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package docs

import (
"fmt"
"github.com/jfrog/jfrog-cli-security/commands/git"
"strings"

"github.com/jfrog/jfrog-cli-security/commands/git"

"github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
Expand Down Expand Up @@ -114,6 +115,7 @@ const (
useWrapperAudit = auditPrefix + UseWrapper
ExcludeTestDeps = "exclude-test-deps"
DepType = "dep-type"
MaxTreeDepth = "max-tree-depth"
ThirdPartyContextualAnalysis = "third-party-contextual-analysis"
RequirementsFile = "requirements-file"
WorkingDirs = "working-dirs"
Expand Down Expand Up @@ -240,17 +242,18 @@ var flagsMap = map[string]components.Flag{
"List of exclusions separated by semicolons, utilized to skip sub-projects from undergoing an audit. These exclusions may incorporate the * and ? wildcards.",
components.WithStrDefaultValue(strings.Join(utils.DefaultScaExcludePatterns, ";")),
),
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 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."),
Pipenv: components.NewBoolFlag(Pipenv, "Set to true to request audit for a Pipenv project."),
Poetry: components.NewBoolFlag(Poetry, "Set to true to request audit for a Poetry project."),
Go: components.NewBoolFlag(Go, "Set to true to request audit for a Go project."),
DepType: components.NewStringFlag(DepType, "[npm] Defines npm dependencies type. Possible values are: all, devOnly and prodOnly."),
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 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."),
Pipenv: components.NewBoolFlag(Pipenv, "Set to true to request audit for a Pipenv project."),
Poetry: components.NewBoolFlag(Poetry, "Set to true to request audit for a Poetry project."),
Go: components.NewBoolFlag(Go, "Set to true to request audit for a Go project."),
DepType: components.NewStringFlag(DepType, "[npm] Defines npm dependencies type. Possible values are: all, devOnly and prodOnly."),
MaxTreeDepth: components.NewStringFlag(MaxTreeDepth, "[pnpm] Max depth of the generated dependencies tree for SCA scan.", components.WithStrDefaultValue("Infinity")),
ThirdPartyContextualAnalysis: components.NewBoolFlag(
ThirdPartyContextualAnalysis,
"[npm] when set, the Contextual Analysis scan also uses the code of the project dependencies to determine the applicability of the vulnerability.",
Expand Down
1 change: 1 addition & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ func CreateAuditCmd(c *components.Context) (string, string, *coreConfig.ServerDe
SetInsecureTls(c.GetBoolFlagValue(flags.InsecureTls)).
SetNpmScope(c.GetStringFlagValue(flags.DepType)).
SetPipRequirementsFile(c.GetStringFlagValue(flags.RequirementsFile)).
SetMaxTreeDepth(c.GetStringFlagValue(flags.MaxTreeDepth)).
SetExclusions(pluginsCommon.GetStringsArrFlagValue(c, flags.Exclusions))
return xrayVersion, xscVersion, serverDetails, auditCmd, err
}
Expand Down
32 changes: 14 additions & 18 deletions commands/audit/sca/pnpm/pnpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ import (
"github.com/jfrog/gofrog/datastructures"
"github.com/jfrog/gofrog/io"
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"

"github.com/jfrog/jfrog-cli-security/commands/audit/sca"
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/npm"
"github.com/jfrog/jfrog-cli-security/utils"
Expand All @@ -21,6 +18,7 @@ import (
"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"
"golang.org/x/exp/maps"

biutils "github.com/jfrog/build-info-go/utils"
xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils"
Expand Down Expand Up @@ -123,13 +121,17 @@ func installProjectIfNeeded(pnpmExecPath, workingDir string) (dirForDependencies
err = fmt.Errorf("failed copying project to temp dir: %w", err)
return
}
err = getPnpmCmd(pnpmExecPath, dirForDependenciesCalculation, "install", npm.IgnoreScriptsFlag).GetCmd().Run()
output, err := getPnpmCmd(pnpmExecPath, dirForDependenciesCalculation, "install", npm.IgnoreScriptsFlag).GetCmd().CombinedOutput()
if err != nil {
err = fmt.Errorf("failed to install project: %w\n%s", err, string(output))
}
return
}

// Run 'pnpm ls ...' command (project must be installed) and parse the returned result to create a dependencies trees for the projects.
func calculateDependencies(executablePath, workingDir string, params utils.AuditParams) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) {
lsArgs := append([]string{"--depth", "Infinity", "--json", "--long"}, params.Args()...)
lsArgs := append([]string{"--depth", params.MaxTreeDepth(), "--json", "--long"}, params.Args()...)
log.Debug("Running Pnpm ls command with args:", lsArgs)
npmLsCmdContent, err := getPnpmCmd(executablePath, workingDir, "ls", lsArgs...).RunWithOutput()
if err != nil {
return
Expand Down Expand Up @@ -163,13 +165,13 @@ func createProjectDependenciesTree(project pnpmLsProject) map[string]xray.DepTre
for depName, dependency := range project.Dependencies {
directDependency := getDependencyId(depName, dependency.Version)
directDependencies = append(directDependencies, directDependency)
appendTransitiveDependencies(directDependency, dependency.Dependencies, treeMap)
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)
appendTransitiveDependencies(directDependency, dependency.Dependencies, &treeMap)
}
if len(directDependencies) > 0 {
treeMap[getDependencyId(project.Name, project.Version)] = xray.DepTreeNode{Children: directDependencies}
Expand All @@ -182,21 +184,15 @@ func getDependencyId(depName, version string) string {
return techutils.Npm.GetPackageTypeId() + depName + ":" + version
}

func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result map[string]xray.DepTreeNode) {
func appendTransitiveDependencies(parent string, dependencies map[string]pnpmLsDependency, result *map[string]xray.DepTreeNode) {
for depName, dependency := range dependencies {
dependencyId := getDependencyId(depName, dependency.Version)
if node, ok := result[parent]; ok {
node.Children = appendUniqueChild(node.Children, dependencyId)
if node, ok := (*result)[parent]; ok {
node.Children = append(node.Children, dependencyId)
(*result)[parent] = node
} else {
result[parent] = xray.DepTreeNode{Children: []string{dependencyId}}
(*result)[parent] = xray.DepTreeNode{Children: []string{dependencyId}}
}
appendTransitiveDependencies(dependencyId, dependency.Dependencies, result)
}
}

func appendUniqueChild(children []string, candidateDependency string) []string {
if slices.Contains(children, candidateDependency) {
return children
}
return append(children, candidateDependency)
}
70 changes: 68 additions & 2 deletions commands/audit/sca/pnpm/pnpm_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package pnpm

import (
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"path/filepath"
"sort"
"testing"

"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand All @@ -16,6 +18,70 @@ import (
"github.com/jfrog/jfrog-cli-security/utils"
)

func TestBuildDependencyTreeLimitedDepth(t *testing.T) {
// Create and change directory to test workspace
_, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("projects", "package-managers", "npm", "npm-big-tree"))
defer cleanUp()
testCases := []struct {
name string
treeDepth string
expectedUniqueDeps []string
expectedTree *xrayUtils.GraphNode
}{
{
name: "Only direct dependencies",
treeDepth: "0",
expectedUniqueDeps: []string{
"npm://zen-website:1.0.0",
"npm://balaganjs:1.0.0",
},
expectedTree: &xrayUtils.GraphNode{
Id: "npm://zen-website:1.0.0",
Nodes: []*xrayUtils.GraphNode{{Id: "npm://balaganjs:1.0.0"}},
},
},
{
name: "With transitive dependencies",
treeDepth: "1",
expectedUniqueDeps: []string{
"npm://axios:1.7.9",
"npm://balaganjs:1.0.0",
"npm://yargs:13.3.0",
"npm://zen-website:1.0.0",
},
expectedTree: &xrayUtils.GraphNode{
Id: "npm://zen-website:1.0.0",
Nodes: []*xrayUtils.GraphNode{
{
Id: "npm://balaganjs:1.0.0",
Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.7.9"}, {Id: "npm://yargs:13.3.0"}},
},
},
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
// Build dependency tree
params := &utils.AuditBasicParams{}
rootNode, uniqueDeps, err := BuildDependencyTree(params.SetMaxTreeDepth(testCase.treeDepth))
require.NoError(t, err)
sort.Slice(uniqueDeps, func(i, j int) bool {
return uniqueDeps[i] < uniqueDeps[j]
})
// 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)
}
}
})
}
}

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"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "zen-website",
"version": "1.0.0",
"description": "",
"main": "index.js",
"publishConfig": {
"registry": "http://artifactory-unified.soleng-us.jfrog.team/artifactory/api/npm/npm/"
},
"scripts": {
"dev": "nodemon ./index.js",
"ui": "browser-sync start --config bs-config.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"balaganjs": "1.0.0"
}
}

12 changes: 12 additions & 0 deletions utils/auditbasicparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type AuditParams interface {
InstallCommandName() string
InstallCommandArgs() []string
SetNpmScope(depType string) *AuditBasicParams
SetMaxTreeDepth(maxTreeDepth string) *AuditBasicParams
MaxTreeDepth() string
OutputFormat() format.OutputFormat
DepsRepo() string
SetDepsRepo(depsRepo string) *AuditBasicParams
Expand Down Expand Up @@ -55,6 +57,7 @@ type AuditBasicParams struct {
ignoreConfigFile bool
isMavenDepTreeInstalled bool
isCurationCmd bool
maxTreeDepth string
pipRequirementsFile string
depsRepo string
installCommandName string
Expand Down Expand Up @@ -118,6 +121,15 @@ func (abp *AuditBasicParams) UseJas() bool {
return abp.useJas
}

func (abp *AuditBasicParams) MaxTreeDepth() string {
return abp.maxTreeDepth
}

func (abp *AuditBasicParams) SetMaxTreeDepth(maxTreeDepth string) *AuditBasicParams {
abp.maxTreeDepth = maxTreeDepth
return abp
}

func (abp *AuditBasicParams) PipRequirementsFile() string {
return abp.pipRequirementsFile
}
Expand Down

0 comments on commit a42170d

Please sign in to comment.