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:
parent
f0828ad5e5
commit
b7754437b3
@ -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
|
Loading…
x
Reference in New Issue
Block a user