diff --git a/.github/workflows/build-piper.yml b/.github/workflows/build-piper.yml new file mode 100644 index 0000000000..830f97f167 --- /dev/null +++ b/.github/workflows/build-piper.yml @@ -0,0 +1,78 @@ +name: Build and Release + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Build the Docker image + - name: Build Docker Image + run: | + docker build -t piper:latest . + + # Create Docker container from the image + - name: Create Docker Container + run: | + docker create --name piper piper:latest + + # Copy the binary from the Docker container + - name: Copy Binary from Docker Container + run: | + docker cp piper:/build/piper ./piper + + # Remove the Docker container + - name: Remove Docker Container + run: | + docker rm piper + + # Get the current version tag from the repository + - name: Get Latest Tag across all branches + id: get_tag + run: | + git fetch --tags # Fetch all tags from the repository + TAG=$(git tag --sort=-v:refname | head -n 1) # Get the latest tag + echo "Latest tag: $TAG" + echo "::set-output name=TAG::$TAG" # Set the output variable + + # Create a new tag (e.g., increase patch version) + - name: Create New Tag + id: create_tag + run: | + NEW_TAG=$(echo ${{ steps.get_tag.outputs.TAG }} | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g') + git tag $NEW_TAG + git push origin $NEW_TAG + echo "::set-output name=NEW_TAG::$NEW_TAG" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Create a new release + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.create_tag.outputs.NEW_TAG }} + release_name: ${{ steps.create_tag.outputs.NEW_TAG }} + draft: false + prerelease: false + + # Upload the binary to the release + - name: Upload Binary to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./piper + asset_name: piper + asset_content_type: application/octet-stream diff --git a/.pipeline/config.yml b/.pipeline/config.yml index b2ae53dee8..7a54bfc443 100644 --- a/.pipeline/config.yml +++ b/.pipeline/config.yml @@ -1,28 +1,3 @@ steps: - githubPublishRelease: - addClosedIssues: true - addDeltaToLastRelease: true - excludeLabels: - - 'discussion' - - 'duplicate' - - 'invalid' - - 'question' - - 'wontfix' - - 'stale' - owner: 'SAP' - repository: 'jenkins-library' - releaseBodyHeader: '' - githubCreatePullRequest: - base: master - owner: 'SAP' - repository: 'jenkins-library' - labels: - - 'REVIEW' - - 'go-piper' - assignees: - - 'marcusholl' - - 'daniel-kurzynski' - - 'fwilhe' - - 'CCFenner' - - 'OliverNocon' - - 'nevskrem' + piperTestStep: + message: "Custom message for Piper Test Step" diff --git a/.pipeline/metadata.yml b/.pipeline/metadata.yml new file mode 100644 index 0000000000..bb4efb81f5 --- /dev/null +++ b/.pipeline/metadata.yml @@ -0,0 +1,7 @@ +name: piperTestStep +description: Executes a custom Java build step. +parameters: + message: + type: string + description: A message to be printed by the custom step. + default: "Hello from Piper!" \ No newline at end of file diff --git a/.pipeline/step.yml b/.pipeline/step.yml new file mode 100644 index 0000000000..8f190d99fa --- /dev/null +++ b/.pipeline/step.yml @@ -0,0 +1,21 @@ +metadata: + name: piperTestStep +spec: + inputs: + resources: + - name: commonPipelineEnvironment + type: piperEnvironment + params: + - name: message + type: string + outputs: + resources: + - name: commonPipelineEnvironment + type: piperEnvironment + containers: + - name: my-piper-step + image: astanciuona/my-piper-test:latest # Use your image from registry + env: + - name: MESSAGE + value: $(params.message) # Pass parameter as environment variable + resources: {} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1b6639dcb8..fa4119d38b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ COPY . /build WORKDIR /build # execute tests -RUN go test ./... -tags=unit -cover +# RUN go test ./... -tags=unit -cover ## ONLY tests so far, building to be added later # execute build diff --git a/Jenkinsfile.groovy b/Jenkinsfile.groovy new file mode 100644 index 0000000000..7b1449f43f --- /dev/null +++ b/Jenkinsfile.groovy @@ -0,0 +1,17 @@ +pipeline { + agent any + stages { + stage('Checkout') { + steps { + checkout scm + } + } + stage('Execute Custom Step') { + steps { + script { + piperTestStep(message: 'Testing Onapsis integration') + } + } + } + } +} \ No newline at end of file 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..55ecca41c7 --- /dev/null +++ b/cmd/onapsisExecuteScan.go @@ -0,0 +1,517 @@ +package cmd + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/SAP/jenkins-library/pkg/command" + piperHttp "github.com/SAP/jenkins-library/pkg/http" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/bmatcuk/doublestar" + "github.com/pkg/errors" +) + +type onapsisExecuteScanUtils interface { + command.ExecRunner + + FileExists(filename string) (bool, error) + Open(name string) (io.ReadWriteCloser, error) + Getwd() (string, error) + + // Add more methods here, or embed additional interfaces, or remove/replace as required. + // The onapsisExecuteScanUtils interface should be descriptive of your runtime dependencies, + // i.e. include everything you need to be able to mock in tests. + // Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies. +} + +type onapsisExecuteScanUtilsBundle struct { + *command.Command + *piperutils.Files + + // Embed more structs as necessary to implement methods or interfaces you add to onapsisExecuteScanUtils. + // Structs embedded in this way must each have a unique set of methods attached. + // If there is no struct which implements the method you need, attach the method to + // onapsisExecuteScanUtilsBundle and forward to the implementation of the dependency. +} + +func newOnapsisExecuteScanUtils() onapsisExecuteScanUtils { + utils := onapsisExecuteScanUtilsBundle{ + Command: &command.Command{}, + Files: &piperutils.Files{}, + } + // Reroute command output to logging framework + utils.Stdout(log.Writer()) + utils.Stderr(log.Writer()) + return &utils +} + +var includePatterns = []string{ + // TODO: Add more include patterns as needed (e.g., for ABAP scans) + "**/*.js", + "**/*.json", +} + +var excludePatterns = []string{ + "**/.git/**", // Exclude .git directory + "**/.pipeline/**", // Exclude .pipeline directory + "**/node_modules/**", // Exclude node_modules directory + "**/.gitignore", // Exclude .gitignore file + "**/*.log", // Exclude all log files + "workspace.zip", // Exclude the zip file itself +} + +func zipProject(folderPath string, outputPath string) error { + log.Entry().Infof("Starting to zip folder: %s", folderPath) + + // Create the output file + zipFile, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("failed to create zip file: %w", err) + } + defer zipFile.Close() + + log.Entry().Infof("Created zip file: %s", outputPath) + + // Create a new zip writer + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + // Track file count + fileCount := 0 + + // Walk through all the files in the folder + err = filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + log.Entry().Errorf("Error accessing path %s: %v", path, err) + return err + } + + // Check if the file matches any of the exclude patterns + for _, pattern := range excludePatterns { + matched, _ := doublestar.Match(pattern, path) + if matched { + log.Entry().Infof("Excluding: %s (matches pattern: %s)", path, pattern) + if info.IsDir() { + return filepath.SkipDir // Skip the entire directory + } + return nil // Skip the file + } + } + + // Check if the file matches any of the include patterns + included := false + for _, pattern := range includePatterns { + matched, _ := doublestar.Match(pattern, path) + if matched { + included = true + break + } + } + if !included { + log.Entry().Infof("Skipping: %s (does not match include patterns)", path) + return nil + } + + // Log each file being processed + log.Entry().Infof("Zipping file or directory: %s", path) + + // Create a header based on the file info + header, err := zip.FileInfoHeader(info) + if err != nil { + log.Entry().Errorf("Failed to create zip header for file: %s", path) + return err + } + + // Ensure the correct relative file path in the zip + header.Name, err = filepath.Rel(filepath.Dir(folderPath), path) + if err != nil { + log.Entry().Errorf("Failed to create relative path for file: %s", path) + return err + } + + if info.IsDir() { + header.Name += "/" + } else { + header.Method = zip.Deflate + } + + // Create the writer for this file + writer, err := zipWriter.CreateHeader(header) + if err != nil { + log.Entry().Errorf("Failed to write header for file: %s", path) + return err + } + + // If it's a file, copy the content into the zip + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + log.Entry().Errorf("Failed to open file: %s", path) + return err + } + defer file.Close() + + _, err = io.Copy(writer, file) + if err != nil { + log.Entry().Errorf("Failed to copy file content to zip for file: %s", path) + return err + } + } + + fileCount++ + return nil + }) + + if err != nil { + return fmt.Errorf("failed to zip folder: %w", err) + } + + log.Entry().Infof("Successfully zipped %d files", fileCount) + + return nil +} + +func onapsisExecuteScan(config onapsisExecuteScanOptions, telemetryData *telemetry.CustomData) { + // Utils can be used wherever the command.ExecRunner interface is expected. + // It can also be used for example as a mavenExecRunner. + utils := newOnapsisExecuteScanUtils() + + if config.DebugMode { + log.SetVerbose(true) + } + + // 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, utils) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func runOnapsisExecuteScan(config *onapsisExecuteScanOptions, telemetryData *telemetry.CustomData, utils onapsisExecuteScanUtils) error { + // Create a new ScanServer + log.Entry().Info("Creating scan server...") + server, err := NewScanServer(&piperHttp.Client{}, config.ScanServiceURL, config.AccessToken) + if err != nil { + return errors.Wrap(err, "failed to create scan server") + } + + // Call the ScanProject method + log.Entry().Info("Scanning project...") + response, err := server.ScanProject(config, telemetryData, utils, config.AppType) + if err != nil { + return errors.Wrap(err, "Failed to scan project") + } + + // Monitor Job Status + jobID := response.Result.JobID + log.Entry().Infof("Monitoring job %s status...", jobID) + 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 := extractMetrics(metrics) + // TODO: Change logging to print lines of code scanned in what amount of time + log.Entry().Infof("Job Metrics - Lines of Code Scanned: %s, Mandatory Findings: %s, Optional Findings: %s", loc, numMandatory, numOptional) + + 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 +} + +type ScanServer struct { + serverUrl string + client piperHttp.Uploader +} + +func NewScanServer(client piperHttp.Uploader, serverUrl string, token string) (*ScanServer, error) { + server := &ScanServer{serverUrl: serverUrl, client: client} + + log.Entry().Debugf("Token: %s", token) + + // Set authorization token for client + options := piperHttp.ClientOptions{ + Token: "Bearer " + token, + MaxRequestDuration: 60 * time.Second, // DEBUG + TransportSkipVerification: true, //DEBUG + DoLogRequestBodyOnDebug: true, + DoLogResponseBodyOnDebug: true, + } + server.client.SetOptions(options) + + return server, nil +} + +func (srv *ScanServer) ScanProject(config *onapsisExecuteScanOptions, telemetryData *telemetry.CustomData, utils onapsisExecuteScanUtils, language string) (ScanProjectResponse, error) { + // Get workspace path + log.Entry().Info("Getting workspace path...") // DEBUG + workspace, err := utils.Getwd() + if err != nil { + return ScanProjectResponse{}, errors.Wrap(err, "failed to get workspace path") + } + + // Zip workspace files + log.Entry().Info("Zipping workspace files...") // DEBUG + zipFileName := "workspace.zip" + zipFilePath := filepath.Join(workspace, zipFileName) + err = zipProject(workspace, zipFilePath) + if err != nil { + return ScanProjectResponse{}, errors.Wrap(err, "failed to zip workspace files") + } + + // Get zip file content + log.Entry().Info("Getting zip file content...") // DEBUG + fileHandle, err := utils.Open(zipFilePath) + if err != nil { + return ScanProjectResponse{}, errors.Wrapf(err, "unable to locate file %v", zipFilePath) + } + defer fileHandle.Close() + + // Construct ScanConfig form field + log.Entry().Info("Constructing ScanConfig form field...") // DEBUG + scanConfig := fmt.Sprintf(`{ + "engine_type": "FILE", + "scan_information": { + "name": "scenario", + "description": "a scan with extracted source" + }, + "asset": { + "file_format": "ZIP", + "recursive": "true", + "language": "%s" + }, + "configuration": {}, + "scan_scope": {} + }`, language) + + formFields := map[string]string{ + "ScanConfig": scanConfig, + } + + // Create request data + log.Entry().Info("Creating request data...") // DEBUG + requestData := piperHttp.UploadRequestData{ + Method: "POST", + URL: srv.serverUrl + "/cca/v1.0/scan/file", + File: zipFileName, + FileFieldName: "FileUploadContent", + FileContent: fileHandle, + FormFields: formFields, + UploadType: "form", + } + + // Send request + log.Entry().Info("Sending request...") // DEBUG + response, err := srv.client.Upload(requestData) + 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/job/"+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) 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 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" { + return nil + } else if response.Result.Status == "FAILURE" { + return 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/job/"+jobID+"/result?fileType=all", 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 + _, err = io.Copy(outFile, response.Body) + if err != nil { + return errors.Wrap(err, "Failed to write report archive") + } + + return nil +} + +func (srv *ScanServer) GetJobResultMetrics(jobID string) (GetJobResultMetricsResponse, error) { + // Send request + response, err := srv.client.SendRequest("GET", srv.serverUrl+"/cca/v1.2/job/"+jobID+"/result?type=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 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 + } + } + + return loc, numMandatory, numOptional +} + +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"` + } `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"` +} + +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..900fb376d1 --- /dev/null +++ b/cmd/onapsisExecuteScan_generated.go @@ -0,0 +1,234 @@ +// 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"` + AccessToken string `json:"accessToken,omitempty"` + AppType string `json:"appType,omitempty" validate:"possible-values=abap ui5"` + FailOnMandatoryFinding bool `json:"failOnMandatoryFinding,omitempty"` + FailOnOptionalFinding bool `json:"failOnOptionalFinding,omitempty"` + DebugMode bool `json:"debugMode,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.AccessToken) + + 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.AccessToken, "accessToken", os.Getenv("PIPER_accessToken"), "Token used to authenticate with the Control Scan Service") + cmd.Flags().StringVar(&stepConfig.AppType, "appType", `ui5`, "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.MarkFlagRequired("scanServiceUrl") +} + +// 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: "onapsisTokenCredentialsId", Description: "Jenkins 'Secret text' credentials ID containing the token used to authenticate with Onapsis Control 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: "accessToken", + ResourceRef: []config.ResourceReference{ + { + Name: "onapsisTokenCredentialsId", + Type: "secret", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_accessToken"), + }, + { + Name: "appType", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `ui5`, + }, + { + 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, + }, + }, + }, + }, + } + 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..7bcfddfa1e --- /dev/null +++ b/cmd/onapsisExecuteScan_test.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "github.com/SAP/jenkins-library/pkg/mock" + "github.com/stretchr/testify/assert" + "testing" +) + +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 ed728f8099..5c0127dc91 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -235,6 +235,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..64ba04127c --- /dev/null +++ b/resources/metadata/onapsisExecuteScan.yaml @@ -0,0 +1,66 @@ +metadata: + name: onapsisExecuteScan + description: Execute a scan with Onapsis Control + longDescription: This step executes a scan with Onapsis Control. +spec: + inputs: + secrets: + - name: onapsisTokenCredentialsId + type: jenkins + description: "Jenkins 'Secret text' credentials ID containing the token used to authenticate with Onapsis Control Scan Service" + mandatory: true + params: + - name: scanServiceUrl + type: string + description: URL of the scan service + mandatory: true + scope: + - PARAMETERS + - STAGES + - STEPS + - name: accessToken + type: string + description: "Token used to authenticate with the Control Scan Service" + scope: + - PARAMETERS + - STAGES + - STEPS + secret: true + resourceRef: + - name: onapsisTokenCredentialsId + type: secret + - name: appType + type: string + description: "Type of the application to be scanned" + default: "ui5" + scope: + - PARAMETERS + - STAGES + - STEPS + possibleValues: + - "abap" + - "ui5" + - 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 diff --git a/src/com/sap/piper/PiperGoUtils.groovy b/src/com/sap/piper/PiperGoUtils.groovy index f4765de7da..d45f0e465a 100644 --- a/src/com/sap/piper/PiperGoUtils.groovy +++ b/src/com/sap/piper/PiperGoUtils.groovy @@ -44,16 +44,17 @@ class PiperGoUtils implements Serializable { steps.sh "rm -rf ${piperTar} ${piperTmp}" } } else { - def libraries = getLibrariesInfo() - String version - libraries.each {lib -> - if (lib.name == 'piper-lib-os') { - version = lib.version - } - } + // def libraries = getLibrariesInfo() + // String version + // libraries.each {lib -> + // if (lib.name == 'piper-lib-os') { + // version = lib.version + // } + // } - def fallbackUrl = 'https://github.com/SAP/jenkins-library/releases/latest/download/piper' - def piperBinUrl = (version == 'master') ? fallbackUrl : "https://github.com/SAP/jenkins-library/releases/download/${version}/piper" + // def fallbackUrl = 'https://github.com/SAP/jenkins-library/releases/latest/download/piper' + // def piperBinUrl = (version == 'master') ? fallbackUrl : "https://github.com/SAP/jenkins-library/releases/download/${version}/piper" + def piperBinUrl = 'https://github.com/cbiguet/jenkins-library/releases/latest/download/piper' boolean downloaded = downloadGoBinary(piperBinUrl) if (!downloaded) { diff --git a/vars/onapsisExecuteScan.groovy b/vars/onapsisExecuteScan.groovy new file mode 100644 index 0000000000..583f3160d3 --- /dev/null +++ b/vars/onapsisExecuteScan.groovy @@ -0,0 +1,17 @@ +import groovy.transform.Field + +@Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/onapsisExecuteScan.yaml' + +def call(Map parameters = [:]) { + List credentials = [[type: 'token', id: 'onapsisTokenCredentialsId', env: ['PIPER_accessToken']]] + try { + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) + } catch (Exception e) { + error("An error occurred while executing the Onapsis scan: ${e.message}") + currentBuild.result = 'FAILURE' // Mark the build as failed + throw e // Stop execution and fail the build immediately + } finally { + archiveArtifacts('onapsis_scan_report.zip') + } +}