From 5b4302ff326a8b4321793485bcf1d10e122f5e1b Mon Sep 17 00:00:00 2001 From: Stefan Pirtoaca Date: Tue, 8 Apr 2025 14:44:20 +0300 Subject: [PATCH] Implement `onapsisExecuteScan` with Onapsis Control --- cmd/metadata_generated.go | 1 + cmd/onapsisExecuteScan.go | 400 +++++++++++++++++++++ cmd/onapsisExecuteScan_generated.go | 278 ++++++++++++++ cmd/onapsisExecuteScan_generated_test.go | 20 ++ cmd/onapsisExecuteScan_test.go | 54 +++ cmd/piper.go | 1 + resources/metadata/onapsisExecuteScan.yaml | 99 +++++ vars/onapsisExecuteScan.groovy | 21 ++ 8 files changed, 874 insertions(+) create mode 100644 cmd/onapsisExecuteScan.go create mode 100644 cmd/onapsisExecuteScan_generated.go create mode 100644 cmd/onapsisExecuteScan_generated_test.go create mode 100644 cmd/onapsisExecuteScan_test.go create mode 100644 resources/metadata/onapsisExecuteScan.yaml create mode 100644 vars/onapsisExecuteScan.groovy diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index 50b0d6dadb..b051fe5778 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -104,6 +104,7 @@ func GetAllStepMetadata() map[string]config.StepData { "npmExecuteLint": npmExecuteLintMetadata(), "npmExecuteScripts": npmExecuteScriptsMetadata(), "npmExecuteTests": npmExecuteTestsMetadata(), + "onapsisExecuteScan": onapsisExecuteScanMetadata(), "pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(), "protecodeExecuteScan": protecodeExecuteScanMetadata(), "pythonBuild": pythonBuildMetadata(), diff --git a/cmd/onapsisExecuteScan.go b/cmd/onapsisExecuteScan.go new file mode 100644 index 0000000000..762bf01906 --- /dev/null +++ b/cmd/onapsisExecuteScan.go @@ -0,0 +1,400 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + piperHttp "github.com/SAP/jenkins-library/pkg/http" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/pkg/errors" + + "github.com/SAP/jenkins-library/pkg/piperutils" +) + +type ScanServer struct { + serverUrl string + client piperHttp.Sender +} + +type ScanProjectResponse struct { + Success bool `json:"success"` + Result struct { + JobID string `json:"job_id"` // present only on success + ResultCode int `json:"result_code"` // present only on failure + Timestamp string `json:"timestamp"` // present only on success + Messages []Message `json:"messages"` + } `json:"result"` +} + +type GetScanJobStatusResponse struct { + Success bool `json:"success"` + Result struct { + JobID string `json:"job_id"` + ReqRecvTime string `json:"req_recv_time"` + ScanStartTime string `json:"scan_start_time"` + ScanEndTime string `json:"scan_end_time"` + EngineType string `json:"engine_type"` + Status string `json:"status"` + Progress int `json:"progress"` + Messages []Message `json:"messages"` + Details struct { + Children []string `json:"children"` + } `json:"details"` + } `json:"result"` +} + +type GetJobResultMetricsResponse struct { + Success bool `json:"success"` + Result struct { + Metrics []struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"metrics"` + } `json:"result"` +} + +type Message struct { + Sequence int `json:"sequence"` + Timestamp string `json:"timestamp"` + Level string `json:"level"` + MessageID string `json:"message_id"` + Param1 string `json:"param1"` + Param2 string `json:"param2"` + Param3 string `json:"param3"` + Param4 string `json:"param4"` +} + +var debugMode bool = false + +func onapsisExecuteScan(config onapsisExecuteScanOptions, telemetryData *telemetry.CustomData) { + + debugMode = config.DebugMode + if debugMode { + log.SetVerbose(true) + } + + err := setupOnapsisCertificate(config.OnapsisCertificatePath) + if err != nil { + log.Entry().WithError(err).Fatal("Could not set up Onapsis Certificate") + } + + // Error situations should be bubbled up until they reach the line below which will then stop execution + // through the log.Entry().Fatal() call leading to an os.Exit(1) in the end. + err = runOnapsisExecuteScan(config, telemetryData) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } + + err = cleanupOnapsisCertificate() + if err != nil { + log.Entry().WithError(err).Info("Could not delete temporary Onapsis certificate") + } +} + +func runOnapsisExecuteScan(config onapsisExecuteScanOptions, telemetryData *telemetry.CustomData) error { + // Create a new ScanServer + log.Entry().Info("Creating scan server...") + server, err := NewScanServer(config) + if err != nil { + return errors.Wrap(err, "failed to create scan server") + } + + // Call the ScanProject method + log.Entry().Info("Scanning project...") + startScanResponse, err := server.ScanProject(config, telemetryData) + if err != nil { + return errors.Wrap(err, "Failed to scan project") + } + + // Monitor Job Status + jobID := startScanResponse.Result.JobID + log.Entry().Infof("Monitoring job %s status...", jobID) + jobStatusResponse, err := server.MonitorJobStatus(jobID) + if err != nil { + return errors.Wrap(err, "Failed to scan project") + } + + // Get Job Reports + log.Entry().Info("Getting job reports...") + err = server.GetJobReports(jobID, "onapsis_scan_report.zip") + if err != nil { + return errors.Wrap(err, "Failed to get job reports") + } + + // Get Job Result Metrics + log.Entry().Info("Getting job result metrics...") + metrics, err := server.GetJobResultMetrics(jobID) + if err != nil { + return errors.Wrap(err, "Failed to get job result metrics") + } + + // Analyze metrics + loc, numMandatory, numOptional, totalTime := extractMetrics(metrics) + log.Entry().Infof("Job Metrics - Lines of Code Scanned: %s, Mandatory Findings: %s, Optional Findings: %s, Total Time: %sms", loc, numMandatory, numOptional, totalTime) + + log.Entry().Infof("The findings can be viewed here: %s/ui/#/admin/scans/%s/%s/findings", config.ScanServiceURL, jobID, jobStatusResponse.Result.Details.Children[0]) + + if config.FailOnMandatoryFinding && numMandatory != "0" { + return errors.Errorf("Scan failed with %s mandatory findings", numMandatory) + } else if config.FailOnOptionalFinding && numOptional != "0" { + return errors.Errorf("Scan failed with %s optional findings", numOptional) + } + + return nil +} + +func NewScanServer(config onapsisExecuteScanOptions) (*ScanServer, error) { + + scanServiceUrl := config.ScanServiceURL + + options := getHttpOptionsWithJwt(config.OnapsisSecretToken, "Bearer", tempCertificatePath) + client := &piperHttp.Client{} + client.SetOptions(options) + + server := &ScanServer{serverUrl: scanServiceUrl, client: client} + + return server, nil +} + +// Obtain http.ClientOptions with JWT and tokenType "Bearer". caCert is the self-signed scan server certificate. +func getHttpOptionsWithJwt(jwt string, tokenType string, caCert string) piperHttp.ClientOptions { + // Set authorization token for client + return piperHttp.ClientOptions{ + Token: fmt.Sprintf("%s %s", tokenType, jwt), + MaxRequestDuration: 60 * time.Second, + DoLogRequestBodyOnDebug: debugMode, + DoLogResponseBodyOnDebug: debugMode, + TrustedCerts: []string{caCert}, + } +} + +const tempCertificatePath = piperHttp.TrustStoreDirectory + "/onapsisCert.pem" + +func setupOnapsisCertificate(certificatePath string) error { + fileUtils := piperutils.Files{} + + if exists, _ := fileUtils.FileExists(certificatePath); !exists { + return errors.Errorf("Onapsis certificate was not found at path: %s", certificatePath) + } + + err := fileUtils.MkdirAll(piperHttp.TrustStoreDirectory, 0755) + if err != nil { + return errors.Wrapf(err, "Could not create dir: %s", piperHttp.TrustStoreDirectory) + } + _, err = fileUtils.Copy(certificatePath, tempCertificatePath) + if err != nil { + return errors.Wrap(err, "Could not copy Onapsis Certificate.") + } + + err = fileUtils.Chmod(tempCertificatePath, 0600) + if err != nil { + return errors.Wrap(err, "Could not set write permissions Onapsis Certificate.") + } + + return nil +} + +func cleanupOnapsisCertificate() error { + fileUtils := piperutils.Files{} + + if exists, _ := fileUtils.FileExists(tempCertificatePath); !exists { + log.Entry().Infof("Onapsis certificate was not found at path: %s", tempCertificatePath) + return nil + } + + return fileUtils.FileRemove(tempCertificatePath) +} + +func (srv *ScanServer) ScanProject(config onapsisExecuteScanOptions, telemetryData *telemetry.CustomData) (ScanProjectResponse, error) { + + jobName, jobNameIsPresent := os.LookupEnv("JOB_BASE_NAME") + if !jobNameIsPresent { + jobName = "piper-ci-cd-scan" + } + + jobDescription := fmt.Sprintf("Job triggered by CI/CD pipeline on git repo: %s, branch: %s", config.ScanGitURL, config.ScanGitBranch) + + // Create request data + log.Entry().Info("Creating request data...") + scanRequest := fmt.Sprintf(`{ + "engine_type": "GIT", + "scan_information": { + "name": "%s", + "description": "%s" + }, + "asset": { + "type": "GITURL", + "url": "%s" + }, + "configuration": { + "origin": "PIPER" + }, + "scan_scope": { + "languages": [ + "%s" + ], + "branch_name": "%s", + "exclude_packages": [] + } + }`, jobName, jobDescription, config.ScanGitURL, config.AppType, config.ScanGitBranch) + + scanRequestReader := strings.NewReader(scanRequest) + scanRequestHeader := http.Header{ + "Content-Type": {"application/json"}, + } + + // Send request + log.Entry().Info("Sending scan request...") + response, err := srv.client.SendRequest("POST", srv.serverUrl+"/cca/v1.2/scan", scanRequestReader, scanRequestHeader, nil) + if err != nil { + return ScanProjectResponse{}, errors.Wrap(err, "Failed to start scan") + } + + // Handle response + var responseData ScanProjectResponse + err = handleResponse(response, &responseData) + if err != nil { + return responseData, errors.Wrap(err, "Failed to parse response") + } + + return responseData, nil +} + +func (srv *ScanServer) GetScanJobStatus(jobID string) (*GetScanJobStatusResponse, error) { + // Send request + response, err := srv.client.SendRequest("GET", srv.serverUrl+"/cca/v1.2/jobs/"+jobID, nil, nil, nil) + if err != nil { + return &GetScanJobStatusResponse{}, errors.Wrap(err, "failed to send request") + } + + var responseData GetScanJobStatusResponse + err = handleResponse(response, &responseData) + if err != nil { + return &responseData, errors.Wrap(err, "Failed to parse response") + } + + return &responseData, nil +} + +func (srv *ScanServer) MonitorJobStatus(jobID string) (*GetScanJobStatusResponse, error) { + // Polling interval + interval := time.Second * 10 // Check every 10 seconds + for { + // Get the job status + response, err := srv.GetScanJobStatus(jobID) + if err != nil { + return &GetScanJobStatusResponse{}, errors.Wrap(err, "Failed to get scan job status") + } + + // Log job progress + log.Entry().Infof("Job %s progress: %d%%", jobID, response.Result.Progress) + + // Check if the job is complete + if response.Result.Status == "SUCCESS" { + log.Entry().Infof("Job %s progress: %d%%. Status: %s", jobID, response.Result.Progress, response.Result.Status) + return response, nil + } else if response.Result.Status == "FAILURE" { + return &GetScanJobStatusResponse{}, errors.Errorf("Job %s failed with status: %s", jobID, response.Result.Status) + } + + // Wait before checking again + time.Sleep(interval) + } +} + +func (srv *ScanServer) GetJobReports(jobID string, reportArchiveName string) error { + response, err := srv.client.SendRequest("GET", srv.serverUrl+"/cca/v1.2/jobs/"+jobID+"/result?format=ZIP", nil, nil, nil) + if err != nil { + return errors.Wrap(err, "Failed to retrieve job report") + } + + // Create the destination zip file + outFile, err := os.Create(reportArchiveName) + if err != nil { + return errors.Wrap(err, "Failed to create report archive") + } + defer outFile.Close() + + // Copy the response body to the file + log.Entry().Info("Writing report file...") + _, err = io.Copy(outFile, response.Body) + if err != nil { + return errors.Wrap(err, "Failed to write report archive") + } + + log.Entry().Info("Report written.") + + return nil +} + +func (srv *ScanServer) GetJobResultMetrics(jobID string) (GetJobResultMetricsResponse, error) { + // Send request + response, err := srv.client.SendRequest("GET", srv.serverUrl+"/cca/v1.2/jobs/"+jobID+"/result/metrics", nil, nil, nil) + if err != nil { + return GetJobResultMetricsResponse{}, errors.Wrap(err, "failed to send request") + } + + var responseData GetJobResultMetricsResponse + err = handleResponse(response, &responseData) + if err != nil { + return responseData, errors.Wrap(err, "Failed to parse response") + } + + return responseData, nil +} + +func extractMetrics(response GetJobResultMetricsResponse) (loc, numMandatory, numOptional, totalTime string) { + for _, metric := range response.Result.Metrics { + switch metric.Name { + case "LOC": + loc = metric.Value + case "num_mandatory": + numMandatory = metric.Value + case "num_optional": + numOptional = metric.Value + case "total_time_used": + totalTime = metric.Value + } + + } + + return loc, numMandatory, numOptional, totalTime +} + +func handleResponse(response *http.Response, responseData interface{}) error { + err := piperHttp.ParseHTTPResponseBodyJSON(response, &responseData) + if err != nil { + return errors.Wrap(err, "Failed to parse file") + } + + // Define a helper function to check success and handle error messages + checkResponse := func(success bool, messages interface{}, resultCode int) error { + if success { + return nil + } + messageJSON, err := json.MarshalIndent(messages, "", " ") + if err != nil { + return errors.Wrap(err, "Failed to marshal Messages") + } + return errors.Errorf("Request failed with result_code: %d, messages: %v", resultCode, string(messageJSON)) + } + + // Use type switch to handle different response types + log.Entry().Debugf("responseData type: %T", responseData) // Log type using %T + switch data := responseData.(type) { + case *ScanProjectResponse: + return checkResponse(data.Success, data.Result.Messages, data.Result.ResultCode) + case *GetScanJobStatusResponse: + return checkResponse(data.Success, data.Result.Messages, 0) + case *GetJobResultMetricsResponse: + return checkResponse(data.Success, data.Result.Metrics, 0) + default: + return errors.New("Unknown response type") + } +} diff --git a/cmd/onapsisExecuteScan_generated.go b/cmd/onapsisExecuteScan_generated.go new file mode 100644 index 0000000000..fdc8038095 --- /dev/null +++ b/cmd/onapsisExecuteScan_generated.go @@ -0,0 +1,278 @@ +// Code generated by piper's step-generator. DO NOT EDIT. + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/gcp" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/splunk" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/SAP/jenkins-library/pkg/validation" + "github.com/spf13/cobra" +) + +type onapsisExecuteScanOptions struct { + ScanServiceURL string `json:"scanServiceUrl,omitempty"` + ScanGitURL string `json:"scanGitUrl,omitempty"` + ScanGitBranch string `json:"scanGitBranch,omitempty"` + AppType string `json:"appType,omitempty" validate:"possible-values=ABAP SAPUI5"` + FailOnMandatoryFinding bool `json:"failOnMandatoryFinding,omitempty"` + FailOnOptionalFinding bool `json:"failOnOptionalFinding,omitempty"` + DebugMode bool `json:"debugMode,omitempty"` + OnapsisSecretToken string `json:"onapsisSecretToken,omitempty"` + OnapsisCertificatePath string `json:"onapsisCertificatePath,omitempty"` +} + +// OnapsisExecuteScanCommand Execute a scan with Onapsis Control +func OnapsisExecuteScanCommand() *cobra.Command { + const STEP_NAME = "onapsisExecuteScan" + + metadata := onapsisExecuteScanMetadata() + var stepConfig onapsisExecuteScanOptions + var startTime time.Time + var logCollector *log.CollectorHook + var splunkClient *splunk.Splunk + telemetryClient := &telemetry.Telemetry{} + + var createOnapsisExecuteScanCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Execute a scan with Onapsis Control", + Long: `This step executes a scan with Onapsis Control.`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + startTime = time.Now() + log.SetStepName(STEP_NAME) + log.SetVerbose(GeneralConfig.Verbose) + + GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens) + + path, _ := os.Getwd() + fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} + log.RegisterHook(fatalHook) + + err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + log.RegisterSecret(stepConfig.OnapsisSecretToken) + log.RegisterSecret(stepConfig.OnapsisCertificatePath) + + if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { + sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) + log.RegisterHook(&sentryHook) + } + + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 || len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 { + splunkClient = &splunk.Splunk{} + logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID} + log.RegisterHook(logCollector) + } + + if err = log.RegisterANSHookIfConfigured(GeneralConfig.CorrelationID); err != nil { + log.Entry().WithError(err).Warn("failed to set up SAP Alert Notification Service log hook") + } + + validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages()) + if err != nil { + return err + } + if err = validation.ValidateStruct(stepConfig); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + + return nil + }, + Run: func(_ *cobra.Command, _ []string) { + vaultClient := config.GlobalVaultClient() + if vaultClient != nil { + defer vaultClient.MustRevokeToken() + } + + stepTelemetryData := telemetry.CustomData{} + stepTelemetryData.ErrorCode = "1" + handler := func() { + config.RemoveVaultSecretFiles() + stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + stepTelemetryData.ErrorCategory = log.GetErrorCategory().String() + stepTelemetryData.PiperCommitHash = GitCommit + telemetryClient.SetData(&stepTelemetryData) + telemetryClient.Send() + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { + splunkClient.Initialize(GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.SplunkConfig.Dsn, + GeneralConfig.HookConfig.SplunkConfig.Token, + GeneralConfig.HookConfig.SplunkConfig.Index, + GeneralConfig.HookConfig.SplunkConfig.SendLogs) + splunkClient.Send(telemetryClient.GetData(), logCollector) + } + if len(GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint) > 0 { + splunkClient.Initialize(GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblEndpoint, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblToken, + GeneralConfig.HookConfig.SplunkConfig.ProdCriblIndex, + GeneralConfig.HookConfig.SplunkConfig.SendLogs) + splunkClient.Send(telemetryClient.GetData(), logCollector) + } + if GeneralConfig.HookConfig.GCPPubSubConfig.Enabled { + err := gcp.NewGcpPubsubClient( + vaultClient, + GeneralConfig.HookConfig.GCPPubSubConfig.ProjectNumber, + GeneralConfig.HookConfig.GCPPubSubConfig.IdentityPool, + GeneralConfig.HookConfig.GCPPubSubConfig.IdentityProvider, + GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.OIDCConfig.RoleID, + ).Publish(GeneralConfig.HookConfig.GCPPubSubConfig.Topic, telemetryClient.GetDataBytes()) + if err != nil { + log.Entry().WithError(err).Warn("event publish failed") + } + } + } + log.DeferExitHandler(handler) + defer handler() + telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME, GeneralConfig.HookConfig.PendoConfig.Token) + onapsisExecuteScan(stepConfig, &stepTelemetryData) + stepTelemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addOnapsisExecuteScanFlags(createOnapsisExecuteScanCmd, &stepConfig) + return createOnapsisExecuteScanCmd +} + +func addOnapsisExecuteScanFlags(cmd *cobra.Command, stepConfig *onapsisExecuteScanOptions) { + cmd.Flags().StringVar(&stepConfig.ScanServiceURL, "scanServiceUrl", os.Getenv("PIPER_scanServiceUrl"), "URL of the scan service") + cmd.Flags().StringVar(&stepConfig.ScanGitURL, "scanGitUrl", os.Getenv("PIPER_scanGitUrl"), "The target git repo to scan") + cmd.Flags().StringVar(&stepConfig.ScanGitBranch, "scanGitBranch", os.Getenv("PIPER_scanGitBranch"), "The target git branch to scan") + cmd.Flags().StringVar(&stepConfig.AppType, "appType", `SAPUI5`, "Type of the application to be scanned") + cmd.Flags().BoolVar(&stepConfig.FailOnMandatoryFinding, "failOnMandatoryFinding", true, "Fail the build if mandatory findings are detected") + cmd.Flags().BoolVar(&stepConfig.FailOnOptionalFinding, "failOnOptionalFinding", false, "Fail the build if optional findings are detected") + cmd.Flags().BoolVar(&stepConfig.DebugMode, "debugMode", false, "Enable debug mode for the scan") + cmd.Flags().StringVar(&stepConfig.OnapsisSecretToken, "onapsisSecretToken", os.Getenv("PIPER_onapsisSecretToken"), "Onapsis JWT, used to authenticate with the Onapsis scan service") + cmd.Flags().StringVar(&stepConfig.OnapsisCertificatePath, "onapsisCertificatePath", os.Getenv("PIPER_onapsisCertificatePath"), "The path to the Onapsis scan server certificate") + + cmd.MarkFlagRequired("scanServiceUrl") + cmd.MarkFlagRequired("scanGitUrl") + cmd.MarkFlagRequired("scanGitBranch") + cmd.MarkFlagRequired("onapsisSecretToken") + cmd.MarkFlagRequired("onapsisCertificatePath") +} + +// retrieve step metadata +func onapsisExecuteScanMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "onapsisExecuteScan", + Aliases: []config.Alias{}, + Description: "Execute a scan with Onapsis Control", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Secrets: []config.StepSecrets{ + {Name: "onapsisSecretTokenId", Description: "Jenkins 'Secret text' for the Onapsis JWT, used to authenticate with the Onapsis scan service", Type: "jenkins"}, + {Name: "onapsisCertificate", Description: "Jenkins 'Secret file' self-signed certificate of the Onapsis scan service", Type: "jenkins"}, + }, + Parameters: []config.StepParameters{ + { + Name: "scanServiceUrl", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_scanServiceUrl"), + }, + { + Name: "scanGitUrl", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_scanGitUrl"), + }, + { + Name: "scanGitBranch", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_scanGitBranch"), + }, + { + Name: "appType", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `SAPUI5`, + }, + { + Name: "failOnMandatoryFinding", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: true, + }, + { + Name: "failOnOptionalFinding", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: false, + }, + { + Name: "debugMode", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: false, + }, + { + Name: "onapsisSecretToken", + ResourceRef: []config.ResourceReference{ + { + Name: "onapsisSecretTokenId", + Type: "secret", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_onapsisSecretToken"), + }, + { + Name: "onapsisCertificatePath", + ResourceRef: []config.ResourceReference{ + { + Name: "onapsisCertificate", + Type: "secret", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_onapsisCertificatePath"), + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/onapsisExecuteScan_generated_test.go b/cmd/onapsisExecuteScan_generated_test.go new file mode 100644 index 0000000000..d7471fe57c --- /dev/null +++ b/cmd/onapsisExecuteScan_generated_test.go @@ -0,0 +1,20 @@ +//go:build unit +// +build unit + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOnapsisExecuteScanCommand(t *testing.T) { + t.Parallel() + + testCmd := OnapsisExecuteScanCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "onapsisExecuteScan", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/onapsisExecuteScan_test.go b/cmd/onapsisExecuteScan_test.go new file mode 100644 index 0000000000..8cc186134f --- /dev/null +++ b/cmd/onapsisExecuteScan_test.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "testing" + + "github.com/SAP/jenkins-library/pkg/mock" + "github.com/stretchr/testify/assert" +) + +type onapsisExecuteScanMockUtils struct { + *mock.ExecMockRunner + *mock.FilesMock +} + +func newOnapsisExecuteScanTestsUtils() onapsisExecuteScanMockUtils { + utils := onapsisExecuteScanMockUtils{ + ExecMockRunner: &mock.ExecMockRunner{}, + FilesMock: &mock.FilesMock{}, + } + return utils +} + +func TestRunOnapsisExecuteScan(t *testing.T) { + t.Parallel() + + t.Run("happy path", func(t *testing.T) { + t.Parallel() + // init + config := OnapsisExecuteScanOptions{} + + utils := newOnapsisExecuteScanTestsUtils() + utils.AddFile("file.txt", []byte("dummy content")) + + // test + err := runOnapsisExecuteScan(&config, nil, utils) + + // assert + assert.NoError(t, err) + }) + + t.Run("error path", func(t *testing.T) { + t.Parallel() + // init + config := OnapsisExecuteScanOptions{} + + utils := newOnapsisExecuteScanTestsUtils() + + // test + err := runOnapsisExecuteScan(&config, nil, utils) + + // assert + assert.EqualError(t, err, "cannot run without important file") + }) +} diff --git a/cmd/piper.go b/cmd/piper.go index 885e916737..3711bf113d 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -230,6 +230,7 @@ func Execute() { rootCmd.AddCommand(AscAppUploadCommand()) rootCmd.AddCommand(AbapLandscapePortalUpdateAddOnProductCommand()) rootCmd.AddCommand(ImagePushToRegistryCommand()) + rootCmd.AddCommand(OnapsisExecuteScanCommand()) addRootFlags(rootCmd) diff --git a/resources/metadata/onapsisExecuteScan.yaml b/resources/metadata/onapsisExecuteScan.yaml new file mode 100644 index 0000000000..8a9f17bcd7 --- /dev/null +++ b/resources/metadata/onapsisExecuteScan.yaml @@ -0,0 +1,99 @@ +metadata: + name: onapsisExecuteScan + description: Execute a scan with Onapsis Control + longDescription: This step executes a scan with Onapsis Control. +spec: + inputs: + secrets: + - name: onapsisSecretTokenId + type: jenkins + description: "Jenkins 'Secret text' for the Onapsis JWT, used to authenticate with the Onapsis scan service" + mandatory: true + - name: onapsisCertificate + type: jenkins + description: "Jenkins 'Secret file' self-signed certificate of the Onapsis scan service" + mandatory: true + params: + - name: scanServiceUrl + type: string + description: URL of the scan service + mandatory: true + scope: + - PARAMETERS + - STAGES + - STEPS + - name: scanGitUrl + type: string + description: "The target git repo to scan" + mandatory: true + scope: + - PARAMETERS + - STAGES + - STEPS + - name: scanGitBranch + type: string + description: "The target git branch to scan" + mandatory: true + scope: + - PARAMETERS + - STAGES + - STEPS + - name: appType + type: string + description: "Type of the application to be scanned" + default: "SAPUI5" + scope: + - PARAMETERS + - STAGES + - STEPS + possibleValues: + - "ABAP" + - "SAPUI5" + - name: failOnMandatoryFinding + type: bool + description: "Fail the build if mandatory findings are detected" + default: true + scope: + - PARAMETERS + - STAGES + - STEPS + - name: failOnOptionalFinding + type: bool + description: "Fail the build if optional findings are detected" + default: false + scope: + - PARAMETERS + - STAGES + - STEPS + - name: debugMode + type: bool + description: "Enable debug mode for the scan" + default: false + scope: + - PARAMETERS + - STAGES + - STEPS + - name: onapsisSecretToken + type: string + description: "Onapsis JWT, used to authenticate with the Onapsis scan service" + mandatory: true + secret: true + scope: + - PARAMETERS + - STAGES + - STEPS + resourceRef: + - name: onapsisSecretTokenId + type: secret + - name: onapsisCertificatePath + type: string + description: "The path to the Onapsis scan server certificate" + mandatory: true + secret: true + scope: + - PARAMETERS + - STAGES + - STEPS + resourceRef: + - name: onapsisCertificate + type: secret diff --git a/vars/onapsisExecuteScan.groovy b/vars/onapsisExecuteScan.groovy new file mode 100644 index 0000000000..6d2fcc278a --- /dev/null +++ b/vars/onapsisExecuteScan.groovy @@ -0,0 +1,21 @@ +import groovy.transform.Field + +@Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/onapsisExecuteScan.yaml' +@Field String ONAPSIS_REPORT_NAME = 'onapsis_scan_report.zip' + +def call(Map parameters = [:]) { + + List credentials = [ + [type: 'token', id: 'onapsisSecretTokenId', env: ['PIPER_onapsisSecretToken'], resolveCredentialsId: false], + [type: 'file', id: 'onapsisCertificate', env: ['PIPER_onapsisCertificatePath'], resolveCredentialsId: false] + ] + + try { + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) + } finally { + if (fileExists(ONAPSIS_REPORT_NAME)) { + archiveArtifacts(ONAPSIS_REPORT_NAME) + } + } +}