1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

whitesourceExecuteScan: Re-organize code between step and whitesource package (#2207)

This commit is contained in:
Stephan Aßmus 2020-10-20 09:49:26 +02:00 committed by GitHub
parent 586044192c
commit 260ca2c5a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1911 additions and 1400 deletions

View File

@ -1,13 +1,8 @@
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"
@ -41,37 +36,12 @@ type whitesource interface {
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)
ws.Utils
GetArtifactCoordinates(buildTool, buildDescriptorFile string,
options *versioning.Options) (versioning.Coordinates, error)
FindPackageJSONFiles(config *ScanOptions) ([]string, error)
InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error
Now() time.Time
}
@ -82,7 +52,7 @@ type whitesourceUtilsBundle struct {
npmExecutor npm.Executor
}
func (w *whitesourceUtilsBundle) FileOpen(name string, flag int, perm os.FileMode) (wsFile, error) {
func (w *whitesourceUtilsBundle) FileOpen(name string, flag int, perm os.FileMode) (ws.File, error) {
return os.OpenFile(name, flag, perm)
}
@ -95,18 +65,18 @@ func (w *whitesourceUtilsBundle) GetArtifactCoordinates(buildTool, buildDescript
return artifact.GetCoordinates()
}
func (w *whitesourceUtilsBundle) getNpmExecutor(config *ScanOptions) npm.Executor {
func (w *whitesourceUtilsBundle) getNpmExecutor(config *ws.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) {
func (w *whitesourceUtilsBundle) FindPackageJSONFiles(config *ws.ScanOptions) ([]string, error) {
return w.getNpmExecutor(config).FindPackageJSONFilesWithExcludes(config.BuildDescriptorExcludeList)
}
func (w *whitesourceUtilsBundle) InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error {
func (w *whitesourceUtilsBundle) InstallAllNPMDependencies(config *ws.ScanOptions, packageJSONFiles []string) error {
return w.getNpmExecutor(config).InstallAllDependencies(packageJSONFiles)
}
@ -126,69 +96,10 @@ func newWhitesourceUtils() *whitesourceUtilsBundle {
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 newWhitesourceScan(config *ScanOptions) *ws.Scan {
return &ws.Scan{
AggregateProjectName: config.ProjectName,
ProductVersion: config.ProductVersion,
}
}
@ -202,7 +113,7 @@ func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData) {
}
}
func runWhitesourceExecuteScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
func runWhitesourceExecuteScan(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource) error {
if err := resolveProjectIdentifiers(config, scan, utils, sys); err != nil {
return fmt.Errorf("failed to resolve project identifiers: %w", err)
}
@ -226,7 +137,7 @@ func runWhitesourceExecuteScan(config *ScanOptions, scan *whitesourceScan, utils
return nil
}
func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
func runWhitesourceScan(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource) error {
// Start the scan
if err := executeScan(config, scan, utils); err != nil {
return err
@ -240,7 +151,7 @@ func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whites
log.Entry().Info("-----------------------------------------------------")
log.Entry().Infof("Product Version: '%s'", config.ProductVersion)
log.Entry().Info("Scanned projects:")
for _, project := range scan.scannedProjects {
for _, project := range scan.ScannedProjects() {
log.Entry().Infof(" Name: '%s', token: %s", project.Name, project.Token)
}
log.Entry().Info("-----------------------------------------------------")
@ -256,7 +167,7 @@ func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whites
return nil
}
func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
func checkAndReportScanResults(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource) error {
if !config.Reporting && !config.SecurityVulnerabilities {
return nil
}
@ -264,7 +175,10 @@ func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils
return err
}
if config.Reporting {
paths, err := downloadReports(config, scan, utils, sys)
paths, err := scan.DownloadReports(ws.ReportOptions{
ReportDirectory: config.ReportDirectoryName,
VulnerabilityReportFormat: config.VulnerabilityReportFormat,
}, utils, sys)
if err != nil {
return err
}
@ -278,8 +192,8 @@ func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils
return nil
}
func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
if scan.aggregateProjectName == "" || config.ProductVersion == "" {
func resolveProjectIdentifiers(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource) error {
if scan.AggregateProjectName == "" || config.ProductVersion == "" {
options := &versioning.Options{
ProjectSettingsFile: config.ProjectSettingsFile,
GlobalSettingsFile: config.GlobalSettingsFile,
@ -292,9 +206,9 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
nameTmpl := `{{list .GroupID .ArtifactID | join "-" | trimAll "-"}}`
name, version := versioning.DetermineProjectCoordinates(nameTmpl, config.VersioningModel, coordinates)
if scan.aggregateProjectName == "" {
if scan.AggregateProjectName == "" {
log.Entry().Infof("Resolved project name '%s' from descriptor file", name)
scan.aggregateProjectName = name
scan.AggregateProjectName = name
}
if config.ProductVersion == "" {
log.Entry().Infof("Resolved product version '%s' from descriptor file with versioning '%s'",
@ -302,7 +216,7 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
config.ProductVersion = version
}
}
scan.productVersion = config.ProductVersion
scan.ProductVersion = validateProductVersion(config.ProductVersion)
// Get product token if user did not specify one at runtime
if config.ProductToken == "" {
@ -314,7 +228,6 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
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 != "" {
@ -334,400 +247,81 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
}
}
return scan.updateProjects(sys)
return scan.UpdateProjects(config.ProductToken, sys)
}
// validateProductVersion makes sure that the version does not contain a dash "-".
func validateProductVersion(version string) string {
// TrimLeft() removes all "-" from the beginning, unlike TrimPrefix()!
version = strings.TrimLeft(version, "-")
if strings.Contains(version, "-") {
version = strings.SplitN(version, "-", 1)[0]
}
return version
}
func wsScanOptions(config *ScanOptions) *ws.ScanOptions {
return &ws.ScanOptions{
ScanType: config.ScanType,
OrgToken: config.OrgToken,
UserToken: config.UserToken,
ProductName: config.ProductName,
ProductToken: config.ProductToken,
ProjectName: config.ProjectName,
BuildDescriptorExcludeList: config.BuildDescriptorExcludeList,
PomPath: config.BuildDescriptorFile,
M2Path: config.M2Path,
GlobalSettingsFile: config.GlobalSettingsFile,
ProjectSettingsFile: config.ProjectSettingsFile,
DefaultNpmRegistry: config.DefaultNpmRegistry,
AgentDownloadURL: config.AgentDownloadURL,
AgentFileName: config.AgentFileName,
ConfigFilePath: config.ConfigFilePath,
Includes: config.Includes,
Excludes: config.Excludes,
}
}
// 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 {
func executeScan(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils) error {
if config.ScanType == "" {
config.ScanType = config.BuildTool
}
options := wsScanOptions(config)
switch config.ScanType {
case "mta":
// Execute scan for maven and all npm modules
if err := executeMTAScan(config, scan, utils); err != nil {
if err := scan.ExecuteMTAScan(options, utils); err != nil {
return err
}
case "maven":
// Execute scan with maven plugin goal
if err := executeMavenScan(config, scan, utils); err != nil {
if err := scan.ExecuteMavenScan(options, 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 {
if err := scan.ExecuteNpmScan(options, utils); err != nil {
return err
}
case "yarn":
// Execute scan with whitesource yarn plugin
if err := executeYarnScan(config, scan, utils); err != nil {
if err := scan.ExecuteYarnScan(options, utils); err != nil {
return err
}
default:
// Execute scan with Unified Agent jar file
if err := executeUAScan(config, scan, utils); err != nil {
if err := scan.ExecuteUAScan(options, 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
// 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(dir, 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 {
func checkSecurityViolations(config *ScanOptions, scan *ws.Scan, 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)
@ -742,7 +336,7 @@ func checkSecurityViolations(config *ScanOptions, scan *whitesourceScan, sys whi
return err
}
} else {
for _, project := range scan.scannedProjects {
for _, project := range scan.ScannedProjects() {
if err := checkProjectSecurityViolations(cvssSeverityLimit, project, sys); err != nil {
return err
}
@ -793,7 +387,7 @@ func checkProjectSecurityViolations(cvssSeverityLimit float64, project ws.Projec
return nil
}
func blockUntilReportsAreaReady(config *ScanOptions, scan *whitesourceScan, sys whitesource) error {
func blockUntilReportsAreaReady(config *ScanOptions, scan *ws.Scan, 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 != "" {
@ -803,8 +397,8 @@ func blockUntilReportsAreaReady(config *ScanOptions, scan *whitesourceScan, sys
}
} else {
// Poll status of all scanned projects
for key, project := range scan.scannedProjects {
if err := pollProjectStatus(project.Token, scan.scanTimes[key], sys); err != nil {
for _, project := range scan.ScannedProjects() {
if err := pollProjectStatus(project.Token, scan.ScanTime(project.Name), sys); err != nil {
return err
}
}
@ -854,140 +448,6 @@ func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTim
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)
@ -1144,13 +604,13 @@ func newLibraryCSVReport(libraries map[string][]ws.Library, config *ScanOptions,
// 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 {
func persistScannedProjects(config *ScanOptions, scan *ws.Scan, 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)
for _, project := range scan.ScannedProjects() {
projectNames = append(projectNames, project.Name)
}
// 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.

View File

@ -1,164 +1,27 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/versioning"
ws "github.com/SAP/jenkins-library/pkg/whitesource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"os"
"path/filepath"
"testing"
"time"
)
type whitesourceSystemMock struct {
productName string
products []ws.Product
projects []ws.Project
alerts []ws.Alert
libraries []ws.Library
riskReport []byte
vulnerabilityReport []byte
}
func (m *whitesourceSystemMock) GetProductByName(productName string) (ws.Product, error) {
for _, product := range m.products {
if product.Name == productName {
return product, nil
}
}
return ws.Product{}, fmt.Errorf("no product with name '%s' found in Whitesource", productName)
}
func (m *whitesourceSystemMock) GetProjectsMetaInfo(productToken string) ([]ws.Project, error) {
return m.projects, nil
}
func (m *whitesourceSystemMock) GetProjectToken(productToken, projectName string) (string, error) {
return "mock-project-token", nil
}
func (m *whitesourceSystemMock) GetProjectByToken(projectToken string) (ws.Project, error) {
for _, project := range m.projects {
if project.Token == projectToken {
return project, nil
}
}
return ws.Project{}, fmt.Errorf("no project with token '%s' found in Whitesource", projectToken)
}
func (m *whitesourceSystemMock) GetProjectRiskReport(projectToken string) ([]byte, error) {
return m.riskReport, nil
}
func (m *whitesourceSystemMock) GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error) {
_, err := m.GetProjectByToken(projectToken)
if err != nil {
return nil, err
}
if m.vulnerabilityReport == nil {
return nil, fmt.Errorf("no report available")
}
return m.vulnerabilityReport, nil
}
func (m *whitesourceSystemMock) GetProjectAlerts(projectToken string) ([]ws.Alert, error) {
return m.alerts, nil
}
func (m *whitesourceSystemMock) GetProjectLibraryLocations(projectToken string) ([]ws.Library, error) {
return m.libraries, nil
}
var mockLibrary = ws.Library{
Name: "mock-library",
Filename: "mock-library-file",
Version: "mock-library-version",
Project: "mock-project - 1",
}
func newWhitesourceSystemMock(lastUpdateDate string) *whitesourceSystemMock {
return &whitesourceSystemMock{
productName: "mock-product",
products: []ws.Product{
{
Name: "mock-product",
Token: "mock-product-token",
CreationDate: "last-thursday",
LastUpdateDate: lastUpdateDate,
},
},
projects: []ws.Project{
{
ID: 42,
Name: "mock-project - 1",
PluginName: "mock-plugin-name",
Token: "mock-project-token",
UploadedBy: "MrBean",
CreationDate: "last-thursday",
LastUpdateDate: lastUpdateDate,
},
},
alerts: []ws.Alert{
{
Vulnerability: ws.Vulnerability{
Name: "something severe",
Score: 5,
},
Library: mockLibrary,
Project: "mock-project - 1",
CreationDate: "last-thursday",
},
},
libraries: []ws.Library{mockLibrary},
riskReport: []byte("mock-risk-report"),
vulnerabilityReport: []byte("mock-vulnerability-report"),
}
}
type whitesourceCoordinatesMock struct {
GroupID string
ArtifactID string
Version string
}
type downloadedFile struct {
sourceURL string
filePath string
}
type npmInstall struct {
currentDir string
packageJSON []string
}
type whitesourceUtilsMock struct {
*mock.FilesMock
*mock.ExecMockRunner
*ws.ScanUtilsMock
coordinates whitesourceCoordinatesMock
usedBuildTool string
usedBuildDescriptorFile string
usedOptions versioning.Options
downloadedFiles []downloadedFile
npmInstalledModules []npmInstall
}
func (w *whitesourceUtilsMock) DownloadFile(url, filename string, _ http.Header, _ []*http.Cookie) error {
w.downloadedFiles = append(w.downloadedFiles, downloadedFile{sourceURL: url, filePath: filename})
return nil
}
func (w *whitesourceUtilsMock) FileOpen(name string, flag int, perm os.FileMode) (wsFile, error) {
return w.Open(name, flag, perm)
}
func (w *whitesourceUtilsMock) RemoveAll(path string) error {
// TODO: Implement in FS Mock
return nil
}
func (w *whitesourceUtilsMock) GetArtifactCoordinates(buildTool, buildDescriptorFile string,
@ -169,19 +32,6 @@ func (w *whitesourceUtilsMock) GetArtifactCoordinates(buildTool, buildDescriptor
return w.coordinates, nil
}
func (w *whitesourceUtilsMock) FindPackageJSONFiles(_ *ScanOptions) ([]string, error) {
matches, _ := w.Glob("**/package.json")
return matches, nil
}
func (w *whitesourceUtilsMock) InstallAllNPMDependencies(_ *ScanOptions, packageJSONs []string) error {
w.npmInstalledModules = append(w.npmInstalledModules, npmInstall{
currentDir: w.CurrentDir,
packageJSON: packageJSONs,
})
return nil
}
const wsTimeNow = "2010-05-10 00:15:42"
func (w *whitesourceUtilsMock) Now() time.Time {
@ -191,8 +41,10 @@ func (w *whitesourceUtilsMock) Now() time.Time {
func newWhitesourceUtilsMock() *whitesourceUtilsMock {
return &whitesourceUtilsMock{
FilesMock: &mock.FilesMock{},
ExecMockRunner: &mock.ExecMockRunner{},
ScanUtilsMock: &ws.ScanUtilsMock{
FilesMock: &mock.FilesMock{},
ExecMockRunner: &mock.ExecMockRunner{},
},
coordinates: whitesourceCoordinatesMock{
GroupID: "mock-group-id",
ArtifactID: "mock-artifact-id",
@ -215,13 +67,13 @@ func TestResolveProjectIdentifiers(t *testing.T) {
GlobalSettingsFile: "global-settings.xml",
}
utilsMock := newWhitesourceUtilsMock()
systemMock := newWhitesourceSystemMock("ignored")
systemMock := ws.NewSystemMock("ignored")
scan := newWhitesourceScan(&config)
// test
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
// assert
if assert.NoError(t, err) {
assert.Equal(t, "mock-group-id-mock-artifact-id", scan.aggregateProjectName)
assert.Equal(t, "mock-group-id-mock-artifact-id", scan.AggregateProjectName)
assert.Equal(t, "1", config.ProductVersion)
assert.Equal(t, "mock-product-token", config.ProductToken)
assert.Equal(t, "mta", utilsMock.usedBuildTool)
@ -238,16 +90,16 @@ func TestResolveProjectIdentifiers(t *testing.T) {
BuildDescriptorFile: "my-mta.yml",
VersioningModel: "major",
ProductName: "mock-product",
ProjectName: "mock-project - 1",
ProjectName: "mock-project",
}
utilsMock := newWhitesourceUtilsMock()
systemMock := newWhitesourceSystemMock("ignored")
systemMock := ws.NewSystemMock("ignored")
scan := newWhitesourceScan(&config)
// test
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
// assert
if assert.NoError(t, err) {
assert.Equal(t, "mock-project - 1", scan.aggregateProjectName)
assert.Equal(t, "mock-project", scan.AggregateProjectName)
assert.Equal(t, "1", config.ProductVersion)
assert.Equal(t, "mock-product-token", config.ProductToken)
assert.Equal(t, "mta", utilsMock.usedBuildTool)
@ -263,7 +115,7 @@ func TestResolveProjectIdentifiers(t *testing.T) {
ProductName: "does-not-exist",
}
utilsMock := newWhitesourceUtilsMock()
systemMock := newWhitesourceSystemMock("ignored")
systemMock := ws.NewSystemMock("ignored")
scan := newWhitesourceScan(&config)
// test
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
@ -272,451 +124,6 @@ func TestResolveProjectIdentifiers(t *testing.T) {
})
}
func TestExecuteScanUA(t *testing.T) {
t.Parallel()
t.Run("happy path UA", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductVersion: "product-version",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
ConfigFilePath: "ua.cfg",
M2Path: ".pipeline/m2",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// many assert
require.NoError(t, err)
content, err := utilsMock.FileRead("ua.cfg")
require.NoError(t, err)
contentAsString := string(content)
assert.Contains(t, contentAsString, "key=value\n")
assert.Contains(t, contentAsString, "gradle.aggregateModules=true\n")
assert.Contains(t, contentAsString, "maven.aggregateModules=true\n")
assert.Contains(t, contentAsString, "maven.m2RepositoryPath=.pipeline/m2\n")
assert.Contains(t, contentAsString, "excludes=")
require.Len(t, utilsMock.Calls, 4)
fmt.Printf("calls: %v\n", utilsMock.Calls)
expectedCall := mock.ExecCall{
Exec: "java",
Params: []string{
"-jar",
config.AgentFileName,
"-d", ".",
"-c", config.ConfigFilePath,
"-apiKey", config.OrgToken,
"-userKey", config.UserToken,
"-project", config.ProjectName,
"-product", config.ProductName,
"-productVersion", config.ProductVersion,
},
}
assert.Equal(t, expectedCall, utilsMock.Calls[3])
})
t.Run("UA is downloaded", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("dummy"))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// many assert
require.NoError(t, err)
require.Len(t, utilsMock.downloadedFiles, 1)
assert.Equal(t, "https://download.ua.org/agent.jar", utilsMock.downloadedFiles[0].sourceURL)
assert.Equal(t, "unified-agent.jar", utilsMock.downloadedFiles[0].filePath)
})
t.Run("UA is NOT downloaded", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("dummy"))
utilsMock.AddFile("unified-agent.jar", []byte("dummy"))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// many assert
require.NoError(t, err)
assert.Len(t, utilsMock.downloadedFiles, 0)
})
}
func TestExecuteScanNPM(t *testing.T) {
config := ScanOptions{
ScanType: "npm",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductVersion: "product-version",
}
t.Parallel()
t.Run("happy path NPM", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
})
t.Run("no NPM modules", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
assert.EqualError(t, err, "found no NPM modules to scan. Configured excludes: []")
assert.Len(t, utilsMock.Calls, 0)
assert.False(t, utilsMock.HasWrittenFile(whiteSourceConfig))
})
t.Run("package.json needs name", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"key":"value"}`))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
assert.EqualError(t, err, "failed to scan NPM module 'package.json': the file 'package.json' must configure a name")
})
t.Run("npm ls fails", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
utilsMock.AddFile(filepath.Join("app", "package.json"), []byte(`{"name":"my-app-module-name"}`))
utilsMock.AddFile("package-lock.json", []byte("dummy"))
utilsMock.ShouldFailOnCommand = make(map[string]error)
utilsMock.ShouldFailOnCommand["npm ls"] = fmt.Errorf("mock failure")
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
assert.NoError(t, err)
expectedNpmInstalls := []npmInstall{
{currentDir: "app", packageJSON: []string{"package.json"}},
{currentDir: "", packageJSON: []string{"package.json"}},
}
assert.Equal(t, expectedNpmInstalls, utilsMock.npmInstalledModules)
assert.True(t, utilsMock.HasRemovedFile("package-lock.json"))
})
}
func TestExecuteScanMaven(t *testing.T) {
t.Parallel()
t.Run("maven modules are aggregated", func(t *testing.T) {
// init
const pomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductVersion: "product-version",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("maven modules are separate projects", func(t *testing.T) {
// init
const rootPomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
<modules>
<module>sub</module>
</modules>
</project>
`
const modulePomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id-sub</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProductVersion: "product-version",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("pom.xml", []byte(rootPomXML))
utilsMock.AddFile(filepath.Join("sub", "pom.xml"), []byte(modulePomXML))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
require.Len(t, scan.scannedProjects, 2)
_, existsRoot := scan.scannedProjects["my-artifact-id - product-version"]
_, existsModule := scan.scannedProjects["my-artifact-id-sub - product-version"]
assert.True(t, existsRoot)
assert.True(t, existsModule)
})
t.Run("pom.xml does not exist", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProductVersion: "product-version",
}
utilsMock := newWhitesourceUtilsMock()
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
assert.EqualError(t, err,
"for scanning with type 'maven', the file 'pom.xml' must exist in the project root")
assert.Len(t, utilsMock.Calls, 0)
})
}
func TestExecuteScanMTA(t *testing.T) {
const pomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
BuildTool: "mta",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductVersion: "product-version",
}
t.Parallel()
t.Run("happy path MTA", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with only maven modules", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.False(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with only NPM modules", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with neither Maven nor NPM modules results in error", func(t *testing.T) {
// init
utilsMock := newWhitesourceUtilsMock()
scan := newWhitesourceScan(&config)
// test
err := executeScan(&config, scan, utilsMock)
// assert
assert.EqualError(t, err, "neither Maven nor NPM modules found, no scan performed")
})
}
func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Parallel()
t.Run("already new enough", func(t *testing.T) {
@ -727,9 +134,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Fatalf(err.Error())
}
lastUpdatedDate := "2010-05-30 00:15:01 +0100"
systemMock := newWhitesourceSystemMock(lastUpdatedDate)
systemMock := ws.NewSystemMock(lastUpdatedDate)
// test
err = blockUntilProjectIsUpdated(systemMock.projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 2*time.Second)
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 2*time.Second)
// assert
assert.NoError(t, err)
})
@ -741,9 +148,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Fatalf(err.Error())
}
lastUpdatedDate := "2010-05-30 00:07:00 +0100"
systemMock := newWhitesourceSystemMock(lastUpdatedDate)
systemMock := ws.NewSystemMock(lastUpdatedDate)
// test
err = blockUntilProjectIsUpdated(systemMock.projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
@ -756,9 +163,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
if err != nil {
t.Fatalf(err.Error())
}
systemMock := newWhitesourceSystemMock("")
systemMock := ws.NewSystemMock("")
// test
err = blockUntilProjectIsUpdated(systemMock.projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
@ -766,176 +173,7 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
})
}
func TestDownloadReports(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
// init
config := &ScanOptions{
ProjectToken: "mock-project-token",
ProjectName: "mock-project",
ReportDirectoryName: "report-dir",
VulnerabilityReportFormat: "txt",
}
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
scan := newWhitesourceScan(config)
// test
paths, err := downloadReports(config, scan, utils, system)
// assert
if assert.NoError(t, err) && assert.Len(t, paths, 2) {
vPath := filepath.Join("report-dir", "mock-project-vulnerability-report.txt")
assert.True(t, utils.HasWrittenFile(vPath))
vContent, _ := utils.FileRead(vPath)
assert.Equal(t, []byte("mock-vulnerability-report"), vContent)
rPath := filepath.Join("report-dir", "mock-project-risk-report.pdf")
assert.True(t, utils.HasWrittenFile(rPath))
rContent, _ := utils.FileRead(rPath)
assert.Equal(t, []byte("mock-risk-report"), rContent)
}
})
t.Run("invalid project token", func(t *testing.T) {
// init
config := &ScanOptions{
ProjectToken: "<invalid>",
ProjectName: "mock-project",
}
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
scan := newWhitesourceScan(config)
// test
paths, err := downloadReports(config, scan, utils, system)
// assert
assert.EqualError(t, err, "no project with token '<invalid>' found in Whitesource")
assert.Nil(t, paths)
})
t.Run("multiple scanned projects", func(t *testing.T) {
// init
config := &ScanOptions{
ReportDirectoryName: "report-dir",
VulnerabilityReportFormat: "txt",
}
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
scan := newWhitesourceScan(config)
scan.init()
scan.scannedProjects["mock-project"] = ws.Project{
Name: "mock-project",
Token: "mock-project-token",
}
// test
paths, err := downloadReports(config, scan, utils, system)
// assert
if assert.NoError(t, err) && assert.Len(t, paths, 2) {
vPath := filepath.Join("report-dir", "mock-project-vulnerability-report.txt")
assert.True(t, utils.HasWrittenFile(vPath))
vContent, _ := utils.FileRead(vPath)
assert.Equal(t, []byte("mock-vulnerability-report"), vContent)
rPath := filepath.Join("report-dir", "mock-project-risk-report.pdf")
assert.True(t, utils.HasWrittenFile(rPath))
rContent, _ := utils.FileRead(rPath)
assert.Equal(t, []byte("mock-risk-report"), rContent)
}
})
}
func TestWriteWhitesourceConfigJSON(t *testing.T) {
config := &ScanOptions{
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductToken: "mock-product-token",
ProductVersion: "42",
}
expected := make(map[string]interface{})
expected["apiKey"] = "org-token"
expected["userKey"] = "user-token"
expected["checkPolicies"] = true
expected["productName"] = "mock-product"
expected["projectName"] = "mock-project"
expected["productToken"] = "mock-product-token"
expected["productVer"] = "42"
expected["devDep"] = true
expected["ignoreNpmLsErrors"] = true
t.Parallel()
t.Run("write config from scratch", func(t *testing.T) {
// init
utils := newWhitesourceUtilsMock()
// test
err := writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
assert.Equal(t, expected, actual)
}
})
t.Run("extend and merge config", func(t *testing.T) {
// init
initial := make(map[string]interface{})
initial["checkPolicies"] = false
initial["productName"] = "mock-product"
initial["productVer"] = "41"
initial["unknown"] = "preserved"
encoded, _ := json.Marshal(initial)
utils := newWhitesourceUtilsMock()
utils.AddFile(whiteSourceConfig, encoded)
// test
err := writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
mergedExpected := expected
mergedExpected["unknown"] = "preserved"
assert.Equal(t, mergedExpected, actual)
}
})
t.Run("extend and merge config, omit productToken", func(t *testing.T) {
// init
initial := make(map[string]interface{})
initial["checkPolicies"] = false
initial["productName"] = "mock-product"
initial["productVer"] = "41"
initial["unknown"] = "preserved"
initial["projectToken"] = "mock-project-token"
encoded, _ := json.Marshal(initial)
utils := newWhitesourceUtilsMock()
utils.AddFile(whiteSourceConfig, encoded)
// test
err := writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
mergedExpected := expected
mergedExpected["unknown"] = "preserved"
mergedExpected["projectToken"] = "mock-project-token"
delete(mergedExpected, "productToken")
assert.Equal(t, mergedExpected, actual)
}
})
}
func TestPersisScannedProjects(t *testing.T) {
func TestPersistScannedProjects(t *testing.T) {
resource := filepath.Join(".pipeline", "commonPipelineEnvironment", "custom", "whitesourceProjectNames")
t.Parallel()
@ -944,7 +182,7 @@ func TestPersisScannedProjects(t *testing.T) {
config := &ScanOptions{ProductVersion: "1"}
utils := newWhitesourceUtilsMock()
scan := newWhitesourceScan(config)
_ = scan.appendScannedProject("project")
_ = scan.AppendScannedProject("project")
// test
err := persistScannedProjects(config, scan, utils)
// assert
@ -958,8 +196,8 @@ func TestPersisScannedProjects(t *testing.T) {
config := &ScanOptions{ProductVersion: "1"}
utils := newWhitesourceUtilsMock()
scan := newWhitesourceScan(config)
_ = scan.appendScannedProject("project-app")
_ = scan.appendScannedProject("project-db")
_ = scan.AppendScannedProject("project-app")
_ = scan.AppendScannedProject("project-db")
// test
err := persistScannedProjects(config, scan, utils)
// assert
@ -1006,7 +244,7 @@ func TestAggregateVersionWideLibraries(t *testing.T) {
ReportDirectoryName: "mock-reports",
}
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
// test
err := aggregateVersionWideLibraries(config, utils, system)
// assert
@ -1029,7 +267,7 @@ func TestAggregateVersionWideVulnerabilities(t *testing.T) {
ReportDirectoryName: "mock-reports",
}
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
// test
err := aggregateVersionWideVulnerabilities(config, utils, system)
// assert
@ -1059,7 +297,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
}
scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
// test
err := checkAndReportScanResults(config, scan, utils, system)
// assert
@ -1077,7 +315,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
}
scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
// test
err := checkAndReportScanResults(config, scan, utils, system)
// assert
@ -1095,7 +333,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
}
scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
// test
err := checkAndReportScanResults(config, scan, utils, system)
// assert
@ -1114,7 +352,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
}
scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock()
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
// test
err := checkAndReportScanResults(config, scan, utils, system)
// assert

110
pkg/whitesource/scan.go Normal file
View File

@ -0,0 +1,110 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"strings"
"time"
)
// Scan stores information about scanned WhiteSource projects (modules).
type Scan struct {
// AggregateProjectName stores the name of the WhiteSource project where scans shall be aggregated.
// It does not include the ProductVersion.
AggregateProjectName string
// ProductVersion is the global version that is used across all Projects (modules) during the scan.
ProductVersion string
scannedProjects map[string]Project
scanTimes map[string]time.Time
}
func (s *Scan) init() {
if s.scannedProjects == nil {
s.scannedProjects = make(map[string]Project)
}
if s.scanTimes == nil {
s.scanTimes = make(map[string]time.Time)
}
}
// AppendScannedProject checks that no Project with the same name is already contained in the list of scanned projects,
// and appends a new Project with the given name. The global product version is appended to the name.
func (s *Scan) AppendScannedProject(projectName string) error {
return s.AppendScannedProjectVersion(projectName + " - " + s.ProductVersion)
}
// AppendScannedProjectVersion checks that no Project with the same name is already contained in the list of scanned
// projects, and appends a new Project with the given name (which is expected to include the product version).
func (s *Scan) AppendScannedProjectVersion(projectName string) error {
if !strings.HasSuffix(projectName, " - "+s.ProductVersion) {
return fmt.Errorf("projectName is expected to include the product version")
}
s.init()
_, 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.", projectName)
return fmt.Errorf("project with name '%s' was already scanned", projectName)
}
s.scannedProjects[projectName] = Project{Name: projectName}
s.scanTimes[projectName] = time.Now()
return nil
}
// ProjectByName returns a WhiteSource Project previously established via AppendScannedProject().
func (s *Scan) ProjectByName(projectName string) (Project, bool) {
project, exists := s.scannedProjects[projectName]
return project, exists
}
// ScannedProjects returns the WhiteSource projects that have been added via AppendScannedProject() as a slice.
func (s *Scan) ScannedProjects() []Project {
var projects []Project
for _, project := range s.scannedProjects {
projects = append(projects, project)
}
return projects
}
// ScanTime returns the time at which the respective WhiteSource Project was scanned, or the the
// zero value of time.Time, if AppendScannedProject() was not called with that name.
func (s *Scan) ScanTime(projectName string) time.Time {
if s.scanTimes == nil {
return time.Time{}
}
return s.scanTimes[projectName]
}
type whitesource interface {
GetProjectsMetaInfo(productToken string) ([]Project, error)
GetProjectRiskReport(projectToken string) ([]byte, error)
GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error)
}
// UpdateProjects pulls the current backend metadata for all WhiteSource projects in the product with
// the given productToken, and updates all scanned projects with the obtained information.
func (s *Scan) UpdateProjects(productToken string, sys whitesource) error {
s.init()
projects, err := sys.GetProjectsMetaInfo(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
}

View File

@ -0,0 +1,32 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
)
// ExecuteMTAScan executes a scan for the Java part with maven, and performs a scan for each NPM module.
func (s *Scan) ExecuteMTAScan(config *ScanOptions, utils Utils) error {
log.Entry().Infof("Executing Whitesource scan for MTA project")
pomExists, _ := utils.FileExists("pom.xml")
if pomExists {
if err := s.ExecuteMavenScanForPomFile(config, utils, "pom.xml"); err != nil {
return err
}
}
modules, err := utils.FindPackageJSONFiles(config)
if err != nil {
return err
}
if len(modules) > 0 {
if err := s.ExecuteNpmScan(config, utils); err != nil {
return err
}
}
if !pomExists && len(modules) == 0 {
return fmt.Errorf("neither Maven nor NPM modules found, no scan performed")
}
return nil
}

View File

@ -0,0 +1,148 @@
package whitesource
import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestExecuteScanMTA(t *testing.T) {
const pomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
ScanType: "mta",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
}
t.Parallel()
t.Run("happy path MTA", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newTestScan(&config)
// test
err := scan.ExecuteMTAScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with only maven modules", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
scan := newTestScan(&config)
// test
err := scan.ExecuteMTAScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.False(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with only NPM modules", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newTestScan(&config)
// test
err := scan.ExecuteMTAScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("MTA with neither Maven nor NPM modules results in error", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
scan := newTestScan(&config)
// test
err := scan.ExecuteMTAScan(&config, utilsMock)
// assert
assert.EqualError(t, err, "neither Maven nor NPM modules found, no scan performed")
})
}

View File

@ -0,0 +1,111 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/maven"
"path/filepath"
"strings"
)
// ExecuteMavenScan constructs maven parameters from the given configuration, and executes the maven goal
// "org.whitesource:whitesource-maven-plugin:19.5.1:update".
func (s *Scan) ExecuteMavenScan(config *ScanOptions, utils Utils) error {
log.Entry().Infof("Using Whitesource scan for Maven project")
pomPath := config.PomPath
if pomPath == "" {
pomPath = "pom.xml"
}
return s.ExecuteMavenScanForPomFile(config, utils, pomPath)
}
// ExecuteMavenScanForPomFile constructs maven parameters from the given configuration, and executes the maven goal
// "org.whitesource:whitesource-maven-plugin:19.5.1:update" for the given pom file.
func (s *Scan) ExecuteMavenScanForPomFile(config *ScanOptions, utils Utils, 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 := s.generateMavenWhitesourceDefines(config)
flags, excludes := generateMavenWhitesourceFlags(config, utils)
err := s.appendModulesThatWillBeScanned(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 (s *Scan) 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 s.ProductVersion != "" {
defines = append(defines, "-Dorg.whitesource.productVersion="+s.ProductVersion)
}
return defines
}
func generateMavenWhitesourceFlags(config *ScanOptions, utils Utils) (flags []string, excludes []string) {
excludes = config.BuildDescriptorExcludeList
// 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 (s *Scan) appendModulesThatWillBeScanned(utils Utils, 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 := s.AppendScannedProject(project.ArtifactID)
if err != nil {
return err
}
}
return nil
})
}

View File

@ -0,0 +1,139 @@
package whitesource
import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"path/filepath"
"testing"
)
func TestExecuteScanMaven(t *testing.T) {
t.Parallel()
t.Run("maven modules are aggregated", func(t *testing.T) {
// init
const pomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
}
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("pom.xml", []byte(pomXML))
scan := newTestScan(&config)
// test
err := scan.ExecuteMavenScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.aggregateProjectName=mock-project",
"-Dorg.whitesource.aggregateModules=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
})
t.Run("maven modules are separate projects", func(t *testing.T) {
// init
const rootPomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id</artifactId>
<packaging>jar</packaging>
<modules>
<module>sub</module>
</modules>
</project>
`
const modulePomXML = `<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>my-artifact-id-sub</artifactId>
<packaging>jar</packaging>
</project>
`
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
}
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("pom.xml", []byte(rootPomXML))
utilsMock.AddFile(filepath.Join("sub", "pom.xml"), []byte(modulePomXML))
scan := newTestScan(&config)
// test
err := scan.ExecuteMavenScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "mvn",
Params: []string{
"--file",
"pom.xml",
"-Dorg.whitesource.orgToken=org-token",
"-Dorg.whitesource.product=mock-product",
"-Dorg.whitesource.checkPolicies=true",
"-Dorg.whitesource.failOnError=true",
"-Dorg.whitesource.userKey=user-token",
"-Dorg.whitesource.productVersion=product-version",
"-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn",
"--batch-mode",
"org.whitesource:whitesource-maven-plugin:19.5.1:update",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
require.Len(t, scan.ScannedProjects(), 2)
_, existsRoot := scan.ProjectByName("my-artifact-id - product-version")
_, existsModule := scan.ProjectByName("my-artifact-id-sub - product-version")
assert.True(t, existsRoot)
assert.True(t, existsModule)
})
t.Run("pom.xml does not exist", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "maven",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
}
utilsMock := NewScanUtilsMock()
scan := newTestScan(&config)
// test
err := scan.ExecuteMavenScan(&config, utilsMock)
// assert
assert.EqualError(t, err,
"for scanning with type 'maven', the file 'pom.xml' must exist in the project root")
assert.Len(t, utilsMock.Calls, 0)
})
}

220
pkg/whitesource/scanNPM.go Normal file
View File

@ -0,0 +1,220 @@
package whitesource
import (
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"io/ioutil"
"os"
"path/filepath"
)
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)
}
// writeWhitesourceConfigJSON creates or merges the file whitesource.config.json in the current
// directory from the given NPMScanOptions.
func (s *Scan) writeWhitesourceConfigJSON(config *ScanOptions, utils Utils, 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", s.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 (s *Scan) ExecuteNpmScan(config *ScanOptions, utils Utils) 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 := s.executeNpmScanForModule(module, config, 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 (s *Scan) executeNpmScanForModule(modulePath string, config *ScanOptions, utils Utils) 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 := s.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 := s.AppendScannedProject(projectName); err != nil {
return err
}
return utils.RunExecutable("npx", "whitesource", "run")
}
// getNpmProjectName tries to read a property "name" of type string from the
// package.json file in the current directory and returns an error, if this is not possible.
func getNpmProjectName(modulePath string, utils Utils) (string, error) {
fileContents, err := utils.FileRead("package.json")
if err != nil {
return "", fmt.Errorf("could not read %s: %w", modulePath, 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", modulePath)
}
projectName, isString := projectNameEntry.(string)
if !isString {
return "", fmt.Errorf("the file '%s' must configure a name", modulePath)
}
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 Utils) 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 (s *Scan) ExecuteYarnScan(config *ScanOptions, utils Utils) 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 = s.AggregateProjectName
if err := s.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
}

View File

@ -0,0 +1,193 @@
package whitesource
import (
"encoding/json"
"fmt"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"path/filepath"
"testing"
)
func TestExecuteScanNPM(t *testing.T) {
config := ScanOptions{
ScanType: "npm",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
}
t.Parallel()
t.Run("happy path NPM", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
scan := newTestScan(&config)
// test
err := scan.ExecuteNpmScan(&config, utilsMock)
// assert
require.NoError(t, err)
expectedCalls := []mock.ExecCall{
{
Exec: "npm",
Params: []string{
"ls",
},
},
{
Exec: "npx",
Params: []string{
"whitesource",
"run",
},
},
}
assert.Equal(t, expectedCalls, utilsMock.Calls)
assert.True(t, utilsMock.HasWrittenFile(whiteSourceConfig))
assert.True(t, utilsMock.HasRemovedFile(whiteSourceConfig))
})
t.Run("no NPM modules", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
scan := newTestScan(&config)
// test
err := scan.ExecuteNpmScan(&config, utilsMock)
// assert
assert.EqualError(t, err, "found no NPM modules to scan. Configured excludes: []")
assert.Len(t, utilsMock.Calls, 0)
assert.False(t, utilsMock.HasWrittenFile(whiteSourceConfig))
})
t.Run("package.json needs name", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"key":"value"}`))
scan := newTestScan(&config)
// test
err := scan.ExecuteNpmScan(&config, utilsMock)
// assert
assert.EqualError(t, err, "failed to scan NPM module 'package.json': the file 'package.json' must configure a name")
})
t.Run("npm ls fails", func(t *testing.T) {
// init
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("package.json", []byte(`{"name":"my-module-name"}`))
utilsMock.AddFile(filepath.Join("app", "package.json"), []byte(`{"name":"my-app-module-name"}`))
utilsMock.AddFile("package-lock.json", []byte("dummy"))
utilsMock.ShouldFailOnCommand = make(map[string]error)
utilsMock.ShouldFailOnCommand["npm ls"] = fmt.Errorf("mock failure")
scan := newTestScan(&config)
// test
err := scan.ExecuteNpmScan(&config, utilsMock)
// assert
assert.NoError(t, err)
expectedNpmInstalls := []NpmInstall{
{currentDir: "app", packageJSON: []string{"package.json"}},
{currentDir: "", packageJSON: []string{"package.json"}},
}
assert.Equal(t, expectedNpmInstalls, utilsMock.NpmInstalledModules)
assert.True(t, utilsMock.HasRemovedFile("package-lock.json"))
})
}
func TestWriteWhitesourceConfigJSON(t *testing.T) {
config := &ScanOptions{
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
ProductToken: "mock-product-token",
}
expected := make(map[string]interface{})
expected["apiKey"] = "org-token"
expected["userKey"] = "user-token"
expected["checkPolicies"] = true
expected["productName"] = "mock-product"
expected["projectName"] = "mock-project"
expected["productToken"] = "mock-product-token"
expected["productVer"] = "product-version"
expected["devDep"] = true
expected["ignoreNpmLsErrors"] = true
t.Parallel()
t.Run("write config from scratch", func(t *testing.T) {
// init
utils := NewScanUtilsMock()
scan := newTestScan(config)
// test
err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
assert.Equal(t, expected, actual)
}
})
t.Run("extend and merge config", func(t *testing.T) {
// init
initial := make(map[string]interface{})
initial["checkPolicies"] = false
initial["productName"] = "mock-product"
initial["productVer"] = "41"
initial["unknown"] = "preserved"
encoded, _ := json.Marshal(initial)
utils := NewScanUtilsMock()
utils.AddFile(whiteSourceConfig, encoded)
scan := newTestScan(config)
// test
err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
mergedExpected := expected
mergedExpected["unknown"] = "preserved"
assert.Equal(t, mergedExpected, actual)
}
})
t.Run("extend and merge config, omit productToken", func(t *testing.T) {
// init
initial := make(map[string]interface{})
initial["checkPolicies"] = false
initial["productName"] = "mock-product"
initial["productVer"] = "41"
initial["unknown"] = "preserved"
initial["projectToken"] = "mock-project-token"
encoded, _ := json.Marshal(initial)
utils := NewScanUtilsMock()
utils.AddFile(whiteSourceConfig, encoded)
scan := newTestScan(config)
// test
err := scan.writeWhitesourceConfigJSON(config, utils, true, true)
// assert
if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(whiteSourceConfig)) {
contents, _ := utils.FileRead(whiteSourceConfig)
actual := make(map[string]interface{})
_ = json.Unmarshal(contents, &actual)
mergedExpected := expected
mergedExpected["unknown"] = "preserved"
mergedExpected["projectToken"] = "mock-project-token"
delete(mergedExpected, "productToken")
assert.Equal(t, mergedExpected, actual)
}
})
}

View File

@ -0,0 +1,33 @@
package whitesource
// ScanOptions contains parameters needed during the scan.
type ScanOptions struct {
// ScanType defines the type of scan. Can be "maven" or "mta" for scanning with Maven or "npm"/"yarn".
ScanType string
OrgToken string
UserToken string
ProductName string
ProductToken string
// ProjectName is an optional name for an "aggregator" project.
// All scanned maven modules will be reflected in the aggregate project.
ProjectName string
BuildDescriptorExcludeList []string
// PomPath is the path to root build descriptor file.
PomPath string
// M2Path is the path to the local maven repository.
M2Path string
// GlobalSettingsFile is an optional path to a global maven settings file.
GlobalSettingsFile string
// ProjectSettingsFile is an optional path to a local maven settings file.
ProjectSettingsFile string
// DefaultNpmRegistry is an optional default registry for NPM.
DefaultNpmRegistry string
AgentDownloadURL string
AgentFileName string
ConfigFilePath string
Includes string
Excludes string
}

View File

@ -0,0 +1,77 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"os"
"path/filepath"
)
// ReportOptions defines options for downloading reports after scanning.
type ReportOptions struct {
// ReportDirectory defines the target directory for downloading reports.
ReportDirectory string
// VulnerabilityReportFormat defines the requested file format of the vulnerability report (i.e. pdf).
VulnerabilityReportFormat string
}
type scanUtils interface {
MkdirAll(path string, perm os.FileMode) error
FileWrite(path string, content []byte, perm os.FileMode) error
}
// DownloadReports downloads a Project's risk and vulnerability reports
func (s *Scan) DownloadReports(options ReportOptions, utils scanUtils, sys whitesource) ([]piperutils.Path, error) {
if err := utils.MkdirAll(options.ReportDirectory, os.ModePerm); err != nil {
return nil, err
}
var paths []piperutils.Path
for _, project := range s.scannedProjects {
vulnPath, err := downloadVulnerabilityReport(options, project, utils, sys)
if err != nil {
return nil, err
}
riskPath, err := downloadRiskReport(options, project, utils, sys)
if err != nil {
return nil, err
}
paths = append(paths, *vulnPath, *riskPath)
}
return paths, nil
}
func downloadVulnerabilityReport(options ReportOptions, project Project, utils scanUtils, sys whitesource) (*piperutils.Path, error) {
reportBytes, err := sys.GetProjectVulnerabilityReport(project.Token, options.VulnerabilityReportFormat)
if err != nil {
return nil, err
}
rptFileName := fmt.Sprintf("%s-vulnerability-report.%s", project.Name, options.VulnerabilityReportFormat)
rptFileName = filepath.Join(options.ReportDirectory, 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(options ReportOptions, project Project, utils scanUtils, 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(options.ReportDirectory, 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
}

View File

@ -0,0 +1,83 @@
package whitesource
import (
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"path/filepath"
"testing"
)
func TestDownloadReports(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
// init
options := ReportOptions{
ReportDirectory: "report-dir",
VulnerabilityReportFormat: "txt",
}
utils := &mock.FilesMock{}
system := NewSystemMock("2010-05-30 00:15:00 +0100")
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("mock-project")
_ = scan.UpdateProjects("mock-product-token", system)
// test
paths, err := scan.DownloadReports(options, utils, system)
// assert
if assert.NoError(t, err) && assert.Len(t, paths, 2) {
vPath := filepath.Join("report-dir", "mock-project - 1-vulnerability-report.txt")
assert.True(t, utils.HasWrittenFile(vPath))
vContent, _ := utils.FileRead(vPath)
assert.Equal(t, []byte("mock-vulnerability-report"), vContent)
rPath := filepath.Join("report-dir", "mock-project - 1-risk-report.pdf")
assert.True(t, utils.HasWrittenFile(rPath))
rContent, _ := utils.FileRead(rPath)
assert.Equal(t, []byte("mock-risk-report"), rContent)
}
})
t.Run("invalid project token", func(t *testing.T) {
// init
options := ReportOptions{
ReportDirectory: "report-dir",
VulnerabilityReportFormat: "txt",
}
utils := &mock.FilesMock{}
system := NewSystemMock("2010-05-30 00:15:00 +0100")
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("no-such-project")
_ = scan.UpdateProjects("mock-product-token", system)
// test
paths, err := scan.DownloadReports(options, utils, system)
// assert
assert.EqualError(t, err, "no project with token '' found in Whitesource")
assert.Nil(t, paths)
})
t.Run("multiple scanned projects", func(t *testing.T) {
// init
options := ReportOptions{
ReportDirectory: "report-dir",
VulnerabilityReportFormat: "txt",
}
utils := &mock.FilesMock{}
system := NewSystemMock("2010-05-30 00:15:00 +0100")
scan := &Scan{ProductVersion: "1"}
err := scan.AppendScannedProjectVersion("mock-project - 1")
require.NoError(t, err)
_ = scan.UpdateProjects("mock-product-token", system)
// test
paths, err := scan.DownloadReports(options, utils, system)
// assert
if assert.NoError(t, err) && assert.Len(t, paths, 2) {
vPath := filepath.Join("report-dir", "mock-project - 1-vulnerability-report.txt")
assert.True(t, utils.HasWrittenFile(vPath))
vContent, _ := utils.FileRead(vPath)
assert.Equal(t, []byte("mock-vulnerability-report"), vContent)
rPath := filepath.Join("report-dir", "mock-project - 1-risk-report.pdf")
assert.True(t, utils.HasWrittenFile(rPath))
rContent, _ := utils.FileRead(rPath)
assert.Equal(t, []byte("mock-risk-report"), rContent)
}
})
}

89
pkg/whitesource/scanUA.go Normal file
View File

@ -0,0 +1,89 @@
package whitesource
import (
"fmt"
"os"
)
// ExecuteUAScan executes a scan with the Whitesource Unified Agent.
func (s *Scan) ExecuteUAScan(config *ScanOptions, utils Utils) 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", s.AggregateProjectName,
"-product", config.ProductName, "-productVersion", s.ProductVersion)
}
// downloadAgent downloads the unified agent jar file if one does not exist
func downloadAgent(config *ScanOptions, utils Utils) 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 Utils) 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
}

View File

@ -0,0 +1,96 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestExecuteScanUA(t *testing.T) {
t.Parallel()
t.Run("happy path UA", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
OrgToken: "org-token",
UserToken: "user-token",
ProductName: "mock-product",
ProjectName: "mock-project",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
ConfigFilePath: "ua.cfg",
M2Path: ".pipeline/m2",
}
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
require.NoError(t, err)
content, err := utilsMock.FileRead("ua.cfg")
require.NoError(t, err)
contentAsString := string(content)
assert.Contains(t, contentAsString, "key=value\n")
assert.Contains(t, contentAsString, "gradle.aggregateModules=true\n")
assert.Contains(t, contentAsString, "maven.aggregateModules=true\n")
assert.Contains(t, contentAsString, "maven.m2RepositoryPath=.pipeline/m2\n")
assert.Contains(t, contentAsString, "excludes=")
require.Len(t, utilsMock.Calls, 4)
fmt.Printf("calls: %v\n", utilsMock.Calls)
expectedCall := mock.ExecCall{
Exec: "java",
Params: []string{
"-jar",
config.AgentFileName,
"-d", ".",
"-c", config.ConfigFilePath,
"-apiKey", config.OrgToken,
"-userKey", config.UserToken,
"-project", config.ProjectName,
"-product", config.ProductName,
"-productVersion", scan.ProductVersion,
},
}
assert.Equal(t, expectedCall, utilsMock.Calls[3])
})
t.Run("UA is downloaded", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("dummy"))
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
require.NoError(t, err)
require.Len(t, utilsMock.DownloadedFiles, 1)
assert.Equal(t, "https://download.ua.org/agent.jar", utilsMock.DownloadedFiles[0].sourceURL)
assert.Equal(t, "unified-agent.jar", utilsMock.DownloadedFiles[0].filePath)
})
t.Run("UA is NOT downloaded", func(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
utilsMock := NewScanUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("dummy"))
utilsMock.AddFile("unified-agent.jar", []byte("dummy"))
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
require.NoError(t, err)
assert.Len(t, utilsMock.DownloadedFiles, 0)
})
}

View File

@ -0,0 +1,238 @@
package whitesource
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestAppendScannedProjectVersion(t *testing.T) {
t.Parallel()
t.Run("single module", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProjectVersion("module-a - 1")
// assert
assert.NoError(t, err)
expected := make(map[string]Project)
expected["module-a - 1"] = Project{Name: "module-a - 1"}
assert.Equal(t, expected, scan.scannedProjects)
_, exists := scan.scanTimes["module-a - 1"]
assert.True(t, exists)
})
t.Run("two modules", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err1 := scan.AppendScannedProjectVersion("module-a - 1")
err2 := scan.AppendScannedProjectVersion("module-b - 1")
// assert
assert.NoError(t, err1)
assert.NoError(t, err2)
expected := make(map[string]Project)
expected["module-a - 1"] = Project{Name: "module-a - 1"}
expected["module-b - 1"] = Project{Name: "module-b - 1"}
assert.Equal(t, expected, scan.scannedProjects)
_, exists := scan.scanTimes["module-b - 1"]
assert.True(t, exists)
})
t.Run("module without version", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProjectVersion("module-a")
// assert
assert.EqualError(t, err, "projectName is expected to include the product version")
assert.Len(t, scan.scannedProjects, 0)
})
t.Run("duplicate module", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err1 := scan.AppendScannedProjectVersion("module-a - 1")
err2 := scan.AppendScannedProjectVersion("module-a - 1")
// assert
assert.NoError(t, err1)
assert.EqualError(t, err2, "project with name 'module-a - 1' was already scanned")
expected := make(map[string]Project)
expected["module-a - 1"] = Project{Name: "module-a - 1"}
assert.Equal(t, expected, scan.scannedProjects)
assert.Len(t, scan.scanTimes, 1)
})
}
func TestAppendScannedProject(t *testing.T) {
t.Parallel()
t.Run("product version is appended", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProject("module-a")
// assert
assert.NoError(t, err)
expected := make(map[string]Project)
expected["module-a - 1"] = Project{Name: "module-a - 1"}
assert.Equal(t, expected, scan.scannedProjects)
})
}
func TestProjectByName(t *testing.T) {
t.Parallel()
t.Run("no init", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
project, exists := scan.ProjectByName("not there")
// assert
assert.False(t, exists)
assert.Equal(t, Project{}, project)
})
t.Run("happy path", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
err := scan.AppendScannedProject("module-a")
require.NoError(t, err)
// test
project, exists := scan.ProjectByName("module-a - 1")
// assert
assert.True(t, exists)
assert.Equal(t, Project{Name: "module-a - 1"}, project)
})
t.Run("no such project", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
err := scan.AppendScannedProject("module-a")
require.NoError(t, err)
// test
project, exists := scan.ProjectByName("not there")
// assert
assert.False(t, exists)
assert.Equal(t, Project{}, project)
})
}
func TestScannedProjects(t *testing.T) {
t.Parallel()
t.Run("no init", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
projects := scan.ScannedProjects()
// assert
assert.Len(t, projects, 0)
})
t.Run("single module", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("module-a")
// test
projects := scan.ScannedProjects()
// assert
assert.Len(t, projects, 1)
assert.Contains(t, projects, Project{Name: "module-a - 1"})
})
t.Run("two modules", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("module-a")
_ = scan.AppendScannedProject("module-b")
// test
projects := scan.ScannedProjects()
// assert
assert.Len(t, projects, 2)
assert.Contains(t, projects, Project{Name: "module-a - 1"})
assert.Contains(t, projects, Project{Name: "module-b - 1"})
})
}
func TestScanTime(t *testing.T) {
t.Parallel()
t.Run("no init", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
timeStamp := scan.ScanTime("module-b - 1")
// assert
assert.Equal(t, time.Time{}, timeStamp)
})
t.Run("happy path", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("module-a")
// test
timeStamp := scan.ScanTime("module-a - 1")
// assert
assert.NotEqual(t, time.Time{}, timeStamp)
})
t.Run("project not scanned", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("module-a")
// test
timeStamp := scan.ScanTime("module-b - 1")
// assert
assert.Equal(t, time.Time{}, timeStamp)
})
}
func TestScanUpdateProjects(t *testing.T) {
t.Parallel()
t.Run("update single project which exists", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("mock-project")
mockSystem := NewSystemMock("just-now")
// test
err := scan.UpdateProjects("mock-product-token", mockSystem)
// assert
assert.NoError(t, err)
expected := make(map[string]Project)
expected["mock-project - 1"] = Project{
Name: "mock-project - 1",
ID: 42,
PluginName: "mock-plugin-name",
Token: "mock-project-token",
UploadedBy: "MrBean",
CreationDate: "last-thursday",
LastUpdateDate: "just-now",
}
assert.Equal(t, expected, scan.scannedProjects)
})
t.Run("update two projects, one of which exist", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("mock-project")
_ = scan.AppendScannedProject("unknown-project")
mockSystem := NewSystemMock("just-now")
// test
err := scan.UpdateProjects("mock-product-token", mockSystem)
// assert
assert.NoError(t, err, "no error expected if not all projects exist (yet)")
expected := make(map[string]Project)
expected["mock-project - 1"] = Project{
Name: "mock-project - 1",
ID: 42,
PluginName: "mock-plugin-name",
Token: "mock-project-token",
UploadedBy: "MrBean",
CreationDate: "last-thursday",
LastUpdateDate: "just-now",
}
expected["unknown-project - 1"] = Project{
Name: "unknown-project - 1",
}
assert.Equal(t, expected, scan.scannedProjects)
})
t.Run("update single project which does not exist", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
_ = scan.AppendScannedProject("mock-project")
mockSystem := &SystemMock{} // empty mock with no products
// test
err := scan.UpdateProjects("mock-product-token", mockSystem)
// assert
assert.EqualError(t, err, "failed to retrieve WhiteSource projects meta info: no product with that token")
})
}

View File

@ -0,0 +1,131 @@
// +build !release
package whitesource
import "fmt"
// SystemMock stores a number of WhiteSource objects and, based on that, mocks the behavior of System.
type SystemMock struct {
ProductName string
Products []Product
Projects []Project
Alerts []Alert
Libraries []Library
RiskReport []byte
VulnerabilityReport []byte
}
// GetProductByName mimics retrieving a Product by name. It returns an error of no Product is stored in the mock.
func (m *SystemMock) GetProductByName(productName string) (Product, error) {
for _, product := range m.Products {
if product.Name == productName {
return product, nil
}
}
return Product{}, fmt.Errorf("no product with name '%s' found in Whitesource", productName)
}
// GetProjectsMetaInfo returns the list of Projects stored in the mock or an error if token is unknown.
func (m *SystemMock) GetProjectsMetaInfo(productToken string) ([]Project, error) {
for _, product := range m.Products {
if product.Token == productToken {
return m.Projects, nil
}
}
return nil, fmt.Errorf("no product with that token")
}
// GetProjectToken checks the Projects stored in the mock and returns a valid token, or an empty token and no error.
func (m *SystemMock) GetProjectToken(productToken, projectName string) (string, error) {
for _, project := range m.Projects {
if project.Name == projectName {
return project.Token, nil
}
}
return "", nil
}
// GetProjectByToken checks the Projects stored in the mock and returns the one with the given token or an error.
func (m *SystemMock) GetProjectByToken(projectToken string) (Project, error) {
for _, project := range m.Projects {
if project.Token == projectToken {
return project, nil
}
}
return Project{}, fmt.Errorf("no project with token '%s' found in Whitesource", projectToken)
}
// GetProjectRiskReport mocks retrieving a risc report.
func (m *SystemMock) GetProjectRiskReport(projectToken string) ([]byte, error) {
return m.RiskReport, nil
}
// GetProjectVulnerabilityReport mocks retrieving a vulnerability report.
// Behavior depends on what is stored in the mock.
func (m *SystemMock) GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error) {
_, err := m.GetProjectByToken(projectToken)
if err != nil {
return nil, err
}
if m.VulnerabilityReport == nil {
return nil, fmt.Errorf("no report available")
}
return m.VulnerabilityReport, nil
}
// GetProjectAlerts returns the alerts stored in the SystemMock.
func (m *SystemMock) GetProjectAlerts(projectToken string) ([]Alert, error) {
return m.Alerts, nil
}
// GetProjectLibraryLocations returns the libraries stored in the SystemMock.
func (m *SystemMock) GetProjectLibraryLocations(projectToken string) ([]Library, error) {
return m.Libraries, nil
}
// NewSystemMock returns a pointer to a new instance of SystemMock.
func NewSystemMock(lastUpdateDate string) *SystemMock {
const projectName = "mock-project - 1"
mockLibrary := Library{
Name: "mock-library",
Filename: "mock-library-file",
Version: "mock-library-version",
Project: projectName,
}
return &SystemMock{
ProductName: "mock-product",
Products: []Product{
{
Name: "mock-product",
Token: "mock-product-token",
CreationDate: "last-thursday",
LastUpdateDate: lastUpdateDate,
},
},
Projects: []Project{
{
ID: 42,
Name: projectName,
PluginName: "mock-plugin-name",
Token: "mock-project-token",
UploadedBy: "MrBean",
CreationDate: "last-thursday",
LastUpdateDate: lastUpdateDate,
},
},
Alerts: []Alert{
{
Vulnerability: Vulnerability{
Name: "something severe",
Score: 5,
},
Library: mockLibrary,
Project: projectName,
CreationDate: "last-thursday",
},
},
Libraries: []Library{mockLibrary},
RiskReport: []byte("mock-risk-report"),
VulnerabilityReport: []byte("mock-vulnerability-report"),
}
}

37
pkg/whitesource/utils.go Normal file
View File

@ -0,0 +1,37 @@
package whitesource
import (
"io"
"net/http"
"os"
)
// File defines the method subset we use from os.File
type File interface {
io.Writer
io.StringWriter
io.Closer
}
// Utils captures all external functionality that needs to be exchangeable in tests.
type Utils 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) (File, error)
FindPackageJSONFiles(config *ScanOptions) ([]string, error)
InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error
}

View File

@ -0,0 +1,76 @@
// +build !release
package whitesource
import (
"github.com/SAP/jenkins-library/pkg/mock"
"net/http"
"os"
)
func newTestScan(config *ScanOptions) *Scan {
return &Scan{
AggregateProjectName: config.ProjectName,
ProductVersion: "product-version",
}
}
// NpmInstall records in which directory "npm install" has been invoked and for which package.json files.
type NpmInstall struct {
currentDir string
packageJSON []string
}
// DownloadedFile records what URL has been downloaded to which file.
type DownloadedFile struct {
sourceURL string
filePath string
}
// ScanUtilsMock is an implementation of the Utils interface that can be used during tests.
type ScanUtilsMock struct {
*mock.FilesMock
*mock.ExecMockRunner
NpmInstalledModules []NpmInstall
DownloadedFiles []DownloadedFile
}
// RemoveAll mimics os.RemoveAll().
func (m *ScanUtilsMock) RemoveAll(_ string) error {
// Can be removed once implemented in mock.FilesMock.
return nil
}
// FindPackageJSONFiles mimics npm.FindPackageJSONFiles() based on the FilesMock setup.
func (m *ScanUtilsMock) FindPackageJSONFiles(_ *ScanOptions) ([]string, error) {
matches, _ := m.Glob("**/package.json")
return matches, nil
}
// InstallAllNPMDependencies mimics npm.InstallAllNPMDependencies() and records the "npm install".
func (m *ScanUtilsMock) InstallAllNPMDependencies(_ *ScanOptions, packageJSONs []string) error {
m.NpmInstalledModules = append(m.NpmInstalledModules, NpmInstall{
currentDir: m.CurrentDir,
packageJSON: packageJSONs,
})
return nil
}
// DownloadFile mimics http.Downloader and records the downloaded file.
func (m *ScanUtilsMock) DownloadFile(url, filename string, _ http.Header, _ []*http.Cookie) error {
m.DownloadedFiles = append(m.DownloadedFiles, DownloadedFile{sourceURL: url, filePath: filename})
return nil
}
// FileOpen mimics os.FileOpen() based on FilesMock Open().
func (m *ScanUtilsMock) FileOpen(name string, flag int, perm os.FileMode) (File, error) {
return m.Open(name, flag, perm)
}
// NewScanUtilsMock returns an initialized ScanUtilsMock instance.
func NewScanUtilsMock() *ScanUtilsMock {
return &ScanUtilsMock{
FilesMock: &mock.FilesMock{},
ExecMockRunner: &mock.ExecMockRunner{},
}
}