1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-16 05:16:08 +02:00

whitesourceExecuteScan-go: Implement parameters "timeout", "createProductFromPipeline" (#2246)

This commit is contained in:
Stephan Aßmus 2020-10-29 09:21:01 +01:00 committed by GitHub
parent f5df553dae
commit 86f335811c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 310 additions and 91 deletions

View File

@ -27,6 +27,8 @@ type ScanOptions = whitesourceExecuteScanOptions
// be available from the whitesource system. // be available from the whitesource system.
type whitesource interface { type whitesource interface {
GetProductByName(productName string) (ws.Product, error) GetProductByName(productName string) (ws.Product, error)
CreateProduct(productName string) (string, error)
SetProductAssignments(productToken string, membership, admins, alertReceivers *ws.Assignment) error
GetProjectsMetaInfo(productToken string) ([]ws.Project, error) GetProjectsMetaInfo(productToken string) ([]ws.Project, error)
GetProjectToken(productToken, projectName string) (string, error) GetProjectToken(productToken, projectName string) (string, error)
GetProjectByToken(projectToken string) (ws.Project, error) GetProjectByToken(projectToken string) (ws.Project, error)
@ -84,7 +86,7 @@ func (w *whitesourceUtilsBundle) Now() time.Time {
return time.Now() return time.Now()
} }
func newWhitesourceUtils() *whitesourceUtilsBundle { func newWhitesourceUtils(config *ScanOptions) *whitesourceUtilsBundle {
utils := whitesourceUtilsBundle{ utils := whitesourceUtilsBundle{
Client: &piperhttp.Client{}, Client: &piperhttp.Client{},
Command: &command.Command{}, Command: &command.Command{},
@ -93,6 +95,8 @@ func newWhitesourceUtils() *whitesourceUtilsBundle {
// Reroute cmd output to logging framework // Reroute cmd output to logging framework
utils.Stdout(log.Writer()) utils.Stdout(log.Writer())
utils.Stderr(log.Writer()) utils.Stderr(log.Writer())
// Configure HTTP Client
utils.SetOptions(piperhttp.ClientOptions{TransportTimeout: time.Duration(config.Timeout) * time.Second})
return &utils return &utils
} }
@ -104,9 +108,10 @@ func newWhitesourceScan(config *ScanOptions) *ws.Scan {
} }
func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData) { func whitesourceExecuteScan(config ScanOptions, _ *telemetry.CustomData) {
utils := newWhitesourceUtils() utils := newWhitesourceUtils(&config)
scan := newWhitesourceScan(&config) scan := newWhitesourceScan(&config)
sys := ws.NewSystem(config.ServiceURL, config.OrgToken, config.UserToken) sys := ws.NewSystem(config.ServiceURL, config.OrgToken, config.UserToken,
time.Duration(config.Timeout)*time.Second)
err := runWhitesourceExecuteScan(&config, scan, utils, sys) err := runWhitesourceExecuteScan(&config, scan, utils, sys)
if err != nil { if err != nil {
log.Entry().WithError(err).Fatal("step execution failed") log.Entry().WithError(err).Fatal("step execution failed")
@ -192,6 +197,26 @@ func checkAndReportScanResults(config *ScanOptions, scan *ws.Scan, utils whiteso
return nil return nil
} }
func createWhiteSourceProduct(config *ScanOptions, sys whitesource) (string, error) {
log.Entry().Infof("Attempting to create new WhiteSource product for '%s'..", config.ProductName)
productToken, err := sys.CreateProduct(config.ProductName)
if err != nil {
return "", fmt.Errorf("failed to create WhiteSource product: %w", err)
}
var admins ws.Assignment
for _, address := range config.EmailAddressesOfInitialProductAdmins {
admins.UserAssignments = append(admins.UserAssignments, ws.UserAssignment{Email: address})
}
err = sys.SetProductAssignments(productToken, nil, &admins, nil)
if err != nil {
return "", fmt.Errorf("failed to set admins on new WhiteSource product: %w", err)
}
return productToken, nil
}
func resolveProjectIdentifiers(config *ScanOptions, scan *ws.Scan, 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{
@ -218,38 +243,66 @@ func resolveProjectIdentifiers(config *ScanOptions, scan *ws.Scan, utils whiteso
} }
scan.ProductVersion = validateProductVersion(config.ProductVersion) scan.ProductVersion = validateProductVersion(config.ProductVersion)
// Get product token if user did not specify one at runtime if err := resolveProductToken(config, sys); err != nil {
if config.ProductToken == "" { return err
log.Entry().Infof("Attempting to resolve product token for product '%s'..", config.ProductName)
product, err := sys.GetProductByName(config.ProductName)
if err != nil {
return err
}
log.Entry().Infof("Resolved product token: '%s'..", product.Token)
config.ProductToken = product.Token
} }
if err := resolveAggregateProjectToken(config, sys); err != nil {
// Get project token if user did not specify one at runtime return err
if config.ProjectToken == "" && config.ProjectName != "" {
log.Entry().Infof("Attempting to resolve project token for project '%s'..", config.ProjectName)
fullProjName := fmt.Sprintf("%s - %s", config.ProjectName, config.ProductVersion)
projectToken, err := sys.GetProjectToken(config.ProductToken, fullProjName)
if err != nil {
return err
}
// A project may not yet exist for this project name-version combo
// It will be created by the scan, we retrieve the token again after scanning.
if projectToken != "" {
log.Entry().Infof("Resolved project token: '%s'..", projectToken)
config.ProjectToken = projectToken
} else {
log.Entry().Infof("Project '%s' not yet present in WhiteSource", fullProjName)
}
} }
return scan.UpdateProjects(config.ProductToken, sys) return scan.UpdateProjects(config.ProductToken, sys)
} }
// resolveProductToken resolves the token of the WhiteSource Product specified by config.ProductName,
// unless the user provided a token in config.ProductToken already, or it was previously resolved.
// If no Product can be found for the given config.ProductName, and the parameter
// config.CreatePipelineFromProduct is set, an attempt will be made to create the product and
// configure the initial product admins.
func resolveProductToken(config *ScanOptions, sys whitesource) error {
if config.ProductToken != "" {
return nil
}
log.Entry().Infof("Attempting to resolve product token for product '%s'..", config.ProductName)
product, err := sys.GetProductByName(config.ProductName)
if err != nil && config.CreateProductFromPipeline {
product = ws.Product{}
product.Token, err = createWhiteSourceProduct(config, sys)
if err != nil {
return err
}
}
if err != nil {
return err
}
log.Entry().Infof("Resolved product token: '%s'..", product.Token)
config.ProductToken = product.Token
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.
func resolveAggregateProjectToken(config *ScanOptions, sys whitesource) error {
if config.ProjectToken != "" || config.ProjectName == "" {
return nil
}
log.Entry().Infof("Attempting to resolve project token for project '%s'..", config.ProjectName)
fullProjName := fmt.Sprintf("%s - %s", config.ProjectName, config.ProductVersion)
projectToken, err := sys.GetProjectToken(config.ProductToken, fullProjName)
if err != nil {
return err
}
// A project may not yet exist for this project name-version combo.
// It will be created by the scan, we retrieve the token again after scanning.
if projectToken != "" {
log.Entry().Infof("Resolved project token: '%s'..", projectToken)
config.ProjectToken = projectToken
} else {
log.Entry().Infof("Project '%s' not yet present in WhiteSource", fullProjName)
}
return nil
}
// validateProductVersion makes sure that the version does not contain a dash "-". // validateProductVersion makes sure that the version does not contain a dash "-".
func validateProductVersion(version string) string { func validateProductVersion(version string) string {
// TrimLeft() removes all "-" from the beginning, unlike TrimPrefix()! // TrimLeft() removes all "-" from the beginning, unlike TrimPrefix()!
@ -411,8 +464,6 @@ func pollProjectStatus(projectToken string, scanTime time.Time, sys whitesource)
return blockUntilProjectIsUpdated(projectToken, sys, scanTime, 20*time.Second, 20*time.Second, 15*time.Minute) return blockUntilProjectIsUpdated(projectToken, sys, scanTime, 20*time.Second, 20*time.Second, 15*time.Minute)
} }
const whitesourceDateTimeLayout = "2006-01-02 15:04:05 -0700"
// blockUntilProjectIsUpdated polls the project LastUpdateDate until it is newer than the given time stamp // 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. // 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 { func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTime time.Time, maxAge, timeBetweenPolls, maxWaitTime time.Duration) error {
@ -426,7 +477,7 @@ func blockUntilProjectIsUpdated(projectToken string, sys whitesource, currentTim
if project.LastUpdateDate == "" { if project.LastUpdateDate == "" {
log.Entry().Infof("last updated time missing from project metadata, retrying") log.Entry().Infof("last updated time missing from project metadata, retrying")
} else { } else {
lastUpdatedTime, err := time.Parse(whitesourceDateTimeLayout, project.LastUpdateDate) lastUpdatedTime, err := time.Parse(ws.DateTimeLayout, project.LastUpdateDate)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse last updated time (%s) of Whitesource project: %w", return fmt.Errorf("failed to parse last updated time (%s) of Whitesource project: %w",
project.LastUpdateDate, err) project.LastUpdateDate, err)

View File

@ -19,7 +19,7 @@ type whitesourceExecuteScanOptions struct {
VersioningModel string `json:"versioningModel,omitempty"` VersioningModel string `json:"versioningModel,omitempty"`
CreateProductFromPipeline bool `json:"createProductFromPipeline,omitempty"` CreateProductFromPipeline bool `json:"createProductFromPipeline,omitempty"`
SecurityVulnerabilities bool `json:"securityVulnerabilities,omitempty"` SecurityVulnerabilities bool `json:"securityVulnerabilities,omitempty"`
Timeout string `json:"timeout,omitempty"` Timeout int `json:"timeout,omitempty"`
AgentDownloadURL string `json:"agentDownloadUrl,omitempty"` AgentDownloadURL string `json:"agentDownloadUrl,omitempty"`
ConfigFilePath string `json:"configFilePath,omitempty"` ConfigFilePath string `json:"configFilePath,omitempty"`
ReportDirectoryName string `json:"reportDirectoryName,omitempty"` ReportDirectoryName string `json:"reportDirectoryName,omitempty"`
@ -131,34 +131,34 @@ func addWhitesourceExecuteScanFlags(cmd *cobra.Command, stepConfig *whitesourceE
cmd.Flags().StringVar(&stepConfig.VersioningModel, "versioningModel", `major`, "The default project versioning model used in case `projectVersion` parameter is empty for creating the version based on the build descriptor version to report results in Whitesource, can be one of `'major'`, `'major-minor'`, `'semantic'`, `'full'`") cmd.Flags().StringVar(&stepConfig.VersioningModel, "versioningModel", `major`, "The default project versioning model used in case `projectVersion` parameter is empty for creating the version based on the build descriptor version to report results in Whitesource, can be one of `'major'`, `'major-minor'`, `'semantic'`, `'full'`")
cmd.Flags().BoolVar(&stepConfig.CreateProductFromPipeline, "createProductFromPipeline", true, "Whether to create the related WhiteSource product on the fly based on the supplied pipeline configuration.") cmd.Flags().BoolVar(&stepConfig.CreateProductFromPipeline, "createProductFromPipeline", true, "Whether to create the related WhiteSource product on the fly based on the supplied pipeline configuration.")
cmd.Flags().BoolVar(&stepConfig.SecurityVulnerabilities, "securityVulnerabilities", true, "Whether security compliance is considered and reported as part of the assessment.") cmd.Flags().BoolVar(&stepConfig.SecurityVulnerabilities, "securityVulnerabilities", true, "Whether security compliance is considered and reported as part of the assessment.")
cmd.Flags().StringVar(&stepConfig.Timeout, "timeout", `0`, "Timeout in seconds until a HTTP call is forcefully terminated.") cmd.Flags().IntVar(&stepConfig.Timeout, "timeout", 900, "Timeout in seconds until an HTTP call is forcefully terminated.")
cmd.Flags().StringVar(&stepConfig.AgentDownloadURL, "agentDownloadUrl", `https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar`, "URL used to download the latest version of the WhiteSource Unified Agent.") cmd.Flags().StringVar(&stepConfig.AgentDownloadURL, "agentDownloadUrl", `https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar`, "URL used to download the latest version of the WhiteSource Unified Agent.")
cmd.Flags().StringVar(&stepConfig.ConfigFilePath, "configFilePath", `./wss-generated-file.config`, "Explicit path to the WhiteSource Unified Agent configuration file.") cmd.Flags().StringVar(&stepConfig.ConfigFilePath, "configFilePath", `./wss-generated-file.config`, "Explicit path to the WhiteSource Unified Agent configuration file.")
cmd.Flags().StringVar(&stepConfig.ReportDirectoryName, "reportDirectoryName", `whitesource-reports`, "Name of the directory to save vulnerability/risk reports to") cmd.Flags().StringVar(&stepConfig.ReportDirectoryName, "reportDirectoryName", `whitesource-reports`, "Name of the directory to save vulnerability/risk reports to")
cmd.Flags().BoolVar(&stepConfig.AggregateVersionWideReport, "aggregateVersionWideReport", false, "This does not run a scan, instead just generated a report for all projects with projectVersion = config.ProductVersion") cmd.Flags().BoolVar(&stepConfig.AggregateVersionWideReport, "aggregateVersionWideReport", false, "This does not run a scan, instead just generated a report for all projects with projectVersion = config.ProductVersion")
cmd.Flags().StringVar(&stepConfig.VulnerabilityReportFormat, "vulnerabilityReportFormat", `xlsx`, "Format of the file the vulnerability report is written to.") cmd.Flags().StringVar(&stepConfig.VulnerabilityReportFormat, "vulnerabilityReportFormat", `xlsx`, "Format of the file the vulnerability report is written to.")
cmd.Flags().StringVar(&stepConfig.ParallelLimit, "parallelLimit", `15`, "Limit of parallel jobs being run at once in case of `scanType: 'mta'` based scenarios, defaults to `15`.") cmd.Flags().StringVar(&stepConfig.ParallelLimit, "parallelLimit", `15`, "[NOT IMPLEMENTED] Limit of parallel jobs being run at once in case of `scanType: 'mta'` based scenarios, defaults to `15`.")
cmd.Flags().BoolVar(&stepConfig.Reporting, "reporting", true, "Whether assessment is being done at all, defaults to `true`") cmd.Flags().BoolVar(&stepConfig.Reporting, "reporting", true, "Whether assessment is being done at all, defaults to `true`")
cmd.Flags().StringVar(&stepConfig.ServiceURL, "serviceUrl", `https://saas.whitesourcesoftware.com/api`, "URL to the WhiteSource server API used for communication.") cmd.Flags().StringVar(&stepConfig.ServiceURL, "serviceUrl", `https://saas.whitesourcesoftware.com/api`, "URL to the WhiteSource server API used for communication.")
cmd.Flags().StringSliceVar(&stepConfig.BuildDescriptorExcludeList, "buildDescriptorExcludeList", []string{`unit-tests/pom.xml`, `integration-tests/pom.xml`}, "List of build descriptors and therefore modules to exclude from the scan and assessment activities.") cmd.Flags().StringSliceVar(&stepConfig.BuildDescriptorExcludeList, "buildDescriptorExcludeList", []string{`unit-tests/pom.xml`, `integration-tests/pom.xml`}, "List of build descriptors and therefore modules to exclude from the scan and assessment activities.")
cmd.Flags().StringVar(&stepConfig.OrgToken, "orgToken", os.Getenv("PIPER_orgToken"), "WhiteSource token identifying your organization.") cmd.Flags().StringVar(&stepConfig.OrgToken, "orgToken", os.Getenv("PIPER_orgToken"), "WhiteSource token identifying your organization.")
cmd.Flags().StringVar(&stepConfig.UserToken, "userToken", os.Getenv("PIPER_userToken"), "WhiteSource token identifying the user executing the scan") cmd.Flags().StringVar(&stepConfig.UserToken, "userToken", os.Getenv("PIPER_userToken"), "WhiteSource token identifying the user executing the scan.")
cmd.Flags().BoolVar(&stepConfig.LicensingVulnerabilities, "licensingVulnerabilities", true, "Whether license compliance is considered and reported as part of the assessment.") cmd.Flags().BoolVar(&stepConfig.LicensingVulnerabilities, "licensingVulnerabilities", true, "[NOT IMPLEMENTED] Whether license compliance is considered and reported as part of the assessment.")
cmd.Flags().StringVar(&stepConfig.AgentFileName, "agentFileName", `wss-unified-agent.jar`, "Locally used name for the Unified Agent jar file after download.") cmd.Flags().StringVar(&stepConfig.AgentFileName, "agentFileName", `wss-unified-agent.jar`, "Locally used name for the Unified Agent jar file after download.")
cmd.Flags().StringSliceVar(&stepConfig.EmailAddressesOfInitialProductAdmins, "emailAddressesOfInitialProductAdmins", []string{}, "The list of email addresses to assign as product admins for newly created WhiteSource products.") cmd.Flags().StringSliceVar(&stepConfig.EmailAddressesOfInitialProductAdmins, "emailAddressesOfInitialProductAdmins", []string{}, "The list of email addresses to assign as product admins for newly created WhiteSource products.")
cmd.Flags().StringVar(&stepConfig.ProductVersion, "productVersion", os.Getenv("PIPER_productVersion"), "Version of the WhiteSource product to be created and used for results aggregation, usually determined automatically.") cmd.Flags().StringVar(&stepConfig.ProductVersion, "productVersion", os.Getenv("PIPER_productVersion"), "Version of the WhiteSource product to be created and used for results aggregation, usually determined automatically.")
cmd.Flags().StringVar(&stepConfig.JreDownloadURL, "jreDownloadUrl", os.Getenv("PIPER_jreDownloadUrl"), "URL used for downloading the Java Runtime Environment (JRE) required to run the WhiteSource Unified Agent.") cmd.Flags().StringVar(&stepConfig.JreDownloadURL, "jreDownloadUrl", os.Getenv("PIPER_jreDownloadUrl"), "[NOT IMPLEMENTED] URL used for downloading the Java Runtime Environment (JRE) required to run the WhiteSource Unified Agent.")
cmd.Flags().StringVar(&stepConfig.ProductName, "productName", os.Getenv("PIPER_productName"), "Name of the WhiteSource product to be created and used for results aggregation.") cmd.Flags().StringVar(&stepConfig.ProductName, "productName", os.Getenv("PIPER_productName"), "Name of the WhiteSource product used for results aggregation. This parameter is mandatory if the parameter `createProductFromPipeline` is set to `true` and the WhiteSource product does not yet exist. It is also mandatory if the parameter `productToken` is not provided.")
cmd.Flags().StringVar(&stepConfig.ProjectName, "projectName", os.Getenv("PIPER_projectName"), "The project used for reporting results in Whitesource") cmd.Flags().StringVar(&stepConfig.ProjectName, "projectName", os.Getenv("PIPER_projectName"), "The project name used for reporting results in WhiteSource. When provided, all source modules will be scanned into one aggregated WhiteSource project. For scan types `maven`, `mta`, `npm`, the default is to generate one WhiteSource project per module, whereas the project name is derived from the module's build descriptor. For NPM modules, project aggregation is not supported, the last scanned NPM module will override all previously aggregated scan results!")
cmd.Flags().StringVar(&stepConfig.ProjectToken, "projectToken", os.Getenv("PIPER_projectToken"), "Project token to execute scan on") cmd.Flags().StringVar(&stepConfig.ProjectToken, "projectToken", os.Getenv("PIPER_projectToken"), "Project token to execute scan on. Ignored for scan types `maven`, `mta` and `npm`. Used for project aggregation when scanning with the Unified Agent and can be provided as an alternative to `projectName`.")
cmd.Flags().StringVar(&stepConfig.VulnerabilityReportTitle, "vulnerabilityReportTitle", `WhiteSource Security Vulnerability Report`, "Title of vulnerability report written during the assessment phase.") cmd.Flags().StringVar(&stepConfig.VulnerabilityReportTitle, "vulnerabilityReportTitle", `WhiteSource Security Vulnerability Report`, "Title of vulnerability report written during the assessment phase.")
cmd.Flags().StringVar(&stepConfig.InstallCommand, "installCommand", os.Getenv("PIPER_installCommand"), "Install command that can be used to populate the default docker image for some scenarios.") cmd.Flags().StringVar(&stepConfig.InstallCommand, "installCommand", os.Getenv("PIPER_installCommand"), "[NOT IMPLEMENTED] Install command that can be used to populate the default docker image for some scenarios.")
cmd.Flags().StringVar(&stepConfig.ScanType, "scanType", os.Getenv("PIPER_scanType"), "Type of development stack used to implement the solution.") cmd.Flags().StringVar(&stepConfig.ScanType, "scanType", os.Getenv("PIPER_scanType"), "Type of development stack used to implement the solution. For scan types other than `mta`, `maven`, and `npm`, the WhiteSource Unified Agent is downloaded and used to perform the scan. If the parameter is not provided, it is derived from the parameter `buildTool`, which is usually configured in the general section of the pipeline config file.")
cmd.Flags().StringVar(&stepConfig.CvssSeverityLimit, "cvssSeverityLimit", `-1`, "Limit of tolerable CVSS v3 score upon assessment and in consequence fails the build, defaults to `-1`.") cmd.Flags().StringVar(&stepConfig.CvssSeverityLimit, "cvssSeverityLimit", `-1`, "Limit of tolerable CVSS v3 score upon assessment and in consequence fails the build, defaults to `-1`.")
cmd.Flags().StringVar(&stepConfig.Includes, "includes", `**\/src\/main\/**\/*.java **\/*.py **\/*.go **\/*.js **\/*.ts`, "Space separated list of file path patterns to include in the scan, slashes must be escaped for sed.") cmd.Flags().StringVar(&stepConfig.Includes, "includes", `**\/src\/main\/**\/*.java **\/*.py **\/*.go **\/*.js **\/*.ts`, "Space separated list of file path patterns to include in the scan, slashes must be escaped for sed.")
cmd.Flags().StringVar(&stepConfig.Excludes, "excludes", `tests/**/*.py **/src/test/**/*.java`, "Space separated list of file path patterns to exclude in the scan") cmd.Flags().StringVar(&stepConfig.Excludes, "excludes", `tests/**/*.py **/src/test/**/*.java`, "Space separated list of file path patterns to exclude in the scan")
cmd.Flags().StringVar(&stepConfig.ProductToken, "productToken", os.Getenv("PIPER_productToken"), "Token of the WhiteSource product to be created and used for results aggregation, usually determined automatically.") cmd.Flags().StringVar(&stepConfig.ProductToken, "productToken", os.Getenv("PIPER_productToken"), "Token of the WhiteSource product to be created and used for results aggregation, usually determined automatically. Can optionally be provided as an alternative to `productName`.")
cmd.Flags().StringVar(&stepConfig.AgentParameters, "agentParameters", os.Getenv("PIPER_agentParameters"), "Additional parameters passed to the Unified Agent command line.") cmd.Flags().StringVar(&stepConfig.AgentParameters, "agentParameters", os.Getenv("PIPER_agentParameters"), "[NOT IMPLEMENTED] Additional parameters passed to the Unified Agent command line.")
cmd.Flags().StringVar(&stepConfig.ProjectSettingsFile, "projectSettingsFile", os.Getenv("PIPER_projectSettingsFile"), "Path to the mvn settings file that should be used as project settings file.") cmd.Flags().StringVar(&stepConfig.ProjectSettingsFile, "projectSettingsFile", os.Getenv("PIPER_projectSettingsFile"), "Path to the mvn settings file that should be used as project settings file.")
cmd.Flags().StringVar(&stepConfig.GlobalSettingsFile, "globalSettingsFile", os.Getenv("PIPER_globalSettingsFile"), "Path to the mvn settings file that should be used as global settings file.") cmd.Flags().StringVar(&stepConfig.GlobalSettingsFile, "globalSettingsFile", os.Getenv("PIPER_globalSettingsFile"), "Path to the mvn settings file that should be used as global settings file.")
cmd.Flags().StringVar(&stepConfig.M2Path, "m2Path", os.Getenv("PIPER_m2Path"), "Path to the location of the local repository that should be used.") cmd.Flags().StringVar(&stepConfig.M2Path, "m2Path", os.Getenv("PIPER_m2Path"), "Path to the location of the local repository that should be used.")
@ -229,7 +229,7 @@ func whitesourceExecuteScanMetadata() config.StepData {
Name: "timeout", Name: "timeout",
ResourceRef: []config.ResourceReference{}, ResourceRef: []config.ResourceReference{},
Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
Type: "string", Type: "int",
Mandatory: false, Mandatory: false,
Aliases: []config.Alias{}, Aliases: []config.Alias{},
}, },

View File

@ -122,6 +122,26 @@ func TestResolveProjectIdentifiers(t *testing.T) {
// assert // assert
assert.EqualError(t, err, "no product with name 'does-not-exist' found in Whitesource") assert.EqualError(t, err, "no product with name 'does-not-exist' found in Whitesource")
}) })
t.Run("product not found, created from pipeline", func(t *testing.T) {
// init
config := ScanOptions{
BuildTool: "mta",
CreateProductFromPipeline: true,
EmailAddressesOfInitialProductAdmins: []string{"user1@domain.org", "user2@domain.org"},
VersioningModel: "major",
ProductName: "created-by-pipeline",
}
utilsMock := newWhitesourceUtilsMock()
systemMock := ws.NewSystemMock("ignored")
scan := newWhitesourceScan(&config)
// test
err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
// assert
assert.NoError(t, err)
assert.Len(t, systemMock.Products, 2)
assert.Equal(t, "created-by-pipeline", systemMock.Products[1].Name)
assert.Equal(t, "mock-product-token-1", config.ProductToken)
})
} }
func TestBlockUntilProjectIsUpdated(t *testing.T) { func TestBlockUntilProjectIsUpdated(t *testing.T) {
@ -129,7 +149,7 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Run("already new enough", func(t *testing.T) { t.Run("already new enough", func(t *testing.T) {
// init // init
nowString := "2010-05-30 00:15:00 +0100" nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(whitesourceDateTimeLayout, nowString) now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
@ -143,7 +163,7 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Run("timeout while polling", func(t *testing.T) { t.Run("timeout while polling", func(t *testing.T) {
// init // init
nowString := "2010-05-30 00:15:00 +0100" nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(whitesourceDateTimeLayout, nowString) now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
@ -159,7 +179,7 @@ func TestBlockUntilProjectIsUpdated(t *testing.T) {
t.Run("timeout while polling, no update time", func(t *testing.T) { t.Run("timeout while polling, no update time", func(t *testing.T) {
// init // init
nowString := "2010-05-30 00:15:00 +0100" nowString := "2010-05-30 00:15:00 +0100"
now, err := time.Parse(whitesourceDateTimeLayout, nowString) now, err := time.Parse(ws.DateTimeLayout, nowString)
if err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
@ -297,14 +317,14 @@ func TestCheckAndReportScanResults(t *testing.T) {
} }
scan := newWhitesourceScan(config) scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock() utils := newWhitesourceUtilsMock()
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout)) system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
// test // test
err := checkAndReportScanResults(config, scan, utils, system) err := checkAndReportScanResults(config, scan, utils, system)
// assert // assert
assert.NoError(t, err) assert.NoError(t, err)
vPath := filepath.Join("report-dir", "mock-project-vulnerability-report.txt") vPath := filepath.Join("mock-reports", "mock-project-vulnerability-report.txt")
assert.False(t, utils.HasWrittenFile(vPath)) assert.False(t, utils.HasWrittenFile(vPath))
rPath := filepath.Join("report-dir", "mock-project-risk-report.pdf") rPath := filepath.Join("mock-reports", "mock-project-risk-report.pdf")
assert.False(t, utils.HasWrittenFile(rPath)) assert.False(t, utils.HasWrittenFile(rPath))
}) })
t.Run("check vulnerabilities - invalid limit", func(t *testing.T) { t.Run("check vulnerabilities - invalid limit", func(t *testing.T) {
@ -315,7 +335,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
} }
scan := newWhitesourceScan(config) scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock() utils := newWhitesourceUtilsMock()
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout)) system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
// test // test
err := checkAndReportScanResults(config, scan, utils, system) err := checkAndReportScanResults(config, scan, utils, system)
// assert // assert
@ -333,7 +353,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
} }
scan := newWhitesourceScan(config) scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock() utils := newWhitesourceUtilsMock()
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout)) system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
// test // test
err := checkAndReportScanResults(config, scan, utils, system) err := checkAndReportScanResults(config, scan, utils, system)
// assert // assert
@ -352,7 +372,7 @@ func TestCheckAndReportScanResults(t *testing.T) {
} }
scan := newWhitesourceScan(config) scan := newWhitesourceScan(config)
utils := newWhitesourceUtilsMock() utils := newWhitesourceUtilsMock()
system := ws.NewSystemMock(time.Now().Format(whitesourceDateTimeLayout)) system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
// test // test
err := checkAndReportScanResults(config, scan, utils, system) err := checkAndReportScanResults(config, scan, utils, system)
// assert // assert

View File

@ -2,7 +2,11 @@
package whitesource package whitesource
import "fmt" import (
"fmt"
"strconv"
"time"
)
// SystemMock stores a number of WhiteSource objects and, based on that, mocks the behavior of System. // SystemMock stores a number of WhiteSource objects and, based on that, mocks the behavior of System.
type SystemMock struct { type SystemMock struct {
@ -25,6 +29,31 @@ func (m *SystemMock) GetProductByName(productName string) (Product, error) {
return Product{}, fmt.Errorf("no product with name '%s' found in Whitesource", productName) return Product{}, fmt.Errorf("no product with name '%s' found in Whitesource", productName)
} }
// CreateProduct appends a new Product to the system mock and returns its token ("mock-product-token-<index>").
func (m *SystemMock) CreateProduct(productName string) (string, error) {
now := time.Now().Format(DateTimeLayout)
productIndex := len(m.Products)
product := Product{
Name: productName,
Token: "mock-product-token-" + strconv.Itoa(productIndex),
CreationDate: now,
LastUpdateDate: now,
}
m.Products = append(m.Products, product)
return product.Token, nil
}
// SetProductAssignments checks if the system mock contains a product with the given token and returns
// an error depending on that, but otherwise does nothing with the provided arguments.
func (m *SystemMock) SetProductAssignments(productToken string, _, _, _ *Assignment) error {
for _, product := range m.Products {
if product.Token == productToken {
return nil
}
}
return fmt.Errorf("no product with that token")
}
// GetProjectsMetaInfo returns the list of Projects stored in the mock or an error if token is unknown. // 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) { func (m *SystemMock) GetProjectsMetaInfo(productToken string) ([]Project, error) {
for _, product := range m.Products { for _, product := range m.Products {

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time"
piperhttp "github.com/SAP/jenkins-library/pkg/http" piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/log"
@ -20,6 +21,23 @@ type Product struct {
LastUpdateDate string `json:"lastUpdatedDate,omitempty"` LastUpdateDate string `json:"lastUpdatedDate,omitempty"`
} }
// Assignment describes a list of UserAssignments and GroupAssignments which can be attributed to a WhiteSource Product.
type Assignment struct {
UserAssignments []UserAssignment `json:"userAssignments,omitempty"`
GroupAssignments []GroupAssignment `json:"groupAssignments,omitempty"`
}
// UserAssignment holds an email address for a WhiteSource user
// which can be assigned to a WhiteSource Product in a specific role.
type UserAssignment struct {
Email string `json:"email,omitempty"`
}
// GroupAssignment refers to the name of a particular group in WhiteSource.
type GroupAssignment struct {
Name string `json:"name,omitempty"`
}
// Alert // Alert
type Alert struct { type Alert struct {
Vulnerability Vulnerability `json:"vulnerability"` Vulnerability Vulnerability `json:"vulnerability"`
@ -63,13 +81,18 @@ type Project struct {
// Request defines a request object to be sent to the WhiteSource system // Request defines a request object to be sent to the WhiteSource system
type Request struct { type Request struct {
RequestType string `json:"requestType,omitempty"` RequestType string `json:"requestType,omitempty"`
UserKey string `json:"userKey,omitempty"` UserKey string `json:"userKey,omitempty"`
ProductToken string `json:"productToken,omitempty"` ProductToken string `json:"productToken,omitempty"`
ProductName string `json:"productName,omitempty"` ProductName string `json:"productName,omitempty"`
ProjectToken string `json:"projectToken,omitempty"` ProjectToken string `json:"projectToken,omitempty"`
OrgToken string `json:"orgToken,omitempty"` OrgToken string `json:"orgToken,omitempty"`
Format string `json:"format,omitempty"` Format string `json:"format,omitempty"`
ProductAdmins *Assignment `json:"productAdmins,omitempty"`
ProductMembership *Assignment `json:"productMembership,omitempty"`
AlertsEmailReceivers *Assignment `json:"alertsEmailReceivers,omitempty"`
ProductApprovers *Assignment `json:"productApprovers,omitempty"`
ProductIntegrators *Assignment `json:"productIntegrators,omitempty"`
} }
// System defines a WhiteSource System including respective tokens (e.g. org token, user token) // System defines a WhiteSource System including respective tokens (e.g. org token, user token)
@ -80,13 +103,18 @@ type System struct {
userToken string userToken string
} }
// DateTimeLayout is the layout of the time format used by the WhiteSource API.
const DateTimeLayout = "2006-01-02 15:04:05 -0700"
// NewSystem constructs a new System instance // NewSystem constructs a new System instance
func NewSystem(serverURL, orgToken, userToken string) *System { func NewSystem(serverURL, orgToken, userToken string, timeout time.Duration) *System {
httpClient := &piperhttp.Client{}
httpClient.SetOptions(piperhttp.ClientOptions{TransportTimeout: timeout})
return &System{ return &System{
serverURL: serverURL, serverURL: serverURL,
orgToken: orgToken, orgToken: orgToken,
userToken: userToken, userToken: userToken,
httpClient: &piperhttp.Client{}, httpClient: httpClient,
} }
} }
@ -126,6 +154,45 @@ func (s *System) GetProductByName(productName string) (Product, error) {
return Product{}, fmt.Errorf("product '%v' not found in WhiteSource", productName) return Product{}, fmt.Errorf("product '%v' not found in WhiteSource", productName)
} }
// CreateProduct creates a new WhiteSource product and returns its product token.
func (s *System) CreateProduct(productName string) (string, error) {
wsResponse := struct {
ProductToken string `json:"productToken"`
}{
ProductToken: "",
}
req := Request{
RequestType: "createProduct",
ProductName: productName,
}
err := s.sendRequestAndDecodeJSON(req, &wsResponse)
if err != nil {
return "", errors.Wrap(err, "WhiteSource request failed")
}
return wsResponse.ProductToken, nil
}
// SetProductAssignments assigns various types of membership to a WhiteSource Product.
func (s *System) SetProductAssignments(productToken string, membership, admins, alertReceivers *Assignment) error {
req := Request{
RequestType: "setProductAssignments",
ProductToken: productToken,
ProductMembership: membership,
ProductAdmins: admins,
AlertsEmailReceivers: alertReceivers,
}
err := s.sendRequestAndDecodeJSON(req, nil)
if err != nil {
return errors.Wrap(err, "WhiteSource request failed")
}
return nil
}
// GetProjectsMetaInfo retrieves the registered projects for a specific WhiteSource product // GetProjectsMetaInfo retrieves the registered projects for a specific WhiteSource product
func (s *System) GetProjectsMetaInfo(productToken string) ([]Project, error) { func (s *System) GetProjectsMetaInfo(productToken string) ([]Project, error) {
wsResponse := struct { wsResponse := struct {
@ -343,19 +410,21 @@ func (s *System) sendRequestAndDecodeJSON(req Request, result interface{}) error
log.Entry().Debugf("response: %v", string(respBody)) log.Entry().Debugf("response: %v", string(respBody))
errorResponse := struct { errorResponse := struct {
ErrorCode string `json:"errorCode"` ErrorCode int `json:"errorCode"`
ErrorMessage string `json:"errorMessage"` ErrorMessage string `json:"errorMessage"`
}{} }{}
err = json.Unmarshal(respBody, &errorResponse) err = json.Unmarshal(respBody, &errorResponse)
if err == nil && errorResponse.ErrorCode != "" { if err == nil && errorResponse.ErrorCode != 0 {
return fmt.Errorf("invalid request, error code %s, message '%s'", return fmt.Errorf("invalid request, error code %v, message '%s'",
errorResponse.ErrorCode, errorResponse.ErrorMessage) errorResponse.ErrorCode, errorResponse.ErrorMessage)
} }
err = json.Unmarshal(respBody, result) if result != nil {
if err != nil { err = json.Unmarshal(respBody, result)
return errors.Wrap(err, "failed to parse WhiteSource response") if err != nil {
return errors.Wrap(err, "failed to parse WhiteSource response")
}
} }
return nil return nil
} }

View File

@ -2,13 +2,13 @@ package whitesource
import ( import (
"bytes" "bytes"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"testing" "testing"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/stretchr/testify/assert"
) )
type whitesourceMockClient struct { type whitesourceMockClient struct {
@ -57,6 +57,42 @@ func TestGetProductsMetaInfo(t *testing.T) {
assert.Equal(t, []Product{{Name: "Test Product", Token: "test_product_token", CreationDate: "2020-01-01 00:00:00", LastUpdateDate: "2020-01-01 01:00:00"}}, products) assert.Equal(t, []Product{{Name: "Test Product", Token: "test_product_token", CreationDate: "2020-01-01 00:00:00", LastUpdateDate: "2020-01-01 01:00:00"}}, products)
} }
func TestCreateProduct(t *testing.T) {
t.Parallel()
t.Run("not allowed error", func(t *testing.T) {
// init
myTestClient := whitesourceMockClient{
responseBody: `{"errorCode":5001,"errorMessage":"User is not allowed to perform this action"}`,
}
expectedRequestBody := `{"requestType":"createProduct","userKey":"test_user_token","productName":"test_product_name","orgToken":"test_org_token"}`
sys := System{serverURL: "https://my.test.server", httpClient: &myTestClient, orgToken: "test_org_token", userToken: "test_user_token"}
// test
productToken, err := sys.CreateProduct("test_product_name")
// assert
assert.EqualError(t, err, "WhiteSource request failed: invalid request, error code 5001, message 'User is not allowed to perform this action'")
requestBody, err := ioutil.ReadAll(myTestClient.requestBody)
require.NoError(t, err)
assert.Equal(t, "", productToken)
assert.Equal(t, expectedRequestBody, string(requestBody))
})
t.Run("happy path", func(t *testing.T) {
// init
myTestClient := whitesourceMockClient{
responseBody: `{"productToken":"test_product_token"}`,
}
expectedRequestBody := `{"requestType":"createProduct","userKey":"test_user_token","productName":"test_product_name","orgToken":"test_org_token"}`
sys := System{serverURL: "https://my.test.server", httpClient: &myTestClient, orgToken: "test_org_token", userToken: "test_user_token"}
// test
productToken, err := sys.CreateProduct("test_product_name")
// assert
assert.NoError(t, err)
requestBody, err := ioutil.ReadAll(myTestClient.requestBody)
require.NoError(t, err)
assert.Equal(t, "test_product_token", productToken)
assert.Equal(t, expectedRequestBody, string(requestBody))
})
}
func TestGetMetaInfoForProduct(t *testing.T) { func TestGetMetaInfoForProduct(t *testing.T) {
myTestClient := whitesourceMockClient{ myTestClient := whitesourceMockClient{
responseBody: `{ responseBody: `{

View File

@ -77,13 +77,13 @@ spec:
- STEPS - STEPS
default: true default: true
- name: timeout - name: timeout
type: string type: int
description: "Timeout in seconds until a HTTP call is forcefully terminated." description: "Timeout in seconds until an HTTP call is forcefully terminated."
scope: scope:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
default: 0 default: 900
- name: agentDownloadUrl - name: agentDownloadUrl
type: string type: string
description: "URL used to download the latest version of the WhiteSource Unified Agent." description: "URL used to download the latest version of the WhiteSource Unified Agent."
@ -128,7 +128,7 @@ spec:
default: xlsx default: xlsx
- name: parallelLimit - name: parallelLimit
type: string type: string
description: 'Limit of parallel jobs being run at once in case of `scanType: description: '[NOT IMPLEMENTED] Limit of parallel jobs being run at once in case of `scanType:
''mta''` based scenarios, defaults to `15`.' ''mta''` based scenarios, defaults to `15`.'
scope: scope:
- PARAMETERS - PARAMETERS
@ -151,7 +151,7 @@ spec:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
default: https://saas.whitesourcesoftware.com/api default: "https://saas.whitesourcesoftware.com/api"
- name: buildDescriptorExcludeList - name: buildDescriptorExcludeList
type: "[]string" type: "[]string"
description: "List of build descriptors and therefore modules to exclude from the scan and assessment activities." description: "List of build descriptors and therefore modules to exclude from the scan and assessment activities."
@ -175,7 +175,7 @@ spec:
type: secret type: secret
- name: userToken - name: userToken
type: string type: string
description: "WhiteSource token identifying the user executing the scan" description: "WhiteSource token identifying the user executing the scan."
scope: scope:
- GENERAL - GENERAL
- PARAMETERS - PARAMETERS
@ -188,7 +188,7 @@ spec:
type: secret type: secret
- name: licensingVulnerabilities - name: licensingVulnerabilities
type: bool type: bool
description: "Whether license compliance is considered and reported as part of the assessment." description: "[NOT IMPLEMENTED] Whether license compliance is considered and reported as part of the assessment."
scope: scope:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
@ -201,7 +201,7 @@ spec:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
default: wss-unified-agent.jar default: "wss-unified-agent.jar"
- name: emailAddressesOfInitialProductAdmins - name: emailAddressesOfInitialProductAdmins
type: "[]string" type: "[]string"
description: "The list of email addresses to assign as product admins for newly created WhiteSource products." description: "The list of email addresses to assign as product admins for newly created WhiteSource products."
@ -223,7 +223,7 @@ spec:
param: artifactVersion param: artifactVersion
- name: jreDownloadUrl - name: jreDownloadUrl
type: string type: string
description: "URL used for downloading the Java Runtime Environment (JRE) required to run the description: "[NOT IMPLEMENTED] URL used for downloading the Java Runtime Environment (JRE) required to run the
WhiteSource Unified Agent." WhiteSource Unified Agent."
scope: scope:
- GENERAL - GENERAL
@ -232,7 +232,10 @@ spec:
- STEPS - STEPS
- name: productName - name: productName
type: string type: string
description: "Name of the WhiteSource product to be created and used for results aggregation." description: "Name of the WhiteSource product used for results aggregation.
This parameter is mandatory if the parameter `createProductFromPipeline` is set to `true`
and the WhiteSource product does not yet exist.
It is also mandatory if the parameter `productToken` is not provided."
mandatory: true mandatory: true
scope: scope:
- GENERAL - GENERAL
@ -243,14 +246,21 @@ spec:
aliases: aliases:
- name: whitesourceProjectName - name: whitesourceProjectName
type: string type: string
description: "The project used for reporting results in Whitesource" description: "The project name used for reporting results in WhiteSource.
When provided, all source modules will be scanned into one aggregated WhiteSource project.
For scan types `maven`, `mta`, `npm`, the default is to generate one WhiteSource project per module,
whereas the project name is derived from the module's build descriptor.
For NPM modules, project aggregation is not supported, the last scanned NPM module will override all
previously aggregated scan results!"
scope: scope:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
- name: projectToken - name: projectToken
type: string type: string
description: Project token to execute scan on description: "Project token to execute scan on. Ignored for scan types `maven`, `mta` and `npm`.
Used for project aggregation when scanning with the Unified Agent and can be provided as an
alternative to `projectName`."
scope: scope:
- GENERAL - GENERAL
- PARAMETERS - PARAMETERS
@ -263,17 +273,21 @@ spec:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
default: WhiteSource Security Vulnerability Report default: "WhiteSource Security Vulnerability Report"
- name: installCommand - name: installCommand
type: string type: string
description: "Install command that can be used to populate the default docker image for some scenarios." description: "[NOT IMPLEMENTED] Install command that can be used to populate the default docker image for some scenarios."
scope: scope:
- PARAMETERS - PARAMETERS
- STAGES - STAGES
- STEPS - STEPS
- name: scanType - name: scanType
type: string type: string
description: "Type of development stack used to implement the solution." description: "Type of development stack used to implement the solution.
For scan types other than `mta`, `maven`, and `npm`,
the WhiteSource Unified Agent is downloaded and used to perform the scan.
If the parameter is not provided, it is derived from the parameter `buildTool`,
which is usually configured in the general section of the pipeline config file."
scope: scope:
- GENERAL - GENERAL
- PARAMETERS - PARAMETERS
@ -308,7 +322,7 @@ spec:
- name: productToken - name: productToken
type: string type: string
description: "Token of the WhiteSource product to be created and used for results aggregation, description: "Token of the WhiteSource product to be created and used for results aggregation,
usually determined automatically." usually determined automatically. Can optionally be provided as an alternative to `productName`."
scope: scope:
- GENERAL - GENERAL
- PARAMETERS - PARAMETERS
@ -316,7 +330,7 @@ spec:
- STEPS - STEPS
- name: agentParameters - name: agentParameters
type: string type: string
description: "Additional parameters passed to the Unified Agent command line." description: "[NOT IMPLEMENTED] Additional parameters passed to the Unified Agent command line."
scope: scope:
- PARAMETERS - PARAMETERS
- STAGES - STAGES