You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	new step to create a scan summary report (#2559)
* new step to create a scan summary report * add flag to collect only failed reports * add stepName to report
This commit is contained in:
		| @@ -27,6 +27,7 @@ func GetAllStepMetadata() map[string]config.StepData { | ||||
| 		"cloudFoundryDeleteService":               cloudFoundryDeleteServiceMetadata(), | ||||
| 		"cloudFoundryDeleteSpace":                 cloudFoundryDeleteSpaceMetadata(), | ||||
| 		"cloudFoundryDeploy":                      cloudFoundryDeployMetadata(), | ||||
| 		"pipelineCreateScanSummary":               pipelineCreateScanSummaryMetadata(), | ||||
| 		"detectExecuteScan":                       detectExecuteScanMetadata(), | ||||
| 		"fortifyExecuteScan":                      fortifyExecuteScanMetadata(), | ||||
| 		"gctsCloneRepository":                     gctsCloneRepositoryMetadata(), | ||||
|   | ||||
							
								
								
									
										74
									
								
								cmd/pipelineCreateScanSummary.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								cmd/pipelineCreateScanSummary.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/piperutils" | ||||
| 	"github.com/SAP/jenkins-library/pkg/reporting" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/pkg/errors" | ||||
| ) | ||||
|  | ||||
| type pipelineCreateScanSummaryUtils interface { | ||||
| 	FileRead(path string) ([]byte, error) | ||||
| 	FileWrite(path string, content []byte, perm os.FileMode) error | ||||
| 	Glob(pattern string) (matches []string, err error) | ||||
| } | ||||
|  | ||||
| type pipelineCreateScanSummaryUtilsBundle struct { | ||||
| 	*piperutils.Files | ||||
| } | ||||
|  | ||||
| func newPipelineCreateScanSummaryUtils() pipelineCreateScanSummaryUtils { | ||||
| 	utils := pipelineCreateScanSummaryUtilsBundle{ | ||||
| 		Files: &piperutils.Files{}, | ||||
| 	} | ||||
| 	return &utils | ||||
| } | ||||
|  | ||||
| func pipelineCreateScanSummary(config pipelineCreateScanSummaryOptions, telemetryData *telemetry.CustomData) { | ||||
| 	utils := newPipelineCreateScanSummaryUtils() | ||||
|  | ||||
| 	err := runPipelineCreateScanSummary(&config, telemetryData, utils) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Fatal("failed to create scan summary") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const reportDir = ".pipeline/stepReports" | ||||
|  | ||||
| func runPipelineCreateScanSummary(config *pipelineCreateScanSummaryOptions, telemetryData *telemetry.CustomData, utils pipelineCreateScanSummaryUtils) error { | ||||
|  | ||||
| 	pattern := reportDir + "/*.json" | ||||
| 	reports, _ := utils.Glob(pattern) | ||||
|  | ||||
| 	scanReports := []reporting.ScanReport{} | ||||
| 	for _, report := range reports { | ||||
| 		reportContent, err := utils.FileRead(report) | ||||
| 		if err != nil { | ||||
| 			log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 			return errors.Wrapf(err, "failed to read report %v", report) | ||||
| 		} | ||||
| 		scanReport := reporting.ScanReport{} | ||||
| 		if err = json.Unmarshal(reportContent, &scanReport); err != nil { | ||||
| 			return errors.Wrapf(err, "failed to parse report %v", report) | ||||
| 		} | ||||
| 		scanReports = append(scanReports, scanReport) | ||||
| 	} | ||||
|  | ||||
| 	output := []byte{} | ||||
| 	for _, scanReport := range scanReports { | ||||
| 		if (config.FailedOnly && !scanReport.SuccessfulScan) || !config.FailedOnly { | ||||
| 			output = append(output, scanReport.ToMarkdown()...) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := utils.FileWrite(config.OutputFilePath, output, 0666); err != nil { | ||||
| 		log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 		return errors.Wrapf(err, "failed to write %v", config.OutputFilePath) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										117
									
								
								cmd/pipelineCreateScanSummary_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								cmd/pipelineCreateScanSummary_generated.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| // 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/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| type pipelineCreateScanSummaryOptions struct { | ||||
| 	FailedOnly     bool   `json:"failedOnly,omitempty"` | ||||
| 	OutputFilePath string `json:"outputFilePath,omitempty"` | ||||
| } | ||||
|  | ||||
| // PipelineCreateScanSummaryCommand Collect scan result information anc create a summary report | ||||
| func PipelineCreateScanSummaryCommand() *cobra.Command { | ||||
| 	const STEP_NAME = "pipelineCreateScanSummary" | ||||
|  | ||||
| 	metadata := pipelineCreateScanSummaryMetadata() | ||||
| 	var stepConfig pipelineCreateScanSummaryOptions | ||||
| 	var startTime time.Time | ||||
|  | ||||
| 	var createPipelineCreateScanSummaryCmd = &cobra.Command{ | ||||
| 		Use:   STEP_NAME, | ||||
| 		Short: "Collect scan result information anc create a summary report", | ||||
| 		Long: `This step allows you to create a summary report of your scan results. | ||||
|  | ||||
| It is for example used to create a markdown file which can be used to create a GitHub issue.`, | ||||
| 		PreRunE: func(cmd *cobra.Command, _ []string) error { | ||||
| 			startTime = time.Now() | ||||
| 			log.SetStepName(STEP_NAME) | ||||
| 			log.SetVerbose(GeneralConfig.Verbose) | ||||
|  | ||||
| 			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 | ||||
| 			} | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { | ||||
| 				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) | ||||
| 				log.RegisterHook(&sentryHook) | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 		Run: func(_ *cobra.Command, _ []string) { | ||||
| 			telemetryData := telemetry.CustomData{} | ||||
| 			telemetryData.ErrorCode = "1" | ||||
| 			handler := func() { | ||||
| 				config.RemoveVaultSecretFiles() | ||||
| 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||
| 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||
| 				telemetry.Send(&telemetryData) | ||||
| 			} | ||||
| 			log.DeferExitHandler(handler) | ||||
| 			defer handler() | ||||
| 			telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) | ||||
| 			pipelineCreateScanSummary(stepConfig, &telemetryData) | ||||
| 			telemetryData.ErrorCode = "0" | ||||
| 			log.Entry().Info("SUCCESS") | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	addPipelineCreateScanSummaryFlags(createPipelineCreateScanSummaryCmd, &stepConfig) | ||||
| 	return createPipelineCreateScanSummaryCmd | ||||
| } | ||||
|  | ||||
| func addPipelineCreateScanSummaryFlags(cmd *cobra.Command, stepConfig *pipelineCreateScanSummaryOptions) { | ||||
| 	cmd.Flags().BoolVar(&stepConfig.FailedOnly, "failedOnly", false, "Defines if only failed scans should be included into the summary.") | ||||
| 	cmd.Flags().StringVar(&stepConfig.OutputFilePath, "outputFilePath", `scanSummary.md`, "Defines the filepath to the target file which will be created by the step.") | ||||
|  | ||||
| } | ||||
|  | ||||
| // retrieve step metadata | ||||
| func pipelineCreateScanSummaryMetadata() config.StepData { | ||||
| 	var theMetaData = config.StepData{ | ||||
| 		Metadata: config.StepMetadata{ | ||||
| 			Name:        "pipelineCreateScanSummary", | ||||
| 			Aliases:     []config.Alias{}, | ||||
| 			Description: "Collect scan result information anc create a summary report", | ||||
| 		}, | ||||
| 		Spec: config.StepSpec{ | ||||
| 			Inputs: config.StepInputs{ | ||||
| 				Parameters: []config.StepParameters{ | ||||
| 					{ | ||||
| 						Name:        "failedOnly", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "bool", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "outputFilePath", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return theMetaData | ||||
| } | ||||
							
								
								
									
										17
									
								
								cmd/pipelineCreateScanSummary_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cmd/pipelineCreateScanSummary_generated_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPipelineCreateScanSummaryCommand(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	testCmd := PipelineCreateScanSummaryCommand() | ||||
|  | ||||
| 	// only high level testing performed - details are tested in step generation procedure | ||||
| 	assert.Equal(t, "pipelineCreateScanSummary", testCmd.Use, "command name incorrect") | ||||
|  | ||||
| } | ||||
							
								
								
									
										110
									
								
								cmd/pipelineCreateScanSummary_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								cmd/pipelineCreateScanSummary_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/mock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type pipelineCreateScanSummaryMockUtils struct { | ||||
| 	*mock.FilesMock | ||||
| } | ||||
|  | ||||
| func newPipelineCreateScanSummaryTestsUtils() pipelineCreateScanSummaryMockUtils { | ||||
| 	utils := pipelineCreateScanSummaryMockUtils{ | ||||
| 		FilesMock: &mock.FilesMock{}, | ||||
| 	} | ||||
| 	return utils | ||||
| } | ||||
|  | ||||
| func TestRunPipelineCreateScanSummary(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	t.Run("success", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
|  | ||||
| 		config := pipelineCreateScanSummaryOptions{ | ||||
| 			OutputFilePath: "scanSummary.md", | ||||
| 		} | ||||
|  | ||||
| 		utils := newPipelineCreateScanSummaryTestsUtils() | ||||
| 		utils.AddFile(".pipeline/stepReports/step1.json", []byte(`{"title":"Title Scan 1"}`)) | ||||
| 		utils.AddFile(".pipeline/stepReports/step2.json", []byte(`{"title":"Title Scan 2"}`)) | ||||
| 		utils.AddFile(".pipeline/stepReports/step3.json", []byte(`{"title":"Title Scan 3"}`)) | ||||
|  | ||||
| 		err := runPipelineCreateScanSummary(&config, nil, utils) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
| 		reportExists, _ := utils.FileExists("scanSummary.md") | ||||
| 		assert.True(t, reportExists) | ||||
| 		fileContent, _ := utils.FileRead("scanSummary.md") | ||||
| 		fileContentString := string(fileContent) | ||||
| 		assert.Contains(t, fileContentString, "Title Scan 1") | ||||
| 		assert.Contains(t, fileContentString, "Title Scan 2") | ||||
| 		assert.Contains(t, fileContentString, "Title Scan 3") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("success - failed only", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
|  | ||||
| 		config := pipelineCreateScanSummaryOptions{ | ||||
| 			OutputFilePath: "scanSummary.md", | ||||
| 			FailedOnly:     true, | ||||
| 		} | ||||
|  | ||||
| 		utils := newPipelineCreateScanSummaryTestsUtils() | ||||
| 		utils.AddFile(".pipeline/stepReports/step1.json", []byte(`{"title":"Title Scan 1", "successfulScan": true}`)) | ||||
| 		utils.AddFile(".pipeline/stepReports/step2.json", []byte(`{"title":"Title Scan 2", "successfulScan": false}`)) | ||||
| 		utils.AddFile(".pipeline/stepReports/step3.json", []byte(`{"title":"Title Scan 3", "successfulScan": false}`)) | ||||
|  | ||||
| 		err := runPipelineCreateScanSummary(&config, nil, utils) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
| 		reportExists, _ := utils.FileExists("scanSummary.md") | ||||
| 		assert.True(t, reportExists) | ||||
| 		fileContent, _ := utils.FileRead("scanSummary.md") | ||||
| 		fileContentString := string(fileContent) | ||||
| 		assert.NotContains(t, fileContentString, "Title Scan 1") | ||||
| 		assert.Contains(t, fileContentString, "Title Scan 2") | ||||
| 		assert.Contains(t, fileContentString, "Title Scan 3") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error - read file", func(t *testing.T) { | ||||
| 		t.Skip() | ||||
| 		//ToDo | ||||
| 		// so far mock cannot create error for reading files | ||||
|  | ||||
| 		config := pipelineCreateScanSummaryOptions{ | ||||
| 			OutputFilePath: "scanSummary.md", | ||||
| 		} | ||||
|  | ||||
| 		utils := newPipelineCreateScanSummaryTestsUtils() | ||||
|  | ||||
| 		err := runPipelineCreateScanSummary(&config, nil, utils) | ||||
|  | ||||
| 		assert.Contains(t, fmt.Sprint(err), "failed to read report") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error - unmarshal json", func(t *testing.T) { | ||||
| 		t.Parallel() | ||||
|  | ||||
| 		config := pipelineCreateScanSummaryOptions{ | ||||
| 			OutputFilePath: "scanSummary.md", | ||||
| 		} | ||||
|  | ||||
| 		utils := newPipelineCreateScanSummaryTestsUtils() | ||||
| 		utils.AddFile(".pipeline/stepReports/step1.json", []byte(`{"title":"Title Scan 1"`)) | ||||
|  | ||||
| 		err := runPipelineCreateScanSummary(&config, nil, utils) | ||||
|  | ||||
| 		assert.Contains(t, fmt.Sprint(err), "failed to parse report") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error - write file", func(t *testing.T) { | ||||
| 		//ToDo | ||||
| 		// so far mock cannot create error | ||||
| 	}) | ||||
|  | ||||
| } | ||||
| @@ -11,32 +11,34 @@ import ( | ||||
|  | ||||
| // ScanReport defines the elements of a scan report used by various scan steps | ||||
| type ScanReport struct { | ||||
| 	Title       string | ||||
| 	Subheaders  []string | ||||
| 	Overview    []string | ||||
| 	FurtherInfo string | ||||
| 	ReportTime  time.Time | ||||
| 	DetailTable ScanDetailTable | ||||
| 	StepName       string          `json:"stepName"` | ||||
| 	Title          string          `json:"title"` | ||||
| 	Subheaders     []string        `json:"subheaders"` | ||||
| 	Overview       []string        `json:"overview"` | ||||
| 	FurtherInfo    string          `json:"furtherInfo"` | ||||
| 	ReportTime     time.Time       `json:"reportTime"` | ||||
| 	DetailTable    ScanDetailTable `json:"detailTable"` | ||||
| 	SuccessfulScan bool            `json:"successfulScan"` | ||||
| } | ||||
|  | ||||
| // ScanDetailTable defines a table containing scan result details | ||||
| type ScanDetailTable struct { | ||||
| 	Headers       []string | ||||
| 	Rows          []ScanRow | ||||
| 	WithCounter   bool | ||||
| 	CounterHeader string | ||||
| 	NoRowsMessage string | ||||
| 	Headers       []string  `json:"headers"` | ||||
| 	Rows          []ScanRow `json:"rows"` | ||||
| 	WithCounter   bool      `json:"withCounter"` | ||||
| 	CounterHeader string    `json:"counterHeader"` | ||||
| 	NoRowsMessage string    `json:"noRowsMessage"` | ||||
| } | ||||
|  | ||||
| // ScanRow defines one row of a scan result table | ||||
| type ScanRow struct { | ||||
| 	Columns []ScanCell | ||||
| 	Columns []ScanCell `json:"columns"` | ||||
| } | ||||
|  | ||||
| // ScanCell defines one column of a scan result table | ||||
| type ScanCell struct { | ||||
| 	Content string | ||||
| 	Style   ColumnStyle | ||||
| 	Content string      `json:"content"` | ||||
| 	Style   ColumnStyle `json:"style"` | ||||
| } | ||||
|  | ||||
| // ColumnStyle defines style for a specific column | ||||
| @@ -184,7 +186,7 @@ func (s *ScanReport) ToHTML() ([]byte, error) { | ||||
| } | ||||
|  | ||||
| // ToMarkdown creates a markdown version of the report content | ||||
| func (s *ScanReport) ToMarkdown() string { | ||||
| func (s *ScanReport) ToMarkdown() []byte { | ||||
| 	//ToDo: create collapsible markdown? | ||||
| 	/* | ||||
| 		## collapsible markdown? | ||||
| @@ -201,7 +203,8 @@ func (s *ScanReport) ToMarkdown() string { | ||||
| 		</p> | ||||
| 		</details> | ||||
| 	*/ | ||||
| 	return "" | ||||
|  | ||||
| 	return []byte(fmt.Sprintf("<summary>%v</summary>", s.Title)) | ||||
| } | ||||
|  | ||||
| func tableColumnCount(scanDetails ScanDetailTable) int { | ||||
|   | ||||
							
								
								
									
										25
									
								
								resources/metadata/createmarkdownreport.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								resources/metadata/createmarkdownreport.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| metadata: | ||||
|   name: pipelineCreateScanSummary | ||||
|   description: Collect scan result information anc create a summary report | ||||
|   longDescription: | | ||||
|     This step allows you to create a summary report of your scan results. | ||||
|  | ||||
|     It is for example used to create a markdown file which can be used to create a GitHub issue. | ||||
| spec: | ||||
|   inputs: | ||||
|     params: | ||||
|       - name: failedOnly | ||||
|         description: Defines if only failed scans should be included into the summary. | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         type: bool | ||||
|       - name: outputFilePath | ||||
|         description: Defines the filepath to the target file which will be created by the step. | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         type: string | ||||
|         default: scanSummary.md | ||||
		Reference in New Issue
	
	Block a user