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

whitesourcExecuteScan-go: Additional fixes (#2315)

* Make sure the UA scan is known to the scan object. Fixes downloading reports later on.
* Move polling into pkg/whitesource, add test for e2e scan
* Remove conditions from stash config resource
* Don't use version stored in CPE. This will prevent the versioningModel from being applied.
This commit is contained in:
Stephan Aßmus 2020-11-10 09:09:51 +01:00 committed by GitHub
parent bf39d2aacc
commit eff38f6c9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 280 additions and 228 deletions

View File

@ -119,6 +119,10 @@ func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData, commonP
}
func runWhitesourceExecuteScan(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils, sys whitesource, commonPipelineEnvironment *whitesourceExecuteScanCommonPipelineEnvironment) error {
if err := resolveAggregateProjectName(config, scan, sys); err != nil {
return err
}
if err := resolveProjectIdentifiers(config, scan, utils, sys); err != nil {
return fmt.Errorf("failed to resolve project identifiers: %w", err)
}
@ -174,7 +178,8 @@ func checkAndReportScanResults(config *ScanOptions, scan *ws.Scan, utils whiteso
if !config.Reporting && !config.SecurityVulnerabilities {
return nil
}
if err := blockUntilReportsAreaReady(config, scan, sys); err != nil {
// Wait for WhiteSource backend to propagate the changes before downloading any reports.
if err := scan.BlockUntilReportsAreReady(sys); err != nil {
return err
}
if config.Reporting {
@ -277,6 +282,25 @@ func resolveProductToken(config *ScanOptions, sys whitesource) error {
return nil
}
// resolveAggregateProjectName checks if config.ProjectToken is configured, and if so, expects a WhiteSource
// project with that token to exist. The AggregateProjectName in the ws.Scan is then configured with that
// project's name.
func resolveAggregateProjectName(config *ScanOptions, scan *ws.Scan, sys whitesource) error {
if config.ProjectToken == "" {
return nil
}
log.Entry().Infof("Attempting to resolve aggregate project name for token '%s'..", config.ProjectToken)
// If the user configured the "projectToken" parameter, we expect this project to exist in the backend.
project, err := sys.GetProjectByToken(config.ProjectToken)
if err != nil {
return err
}
nameVersion := strings.Split(project.Name, " - ")
scan.AggregateProjectName = nameVersion[0]
log.Entry().Infof("Resolve aggregate project name '%s'..", scan.AggregateProjectName)
return nil
}
// resolveAggregateProjectToken fetches the token of the WhiteSource Project specified by config.ProjectName
// and stores it in config.ProjectToken.
// The user can configure a projectName or projectToken of the project to be used as for aggregation of scan results.
@ -439,65 +463,6 @@ func checkProjectSecurityViolations(cvssSeverityLimit float64, project ws.Projec
return nil
}
func blockUntilReportsAreaReady(config *ScanOptions, scan *ws.Scan, sys whitesource) error {
// Project was scanned. We need to wait for WhiteSource backend to propagate the changes
// before downloading any reports or check security vulnerabilities.
if config.ProjectToken != "" {
// Poll status of aggregated project
if err := pollProjectStatus(config.ProjectToken, time.Now(), sys); err != nil {
return err
}
} else {
// Poll status of all scanned projects
for _, project := range scan.ScannedProjects() {
if err := pollProjectStatus(project.Token, scan.ScanTime(project.Name), sys); err != nil {
return err
}
}
}
return nil
}
// pollProjectStatus polls project LastUpdateDate until it reflects the most recent scan
func pollProjectStatus(projectToken string, scanTime time.Time, sys whitesource) error {
return blockUntilProjectIsUpdated(projectToken, sys, scanTime, 20*time.Second, 20*time.Second, 15*time.Minute)
}
// blockUntilProjectIsUpdated polls the project LastUpdateDate until it is newer than the given time stamp
// or no older than maxAge relative to the given time stamp.
func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTime time.Time, maxAge, timeBetweenPolls, maxWaitTime time.Duration) error {
startTime := time.Now()
for {
project, err := sys.GetProjectByToken(projectToken)
if err != nil {
return err
}
if project.LastUpdateDate == "" {
log.Entry().Infof("last updated time missing from project metadata, retrying")
} else {
lastUpdatedTime, err := time.Parse(ws.DateTimeLayout, project.LastUpdateDate)
if err != nil {
return fmt.Errorf("failed to parse last updated time (%s) of Whitesource project: %w",
project.LastUpdateDate, err)
}
age := currentTime.Sub(lastUpdatedTime)
if age < maxAge {
//done polling
break
}
log.Entry().Infof("time since project was last updated %v > %v, polling status...", age, maxAge)
}
if time.Now().Sub(startTime) > maxWaitTime {
return fmt.Errorf("timeout while waiting for Whitesource scan results to be reflected in service")
}
time.Sleep(timeBetweenPolls)
}
return nil
}
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)

View File

@ -390,17 +390,12 @@ func whitesourceExecuteScanMetadata() config.StepData {
Aliases: []config.Alias{},
},
{
Name: "productVersion",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "artifactVersion",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Name: "productVersion",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
},
{
Name: "jreDownloadUrl",

View File

@ -145,52 +145,70 @@ func TestResolveProjectIdentifiers(t *testing.T) {
})
}
func TestBlockUntilProjectIsUpdated(t *testing.T) {
func TestRunWhitesourceExecuteScan(t *testing.T) {
t.Parallel()
t.Run("already new enough", func(t *testing.T) {
t.Run("fails for invalid configured project token", func(t *testing.T) {
// init
nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil {
t.Fatalf(err.Error())
config := ScanOptions{
ScanType: "unified-agent",
BuildDescriptorFile: "my-mta.yml",
VersioningModel: "major",
ProductName: "mock-product",
ProjectToken: "no-such-project-token",
AgentDownloadURL: "https://whitesource.com/agent.jar",
AgentFileName: "ua.jar",
}
lastUpdatedDate := "2010-05-30 00:15:01 +0100"
systemMock := ws.NewSystemMock(lastUpdatedDate)
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
systemMock := ws.NewSystemMock("ignored")
scan := newWhitesourceScan(&config)
cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 2*time.Second)
err := runWhitesourceExecuteScan(&config, scan, utilsMock, systemMock, &cpe)
// assert
assert.EqualError(t, err, "no project with token 'no-such-project-token' found in Whitesource")
assert.Equal(t, "", config.ProjectName)
assert.Equal(t, "", scan.AggregateProjectName)
})
t.Run("retrieves aggregate project name by configured token", func(t *testing.T) {
// init
config := ScanOptions{
BuildDescriptorFile: "my-mta.yml",
VersioningModel: "major",
AgentDownloadURL: "https://whitesource.com/agent.jar",
ReportDirectoryName: "ws-reports",
VulnerabilityReportFormat: "pdf",
Reporting: true,
AgentFileName: "ua.jar",
ProductName: "mock-product",
ProjectToken: "mock-project-token",
ScanType: "unified-agent",
}
utilsMock := newWhitesourceUtilsMock()
utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
lastUpdatedDate := time.Now().Format(ws.DateTimeLayout)
systemMock := ws.NewSystemMock(lastUpdatedDate)
scan := newWhitesourceScan(&config)
cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
// test
err := runWhitesourceExecuteScan(&config, scan, utilsMock, systemMock, &cpe)
// assert
assert.NoError(t, err)
})
t.Run("timeout while polling", func(t *testing.T) {
// init
nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil {
t.Fatalf(err.Error())
// Retrieved project name is stored in scan.AggregateProjectName, but not in config.ProjectName
// in order to differentiate between aggregate-project scanning and multi-project scanning.
assert.Equal(t, "", config.ProjectName)
assert.Equal(t, "mock-project", scan.AggregateProjectName)
if assert.Len(t, utilsMock.DownloadedFiles, 1) {
assert.Equal(t, ws.DownloadedFile{
SourceURL: "https://whitesource.com/agent.jar",
FilePath: "ua.jar",
}, utilsMock.DownloadedFiles[0])
}
lastUpdatedDate := "2010-05-30 00:07:00 +0100"
systemMock := ws.NewSystemMock(lastUpdatedDate)
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
}
})
t.Run("timeout while polling, no update time", func(t *testing.T) {
// init
nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil {
t.Fatalf(err.Error())
}
systemMock := ws.NewSystemMock("")
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, now, 2*time.Second, 1*time.Second, 1*time.Second)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
if assert.Len(t, cpe.custom.whitesourceProjectNames, 1) {
assert.Equal(t, []string{"mock-project - 1"}, cpe.custom.whitesourceProjectNames)
}
assert.True(t, utilsMock.HasWrittenFile("ws-reports/mock-project - 1-vulnerability-report.pdf"))
assert.True(t, utilsMock.HasWrittenFile("ws-reports/mock-project - 1-risk-report.pdf"))
})
}

View File

@ -28,18 +28,31 @@ func (s *Scan) init() {
}
}
func (s *Scan) versionSuffix() string {
return " - " + s.ProductVersion
}
// 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)
if len(projectName) == 0 {
return fmt.Errorf("projectName must not be empty")
}
if strings.HasSuffix(projectName, s.versionSuffix()) {
return fmt.Errorf("projectName is not expected to include the product version already")
}
return s.AppendScannedProjectVersion(projectName + s.versionSuffix())
}
// 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) {
if !strings.HasSuffix(projectName, s.versionSuffix()) {
return fmt.Errorf("projectName is expected to include the product version")
}
if len(projectName) == len(s.versionSuffix()) {
return fmt.Errorf("projectName consists only of the product version")
}
s.init()
_, exists := s.scannedProjects[projectName]
if exists {

View File

@ -85,8 +85,8 @@ func TestExecuteScanNPM(t *testing.T) {
// assert
assert.NoError(t, err)
expectedNpmInstalls := []NpmInstall{
{currentDir: "app", packageJSON: []string{"package.json"}},
{currentDir: "", packageJSON: []string{"package.json"}},
{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"))

View File

@ -0,0 +1,75 @@
package whitesource
import (
"fmt"
"github.com/SAP/jenkins-library/pkg/log"
"time"
)
type whitesourcePoller interface {
GetProjectByToken(projectToken string) (Project, error)
}
// BlockUntilReportsAreReady polls the WhiteSource system for all projects known to the Scan and blocks
// until their LastUpdateDate time stamp is from within the last 20 seconds.
func (s *Scan) BlockUntilReportsAreReady(sys whitesourcePoller) error {
for _, project := range s.ScannedProjects() {
if err := pollProjectStatus(project.Token, s.ScanTime(project.Name), sys); err != nil {
return err
}
}
return nil
}
type pollOptions struct {
scanTime time.Time
maxAge time.Duration
timeBetweenPolls time.Duration
maxWaitTime time.Duration
}
// pollProjectStatus polls project LastUpdateDate until it reflects the most recent scan
func pollProjectStatus(projectToken string, scanTime time.Time, sys whitesourcePoller) error {
options := pollOptions{
scanTime: scanTime,
maxAge: 20 * time.Second,
timeBetweenPolls: 20 * time.Second,
maxWaitTime: 15 * time.Minute,
}
return blockUntilProjectIsUpdated(projectToken, sys, options)
}
// blockUntilProjectIsUpdated polls the project LastUpdateDate until it is newer than the given time stamp
// or no older than maxAge relative to the given time stamp.
func blockUntilProjectIsUpdated(projectToken string, sys whitesourcePoller, options pollOptions) error {
startTime := time.Now()
for {
project, err := sys.GetProjectByToken(projectToken)
if err != nil {
return err
}
if project.LastUpdateDate == "" {
log.Entry().Infof("last updated time missing from project metadata, retrying")
} else {
lastUpdatedTime, err := time.Parse(DateTimeLayout, project.LastUpdateDate)
if err != nil {
return fmt.Errorf("failed to parse last updated time (%s) of Whitesource project: %w",
project.LastUpdateDate, err)
}
age := options.scanTime.Sub(lastUpdatedTime)
if age < options.maxAge {
//done polling
break
}
log.Entry().Infof("time since project was last updated %v > %v, polling status...", age, options.maxAge)
}
if time.Now().Sub(startTime) > options.maxWaitTime {
return fmt.Errorf("timeout while waiting for Whitesource scan results to be reflected in service")
}
time.Sleep(options.timeBetweenPolls)
}
return nil
}

View File

@ -0,0 +1,53 @@
package whitesource
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Parallel()
nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(DateTimeLayout, nowString)
require.NoError(t, err)
options := pollOptions{
scanTime: now,
maxAge: 2 * time.Second,
timeBetweenPolls: 1 * time.Second,
maxWaitTime: 1 * time.Second,
}
t.Run("already new enough", func(t *testing.T) {
// init
lastUpdatedDate := "2010-05-30 00:15:01 +0100"
systemMock := NewSystemMock(lastUpdatedDate)
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, options)
// assert
assert.NoError(t, err)
})
t.Run("timeout while polling", func(t *testing.T) {
// init
lastUpdatedDate := "2010-05-30 00:07:00 +0100"
systemMock := NewSystemMock(lastUpdatedDate)
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, options)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
}
})
t.Run("timeout while polling, no update time", func(t *testing.T) {
// init
systemMock := NewSystemMock("")
// test
err = blockUntilProjectIsUpdated(systemMock.Projects[0].Token, systemMock, options)
// assert
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "timeout while waiting")
}
})
}

View File

@ -18,6 +18,10 @@ func (s *Scan) ExecuteUAScan(config *ScanOptions, utils Utils) error {
return err
}
if err := s.AppendScannedProject(s.AggregateProjectName); 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)

View File

@ -28,9 +28,8 @@ func TestExecuteScanUA(t *testing.T) {
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
// assert
require.NoError(t, err)
content, err := utilsMock.FileRead("ua.cfg")
require.NoError(t, err)
contentAsString := string(content)
@ -62,6 +61,7 @@ func TestExecuteScanUA(t *testing.T) {
// init
config := ScanOptions{
ScanType: "unified-agent",
ProjectName: "mock-project",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
@ -70,16 +70,17 @@ func TestExecuteScanUA(t *testing.T) {
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
// 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)
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",
ProjectName: "mock-project",
AgentDownloadURL: "https://download.ua.org/agent.jar",
AgentFileName: "unified-agent.jar",
}
@ -89,7 +90,7 @@ func TestExecuteScanUA(t *testing.T) {
scan := newTestScan(&config)
// test
err := scan.ExecuteUAScan(&config, utilsMock)
// many assert
// assert
require.NoError(t, err)
assert.Len(t, utilsMock.DownloadedFiles, 0)
})

View File

@ -61,6 +61,36 @@ func TestAppendScannedProjectVersion(t *testing.T) {
assert.Equal(t, expected, scan.scannedProjects)
assert.Len(t, scan.scanTimes, 1)
})
t.Run("empty project name", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProject("")
// assert
assert.EqualError(t, err, "projectName must not be empty")
assert.Len(t, scan.scannedProjects, 0)
assert.Len(t, scan.scanTimes, 0)
})
t.Run("product version supplied wrongly", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProject("name - 1")
// assert
assert.EqualError(t, err, "projectName is not expected to include the product version already")
assert.Len(t, scan.scannedProjects, 0)
assert.Len(t, scan.scanTimes, 0)
})
t.Run("only version part in project name", func(t *testing.T) {
// init
scan := &Scan{ProductVersion: "1"}
// test
err := scan.AppendScannedProjectVersion(" - 1")
// assert
assert.EqualError(t, err, "projectName consists only of the product version")
assert.Len(t, scan.scannedProjects, 0)
assert.Len(t, scan.scanTimes, 0)
})
}
func TestAppendScannedProject(t *testing.T) {

View File

@ -17,14 +17,14 @@ func newTestScan(config *ScanOptions) *Scan {
// NpmInstall records in which directory "npm install" has been invoked and for which package.json files.
type NpmInstall struct {
currentDir string
packageJSON []string
CurrentDir string
PackageJSON []string
}
// DownloadedFile records what URL has been downloaded to which file.
type DownloadedFile struct {
sourceURL string
filePath string
SourceURL string
FilePath string
}
// ScanUtilsMock is an implementation of the Utils interface that can be used during tests.
@ -50,15 +50,15 @@ func (m *ScanUtilsMock) FindPackageJSONFiles(_ *ScanOptions) ([]string, error) {
// 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,
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})
m.DownloadedFiles = append(m.DownloadedFiles, DownloadedFile{SourceURL: url, FilePath: filename})
return nil
}

View File

@ -222,9 +222,6 @@ spec:
- PARAMETERS
- STAGES
- STEPS
resourceRef:
- name: commonPipelineEnvironment
param: artifactVersion
- name: jreDownloadUrl
type: string
description: "[NOT IMPLEMENTED] URL used for downloading the Java Runtime Environment (JRE) required to run the
@ -398,109 +395,10 @@ spec:
resources:
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: golang
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: golang
- name: checkmarx
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: golang
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: maven
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: maven
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: mta
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: mta
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: npm
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: npm
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: pip
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: pip
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: sbt
- name: opensourceConfiguration
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: sbt
- name: buildDescriptor
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: dub
- name: checkmarx
type: stash
conditions:
- conditionRef: strings-equal
params:
- name: scanType
value: dub
outputs:
resources:
- name: commonPipelineEnvironment