1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-02-21 19:48:53 +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:
Oliver Nocon 2021-02-02 14:36:40 +01:00 committed by GitHub
parent f0828ad5e5
commit b7754437b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 363 additions and 16 deletions

View File

@ -27,6 +27,7 @@ func GetAllStepMetadata() map[string]config.StepData {
"cloudFoundryDeleteService": cloudFoundryDeleteServiceMetadata(),
"cloudFoundryDeleteSpace": cloudFoundryDeleteSpaceMetadata(),
"cloudFoundryDeploy": cloudFoundryDeployMetadata(),
"pipelineCreateScanSummary": pipelineCreateScanSummaryMetadata(),
"detectExecuteScan": detectExecuteScanMetadata(),
"fortifyExecuteScan": fortifyExecuteScanMetadata(),
"gctsCloneRepository": gctsCloneRepositoryMetadata(),

View 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
}

View 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
}

View 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")
}

View 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
})
}

View File

@ -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 {

View 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