From cbe368fe364799de2167292bd41daae587bbe1c0 Mon Sep 17 00:00:00 2001
From: Sven Merk <33895725+nevskrem@users.noreply.github.com>
Date: Mon, 27 Jan 2020 23:40:53 +0100
Subject: [PATCH] Checkmarx as golang (#1075)
* Added base functionality for checkmarx interaction
* Extend http client with file upload capabilities
* Latest changes
* Add debug logging
* Introduce Uploader interface
* Add tests for checkmarx client
* Hook new checkmarx command
* Improve coverage
* Add tests
* Improved test coverage and fixed code
* Add influx reporting
* Add alternation capabilities
* Add groovy step
* Try fix cmd
* Enhancements
* Fix report generation
* Final performance improvements
* Fix code
* Structure code, cleanup
* Improvements
* Fix codeclimate issue
* Update groovy
* Adapt latest changes to http
* Fix test
* Fix http tests
* Fix test
* Fix test
* Fix test 2
* Fix code
* Fix code 2
* Fix code
* Code
* Fix
* Fix
* Add report and link handling
* Fix returns, add groovy test
* Review comments
* Added doc template
* Docs update
* Remove SAP internals
* Better status display
* Add name to link
* Fix test
* Fix
* Fix verbose handling
* Fix verbose handling 2
* Fix verbose handling 3
* Fix
* Tiny improvements
* Regenerate
* Fix test
* Fix test code
* Fix verbosity issue
* Fix test
* Fix test
* Fix test
---
cmd/checkmarxExecuteScan.go | 582 +++++++++++++++
cmd/checkmarxExecuteScan_generated.go | 414 +++++++++++
cmd/checkmarxExecuteScan_generated_test.go | 16 +
cmd/checkmarxExecuteScan_test.go | 544 ++++++++++++++
cmd/getConfig_test.go | 2 +-
cmd/piper.go | 7 +
.../docs/steps/checkmarxExecuteScan.md | 7 +
go.mod | 1 +
go.sum | 2 +
pkg/checkmarx/checkmarx.go | 703 ++++++++++++++++++
pkg/checkmarx/checkmarx_test.go | 566 ++++++++++++++
pkg/config/flags.go | 2 +
pkg/config/stepmeta.go | 2 +-
pkg/config/stepmeta_test.go | 35 +-
pkg/http/http.go | 6 +-
pkg/http/http_test.go | 6 +-
pkg/piperutils/slices.go | 13 +
pkg/piperutils/slices_test.go | 17 +
pkg/piperutils/stepResults.go | 41 +
pkg/piperutils/stepResults_test.go | 57 ++
resources/metadata/checkmarx.yaml | 262 +++++++
src/com/sap/piper/JenkinsUtils.groovy | 18 +-
test/groovy/CheckmarxExecuteScanTest.groovy | 66 ++
test/groovy/CommonStepsTest.groovy | 1 +
vars/checkmarxExecuteScan.groovy | 58 ++
25 files changed, 3404 insertions(+), 24 deletions(-)
create mode 100644 cmd/checkmarxExecuteScan.go
create mode 100644 cmd/checkmarxExecuteScan_generated.go
create mode 100644 cmd/checkmarxExecuteScan_generated_test.go
create mode 100644 cmd/checkmarxExecuteScan_test.go
create mode 100644 documentation/docs/steps/checkmarxExecuteScan.md
create mode 100644 pkg/checkmarx/checkmarx.go
create mode 100644 pkg/checkmarx/checkmarx_test.go
create mode 100644 pkg/piperutils/slices.go
create mode 100644 pkg/piperutils/slices_test.go
create mode 100644 pkg/piperutils/stepResults.go
create mode 100644 pkg/piperutils/stepResults_test.go
create mode 100644 resources/metadata/checkmarx.yaml
create mode 100644 test/groovy/CheckmarxExecuteScanTest.groovy
create mode 100644 vars/checkmarxExecuteScan.groovy
diff --git a/cmd/checkmarxExecuteScan.go b/cmd/checkmarxExecuteScan.go
new file mode 100644
index 000000000..116f34672
--- /dev/null
+++ b/cmd/checkmarxExecuteScan.go
@@ -0,0 +1,582 @@
+package cmd
+
+import (
+ "archive/zip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "encoding/xml"
+
+ "github.com/SAP/jenkins-library/pkg/checkmarx"
+ piperHttp "github.com/SAP/jenkins-library/pkg/http"
+ "github.com/SAP/jenkins-library/pkg/log"
+ "github.com/SAP/jenkins-library/pkg/piperutils"
+ "github.com/bmatcuk/doublestar"
+)
+
+func checkmarxExecuteScan(config checkmarxExecuteScanOptions, influx *checkmarxExecuteScanInflux) error {
+ client := &piperHttp.Client{}
+ sys, err := checkmarx.NewSystemInstance(client, config.ServerURL, config.Username, config.Password)
+ if err != nil {
+ log.Entry().WithError(err).Fatalf("Failed to create Checkmarx client talking to URL %v", config.ServerURL)
+ }
+ runScan(config, sys, "./", influx)
+ return nil
+}
+
+func runScan(config checkmarxExecuteScanOptions, sys checkmarx.System, workspace string, influx *checkmarxExecuteScanInflux) {
+
+ team := loadTeam(sys, config.TeamName, config.TeamID)
+ projectName := config.ProjectName
+
+ project := loadExistingProject(sys, config.ProjectName, config.PullRequestName, team.ID)
+ if project.Name == projectName {
+ log.Entry().Debugf("Project %v exists...", projectName)
+ } else {
+ log.Entry().Debugf("Project %v does not exist, starting to create it...", projectName)
+ project = createAndConfigureNewProject(sys, projectName, team.ID, config.Preset, config.SourceEncoding)
+ }
+
+ uploadAndScan(config, sys, project, workspace, influx)
+}
+
+func loadTeam(sys checkmarx.System, teamName, teamID string) checkmarx.Team {
+ teams := sys.GetTeams()
+ team := checkmarx.Team{}
+ if len(teams) > 0 {
+ if len(teamName) > 0 {
+ team = sys.FilterTeamByName(teams, teamName)
+ } else {
+ team = sys.FilterTeamByID(teams, teamID)
+ }
+ }
+ if len(team.ID) == 0 {
+ log.Entry().Fatalf("Failed to identify team by teamName %v as well as by checkmarxGroupId %v", teamName, teamID)
+ }
+ return team
+}
+
+func loadExistingProject(sys checkmarx.System, initialProjectName, pullRequestName, teamID string) checkmarx.Project {
+ var project checkmarx.Project
+ projectName := initialProjectName
+ if len(pullRequestName) > 0 {
+ projectName = fmt.Sprintf("%v_%v", initialProjectName, pullRequestName)
+ projects := sys.GetProjectsByNameAndTeam(projectName, teamID)
+ if len(projects) == 0 {
+ projects = sys.GetProjectsByNameAndTeam(initialProjectName, teamID)
+ if len(projects) > 0 {
+ ok, branchProject := sys.GetProjectByID(sys.CreateBranch(projects[0].ID, projectName))
+ if !ok {
+ log.Entry().Fatalf("Failed to create branch %v for project %v", projectName, initialProjectName)
+ }
+ project = branchProject
+ }
+ }
+ } else {
+ projects := sys.GetProjectsByNameAndTeam(projectName, teamID)
+ if len(projects) > 0 {
+ project = projects[0]
+ log.Entry().Debugf("Loaded project with name %v", project.Name)
+ }
+ }
+ return project
+}
+
+func zipWorkspaceFiles(workspace, filterPattern string) *os.File {
+ zipFileName := filepath.Join(workspace, "workspace.zip")
+ patterns := strings.Split(filterPattern, ",")
+ sort.Strings(patterns)
+ zipFile, err := os.Create(zipFileName)
+ if err != nil {
+ log.Entry().WithError(err).Fatal("Failed to create archive of project sources")
+ }
+ defer zipFile.Close()
+ zipFolder(workspace, zipFile, patterns)
+ return zipFile
+}
+
+func uploadAndScan(config checkmarxExecuteScanOptions, sys checkmarx.System, project checkmarx.Project, workspace string, influx *checkmarxExecuteScanInflux) {
+ zipFile := zipWorkspaceFiles(workspace, config.FilterPattern)
+ sourceCodeUploaded := sys.UploadProjectSourceCode(project.ID, zipFile.Name())
+ if sourceCodeUploaded {
+ log.Entry().Debugf("Source code uploaded for project %v", project.Name)
+ zipFile.Close()
+ err := os.Remove(zipFile.Name())
+ if err != nil {
+ log.Entry().WithError(err).Warnf("Failed to delete zipped source code for project %v", project.Name)
+ }
+
+ incremental := config.Incremental
+ fullScanCycle, err := strconv.Atoi(config.FullScanCycle)
+ if err != nil {
+ log.Entry().WithError(err).Fatalf("Invalid configuration value for fullScanCycle %v, must be a positive int", config.FullScanCycle)
+ }
+ if incremental && config.FullScansScheduled && fullScanCycle > 0 && (getNumCoherentIncrementalScans(sys, project.ID)+1)%fullScanCycle == 0 {
+ incremental = false
+ }
+
+ triggerScan(config, sys, project, workspace, incremental, influx)
+ } else {
+ log.Entry().Fatalf("Cannot upload source code for project %v", project.Name)
+ }
+}
+
+func triggerScan(config checkmarxExecuteScanOptions, sys checkmarx.System, project checkmarx.Project, workspace string, incremental bool, influx *checkmarxExecuteScanInflux) {
+ projectIsScanning, scan := sys.ScanProject(project.ID, incremental, false, !config.AvoidDuplicateProjectScans)
+ if projectIsScanning {
+ log.Entry().Debugf("Scanning project %v ", project.Name)
+ pollScanStatus(sys, scan)
+
+ log.Entry().Debugln("Scan finished")
+
+ var reports []piperutils.Path
+ if config.GeneratePdfReport {
+ pdfReportName := createReportName(workspace, "CxSASTReport_%v.pdf")
+ ok := downloadAndSaveReport(sys, pdfReportName, scan)
+ if ok {
+ reports = append(reports, piperutils.Path{Target: pdfReportName, Mandatory: true})
+ }
+ } else {
+ log.Entry().Debug("Report generation is disabled via configuration")
+ }
+
+ xmlReportName := createReportName(workspace, "CxSASTResults_%v.xml")
+ results := getDetailedResults(sys, xmlReportName, scan.ID)
+ reports = append(reports, piperutils.Path{Target: xmlReportName})
+ links := []piperutils.Path{piperutils.Path{Target: results["DeepLink"].(string), Name: "Checkmarx Web UI"}}
+ piperutils.PersistReportsAndLinks(workspace, reports, links)
+
+ reportToInflux(results, influx)
+
+ insecure := false
+ if config.VulnerabilityThresholdEnabled {
+ insecure = enforceThresholds(config, results)
+ }
+
+ if insecure {
+ if config.VulnerabilityThresholdResult == "FAILURE" {
+ log.Entry().Fatalln("Checkmarx scan failed, the project is not compliant. For details see the archived report.")
+ }
+ log.Entry().Errorf("Checkmarx scan result set to %v, some results are not meeting defined thresholds. For details see the archived report.", config.VulnerabilityThresholdResult)
+ } else {
+ log.Entry().Infoln("Checkmarx scan finished")
+ }
+ } else {
+ log.Entry().Fatalf("Cannot scan project %v", project.Name)
+ }
+}
+
+func createReportName(workspace, reportFileNameTemplate string) string {
+ regExpFileName := regexp.MustCompile(`[^\w\d]`)
+ timeStamp, _ := time.Now().Local().MarshalText()
+ return filepath.Join(workspace, fmt.Sprintf(reportFileNameTemplate, regExpFileName.ReplaceAllString(string(timeStamp), "_")))
+}
+
+func pollScanStatus(sys checkmarx.System, scan checkmarx.Scan) {
+ status := "Scan phase: New"
+ pastStatus := status
+ log.Entry().Info(status)
+ for true {
+ stepDetail := "..."
+ stageDetail := "..."
+ status, detail := sys.GetScanStatusAndDetail(scan.ID)
+ if status == "Finished" || status == "Canceled" || status == "Failed" {
+ break
+ }
+ if len(detail.Stage) > 0 {
+ stageDetail = detail.Stage
+ }
+ if len(detail.Step) > 0 {
+ stepDetail = detail.Step
+ }
+
+ status = fmt.Sprintf("Scan phase: %v (%v / %v)", status, stageDetail, stepDetail)
+ if pastStatus != status {
+ log.Entry().Info(status)
+ pastStatus = status
+ }
+ log.Entry().Debug("Polling for status: sleeping...")
+ time.Sleep(10 * time.Second)
+ }
+ if status == "Canceled" {
+ log.Entry().Fatalln("Scan canceled via web interface")
+ }
+ if status == "Failed" {
+ log.Entry().Fatalln("Scan failed, please check the Checkmarx UI for details")
+ }
+}
+
+func reportToInflux(results map[string]interface{}, influx *checkmarxExecuteScanInflux) {
+ influx.checkmarx_data.fields.high_issues = strconv.Itoa(results["High"].(map[string]int)["Issues"])
+ influx.checkmarx_data.fields.high_not_false_postive = strconv.Itoa(results["High"].(map[string]int)["NotFalsePositive"])
+ influx.checkmarx_data.fields.high_not_exploitable = strconv.Itoa(results["High"].(map[string]int)["NotExploitable"])
+ influx.checkmarx_data.fields.high_confirmed = strconv.Itoa(results["High"].(map[string]int)["Confirmed"])
+ influx.checkmarx_data.fields.high_urgent = strconv.Itoa(results["High"].(map[string]int)["Urgent"])
+ influx.checkmarx_data.fields.high_proposed_not_exploitable = strconv.Itoa(results["High"].(map[string]int)["ProposedNotExploitable"])
+ influx.checkmarx_data.fields.high_to_verify = strconv.Itoa(results["High"].(map[string]int)["ToVerify"])
+ influx.checkmarx_data.fields.medium_issues = strconv.Itoa(results["Medium"].(map[string]int)["Issues"])
+ influx.checkmarx_data.fields.medium_not_false_postive = strconv.Itoa(results["Medium"].(map[string]int)["NotFalsePositive"])
+ influx.checkmarx_data.fields.medium_not_exploitable = strconv.Itoa(results["Medium"].(map[string]int)["NotExploitable"])
+ influx.checkmarx_data.fields.medium_confirmed = strconv.Itoa(results["Medium"].(map[string]int)["Confirmed"])
+ influx.checkmarx_data.fields.medium_urgent = strconv.Itoa(results["Medium"].(map[string]int)["Urgent"])
+ influx.checkmarx_data.fields.medium_proposed_not_exploitable = strconv.Itoa(results["Medium"].(map[string]int)["ProposedNotExploitable"])
+ influx.checkmarx_data.fields.medium_to_verify = strconv.Itoa(results["Medium"].(map[string]int)["ToVerify"])
+ influx.checkmarx_data.fields.low_issues = strconv.Itoa(results["Low"].(map[string]int)["Issues"])
+ influx.checkmarx_data.fields.low_not_false_postive = strconv.Itoa(results["Low"].(map[string]int)["NotFalsePositive"])
+ influx.checkmarx_data.fields.low_not_exploitable = strconv.Itoa(results["Low"].(map[string]int)["NotExploitable"])
+ influx.checkmarx_data.fields.low_confirmed = strconv.Itoa(results["Low"].(map[string]int)["Confirmed"])
+ influx.checkmarx_data.fields.low_urgent = strconv.Itoa(results["Low"].(map[string]int)["Urgent"])
+ influx.checkmarx_data.fields.low_proposed_not_exploitable = strconv.Itoa(results["Low"].(map[string]int)["ProposedNotExploitable"])
+ influx.checkmarx_data.fields.low_to_verify = strconv.Itoa(results["Low"].(map[string]int)["ToVerify"])
+ influx.checkmarx_data.fields.information_issues = strconv.Itoa(results["Information"].(map[string]int)["Issues"])
+ influx.checkmarx_data.fields.information_not_false_postive = strconv.Itoa(results["Information"].(map[string]int)["NotFalsePositive"])
+ influx.checkmarx_data.fields.information_not_exploitable = strconv.Itoa(results["Information"].(map[string]int)["NotExploitable"])
+ influx.checkmarx_data.fields.information_confirmed = strconv.Itoa(results["Information"].(map[string]int)["Confirmed"])
+ influx.checkmarx_data.fields.information_urgent = strconv.Itoa(results["Information"].(map[string]int)["Urgent"])
+ influx.checkmarx_data.fields.information_proposed_not_exploitable = strconv.Itoa(results["Information"].(map[string]int)["ProposedNotExploitable"])
+ influx.checkmarx_data.fields.information_to_verify = strconv.Itoa(results["Information"].(map[string]int)["ToVerify"])
+ influx.checkmarx_data.fields.initiator_name = results["InitiatorName"].(string)
+ influx.checkmarx_data.fields.owner = results["Owner"].(string)
+ influx.checkmarx_data.fields.scan_id = results["ScanId"].(string)
+ influx.checkmarx_data.fields.project_id = results["ProjectId"].(string)
+ influx.checkmarx_data.fields.project_name = results["ProjectName"].(string)
+ influx.checkmarx_data.fields.team = results["Team"].(string)
+ influx.checkmarx_data.fields.team_full_path_on_report_date = results["TeamFullPathOnReportDate"].(string)
+ influx.checkmarx_data.fields.scan_start = results["ScanStart"].(string)
+ influx.checkmarx_data.fields.scan_time = results["ScanTime"].(string)
+ influx.checkmarx_data.fields.lines_of_code_scanned = results["LinesOfCodeScanned"].(string)
+ influx.checkmarx_data.fields.files_scanned = results["FilesScanned"].(string)
+ influx.checkmarx_data.fields.checkmarx_version = results["CheckmarxVersion"].(string)
+ influx.checkmarx_data.fields.scan_type = results["ScanType"].(string)
+ influx.checkmarx_data.fields.preset = results["Preset"].(string)
+ influx.checkmarx_data.fields.deep_link = results["DeepLink"].(string)
+ influx.checkmarx_data.fields.report_creation_time = results["ReportCreationTime"].(string)
+}
+
+func downloadAndSaveReport(sys checkmarx.System, reportFileName string, scan checkmarx.Scan) bool {
+ ok, report := generateAndDownloadReport(sys, scan.ID, "PDF")
+ if ok {
+ log.Entry().Debugf("Saving report to file %v...", reportFileName)
+ ioutil.WriteFile(reportFileName, report, 0700)
+ return true
+ }
+ log.Entry().Debugf("Failed to fetch report %v from backend...", reportFileName)
+ return false
+}
+
+func enforceThresholds(config checkmarxExecuteScanOptions, results map[string]interface{}) bool {
+ insecure := false
+ cxHighThreshold := config.VulnerabilityThresholdHigh
+ cxMediumThreshold := config.VulnerabilityThresholdMedium
+ cxLowThreshold := config.VulnerabilityThresholdLow
+ highValue := results["High"].(map[string]int)["NotFalsePositive"]
+ mediumValue := results["Medium"].(map[string]int)["NotFalsePositive"]
+ lowValue := results["Low"].(map[string]int)["NotFalsePositive"]
+ var unit string
+ highViolation := ""
+ mediumViolation := ""
+ lowViolation := ""
+ if config.VulnerabilityThresholdUnit == "percentage" {
+ unit = "%"
+ highAudited := results["High"].(map[string]int)["Issues"] - results["High"].(map[string]int)["NotFalsePositive"]
+ highOverall := results["High"].(map[string]int)["Issues"]
+ if highOverall == 0 {
+ highAudited = 1
+ highOverall = 1
+ }
+ mediumAudited := results["Medium"].(map[string]int)["Issues"] - results["Medium"].(map[string]int)["NotFalsePositive"]
+ mediumOverall := results["Medium"].(map[string]int)["Issues"]
+ if mediumOverall == 0 {
+ mediumAudited = 1
+ mediumOverall = 1
+ }
+ lowAudited := results["Low"].(map[string]int)["Confirmed"] + results["Low"].(map[string]int)["NotExploitable"]
+ lowOverall := results["Low"].(map[string]int)["Issues"]
+ if lowOverall == 0 {
+ lowAudited = 1
+ lowOverall = 1
+ }
+ highValue = int(float32(highAudited) / float32(highOverall) * 100.0)
+ mediumValue = int(float32(mediumAudited) / float32(mediumOverall) * 100.0)
+ lowValue = int(float32(lowAudited) / float32(lowOverall) * 100.0)
+
+ if highValue < cxHighThreshold {
+ insecure = true
+ highViolation = fmt.Sprintf("<-- %v %v deviation", cxHighThreshold-highValue, unit)
+ }
+ if mediumValue < cxMediumThreshold {
+ insecure = true
+ mediumViolation = fmt.Sprintf("<-- %v %v deviation", cxMediumThreshold-mediumValue, unit)
+ }
+ if lowValue < cxLowThreshold {
+ insecure = true
+ lowViolation = fmt.Sprintf("<-- %v %v deviation", cxLowThreshold-lowValue, unit)
+ }
+ }
+ if config.VulnerabilityThresholdUnit == "absolute" {
+ unit = "findings"
+ if highValue > cxHighThreshold {
+ insecure = true
+ highViolation = fmt.Sprintf("<-- %v %v deviation", highValue-cxHighThreshold, unit)
+ }
+ if mediumValue > cxMediumThreshold {
+ insecure = true
+ mediumViolation = fmt.Sprintf("<-- %v %v deviation", mediumValue-cxMediumThreshold, unit)
+ }
+ if lowValue > cxLowThreshold {
+ insecure = true
+ lowViolation = fmt.Sprintf("<-- %v %v deviation", lowValue-cxLowThreshold, unit)
+ }
+ }
+
+ log.Entry().Infoln("")
+ log.Entry().Infof("High %v%v %v", highValue, unit, highViolation)
+ log.Entry().Infof("Medium %v%v %v", mediumValue, unit, mediumViolation)
+ log.Entry().Infof("Low %v%v %v", lowValue, unit, lowViolation)
+ log.Entry().Infoln("")
+
+ return insecure
+}
+
+func createAndConfigureNewProject(sys checkmarx.System, projectName, teamID, presetValue, engineConfiguration string) checkmarx.Project {
+ ok, projectCreateResult := sys.CreateProject(projectName, teamID)
+ if ok {
+ if len(presetValue) > 0 {
+ ok, preset := loadPreset(sys, presetValue)
+ if ok {
+ configurationUpdated := sys.UpdateProjectConfiguration(projectCreateResult.ID, preset.ID, engineConfiguration)
+ if configurationUpdated {
+ log.Entry().Debugf("Configuration of project %v updated", projectName)
+ } else {
+ log.Entry().Fatalf("Updating configuration of project %v failed", projectName)
+ }
+ } else {
+ log.Entry().Fatalf("Preset %v not found, creation of project %v failed", presetValue, projectName)
+ }
+ } else {
+ log.Entry().Fatalf("Preset not specified, creation of project %v failed", projectName)
+ }
+ projects := sys.GetProjectsByNameAndTeam(projectName, teamID)
+ if len(projects) > 0 {
+ log.Entry().Debugf("New Project %v created", projectName)
+ return projects[0]
+ }
+ log.Entry().Fatalf("Failed to load newly created project %v", projectName)
+ }
+ log.Entry().Fatalf("Cannot create project %v", projectName)
+ return checkmarx.Project{}
+}
+
+func loadPreset(sys checkmarx.System, presetValue string) (bool, checkmarx.Preset) {
+ presets := sys.GetPresets()
+ var preset checkmarx.Preset
+ presetID, err := strconv.Atoi(presetValue)
+ var configuredPresetID int
+ var configuredPresetName string
+ if err != nil {
+ preset = sys.FilterPresetByName(presets, presetValue)
+ configuredPresetName = presetValue
+ } else {
+ preset = sys.FilterPresetByID(presets, presetID)
+ configuredPresetID = presetID
+ }
+
+ if configuredPresetID > 0 && preset.ID == configuredPresetID || len(configuredPresetName) > 0 && preset.Name == configuredPresetName {
+ log.Entry().Debugf("Loaded preset %v", preset.Name)
+ return true, preset
+ }
+ return false, checkmarx.Preset{}
+}
+
+func generateAndDownloadReport(sys checkmarx.System, scanID int, reportType string) (bool, []byte) {
+ success, report := sys.RequestNewReport(scanID, reportType)
+ if success {
+ finalStatus := 1
+ for {
+ finalStatus = sys.GetReportStatus(report.ReportID).Status.ID
+ if finalStatus != 1 {
+ break
+ }
+ time.Sleep(10 * time.Second)
+ }
+ if finalStatus == 2 {
+ return sys.DownloadReport(report.ReportID)
+ }
+ }
+ return false, []byte{}
+}
+
+func getNumCoherentIncrementalScans(sys checkmarx.System, projectID int) int {
+ ok, scans := sys.GetScans(projectID)
+ count := 0
+ if ok {
+ for _, scan := range scans {
+ if !scan.IsIncremental {
+ break
+ }
+ count++
+ }
+ }
+ return count
+}
+
+func getDetailedResults(sys checkmarx.System, reportFileName string, scanID int) map[string]interface{} {
+ resultMap := map[string]interface{}{}
+ ok, data := generateAndDownloadReport(sys, scanID, "XML")
+ if ok && len(data) > 0 {
+ ioutil.WriteFile(reportFileName, data, 0700)
+ var xmlResult checkmarx.DetailedResult
+ err := xml.Unmarshal(data, &xmlResult)
+ if err != nil {
+ log.Entry().Fatalf("Failed to unmarshal XML report for scan %v: %s", scanID, err)
+ }
+ resultMap["InitiatorName"] = xmlResult.InitiatorName
+ resultMap["Owner"] = xmlResult.Owner
+ resultMap["ScanId"] = xmlResult.ScanID
+ resultMap["ProjectId"] = xmlResult.ProjectID
+ resultMap["ProjectName"] = xmlResult.ProjectName
+ resultMap["Team"] = xmlResult.Team
+ resultMap["TeamFullPathOnReportDate"] = xmlResult.TeamFullPathOnReportDate
+ resultMap["ScanStart"] = xmlResult.ScanStart
+ resultMap["ScanTime"] = xmlResult.ScanTime
+ resultMap["LinesOfCodeScanned"] = xmlResult.LinesOfCodeScanned
+ resultMap["FilesScanned"] = xmlResult.FilesScanned
+ resultMap["CheckmarxVersion"] = xmlResult.CheckmarxVersion
+ resultMap["ScanType"] = xmlResult.ScanType
+ resultMap["Preset"] = xmlResult.Preset
+ resultMap["DeepLink"] = xmlResult.DeepLink
+ resultMap["ReportCreationTime"] = xmlResult.ReportCreationTime
+ resultMap["High"] = map[string]int{}
+ resultMap["Medium"] = map[string]int{}
+ resultMap["Low"] = map[string]int{}
+ resultMap["Information"] = map[string]int{}
+ for _, query := range xmlResult.Queries {
+ for _, result := range query.Results {
+ key := result.Severity
+ var submap map[string]int
+ if resultMap[key] == nil {
+ submap = map[string]int{}
+ resultMap[key] = submap
+ } else {
+ submap = resultMap[key].(map[string]int)
+ }
+ submap["Issues"]++
+
+ auditState := "ToVerify"
+ switch result.State {
+ case "1":
+ auditState = "NotExploitable"
+ break
+ case "2":
+ auditState = "Confirmed"
+ break
+ case "3":
+ auditState = "Urgent"
+ break
+ case "4":
+ auditState = "ProposedNotExploitable"
+ break
+ case "0":
+ default:
+ auditState = "ToVerify"
+ break
+ }
+ submap[auditState]++
+
+ if result.FalsePositive != "True" {
+ submap["NotFalsePositive"]++
+ }
+ }
+ }
+ }
+ return resultMap
+}
+
+func zipFolder(source string, zipFile io.Writer, patterns []string) error {
+ archive := zip.NewWriter(zipFile)
+ defer archive.Close()
+
+ info, err := os.Stat(source)
+ if err != nil {
+ return nil
+ }
+
+ var baseDir string
+ if info.IsDir() {
+ baseDir = filepath.Base(source)
+ }
+
+ filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if filterFileGlob(patterns, path, info) {
+ return nil
+ }
+
+ header, err := zip.FileInfoHeader(info)
+ if err != nil {
+ return err
+ }
+
+ if baseDir != "" {
+ header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
+ }
+
+ if info.IsDir() {
+ header.Name += "/"
+ } else {
+ header.Method = zip.Deflate
+ }
+
+ writer, err := archive.CreateHeader(header)
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ _, err = io.Copy(writer, file)
+ return err
+ })
+
+ return err
+}
+
+func filterFileGlob(patterns []string, path string, info os.FileInfo) bool {
+ for index := 0; index < len(patterns); index++ {
+ pattern := patterns[index]
+ negative := false
+ if strings.Index(pattern, "!") == 0 {
+ pattern = strings.TrimLeft(pattern, "!")
+ negative = true
+ }
+ match, _ := doublestar.Match(pattern, path)
+ if !info.IsDir() {
+ if match && negative {
+ return true
+ } else if match && !negative {
+ return false
+ }
+ } else {
+ return false
+ }
+ }
+ return true
+}
diff --git a/cmd/checkmarxExecuteScan_generated.go b/cmd/checkmarxExecuteScan_generated.go
new file mode 100644
index 000000000..1cea918b4
--- /dev/null
+++ b/cmd/checkmarxExecuteScan_generated.go
@@ -0,0 +1,414 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/SAP/jenkins-library/pkg/config"
+ "github.com/SAP/jenkins-library/pkg/log"
+ "github.com/SAP/jenkins-library/pkg/piperenv"
+ "github.com/spf13/cobra"
+)
+
+type checkmarxExecuteScanOptions struct {
+ AvoidDuplicateProjectScans bool `json:"avoidDuplicateProjectScans,omitempty"`
+ FilterPattern string `json:"filterPattern,omitempty"`
+ FullScanCycle string `json:"fullScanCycle,omitempty"`
+ FullScansScheduled bool `json:"fullScansScheduled,omitempty"`
+ GeneratePdfReport bool `json:"generatePdfReport,omitempty"`
+ Incremental bool `json:"incremental,omitempty"`
+ Password string `json:"password,omitempty"`
+ Preset string `json:"preset,omitempty"`
+ ProjectName string `json:"projectName,omitempty"`
+ PullRequestName string `json:"pullRequestName,omitempty"`
+ ServerURL string `json:"serverUrl,omitempty"`
+ SourceEncoding string `json:"sourceEncoding,omitempty"`
+ TeamID string `json:"teamId,omitempty"`
+ TeamName string `json:"teamName,omitempty"`
+ Username string `json:"username,omitempty"`
+ VulnerabilityThresholdEnabled bool `json:"vulnerabilityThresholdEnabled,omitempty"`
+ VulnerabilityThresholdHigh int `json:"vulnerabilityThresholdHigh,omitempty"`
+ VulnerabilityThresholdLow int `json:"vulnerabilityThresholdLow,omitempty"`
+ VulnerabilityThresholdMedium int `json:"vulnerabilityThresholdMedium,omitempty"`
+ VulnerabilityThresholdResult string `json:"vulnerabilityThresholdResult,omitempty"`
+ VulnerabilityThresholdUnit string `json:"vulnerabilityThresholdUnit,omitempty"`
+ Verbose bool `json:"verbose,omitempty"`
+}
+
+type checkmarxExecuteScanInflux struct {
+ checkmarx_data struct {
+ fields struct {
+ high_issues string
+ high_not_false_postive string
+ high_not_exploitable string
+ high_confirmed string
+ high_urgent string
+ high_proposed_not_exploitable string
+ high_to_verify string
+ medium_issues string
+ medium_not_false_postive string
+ medium_not_exploitable string
+ medium_confirmed string
+ medium_urgent string
+ medium_proposed_not_exploitable string
+ medium_to_verify string
+ low_issues string
+ low_not_false_postive string
+ low_not_exploitable string
+ low_confirmed string
+ low_urgent string
+ low_proposed_not_exploitable string
+ low_to_verify string
+ information_issues string
+ information_not_false_postive string
+ information_not_exploitable string
+ information_confirmed string
+ information_urgent string
+ information_proposed_not_exploitable string
+ information_to_verify string
+ initiator_name string
+ owner string
+ scan_id string
+ project_id string
+ project_name string
+ team string
+ team_full_path_on_report_date string
+ scan_start string
+ scan_time string
+ lines_of_code_scanned string
+ files_scanned string
+ checkmarx_version string
+ scan_type string
+ preset string
+ deep_link string
+ report_creation_time string
+ }
+ tags struct {
+ }
+ }
+}
+
+func (i *checkmarxExecuteScanInflux) persist(path, resourceName string) {
+ measurementContent := []struct {
+ measurement string
+ valType string
+ name string
+ value string
+ }{
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_issues", value: i.checkmarx_data.fields.high_issues},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_not_false_postive", value: i.checkmarx_data.fields.high_not_false_postive},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_not_exploitable", value: i.checkmarx_data.fields.high_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_confirmed", value: i.checkmarx_data.fields.high_confirmed},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_urgent", value: i.checkmarx_data.fields.high_urgent},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_proposed_not_exploitable", value: i.checkmarx_data.fields.high_proposed_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "high_to_verify", value: i.checkmarx_data.fields.high_to_verify},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_issues", value: i.checkmarx_data.fields.medium_issues},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_not_false_postive", value: i.checkmarx_data.fields.medium_not_false_postive},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_not_exploitable", value: i.checkmarx_data.fields.medium_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_confirmed", value: i.checkmarx_data.fields.medium_confirmed},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_urgent", value: i.checkmarx_data.fields.medium_urgent},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_proposed_not_exploitable", value: i.checkmarx_data.fields.medium_proposed_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "medium_to_verify", value: i.checkmarx_data.fields.medium_to_verify},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_issues", value: i.checkmarx_data.fields.low_issues},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_not_false_postive", value: i.checkmarx_data.fields.low_not_false_postive},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_not_exploitable", value: i.checkmarx_data.fields.low_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_confirmed", value: i.checkmarx_data.fields.low_confirmed},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_urgent", value: i.checkmarx_data.fields.low_urgent},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_proposed_not_exploitable", value: i.checkmarx_data.fields.low_proposed_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "low_to_verify", value: i.checkmarx_data.fields.low_to_verify},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_issues", value: i.checkmarx_data.fields.information_issues},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_not_false_postive", value: i.checkmarx_data.fields.information_not_false_postive},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_not_exploitable", value: i.checkmarx_data.fields.information_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_confirmed", value: i.checkmarx_data.fields.information_confirmed},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_urgent", value: i.checkmarx_data.fields.information_urgent},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_proposed_not_exploitable", value: i.checkmarx_data.fields.information_proposed_not_exploitable},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "information_to_verify", value: i.checkmarx_data.fields.information_to_verify},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "initiator_name", value: i.checkmarx_data.fields.initiator_name},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "owner", value: i.checkmarx_data.fields.owner},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "scan_id", value: i.checkmarx_data.fields.scan_id},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "project_id", value: i.checkmarx_data.fields.project_id},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "project_name", value: i.checkmarx_data.fields.project_name},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "team", value: i.checkmarx_data.fields.team},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "team_full_path_on_report_date", value: i.checkmarx_data.fields.team_full_path_on_report_date},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "scan_start", value: i.checkmarx_data.fields.scan_start},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "scan_time", value: i.checkmarx_data.fields.scan_time},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "lines_of_code_scanned", value: i.checkmarx_data.fields.lines_of_code_scanned},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "files_scanned", value: i.checkmarx_data.fields.files_scanned},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "checkmarx_version", value: i.checkmarx_data.fields.checkmarx_version},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "scan_type", value: i.checkmarx_data.fields.scan_type},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "preset", value: i.checkmarx_data.fields.preset},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "deep_link", value: i.checkmarx_data.fields.deep_link},
+ {valType: config.InfluxField, measurement: "checkmarx_data", name: "report_creation_time", value: i.checkmarx_data.fields.report_creation_time},
+ }
+
+ errCount := 0
+ for _, metric := range measurementContent {
+ err := piperenv.SetResourceParameter(path, resourceName, filepath.Join(metric.measurement, fmt.Sprintf("%vs", metric.valType), metric.name), metric.value)
+ if err != nil {
+ log.Entry().WithError(err).Error("Error persisting influx environment.")
+ errCount++
+ }
+ }
+ if errCount > 0 {
+ os.Exit(1)
+ }
+}
+
+var myCheckmarxExecuteScanOptions checkmarxExecuteScanOptions
+
+// CheckmarxExecuteScanCommand Checkmarx is the recommended tool for security scans of JavaScript, iOS, Swift and Ruby code.
+func CheckmarxExecuteScanCommand() *cobra.Command {
+ metadata := checkmarxExecuteScanMetadata()
+ var influx checkmarxExecuteScanInflux
+
+ var createCheckmarxExecuteScanCmd = &cobra.Command{
+ Use: "checkmarxExecuteScan",
+ Short: "Checkmarx is the recommended tool for security scans of JavaScript, iOS, Swift and Ruby code.",
+ Long: `Checkmarx is a Static Application Security Testing (SAST) tool to analyze i.e. Java- or TypeScript, Swift, Golang, Ruby code,
+and many other programming languages for security flaws based on a set of provided rules/queries that can be customized and extended.
+
+This step by default enforces a specific audit baseline for findings and therefore ensures that:
+* No 'To Verify' High and Medium issues exist in your project
+* Total number of High and Medium 'Confirmed' or 'Urgent' issues is zero
+* 10% of all Low issues are 'Confirmed' or 'Not Exploitable'
+
+You can adapt above thresholds specifically using the provided configuration parameters and i.e. check for ` + "`" + `absolute` + "`" + `
+thresholds instead of ` + "`" + `percentage` + "`" + ` whereas we strongly recommend you to stay with the defaults provided.`,
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ log.SetStepName("checkmarxExecuteScan")
+ log.SetVerbose(GeneralConfig.Verbose)
+ return PrepareConfig(cmd, &metadata, "checkmarxExecuteScan", &myCheckmarxExecuteScanOptions, config.OpenPiperFile)
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ handler := func() {
+ influx.persist(GeneralConfig.EnvRootPath, "influx")
+ }
+ log.DeferExitHandler(handler)
+ defer handler()
+ return checkmarxExecuteScan(myCheckmarxExecuteScanOptions, &influx)
+ },
+ }
+
+ addCheckmarxExecuteScanFlags(createCheckmarxExecuteScanCmd)
+ return createCheckmarxExecuteScanCmd
+}
+
+func addCheckmarxExecuteScanFlags(cmd *cobra.Command) {
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.AvoidDuplicateProjectScans, "avoidDuplicateProjectScans", false, "Whether duplicate scans of the same project state shall be avoided or not")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.FilterPattern, "filterPattern", "!**/node_modules/**, !**/.xmake/**, !**/*_test.go, !**/vendor/**/*.go, **/*.html, **/*.xml, **/*.go, **/*.py, **/*.js, **/*.scala, **/*.ts", "The filter pattern used to zip the files relevant for scanning, patterns can be negated by setting an exclamation mark in front i.e. `!test/*.js` would avoid adding any javascript files located in the test directory")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.FullScanCycle, "fullScanCycle", "5", "Indicates how often a full scan should happen between the incremental scans when activated")
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.FullScansScheduled, "fullScansScheduled", true, "Whether full scans are to be scheduled or not. Should be used in relation with `incremental` and `fullScanCycle`")
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.GeneratePdfReport, "generatePdfReport", true, "Whether to generate a PDF report of the analysis results or not")
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.Incremental, "incremental", true, "Whether incremental scans are to be applied which optimizes the scan time but might reduce detection capabilities. Therefore full scans are still required from time to time and should be scheduled via `fullScansScheduled` and `fullScanCycle`")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.Password, "password", os.Getenv("PIPER_password"), "The password to authenticate")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.Preset, "preset", os.Getenv("PIPER_preset"), "The preset to use for scanning, if not set explicitly the step will attempt to look up the project's setting based on the availability of `checkmarxCredentialsId`")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.ProjectName, "projectName", os.Getenv("PIPER_projectName"), "The name of the Checkmarx project to scan into")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.PullRequestName, "pullRequestName", os.Getenv("PIPER_pullRequestName"), "Used to supply the name for the newly created PR project branch when being used in pull request scenarios")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.ServerURL, "serverUrl", os.Getenv("PIPER_serverUrl"), "The URL pointing to the root of the Checkmarx server to be used")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.SourceEncoding, "sourceEncoding", "1", "The source encoding to be used, if not set explicitly the project's default will be used")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.TeamID, "teamId", os.Getenv("PIPER_teamId"), "The group ID related to your team which can be obtained via the Pipeline Syntax plugin as described in the `Details` section")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.TeamName, "teamName", os.Getenv("PIPER_teamName"), "The full name of the team to assign newly created projects to which is preferred to teamId")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.Username, "username", os.Getenv("PIPER_username"), "The username to authenticate")
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdEnabled, "vulnerabilityThresholdEnabled", true, "Whether the thresholds are enabled or not. If enabled the build will be set to `vulnerabilityThresholdResult` in case a specific threshold value is exceeded")
+ cmd.Flags().IntVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdHigh, "vulnerabilityThresholdHigh", 100, "The specific threshold for high severity findings")
+ cmd.Flags().IntVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdLow, "vulnerabilityThresholdLow", 10, "The specific threshold for low severity findings")
+ cmd.Flags().IntVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdMedium, "vulnerabilityThresholdMedium", 100, "The specific threshold for medium severity findings")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdResult, "vulnerabilityThresholdResult", "FAILURE", "The result of the build in case thresholds are enabled and exceeded")
+ cmd.Flags().StringVar(&myCheckmarxExecuteScanOptions.VulnerabilityThresholdUnit, "vulnerabilityThresholdUnit", "percentage", "The unit for the threshold to apply.")
+ cmd.Flags().BoolVar(&myCheckmarxExecuteScanOptions.Verbose, "verbose", false, "Whether the step shall provide verbose logging output")
+
+ cmd.MarkFlagRequired("password")
+ cmd.MarkFlagRequired("projectName")
+ cmd.MarkFlagRequired("serverUrl")
+ cmd.MarkFlagRequired("username")
+}
+
+// retrieve step metadata
+func checkmarxExecuteScanMetadata() config.StepData {
+ var theMetaData = config.StepData{
+ Spec: config.StepSpec{
+ Inputs: config.StepInputs{
+ Parameters: []config.StepParameters{
+ {
+ Name: "avoidDuplicateProjectScans",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "filterPattern",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "fullScanCycle",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "fullScansScheduled",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "generatePdfReport",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "incremental",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "password",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: true,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "preset",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "projectName",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: true,
+ Aliases: []config.Alias{{Name: "checkmarxProject"}},
+ },
+ {
+ Name: "pullRequestName",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "serverUrl",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: true,
+ Aliases: []config.Alias{{Name: "checkmarxServerUrl"}},
+ },
+ {
+ Name: "sourceEncoding",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "teamId",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{{Name: "checkmarxGroupId"}},
+ },
+ {
+ Name: "teamName",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "username",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: true,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdEnabled",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdHigh",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "int",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdLow",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "int",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdMedium",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "int",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdResult",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "vulnerabilityThresholdUnit",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"PARAMETERS", "STAGES", "STEPS"},
+ Type: "string",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ {
+ Name: "verbose",
+ ResourceRef: []config.ResourceReference{},
+ Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
+ Type: "bool",
+ Mandatory: false,
+ Aliases: []config.Alias{},
+ },
+ },
+ },
+ },
+ }
+ return theMetaData
+}
diff --git a/cmd/checkmarxExecuteScan_generated_test.go b/cmd/checkmarxExecuteScan_generated_test.go
new file mode 100644
index 000000000..f4947a3f9
--- /dev/null
+++ b/cmd/checkmarxExecuteScan_generated_test.go
@@ -0,0 +1,16 @@
+package cmd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCheckmarxExecuteScanCommand(t *testing.T) {
+
+ testCmd := CheckmarxExecuteScanCommand()
+
+ // only high level testing performed - details are tested in step generation procudure
+ assert.Equal(t, "checkmarxExecuteScan", testCmd.Use, "command name incorrect")
+
+}
diff --git a/cmd/checkmarxExecuteScan_test.go b/cmd/checkmarxExecuteScan_test.go
new file mode 100644
index 000000000..a3c80191a
--- /dev/null
+++ b/cmd/checkmarxExecuteScan_test.go
@@ -0,0 +1,544 @@
+package cmd
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/SAP/jenkins-library/pkg/checkmarx"
+ "github.com/stretchr/testify/assert"
+)
+
+type fileInfo struct {
+ nam string // base name of the file
+ siz int64 // length in bytes for regular files; system-dependent for others
+ mod os.FileMode // file mode bits
+ modtime time.Time // modification time
+ dir bool // abbreviation for Mode().IsDir()
+ syss interface{} // underlying data source (can return nil)
+}
+
+func (fi fileInfo) IsDir() bool {
+ return fi.dir
+}
+func (fi fileInfo) Name() string {
+ return fi.nam
+}
+func (fi fileInfo) Size() int64 {
+ return fi.siz
+}
+func (fi fileInfo) ModTime() time.Time {
+ return fi.modtime
+}
+func (fi fileInfo) Mode() os.FileMode {
+ return fi.mod
+}
+func (fi fileInfo) Sys() interface{} {
+ return fi.syss
+}
+
+type systemMock struct {
+ response interface{}
+ isIncremental bool
+ isPublic bool
+ forceScan bool
+ createProject bool
+ projectLoadCount int
+}
+
+func (sys *systemMock) FilterPresetByName(presets []checkmarx.Preset, presetName string) checkmarx.Preset {
+ return checkmarx.Preset{ID: 10050, Name: "SAP_JS_Default", OwnerName: "16"}
+}
+func (sys *systemMock) FilterPresetByID(presets []checkmarx.Preset, presetID int) checkmarx.Preset {
+ return checkmarx.Preset{ID: 10048, Name: "SAP_Default", OwnerName: "16"}
+}
+func (sys *systemMock) FilterProjectByName(projects []checkmarx.Project, projectName string) checkmarx.Project {
+ return checkmarx.Project{ID: 1, Name: "Test", TeamID: "16", IsPublic: false}
+}
+func (sys *systemMock) GetProjectByID(projectID int) (bool, checkmarx.Project) {
+ return true, checkmarx.Project{ID: 19, Name: "Test_PR-19", TeamID: "16", IsPublic: false}
+}
+func (sys *systemMock) GetProjectsByNameAndTeam(projectName, teamID string) []checkmarx.Project {
+ sys.projectLoadCount++
+ if !sys.createProject || sys.projectLoadCount%2 == 0 {
+ return []checkmarx.Project{checkmarx.Project{ID: 19, Name: projectName, TeamID: teamID, IsPublic: false}}
+ }
+ return []checkmarx.Project{}
+}
+func (sys *systemMock) FilterTeamByName(teams []checkmarx.Team, teamName string) checkmarx.Team {
+ return checkmarx.Team{ID: "16", FullName: "OpenSource/Cracks/16"}
+}
+func (sys *systemMock) FilterTeamByID(teams []checkmarx.Team, teamID string) checkmarx.Team {
+ return checkmarx.Team{ID: "15", FullName: "OpenSource/Cracks/15"}
+}
+func (sys *systemMock) DownloadReport(reportID int) (bool, []byte) {
+ return true, sys.response.([]byte)
+}
+func (sys *systemMock) GetReportStatus(reportID int) checkmarx.ReportStatusResponse {
+ return checkmarx.ReportStatusResponse{Status: checkmarx.ReportStatus{ID: 2, Value: "Created"}}
+}
+func (sys *systemMock) RequestNewReport(scanID int, reportType string) (bool, checkmarx.Report) {
+ return true, checkmarx.Report{ReportID: 17}
+}
+func (sys *systemMock) GetResults(scanID int) checkmarx.ResultsStatistics {
+ return checkmarx.ResultsStatistics{}
+}
+func (sys *systemMock) GetScans(projectID int) (bool, []checkmarx.ScanStatus) {
+ return true, []checkmarx.ScanStatus{checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: false}}
+}
+func (sys *systemMock) GetScanStatusAndDetail(scanID int) (string, checkmarx.ScanStatusDetail) {
+ return "Finished", checkmarx.ScanStatusDetail{Stage: "Step 1 of 25", Step: "Scan something"}
+}
+func (sys *systemMock) ScanProject(projectID int, isIncrementalV, isPublicV, forceScanV bool) (bool, checkmarx.Scan) {
+ sys.isIncremental = isIncrementalV
+ sys.isPublic = isPublicV
+ sys.forceScan = forceScanV
+ return true, checkmarx.Scan{ID: 16}
+}
+func (sys *systemMock) UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool {
+ return true
+}
+func (sys *systemMock) UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool {
+ return true
+}
+func (sys *systemMock) UploadProjectSourceCode(projectID int, zipFile string) bool {
+ return true
+}
+func (sys *systemMock) CreateProject(projectName string, teamID string) (bool, checkmarx.ProjectCreateResult) {
+ return true, checkmarx.ProjectCreateResult{ID: 20}
+}
+func (sys *systemMock) CreateBranch(projectID int, branchName string) int {
+ return 18
+}
+func (sys *systemMock) GetPresets() []checkmarx.Preset {
+ return []checkmarx.Preset{checkmarx.Preset{ID: 10078, Name: "SAP Java Default", OwnerName: "16"}, checkmarx.Preset{ID: 10048, Name: "SAP JS Default", OwnerName: "16"}}
+}
+func (sys *systemMock) GetProjects() []checkmarx.Project {
+ return []checkmarx.Project{checkmarx.Project{ID: 15, Name: "OtherTest", TeamID: "16"}, checkmarx.Project{ID: 1, Name: "Test", TeamID: "16"}}
+}
+func (sys *systemMock) GetTeams() []checkmarx.Team {
+ sys.projectLoadCount = 0
+ return []checkmarx.Team{checkmarx.Team{ID: "16", FullName: "OpenSource/Cracks/16"}, checkmarx.Team{ID: "15", FullName: "OpenSource/Cracks/15"}}
+}
+
+type systemMockForExistingProject struct {
+ response interface{}
+ isIncremental bool
+ isPublic bool
+ forceScan bool
+}
+
+func (sys *systemMockForExistingProject) FilterPresetByName(presets []checkmarx.Preset, presetName string) checkmarx.Preset {
+ return checkmarx.Preset{ID: 10050, Name: "SAP_JS_Default", OwnerName: "16"}
+}
+func (sys *systemMockForExistingProject) FilterPresetByID(presets []checkmarx.Preset, presetID int) checkmarx.Preset {
+ return checkmarx.Preset{ID: 10048, Name: "SAP_Default", OwnerName: "16"}
+}
+func (sys *systemMockForExistingProject) FilterProjectByName(projects []checkmarx.Project, projectName string) checkmarx.Project {
+ return checkmarx.Project{ID: 1, Name: "TestExisting", TeamID: "16", IsPublic: false}
+}
+func (sys *systemMockForExistingProject) GetProjectByID(projectID int) (bool, checkmarx.Project) {
+ return false, checkmarx.Project{}
+}
+func (sys *systemMockForExistingProject) GetProjectsByNameAndTeam(projectName, teamID string) []checkmarx.Project {
+ return []checkmarx.Project{checkmarx.Project{ID: 19, Name: projectName, TeamID: teamID, IsPublic: false}}
+}
+func (sys *systemMockForExistingProject) FilterTeamByName(teams []checkmarx.Team, teamName string) checkmarx.Team {
+ return checkmarx.Team{ID: "16", FullName: "OpenSource/Cracks/16"}
+}
+func (sys *systemMockForExistingProject) FilterTeamByID(teams []checkmarx.Team, teamID string) checkmarx.Team {
+ return checkmarx.Team{ID: "15", FullName: "OpenSource/Cracks/15"}
+}
+func (sys *systemMockForExistingProject) DownloadReport(reportID int) (bool, []byte) {
+ return true, sys.response.([]byte)
+}
+func (sys *systemMockForExistingProject) GetReportStatus(reportID int) checkmarx.ReportStatusResponse {
+ return checkmarx.ReportStatusResponse{Status: checkmarx.ReportStatus{ID: 2, Value: "Created"}}
+}
+func (sys *systemMockForExistingProject) RequestNewReport(scanID int, reportType string) (bool, checkmarx.Report) {
+ return true, checkmarx.Report{ReportID: 17}
+}
+func (sys *systemMockForExistingProject) GetResults(scanID int) checkmarx.ResultsStatistics {
+ return checkmarx.ResultsStatistics{}
+}
+func (sys *systemMockForExistingProject) GetScans(projectID int) (bool, []checkmarx.ScanStatus) {
+ return true, []checkmarx.ScanStatus{checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: true}, checkmarx.ScanStatus{IsIncremental: false}}
+}
+func (sys *systemMockForExistingProject) GetScanStatusAndDetail(scanID int) (string, checkmarx.ScanStatusDetail) {
+ return "Finished", checkmarx.ScanStatusDetail{Stage: "", Step: ""}
+}
+func (sys *systemMockForExistingProject) ScanProject(projectID int, isIncrementalV, isPublicV, forceScanV bool) (bool, checkmarx.Scan) {
+ sys.isIncremental = isIncrementalV
+ sys.isPublic = isPublicV
+ sys.forceScan = forceScanV
+ return true, checkmarx.Scan{ID: 16}
+}
+func (sys *systemMockForExistingProject) UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool {
+ return true
+}
+func (sys *systemMockForExistingProject) UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool {
+ return true
+}
+func (sys *systemMockForExistingProject) UploadProjectSourceCode(projectID int, zipFile string) bool {
+ return true
+}
+func (sys *systemMockForExistingProject) CreateProject(projectName string, teamID string) (bool, checkmarx.ProjectCreateResult) {
+ return false, checkmarx.ProjectCreateResult{}
+}
+func (sys *systemMockForExistingProject) CreateBranch(projectID int, branchName string) int {
+ return 0
+}
+func (sys *systemMockForExistingProject) GetPresets() []checkmarx.Preset {
+ return []checkmarx.Preset{checkmarx.Preset{ID: 10078, Name: "SAP Java Default", OwnerName: "16"}, checkmarx.Preset{ID: 10048, Name: "SAP JS Default", OwnerName: "16"}}
+}
+func (sys *systemMockForExistingProject) GetProjects() []checkmarx.Project {
+ return []checkmarx.Project{checkmarx.Project{ID: 1, Name: "TestExisting", TeamID: "16"}}
+}
+func (sys *systemMockForExistingProject) GetTeams() []checkmarx.Team {
+ return []checkmarx.Team{checkmarx.Team{ID: "16", FullName: "OpenSource/Cracks/16"}, checkmarx.Team{ID: "15", FullName: "OpenSource/Cracks/15"}}
+}
+
+func TestFilterFileGlob(t *testing.T) {
+ tt := []struct {
+ input string
+ fInfo fileInfo
+ expected bool
+ }{
+ {input: "somepath/node_modules/someOther/some.file", fInfo: fileInfo{}, expected: true},
+ {input: "somepath/non_modules/someOther/some.go", fInfo: fileInfo{}, expected: false},
+ {input: ".xmake/someOther/some.go", fInfo: fileInfo{}, expected: true},
+ {input: "another/vendor/some.html", fInfo: fileInfo{}, expected: false},
+ {input: "another/vendor/some.pdf", fInfo: fileInfo{}, expected: true},
+ {input: "another/vendor/some.test", fInfo: fileInfo{}, expected: true},
+ {input: "some.test", fInfo: fileInfo{}, expected: false},
+ {input: "a/b/c", fInfo: fileInfo{dir: true}, expected: false},
+ }
+
+ for k, v := range tt {
+ assert.Equal(t, v.expected, filterFileGlob([]string{"!**/node_modules/**", "!**/.xmake/**", "!**/*_test.go", "!**/vendor/**/*.go", "**/*.go", "**/*.html", "*.test"}, v.input, v.fInfo), fmt.Sprintf("wrong long name for run %v", k))
+ }
+}
+
+func TestZipFolder(t *testing.T) {
+
+ t.Run("zip files", func(t *testing.T) {
+ dir, err := ioutil.TempDir("", "test zip files")
+ if err != nil {
+ t.Fatal("Failed to create temporary directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(dir)
+
+ ioutil.WriteFile(filepath.Join(dir, "abcd.go"), []byte{byte(1), byte(2), byte(3)}, 0700)
+ ioutil.WriteFile(filepath.Join(dir, "somepath", "abcd.txt"), []byte{byte(1), byte(2), byte(3)}, 0700)
+ ioutil.WriteFile(filepath.Join(dir, "abcd_test.go"), []byte{byte(1), byte(2), byte(3)}, 0700)
+
+ var zipFileMock bytes.Buffer
+ zipFolder(dir, &zipFileMock, []string{"!abc_test.go", "**/abcd.txt", "**/abc.go"})
+
+ got := zipFileMock.Len()
+ want := 164
+
+ if got != want {
+ t.Errorf("Zipping test failed expected %v but got %v", want, got)
+ }
+ })
+}
+
+func TestGetDetailedResults(t *testing.T) {
+
+ t.Run("success case", func(t *testing.T) {
+ sys := &systemMock{response: []byte(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `)}
+ dir, err := ioutil.TempDir("", "test detailed results")
+ if err != nil {
+ t.Fatal("Failed to create temporary directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(dir)
+ result := getDetailedResults(sys, filepath.Join(dir, "abc.xml"), 2635)
+ assert.Equal(t, "2", result["ProjectId"], "Project ID incorrect")
+ assert.Equal(t, "Project 1", result["ProjectName"], "Project name incorrect")
+ assert.Equal(t, 2, result["High"].(map[string]int)["Issues"], "Number of High issues incorrect")
+ assert.Equal(t, 2, result["High"].(map[string]int)["NotFalsePositive"], "Number of High NotFalsePositive issues incorrect")
+ assert.Equal(t, 1, result["Medium"].(map[string]int)["Issues"], "Number of Medium issues incorrect")
+ assert.Equal(t, 0, result["Medium"].(map[string]int)["NotFalsePositive"], "Number of Medium NotFalsePositive issues incorrect")
+ })
+}
+
+func TestRunScan(t *testing.T) {
+ sys := &systemMockForExistingProject{response: []byte(``)}
+ options := checkmarxExecuteScanOptions{ProjectName: "TestExisting", VulnerabilityThresholdUnit: "absolute", FullScanCycle: "2", Incremental: true, FullScansScheduled: true, Preset: "10048", TeamID: "16", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true}
+ workspace, err := ioutil.TempDir("", "workspace1")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ assert.Equal(t, false, sys.isIncremental, "isIncremental has wrong value")
+ assert.Equal(t, false, sys.isPublic, "isPublic has wrong value")
+ assert.Equal(t, true, sys.forceScan, "forceScan has wrong value")
+}
+
+func TestRunScanWOtherCycle(t *testing.T) {
+ sys := &systemMock{response: []byte(``), createProject: true}
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", FullScanCycle: "3", Incremental: true, FullScansScheduled: true, Preset: "SAP_JS_Default", TeamID: "16", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true}
+ workspace, err := ioutil.TempDir("", "workspace2")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ assert.Equal(t, true, sys.isIncremental, "isIncremental has wrong value")
+ assert.Equal(t, false, sys.isPublic, "isPublic has wrong value")
+ assert.Equal(t, true, sys.forceScan, "forceScan has wrong value")
+}
+
+func TestRunScanForPullRequest(t *testing.T) {
+ sys := &systemMock{response: []byte(``)}
+ options := checkmarxExecuteScanOptions{PullRequestName: "Test_PR-19", ProjectName: "Test_PR-19", VulnerabilityThresholdUnit: "percentage", FullScanCycle: "3", Incremental: true, FullScansScheduled: true, Preset: "SAP_JS_Default", TeamID: "16", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true, AvoidDuplicateProjectScans: false}
+ workspace, err := ioutil.TempDir("", "workspace3")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ assert.Equal(t, true, sys.isIncremental, "isIncremental has wrong value")
+ assert.Equal(t, false, sys.isPublic, "isPublic has wrong value")
+ assert.Equal(t, true, sys.forceScan, "forceScan has wrong value")
+}
+
+func TestRunScanForPullRequestProjectNew(t *testing.T) {
+ sys := &systemMock{response: []byte(``), createProject: true}
+ options := checkmarxExecuteScanOptions{PullRequestName: "PR-17", ProjectName: "Test_PR-19", VulnerabilityThresholdUnit: "percentage", FullScanCycle: "3", Incremental: true, FullScansScheduled: true, Preset: "10048", TeamName: "OpenSource/Cracks/15", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true}
+ workspace, err := ioutil.TempDir("", "workspace4")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ assert.Equal(t, true, sys.isIncremental, "isIncremental has wrong value")
+ assert.Equal(t, false, sys.isPublic, "isPublic has wrong value")
+ assert.Equal(t, true, sys.forceScan, "forceScan has wrong value")
+}
+
+func TestRunScanHighViolationPercentage(t *testing.T) {
+ if os.Getenv("BE_CRASHER") == "1" {
+ sys := &systemMock{response: []byte(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `)}
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", VulnerabilityThresholdResult: "FAILURE", VulnerabilityThresholdHigh: 100, FullScanCycle: "10", FullScansScheduled: true, Preset: "10048", TeamID: "16", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true}
+ workspace, err := ioutil.TempDir("", "workspace5")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ return
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=TestRunScanHighViolationPercentage")
+ cmd.Env = append(os.Environ(), "BE_CRASHER=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok && !e.Success() {
+ return
+ }
+ t.Fatalf("process ran with err %v, want exit status 1", err)
+}
+
+func TestRunScanHighViolationAbsolute(t *testing.T) {
+ if os.Getenv("BE_CRASHER") == "1" {
+ sys := &systemMock{response: []byte(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `)}
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "absolute", VulnerabilityThresholdResult: "FAILURE", VulnerabilityThresholdLow: 1, FullScanCycle: "10", FullScansScheduled: true, Preset: "10048", TeamID: "16", VulnerabilityThresholdEnabled: true, GeneratePdfReport: true}
+ workspace, err := ioutil.TempDir("", "workspace6")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ influx := checkmarxExecuteScanInflux{}
+
+ runScan(options, sys, workspace, &influx)
+ return
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=TestRunScanHighViolationAbsolute")
+ cmd.Env = append(os.Environ(), "BE_CRASHER=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok && !e.Success() {
+ return
+ }
+ t.Fatalf("process ran with err %v, want exit status 1", err)
+}
+
+func TestEnforceThresholds(t *testing.T) {
+ results := map[string]interface{}{}
+ results["High"] = map[string]int{}
+ results["Medium"] = map[string]int{}
+ results["Low"] = map[string]int{}
+
+ results["High"].(map[string]int)["NotFalsePositive"] = 10
+ results["Medium"].(map[string]int)["NotFalsePositive"] = 10
+ results["Low"].(map[string]int)["NotFalsePositive"] = 10
+ results["Low"].(map[string]int)["NotExploitable"] = 0
+ results["Low"].(map[string]int)["Confirmed"] = 0
+
+ results["High"].(map[string]int)["Issues"] = 10
+ results["Medium"].(map[string]int)["Issues"] = 10
+ results["Low"].(map[string]int)["Issues"] = 10
+
+ t.Run("percentage high violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", VulnerabilityThresholdHigh: 100, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("absolute high violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "absolute", VulnerabilityThresholdHigh: 5, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("percentage medium violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", VulnerabilityThresholdMedium: 100, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("absolute medium violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "absolute", VulnerabilityThresholdMedium: 5, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("percentage low violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", VulnerabilityThresholdLow: 100, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("absolute low violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "absolute", VulnerabilityThresholdLow: 5, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, true, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("percentage no violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "percentage", VulnerabilityThresholdLow: 0, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, false, insecure, "Expected results to be insecure but where not")
+ })
+
+ t.Run("absolute no violation", func(t *testing.T) {
+ options := checkmarxExecuteScanOptions{VulnerabilityThresholdUnit: "absolute", VulnerabilityThresholdLow: 15, VulnerabilityThresholdMedium: 15, VulnerabilityThresholdHigh: 15, VulnerabilityThresholdEnabled: true}
+ insecure := enforceThresholds(options, results)
+
+ assert.Equal(t, false, insecure, "Expected results to be insecure but where not")
+ })
+}
+
+func TestLoadPreset(t *testing.T) {
+ sys := &systemMock{}
+ t.Run("resolve via code", func(t *testing.T) {
+ ok, preset := loadPreset(sys, "10048")
+ assert.Equal(t, true, ok, "Expected success but failed")
+ assert.Equal(t, 10048, preset.ID, "Expected result but got none")
+ })
+
+ t.Run("resolve via name", func(t *testing.T) {
+ ok, preset := loadPreset(sys, "SAP_JS_Default")
+ assert.Equal(t, true, ok, "Expected success but failed")
+ assert.Equal(t, "SAP_JS_Default", preset.Name, "Expected result but got none")
+ })
+
+ t.Run("error case", func(t *testing.T) {
+ ok, preset := loadPreset(sys, "")
+ assert.Equal(t, false, ok, "Expected error but succeeded")
+ assert.Equal(t, 0, preset.ID, "Expected result but got none")
+ })
+}
diff --git a/cmd/getConfig_test.go b/cmd/getConfig_test.go
index 65b1127b7..a0fdaefb4 100644
--- a/cmd/getConfig_test.go
+++ b/cmd/getConfig_test.go
@@ -84,7 +84,7 @@ func TestDefaultsAndFilters(t *testing.T) {
t.Run("Step config", func(t *testing.T) {
defaults, filters, err := defaultsAndFilters(&metadata, "stepName")
assert.Equal(t, 0, len(defaults), "getting defaults failed")
- assert.Equal(t, 1, len(filters.All), "wrong number of filter values")
+ assert.Equal(t, 2, len(filters.All), "wrong number of filter values")
assert.NoError(t, err, "error occured but none expected")
})
}
diff --git a/cmd/piper.go b/cmd/piper.go
index 212316c25..adfeda319 100644
--- a/cmd/piper.go
+++ b/cmd/piper.go
@@ -50,6 +50,7 @@ func Execute() {
rootCmd.AddCommand(XsDeployCommand())
rootCmd.AddCommand(GithubPublishReleaseCommand())
rootCmd.AddCommand(GithubCreatePullRequestCommand())
+ rootCmd.AddCommand(CheckmarxExecuteScanCommand())
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {
@@ -118,6 +119,12 @@ func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName strin
}
}
+ if !GeneralConfig.Verbose {
+ if stepConfig.Config["verbose"] != nil && stepConfig.Config["verbose"].(bool) {
+ log.SetVerbose(stepConfig.Config["verbose"].(bool))
+ }
+ }
+
confJSON, _ := json.Marshal(stepConfig.Config)
json.Unmarshal(confJSON, &options)
diff --git a/documentation/docs/steps/checkmarxExecuteScan.md b/documentation/docs/steps/checkmarxExecuteScan.md
new file mode 100644
index 000000000..63991c134
--- /dev/null
+++ b/documentation/docs/steps/checkmarxExecuteScan.md
@@ -0,0 +1,7 @@
+# ${docGenStepName}
+
+## ${docGenDescription}
+
+## ${docGenParameters}
+
+## ${docGenConfiguration}
diff --git a/go.mod b/go.mod
index 5a542fdae..9d26ef1fc 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/SAP/jenkins-library
go 1.13
require (
+ github.com/bmatcuk/doublestar v1.2.2
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.3.1
github.com/google/go-github/v28 v28.1.1
diff --git a/go.sum b/go.sum
index 66ca900cd..ef40f6648 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/bmatcuk/doublestar v1.2.2 h1:oC24CykoSAB8zd7XgruHo33E0cHJf/WhQA/7BeXj+x0=
+github.com/bmatcuk/doublestar v1.2.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
diff --git a/pkg/checkmarx/checkmarx.go b/pkg/checkmarx/checkmarx.go
new file mode 100644
index 000000000..2ea922509
--- /dev/null
+++ b/pkg/checkmarx/checkmarx.go
@@ -0,0 +1,703 @@
+package checkmarx
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "encoding/xml"
+
+ piperHttp "github.com/SAP/jenkins-library/pkg/http"
+ "github.com/SAP/jenkins-library/pkg/log"
+ "github.com/SAP/jenkins-library/pkg/piperutils"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// AuthToken - Structure to store OAuth2 token
+type AuthToken struct {
+ TokenType string `json:"token_type"`
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+// Preset - Project's Preset
+type Preset struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ OwnerName string `json:"ownerName"`
+ Link Link `json:"link"`
+}
+
+// Scan - Scan Structure
+type Scan struct {
+ ID int `json:"id"`
+ Link Link `json:"link"`
+}
+
+// ProjectCreateResult - ProjectCreateResult Structure
+type ProjectCreateResult struct {
+ ID int `json:"id"`
+ Link Link `json:"link"`
+}
+
+// Report - Report Structure
+type Report struct {
+ ReportID int `json:"reportId"`
+ Links Links `json:"links"`
+}
+
+// ResultsStatistics - ResultsStatistics Structure
+type ResultsStatistics struct {
+ High int `json:"highSeverity"`
+ Medium int `json:"mediumSeverity"`
+ Low int `json:"lowSeverity"`
+ Info int `json:"infoSeverity"`
+}
+
+// ScanStatus - ScanStatus Structure
+type ScanStatus struct {
+ ID int `json:"id"`
+ Link Link `json:"link"`
+ Status Status `json:"status"`
+ ScanType string `json:"scanType"`
+ Comment string `json:"comment"`
+ IsIncremental bool `json:"isIncremental"`
+}
+
+// Status - Status Structure
+type Status struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Details ScanStatusDetail `json:"details"`
+}
+
+// ScanStatusDetail - ScanStatusDetail Structure
+type ScanStatusDetail struct {
+ Stage string `json:"stage"`
+ Step string `json:"step"`
+}
+
+// ReportStatusResponse - ReportStatusResponse Structure
+type ReportStatusResponse struct {
+ Location string `json:"location"`
+ ContentType string `json:"contentType"`
+ Status ReportStatus `json:"status"`
+}
+
+// ReportStatus - ReportStatus Structure
+type ReportStatus struct {
+ ID int `json:"id"`
+ Value string `json:"value"`
+}
+
+// Project - Project Structure
+type Project struct {
+ ID int `json:"id"`
+ TeamID string `json:"teamId"`
+ Name string `json:"name"`
+ IsPublic bool `json:"isPublic"`
+ SourceSettingsLink SourceSettingsLink `json:"sourceSettingsLink"`
+ Link Link `json:"link"`
+}
+
+// Team - Team Structure
+type Team struct {
+ ID string `json:"id"`
+ FullName string `json:"fullName"`
+}
+
+// Links - Links Structure
+type Links struct {
+ Report Link `json:"report"`
+ Status Link `json:"status"`
+}
+
+// Link - Link Structure
+type Link struct {
+ Rel string `json:"rel"`
+ URI string `json:"uri"`
+}
+
+// SourceSettingsLink - SourceSettingsLink Structure
+type SourceSettingsLink struct {
+ Type string `json:"type"`
+ Rel string `json:"rel"`
+ URI string `json:"uri"`
+}
+
+//DetailedResult - DetailedResult Structure
+type DetailedResult struct {
+ XMLName xml.Name `xml:"CxXMLResults"`
+ InitiatorName string `xml:"InitiatorName,attr"`
+ ScanID string `xml:"ScanId,attr"`
+ Owner string `xml:"Owner,attr"`
+ ProjectID string `xml:"ProjectId,attr"`
+ ProjectName string `xml:"ProjectName,attr"`
+ TeamFullPathOnReportDate string `xml:"TeamFullPathOnReportDate,attr"`
+ DeepLink string `xml:"DeepLink,attr"`
+ ScanStart string `xml:"ScanStart,attr"`
+ Preset string `xml:"Preset,attr"`
+ ScanTime string `xml:"ScanTime,attr"`
+ LinesOfCodeScanned string `xml:"LinesOfCodeScanned,attr"`
+ FilesScanned string `xml:"FilesScanned,attr"`
+ ReportCreationTime string `xml:"ReportCreationTime,attr"`
+ Team string `xml:"Team,attr"`
+ CheckmarxVersion string `xml:"CheckmarxVersion,attr"`
+ ScanType string `xml:"ScanType,attr"`
+ SourceOrigin string `xml:"SourceOrigin,attr"`
+ Visibility string `xml:"Visibility,attr"`
+ Queries []Query `xml:"Query"`
+}
+
+// Query - Query Structure
+type Query struct {
+ XMLName xml.Name `xml:"Query"`
+ Results []Result `xml:"Result"`
+}
+
+// Result - Result Structure
+type Result struct {
+ XMLName xml.Name `xml:"Result"`
+ State string `xml:"state,attr"`
+ Severity string `xml:"Severity,attr"`
+ FalsePositive string `xml:"FalsePositive,attr"`
+}
+
+// SystemInstance is the client communicating with the Checkmarx backend
+type SystemInstance struct {
+ serverURL string
+ username string
+ password string
+ client piperHttp.Uploader
+ logger *logrus.Entry
+}
+
+// System is the interface abstraction of a specific SystemIns
+type System interface {
+ FilterPresetByName(presets []Preset, presetName string) Preset
+ FilterPresetByID(presets []Preset, presetID int) Preset
+ FilterProjectByName(projects []Project, projectName string) Project
+ FilterTeamByName(teams []Team, teamName string) Team
+ FilterTeamByID(teams []Team, teamID string) Team
+ DownloadReport(reportID int) (bool, []byte)
+ GetReportStatus(reportID int) ReportStatusResponse
+ RequestNewReport(scanID int, reportType string) (bool, Report)
+ GetResults(scanID int) ResultsStatistics
+ GetScanStatusAndDetail(scanID int) (string, ScanStatusDetail)
+ GetScans(projectID int) (bool, []ScanStatus)
+ ScanProject(projectID int, isIncremental, isPublic, forceScan bool) (bool, Scan)
+ UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool
+ UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool
+ UploadProjectSourceCode(projectID int, zipFile string) bool
+ CreateProject(projectName string, teamID string) (bool, ProjectCreateResult)
+ CreateBranch(projectID int, branchName string) int
+ GetPresets() []Preset
+ GetProjectByID(projectID int) (bool, Project)
+ GetProjectsByNameAndTeam(projectName, teamID string) []Project
+ GetProjects() []Project
+ GetTeams() []Team
+}
+
+// NewSystemInstance returns a new Checkmarx client for communicating with the backend
+func NewSystemInstance(client piperHttp.Uploader, serverURL, username, password string) (*SystemInstance, error) {
+ loggerInstance := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx")
+ sys := &SystemInstance{
+ serverURL: serverURL,
+ username: username,
+ password: password,
+ client: client,
+ logger: loggerInstance,
+ }
+
+ token, err := sys.getOAuth2Token()
+ if err != nil {
+ return sys, errors.Wrap(err, "Error fetching oAuth token")
+ }
+
+ options := piperHttp.ClientOptions{
+ Token: token,
+ Timeout: time.Second * 60,
+ }
+ sys.client.SetOptions(options)
+
+ return sys, nil
+}
+
+func sendRequest(sys *SystemInstance, method, url string, body io.Reader, header http.Header) ([]byte, error) {
+ return sendRequestInternal(sys, method, url, body, header, "200:399")
+}
+
+func sendRequestInternal(sys *SystemInstance, method, url string, body io.Reader, header http.Header, validStatusCodeRange string) ([]byte, error) {
+ var requestBody io.Reader
+ var requestBodyCopy io.Reader
+ if body != nil {
+ closer := ioutil.NopCloser(body)
+ bodyBytes, _ := ioutil.ReadAll(closer)
+ requestBody = bytes.NewBuffer(bodyBytes)
+ requestBodyCopy = bytes.NewBuffer(bodyBytes)
+ defer closer.Close()
+ }
+ response, err := sys.client.SendRequest(method, fmt.Sprintf("%v/cxrestapi%v", sys.serverURL, url), requestBody, header, nil)
+ if err != nil {
+ sys.recordRequestDetailsInErrorCase(requestBodyCopy, response)
+ sys.logger.Errorf("HTTP request failed with error: %s", err)
+ return nil, err
+ }
+
+ var validResponseCodeList []int
+ values := strings.Split(validStatusCodeRange, ",")
+ for _, value := range values {
+ parts := strings.Split(value, ":")
+ if len(parts) > 1 {
+ lower, _ := strconv.Atoi(parts[0])
+ upper, _ := strconv.Atoi(parts[1])
+ for i := lower; i <= upper; i++ {
+ validResponseCodeList = append(validResponseCodeList, i)
+ }
+ } else {
+ validCode, _ := strconv.Atoi(value)
+ validResponseCodeList = append(validResponseCodeList, validCode)
+ }
+ }
+
+ if piperutils.ContainsInt(validResponseCodeList, response.StatusCode) {
+ data, _ := ioutil.ReadAll(response.Body)
+ sys.logger.Debugf("Valid response body: %v", string(data))
+ defer response.Body.Close()
+ return data, nil
+ }
+ sys.recordRequestDetailsInErrorCase(requestBodyCopy, response)
+ sys.logger.Errorf("HTTP request failed with error %s", response.Status)
+ return nil, errors.Errorf("Invalid HTTP status %v with with code %v received", response.Status, response.StatusCode)
+}
+
+func (sys *SystemInstance) recordRequestDetailsInErrorCase(requestBody io.Reader, response *http.Response) {
+ if requestBody != nil {
+ data, _ := ioutil.ReadAll(ioutil.NopCloser(requestBody))
+ sys.logger.Errorf("Request body: %s", data)
+ }
+ if response != nil && response.Body != nil {
+ data, _ := ioutil.ReadAll(response.Body)
+ sys.logger.Errorf("Response body: %s", data)
+ response.Body.Close()
+ }
+}
+
+func (sys *SystemInstance) getOAuth2Token() (string, error) {
+ body := url.Values{
+ "username": {sys.username},
+ "password": {sys.password},
+ "grant_type": {"password"},
+ "scope": {"sast_rest_api"},
+ "client_id": {"resource_owner_client"},
+ "client_secret": {"014DF517-39D1-4453-B7B3-9930C563627C"},
+ }
+ header := http.Header{}
+ header.Add("Content-type", "application/x-www-form-urlencoded")
+ data, err := sendRequest(sys, http.MethodPost, "/auth/identity/connect/token", strings.NewReader(body.Encode()), header)
+ if err != nil {
+ return "", err
+ }
+
+ var token AuthToken
+ json.Unmarshal(data, &token)
+ return token.TokenType + " " + token.AccessToken, nil
+}
+
+// GetTeams returns the teams the user is assigned to
+func (sys *SystemInstance) GetTeams() []Team {
+ sys.logger.Debug("Getting Teams...")
+ var teams []Team
+
+ data, err := sendRequest(sys, http.MethodGet, "/auth/teams", nil, nil)
+ if err != nil {
+ sys.logger.Errorf("Fetching teams failed: %s", err)
+ return teams
+ }
+
+ json.Unmarshal(data, &teams)
+ return teams
+}
+
+// GetProjects returns the projects defined in the Checkmarx backend which the user has access to
+func (sys *SystemInstance) GetProjects() []Project {
+ return sys.GetProjectsByNameAndTeam("", "")
+}
+
+// GetProjectByID returns the project addressed by projectID from the Checkmarx backend which the user has access to
+func (sys *SystemInstance) GetProjectByID(projectID int) (bool, Project) {
+ sys.logger.Debugf("Getting Project with ID %v...", projectID)
+ var project Project
+
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/projects/%v", projectID), nil, nil)
+ if err != nil {
+ sys.logger.Errorf("Fetching projects failed: %s", err)
+ return false, project
+ }
+
+ json.Unmarshal(data, &project)
+ return true, project
+}
+
+// GetProjectsByNameAndTeam returns the project addressed by projectID from the Checkmarx backend which the user has access to
+func (sys *SystemInstance) GetProjectsByNameAndTeam(projectName, teamID string) []Project {
+ sys.logger.Debugf("Getting projects with name %v of team %v...", projectName, teamID)
+ var projects []Project
+ header := http.Header{}
+ header.Set("Accept-Type", "application/json")
+ var data []byte
+ var err error
+ if len(teamID) > 0 && len(projectName) > 0 {
+ body := url.Values{
+ "projectName": {projectName},
+ "teamId": {teamID},
+ }
+ data, err = sendRequestInternal(sys, http.MethodGet, fmt.Sprintf("/projects?%v", body.Encode()), nil, header, "200:399,404")
+ } else {
+ data, err = sendRequestInternal(sys, http.MethodGet, "/projects", nil, header, "200:399,404")
+ }
+ if err != nil {
+ sys.logger.Errorf("Fetching projects failed: %s", err)
+ return projects
+ }
+
+ json.Unmarshal(data, &projects)
+ return projects
+}
+
+// CreateProject creates a new project in the Checkmarx backend
+func (sys *SystemInstance) CreateProject(projectName string, teamID string) (bool, ProjectCreateResult) {
+ var result ProjectCreateResult
+ jsonData := map[string]interface{}{
+ "name": projectName,
+ "owningTeam": teamID,
+ "isPublic": true,
+ }
+
+ jsonValue, err := json.Marshal(jsonData)
+ if err != nil {
+ sys.logger.Errorf("Error Marshal: %s", err)
+ return false, result
+ }
+
+ header := http.Header{}
+ header.Set("Content-Type", "application/json")
+
+ data, err := sendRequest(sys, http.MethodPost, "/projects", bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("Failed to create project: %s", err)
+ return false, result
+ }
+
+ json.Unmarshal(data, &result)
+ return true, result
+}
+
+// CreateBranch creates a branch of an existing project in the Checkmarx backend
+func (sys *SystemInstance) CreateBranch(projectID int, branchName string) int {
+ jsonData := map[string]interface{}{
+ "name": branchName,
+ }
+
+ jsonValue, err := json.Marshal(jsonData)
+ if err != nil {
+ sys.logger.Errorf("Error Marshal: %s", err)
+ return 0
+ }
+
+ header := http.Header{}
+ header.Set("Content-Type", "application/json")
+ data, err := sendRequest(sys, http.MethodPost, fmt.Sprintf("/projects/%v/branch", projectID), bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("Failed to create project: %s", err)
+ return 0
+ }
+
+ var scan Scan
+
+ json.Unmarshal(data, &scan)
+ return scan.ID
+}
+
+// UploadProjectSourceCode zips and uploads the project sources for scanning
+func (sys *SystemInstance) UploadProjectSourceCode(projectID int, zipFile string) bool {
+ sys.logger.Debug("Starting to upload files...")
+
+ header := http.Header{}
+ header.Add("Accept-Encoding", "gzip,deflate")
+ header.Add("Accept", "text/plain")
+ resp, err := sys.client.UploadFile(fmt.Sprintf("%v/cxrestapi/projects/%v/sourceCode/attachments", sys.serverURL, projectID), zipFile, "zippedSource", header, nil)
+ if err != nil {
+ sys.logger.Errorf("Failed to uploaded zipped sources %s", err)
+ return false
+ }
+
+ data, err := ioutil.ReadAll(resp.Body)
+ defer resp.Body.Close()
+ if err != nil {
+ sys.logger.Errorf("Error reading the response data %s", err)
+ return false
+ }
+
+ responseData := make(map[string]string)
+ json.Unmarshal(data, &responseData)
+
+ if resp.StatusCode == http.StatusNoContent {
+ return true
+ }
+
+ sys.logger.Debugf("Body %s", data)
+ sys.logger.Errorf("Error writing the request's body: %s", resp.Status)
+ return false
+}
+
+// UpdateProjectExcludeSettings updates the exclude configuration of the project
+func (sys *SystemInstance) UpdateProjectExcludeSettings(projectID int, excludeFolders string, excludeFiles string) bool {
+ jsonData := map[string]string{
+ "excludeFoldersPattern": excludeFolders,
+ "excludeFilesPattern": excludeFiles,
+ }
+
+ jsonValue, err := json.Marshal(jsonData)
+ if err != nil {
+ sys.logger.Errorf("Error Marshal: %s", err)
+ return false
+ }
+
+ header := http.Header{}
+ header.Set("Content-Type", "application/json")
+ _, err = sendRequest(sys, http.MethodPut, fmt.Sprintf("/projects/%v/sourceCode/excludeSettings", projectID), bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("HTTP request failed with error: %s", err)
+ return false
+ }
+
+ return true
+}
+
+// GetPresets loads the preset values defined in the Checkmarx backend
+func (sys *SystemInstance) GetPresets() []Preset {
+ sys.logger.Debug("Getting Presets...")
+ var presets []Preset
+
+ data, err := sendRequest(sys, http.MethodGet, "/sast/presets", nil, nil)
+ if err != nil {
+ sys.logger.Errorf("Fetching presets failed: %s", err)
+ return presets
+ }
+
+ json.Unmarshal(data, &presets)
+ return presets
+}
+
+// UpdateProjectConfiguration updates the configuration of the project addressed by projectID
+func (sys *SystemInstance) UpdateProjectConfiguration(projectID int, presetID int, engineConfigurationID string) bool {
+ engineConfigID, _ := strconv.Atoi(engineConfigurationID)
+ jsonData := map[string]interface{}{
+ "projectId": projectID,
+ "presetId": presetID,
+ "engineConfigurationId": engineConfigID,
+ }
+
+ jsonValue, err := json.Marshal(jsonData)
+ if err != nil {
+ sys.logger.Errorf("Error marshal: %s", err)
+ return false
+ }
+
+ header := http.Header{}
+ header.Set("Content-Type", "application/json")
+ _, err = sendRequest(sys, http.MethodPost, "/sast/scanSettings", bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("HTTP request failed with error: %s", err)
+ return false
+ }
+
+ return true
+}
+
+// ScanProject triggers a scan on the project addressed by projectID
+func (sys *SystemInstance) ScanProject(projectID int, isIncremental, isPublic, forceScan bool) (bool, Scan) {
+ scan := Scan{}
+ jsonData := map[string]interface{}{
+ "projectId": projectID,
+ "isIncremental": false,
+ "isPublic": true,
+ "forceScan": true,
+ "comment": "Scan From Golang Script",
+ }
+
+ jsonValue, _ := json.Marshal(jsonData)
+
+ header := http.Header{}
+ header.Set("cxOrigin", "GolangScript")
+ header.Set("Content-Type", "application/json")
+ data, err := sendRequest(sys, http.MethodPost, "/sast/scans", bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("Failed to trigger scan of project %v: %s", projectID, err)
+ return false, scan
+ }
+
+ json.Unmarshal(data, &scan)
+ return true, scan
+}
+
+// GetScans returns all scan status on the project addressed by projectID
+func (sys *SystemInstance) GetScans(projectID int) (bool, []ScanStatus) {
+ scans := []ScanStatus{}
+ body := url.Values{
+ "projectId": {fmt.Sprintf("%v", projectID)},
+ "last": {fmt.Sprintf("%v", 20)},
+ }
+
+ header := http.Header{}
+ header.Set("cxOrigin", "GolangScript")
+ header.Set("Accept-Type", "application/json")
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans?%v", body.Encode()), nil, header)
+ if err != nil {
+ sys.logger.Errorf("Failed to fetch scans of project %v: %s", projectID, err)
+ return false, scans
+ }
+
+ json.Unmarshal(data, &scans)
+ return true, scans
+}
+
+// GetScanStatusAndDetail returns the status of the scan addressed by scanID
+func (sys *SystemInstance) GetScanStatusAndDetail(scanID int) (string, ScanStatusDetail) {
+ var scanStatus ScanStatus
+
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans/%v", scanID), nil, nil)
+ if err != nil {
+ sys.logger.Errorf("Failed to get scan status for scanID %v: %s", scanID, err)
+ return "Failed", ScanStatusDetail{}
+ }
+
+ json.Unmarshal(data, &scanStatus)
+ return scanStatus.Status.Name, scanStatus.Status.Details
+}
+
+// GetResults returns the results of the scan addressed by scanID
+func (sys *SystemInstance) GetResults(scanID int) ResultsStatistics {
+ var results ResultsStatistics
+
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/sast/scans/%v/resultsStatistics", scanID), nil, nil)
+ if err != nil {
+ sys.logger.Errorf("Failed to fetch scan results for scanID %v: %s", scanID, err)
+ return results
+ }
+
+ json.Unmarshal(data, &results)
+ return results
+}
+
+// RequestNewReport triggers the gereration of a report for a specific scan addressed by scanID
+func (sys *SystemInstance) RequestNewReport(scanID int, reportType string) (bool, Report) {
+ report := Report{}
+ jsonData := map[string]interface{}{
+ "scanId": scanID,
+ "reportType": reportType,
+ "comment": "Scan report triggered by Piper",
+ }
+
+ jsonValue, _ := json.Marshal(jsonData)
+
+ header := http.Header{}
+ header.Set("cxOrigin", "GolangScript")
+ header.Set("Content-Type", "application/json")
+ data, err := sendRequest(sys, http.MethodPost, "/reports/sastScan", bytes.NewBuffer(jsonValue), header)
+ if err != nil {
+ sys.logger.Errorf("Failed to trigger report generation for scan %v: %s", scanID, err)
+ return false, report
+ }
+
+ json.Unmarshal(data, &report)
+ return true, report
+}
+
+// GetReportStatus returns the status of the report generation process
+func (sys *SystemInstance) GetReportStatus(reportID int) ReportStatusResponse {
+ var response ReportStatusResponse
+
+ header := http.Header{}
+ header.Set("Accept", "application/json")
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/reports/sastScan/%v/status", reportID), nil, header)
+ if err != nil {
+ sys.logger.Errorf("Failed to fetch report status for reportID %v: %s", reportID, err)
+ return response
+ }
+
+ json.Unmarshal(data, &response)
+ return response
+}
+
+// DownloadReport downloads the report addressed by reportID and returns the XML contents
+func (sys *SystemInstance) DownloadReport(reportID int) (bool, []byte) {
+ header := http.Header{}
+ header.Set("Accept", "application/json")
+ data, err := sendRequest(sys, http.MethodGet, fmt.Sprintf("/reports/sastScan/%v", reportID), nil, header)
+ if err != nil {
+ sys.logger.Errorf("Failed to download report with reportID %v: %s", reportID, err)
+ return false, []byte{}
+ }
+ return true, data
+}
+
+// FilterTeamByName filters a team by its name
+func (sys *SystemInstance) FilterTeamByName(teams []Team, teamName string) Team {
+ for _, team := range teams {
+ if team.FullName == teamName {
+ return team
+ }
+ }
+ return Team{}
+}
+
+// FilterTeamByID filters a team by its ID
+func (sys *SystemInstance) FilterTeamByID(teams []Team, teamID string) Team {
+ for _, team := range teams {
+ if team.ID == teamID {
+ return team
+ }
+ }
+ return Team{}
+}
+
+// FilterProjectByName filters a project by its name
+func (sys *SystemInstance) FilterProjectByName(projects []Project, projectName string) Project {
+ for _, project := range projects {
+ if project.Name == projectName {
+ sys.logger.Debugf("Filtered project with name %v", project.Name)
+ return project
+ }
+ }
+ return Project{}
+}
+
+// FilterPresetByName filters a preset by its name
+func (sys *SystemInstance) FilterPresetByName(presets []Preset, presetName string) Preset {
+ for _, preset := range presets {
+ if preset.Name == presetName {
+ return preset
+ }
+ }
+ return Preset{}
+}
+
+// FilterPresetByID filters a preset by its name
+func (sys *SystemInstance) FilterPresetByID(presets []Preset, presetID int) Preset {
+ for _, preset := range presets {
+ if preset.ID == presetID {
+ return preset
+ }
+ }
+ return Preset{}
+}
diff --git a/pkg/checkmarx/checkmarx_test.go b/pkg/checkmarx/checkmarx_test.go
new file mode 100644
index 000000000..0b901e63e
--- /dev/null
+++ b/pkg/checkmarx/checkmarx_test.go
@@ -0,0 +1,566 @@
+package checkmarx
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "testing"
+
+ piperHttp "github.com/SAP/jenkins-library/pkg/http"
+ "github.com/SAP/jenkins-library/pkg/log"
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+)
+
+type senderMock struct {
+ token string
+ httpMethod string
+ httpStatusCode int
+ urlCalled string
+ requestBody string
+ responseBody string
+ header http.Header
+ logger *logrus.Entry
+ errorExp bool
+}
+
+func (sm *senderMock) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
+ if sm.errorExp {
+ return &http.Response{}, errors.New("Provoked technical error")
+ }
+ sm.httpMethod = method
+ sm.urlCalled = url
+ sm.header = header
+ if body != nil {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(body)
+ sm.requestBody = buf.String()
+ }
+ return &http.Response{StatusCode: sm.httpStatusCode, Body: ioutil.NopCloser(strings.NewReader(sm.responseBody))}, nil
+}
+func (sm *senderMock) UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
+ sm.httpMethod = http.MethodPost
+ sm.urlCalled = url
+ sm.header = header
+ return &http.Response{StatusCode: sm.httpStatusCode, Body: ioutil.NopCloser(bytes.NewReader([]byte(sm.responseBody)))}, nil
+}
+func (sm *senderMock) UploadRequest(method, url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
+ sm.httpMethod = http.MethodPost
+ sm.urlCalled = url
+ sm.header = header
+ return &http.Response{StatusCode: sm.httpStatusCode, Body: ioutil.NopCloser(bytes.NewReader([]byte(sm.responseBody)))}, nil
+}
+func (sm *senderMock) SetOptions(opts piperHttp.ClientOptions) {
+ sm.token = opts.Token
+}
+
+func TestSendRequest(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"some": "test"}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ _, err := sendRequest(&sys, "GET", "/test", nil, nil)
+
+ assert.NoError(t, err, "Error occured but none expected")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/test", myTestClient.urlCalled, "Called url incorrect")
+ })
+
+ t.Run("test error", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"some": "test"}`, httpStatusCode: 400}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ _, err := sendRequest(&sys, "GET", "/test", nil, nil)
+
+ assert.Error(t, err, "Error expected but none occured")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/test", myTestClient.urlCalled, "Called url incorrect")
+ })
+
+ t.Run("test technical error", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"some": "test"}`, httpStatusCode: 400}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ _, err := sendRequest(&sys, "error", "/test", nil, nil)
+
+ assert.Error(t, err, "Error expected but none occured")
+ })
+}
+
+func TestGetOAuthToken(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"token_type":"Bearer","access_token":"abcd12345","expires_in":7045634}`, httpStatusCode: 200}
+ sys, _ := NewSystemInstance(&myTestClient, "https://cx.wdf.sap.corp", "test", "user")
+ myTestClient.SetOptions(opts)
+
+ token, err := sys.getOAuth2Token()
+
+ assert.NoError(t, err, "Error occured but none expected")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/auth/identity/connect/token", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "Bearer abcd12345", token, "Token incorrect")
+ assert.Equal(t, "client_id=resource_owner_client&client_secret=014DF517-39D1-4453-B7B3-9930C563627C&grant_type=password&password=user&scope=sast_rest_api&username=test", myTestClient.requestBody, "Request body incorrect")
+ })
+
+ t.Run("test authentication failure", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{}`, httpStatusCode: 400}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ _, err := sys.getOAuth2Token()
+
+ assert.Error(t, err, "Error expected but none occured")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/auth/identity/connect/token", myTestClient.urlCalled, "Called url incorrect")
+ })
+
+ t.Run("test new system", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"token_type":"Bearer","access_token":"abcd12345","expires_in":7045634}`, httpStatusCode: 200}
+ _, err := NewSystemInstance(&myTestClient, "https://cx.wdf.sap.corp", "test", "user")
+
+ assert.NoError(t, err, "Error occured but none expected")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/auth/identity/connect/token", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "Bearer abcd12345", myTestClient.token, "Token incorrect")
+ })
+
+ t.Run("test technical error", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{}`, httpStatusCode: 400}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ myTestClient.errorExp = true
+
+ _, err := sys.getOAuth2Token()
+
+ assert.Error(t, err, "Error expected but none occured")
+ })
+}
+
+func TestGetTeams(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[{"id":"1", "fullName":"Team1"}, {"id":"2", "fullName":"Team2"}, {"id":"3", "fullName":"Team3"}]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ teams := sys.GetTeams()
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/auth/teams", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, 3, len(teams), "Number of Teams incorrect")
+ assert.Equal(t, "Team1", teams[0].FullName, "Team name 1 incorrect")
+ assert.Equal(t, "Team2", teams[1].FullName, "Team name 2 incorrect")
+ assert.Equal(t, "Team3", teams[2].FullName, "Team name 3 incorrect")
+
+ t.Run("test filter teams by name", func(t *testing.T) {
+ team2 := sys.FilterTeamByName(teams, "Team2")
+ assert.Equal(t, "Team2", team2.FullName, "Team name incorrect")
+ assert.Equal(t, "2", team2.ID, "Team id incorrect")
+ })
+
+ t.Run("test Filter teams by ID", func(t *testing.T) {
+ team1 := sys.FilterTeamByID(teams, "1")
+ assert.Equal(t, "Team1", team1.FullName, "Team name incorrect")
+ assert.Equal(t, "1", team1.ID, "Team id incorrect")
+ })
+
+ t.Run("test fail Filter teams by name", func(t *testing.T) {
+ team := sys.FilterTeamByName(teams, "Team")
+ assert.Equal(t, "", team.FullName, "Team name incorrect")
+ })
+ })
+
+ t.Run("test technical error", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[{"id":"1", "fullName":"Team1"}, {"id":"2", "fullName":"Team2"}, {"id":"3", "fullName":"Team3"}]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ myTestClient.errorExp = true
+
+ teams := sys.GetTeams()
+
+ assert.Equal(t, 0, len(teams), "Error expected but none occured")
+ })
+}
+
+func TestGetProjects(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[{"id":"1", "teamId":"1", "name":"Project1"}, {"id":"2", "teamId":"2", "name":"Project2"}]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ projects := sys.GetProjects()
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, 2, len(projects), "Number of Projects incorrect")
+ assert.Equal(t, "Project1", projects[0].Name, "Project name 1 incorrect")
+ assert.Equal(t, "Project2", projects[1].Name, "Project name 2 incorrect")
+
+ t.Run("test Filter projects by name", func(t *testing.T) {
+ project1 := sys.FilterProjectByName(projects, "Project1")
+ assert.Equal(t, "Project1", project1.Name, "Project name incorrect")
+ assert.Equal(t, "1", project1.TeamID, "Project teamId incorrect")
+ })
+
+ t.Run("test fail Filter projects by name", func(t *testing.T) {
+ project := sys.FilterProjectByName(projects, "Project5")
+ assert.Equal(t, "", project.Name, "Project name incorrect")
+ })
+ })
+
+ t.Run("test technical error", func(t *testing.T) {
+ myTestClient := senderMock{httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ myTestClient.errorExp = true
+
+ projects := sys.GetProjects()
+
+ assert.Equal(t, 0, len(projects), "Error expected but none occured")
+ })
+}
+
+func TestCreateProject(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"id": 16}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ ok, result := sys.CreateProject("TestProjectCreate", "4711")
+
+ assert.Equal(t, true, ok, "CreateProject call not successful")
+ assert.Equal(t, 16, result.ID, "Wrong project ID")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, "application/json", myTestClient.header.Get("Content-Type"), "Called url incorrect")
+ assert.Equal(t, `{"isPublic":true,"name":"TestProjectCreate","owningTeam":"4711"}`, myTestClient.requestBody, "Request body incorrect")
+ })
+
+ t.Run("test technical error", func(t *testing.T) {
+ myTestClient := senderMock{httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+ myTestClient.errorExp = true
+
+ result, _ := sys.CreateProject("Test", "13")
+
+ assert.Equal(t, false, result, "Error expected but none occured")
+ })
+}
+
+func TestUploadProjectSourceCode(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{httpStatusCode: 204}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.UploadProjectSourceCode(10415, "sources.zip")
+
+ assert.Equal(t, true, result, "UploadProjectSourceCode call not successful")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects/10415/sourceCode/attachments", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 2, len(myTestClient.header), "HTTP header incorrect")
+ assert.Equal(t, "gzip,deflate", myTestClient.header.Get("Accept-Encoding"), "HTTP header incorrect")
+ assert.Equal(t, "text/plain", myTestClient.header.Get("Accept"), "HTTP header incorrect")
+ })
+}
+
+func TestUpdateProjectExcludeSettings(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{httpStatusCode: 204}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.UpdateProjectExcludeSettings(10457, "some,test,a/b/c", "*.go")
+
+ assert.Equal(t, true, result, "UpdateProjectExcludeSettings call not successful")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects/10457/sourceCode/excludeSettings", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "PUT", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 1, len(myTestClient.header), "HTTP header incorrect")
+ assert.Equal(t, "application/json", myTestClient.header.Get("Content-Type"), "HTTP header incorrect")
+ assert.Equal(t, `{"excludeFilesPattern":"*.go","excludeFoldersPattern":"some,test,a/b/c"}`, myTestClient.requestBody, "Request body incorrect")
+ })
+}
+
+func TestGetPresets(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[{"id":1, "name":"Preset1", "ownerName":"Team1", "link":{"rel":"rel", "uri":"https://1234"}}, {"id":2, "name":"Preset2", "ownerName":"Team1", "link":{"rel":"re2l", "uri":"https://12347"}}]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ presets := sys.GetPresets()
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/presets", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, 2, len(presets), "Number of Presets incorrect")
+ assert.Equal(t, "Preset1", presets[0].Name, "Preset name incorrect")
+ assert.Equal(t, "https://1234", presets[0].Link.URI, "Preset name incorrect")
+ assert.Equal(t, "Preset2", presets[1].Name, "Preset name incorrect")
+
+ t.Run("test Filter preset by name", func(t *testing.T) {
+ preset2 := sys.FilterPresetByName(presets, "Preset2")
+ assert.Equal(t, "Preset2", preset2.Name, "Preset name incorrect")
+ assert.Equal(t, "Team1", preset2.OwnerName, "Preset ownerName incorrect")
+ })
+ t.Run("test fail Filter preset by name", func(t *testing.T) {
+ preset := sys.FilterPresetByName(presets, "Preset5")
+ assert.Equal(t, "", preset.Name, "Preset name incorrect")
+ })
+ t.Run("test Filter preset by ID", func(t *testing.T) {
+ preset2 := sys.FilterPresetByID(presets, 2)
+ assert.Equal(t, "Preset2", preset2.Name, "Preset ID incorrect")
+ assert.Equal(t, "Team1", preset2.OwnerName, "Preset ownerName incorrect")
+ })
+ t.Run("test fail Filter preset by ID", func(t *testing.T) {
+ preset := sys.FilterPresetByID(presets, 15)
+ assert.Equal(t, "", preset.Name, "Preset ID incorrect")
+ })
+ })
+}
+
+func TestUpdateProjectConfiguration(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{httpStatusCode: 204}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.UpdateProjectConfiguration(12, 15, "1")
+
+ assert.Equal(t, true, result, "UpdateProjectConfiguration call not successful")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/scanSettings", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, `{"engineConfigurationId":1,"presetId":15,"projectId":12}`, myTestClient.requestBody, "Request body incorrect")
+ })
+}
+
+func TestScanProject(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"id":1, "link":{"rel":"rel", "uri":"https://scan1234"}}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result, scan := sys.ScanProject(10745, false, false, false)
+
+ assert.Equal(t, true, result, "ScanProject call not successful")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/scans", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 1, scan.ID, "Scan ID incorrect")
+ assert.Equal(t, "https://scan1234", scan.Link.URI, "Scan link URI incorrect")
+ })
+}
+
+func TestGetScans(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[
+ {
+ "id": 1000000,
+ "project": {
+ "id": 1,
+ "name": "Project 1 (CxTechDocs)"
+ },
+ "status": {
+ "id": 7,
+ "name": "Finished"
+ },
+ "isIncremental": false
+ },
+ {
+ "id": 1000001,
+ "project": {
+ "id": 2,
+ "name": "Project 2 (CxTechDocs)"
+ },
+ "status": {
+ "id": 7,
+ "name": "Finished"
+ },
+ "isIncremental": true
+ }
+ ]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result, scans := sys.GetScans(10745)
+
+ assert.Equal(t, true, result, "ScanProject call not successful")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/scans?last=20&projectId=10745", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 2, len(scans), "Incorrect number of scans")
+ assert.Equal(t, true, scans[1].IsIncremental, "Scan link URI incorrect")
+ })
+}
+
+func TestGetScanStatusAndDetail(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"status":{"id":1,"name":"SUCCESS", "details":{"stage": "1 of 15", "step": "One"}}}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result, detail := sys.GetScanStatusAndDetail(10745)
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/scans/10745", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, "SUCCESS", result, "Request body incorrect")
+ assert.Equal(t, "One", detail.Step, "Detail step incorrect")
+ assert.Equal(t, "1 of 15", detail.Stage, "Detail stage incorrect")
+ })
+}
+
+func TestGetResults(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"highSeverity":5, "mediumSeverity":4, "lowSeverity":20, "infoSeverity":10}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.GetResults(10745)
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/sast/scans/10745/resultsStatistics", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 5, result.High, "High findings incorrect")
+ assert.Equal(t, 4, result.Medium, "Medium findings incorrect")
+ assert.Equal(t, 20, result.Low, "Low findings incorrect")
+ assert.Equal(t, 10, result.Info, "Info findings incorrect")
+ })
+}
+
+func TestRequestNewReport(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{
+ "reportId": 6,
+ "links": {
+ "report": {
+ "rel": "content",
+ "uri": "/reports/sastScan/6"
+ },
+ "status": {
+ "rel": "status",
+ "uri": "/reports/sastScan/6/status"
+ }
+ }
+ }`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ success, result := sys.RequestNewReport(10745, "XML")
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/reports/sastScan", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, `{"comment":"Scan report triggered by Piper","reportType":"XML","scanId":10745}`, myTestClient.requestBody, "Request body incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, true, success, "Result status incorrect")
+ assert.Equal(t, 6, result.ReportID, "Report ID incorrect")
+ })
+}
+
+func TestGetReportStatus(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{
+ "link": {
+ "rel": "content",
+ "uri": "/reports/sastScan/51"
+ },
+ "contentType": "application/xml",
+ "status": {
+ "id": 2,
+ "value": "Created"
+ }
+ }`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.GetReportStatus(6)
+
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/reports/sastScan/6/status", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 2, result.Status.ID, "Status ID incorrect")
+ assert.Equal(t, "Created", result.Status.Value, "Status incorrect")
+ })
+}
+
+func TestDownloadReport(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: "abc", httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ ok, result := sys.DownloadReport(6)
+ assert.Equal(t, true, ok, "DownloadReport returned unexpected error")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/reports/sastScan/6", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, []byte("abc"), result, "Result incorrect")
+ })
+}
+
+func TestCreateBranch(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"id": 13, "link": {}}`, httpStatusCode: 201}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.CreateBranch(6, "PR-17")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects/6/branch", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "POST", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, `{"name":"PR-17"}`, myTestClient.requestBody, "Request body incorrect")
+ assert.Equal(t, 13, result, "result incorrect")
+ })
+}
+
+func TestGetProjectByID(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `{"id": 209, "teamID": "Test", "name":"Project1_PR-18"}`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ ok, result := sys.GetProjectByID(815)
+ assert.Equal(t, true, ok, "GetProjectByID returned unexpected error")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects/815", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, 209, result.ID, "Result incorrect")
+ })
+}
+
+func TestGetProjectByName(t *testing.T) {
+ logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/checkmarx_test")
+ opts := piperHttp.ClientOptions{}
+ t.Run("test success", func(t *testing.T) {
+ myTestClient := senderMock{responseBody: `[{"id": 209, "teamID": "Test", "name":"Project1_PR-18"}]`, httpStatusCode: 200}
+ sys := SystemInstance{serverURL: "https://cx.wdf.sap.corp", client: &myTestClient, logger: logger}
+ myTestClient.SetOptions(opts)
+
+ result := sys.GetProjectsByNameAndTeam("Project1_PR-18", "Test")
+ assert.Equal(t, 1, len(result), "GetProjectByName returned unexpected error")
+ assert.Equal(t, "https://cx.wdf.sap.corp/cxrestapi/projects?projectName=Project1_PR-18&teamId=Test", myTestClient.urlCalled, "Called url incorrect")
+ assert.Equal(t, "GET", myTestClient.httpMethod, "HTTP method incorrect")
+ assert.Equal(t, "Project1_PR-18", result[0].Name, "Result incorrect")
+ })
+}
diff --git a/pkg/config/flags.go b/pkg/config/flags.go
index 6ad7c591e..2049be543 100644
--- a/pkg/config/flags.go
+++ b/pkg/config/flags.go
@@ -22,6 +22,8 @@ func AvailableFlagValues(cmd *cobra.Command, filters *StepFilters) map[string]in
flagValues[pflag.Name], _ = flags.GetStringSlice(pflag.Name)
case "bool":
flagValues[pflag.Name], _ = flags.GetBool(pflag.Name)
+ case "int":
+ flagValues[pflag.Name], _ = flags.GetInt(pflag.Name)
default:
fmt.Printf("Meta data type not set or not known: '%v'\n", pflag.Value.Type())
os.Exit(1)
diff --git a/pkg/config/stepmeta.go b/pkg/config/stepmeta.go
index bbe77c343..2ce711585 100644
--- a/pkg/config/stepmeta.go
+++ b/pkg/config/stepmeta.go
@@ -162,7 +162,7 @@ func (m *StepData) ReadPipelineStepData(metadata io.ReadCloser) error {
// GetParameterFilters retrieves all scope dependent parameter filters
func (m *StepData) GetParameterFilters() StepFilters {
- var filters StepFilters
+ filters := StepFilters{All: []string{"verbose"}, General: []string{"verbose"}, Steps: []string{"verbose"}, Stages: []string{"verbose"}, Parameters: []string{"verbose"}}
for _, param := range m.Spec.Inputs.Parameters {
parameterKeys := []string{param.Name}
for _, condition := range param.Conditions {
diff --git a/pkg/config/stepmeta_test.go b/pkg/config/stepmeta_test.go
index 5486ed262..3d7eb3834 100644
--- a/pkg/config/stepmeta_test.go
+++ b/pkg/config/stepmeta_test.go
@@ -116,41 +116,42 @@ func TestGetParameterFilters(t *testing.T) {
}{
{
Metadata: metadata1,
- ExpectedGeneral: []string{"paramOne", "paramSeven", "mta"},
- ExpectedSteps: []string{"paramOne", "paramTwo", "paramSeven", "mta"},
- ExpectedStages: []string{"paramOne", "paramTwo", "paramThree", "paramSeven", "mta"},
- ExpectedParameters: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramSeven", "mta"},
- ExpectedEnv: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSeven", "mta"},
- ExpectedAll: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix", "paramSeven", "mta"},
+ ExpectedGeneral: []string{"verbose", "paramOne", "paramSeven", "mta"},
+ ExpectedSteps: []string{"verbose", "paramOne", "paramTwo", "paramSeven", "mta"},
+ ExpectedStages: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramSeven", "mta"},
+ ExpectedParameters: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramFour", "paramSeven", "mta"},
+ ExpectedEnv: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSeven", "mta"},
+ ExpectedAll: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix", "paramSeven", "mta"},
NotExpectedGeneral: []string{"paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedSteps: []string{"paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedStages: []string{"paramFour", "paramFive", "paramSix"},
NotExpectedParameters: []string{"paramFive", "paramSix"},
- NotExpectedEnv: []string{"paramSix", "mta"},
+ NotExpectedEnv: []string{"verbose", "paramSix", "mta"},
NotExpectedAll: []string{},
},
{
Metadata: metadata2,
- ExpectedGeneral: []string{"paramOne"},
- ExpectedSteps: []string{"paramTwo"},
- ExpectedStages: []string{"paramThree"},
- ExpectedParameters: []string{"paramFour"},
+ ExpectedGeneral: []string{"verbose", "paramOne"},
+ ExpectedSteps: []string{"verbose", "paramTwo"},
+ ExpectedStages: []string{"verbose", "paramThree"},
+ ExpectedParameters: []string{"verbose", "paramFour"},
ExpectedEnv: []string{"paramFive"},
- ExpectedAll: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
+ ExpectedAll: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedGeneral: []string{"paramTwo", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedSteps: []string{"paramOne", "paramThree", "paramFour", "paramFive", "paramSix"},
NotExpectedStages: []string{"paramOne", "paramTwo", "paramFour", "paramFive", "paramSix"},
NotExpectedParameters: []string{"paramOne", "paramTwo", "paramThree", "paramFive", "paramSix"},
- NotExpectedEnv: []string{"paramOne", "paramTwo", "paramThree", "paramFour", "paramSix"},
+ NotExpectedEnv: []string{"verbose", "paramOne", "paramTwo", "paramThree", "paramFour", "paramSix"},
NotExpectedAll: []string{},
},
{
Metadata: metadata3,
- ExpectedGeneral: []string{},
- ExpectedStages: []string{},
- ExpectedSteps: []string{},
- ExpectedParameters: []string{},
+ ExpectedGeneral: []string{"verbose"},
+ ExpectedStages: []string{"verbose"},
+ ExpectedSteps: []string{"verbose"},
+ ExpectedParameters: []string{"verbose"},
ExpectedEnv: []string{},
+ ExpectedAll: []string{"verbose"},
},
}
diff --git a/pkg/http/http.go b/pkg/http/http.go
index ec9ac5487..f0166ac78 100644
--- a/pkg/http/http.go
+++ b/pkg/http/http.go
@@ -30,6 +30,7 @@ type ClientOptions struct {
Username string
Password string
Token string
+ Logger *logrus.Entry
}
// Sender provides an interface to the piper http client for uid/pwd and token authenticated requests
@@ -124,7 +125,7 @@ func (c *Client) SetOptions(options ClientOptions) {
c.username = options.Username
c.password = options.Password
c.token = options.Token
- c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
+ c.logger = options.Logger
}
func (c *Client) initialize() *http.Client {
@@ -196,4 +197,7 @@ func (c *Client) applyDefaults() {
if c.timeout == 0 {
c.timeout = time.Second * 10
}
+ if c.logger == nil {
+ c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
+ }
}
diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go
index a6d94f257..956123102 100644
--- a/pkg/http/http_test.go
+++ b/pkg/http/http_test.go
@@ -80,7 +80,7 @@ func TestSendRequest(t *testing.T) {
func TestSetOptions(t *testing.T) {
c := Client{}
- opts := ClientOptions{Timeout: 10, Username: "TestUser", Password: "TestPassword", Token: "TestToken"}
+ opts := ClientOptions{Timeout: 10, Username: "TestUser", Password: "TestPassword", Token: "TestToken", Logger: log.Entry().WithField("package", "github.com/SAP/jenkins-library/pkg/http")}
c.SetOptions(opts)
assert.Equal(t, opts.Timeout, c.timeout)
@@ -94,8 +94,8 @@ func TestApplyDefaults(t *testing.T) {
client Client
expected Client
}{
- {client: Client{}, expected: Client{timeout: time.Second * 10}},
- {client: Client{timeout: 10}, expected: Client{timeout: 10}},
+ {client: Client{}, expected: Client{timeout: time.Second * 10, logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}},
+ {client: Client{timeout: 10}, expected: Client{timeout: 10, logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}},
}
for k, v := range tt {
diff --git a/pkg/piperutils/slices.go b/pkg/piperutils/slices.go
new file mode 100644
index 000000000..52991916c
--- /dev/null
+++ b/pkg/piperutils/slices.go
@@ -0,0 +1,13 @@
+package piperutils
+
+import ()
+
+//ContainsInt check wether the element is part of the slice
+func ContainsInt(s []int, e int) bool {
+ for _, a := range s {
+ if a == e {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/piperutils/slices_test.go b/pkg/piperutils/slices_test.go
new file mode 100644
index 000000000..78eb46c31
--- /dev/null
+++ b/pkg/piperutils/slices_test.go
@@ -0,0 +1,17 @@
+package piperutils
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestContainsInt(t *testing.T) {
+ var intList []int
+ assert.Equal(t, false, ContainsInt(intList, 4), "False expected but returned true")
+
+ intList = append(intList, 1, 2, 3, 4, 5, 6, 20)
+ assert.Equal(t, true, ContainsInt(intList, 20), "True expected but returned false")
+ assert.Equal(t, true, ContainsInt(intList, 1), "True expected but returned false")
+ assert.Equal(t, true, ContainsInt(intList, 4), "True expected but returned false")
+ assert.Equal(t, false, ContainsInt(intList, 13), "False expected but returned true")
+}
diff --git a/pkg/piperutils/stepResults.go b/pkg/piperutils/stepResults.go
new file mode 100644
index 000000000..8f318d4bc
--- /dev/null
+++ b/pkg/piperutils/stepResults.go
@@ -0,0 +1,41 @@
+package piperutils
+
+import (
+ "encoding/json"
+
+ "github.com/SAP/jenkins-library/pkg/log"
+ "github.com/SAP/jenkins-library/pkg/piperenv"
+)
+
+// Path - struct to serialize paths and some metadata back to the invoker
+type Path struct {
+ Name string `json:"name"`
+ Target string `json:"target"`
+ Mandatory bool `json:"mandatory"`
+}
+
+// PersistReportsAndLinks stores the report paths and links in JSON format in the workspace for processing outside
+func PersistReportsAndLinks(workspace string, reports, links []Path) {
+ hasMandatoryReport := false
+ for _, report := range reports {
+ if report.Mandatory {
+ hasMandatoryReport = true
+ break
+ }
+ }
+ reportList, err := json.Marshal(&reports)
+ if err != nil {
+ if hasMandatoryReport {
+ log.Entry().Fatalln("Failed to marshall reports.json data for archiving")
+ }
+ log.Entry().Errorln("Failed to marshall reports.json data for archiving")
+ }
+ piperenv.SetParameter(workspace, "reports.json", string(reportList))
+
+ linkList, err := json.Marshal(&links)
+ if err != nil {
+ log.Entry().Errorln("Failed to marshall links.json data for archiving")
+ } else {
+ piperenv.SetParameter(workspace, "links.json", string(linkList))
+ }
+}
diff --git a/pkg/piperutils/stepResults_test.go b/pkg/piperutils/stepResults_test.go
new file mode 100644
index 000000000..48ed6c5c6
--- /dev/null
+++ b/pkg/piperutils/stepResults_test.go
@@ -0,0 +1,57 @@
+package piperutils
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPersistReportAndLinks(t *testing.T) {
+ workspace, err := ioutil.TempDir("", "workspace5")
+ if err != nil {
+ t.Fatal("Failed to create temporary workspace directory")
+ }
+ // clean up tmp dir
+ defer os.RemoveAll(workspace)
+
+ reports := []Path{Path{Target: "testFile1.json", Mandatory: true}, Path{Target: "testFile2.json"}}
+ links := []Path{Path{Target: "https://1234568.com/test", Name: "Weblink"}}
+ PersistReportsAndLinks(workspace, reports, links)
+
+ reportsJSONPath := filepath.Join(workspace, "reports.json")
+ reportsFileExists, err := FileExists(reportsJSONPath)
+ assert.NoError(t, err, "No error expected but got one")
+ assert.Equal(t, true, reportsFileExists, "reports.json missing")
+
+ linksJSONPath := filepath.Join(workspace, "links.json")
+ linksFileExists, err := FileExists(linksJSONPath)
+ assert.NoError(t, err, "No error expected but got one")
+ assert.Equal(t, true, linksFileExists, "links.json missing")
+
+ var reportsLoaded []Path
+ var linksLoaded []Path
+ reportsFileData, err := ioutil.ReadFile(reportsJSONPath)
+ reportsDataString := string(reportsFileData)
+ println(reportsDataString)
+ assert.NoError(t, err, "No error expected but got one")
+ linksFileData, err := ioutil.ReadFile(linksJSONPath)
+ linksDataString := string(linksFileData)
+ println(linksDataString)
+ assert.NoError(t, err, "No error expected but got one")
+ json.Unmarshal(reportsFileData, &reportsLoaded)
+ json.Unmarshal(linksFileData, &linksLoaded)
+
+ assert.Equal(t, 2, len(reportsLoaded), "wrong number of reports")
+ assert.Equal(t, 1, len(linksLoaded), "wrong number of links")
+ assert.Equal(t, true, reportsLoaded[0].Mandatory, "mandatory flag on report 1 has wrong value")
+ assert.Equal(t, "testFile1.json", reportsLoaded[0].Target, "target value on report 1 has wrong value")
+ assert.Equal(t, false, reportsLoaded[1].Mandatory, "mandatory flag on report 2 has wrong value")
+ assert.Equal(t, "testFile2.json", reportsLoaded[1].Target, "target value on report 1 has wrong value")
+ assert.Equal(t, false, linksLoaded[0].Mandatory, "mandatory flag on link 1 has wrong value")
+ assert.Equal(t, "https://1234568.com/test", linksLoaded[0].Target, "target value on link 1 has wrong value")
+ assert.Equal(t, "Weblink", linksLoaded[0].Name, "name value on link 1 has wrong value")
+}
diff --git a/resources/metadata/checkmarx.yaml b/resources/metadata/checkmarx.yaml
new file mode 100644
index 000000000..e87074558
--- /dev/null
+++ b/resources/metadata/checkmarx.yaml
@@ -0,0 +1,262 @@
+metadata:
+ name: checkmarxExecuteScan
+ description: Checkmarx is the recommended tool for security scans of JavaScript, iOS, Swift and Ruby code.
+ longDescription: |-
+ Checkmarx is a Static Application Security Testing (SAST) tool to analyze i.e. Java- or TypeScript, Swift, Golang, Ruby code,
+ and many other programming languages for security flaws based on a set of provided rules/queries that can be customized and extended.
+
+ This step by default enforces a specific audit baseline for findings and therefore ensures that:
+ * No 'To Verify' High and Medium issues exist in your project
+ * Total number of High and Medium 'Confirmed' or 'Urgent' issues is zero
+ * 10% of all Low issues are 'Confirmed' or 'Not Exploitable'
+
+ You can adapt above thresholds specifically using the provided configuration parameters and i.e. check for `absolute`
+ thresholds instead of `percentage` whereas we strongly recommend you to stay with the defaults provided.
+spec:
+ inputs:
+ secrets:
+ - name: checkmarxCredentialsId
+ description: The technical user/password credential used to communicate with the Checkmarx backend
+ type: jenkins
+ params:
+ - name: avoidDuplicateProjectScans
+ type: bool
+ description: Whether duplicate scans of the same project state shall be avoided or not
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: false
+ - name: filterPattern
+ type: string
+ description: The filter pattern used to zip the files relevant for scanning, patterns can be negated by setting an exclamation mark in front i.e. `!test/*.js` would avoid adding any javascript files located in the test directory
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: '!**/node_modules/**, !**/.xmake/**, !**/*_test.go, !**/vendor/**/*.go,
+ **/*.html, **/*.xml, **/*.go, **/*.py, **/*.js, **/*.scala, **/*.ts'
+ - name: fullScanCycle
+ type: string
+ description: Indicates how often a full scan should happen between the incremental scans when activated
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: 5
+ - name: fullScansScheduled
+ type: bool
+ description: Whether full scans are to be scheduled or not. Should be used in relation with `incremental` and `fullScanCycle`
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: true
+ - name: generatePdfReport
+ type: bool
+ description: Whether to generate a PDF report of the analysis results or not
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: true
+ - name: incremental
+ type: bool
+ description: Whether incremental scans are to be applied which optimizes the scan time but might reduce detection capabilities. Therefore full scans are still required from time to time and should be scheduled via `fullScansScheduled` and `fullScanCycle`
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: true
+ - name: password
+ type: string
+ description: The password to authenticate
+ mandatory: true
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: preset
+ type: string
+ description: The preset to use for scanning, if not set explicitly the step will attempt to look up the project's setting based on the availability of `checkmarxCredentialsId`
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: null
+ - name: projectName
+ aliases:
+ - name: checkmarxProject
+ type: string
+ description: The name of the Checkmarx project to scan into
+ mandatory: true
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: pullRequestName
+ type: string
+ description: Used to supply the name for the newly created PR project branch when being used in pull request scenarios
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: serverUrl
+ aliases:
+ - name: checkmarxServerUrl
+ type: string
+ description: The URL pointing to the root of the Checkmarx server to be used
+ mandatory: true
+ scope:
+ - GENERAL
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: sourceEncoding
+ type: string
+ description: The source encoding to be used, if not set explicitly the project's default will be used
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: '1'
+ - name: teamId
+ aliases:
+ - name: checkmarxGroupId
+ type: string
+ description: The group ID related to your team which can be obtained via the Pipeline Syntax plugin as described in the `Details` section
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: teamName
+ type: string
+ description: The full name of the team to assign newly created projects to which is preferred to teamId
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: username
+ type: string
+ description: The username to authenticate
+ mandatory: true
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ - name: vulnerabilityThresholdEnabled
+ type: bool
+ description: Whether the thresholds are enabled or not. If enabled the build will be set to `vulnerabilityThresholdResult` in case a specific threshold value is exceeded
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: true
+ - name: vulnerabilityThresholdHigh
+ type: int
+ description: The specific threshold for high severity findings
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: 100
+ - name: vulnerabilityThresholdLow
+ type: int
+ description: The specific threshold for low severity findings
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: 10
+ - name: vulnerabilityThresholdMedium
+ type: int
+ description: The specific threshold for medium severity findings
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: 100
+ - name: vulnerabilityThresholdResult
+ type: string
+ description: The result of the build in case thresholds are enabled and exceeded
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: FAILURE
+ - name: vulnerabilityThresholdUnit
+ type: string
+ description: The unit for the threshold to apply.
+ mandatory: false
+ scope:
+ - PARAMETERS
+ - STAGES
+ - STEPS
+ default: percentage
+ outputs:
+ resources:
+ - name: influx
+ type: influx
+ params:
+ - name: checkmarx_data
+ fields:
+ - name: high_issues
+ - name: high_not_false_postive
+ - name: high_not_exploitable
+ - name: high_confirmed
+ - name: high_urgent
+ - name: high_proposed_not_exploitable
+ - name: high_to_verify
+ - name: medium_issues
+ - name: medium_not_false_postive
+ - name: medium_not_exploitable
+ - name: medium_confirmed
+ - name: medium_urgent
+ - name: medium_proposed_not_exploitable
+ - name: medium_to_verify
+ - name: low_issues
+ - name: low_not_false_postive
+ - name: low_not_exploitable
+ - name: low_confirmed
+ - name: low_urgent
+ - name: low_proposed_not_exploitable
+ - name: low_to_verify
+ - name: information_issues
+ - name: information_not_false_postive
+ - name: information_not_exploitable
+ - name: information_confirmed
+ - name: information_urgent
+ - name: information_proposed_not_exploitable
+ - name: information_to_verify
+ - name: initiator_name
+ - name: owner
+ - name: scan_id
+ - name: project_id
+ - name: project_name
+ - name: team
+ - name: team_full_path_on_report_date
+ - name: scan_start
+ - name: scan_time
+ - name: lines_of_code_scanned
+ - name: files_scanned
+ - name: checkmarx_version
+ - name: scan_type
+ - name: preset
+ - name: deep_link
+ - name: report_creation_time
diff --git a/src/com/sap/piper/JenkinsUtils.groovy b/src/com/sap/piper/JenkinsUtils.groovy
index dadbff790..9c963b27a 100644
--- a/src/com/sap/piper/JenkinsUtils.groovy
+++ b/src/com/sap/piper/JenkinsUtils.groovy
@@ -1,7 +1,7 @@
package com.sap.piper
import com.cloudbees.groovy.cps.NonCPS
-
+import hudson.Functions
import hudson.tasks.junit.TestResultAction
import jenkins.model.Jenkins
@@ -128,6 +128,22 @@ def getLibrariesInfo() {
return libraries
}
+@NonCPS
+void addRunSideBarLink(String relativeUrl, String displayName, String relativeIconPath) {
+ try {
+ def linkActionClass = this.class.classLoader.loadClass("hudson.plugins.sidebar_link.LinkAction")
+ if (relativeUrl != null && displayName != null) {
+ def run = getRawBuild()
+ def iconPath = (null != relativeIconPath) ? "${Functions.getResourcePath()}/${relativeIconPath}" : null
+ def action = linkActionClass.newInstance(relativeUrl, displayName, iconPath)
+ echo "Added run level sidebar link to '${action.getUrlName()}' with name '${action.getDisplayName()}' and icon '${action.getIconFileName()}'"
+ run.getActions().add(action)
+ }
+ } catch (e) {
+ e.printStackTrace()
+ }
+}
+
def getInstance() {
Jenkins.get()
}
diff --git a/test/groovy/CheckmarxExecuteScanTest.groovy b/test/groovy/CheckmarxExecuteScanTest.groovy
new file mode 100644
index 000000000..d5bee9d14
--- /dev/null
+++ b/test/groovy/CheckmarxExecuteScanTest.groovy
@@ -0,0 +1,66 @@
+import groovy.json.JsonSlurper
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import util.*
+
+import static org.hamcrest.Matchers.*
+import static org.junit.Assert.assertThat
+
+class CheckmarxExecuteScanTest extends BasePiperTest {
+
+ private JenkinsCredentialsRule credentialsRule = new JenkinsCredentialsRule(this)
+ private JenkinsReadJsonRule readJsonRule = new JenkinsReadJsonRule(this)
+ private JenkinsShellCallRule shellCallRule = new JenkinsShellCallRule(this)
+ private JenkinsStepRule stepRule = new JenkinsStepRule(this)
+ private JenkinsWriteFileRule writeFileRule = new JenkinsWriteFileRule(this)
+ private JenkinsFileExistsRule fileExistsRule = new JenkinsFileExistsRule(this, [])
+
+ private List withEnvArgs = []
+
+ @Rule
+ public RuleChain rules = Rules
+ .getCommonRules(this)
+ .around(new JenkinsReadYamlRule(this))
+ .around(credentialsRule)
+ .around(readJsonRule)
+ .around(shellCallRule)
+ .around(stepRule)
+ .around(writeFileRule)
+ .around(fileExistsRule)
+
+ @Before
+ void init() {
+ helper.registerAllowedMethod("readJSON", [Map], { m ->
+ if(m.file == 'reports.json')
+ return [[target: "1234.pdf", mandatory: true]]
+ if(m.file == 'links.json')
+ return []
+ if(m.text != null)
+ return new JsonSlurper().parseText(m.text)
+ })
+ helper.registerAllowedMethod("withEnv", [List.class, Closure.class], {arguments, closure ->
+ arguments.each {arg ->
+ withEnvArgs.add(arg.toString())
+ }
+ return closure()
+ })
+ credentialsRule.withCredentials('idOfCxCredential', "PIPER_username", "PIPER_password")
+ shellCallRule.setReturnValue('./piper getConfig --contextConfig --stepMetadata \'metadata/checkmarx.yaml\'', '{"checkmarxCredentialsId": "idOfCxCredential", "verbose": false}')
+ }
+
+ @Test
+ void testCheckmarxExecuteScanDefault() {
+ stepRule.step.checkmarxExecuteScan(
+ juStabUtils: utils,
+ jenkinsUtilsStub: jenkinsUtils,
+ testParam: "This is test content",
+ script: nullScript
+ )
+ // asserts
+ assertThat(writeFileRule.files['metadata/checkmarx.yaml'], containsString('name: checkmarxExecuteScan'))
+ assertThat(withEnvArgs[0], allOf(startsWith('PIPER_parametersJSON'), containsString('"testParam":"This is test content"')))
+ assertThat(shellCallRule.shell[1], is('./piper checkmarxExecuteScan'))
+ }
+}
diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy
index 81f1af7a8..e538bdd2d 100644
--- a/test/groovy/CommonStepsTest.groovy
+++ b/test/groovy/CommonStepsTest.groovy
@@ -115,6 +115,7 @@ public class CommonStepsTest extends BasePiperTest{
'handlePipelineStepErrors', // special step (infrastructure)
'piperStageWrapper', //intended to be called from within stages
'buildSetResult',
+ 'checkmarxExecuteScan', //implementing new golang pattern without fields
'githubPublishRelease', //implementing new golang pattern without fields
'kubernetesDeploy', //implementing new golang pattern without fields
'xsDeploy', //implementing new golang pattern without fields
diff --git a/vars/checkmarxExecuteScan.groovy b/vars/checkmarxExecuteScan.groovy
new file mode 100644
index 000000000..3ecc8921f
--- /dev/null
+++ b/vars/checkmarxExecuteScan.groovy
@@ -0,0 +1,58 @@
+import com.sap.piper.PiperGoUtils
+import com.sap.piper.Utils
+import com.sap.piper.JenkinsUtils
+import groovy.transform.Field
+
+import static com.sap.piper.Prerequisites.checkScript
+
+@Field String STEP_NAME = getClass().getName()
+@Field String METADATA_FILE = 'metadata/checkmarx.yaml'
+
+//Metadata maintained in file project://resources/metadata/checkmarx.yaml
+
+void call(Map parameters = [:]) {
+ handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) {
+
+ def script = checkScript(this, parameters) ?: this
+
+ Map config
+ def utils = parameters.juStabUtils ?: new Utils()
+ parameters.juStabUtils = null
+ def jenkinsUtils = parameters.jenkinsUtilsStub ?: new JenkinsUtils()
+ parameters.jenkinsUtilsStub = null
+
+ new PiperGoUtils(this, utils).unstashPiperBin()
+ utils.unstash('pipelineConfigAndTests')
+ script.commonPipelineEnvironment.writeToDisk(script)
+
+ writeFile(file: METADATA_FILE, text: libraryResource(METADATA_FILE))
+
+ withEnv([
+ "PIPER_parametersJSON=${groovy.json.JsonOutput.toJson(parameters)}",
+ ]) {
+ // get context configuration
+ config = readJSON (text: sh(returnStdout: true, script: "./piper getConfig --contextConfig --stepMetadata '${METADATA_FILE}'"))
+
+ // execute step
+ withCredentials([usernamePassword(
+ credentialsId: config.checkmarxCredentialsId,
+ passwordVariable: 'PIPER_password',
+ usernameVariable: 'PIPER_username'
+ )]) {
+ sh "./piper checkmarxExecuteScan"
+ }
+
+ def reports = readJSON (file: 'reports.json')
+ for (report in reports) {
+ archiveArtifacts artifacts: report['target'], allowEmptyArchive: !report['mandatory']
+ }
+
+ if (fileExists(file: 'links.json')) {
+ def links = readJSON(file: 'links.json')
+ for (link in links) {
+ jenkinsUtils.addRunSideBarLink(link['target'], link['name'], "images/24x24/graph.png")
+ }
+ }
+ }
+ }
+}