Skip to content

Commit

Permalink
Skip install in dependencies map calculation if requested (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
eranturgeman authored Sep 30, 2024
1 parent c694d21 commit 7bd47c2
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 11 deletions.
17 changes: 17 additions & 0 deletions build/testdata/npm/noBuildProject/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "npm_test2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"lightweight": "^0.1.0",
"minimist": "^0.1.0",
"underscore": "^1.13.6",
"cors.js": "0.0.1-security"
}
}
47 changes: 38 additions & 9 deletions build/utils/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func CalculateNpmDependenciesList(executablePath, srcPath, moduleId string, npmP
log = &utils.NullLog{}
}
// Calculate npm dependency tree using 'npm ls...'.
dependenciesMap, err := CalculateDependenciesMap(executablePath, srcPath, moduleId, npmParams, log)
dependenciesMap, err := CalculateDependenciesMap(executablePath, srcPath, moduleId, npmParams, log, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -92,7 +92,7 @@ type dependencyInfo struct {

// Run 'npm list ...' command and parse the returned result to create a dependencies map of.
// The dependencies map looks like name:version -> entities.Dependency.
func CalculateDependenciesMap(executablePath, srcPath, moduleId string, npmListParams NpmTreeDepListParam, log utils.Log) (map[string]*dependencyInfo, error) {
func CalculateDependenciesMap(executablePath, srcPath, moduleId string, npmListParams NpmTreeDepListParam, log utils.Log, skipInstall bool) (map[string]*dependencyInfo, error) {
dependenciesMap := make(map[string]*dependencyInfo)
// These arguments must be added at the end of the command, to override their other values (if existed in nm.npmArgs).
npmVersion, err := GetNpmVersion(executablePath, log)
Expand All @@ -108,7 +108,7 @@ func CalculateDependenciesMap(executablePath, srcPath, moduleId string, npmListP
if nodeModulesExist && !npmListParams.IgnoreNodeModules {
data = runNpmLsWithNodeModules(executablePath, srcPath, npmListParams.Args, log)
} else {
data, err = runNpmLsWithoutNodeModules(executablePath, srcPath, npmListParams, log, npmVersion)
data, err = runNpmLsWithoutNodeModules(executablePath, srcPath, npmListParams, log, npmVersion, skipInstall)
if err != nil {
return nil, err
}
Expand All @@ -135,13 +135,14 @@ func runNpmLsWithNodeModules(executablePath, srcPath string, npmArgs []string, l
return
}

func runNpmLsWithoutNodeModules(executablePath, srcPath string, npmListParams NpmTreeDepListParam, log utils.Log, npmVersion *version.Version) ([]byte, error) {
isPackageLockExist, isDirExistsErr := utils.IsFileExists(filepath.Join(srcPath, "package-lock.json"), false)
if isDirExistsErr != nil {
return nil, isDirExistsErr
func runNpmLsWithoutNodeModules(executablePath, srcPath string, npmListParams NpmTreeDepListParam, log utils.Log, npmVersion *version.Version, skipInstall bool) ([]byte, error) {
installRequired, err := isInstallRequired(srcPath, npmListParams, log, skipInstall)
if err != nil {
return nil, err
}
if !isPackageLockExist || (npmListParams.OverwritePackageLock && checkIfLockFileShouldBeUpdated(srcPath, log)) {
err := installPackageLock(executablePath, srcPath, npmListParams.InstallCommandArgs, npmListParams.Args, log, npmVersion)

if installRequired {
err = installPackageLock(executablePath, srcPath, npmListParams.InstallCommandArgs, npmListParams.Args, log, npmVersion)
if err != nil {
return nil, err
}
Expand All @@ -156,6 +157,34 @@ func runNpmLsWithoutNodeModules(executablePath, srcPath string, npmListParams Np
return data, nil
}

// This function determines whether a project installation is required by evaluating the following criteria:
// 1) Checks if the "package-lock.json" file exists in the project directory.
// 2) Verifies if an installation command was provided by the user.
// 3) Checks if the lock file should be updated due to an override request.
//
// Conditions for triggering installation:
// - If the user provided an installation command, installation is required.
// - If the "package-lock.json" file is missing or an override request to update the lock file exists, installation is required, unless the user explicitly requested to skip the installation.
//
// If installation is required but skipped by the user's request, an error is returned.
func isInstallRequired(srcPath string, npmListParams NpmTreeDepListParam, log utils.Log, skipInstall bool) (bool, error) {
isPackageLockExist, err := utils.IsFileExists(filepath.Join(srcPath, "package-lock.json"), false)
if err != nil {
return false, err
}

if len(npmListParams.InstallCommandArgs) > 0 {
return true, nil
}
if !isPackageLockExist || (npmListParams.OverwritePackageLock && checkIfLockFileShouldBeUpdated(srcPath, log)) {
if skipInstall {
return false, &utils.ErrProjectNotInstalled{UninstalledDir: srcPath}
}
return true, nil
}
return false, nil
}

func installPackageLock(executablePath, srcPath string, npmInstallCommandArgs, npmArgs []string, log utils.Log, npmVersion *version.Version) error {
if npmVersion.AtLeast("6.0.0") {
npmArgs = append(npmArgs, "--package-lock-only")
Expand Down
18 changes: 16 additions & 2 deletions build/utils/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -233,7 +234,7 @@ func TestDependencyWithNoIntegrity(t *testing.T) {
assert.Greaterf(t, len(dependencies), 0, "Error: dependencies are not found!")
}

// This test case verifies that CalculateNpmDependenciesList correctly handles the exclusion of 'node_modules'
// This test case verifies that CalculateDependenciesMap correctly handles the exclusion of 'node_modules'
// and updates 'package-lock.json' as required, based on the 'IgnoreNodeModules' and 'OverwritePackageLock' parameters.
func TestDependencyPackageLockOnly(t *testing.T) {
npmVersion, _, err := GetNpmVersionAndExecPath(logger)
Expand All @@ -249,12 +250,25 @@ func TestDependencyPackageLockOnly(t *testing.T) {

// Calculate dependencies.
dependencies, err := CalculateDependenciesMap("npm", path, "jfrogtest",
NpmTreeDepListParam{Args: []string{}, IgnoreNodeModules: true, OverwritePackageLock: true}, logger)
NpmTreeDepListParam{Args: []string{}, IgnoreNodeModules: true, OverwritePackageLock: true}, logger, false)
assert.NoError(t, err)
var expectedRes = getExpectedRespForTestDependencyPackageLockOnly()
assert.Equal(t, expectedRes, dependencies)
}

func TestCalculateDependenciesMapWithProhibitedInstallation(t *testing.T) {
path, cleanup := tests.CreateTestProject(t, filepath.Join("..", "testdata", "npm", "noBuildProject"))
defer cleanup()

dependencies, err := CalculateDependenciesMap("npm", path, "jfrogtest",
NpmTreeDepListParam{Args: []string{}, IgnoreNodeModules: false, OverwritePackageLock: false}, logger, true)

assert.Nil(t, dependencies)
assert.Error(t, err)
var installForbiddenErr *utils.ErrProjectNotInstalled
assert.True(t, errors.As(err, &installForbiddenErr))
}

func getExpectedRespForTestDependencyPackageLockOnly() map[string]*dependencyInfo {
return map[string]*dependencyInfo{
"underscore:1.13.6": {
Expand Down
9 changes: 9 additions & 0 deletions utils/error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -28,6 +29,14 @@ func NewForbiddenError() *ForbiddenError {
return &ForbiddenError{}
}

type ErrProjectNotInstalled struct {
UninstalledDir string
}

func (err *ErrProjectNotInstalled) Error() string {
return fmt.Sprintf("Directory '%s' is not installed. Skipping SCA scan in this directory...", err.UninstalledDir)
}

// IsForbiddenOutput checks whether the provided output includes a 403 Forbidden. The various package managers have their own forbidden output formats.
func IsForbiddenOutput(tech PackageManager, cmdOutput string) bool {
switch tech {
Expand Down

0 comments on commit 7bd47c2

Please sign in to comment.