mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-18 05:18:24 +02:00
fix(whitesourceExecuteScan): properly handle policy violations (#4089)
* fix(whitesourceExecuteScan): properly handle policy violations * update files * update formatting Co-authored-by: Sven Merk <33895725+nevskrem@users.noreply.github.com>
This commit is contained in:
parent
2866ef5592
commit
a2815c4567
@ -260,9 +260,14 @@ func checkAndReportScanResults(ctx context.Context, config *ScanOptions, scan *w
|
||||
|
||||
checkErrors := []string{}
|
||||
|
||||
rPath, err := checkPolicyViolations(config, scan, sys, utils, reportPaths, influx)
|
||||
rPath, err := checkPolicyViolations(ctx, config, scan, sys, utils, reportPaths, influx)
|
||||
|
||||
if err != nil {
|
||||
checkErrors = append(checkErrors, fmt.Sprint(err))
|
||||
if !config.FailOnSevereVulnerabilities && log.GetErrorCategory() == log.ErrorCompliance {
|
||||
log.Entry().Infof("policy violation(s) found - step will only create data but not fail due to setting failOnSevereVulnerabilities: false")
|
||||
} else {
|
||||
checkErrors = append(checkErrors, fmt.Sprint(err))
|
||||
}
|
||||
}
|
||||
reportPaths = append(reportPaths, rPath)
|
||||
|
||||
@ -270,7 +275,11 @@ func checkAndReportScanResults(ctx context.Context, config *ScanOptions, scan *w
|
||||
rPaths, err := checkSecurityViolations(ctx, config, scan, sys, utils, influx)
|
||||
reportPaths = append(reportPaths, rPaths...)
|
||||
if err != nil {
|
||||
checkErrors = append(checkErrors, fmt.Sprint(err))
|
||||
if !config.FailOnSevereVulnerabilities && log.GetErrorCategory() == log.ErrorCompliance {
|
||||
log.Entry().Infof("policy violation(s) found - step will only create data but not fail due to setting failOnSevereVulnerabilities: false")
|
||||
} else {
|
||||
checkErrors = append(checkErrors, fmt.Sprint(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,14 +489,16 @@ func executeScan(config *ScanOptions, scan *ws.Scan, utils whitesourceUtils) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPolicyViolations(config *ScanOptions, scan *ws.Scan, sys whitesource, utils whitesourceUtils, reportPaths []piperutils.Path, influx *whitesourceExecuteScanInflux) (piperutils.Path, error) {
|
||||
func checkPolicyViolations(ctx context.Context, config *ScanOptions, scan *ws.Scan, sys whitesource, utils whitesourceUtils, reportPaths []piperutils.Path, influx *whitesourceExecuteScanInflux) (piperutils.Path, error) {
|
||||
policyViolationCount := 0
|
||||
allAlerts := []ws.Alert{}
|
||||
for _, project := range scan.ScannedProjects() {
|
||||
alerts, err := sys.GetProjectAlertsByType(project.Token, "REJECTED_BY_POLICY_RESOURCE")
|
||||
if err != nil {
|
||||
return piperutils.Path{}, fmt.Errorf("failed to retrieve project policy alerts from WhiteSource: %w", err)
|
||||
}
|
||||
policyViolationCount += len(alerts)
|
||||
allAlerts = append(allAlerts, alerts...)
|
||||
}
|
||||
|
||||
violations := struct {
|
||||
@ -547,11 +558,24 @@ func checkPolicyViolations(config *ScanOptions, scan *ws.Scan, sys whitesource,
|
||||
|
||||
if policyViolationCount > 0 {
|
||||
influx.whitesource_data.fields.policy_violations = policyViolationCount
|
||||
if config.FailOnSevereVulnerabilities {
|
||||
log.SetErrorCategory(log.ErrorCompliance)
|
||||
return policyReport, fmt.Errorf("%v policy violation(s) found", policyViolationCount)
|
||||
log.SetErrorCategory(log.ErrorCompliance)
|
||||
|
||||
if config.CreateResultIssue && policyViolationCount > 0 && len(config.GithubToken) > 0 && len(config.GithubAPIURL) > 0 && len(config.Owner) > 0 && len(config.Repository) > 0 {
|
||||
log.Entry().Debugf("Creating result issues for %v alert(s)", policyViolationCount)
|
||||
issueDetails := make([]reporting.IssueDetail, len(allAlerts))
|
||||
piperutils.CopyAtoB(allAlerts, issueDetails)
|
||||
gh := reporting.GitHub{
|
||||
Owner: &config.Owner,
|
||||
Repository: &config.Repository,
|
||||
Assignees: &config.Assignees,
|
||||
IssueService: utils.GetIssueService(),
|
||||
SearchService: utils.GetSearchService(),
|
||||
}
|
||||
if err := gh.UploadMultipleReports(ctx, &issueDetails); err != nil {
|
||||
return policyReport, fmt.Errorf("failed to upload reports to GitHub for %v policy violations: %w", policyViolationCount, err)
|
||||
}
|
||||
}
|
||||
log.Entry().Infof("%v policy violation(s) found - step will only create data but not fail due to setting failOnSevereVulnerabilities: false", policyViolationCount)
|
||||
return policyReport, fmt.Errorf("%v policy violation(s) found", policyViolationCount)
|
||||
}
|
||||
|
||||
return policyReport, nil
|
||||
@ -900,7 +924,6 @@ func persistScannedProjects(config *ScanOptions, scan *ws.Scan, commonPipelineEn
|
||||
}
|
||||
|
||||
// create toolrecord file for whitesource
|
||||
//
|
||||
func createToolRecordWhitesource(utils whitesourceUtils, workspace string, config *whitesourceExecuteScanOptions, scan *ws.Scan) (string, error) {
|
||||
record := toolrecord.New(utils, workspace, "whitesource", config.ServiceURL)
|
||||
wsUiRoot := "https://saas.whitesourcesoftware.com"
|
||||
|
@ -377,6 +377,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success - no violations", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{ProductName: "mock-product", Version: "1"}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -391,7 +392,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
path, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(ws.ReportsDirectory, "whitesource-ip.json"), path.Target)
|
||||
|
||||
@ -405,6 +406,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("success - no reports", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -416,7 +418,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
reportPaths := []piperutils.Path{}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
path, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fileContent, _ := utilsMock.FileRead(path.Target)
|
||||
@ -425,6 +427,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("error - policy violations", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{FailOnSevereVulnerabilities: true}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -442,7 +445,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
path, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.Contains(t, fmt.Sprint(err), "2 policy violation(s) found")
|
||||
|
||||
fileContent, _ := utilsMock.FileRead(path.Target)
|
||||
@ -452,6 +455,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("error - get alerts", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -463,11 +467,12 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
reportPaths := []piperutils.Path{}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
_, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.Contains(t, fmt.Sprint(err), "failed to retrieve project policy alerts from WhiteSource")
|
||||
})
|
||||
|
||||
t.Run("error - write file", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -480,11 +485,12 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
reportPaths := []piperutils.Path{}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
_, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.Contains(t, fmt.Sprint(err), "failed to write policy violation report:")
|
||||
})
|
||||
|
||||
t.Run("failed to write json report", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := ScanOptions{ProductName: "mock-product", Version: "1"}
|
||||
scan := newWhitesourceScan(&config)
|
||||
if err := scan.AppendScannedProject("testProject1"); err != nil {
|
||||
@ -499,7 +505,7 @@ func TestCheckPolicyViolations(t *testing.T) {
|
||||
reportPaths := []piperutils.Path{}
|
||||
influx := whitesourceExecuteScanInflux{}
|
||||
|
||||
_, err := checkPolicyViolations(&config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
|
||||
assert.Contains(t, fmt.Sprint(err), "failed to write json report")
|
||||
})
|
||||
}
|
||||
|
93
pkg/reporting/policyViolation.go
Normal file
93
pkg/reporting/policyViolation.go
Normal file
@ -0,0 +1,93 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/orchestrator"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type PolicyViolationReport struct {
|
||||
ArtifactID string
|
||||
Branch string
|
||||
CommitID string
|
||||
Description string
|
||||
DirectDependency string
|
||||
Footer string
|
||||
Group string
|
||||
PackageURL string
|
||||
PipelineName string
|
||||
PipelineLink string
|
||||
Version string
|
||||
}
|
||||
|
||||
const policyViolationMdTemplate string = `# Policy Violation - {{ .PackageURL }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
## Context
|
||||
|
||||
{{if .PipelineLink -}}
|
||||
### Pipeline
|
||||
|
||||
Pipeline run: [{{ .PipelineName }}]({{ .PipelineLink }})
|
||||
{{- end}}
|
||||
|
||||
### Detected in
|
||||
|
||||
{{if .Branch}}**Branch:** {{ .Branch }}{{- end}}
|
||||
{{if .CommitID}}**CommitId:** {{ .CommitID }}{{- end}}
|
||||
{{if .DirectDependency}}**Dependency:** {{if (eq .DirectDependency "true")}}direct{{ else }}indirect{{ end }}{{- end}}
|
||||
{{if .ArtifactID}}**ArtifactId:** {{ .ArtifactID }}{{- end}}
|
||||
{{if .Group}}**Group:** {{ .Group }}{{- end}}
|
||||
{{if .Version}}**Version:** {{ .Version }}{{- end}}
|
||||
{{if .PackageURL}}**Package URL:** {{ .PackageURL }}{{- end}}
|
||||
|
||||
---
|
||||
|
||||
{{.Footer}}
|
||||
`
|
||||
|
||||
func (p *PolicyViolationReport) ToMarkdown() ([]byte, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"date": func(t time.Time) string {
|
||||
return t.Format("2006-01-02")
|
||||
},
|
||||
"title": func(s string) string {
|
||||
caser := cases.Title(language.AmericanEnglish)
|
||||
return caser.String(s)
|
||||
},
|
||||
}
|
||||
|
||||
// only fill with orchestrator information if orchestrator can be identified properly
|
||||
if provider, err := orchestrator.NewOrchestratorSpecificConfigProvider(); err == nil {
|
||||
// only add information if not yet provided
|
||||
if len(p.CommitID) == 0 {
|
||||
p.CommitID = provider.GetCommit()
|
||||
}
|
||||
if len(p.PipelineLink) == 0 {
|
||||
p.PipelineLink = provider.GetJobURL()
|
||||
p.PipelineName = provider.GetJobName()
|
||||
}
|
||||
}
|
||||
|
||||
md := []byte{}
|
||||
tmpl, err := template.New("report").Funcs(funcMap).Parse(policyViolationMdTemplate)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to create markdown issue template: %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = tmpl.Execute(buf, p)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to execute markdown issue template: %w", err)
|
||||
}
|
||||
md = buf.Bytes()
|
||||
return md, nil
|
||||
}
|
43
pkg/reporting/policyViolation_test.go
Normal file
43
pkg/reporting/policyViolation_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPolicyViolationToMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("success - empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
policyReport := PolicyViolationReport{}
|
||||
_, err := policyReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("success - filled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
policyReport := PolicyViolationReport{
|
||||
ArtifactID: "theArtifact",
|
||||
Branch: "main",
|
||||
CommitID: "acb123",
|
||||
Description: "This is the test description.",
|
||||
DirectDependency: "true",
|
||||
Footer: "This is the test footer",
|
||||
Group: "the.group",
|
||||
PipelineName: "thePipelineName",
|
||||
PipelineLink: "https://the.link.to.the.pipeline",
|
||||
Version: "1.2.3",
|
||||
PackageURL: "pkg:generic/the.group/theArtifact@1.2.3",
|
||||
}
|
||||
goldenFilePath := filepath.Join("testdata", "markdownPolicyViolation.golden")
|
||||
expected, err := os.ReadFile(goldenFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := policyReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(expected), string(res))
|
||||
})
|
||||
}
|
@ -7,11 +7,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/orchestrator"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// IssueDetail represents any content that can be transformed into the body of a GitHub issue
|
||||
@ -21,101 +17,6 @@ type IssueDetail interface {
|
||||
ToTxt() string
|
||||
}
|
||||
|
||||
// VulnerabilityReport represents metadata for a report on a vulnerability
|
||||
type VulnerabilityReport struct {
|
||||
ArtifactID string
|
||||
Branch string
|
||||
CommitID string
|
||||
Description string
|
||||
DirectDependency string
|
||||
Footer string
|
||||
Group string
|
||||
PackageURL string
|
||||
PipelineName string
|
||||
PipelineLink string
|
||||
PublishDate string
|
||||
Resolution string
|
||||
Score float64
|
||||
Severity string
|
||||
Version string
|
||||
VulnerabilityLink string
|
||||
VulnerabilityName string
|
||||
}
|
||||
|
||||
const vulnerabilityMdTemplate string = `# {{title .Severity }} ({{ .Score }}) Vulnerability {{ .VulnerabilityName }} - {{ .ArtifactID }}
|
||||
|
||||
**Vulnerability link:** [{{ .VulnerabilityLink }}]({{ .VulnerabilityLink }})
|
||||
|
||||
## Fix
|
||||
|
||||
**{{ .Resolution }}**
|
||||
|
||||
## Context
|
||||
|
||||
{{if .PipelineLink -}}
|
||||
### Pipeline
|
||||
|
||||
Pipeline run: [{{ .PipelineName }}]({{ .PipelineLink }})
|
||||
{{- end}}
|
||||
|
||||
### Detected in
|
||||
|
||||
{{if .Branch}}**Branch:** {{ .Branch }}{{- end}}
|
||||
{{if .CommitID}}**CommitId:** {{ .CommitID }}{{- end}}
|
||||
{{if .DirectDependency}}**Dependency:** {{if (eq .DirectDependency "true")}}direct{{ else }}indirect{{ end }}{{- end}}
|
||||
{{if .ArtifactID}}**ArtifactId:** {{ .ArtifactID }}{{- end}}
|
||||
{{if .Group}}**Group:** {{ .Group }}{{- end}}
|
||||
{{if .Version}}**Version:** {{ .Version }}{{- end}}
|
||||
{{if .PackageURL}}**Package URL:** {{ .PackageURL }}{{- end}}
|
||||
{{if .PublishDate}}**Publishing date:** {{.PublishDate }}{{- end}}
|
||||
|
||||
## Description
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
---
|
||||
|
||||
{{.Footer}}
|
||||
`
|
||||
|
||||
// ToMarkdown creates a vulnerability in markdown format which can be used in GitHub issues
|
||||
func (v *VulnerabilityReport) ToMarkdown() ([]byte, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"date": func(t time.Time) string {
|
||||
return t.Format("2006-01-02")
|
||||
},
|
||||
"title": func(s string) string {
|
||||
caser := cases.Title(language.AmericanEnglish)
|
||||
return caser.String(s)
|
||||
},
|
||||
}
|
||||
|
||||
// only fill with orchestrator information if orchestrator can be identified properly
|
||||
if provider, err := orchestrator.NewOrchestratorSpecificConfigProvider(); err == nil {
|
||||
// only add information if not yet provided
|
||||
if len(v.CommitID) == 0 {
|
||||
v.CommitID = provider.GetCommit()
|
||||
}
|
||||
if len(v.PipelineLink) == 0 {
|
||||
v.PipelineLink = provider.GetJobURL()
|
||||
v.PipelineName = provider.GetJobName()
|
||||
}
|
||||
}
|
||||
|
||||
md := []byte{}
|
||||
tmpl, err := template.New("report").Funcs(funcMap).Parse(vulnerabilityMdTemplate)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to create markdown issue template: %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = tmpl.Execute(buf, v)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to execute markdown issue template: %w", err)
|
||||
}
|
||||
md = buf.Bytes()
|
||||
return md, nil
|
||||
}
|
||||
|
||||
// ScanReport defines the elements of a scan report used by various scan steps
|
||||
type ScanReport struct {
|
||||
StepName string `json:"stepName"`
|
||||
@ -426,10 +327,7 @@ func drawCell(cell ScanCell) string {
|
||||
}
|
||||
|
||||
func shouldDrawTable(table ScanDetailTable) bool {
|
||||
if len(table.Headers) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return len(table.Headers) > 0
|
||||
}
|
||||
|
||||
func drawOverviewRow(row OverviewRow) string {
|
||||
|
@ -1,54 +1,12 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVulToMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("success - empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vulReport := VulnerabilityReport{}
|
||||
_, err := vulReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("success - filled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vulReport := VulnerabilityReport{
|
||||
ArtifactID: "theArtifact",
|
||||
Branch: "main",
|
||||
CommitID: "acb123",
|
||||
Description: "This is the test description.",
|
||||
DirectDependency: "true",
|
||||
Footer: "This is the test footer",
|
||||
Group: "the.group",
|
||||
PipelineName: "thePipelineName",
|
||||
PipelineLink: "https://the.link.to.the.pipeline",
|
||||
PublishDate: "2022-06-30",
|
||||
Resolution: "This is the test resolution.",
|
||||
Score: 7.8,
|
||||
Severity: "high",
|
||||
Version: "1.2.3",
|
||||
PackageURL: "pkg:generic/the.group/theArtifact@1.2.3",
|
||||
VulnerabilityLink: "https://the.link/to/the/vulnerability",
|
||||
VulnerabilityName: "CVE-Test-001",
|
||||
}
|
||||
goldenFilePath := filepath.Join("testdata", "markdownVulnerability.golden")
|
||||
expected, err := ioutil.ReadFile(goldenFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := vulReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(expected), string(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestToHTML(t *testing.T) {
|
||||
t.Run("empty table", func(t *testing.T) {
|
||||
report := ScanReport{
|
||||
|
108
pkg/reporting/securityVulnerability.go
Normal file
108
pkg/reporting/securityVulnerability.go
Normal file
@ -0,0 +1,108 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/SAP/jenkins-library/pkg/orchestrator"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// VulnerabilityReport represents metadata for a report on a vulnerability
|
||||
type VulnerabilityReport struct {
|
||||
ArtifactID string
|
||||
Branch string
|
||||
CommitID string
|
||||
Description string
|
||||
DirectDependency string
|
||||
Footer string
|
||||
Group string
|
||||
PackageURL string
|
||||
PipelineName string
|
||||
PipelineLink string
|
||||
PublishDate string
|
||||
Resolution string
|
||||
Score float64
|
||||
Severity string
|
||||
Version string
|
||||
VulnerabilityLink string
|
||||
VulnerabilityName string
|
||||
}
|
||||
|
||||
const vulnerabilityMdTemplate string = `# {{title .Severity }} ({{ .Score }}) Vulnerability {{ .VulnerabilityName }} - {{ .ArtifactID }}
|
||||
|
||||
**Vulnerability link:** [{{ .VulnerabilityLink }}]({{ .VulnerabilityLink }})
|
||||
|
||||
## Fix
|
||||
|
||||
**{{ .Resolution }}**
|
||||
|
||||
## Context
|
||||
|
||||
{{if .PipelineLink -}}
|
||||
### Pipeline
|
||||
|
||||
Pipeline run: [{{ .PipelineName }}]({{ .PipelineLink }})
|
||||
{{- end}}
|
||||
|
||||
### Detected in
|
||||
|
||||
{{if .Branch}}**Branch:** {{ .Branch }}{{- end}}
|
||||
{{if .CommitID}}**CommitId:** {{ .CommitID }}{{- end}}
|
||||
{{if .DirectDependency}}**Dependency:** {{if (eq .DirectDependency "true")}}direct{{ else }}indirect{{ end }}{{- end}}
|
||||
{{if .ArtifactID}}**ArtifactId:** {{ .ArtifactID }}{{- end}}
|
||||
{{if .Group}}**Group:** {{ .Group }}{{- end}}
|
||||
{{if .Version}}**Version:** {{ .Version }}{{- end}}
|
||||
{{if .PackageURL}}**Package URL:** {{ .PackageURL }}{{- end}}
|
||||
{{if .PublishDate}}**Publishing date:** {{.PublishDate }}{{- end}}
|
||||
|
||||
## Description
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
---
|
||||
|
||||
{{.Footer}}
|
||||
`
|
||||
|
||||
// ToMarkdown creates a vulnerability in markdown format which can be used in GitHub issues
|
||||
func (v *VulnerabilityReport) ToMarkdown() ([]byte, error) {
|
||||
funcMap := template.FuncMap{
|
||||
"date": func(t time.Time) string {
|
||||
return t.Format("2006-01-02")
|
||||
},
|
||||
"title": func(s string) string {
|
||||
caser := cases.Title(language.AmericanEnglish)
|
||||
return caser.String(s)
|
||||
},
|
||||
}
|
||||
|
||||
// only fill with orchestrator information if orchestrator can be identified properly
|
||||
if provider, err := orchestrator.NewOrchestratorSpecificConfigProvider(); err == nil {
|
||||
// only add information if not yet provided
|
||||
if len(v.CommitID) == 0 {
|
||||
v.CommitID = provider.GetCommit()
|
||||
}
|
||||
if len(v.PipelineLink) == 0 {
|
||||
v.PipelineLink = provider.GetJobURL()
|
||||
v.PipelineName = provider.GetJobName()
|
||||
}
|
||||
}
|
||||
|
||||
md := []byte{}
|
||||
tmpl, err := template.New("report").Funcs(funcMap).Parse(vulnerabilityMdTemplate)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to create markdown issue template: %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
err = tmpl.Execute(buf, v)
|
||||
if err != nil {
|
||||
return md, fmt.Errorf("failed to execute markdown issue template: %w", err)
|
||||
}
|
||||
md = buf.Bytes()
|
||||
return md, nil
|
||||
}
|
49
pkg/reporting/securityVulnerability_test.go
Normal file
49
pkg/reporting/securityVulnerability_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVulnerabilityReportToMarkdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("success - empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vulReport := VulnerabilityReport{}
|
||||
_, err := vulReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("success - filled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
vulReport := VulnerabilityReport{
|
||||
ArtifactID: "theArtifact",
|
||||
Branch: "main",
|
||||
CommitID: "acb123",
|
||||
Description: "This is the test description.",
|
||||
DirectDependency: "true",
|
||||
Footer: "This is the test footer",
|
||||
Group: "the.group",
|
||||
PipelineName: "thePipelineName",
|
||||
PipelineLink: "https://the.link.to.the.pipeline",
|
||||
PublishDate: "2022-06-30",
|
||||
Resolution: "This is the test resolution.",
|
||||
Score: 7.8,
|
||||
Severity: "high",
|
||||
Version: "1.2.3",
|
||||
PackageURL: "pkg:generic/the.group/theArtifact@1.2.3",
|
||||
VulnerabilityLink: "https://the.link/to/the/vulnerability",
|
||||
VulnerabilityName: "CVE-Test-001",
|
||||
}
|
||||
goldenFilePath := filepath.Join("testdata", "markdownVulnerability.golden")
|
||||
expected, err := os.ReadFile(goldenFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err := vulReport.ToMarkdown()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(expected), string(res))
|
||||
})
|
||||
}
|
25
pkg/reporting/testdata/markdownPolicyViolation.golden
vendored
Normal file
25
pkg/reporting/testdata/markdownPolicyViolation.golden
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Policy Violation - pkg:generic/the.group/theArtifact@1.2.3
|
||||
|
||||
## Description
|
||||
|
||||
This is the test description.
|
||||
|
||||
## Context
|
||||
|
||||
### Pipeline
|
||||
|
||||
Pipeline run: [thePipelineName](https://the.link.to.the.pipeline)
|
||||
|
||||
### Detected in
|
||||
|
||||
**Branch:** main
|
||||
**CommitId:** acb123
|
||||
**Dependency:** direct
|
||||
**ArtifactId:** theArtifact
|
||||
**Group:** the.group
|
||||
**Version:** 1.2.3
|
||||
**Package URL:** pkg:generic/the.group/theArtifact@1.2.3
|
||||
|
||||
---
|
||||
|
||||
This is the test footer
|
@ -305,7 +305,7 @@ func mvnProjectExcludes(buildDescriptorExcludeList []string, utils Utils) []stri
|
||||
return []string{}
|
||||
}
|
||||
|
||||
//ToDo: Check if we want to optionally allow auto generation for unknown projects
|
||||
// ToDo: Check if we want to optionally allow auto generation for unknown projects
|
||||
func autoGenerateWhitesourceConfig(config *ScanOptions, utils Utils) error {
|
||||
// TODO: Should we rely on -detect, or set the parameters manually?
|
||||
if err := utils.RunExecutable("java", "-jar", config.AgentFileName, "-d", ".", "-detect"); err != nil {
|
||||
|
@ -64,6 +64,8 @@ type Alert struct {
|
||||
func (a *Alert) Title() string {
|
||||
if a.Type == "SECURITY_VULNERABILITY" {
|
||||
return fmt.Sprintf("Security Vulnerability %v %v", a.Vulnerability.Name, a.Library.ArtifactID)
|
||||
} else if a.Type == "REJECTED_BY_POLICY_RESOURCE" {
|
||||
return fmt.Sprintf("Policy Violation %v %v", a.Vulnerability.Name, a.Library.ArtifactID)
|
||||
}
|
||||
return fmt.Sprintf("%v %v %v ", a.Type, a.Vulnerability.Name, a.Library.ArtifactID)
|
||||
}
|
||||
@ -144,32 +146,54 @@ func consolidate(cvss2severity, cvss3severity string, cvss2score, cvss3score flo
|
||||
|
||||
// ToMarkdown returns the markdown representation of the contents
|
||||
func (a *Alert) ToMarkdown() ([]byte, error) {
|
||||
score := consolidateScores(a.Vulnerability.Score, a.Vulnerability.CVSS3Score)
|
||||
|
||||
vul := reporting.VulnerabilityReport{
|
||||
ArtifactID: a.Library.ArtifactID,
|
||||
// no information available about branch and commit, yet
|
||||
Branch: "",
|
||||
CommitID: "",
|
||||
Description: a.Vulnerability.Description,
|
||||
DirectDependency: fmt.Sprint(a.DirectDependency),
|
||||
// no information available about footer, yet
|
||||
Footer: "",
|
||||
Group: a.Library.GroupID,
|
||||
// no information available about pipeline name and link, yet
|
||||
PipelineName: "",
|
||||
PipelineLink: "",
|
||||
PublishDate: a.Vulnerability.PublishDate,
|
||||
Resolution: a.Vulnerability.TopFix.FixResolution,
|
||||
Score: score,
|
||||
Severity: consolidate(a.Vulnerability.Severity, a.Vulnerability.CVSS3Severity, a.Vulnerability.Score, a.Vulnerability.CVSS3Score),
|
||||
Version: a.Library.Version,
|
||||
PackageURL: a.Library.ToPackageUrl().ToString(),
|
||||
VulnerabilityLink: a.Vulnerability.URL,
|
||||
VulnerabilityName: a.Vulnerability.Name,
|
||||
if a.Type == "SECURITY_VULNERABILITY" {
|
||||
score := consolidateScores(a.Vulnerability.Score, a.Vulnerability.CVSS3Score)
|
||||
|
||||
vul := reporting.VulnerabilityReport{
|
||||
ArtifactID: a.Library.ArtifactID,
|
||||
// no information available about branch and commit, yet
|
||||
Branch: "",
|
||||
CommitID: "",
|
||||
Description: a.Vulnerability.Description,
|
||||
DirectDependency: fmt.Sprint(a.DirectDependency),
|
||||
// no information available about footer, yet
|
||||
Footer: "",
|
||||
Group: a.Library.GroupID,
|
||||
// no information available about pipeline name and link, yet
|
||||
PipelineName: "",
|
||||
PipelineLink: "",
|
||||
PublishDate: a.Vulnerability.PublishDate,
|
||||
Resolution: a.Vulnerability.TopFix.FixResolution,
|
||||
Score: score,
|
||||
Severity: consolidate(a.Vulnerability.Severity, a.Vulnerability.CVSS3Severity, a.Vulnerability.Score, a.Vulnerability.CVSS3Score),
|
||||
Version: a.Library.Version,
|
||||
PackageURL: a.Library.ToPackageUrl().ToString(),
|
||||
VulnerabilityLink: a.Vulnerability.URL,
|
||||
VulnerabilityName: a.Vulnerability.Name,
|
||||
}
|
||||
return vul.ToMarkdown()
|
||||
} else if a.Type == "REJECTED_BY_POLICY_RESOURCE" {
|
||||
policyReport := reporting.PolicyViolationReport{
|
||||
ArtifactID: a.Library.ArtifactID,
|
||||
// no information available about branch and commit, yet
|
||||
Branch: "",
|
||||
CommitID: "",
|
||||
Description: a.Vulnerability.Description,
|
||||
DirectDependency: fmt.Sprint(a.DirectDependency),
|
||||
// no information available about footer, yet
|
||||
Footer: "",
|
||||
Group: a.Library.GroupID,
|
||||
// no information available about pipeline name and link, yet
|
||||
PipelineName: "",
|
||||
PipelineLink: "",
|
||||
Version: a.Library.Version,
|
||||
PackageURL: a.Library.ToPackageUrl().ToString(),
|
||||
}
|
||||
return policyReport.ToMarkdown()
|
||||
}
|
||||
|
||||
return vul.ToMarkdown()
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
// ToTxt returns the textual representation of the contents
|
||||
|
Loading…
x
Reference in New Issue
Block a user