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

Skip install in dependencies map calculation if requested #277

Merged
merged 6 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading