1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/cmd/whitesourceExecuteScan.go
Stephan Aßmus ed97142f6d
Pass correct module path to npm.InstallAllDependencies() (#2144)
We are already in the module's directory.
2020-10-09 08:33:36 +02:00

1176 lines
41 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/maven"
"github.com/SAP/jenkins-library/pkg/npm"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/360EntSecGroup-Skylar/excelize/v2"
"github.com/SAP/jenkins-library/pkg/command"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/versioning"
ws "github.com/SAP/jenkins-library/pkg/whitesource"
)
// just to make the lines less long
type ScanOptions = whitesourceExecuteScanOptions
// whitesource defines the functions that are expected by the step implementation to
// be available from the whitesource system.
type whitesource interface {
GetProductByName(productName string) (ws.Product, error)
GetProjectsMetaInfo(productToken string) ([]ws.Project, error)
GetProjectToken(productToken, projectName string) (string, error)
GetProjectByToken(projectToken string) (ws.Project, error)
GetProjectRiskReport(projectToken string) ([]byte, error)
GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error)
GetProjectAlerts(projectToken string) ([]ws.Alert, error)
GetProjectLibraryLocations(projectToken string) ([]ws.Library, error)
}
// wsFile defines the method subset we use from os.File
type wsFile interface {
io.Writer
io.StringWriter
io.Closer
}
type whitesourceUtils interface {
Stdout(out io.Writer)
Stderr(err io.Writer)
RunExecutable(executable string, params ...string) error
DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
Chdir(path string) error
Getwd() (string, error)
MkdirAll(path string, perm os.FileMode) error
FileExists(path string) (bool, error)
FileRead(path string) ([]byte, error)
FileWrite(path string, content []byte, perm os.FileMode) error
FileRemove(path string) error
FileRename(oldPath, newPath string) error
RemoveAll(path string) error
FileOpen(name string, flag int, perm os.FileMode) (wsFile, error)
GetArtifactCoordinates(buildTool, buildDescriptorFile string,
options *versioning.Options) (versioning.Coordinates, error)
FindPackageJSONFiles(config *ScanOptions) ([]string, error)
InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error
Now() time.Time
}
type whitesourceUtilsBundle struct {
*piperhttp.Client
*command.Command
*piperutils.Files
npmExecutor npm.Executor
}
func (w *whitesourceUtilsBundle) FileOpen(name string, flag int, perm os.FileMode) (wsFile, error) {
return os.OpenFile(name, flag, perm)
}
func (w *whitesourceUtilsBundle) GetArtifactCoordinates(buildTool, buildDescriptorFile string,
options *versioning.Options) (versioning.Coordinates, error) {
artifact, err := versioning.GetArtifact(buildTool, buildDescriptorFile, options, w)
if err != nil {
return nil, err
}
return artifact.GetCoordinates()
}
func (w *whitesourceUtilsBundle) getNpmExecutor(config *ScanOptions) npm.Executor {
if w.npmExecutor == nil {
w.npmExecutor = npm.NewExecutor(npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry})
}
return w.npmExecutor
}
func (w *whitesourceUtilsBundle) FindPackageJSONFiles(config *ScanOptions) ([]string, error) {
return w.getNpmExecutor(config).FindPackageJSONFilesWithExcludes(config.BuildDescriptorExcludeList)
}
func (w *whitesourceUtilsBundle) InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error {
return w.getNpmExecutor(config).InstallAllDependencies(packageJSONFiles)
}
func (w *whitesourceUtilsBundle) Now() time.Time {
return time.Now()
}
func newWhitesourceUtils() *whitesourceUtilsBundle {
utils := whitesourceUtilsBundle{
Client: &piperhttp.Client{},
Command: &command.Command{},
Files: &piperutils.Files{},
}
// Reroute cmd output to logging framework
utils.Stdout(log.Writer())
utils.Stderr(log.Writer())
return &utils
}
// whitesourceScan stores information about scanned projects
type whitesourceScan struct {
productToken string
aggregateProjectName string
productVersion string
scannedProjects map[string]ws.Project
scanTimes map[string]time.Time
}
func (s *whitesourceScan) init() {
if s.scannedProjects == nil {
s.scannedProjects = make(map[string]ws.Project)
}
if s.scanTimes == nil {
s.scanTimes = make(map[string]time.Time)
}
}
// appendScannedProject checks that no whitesource.Project is already contained in the list of scanned projects,
// and appends a new whitesource.Project with the given name.
func (s *whitesourceScan) appendScannedProject(moduleName string) error {
s.init()
projectName := moduleName + " - " + s.productVersion
_, exists := s.scannedProjects[projectName]
if exists {
log.Entry().Errorf("A module with the name '%s' was already scanned. "+
"Your project's modules must have unique names.", moduleName)
return fmt.Errorf("project with name '%s' was already scanned", moduleName)
}
s.scannedProjects[projectName] = ws.Project{Name: projectName}
s.scanTimes[projectName] = time.Now()
return nil
}
func (s *whitesourceScan) updateProjects(sys whitesource) error {
s.init()
projects, err := sys.GetProjectsMetaInfo(s.productToken)
if err != nil {
return fmt.Errorf("failed to retrieve WhiteSource projects meta info: %w", err)
}
var projectsToUpdate []string
for projectName := range s.scannedProjects {
projectsToUpdate = append(projectsToUpdate, projectName)
}
for _, project := range projects {
_, exists := s.scannedProjects[project.Name]
if exists {
s.scannedProjects[project.Name] = project
projectsToUpdate, _ = piperutils.RemoveAll(projectsToUpdate, project.Name)
}
}
if len(projectsToUpdate) != 0 {
log.Entry().Warnf("Could not fetch metadata for projects %v", projectsToUpdate)
}
return nil
}
func newWhitesourceScan(config *ScanOptions) *whitesourceScan {
return &whitesourceScan{
aggregateProjectName: config.ProjectName,
productVersion: config.ProductVersion,
}
}
func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData) {
utils := newWhitesourceUtils()
scan := newWhitesourceScan(&config)
sys := ws.NewSystem(config.ServiceURL, config.OrgToken, config.UserToken)
err := runWhitesourceExecuteScan(&config, scan, utils, sys)
if err != nil {
log.Entry().WithError(err).Fatal("step execution failed")
}
}
func runWhitesourceExecuteScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
if err := resolveProjectIdentifiers(config, scan, utils, sys); err != nil {
return fmt.Errorf("failed to resolve project identifiers: %w", err)
}
if config.AggregateVersionWideReport {
// Generate a vulnerability report for all projects with version = config.ProjectVersion
// Note that this is not guaranteed that all projects are from the same scan.
// For example, if a module was removed from the source code, the project may still
// exist in the WhiteSource system.
if err := aggregateVersionWideLibraries(config, utils, sys); err != nil {
return fmt.Errorf("failed to aggregate version wide libraries: %w", err)
}
if err := aggregateVersionWideVulnerabilities(config, utils, sys); err != nil {
return fmt.Errorf("failed to aggregate version wide vulnerabilities: %w", err)
}
} else {
if err := runWhitesourceScan(config, scan, utils, sys); err != nil {
return fmt.Errorf("failed to execute WhiteSource scan: %w", err)
}
}
return nil
}
func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
// Start the scan
if err := executeScan(config, scan, utils); err != nil {
return err
}
// Could perhaps use scan.updateProjects(sys) directly... have not investigated what could break
if err := resolveProjectIdentifiers(config, scan, utils, sys); err != nil {
return err
}
log.Entry().Info("-----------------------------------------------------")
log.Entry().Infof("Product Version: '%s'", config.ProductVersion)
log.Entry().Info("Scanned projects:")
for _, project := range scan.scannedProjects {
log.Entry().Infof(" Name: '%s', token: %s", project.Name, project.Token)
}
log.Entry().Info("-----------------------------------------------------")
if err := checkAndReportScanResults(config, scan, utils, sys); err != nil {
return err
}
if err := persistScannedProjects(config, scan, utils); err != nil {
return fmt.Errorf("failed to persist scanned WhiteSource project names: %w", err)
}
return nil
}
func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
if !config.Reporting && !config.SecurityVulnerabilities {
return nil
}
if err := blockUntilReportsAreaReady(config, scan, sys); err != nil {
return err
}
if config.Reporting {
paths, err := downloadReports(config, scan, utils, sys)
if err != nil {
return err
}
piperutils.PersistReportsAndLinks("whitesourceExecuteScan", "", nil, paths)
}
if config.SecurityVulnerabilities {
if err := checkSecurityViolations(config, scan, sys); err != nil {
return err
}
}
return nil
}
func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
if scan.aggregateProjectName == "" || config.ProductVersion == "" {
options := &versioning.Options{
ProjectSettingsFile: config.ProjectSettingsFile,
GlobalSettingsFile: config.GlobalSettingsFile,
M2Path: config.M2Path,
}
coordinates, err := utils.GetArtifactCoordinates(config.BuildTool, config.BuildDescriptorFile, options)
if err != nil {
return fmt.Errorf("failed to get build artifact description: %w", err)
}
nameTmpl := `{{list .GroupID .ArtifactID | join "-" | trimAll "-"}}`
name, version := versioning.DetermineProjectCoordinates(nameTmpl, config.VersioningModel, coordinates)
if scan.aggregateProjectName == "" {
log.Entry().Infof("Resolved project name '%s' from descriptor file", name)
scan.aggregateProjectName = name
}
if config.ProductVersion == "" {
log.Entry().Infof("Resolved product version '%s' from descriptor file with versioning '%s'",
version, config.VersioningModel)
config.ProductVersion = version
}
}
scan.productVersion = config.ProductVersion
// Get product token if user did not specify one at runtime
if config.ProductToken == "" {
log.Entry().Infof("Attempting to resolve product token for product '%s'..", config.ProductName)
product, err := sys.GetProductByName(config.ProductName)
if err != nil {
return err
}
log.Entry().Infof("Resolved product token: '%s'..", product.Token)
config.ProductToken = product.Token
}
scan.productToken = config.ProductToken
// Get project token if user did not specify one at runtime
if config.ProjectToken == "" && config.ProjectName != "" {
log.Entry().Infof("Attempting to resolve project token for project '%s'..", config.ProjectName)
fullProjName := fmt.Sprintf("%s - %s", config.ProjectName, config.ProductVersion)
projectToken, err := sys.GetProjectToken(config.ProductToken, fullProjName)
if err != nil {
return err
}
// A project may not yet exist for this project name-version combo
// It will be created by the scan, we retrieve the token again after scanning.
if projectToken != "" {
log.Entry().Infof("Resolved project token: '%s'..", projectToken)
config.ProjectToken = projectToken
} else {
log.Entry().Infof("Project '%s' not yet present in WhiteSource", fullProjName)
}
}
return scan.updateProjects(sys)
}
// executeScan executes different types of scans depending on the scanType parameter.
// The default is to download the Unified Agent and use it to perform the scan.
func executeScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
if config.ScanType == "" {
config.ScanType = config.BuildTool
}
switch config.ScanType {
case "mta":
// Execute scan for maven and all npm modules
if err := executeMTAScan(config, scan, utils); err != nil {
return err
}
case "maven":
// Execute scan with maven plugin goal
if err := executeMavenScan(config, scan, utils); err != nil {
return err
}
case "npm":
// Execute scan with in each npm module using npm.Executor
if err := executeNpmScan(config, scan, utils); err != nil {
return err
}
case "yarn":
// Execute scan with whitesource yarn plugin
if err := executeYarnScan(config, scan, utils); err != nil {
return err
}
default:
// Execute scan with Unified Agent jar file
if err := executeUAScan(config, scan, utils); err != nil {
return err
}
}
return nil
}
// executeUAScan executes a scan with the Whitesource Unified Agent.
func executeUAScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
// Download the unified agent jar file if one does not exist
if err := downloadAgent(config, utils); err != nil {
return err
}
// Auto generate a config file based on the working directory's contents.
// TODO/NOTE: Currently this scans the UA jar file as a dependency since it is downloaded beforehand
if err := autoGenerateWhitesourceConfig(config, utils); err != nil {
return err
}
return utils.RunExecutable("java", "-jar", config.AgentFileName, "-d", ".", "-c", config.ConfigFilePath,
"-apiKey", config.OrgToken, "-userKey", config.UserToken, "-project", scan.aggregateProjectName,
"-product", config.ProductName, "-productVersion", config.ProductVersion)
}
// executeMTAScan executes a scan for the Java part with maven, and performs a scan for each NPM module.
func executeMTAScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
log.Entry().Infof("Executing Whitesource scan for MTA project")
pomExists, _ := utils.FileExists("pom.xml")
if pomExists {
if err := executeMavenScanForPomFile(config, scan, utils, "pom.xml"); err != nil {
return err
}
}
modules, err := utils.FindPackageJSONFiles(config)
if err != nil {
return err
}
if len(modules) > 0 {
if err := executeNpmScan(config, scan, utils); err != nil {
return err
}
}
if !pomExists && len(modules) == 0 {
return fmt.Errorf("neither Maven nor NPM modules found, no scan performed")
}
return nil
}
// executeMavenScan constructs maven parameters from the given configuration, and executes the maven goal
// "org.whitesource:whitesource-maven-plugin:19.5.1:update".
func executeMavenScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
log.Entry().Infof("Using Whitesource scan for Maven project")
pomPath := config.BuildDescriptorFile
if pomPath == "" {
pomPath = "pom.xml"
}
return executeMavenScanForPomFile(config, scan, utils, pomPath)
}
func executeMavenScanForPomFile(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, pomPath string) error {
pomExists, _ := utils.FileExists(pomPath)
if !pomExists {
return fmt.Errorf("for scanning with type '%s', the file '%s' must exist in the project root",
config.ScanType, pomPath)
}
defines := generateMavenWhitesourceDefines(config)
flags, excludes := generateMavenWhitesourceFlags(config, utils)
err := appendModulesThatWillBeScanned(scan, utils, excludes)
if err != nil {
return fmt.Errorf("failed to determine maven modules which will be scanned: %w", err)
}
_, err = maven.Execute(&maven.ExecuteOptions{
PomPath: pomPath,
M2Path: config.M2Path,
GlobalSettingsFile: config.GlobalSettingsFile,
ProjectSettingsFile: config.ProjectSettingsFile,
Defines: defines,
Flags: flags,
Goals: []string{"org.whitesource:whitesource-maven-plugin:19.5.1:update"},
}, utils)
return err
}
func generateMavenWhitesourceDefines(config *ScanOptions) []string {
defines := []string{
"-Dorg.whitesource.orgToken=" + config.OrgToken,
"-Dorg.whitesource.product=" + config.ProductName,
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
}
// Aggregate all modules into one WhiteSource project, if user specified the 'projectName' parameter.
if config.ProjectName != "" {
defines = append(defines, "-Dorg.whitesource.aggregateProjectName="+config.ProjectName)
defines = append(defines, "-Dorg.whitesource.aggregateModules=true")
}
if config.UserToken != "" {
defines = append(defines, "-Dorg.whitesource.userKey="+config.UserToken)
}
if config.ProductVersion != "" {
defines = append(defines, "-Dorg.whitesource.productVersion="+config.ProductVersion)
}
return defines
}
func generateMavenWhitesourceFlags(config *ScanOptions, utils whitesourceUtils) (flags []string, excludes []string) {
excludes = config.BuildDescriptorExcludeList
if len(excludes) == 0 {
excludes = []string{
filepath.Join("unit-tests", "pom.xml"),
filepath.Join("integration-tests", "pom.xml"),
filepath.Join("performance-tests", "pom.xml"),
}
}
// From the documentation, these are file paths to a module's pom.xml.
// For MTA projects, we want to support mixing paths to package.json files and pom.xml files.
for _, exclude := range excludes {
if !strings.HasSuffix(exclude, "pom.xml") {
continue
}
exists, _ := utils.FileExists(exclude)
if !exists {
continue
}
moduleName := filepath.Dir(exclude)
if moduleName != "" {
flags = append(flags, "-pl", "!"+moduleName)
}
}
return flags, excludes
}
func appendModulesThatWillBeScanned(scan *whitesourceScan, utils whitesourceUtils, excludes []string) error {
return maven.VisitAllMavenModules(".", utils, excludes, func(info maven.ModuleInfo) error {
project := info.Project
if project.Packaging != "pom" {
if project.ArtifactID == "" {
return fmt.Errorf("artifactId missing from '%s'", info.PomXMLPath)
}
err := scan.appendScannedProject(project.ArtifactID)
if err != nil {
return err
}
}
return nil
})
}
const whiteSourceConfig = "whitesource.config.json"
func setValueAndLogChange(config map[string]interface{}, key string, value interface{}) {
oldValue, exists := config[key]
if exists && oldValue != value {
log.Entry().Infof("overwriting '%s' in %s: %v -> %v", key, whiteSourceConfig, oldValue, value)
}
config[key] = value
}
func setValueOmitIfPresent(config map[string]interface{}, key, omitIfPresent string, value interface{}) {
_, exists := config[omitIfPresent]
if exists {
return
}
setValueAndLogChange(config, key, value)
}
func writeWhitesourceConfigJSON(config *ScanOptions, utils whitesourceUtils, devDep, ignoreLsErrors bool) error {
var npmConfig = make(map[string]interface{})
exists, _ := utils.FileExists(whiteSourceConfig)
if exists {
fileContents, err := utils.FileRead(whiteSourceConfig)
if err != nil {
return fmt.Errorf("file '%s' already exists, but could not be read: %w", whiteSourceConfig, err)
}
err = json.Unmarshal(fileContents, &npmConfig)
if err != nil {
return fmt.Errorf("file '%s' already exists, but could not be parsed: %w", whiteSourceConfig, err)
}
log.Entry().Infof("The file '%s' already exists in the project. Changed config details will be logged.",
whiteSourceConfig)
}
npmConfig["apiKey"] = config.OrgToken
npmConfig["userKey"] = config.UserToken
setValueAndLogChange(npmConfig, "checkPolicies", true)
setValueAndLogChange(npmConfig, "productName", config.ProductName)
setValueAndLogChange(npmConfig, "productVer", config.ProductVersion)
setValueOmitIfPresent(npmConfig, "productToken", "projectToken", config.ProductToken)
if config.ProjectName != "" {
// In case there are other modules (i.e. maven modules in MTA projects),
// or more than one NPM module, setting the project name will lead to
// overwriting any previous scan results with the one from this module!
// If this is not provided, the WhiteSource project name will be generated
// from "name" in package.json plus " - " plus productVersion.
setValueAndLogChange(npmConfig, "projectName", config.ProjectName)
}
setValueAndLogChange(npmConfig, "devDep", devDep)
setValueAndLogChange(npmConfig, "ignoreNpmLsErrors", ignoreLsErrors)
jsonBuffer, err := json.Marshal(npmConfig)
if err != nil {
return fmt.Errorf("failed to generate '%s': %w", whiteSourceConfig, err)
}
err = utils.FileWrite(whiteSourceConfig, jsonBuffer, 0644)
if err != nil {
return fmt.Errorf("failed to write '%s': %w", whiteSourceConfig, err)
}
return nil
}
// executeNpmScan iterates over all found npm modules and performs a scan in each one.
func executeNpmScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
modules, err := utils.FindPackageJSONFiles(config)
if err != nil {
return fmt.Errorf("failed to find package.json files with excludes: %w", err)
}
if len(modules) == 0 {
return fmt.Errorf("found no NPM modules to scan. Configured excludes: %v",
config.BuildDescriptorExcludeList)
}
for _, module := range modules {
err := executeNpmScanForModule(module, config, scan, utils)
if err != nil {
return fmt.Errorf("failed to scan NPM module '%s': %w", module, err)
}
}
return nil
}
// executeNpmScanForModule generates a configuration file whitesource.config.json with appropriate values from config,
// installs all dependencies if necessary, and executes the scan via "npx whitesource run".
func executeNpmScanForModule(modulePath string, config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
log.Entry().Infof("Executing Whitesource scan for NPM module '%s'", modulePath)
resetDir, err := utils.Getwd()
if err != nil {
return fmt.Errorf("failed to obtain current directory: %w", err)
}
dir := filepath.Dir(modulePath)
if err := utils.Chdir(dir); err != nil {
return fmt.Errorf("failed to change into directory '%s': %w", dir, err)
}
defer func() {
err = utils.Chdir(resetDir)
if err != nil {
log.Entry().Errorf("Failed to reset into directory '%s': %v", resetDir, err)
}
}()
if err := writeWhitesourceConfigJSON(config, utils, false, true); err != nil {
return err
}
defer func() { _ = utils.FileRemove(whiteSourceConfig) }()
projectName, err := getNpmProjectName(modulePath, utils)
if err != nil {
return err
}
if err := reinstallNodeModulesIfLsFails(config, utils); err != nil {
return err
}
if err := scan.appendScannedProject(projectName); err != nil {
return err
}
return utils.RunExecutable("npx", "whitesource", "run")
}
func getNpmProjectName(modulePath string, utils whitesourceUtils) (string, error) {
fileContents, err := utils.FileRead("package.json")
if err != nil {
return "", fmt.Errorf("could not read package.json: %w", err)
}
var packageJSON = make(map[string]interface{})
err = json.Unmarshal(fileContents, &packageJSON)
projectNameEntry, exists := packageJSON["name"]
if !exists {
return "", fmt.Errorf("the file '%s' must configure a name",
filepath.Join(modulePath, "package.json"))
}
projectName, isString := projectNameEntry.(string)
if !isString {
return "", fmt.Errorf("the file '%s' must configure a name",
filepath.Join(modulePath, "package.json"))
}
return projectName, nil
}
// reinstallNodeModulesIfLsFails tests running of "npm ls".
// If that fails, the node_modules directory is cleared and the file "package-lock.json" is removed.
// Then "npm install" is performed. Without this, the npm whitesource plugin will consistently hang,
// when encountering npm ls errors, even with "ignoreNpmLsErrors:true" in the configuration.
// The consequence is that what was scanned is not guaranteed to be identical to what was built & deployed.
// This hack/work-around that should be removed once scanning it consistently performed using the Unified Agent.
// A possible reason for encountering "npm ls" errors in the first place is that a different node version
// is used for whitesourceExecuteScan due to a different docker image being used compared to the build stage.
func reinstallNodeModulesIfLsFails(config *ScanOptions, utils whitesourceUtils) error {
// No need to have output from "npm ls" in the log
utils.Stdout(ioutil.Discard)
defer utils.Stdout(log.Writer())
err := utils.RunExecutable("npm", "ls")
if err == nil {
return nil
}
log.Entry().Warnf("'npm ls' failed. Re-installing NPM Node Modules")
err = utils.RemoveAll("node_modules")
if err != nil {
return fmt.Errorf("failed to remove node_modules directory: %w", err)
}
err = utils.MkdirAll("node_modules", os.ModePerm)
if err != nil {
return fmt.Errorf("failed to recreate node_modules directory: %w", err)
}
exists, _ := utils.FileExists("package-lock.json")
if exists {
err = utils.FileRemove("package-lock.json")
if err != nil {
return fmt.Errorf("failed to remove package-lock.json: %w", err)
}
}
// Passing only "package.json", because we are already inside the module's directory.
return utils.InstallAllNPMDependencies(config, []string{"package.json"})
}
// executeYarnScan generates a configuration file whitesource.config.json with appropriate values from config,
// installs whitesource yarn plugin and executes the scan.
func executeYarnScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
// To stay compatible with what the step was doing before, trigger aggregation, although
// there is a great chance that it doesn't work with yarn the same way it doesn't with npm.
// Maybe the yarn code-path should be removed, and only npm stays.
config.ProjectName = scan.aggregateProjectName
if err := writeWhitesourceConfigJSON(config, utils, true, false); err != nil {
return err
}
defer func() { _ = utils.FileRemove(whiteSourceConfig) }()
if err := utils.RunExecutable("yarn", "global", "add", "whitesource"); err != nil {
return err
}
if err := utils.RunExecutable("yarn", "install"); err != nil {
return err
}
if err := utils.RunExecutable("whitesource", "yarn"); err != nil {
return err
}
return nil
}
func checkSecurityViolations(config *ScanOptions, scan *whitesourceScan, sys whitesource) error {
// Check for security vulnerabilities and fail the build if cvssSeverityLimit threshold is crossed
// convert config.CvssSeverityLimit to float64
cvssSeverityLimit, err := strconv.ParseFloat(config.CvssSeverityLimit, 64)
if err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return fmt.Errorf("failed to parse parameter cvssSeverityLimit (%s) "+
"as floating point number: %w", config.CvssSeverityLimit, err)
}
if config.ProjectToken != "" {
project := ws.Project{Name: config.ProjectName, Token: config.ProjectToken}
if err := checkProjectSecurityViolations(cvssSeverityLimit, project, sys); err != nil {
return err
}
} else {
for _, project := range scan.scannedProjects {
if err := checkProjectSecurityViolations(cvssSeverityLimit, project, sys); err != nil {
return err
}
}
}
return nil
}
// checkSecurityViolations checks security violations and returns an error if the configured severity limit is crossed.
func checkProjectSecurityViolations(cvssSeverityLimit float64, project ws.Project, sys whitesource) error {
// get project alerts (vulnerabilities)
alerts, err := sys.GetProjectAlerts(project.Token)
if err != nil {
return fmt.Errorf("failed to retrieve project alerts from Whitesource: %w", err)
}
severeVulnerabilities := 0
// https://github.com/SAP/jenkins-library/blob/master/vars/whitesourceExecuteScan.groovy#L537
for _, alert := range alerts {
vuln := alert.Vulnerability
if (vuln.Score >= cvssSeverityLimit || vuln.CVSS3Score >= cvssSeverityLimit) && cvssSeverityLimit >= 0 {
log.Entry().Infof("Vulnerability with Score %v / CVSS3Score %v treated as severe",
vuln.Score, vuln.CVSS3Score)
severeVulnerabilities++
} else {
log.Entry().Infof("Ignoring vulnerability with Score %v / CVSS3Score %v",
vuln.Score, vuln.CVSS3Score)
}
}
//https://github.com/SAP/jenkins-library/blob/master/vars/whitesourceExecuteScan.groovy#L547
nonSevereVulnerabilities := len(alerts) - severeVulnerabilities
if nonSevereVulnerabilities > 0 {
log.Entry().Warnf("WARNING: %v Open Source Software Security vulnerabilities with "+
"CVSS score below threshold %.1f detected in project %s.", nonSevereVulnerabilities,
cvssSeverityLimit, project.Name)
} else if len(alerts) == 0 {
log.Entry().Infof("No Open Source Software Security vulnerabilities detected in project %s",
project.Name)
}
// https://github.com/SAP/jenkins-library/blob/master/vars/whitesourceExecuteScan.groovy#L558
if severeVulnerabilities > 0 {
return fmt.Errorf("%v Open Source Software Security vulnerabilities with CVSS score greater "+
"or equal to %.1f detected in project %s",
severeVulnerabilities, cvssSeverityLimit, project.Name)
}
return nil
}
func blockUntilReportsAreaReady(config *ScanOptions, scan *whitesourceScan, sys whitesource) error {
// Project was scanned. We need to wait for WhiteSource backend to propagate the changes
// before downloading any reports or check security vulnerabilities.
if config.ProjectToken != "" {
// Poll status of aggregated project
if err := pollProjectStatus(config.ProjectToken, time.Now(), sys); err != nil {
return err
}
} else {
// Poll status of all scanned projects
for key, project := range scan.scannedProjects {
if err := pollProjectStatus(project.Token, scan.scanTimes[key], sys); err != nil {
return err
}
}
}
return nil
}
// pollProjectStatus polls project LastUpdateDate until it reflects the most recent scan
func pollProjectStatus(projectToken string, scanTime time.Time, sys whitesource) error {
return blockUntilProjectIsUpdated(projectToken, sys, scanTime, 20*time.Second, 20*time.Second, 15*time.Minute)
}
const whitesourceDateTimeLayout = "2006-01-02 15:04:05 -0700"
// blockUntilProjectIsUpdated polls the project LastUpdateDate until it is newer than the given time stamp
// or no older than maxAge relative to the given time stamp.
func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTime time.Time, maxAge, timeBetweenPolls, maxWaitTime time.Duration) error {
startTime := time.Now()
for {
project, err := sys.GetProjectByToken(projectToken)
if err != nil {
return err
}
if project.LastUpdateDate == "" {
log.Entry().Infof("last updated time missing from project metadata, retrying")
} else {
lastUpdatedTime, err := time.Parse(whitesourceDateTimeLayout, project.LastUpdateDate)
if err != nil {
return fmt.Errorf("failed to parse last updated time (%s) of Whitesource project: %w",
project.LastUpdateDate, err)
}
age := currentTime.Sub(lastUpdatedTime)
if age < maxAge {
//done polling
break
}
log.Entry().Infof("time since project was last updated %v > %v, polling status...", age, maxAge)
}
if time.Now().Sub(startTime) > maxWaitTime {
return fmt.Errorf("timeout while waiting for Whitesource scan results to be reflected in service")
}
time.Sleep(timeBetweenPolls)
}
return nil
}
// downloadReports downloads a project's risk and vulnerability reports
func downloadReports(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) ([]piperutils.Path, error) {
if err := utils.MkdirAll(config.ReportDirectoryName, os.ModePerm); err != nil {
return nil, err
}
var paths []piperutils.Path
if config.ProjectName != "" {
aggregateProject := ws.Project{Token: config.ProjectToken, Name: config.ProjectName}
vulnPath, err := downloadVulnerabilityReport(config, aggregateProject, utils, sys)
if err != nil {
return nil, err
}
riskPath, err := downloadRiskReport(config, aggregateProject, utils, sys)
if err != nil {
return nil, err
}
paths = append(paths, *vulnPath, *riskPath)
} else {
for _, project := range scan.scannedProjects {
vulnPath, err := downloadVulnerabilityReport(config, project, utils, sys)
if err != nil {
return nil, err
}
riskPath, err := downloadRiskReport(config, project, utils, sys)
if err != nil {
return nil, err
}
paths = append(paths, *vulnPath, *riskPath)
}
}
return paths, nil
}
func downloadVulnerabilityReport(config *ScanOptions, project ws.Project, utils whitesourceUtils, sys whitesource) (*piperutils.Path, error) {
reportBytes, err := sys.GetProjectVulnerabilityReport(project.Token, config.VulnerabilityReportFormat)
if err != nil {
return nil, err
}
// Write report to file
rptFileName := fmt.Sprintf("%s-vulnerability-report.%s", project.Name, config.VulnerabilityReportFormat)
rptFileName = filepath.Join(config.ReportDirectoryName, rptFileName)
if err := utils.FileWrite(rptFileName, reportBytes, 0644); err != nil {
return nil, err
}
log.Entry().Infof("Successfully downloaded vulnerability report to %s", rptFileName)
pathName := fmt.Sprintf("%s Vulnerability Report", project.Name)
return &piperutils.Path{Name: pathName, Target: rptFileName}, nil
}
func downloadRiskReport(config *ScanOptions, project ws.Project, utils whitesourceUtils, sys whitesource) (*piperutils.Path, error) {
reportBytes, err := sys.GetProjectRiskReport(project.Token)
if err != nil {
return nil, err
}
rptFileName := fmt.Sprintf("%s-risk-report.pdf", project.Name)
rptFileName = filepath.Join(config.ReportDirectoryName, rptFileName)
if err := utils.FileWrite(rptFileName, reportBytes, 0644); err != nil {
return nil, err
}
log.Entry().Infof("Successfully downloaded risk report to %s", rptFileName)
pathName := fmt.Sprintf("%s PDF Risk Report", project.Name)
return &piperutils.Path{Name: pathName, Target: rptFileName}, nil
}
// downloadAgent downloads the unified agent jar file if one does not exist
func downloadAgent(config *ScanOptions, utils whitesourceUtils) error {
agentFile := config.AgentFileName
exists, err := utils.FileExists(agentFile)
if err != nil {
return fmt.Errorf("could not check whether the file '%s' exists: %w", agentFile, err)
}
if !exists {
err := utils.DownloadFile(config.AgentDownloadURL, agentFile, nil, nil)
if err != nil {
return fmt.Errorf("failed to download unified agent from URL '%s' to file '%s': %w",
config.AgentDownloadURL, agentFile, err)
}
}
return nil
}
// autoGenerateWhitesourceConfig
// Auto generate a config file based on the current directory structure, renames it to user specified configFilePath
// Generated file name will be 'wss-generated-file.config'
func autoGenerateWhitesourceConfig(config *ScanOptions, utils whitesourceUtils) error {
// TODO: Should we rely on -detect, or set the parameters manually?
if err := utils.RunExecutable("java", "-jar", config.AgentFileName, "-d", ".", "-detect"); err != nil {
return err
}
// Rename generated config file to config.ConfigFilePath parameter
if err := utils.FileRename("wss-generated-file.config", config.ConfigFilePath); err != nil {
return err
}
// Append aggregateModules=true parameter to config file (consolidates multi-module projects into one)
f, err := utils.FileOpen(config.ConfigFilePath, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
// Append additional config parameters to prevent multiple projects being generated
m2Path := config.M2Path
if m2Path == "" {
m2Path = ".m2"
}
cfg := fmt.Sprintf("\ngradle.aggregateModules=true\nmaven.aggregateModules=true\ngradle.localRepositoryPath=.gradle\nmaven.m2RepositoryPath=%s\nexcludes=%s",
m2Path,
config.Excludes)
if _, err = f.WriteString(cfg); err != nil {
return err
}
// archiveExtractionDepth=0
if err := utils.RunExecutable("sed", "-ir", `s/^[#]*\s*archiveExtractionDepth=.*/archiveExtractionDepth=0/`,
config.ConfigFilePath); err != nil {
return err
}
// config.Includes defaults to "**/*.java **/*.jar **/*.py **/*.go **/*.js **/*.ts"
regex := fmt.Sprintf(`s/^[#]*\s*includes=.*/includes="%s"/`, config.Includes)
if err := utils.RunExecutable("sed", "-ir", regex, config.ConfigFilePath); err != nil {
return err
}
return nil
}
func aggregateVersionWideLibraries(config *ScanOptions, utils whitesourceUtils, sys whitesource) error {
log.Entry().Infof("Aggregating list of libraries used for all projects with version: %s", config.ProductVersion)
projects, err := sys.GetProjectsMetaInfo(config.ProductToken)
if err != nil {
return err
}
versionWideLibraries := map[string][]ws.Library{} // maps project name to slice of libraries
for _, project := range projects {
projectVersion := strings.Split(project.Name, " - ")[1]
projectName := strings.Split(project.Name, " - ")[0]
if projectVersion == config.ProductVersion {
libs, err := sys.GetProjectLibraryLocations(project.Token)
if err != nil {
return err
}
log.Entry().Infof("Found project: %s with %v libraries.", project.Name, len(libs))
versionWideLibraries[projectName] = libs
}
}
if err := newLibraryCSVReport(versionWideLibraries, config, utils); err != nil {
return err
}
return nil
}
func aggregateVersionWideVulnerabilities(config *ScanOptions, utils whitesourceUtils, sys whitesource) error {
log.Entry().Infof("Aggregating list of vulnerabilities for all projects with version: %s", config.ProductVersion)
projects, err := sys.GetProjectsMetaInfo(config.ProductToken)
if err != nil {
return err
}
var versionWideAlerts []ws.Alert // all alerts for a given project version
projectNames := `` // holds all project tokens considered a part of the report for debugging
for _, project := range projects {
projectVersion := strings.Split(project.Name, " - ")[1]
if projectVersion == config.ProductVersion {
projectNames += project.Name + "\n"
alerts, err := sys.GetProjectAlerts(project.Token)
if err != nil {
return err
}
log.Entry().Infof("Found project: %s with %v vulnerabilities.", project.Name, len(alerts))
versionWideAlerts = append(versionWideAlerts, alerts...)
}
}
reportPath := filepath.Join(config.ReportDirectoryName, "project-names-aggregated.txt")
if err := utils.FileWrite(reportPath, []byte(projectNames), 0644); err != nil {
return err
}
if err := newVulnerabilityExcelReport(versionWideAlerts, config, utils); err != nil {
return err
}
return nil
}
const wsReportTimeStampLayout = "20060102-150405"
// outputs an slice of alerts to an excel file
func newVulnerabilityExcelReport(alerts []ws.Alert, config *ScanOptions, utils whitesourceUtils) error {
file := excelize.NewFile()
streamWriter, err := file.NewStreamWriter("Sheet1")
if err != nil {
return err
}
styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`)
if err != nil {
return err
}
if err := fillVulnerabilityExcelReport(alerts, streamWriter, styleID); err != nil {
return err
}
if err := streamWriter.Flush(); err != nil {
return err
}
if err := utils.MkdirAll(config.ReportDirectoryName, 0777); err != nil {
return err
}
fileName := filepath.Join(config.ReportDirectoryName,
fmt.Sprintf("vulnerabilities-%s.xlsx", utils.Now().Format(wsReportTimeStampLayout)))
stream, err := utils.FileOpen(fileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
return err
}
if err := file.Write(stream); err != nil {
return err
}
return nil
}
func fillVulnerabilityExcelReport(alerts []ws.Alert, streamWriter *excelize.StreamWriter, styleID int) error {
rows := []struct {
axis string
title string
}{
{"A1", "Severity"},
{"B1", "Library"},
{"C1", "Vulnerability ID"},
{"D1", "Project"},
{"E1", "Resolution"},
}
for _, row := range rows {
err := streamWriter.SetRow(row.axis, []interface{}{excelize.Cell{StyleID: styleID, Value: row.title}})
if err != nil {
return err
}
}
for i, alert := range alerts {
row := make([]interface{}, 5)
vuln := alert.Vulnerability
row[0] = vuln.Severity
row[1] = alert.Library.Filename
row[2] = vuln.Level
row[3] = alert.Project
row[4] = vuln.FixResolutionText
cell, _ := excelize.CoordinatesToCellName(1, i+2)
if err := streamWriter.SetRow(cell, row); err != nil {
log.Entry().Errorf("failed to write alert row: %v", err)
}
}
return nil
}
// outputs an slice of libraries to an excel file based on projects with version == config.ProductVersion
func newLibraryCSVReport(libraries map[string][]ws.Library, config *ScanOptions, utils whitesourceUtils) error {
output := "Library Name, Project Name\n"
for projectName, libraries := range libraries {
log.Entry().Infof("Writing %v libraries for project %s to excel report..", len(libraries), projectName)
for _, library := range libraries {
output += library.Name + ", " + projectName + "\n"
}
}
// Ensure reporting directory exists
if err := utils.MkdirAll(config.ReportDirectoryName, 0777); err != nil {
return err
}
// Write result to file
fileName := fmt.Sprintf("%s/libraries-%s.csv", config.ReportDirectoryName,
utils.Now().Format(wsReportTimeStampLayout))
if err := utils.FileWrite(fileName, []byte(output), 0777); err != nil {
return err
}
return nil
}
// persistScannedProjects writes all actually scanned WhiteSource project names as comma separated
// string into the Common Pipeline Environment, from where it can be used by sub-sequent steps.
func persistScannedProjects(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils) error {
var projectNames []string
if config.ProjectName != "" {
projectNames = []string{config.ProjectName + " - " + config.ProductVersion}
} else {
for projectName := range scan.scannedProjects {
projectNames = append(projectNames, projectName)
}
// Sorting helps the list become stable across pipeline runs (and in the unit tests),
// as the order in which we travers map keys is not deterministic.
sort.Strings(projectNames)
}
resourceDir := filepath.Join(".pipeline", "commonPipelineEnvironment", "custom")
if err := utils.MkdirAll(resourceDir, 0755); err != nil {
return err
}
fileContents := strings.Join(projectNames, ",")
resource := filepath.Join(resourceDir, "whitesourceProjectNames")
if err := utils.FileWrite(resource, []byte(fileContents), 0644); err != nil {
return err
}
return nil
}