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 }