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:
parent
586044192c
commit
260ca2c5a5
@ -1,13 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/SAP/jenkins-library/pkg/maven"
|
|
||||||
"github.com/SAP/jenkins-library/pkg/npm"
|
"github.com/SAP/jenkins-library/pkg/npm"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@ -41,37 +36,12 @@ type whitesource interface {
|
|||||||
GetProjectLibraryLocations(projectToken string) ([]ws.Library, error)
|
GetProjectLibraryLocations(projectToken string) ([]ws.Library, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wsFile defines the method subset we use from os.File
|
|
||||||
type wsFile interface {
|
|
||||||
io.Writer
|
|
||||||
io.StringWriter
|
|
||||||
io.Closer
|
|
||||||
}
|
|
||||||
|
|
||||||
type whitesourceUtils interface {
|
type whitesourceUtils interface {
|
||||||
Stdout(out io.Writer)
|
ws.Utils
|
||||||
Stderr(err io.Writer)
|
|
||||||
RunExecutable(executable string, params ...string) error
|
|
||||||
|
|
||||||
DownloadFile(url, filename string, header http.Header, cookies []*http.Cookie) error
|
|
||||||
|
|
||||||
Chdir(path string) error
|
|
||||||
Getwd() (string, error)
|
|
||||||
MkdirAll(path string, perm os.FileMode) error
|
|
||||||
FileExists(path string) (bool, error)
|
|
||||||
FileRead(path string) ([]byte, error)
|
|
||||||
FileWrite(path string, content []byte, perm os.FileMode) error
|
|
||||||
FileRemove(path string) error
|
|
||||||
FileRename(oldPath, newPath string) error
|
|
||||||
RemoveAll(path string) error
|
|
||||||
FileOpen(name string, flag int, perm os.FileMode) (wsFile, error)
|
|
||||||
|
|
||||||
GetArtifactCoordinates(buildTool, buildDescriptorFile string,
|
GetArtifactCoordinates(buildTool, buildDescriptorFile string,
|
||||||
options *versioning.Options) (versioning.Coordinates, error)
|
options *versioning.Options) (versioning.Coordinates, error)
|
||||||
|
|
||||||
FindPackageJSONFiles(config *ScanOptions) ([]string, error)
|
|
||||||
InstallAllNPMDependencies(config *ScanOptions, packageJSONFiles []string) error
|
|
||||||
|
|
||||||
Now() time.Time
|
Now() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +52,7 @@ type whitesourceUtilsBundle struct {
|
|||||||
npmExecutor npm.Executor
|
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)
|
return os.OpenFile(name, flag, perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,18 +65,18 @@ func (w *whitesourceUtilsBundle) GetArtifactCoordinates(buildTool, buildDescript
|
|||||||
return artifact.GetCoordinates()
|
return artifact.GetCoordinates()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *whitesourceUtilsBundle) getNpmExecutor(config *ScanOptions) npm.Executor {
|
func (w *whitesourceUtilsBundle) getNpmExecutor(config *ws.ScanOptions) npm.Executor {
|
||||||
if w.npmExecutor == nil {
|
if w.npmExecutor == nil {
|
||||||
w.npmExecutor = npm.NewExecutor(npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry})
|
w.npmExecutor = npm.NewExecutor(npm.ExecutorOptions{DefaultNpmRegistry: config.DefaultNpmRegistry})
|
||||||
}
|
}
|
||||||
return w.npmExecutor
|
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)
|
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)
|
return w.getNpmExecutor(config).InstallAllDependencies(packageJSONFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,69 +96,10 @@ func newWhitesourceUtils() *whitesourceUtilsBundle {
|
|||||||
return &utils
|
return &utils
|
||||||
}
|
}
|
||||||
|
|
||||||
// whitesourceScan stores information about scanned projects
|
func newWhitesourceScan(config *ScanOptions) *ws.Scan {
|
||||||
type whitesourceScan struct {
|
return &ws.Scan{
|
||||||
productToken string
|
AggregateProjectName: config.ProjectName,
|
||||||
aggregateProjectName string
|
ProductVersion: config.ProductVersion,
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
if err := resolveProjectIdentifiers(config, scan, utils, sys); err != nil {
|
||||||
return fmt.Errorf("failed to resolve project identifiers: %w", err)
|
return fmt.Errorf("failed to resolve project identifiers: %w", err)
|
||||||
}
|
}
|
||||||
@ -226,7 +137,7 @@ func runWhitesourceExecuteScan(config *ScanOptions, scan *whitesourceScan, utils
|
|||||||
return nil
|
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
|
// Start the scan
|
||||||
if err := executeScan(config, scan, utils); err != nil {
|
if err := executeScan(config, scan, utils); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -240,7 +151,7 @@ func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whites
|
|||||||
log.Entry().Info("-----------------------------------------------------")
|
log.Entry().Info("-----------------------------------------------------")
|
||||||
log.Entry().Infof("Product Version: '%s'", config.ProductVersion)
|
log.Entry().Infof("Product Version: '%s'", config.ProductVersion)
|
||||||
log.Entry().Info("Scanned projects:")
|
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().Infof(" Name: '%s', token: %s", project.Name, project.Token)
|
||||||
}
|
}
|
||||||
log.Entry().Info("-----------------------------------------------------")
|
log.Entry().Info("-----------------------------------------------------")
|
||||||
@ -256,7 +167,7 @@ func runWhitesourceScan(config *ScanOptions, scan *whitesourceScan, utils whites
|
|||||||
return nil
|
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 {
|
if !config.Reporting && !config.SecurityVulnerabilities {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -264,7 +175,10 @@ func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if config.Reporting {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -278,8 +192,8 @@ func checkAndReportScanResults(config *ScanOptions, scan *whitesourceScan, utils
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils whitesourceUtils, sys whitesource) error {
|
func resolveProjectIdentifiers(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource) error {
|
||||||
if scan.aggregateProjectName == "" || config.ProductVersion == "" {
|
if scan.AggregateProjectName == "" || config.ProductVersion == "" {
|
||||||
options := &versioning.Options{
|
options := &versioning.Options{
|
||||||
ProjectSettingsFile: config.ProjectSettingsFile,
|
ProjectSettingsFile: config.ProjectSettingsFile,
|
||||||
GlobalSettingsFile: config.GlobalSettingsFile,
|
GlobalSettingsFile: config.GlobalSettingsFile,
|
||||||
@ -292,9 +206,9 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
|
|||||||
|
|
||||||
nameTmpl := `{{list .GroupID .ArtifactID | join "-" | trimAll "-"}}`
|
nameTmpl := `{{list .GroupID .ArtifactID | join "-" | trimAll "-"}}`
|
||||||
name, version := versioning.DetermineProjectCoordinates(nameTmpl, config.VersioningModel, coordinates)
|
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)
|
log.Entry().Infof("Resolved project name '%s' from descriptor file", name)
|
||||||
scan.aggregateProjectName = name
|
scan.AggregateProjectName = name
|
||||||
}
|
}
|
||||||
if config.ProductVersion == "" {
|
if config.ProductVersion == "" {
|
||||||
log.Entry().Infof("Resolved product version '%s' from descriptor file with versioning '%s'",
|
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
|
config.ProductVersion = version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scan.productVersion = config.ProductVersion
|
scan.ProductVersion = validateProductVersion(config.ProductVersion)
|
||||||
|
|
||||||
// Get product token if user did not specify one at runtime
|
// Get product token if user did not specify one at runtime
|
||||||
if config.ProductToken == "" {
|
if config.ProductToken == "" {
|
||||||
@ -314,7 +228,6 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *whitesourceScan, utils
|
|||||||
log.Entry().Infof("Resolved product token: '%s'..", product.Token)
|
log.Entry().Infof("Resolved product token: '%s'..", product.Token)
|
||||||
config.ProductToken = product.Token
|
config.ProductToken = product.Token
|
||||||
}
|
}
|
||||||
scan.productToken = config.ProductToken
|
|
||||||
|
|
||||||
// Get project token if user did not specify one at runtime
|
// Get project token if user did not specify one at runtime
|
||||||
if config.ProjectToken == "" && config.ProjectName != "" {
|
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.
|
// 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.
|
// 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 == "" {
|
if config.ScanType == "" {
|
||||||
config.ScanType = config.BuildTool
|
config.ScanType = config.BuildTool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options := wsScanOptions(config)
|
||||||
|
|
||||||
switch config.ScanType {
|
switch config.ScanType {
|
||||||
case "mta":
|
case "mta":
|
||||||
// Execute scan for maven and all npm modules
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
case "maven":
|
case "maven":
|
||||||
// Execute scan with maven plugin goal
|
// Execute scan with maven plugin goal
|
||||||
if err := executeMavenScan(config, scan, utils); err != nil {
|
if err := scan.ExecuteMavenScan(options, utils); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "npm":
|
case "npm":
|
||||||
// Execute scan with in each npm module using npm.Executor
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
case "yarn":
|
case "yarn":
|
||||||
// Execute scan with whitesource yarn plugin
|
// Execute scan with whitesource yarn plugin
|
||||||
if err := executeYarnScan(config, scan, utils); err != nil {
|
if err := scan.ExecuteYarnScan(options, utils); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Execute scan with Unified Agent jar file
|
// 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 err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeUAScan executes a scan with the Whitesource Unified Agent.
|
func checkSecurityViolations(config *ScanOptions, scan *ws.Scan, sys whitesource) error {
|
||||||
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 {
|
|
||||||
// Check for security vulnerabilities and fail the build if cvssSeverityLimit threshold is crossed
|
// Check for security vulnerabilities and fail the build if cvssSeverityLimit threshold is crossed
|
||||||
// convert config.CvssSeverityLimit to float64
|
// convert config.CvssSeverityLimit to float64
|
||||||
cvssSeverityLimit, err := strconv.ParseFloat(config.CvssSeverityLimit, 64)
|
cvssSeverityLimit, err := strconv.ParseFloat(config.CvssSeverityLimit, 64)
|
||||||
@ -742,7 +336,7 @@ func checkSecurityViolations(config *ScanOptions, scan *whitesourceScan, sys whi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, project := range scan.scannedProjects {
|
for _, project := range scan.ScannedProjects() {
|
||||||
if err := checkProjectSecurityViolations(cvssSeverityLimit, project, sys); err != nil {
|
if err := checkProjectSecurityViolations(cvssSeverityLimit, project, sys); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -793,7 +387,7 @@ func checkProjectSecurityViolations(cvssSeverityLimit float64, project ws.Projec
|
|||||||
return nil
|
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
|
// Project was scanned. We need to wait for WhiteSource backend to propagate the changes
|
||||||
// before downloading any reports or check security vulnerabilities.
|
// before downloading any reports or check security vulnerabilities.
|
||||||
if config.ProjectToken != "" {
|
if config.ProjectToken != "" {
|
||||||
@ -803,8 +397,8 @@ func blockUntilReportsAreaReady(config *ScanOptions, scan *whitesourceScan, sys
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Poll status of all scanned projects
|
// Poll status of all scanned projects
|
||||||
for key, project := range scan.scannedProjects {
|
for _, project := range scan.ScannedProjects() {
|
||||||
if err := pollProjectStatus(project.Token, scan.scanTimes[key], sys); err != nil {
|
if err := pollProjectStatus(project.Token, scan.ScanTime(project.Name), sys); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -854,140 +448,6 @@ func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTim
|
|||||||
return nil
|
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 {
|
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)
|
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
|
// 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.
|
// 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
|
var projectNames []string
|
||||||
if config.ProjectName != "" {
|
if config.ProjectName != "" {
|
||||||
projectNames = []string{config.ProjectName + " - " + config.ProductVersion}
|
projectNames = []string{config.ProjectName + " - " + config.ProductVersion}
|
||||||
} else {
|
} else {
|
||||||
for projectName := range scan.scannedProjects {
|
for _, project := range scan.ScannedProjects() {
|
||||||
projectNames = append(projectNames, projectName)
|
projectNames = append(projectNames, project.Name)
|
||||||
}
|
}
|
||||||
// Sorting helps the list become stable across pipeline runs (and in the unit tests),
|
// 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.
|
// as the order in which we travers map keys is not deterministic.
|
||||||
|
@ -1,164 +1,27 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/SAP/jenkins-library/pkg/mock"
|
"github.com/SAP/jenkins-library/pkg/mock"
|
||||||
"github.com/SAP/jenkins-library/pkg/versioning"
|
"github.com/SAP/jenkins-library/pkg/versioning"
|
||||||
ws "github.com/SAP/jenkins-library/pkg/whitesource"
|
ws "github.com/SAP/jenkins-library/pkg/whitesource"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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 {
|
type whitesourceCoordinatesMock struct {
|
||||||
GroupID string
|
GroupID string
|
||||||
ArtifactID string
|
ArtifactID string
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
type downloadedFile struct {
|
|
||||||
sourceURL string
|
|
||||||
filePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
type npmInstall struct {
|
|
||||||
currentDir string
|
|
||||||
packageJSON []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type whitesourceUtilsMock struct {
|
type whitesourceUtilsMock struct {
|
||||||
*mock.FilesMock
|
*ws.ScanUtilsMock
|
||||||
*mock.ExecMockRunner
|
|
||||||
coordinates whitesourceCoordinatesMock
|
coordinates whitesourceCoordinatesMock
|
||||||
usedBuildTool string
|
usedBuildTool string
|
||||||
usedBuildDescriptorFile string
|
usedBuildDescriptorFile string
|
||||||
usedOptions versioning.Options
|
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,
|
func (w *whitesourceUtilsMock) GetArtifactCoordinates(buildTool, buildDescriptorFile string,
|
||||||
@ -169,19 +32,6 @@ func (w *whitesourceUtilsMock) GetArtifactCoordinates(buildTool, buildDescriptor
|
|||||||
return w.coordinates, nil
|
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"
|
const wsTimeNow = "2010-05-10 00:15:42"
|
||||||
|
|
||||||
func (w *whitesourceUtilsMock) Now() time.Time {
|
func (w *whitesourceUtilsMock) Now() time.Time {
|
||||||
@ -191,8 +41,10 @@ func (w *whitesourceUtilsMock) Now() time.Time {
|
|||||||
|
|
||||||
func newWhitesourceUtilsMock() *whitesourceUtilsMock {
|
func newWhitesourceUtilsMock() *whitesourceUtilsMock {
|
||||||
return &whitesourceUtilsMock{
|
return &whitesourceUtilsMock{
|
||||||
FilesMock: &mock.FilesMock{},
|
ScanUtilsMock: &ws.ScanUtilsMock{
|
||||||
ExecMockRunner: &mock.ExecMockRunner{},
|
FilesMock: &mock.FilesMock{},
|
||||||
|
ExecMockRunner: &mock.ExecMockRunner{},
|
||||||
|
},
|
||||||
coordinates: whitesourceCoordinatesMock{
|
coordinates: whitesourceCoordinatesMock{
|
||||||
GroupID: "mock-group-id",
|
GroupID: "mock-group-id",
|
||||||
ArtifactID: "mock-artifact-id",
|
ArtifactID: "mock-artifact-id",
|
||||||
@ -215,13 +67,13 @@ func TestResolveProjectIdentifiers(t *testing.T) {
|
|||||||
GlobalSettingsFile: "global-settings.xml",
|
GlobalSettingsFile: "global-settings.xml",
|
||||||
}
|
}
|
||||||
utilsMock := newWhitesourceUtilsMock()
|
utilsMock := newWhitesourceUtilsMock()
|
||||||
systemMock := newWhitesourceSystemMock("ignored")
|
systemMock := ws.NewSystemMock("ignored")
|
||||||
scan := newWhitesourceScan(&config)
|
scan := newWhitesourceScan(&config)
|
||||||
// test
|
// test
|
||||||
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
|
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
|
||||||
// assert
|
// assert
|
||||||
if assert.NoError(t, err) {
|
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, "1", config.ProductVersion)
|
||||||
assert.Equal(t, "mock-product-token", config.ProductToken)
|
assert.Equal(t, "mock-product-token", config.ProductToken)
|
||||||
assert.Equal(t, "mta", utilsMock.usedBuildTool)
|
assert.Equal(t, "mta", utilsMock.usedBuildTool)
|
||||||
@ -238,16 +90,16 @@ func TestResolveProjectIdentifiers(t *testing.T) {
|
|||||||
BuildDescriptorFile: "my-mta.yml",
|
BuildDescriptorFile: "my-mta.yml",
|
||||||
VersioningModel: "major",
|
VersioningModel: "major",
|
||||||
ProductName: "mock-product",
|
ProductName: "mock-product",
|
||||||
ProjectName: "mock-project - 1",
|
ProjectName: "mock-project",
|
||||||
}
|
}
|
||||||
utilsMock := newWhitesourceUtilsMock()
|
utilsMock := newWhitesourceUtilsMock()
|
||||||
systemMock := newWhitesourceSystemMock("ignored")
|
systemMock := ws.NewSystemMock("ignored")
|
||||||
scan := newWhitesourceScan(&config)
|
scan := newWhitesourceScan(&config)
|
||||||
// test
|
// test
|
||||||
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
|
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
|
||||||
// assert
|
// assert
|
||||||
if assert.NoError(t, err) {
|
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, "1", config.ProductVersion)
|
||||||
assert.Equal(t, "mock-product-token", config.ProductToken)
|
assert.Equal(t, "mock-product-token", config.ProductToken)
|
||||||
assert.Equal(t, "mta", utilsMock.usedBuildTool)
|
assert.Equal(t, "mta", utilsMock.usedBuildTool)
|
||||||
@ -263,7 +115,7 @@ func TestResolveProjectIdentifiers(t *testing.T) {
|
|||||||
ProductName: "does-not-exist",
|
ProductName: "does-not-exist",
|
||||||
}
|
}
|
||||||
utilsMock := newWhitesourceUtilsMock()
|
utilsMock := newWhitesourceUtilsMock()
|
||||||
systemMock := newWhitesourceSystemMock("ignored")
|
systemMock := ws.NewSystemMock("ignored")
|
||||||
scan := newWhitesourceScan(&config)
|
scan := newWhitesourceScan(&config)
|
||||||
// test
|
// test
|
||||||
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
|
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) {
|
func TestBlockUntilProjectIsUpdated(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("already new enough", func(t *testing.T) {
|
t.Run("already new enough", func(t *testing.T) {
|
||||||
@ -727,9 +134,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
|
|||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
lastUpdatedDate := "2010-05-30 00:15:01 +0100"
|
lastUpdatedDate := "2010-05-30 00:15:01 +0100"
|
||||||
systemMock := newWhitesourceSystemMock(lastUpdatedDate)
|
systemMock := ws.NewSystemMock(lastUpdatedDate)
|
||||||
// test
|
// 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
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
@ -741,9 +148,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
|
|||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
lastUpdatedDate := "2010-05-30 00:07:00 +0100"
|
lastUpdatedDate := "2010-05-30 00:07:00 +0100"
|
||||||
systemMock := newWhitesourceSystemMock(lastUpdatedDate)
|
systemMock := ws.NewSystemMock(lastUpdatedDate)
|
||||||
// test
|
// 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
|
// assert
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "timeout while waiting")
|
assert.Contains(t, err.Error(), "timeout while waiting")
|
||||||
@ -756,9 +163,9 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
systemMock := newWhitesourceSystemMock("")
|
systemMock := ws.NewSystemMock("")
|
||||||
// test
|
// 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
|
// assert
|
||||||
if assert.Error(t, err) {
|
if assert.Error(t, err) {
|
||||||
assert.Contains(t, err.Error(), "timeout while waiting")
|
assert.Contains(t, err.Error(), "timeout while waiting")
|
||||||
@ -766,176 +173,7 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloadReports(t *testing.T) {
|
func TestPersistScannedProjects(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) {
|
|
||||||
resource := filepath.Join(".pipeline", "commonPipelineEnvironment", "custom", "whitesourceProjectNames")
|
resource := filepath.Join(".pipeline", "commonPipelineEnvironment", "custom", "whitesourceProjectNames")
|
||||||
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@ -944,7 +182,7 @@ func TestPersisScannedProjects(t *testing.T) {
|
|||||||
config := &ScanOptions{ProductVersion: "1"}
|
config := &ScanOptions{ProductVersion: "1"}
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
_ = scan.appendScannedProject("project")
|
_ = scan.AppendScannedProject("project")
|
||||||
// test
|
// test
|
||||||
err := persistScannedProjects(config, scan, utils)
|
err := persistScannedProjects(config, scan, utils)
|
||||||
// assert
|
// assert
|
||||||
@ -958,8 +196,8 @@ func TestPersisScannedProjects(t *testing.T) {
|
|||||||
config := &ScanOptions{ProductVersion: "1"}
|
config := &ScanOptions{ProductVersion: "1"}
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
_ = scan.appendScannedProject("project-app")
|
_ = scan.AppendScannedProject("project-app")
|
||||||
_ = scan.appendScannedProject("project-db")
|
_ = scan.AppendScannedProject("project-db")
|
||||||
// test
|
// test
|
||||||
err := persistScannedProjects(config, scan, utils)
|
err := persistScannedProjects(config, scan, utils)
|
||||||
// assert
|
// assert
|
||||||
@ -1006,7 +244,7 @@ func TestAggregateVersionWideLibraries(t *testing.T) {
|
|||||||
ReportDirectoryName: "mock-reports",
|
ReportDirectoryName: "mock-reports",
|
||||||
}
|
}
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
|
system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
|
||||||
// test
|
// test
|
||||||
err := aggregateVersionWideLibraries(config, utils, system)
|
err := aggregateVersionWideLibraries(config, utils, system)
|
||||||
// assert
|
// assert
|
||||||
@ -1029,7 +267,7 @@ func TestAggregateVersionWideVulnerabilities(t *testing.T) {
|
|||||||
ReportDirectoryName: "mock-reports",
|
ReportDirectoryName: "mock-reports",
|
||||||
}
|
}
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock("2010-05-30 00:15:00 +0100")
|
system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
|
||||||
// test
|
// test
|
||||||
err := aggregateVersionWideVulnerabilities(config, utils, system)
|
err := aggregateVersionWideVulnerabilities(config, utils, system)
|
||||||
// assert
|
// assert
|
||||||
@ -1059,7 +297,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
||||||
// test
|
// test
|
||||||
err := checkAndReportScanResults(config, scan, utils, system)
|
err := checkAndReportScanResults(config, scan, utils, system)
|
||||||
// assert
|
// assert
|
||||||
@ -1077,7 +315,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
||||||
// test
|
// test
|
||||||
err := checkAndReportScanResults(config, scan, utils, system)
|
err := checkAndReportScanResults(config, scan, utils, system)
|
||||||
// assert
|
// assert
|
||||||
@ -1095,7 +333,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
||||||
// test
|
// test
|
||||||
err := checkAndReportScanResults(config, scan, utils, system)
|
err := checkAndReportScanResults(config, scan, utils, system)
|
||||||
// assert
|
// assert
|
||||||
@ -1114,7 +352,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
scan := newWhitesourceScan(config)
|
scan := newWhitesourceScan(config)
|
||||||
utils := newWhitesourceUtilsMock()
|
utils := newWhitesourceUtilsMock()
|
||||||
system := newWhitesourceSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout))
|
||||||
// test
|
// test
|
||||||
err := checkAndReportScanResults(config, scan, utils, system)
|
err := checkAndReportScanResults(config, scan, utils, system)
|
||||||
// assert
|
// assert
|
||||||
|
110
pkg/whitesource/scan.go
Normal file
110
pkg/whitesource/scan.go
Normal 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
|
||||||
|
}
|
32
pkg/whitesource/scanMTA.go
Normal file
32
pkg/whitesource/scanMTA.go
Normal 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
|
||||||
|
}
|
148
pkg/whitesource/scanMTA_test.go
Normal file
148
pkg/whitesource/scanMTA_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
111
pkg/whitesource/scanMaven.go
Normal file
111
pkg/whitesource/scanMaven.go
Normal 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
|
||||||
|
})
|
||||||
|
}
|
139
pkg/whitesource/scanMaven_test.go
Normal file
139
pkg/whitesource/scanMaven_test.go
Normal 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
220
pkg/whitesource/scanNPM.go
Normal 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
|
||||||
|
}
|
193
pkg/whitesource/scanNPM_test.go
Normal file
193
pkg/whitesource/scanNPM_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
33
pkg/whitesource/scanOptions.go
Normal file
33
pkg/whitesource/scanOptions.go
Normal 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
|
||||||
|
}
|
77
pkg/whitesource/scanReports.go
Normal file
77
pkg/whitesource/scanReports.go
Normal 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
|
||||||
|
}
|
83
pkg/whitesource/scanReports_test.go
Normal file
83
pkg/whitesource/scanReports_test.go
Normal 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
89
pkg/whitesource/scanUA.go
Normal 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
|
||||||
|
}
|
96
pkg/whitesource/scanUA_test.go
Normal file
96
pkg/whitesource/scanUA_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
238
pkg/whitesource/scan_test.go
Normal file
238
pkg/whitesource/scan_test.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
131
pkg/whitesource/sytemMock.go
Normal file
131
pkg/whitesource/sytemMock.go
Normal 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
37
pkg/whitesource/utils.go
Normal 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
|
||||||
|
}
|
76
pkg/whitesource/utilsMock.go
Normal file
76
pkg/whitesource/utilsMock.go
Normal 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{},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user