github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/whitesource/scanNPM.go (about)

     1  package whitesource
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/SAP/jenkins-library/pkg/log"
    11  	"github.com/pkg/errors"
    12  )
    13  
    14  const whiteSourceConfig = "whitesource.config.json"
    15  
    16  func setValueAndLogChange(config map[string]interface{}, key string, value interface{}) {
    17  	oldValue, exists := config[key]
    18  	if exists && oldValue != value {
    19  		log.Entry().Infof("overwriting '%s' in %s: %v -> %v", key, whiteSourceConfig, oldValue, value)
    20  	}
    21  	config[key] = value
    22  }
    23  
    24  func setValueOmitIfPresent(config map[string]interface{}, key, omitIfPresent string, value interface{}) {
    25  	_, exists := config[omitIfPresent]
    26  	if exists {
    27  		return
    28  	}
    29  	setValueAndLogChange(config, key, value)
    30  }
    31  
    32  // writeWhitesourceConfigJSON creates or merges the file whitesource.config.json in the current
    33  // directory from the given NPMScanOptions.
    34  func (s *Scan) writeWhitesourceConfigJSON(config *ScanOptions, utils Utils, devDep, ignoreLsErrors bool) error {
    35  	var npmConfig = make(map[string]interface{})
    36  
    37  	exists, _ := utils.FileExists(whiteSourceConfig)
    38  	if exists {
    39  		fileContents, err := utils.FileRead(whiteSourceConfig)
    40  		if err != nil {
    41  			return fmt.Errorf("file '%s' already exists, but could not be read: %w", whiteSourceConfig, err)
    42  		}
    43  		err = json.Unmarshal(fileContents, &npmConfig)
    44  		if err != nil {
    45  			return fmt.Errorf("file '%s' already exists, but could not be parsed: %w", whiteSourceConfig, err)
    46  		}
    47  		log.Entry().Infof("The file '%s' already exists in the project. Changed config details will be logged.",
    48  			whiteSourceConfig)
    49  	}
    50  
    51  	npmConfig["apiKey"] = config.OrgToken
    52  	npmConfig["userKey"] = config.UserToken
    53  	setValueAndLogChange(npmConfig, "checkPolicies", true)
    54  	// When checkPolicies detects any violations, it will by default not update the WS project in the backend.
    55  	// Therefore we also need "forceUpdate".
    56  	setValueAndLogChange(npmConfig, "forceUpdate", true)
    57  	setValueAndLogChange(npmConfig, "productName", config.ProductName)
    58  	setValueAndLogChange(npmConfig, "productVer", s.ProductVersion)
    59  	setValueOmitIfPresent(npmConfig, "productToken", "projectToken", config.ProductToken)
    60  	if config.ProjectName != "" {
    61  		// In case there are other modules (i.e. maven modules in MTA projects),
    62  		// or more than one NPM module, setting the project name will lead to
    63  		// overwriting any previous scan results with the one from this module!
    64  		// If this is not provided, the WhiteSource project name will be generated
    65  		// from "name" in package.json plus " - " plus productVersion.
    66  		setValueAndLogChange(npmConfig, "projectName", config.ProjectName)
    67  	}
    68  	setValueAndLogChange(npmConfig, "devDep", devDep)
    69  	setValueAndLogChange(npmConfig, "ignoreNpmLsErrors", ignoreLsErrors)
    70  
    71  	jsonBuffer, err := json.Marshal(npmConfig)
    72  	if err != nil {
    73  		return fmt.Errorf("failed to generate '%s': %w", whiteSourceConfig, err)
    74  	}
    75  
    76  	err = utils.FileWrite(whiteSourceConfig, jsonBuffer, 0644)
    77  	if err != nil {
    78  		return fmt.Errorf("failed to write '%s': %w", whiteSourceConfig, err)
    79  	}
    80  	return nil
    81  }
    82  
    83  // ExecuteNpmScan iterates over all found npm modules and performs a scan in each one.
    84  func (s *Scan) ExecuteNpmScan(config *ScanOptions, utils Utils) error {
    85  	s.AgentName = "WhiteSource NPM Plugin"
    86  	s.AgentVersion = "unknown"
    87  	modules, err := utils.FindPackageJSONFiles(config)
    88  	if err != nil {
    89  		return fmt.Errorf("failed to find package.json files with excludes: %w", err)
    90  	}
    91  	if len(modules) == 0 {
    92  		return fmt.Errorf("found no NPM modules to scan. Configured excludes: %v",
    93  			config.BuildDescriptorExcludeList)
    94  	}
    95  	for _, module := range modules {
    96  		err := s.executeNpmScanForModule(module, config, utils)
    97  		if err != nil {
    98  			return fmt.Errorf("failed to scan NPM module '%s': %w", module, err)
    99  		}
   100  	}
   101  	return nil
   102  }
   103  
   104  // executeNpmScanForModule generates a configuration file whitesource.config.json with appropriate values from config,
   105  // installs all dependencies if necessary, and executes the scan via "npx whitesource run".
   106  func (s *Scan) executeNpmScanForModule(modulePath string, config *ScanOptions, utils Utils) error {
   107  	log.Entry().Infof("Executing Whitesource scan for NPM module '%s'", modulePath)
   108  
   109  	resetDir, err := utils.Getwd()
   110  	if err != nil {
   111  		return fmt.Errorf("failed to obtain current directory: %w", err)
   112  	}
   113  
   114  	dir := filepath.Dir(modulePath)
   115  	if err := utils.Chdir(dir); err != nil {
   116  		return fmt.Errorf("failed to change into directory '%s': %w", dir, err)
   117  	}
   118  	defer func() {
   119  		err = utils.Chdir(resetDir)
   120  		if err != nil {
   121  			log.Entry().Errorf("Failed to reset into directory '%s': %v", resetDir, err)
   122  		}
   123  	}()
   124  
   125  	if err := s.writeWhitesourceConfigJSON(config, utils, false, true); err != nil {
   126  		return err
   127  	}
   128  	defer func() { _ = utils.FileRemove(whiteSourceConfig) }()
   129  
   130  	projectName, err := getNpmProjectName(modulePath, utils)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	if err := reinstallNodeModulesIfLsFails(config, utils); err != nil {
   136  		return err
   137  	}
   138  
   139  	if err := s.AppendScannedProject(projectName); err != nil {
   140  		return err
   141  	}
   142  
   143  	return utils.RunExecutable("npx", "whitesource", "run")
   144  }
   145  
   146  // getNpmProjectName tries to read a property "name" of type string from the
   147  // package.json file in the current directory and returns an error, if this is not possible.
   148  func getNpmProjectName(modulePath string, utils Utils) (string, error) {
   149  	fileContents, err := utils.FileRead("package.json")
   150  	if err != nil {
   151  		return "", fmt.Errorf("could not read %s: %w", modulePath, err)
   152  	}
   153  	var packageJSON = make(map[string]interface{})
   154  	err = json.Unmarshal(fileContents, &packageJSON)
   155  	if err != nil {
   156  		return "", errors.Wrapf(err, "failed to unmarshall the file '%s'", modulePath)
   157  	}
   158  
   159  	projectNameEntry, exists := packageJSON["name"]
   160  	if !exists {
   161  		return "", fmt.Errorf("the file '%s' must configure a name", modulePath)
   162  	}
   163  
   164  	projectName, isString := projectNameEntry.(string)
   165  	if !isString {
   166  		return "", fmt.Errorf("the file '%s' must configure a name", modulePath)
   167  	}
   168  
   169  	return projectName, nil
   170  }
   171  
   172  // reinstallNodeModulesIfLsFails tests running of "npm ls".
   173  // If that fails, the node_modules directory is cleared and the file "package-lock.json" is removed.
   174  // Then "npm install" is performed. Without this, the npm whitesource plugin will consistently hang,
   175  // when encountering npm ls errors, even with "ignoreNpmLsErrors:true" in the configuration.
   176  // The consequence is that what was scanned is not guaranteed to be identical to what was built & deployed.
   177  // This hack/work-around that should be removed once scanning it consistently performed using the Unified Agent.
   178  // A possible reason for encountering "npm ls" errors in the first place is that a different node version
   179  // is used for whitesourceExecuteScan due to a different docker image being used compared to the build stage.
   180  func reinstallNodeModulesIfLsFails(config *ScanOptions, utils Utils) error {
   181  	// No need to have output from "npm ls" in the log
   182  	utils.Stdout(io.Discard)
   183  	defer utils.Stdout(log.Writer())
   184  
   185  	err := utils.RunExecutable("npm", "ls")
   186  	if err == nil {
   187  		return nil
   188  	}
   189  	log.Entry().Warnf("'npm ls' failed. Re-installing NPM Node Modules")
   190  	err = utils.RemoveAll("node_modules")
   191  	if err != nil {
   192  		return fmt.Errorf("failed to remove node_modules directory: %w", err)
   193  	}
   194  	err = utils.MkdirAll("node_modules", os.ModePerm)
   195  	if err != nil {
   196  		return fmt.Errorf("failed to recreate node_modules directory: %w", err)
   197  	}
   198  	exists, _ := utils.FileExists("package-lock.json")
   199  	if exists {
   200  		err = utils.FileRemove("package-lock.json")
   201  		if err != nil {
   202  			return fmt.Errorf("failed to remove package-lock.json: %w", err)
   203  		}
   204  	}
   205  	// Passing only "package.json", because we are already inside the module's directory.
   206  	return utils.InstallAllNPMDependencies(config, []string{"package.json"})
   207  }
   208  
   209  // ExecuteYarnScan generates a configuration file whitesource.config.json with appropriate values from config,
   210  // installs whitesource yarn plugin and executes the scan.
   211  func (s *Scan) ExecuteYarnScan(config *ScanOptions, utils Utils) error {
   212  	// To stay compatible with what the step was doing before, trigger aggregation, although
   213  	// there is a great chance that it doesn't work with yarn the same way it doesn't with npm.
   214  	// Maybe the yarn code-path should be removed, and only npm stays.
   215  	config.ProjectName = s.AggregateProjectName
   216  	if err := s.writeWhitesourceConfigJSON(config, utils, true, false); err != nil {
   217  		return err
   218  	}
   219  	defer func() { _ = utils.FileRemove(whiteSourceConfig) }()
   220  	if err := utils.RunExecutable("yarn", "global", "add", "whitesource"); err != nil {
   221  		return err
   222  	}
   223  	if err := utils.RunExecutable("yarn", "install"); err != nil {
   224  		return err
   225  	}
   226  	if err := utils.RunExecutable("whitesource", "yarn"); err != nil {
   227  		return err
   228  	}
   229  	return nil
   230  }