1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00

detectExecuteScan : Policy reports in HTML, JSON and for cumulus (#3057)

* add policy status reports

* add policy status and cumulus json

* update projectver link + test

Co-authored-by: Sven Merk <33895725+nevskrem@users.noreply.github.com>
This commit is contained in:
Giridhar Shenoy 2021-09-07 17:17:03 +02:00 committed by GitHub
parent 2997714a02
commit b92e7f699c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 333 additions and 56 deletions

View File

@ -150,31 +150,6 @@ func runDetect(config detectExecuteScanOptions, utils detectUtils, influx *detec
if reportingErr != nil {
log.Entry().Warnf("Failed to generate reports: %v", reportingErr)
}
if err == nil && piperutils.ContainsString(config.FailOn, "BLOCKER") {
violations := struct {
PolicyViolations int `json:"policyViolations"`
Reports []string `json:"reports"`
}{
PolicyViolations: 0,
Reports: []string{},
}
if files, err := utils.Glob("**/*BlackDuck_RiskReport.pdf"); err == nil && len(files) > 0 {
// there should only be one RiskReport thus only taking the first one
_, reportFile := filepath.Split(files[0])
violations.Reports = append(violations.Reports, reportFile)
}
violationContent, err := json.Marshal(violations)
if err != nil {
return fmt.Errorf("failed to marshal policy violation data: %w", err)
}
err = utils.FileWrite("blackduck-ip.json", violationContent, 0666)
if err != nil {
return fmt.Errorf("failed to write policy violation report: %w", err)
}
}
return err
}
@ -292,12 +267,22 @@ func postScanChecksAndReporting(config detectExecuteScanOptions, influx *detectE
if err != nil {
return err
}
scanReport := createVulnerabilityReport(config, vulns, influx)
scanReport := createVulnerabilityReport(config, vulns, influx, sys)
paths, err := writeVulnerabilityReports(scanReport, config, utils)
policyStatus, err := getPolicyStatus(config, influx, sys)
policyReport := createPolicyStatusReport(config, policyStatus, influx, sys)
policyReportPaths, err := writePolicyStatusReports(policyReport, config, utils)
paths = append(paths, policyReportPaths...)
piperutils.PersistReportsAndLinks("detectExecuteScan", "", paths, nil)
if err != nil {
return errors.Wrapf(err, "failed to check and report scan results")
}
policyJsonErr := writeIpPolicyJson(config, utils, paths, sys)
if policyJsonErr != nil {
return errors.Wrapf(policyJsonErr, "failed to write IP policy violations json file")
}
return nil
}
@ -331,12 +316,14 @@ func getVulnsAndComponents(config detectExecuteScanOptions, influx *detectExecut
return vulns, components, nil
}
func createVulnerabilityReport(config detectExecuteScanOptions, vulns *bd.Vulnerabilities, influx *detectExecuteScanInflux) reporting.ScanReport {
func createVulnerabilityReport(config detectExecuteScanOptions, vulns *bd.Vulnerabilities, influx *detectExecuteScanInflux, sys *blackduckSystem) reporting.ScanReport {
versionName := getVersionName(config)
versionUrl, _ := sys.Client.GetProjectVersionLink(config.ProjectName, versionName)
scanReport := reporting.ScanReport{
Title: "BlackDuck Security Vulnerability Report",
Subheaders: []reporting.Subheader{
{Description: "BlackDuck Project Name ", Details: config.ProjectName},
{Description: "BlackDuck Project Version ", Details: getVersionName(config)},
{Description: "BlackDuck Project Version ", Details: fmt.Sprintf("<a href='%v'>%v</a>", versionUrl, versionName)},
},
Overview: []reporting.OverviewRow{
{Description: "Total number of vulnerabilities ", Details: fmt.Sprint(influx.detect_data.fields.vulnerabilities)},
@ -418,6 +405,138 @@ func writeVulnerabilityReports(scanReport reporting.ScanReport, config detectExe
return reportPaths, nil
}
func getPolicyStatus(config detectExecuteScanOptions, influx *detectExecuteScanInflux, sys *blackduckSystem) (*bd.PolicyStatus, error) {
policyStatus, err := sys.Client.GetPolicyStatus(config.ProjectName, getVersionName(config))
if err != nil {
return nil, err
}
totalViolations := 0
for _, level := range policyStatus.SeverityLevels {
totalViolations += level.Value
}
influx.detect_data.fields.policy_violations = totalViolations
return policyStatus, nil
}
func createPolicyStatusReport(config detectExecuteScanOptions, policyStatus *bd.PolicyStatus, influx *detectExecuteScanInflux, sys *blackduckSystem) reporting.ScanReport {
versionName := getVersionName(config)
versionUrl, _ := sys.Client.GetProjectVersionLink(config.ProjectName, versionName)
policyReport := reporting.ScanReport{
Title: "BlackDuck Policy Violations Report",
Subheaders: []reporting.Subheader{
{Description: "BlackDuck project name ", Details: config.ProjectName},
{Description: "BlackDuck project version name", Details: fmt.Sprintf("<a href='%v'>%v</a>", versionUrl, versionName)},
},
Overview: []reporting.OverviewRow{
{Description: "Overall Policy Violation Status", Details: policyStatus.OverallStatus},
{Description: "Total Number of Policy Vioaltions", Details: fmt.Sprint(influx.detect_data.fields.policy_violations)},
},
SuccessfulScan: influx.detect_data.fields.policy_violations > 0,
ReportTime: time.Now(),
}
detailTable := reporting.ScanDetailTable{
Headers: []string{
"Policy Severity Level", "Number of Components in Violation",
},
WithCounter: false,
}
for _, level := range policyStatus.SeverityLevels {
row := reporting.ScanRow{}
row.AddColumn(level.Name, 0)
row.AddColumn(level.Value, 0)
detailTable.Rows = append(detailTable.Rows, row)
}
policyReport.DetailTable = detailTable
return policyReport
}
func writePolicyStatusReports(scanReport reporting.ScanReport, config detectExecuteScanOptions, utils detectUtils) ([]piperutils.Path, error) {
reportPaths := []piperutils.Path{}
htmlReport, _ := scanReport.ToHTML()
htmlReportPath := "piper_detect_policy_violation_report.html"
if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return reportPaths, errors.Wrapf(err, "failed to write html report")
}
reportPaths = append(reportPaths, piperutils.Path{Name: "BlackDuck Policy Violation Report", Target: htmlReportPath})
jsonReport, _ := scanReport.ToJSON()
if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists {
err := utils.MkdirAll(reporting.StepReportDirectory, 0777)
if err != nil {
return reportPaths, errors.Wrap(err, "failed to create reporting directory")
}
}
if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("detectExecuteScan_policy_%v.json", fmt.Sprintf("%v", time.Now()))), jsonReport, 0666); err != nil {
return reportPaths, errors.Wrapf(err, "failed to write json report")
}
return reportPaths, nil
}
func writeIpPolicyJson(config detectExecuteScanOptions, utils detectUtils, paths []piperutils.Path, sys *blackduckSystem) error {
components, err := sys.Client.GetComponentsWithLicensePolicyRule(config.ProjectName, getVersionName(config))
if err != nil {
errors.Wrapf(err, "failed to get License Policy Violations")
return err
}
violationCount := getActivePolicyViolations(components)
violations := struct {
PolicyViolations int `json:"policyViolations"`
Reports []string `json:"reports"`
}{
PolicyViolations: violationCount,
Reports: []string{},
}
for _, path := range paths {
violations.Reports = append(violations.Reports, path.Target)
}
if files, err := utils.Glob("**/*BlackDuck_RiskReport.pdf"); err == nil && len(files) > 0 {
// there should only be one RiskReport thus only taking the first one
_, reportFile := filepath.Split(files[0])
violations.Reports = append(violations.Reports, reportFile)
}
violationContent, err := json.Marshal(violations)
if err != nil {
return fmt.Errorf("failed to marshal policy violation data: %w", err)
}
err = utils.FileWrite("blackduck-ip.json", violationContent, 0666)
if err != nil {
return fmt.Errorf("failed to write policy violation report: %w", err)
}
return nil
}
func getActivePolicyViolations(components *bd.Components) int {
if components.TotalCount == 0 {
return 0
}
activeViolations := 0
for _, component := range components.Items {
if isActivePolicyViolation(component.PolicyStatus) {
activeViolations++
}
}
return activeViolations
}
func isActivePolicyViolation(status string) bool {
if status == "IN_VIOLATION" {
return true
}
return false
}
func isActiveVulnerability(v bd.Vulnerability) bool {
switch v.VulnerabilityWithRemediation.RemediationStatus {
case "NEW":

View File

@ -54,11 +54,13 @@ func (c *httpMockClient) SendRequest(method, url string, body io.Reader, header
func newBlackduckMockSystem(config detectExecuteScanOptions) blackduckSystem {
myTestClient := httpMockClient{
responseBodyForURL: map[string]string{
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/components?limit=999&offset=0": componentsContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/vunlerable-bom-components?limit=999&offset=0": vulnerabilitiesContent,
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/components?limit=999&offset=0": componentsContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/vunlerable-bom-components?limit=999&offset=0": vulnerabilitiesContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/components?filter=policyCategory%3Alicense&limit=999&offset=0": componentsContent,
"https://my.blackduck.system/api/projects/5ca86e11/versions/a6c94786/policy-status": policyStatusContent,
},
header: map[string]http.Header{},
}
@ -120,10 +122,12 @@ const (
"items" : [
{
"componentName": "Spring Framework",
"componentVersionName": "5.3.9"
"componentVersionName": "5.3.9",
"policyStatus": "IN_VIOLATION"
}, {
"componentName": "Apache Tomcat",
"componentVersionName": "9.0.52"
"componentVersionName": "9.0.52",
"policyStatus": "IN_VIOLATION"
}
]
}`
@ -144,6 +148,13 @@ const (
}
]
}`
policyStatusContent = `{
"overallStatus": "IN_VIOLATION",
"componentVersionPolicyViolationDetails": {
"name": "IN_VIOLATION",
"severityLevels": [{"name":"BLOCKER", "value": 1}, {"name": "CRITICAL", "value": 1}]
}
}`
)
func (c *detectTestUtilsBundle) RunExecutable(string, ...string) error {
@ -191,27 +202,28 @@ func TestRunDetect(t *testing.T) {
expectedScript := "./detect.sh --blackduck.url= --blackduck.api.token= \"--detect.project.name=''\" \"--detect.project.version.name=''\" \"--detect.code.location.name=''\" --detect.source.path='.'"
assert.Equal(t, expectedScript, utilsMock.Calls[0])
})
//Creation of report is covered in the test case for postScanChecks
/*
t.Run("success case - with report", func(t *testing.T) {
t.Parallel()
utilsMock := newDetectTestUtilsBundle()
utilsMock.AddFile("detect.sh", []byte(""))
utilsMock.AddFile("my_BlackDuck_RiskReport.pdf", []byte(""))
err := runDetect(detectExecuteScanOptions{FailOn: []string{"BLOCKER"}}, utilsMock, &detectExecuteScanInflux{})
t.Run("success case - with report", func(t *testing.T) {
t.Parallel()
utilsMock := newDetectTestUtilsBundle()
utilsMock.AddFile("detect.sh", []byte(""))
utilsMock.AddFile("my_BlackDuck_RiskReport.pdf", []byte(""))
err := runDetect(detectExecuteScanOptions{FailOn: []string{"BLOCKER"}}, utilsMock, &detectExecuteScanInflux{})
assert.Equal(t, utilsMock.downloadedFiles["https://detect.synopsys.com/detect.sh"], "detect.sh")
assert.True(t, utilsMock.HasRemovedFile("detect.sh"))
assert.NoError(t, err)
assert.Equal(t, ".", utilsMock.Dir, "Wrong execution directory used")
assert.Equal(t, "/bin/bash", utilsMock.Shell[0], "Bash shell expected")
expectedScript := "./detect.sh --blackduck.url= --blackduck.api.token= \"--detect.project.name=''\" \"--detect.project.version.name=''\" --detect.policy.check.fail.on.severities=BLOCKER \"--detect.code.location.name=''\" --detect.source.path='.'"
assert.Equal(t, expectedScript, utilsMock.Calls[0])
content, err := utilsMock.FileRead("blackduck-ip.json")
assert.NoError(t, err)
assert.Contains(t, string(content), `"policyViolations":0`)
})
assert.Equal(t, utilsMock.downloadedFiles["https://detect.synopsys.com/detect.sh"], "detect.sh")
assert.True(t, utilsMock.HasRemovedFile("detect.sh"))
assert.NoError(t, err)
assert.Equal(t, ".", utilsMock.Dir, "Wrong execution directory used")
assert.Equal(t, "/bin/bash", utilsMock.Shell[0], "Bash shell expected")
expectedScript := "./detect.sh --blackduck.url= --blackduck.api.token= \"--detect.project.name=''\" \"--detect.project.version.name=''\" --detect.policy.check.fail.on.severities=BLOCKER \"--detect.code.location.name=''\" --detect.source.path='.'"
assert.Equal(t, expectedScript, utilsMock.Calls[0])
content, err := utilsMock.FileRead("blackduck-ip.json")
assert.NoError(t, err)
assert.Contains(t, string(content), `"policyViolations":0`)
})
*/
t.Run("failure case", func(t *testing.T) {
t.Parallel()
utilsMock := newDetectTestUtilsBundle()
@ -567,6 +579,9 @@ func TestPostScanChecksAndReporting(t *testing.T) {
err := postScanChecksAndReporting(config, &detectExecuteScanInflux{}, utils, &sys)
assert.NoError(t, err)
content, err := utils.FileRead("blackduck-ip.json")
assert.NoError(t, err)
assert.Contains(t, string(content), `"policyViolations":2`)
})
}

View File

@ -62,8 +62,10 @@ type Components struct {
}
type Component struct {
Name string `json:"componentName,omitempty"`
Version string `json:"componentVersionName,omitempty"`
Name string `json:"componentName,omitempty"`
Version string `json:"componentVersionName,omitempty"`
PolicyStatus string `json:"policyStatus,omitempty"`
Metadata `json:"_meta,omitempty"`
}
type Vulnerabilities struct {
@ -193,6 +195,17 @@ func (b *Client) GetProjectVersion(projectName, projectVersion string) (*Project
return nil, fmt.Errorf("failed to get project version '%v'", projectVersion)
}
func (b *Client) GetProjectVersionLink(projectName, versionName string) (string, error) {
projectVersion, err := b.GetProjectVersion(projectName, versionName)
if err != nil {
return "", err
}
if projectVersion != nil {
return projectVersion.Href, nil
}
return "", nil
}
func (b *Client) GetComponents(projectName, versionName string) (*Components, error) {
projectVersion, err := b.GetProjectVersion(projectName, versionName)
if err != nil {
@ -227,6 +240,52 @@ func (b *Client) GetComponents(projectName, versionName string) (*Components, er
//Just return the components, the details of the components are not necessary
return &components, nil
}
func (b *Client) GetComponentsWithLicensePolicyRule(projectName, versionName string) (*Components, error) {
projectVersion, err := b.GetProjectVersion(projectName, versionName)
if err != nil {
return nil, err
}
headers := http.Header{}
headers.Add("Accept", HEADER_BOM_V6)
var componentsPath string
for _, link := range projectVersion.Links {
if link.Rel == "components" {
componentsPath = urlPath(link.Href)
break
}
}
respBody, err := b.sendRequest("GET", componentsPath, map[string]string{"offset": "0", "limit": "999", "filter": "policyCategory:license"}, nil, headers)
if err != nil {
return nil, errors.Wrapf(err, "Failed to get components list for project version '%v:%v'", projectName, versionName)
}
components := Components{}
err = json.Unmarshal(respBody, &components)
if err != nil {
return nil, errors.Wrapf(err, "failed to retrieve component details for project version '%v:%v'", projectName, versionName)
}
//Just return the components, the details of the components are not necessary
return &components, nil
}
// func (b *Client) GetComponentPolicyStatus(component Component) (ComponentPolicyStatus, error) {
// var policyStatusUrl string
// for _, link := range component.Links {
// if link.Rel == "policy-status" {
// policyStatusUrl = urlPath(link.Href)
// }
// }
// headers := http.Header{}
// headers.Add("Accept", HEADER_BOM_V6)
// respBody, err := b.sendRequest("GET", policyStatusUrl, map[string]string{}, nil, headers)
// }
func (b *Client) GetVulnerabilities(projectName, versionName string) (*Vulnerabilities, error) {
projectVersion, err := b.GetProjectVersion(projectName, versionName)

View File

@ -362,6 +362,73 @@ func TestGetComponents(t *testing.T) {
})
}
func TestGetComponentsWithLicensePolicyRule(t *testing.T) {
t.Run("success", func(t *testing.T) {
myTestClient := httpMockClient{
responseBodyForURL: map[string]string{
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions/a6c94786-0ee6-414f-9054-90d549c69c36/components?filter=policyCategory%3Alicense&limit=999&offset=0": `{
"totalCount": 2,
"items" : [
{
"componentName": "Spring Framework",
"componentVersionName": "5.3.9",
"policyStatus": "IN_VIOLATION"
}, {
"componentName": "Apache Tomcat",
"componentVersionName": "9.0.52",
"policyStatus": "NOT_IN_VIOLATION"
}
]
}`,
},
header: map[string]http.Header{},
}
bdClient := NewClient("token", "https://my.blackduck.system", &myTestClient)
components, err := bdClient.GetComponentsWithLicensePolicyRule("SHC-PiperTest", "1.0")
assert.NoError(t, err)
assert.GreaterOrEqual(t, components.TotalCount, 2)
assert.Equal(t, components.Items[0].PolicyStatus, "IN_VIOLATION")
})
t.Run("Failure - 0 components", func(t *testing.T) {
myTestClient := httpMockClient{
responseBodyForURL: map[string]string{
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions/a6c94786-0ee6-414f-9054-90d549c69c36/components?filter=policyCategory%3Alicense&limit=999&offset=0": `{
"totalCount": 0,
"items" : []}`,
},
header: map[string]http.Header{},
}
bdClient := NewClient("token", "https://my.blackduck.system", &myTestClient)
components, err := bdClient.GetComponentsWithLicensePolicyRule("SHC-PiperTest", "1.0")
assert.NoError(t, err)
assert.NotNil(t, components)
assert.Equal(t, components.TotalCount, 0)
})
t.Run("Failure - unmarshalling", func(t *testing.T) {
myTestClient := httpMockClient{
responseBodyForURL: map[string]string{
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions/a6c94786-0ee6-414f-9054-90d549c69c36/components?filter=policyCategory%3Alicense&limit=999&offset=0": "",
},
header: map[string]http.Header{},
}
bdClient := NewClient("token", "https://my.blackduck.system", &myTestClient)
components, err := bdClient.GetComponentsWithLicensePolicyRule("SHC-PiperTest", "1.0")
assert.Contains(t, fmt.Sprint(err), "failed to retrieve component details for project version 'SHC-PiperTest:1.0'")
assert.Nilf(t, components, "Expected Components to be nil")
})
}
func TestGetPolicyStatus(t *testing.T) {
t.Run("success", func(t *testing.T) {
myTestClient := httpMockClient{
@ -412,6 +479,23 @@ func TestGetPolicyStatus(t *testing.T) {
})
}
func TestGetProjectVersionLink(t *testing.T) {
t.Run("Success Case", func(t *testing.T) {
myTestClient := httpMockClient{
responseBodyForURL: map[string]string{
"https://my.blackduck.system/api/tokens/authenticate": authContent,
"https://my.blackduck.system/api/projects?q=name%3ASHC-PiperTest": projectContent,
"https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions": projectVersionContent,
},
header: map[string]http.Header{},
}
bdClient := NewClient("token", "https://my.blackduck.system", &myTestClient)
link, err := bdClient.GetProjectVersionLink("SHC-PiperTest", "1.0")
assert.NoError(t, err)
assert.Equal(t, link, "https://my.blackduck.system/api/projects/5ca86e11-1983-4e7b-97d4-eb1a0aeffbbf/versions/a6c94786-0ee6-414f-9054-90d549c69c36")
})
}
func TestAuthenticate(t *testing.T) {
t.Run("success", func(t *testing.T) {
myTestClient := httpMockClient{