Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control the depth of the pnpm dependency tree #202

Merged
merged 12 commits into from
Dec 15, 2024
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")),
attiasas marked this conversation as resolved.
Show resolved Hide resolved
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) {
attiasas marked this conversation as resolved.
Show resolved Hide resolved
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
Loading