From f4fbf0f1ed2a64b4379b2df484434e5376b332a4 Mon Sep 17 00:00:00 2001 From: Akramdzhon Azamov <58902906+akram8008@users.noreply.github.com> Date: Thu, 2 Mar 2023 15:04:21 +0500 Subject: [PATCH] feat(detectExecuteScan) execution of rapid scans (#4211) Co-authored-by: akram8008 <900658008.akram@email.com> Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> Co-authored-by: Andrei Kireev Co-authored-by: ffeldmann Co-authored-by: sumeet patil --- cmd/detectExecuteScan.go | 77 +++- cmd/detectExecuteScan_test.go | 149 ++++++- documentation/docs/steps/detectExecuteScan.md | 2 +- pkg/reporting/pullRequestReport.go | 420 ++++++++++++++++++ pkg/reporting/pullRequestReport_test.go | 249 +++++++++++ 5 files changed, 873 insertions(+), 24 deletions(-) create mode 100644 pkg/reporting/pullRequestReport.go create mode 100644 pkg/reporting/pullRequestReport_test.go diff --git a/cmd/detectExecuteScan.go b/cmd/detectExecuteScan.go index 2a008b4ab..7a481cae6 100644 --- a/cmd/detectExecuteScan.go +++ b/cmd/detectExecuteScan.go @@ -14,20 +14,20 @@ import ( "time" bd "github.com/SAP/jenkins-library/pkg/blackduck" + "github.com/SAP/jenkins-library/pkg/command" piperGithub "github.com/SAP/jenkins-library/pkg/github" piperhttp "github.com/SAP/jenkins-library/pkg/http" - "github.com/SAP/jenkins-library/pkg/maven" - "github.com/SAP/jenkins-library/pkg/reporting" - "github.com/SAP/jenkins-library/pkg/versioning" - "github.com/pkg/errors" - - "github.com/SAP/jenkins-library/pkg/command" "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/maven" + "github.com/SAP/jenkins-library/pkg/orchestrator" "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/SAP/jenkins-library/pkg/reporting" "github.com/SAP/jenkins-library/pkg/telemetry" "github.com/SAP/jenkins-library/pkg/toolrecord" + "github.com/SAP/jenkins-library/pkg/versioning" "github.com/google/go-github/v45/github" + "github.com/pkg/errors" ) type detectUtils interface { @@ -46,14 +46,16 @@ type detectUtils interface { GetIssueService() *github.IssuesService GetSearchService() *github.SearchService + GetProvider() orchestrator.OrchestratorSpecificConfigProviding } type detectUtilsBundle struct { *command.Command *piperutils.Files *piperhttp.Client - issues *github.IssuesService - search *github.SearchService + issues *github.IssuesService + search *github.SearchService + provider orchestrator.OrchestratorSpecificConfigProviding } func (d *detectUtilsBundle) GetIssueService() *github.IssuesService { @@ -64,6 +66,10 @@ func (d *detectUtilsBundle) GetSearchService() *github.SearchService { return d.search } +func (d *detectUtilsBundle) GetProvider() orchestrator.OrchestratorSpecificConfigProviding { + return d.provider +} + type blackduckSystem struct { Client bd.Client } @@ -104,6 +110,15 @@ func newDetectUtils(client *github.Client) detectUtils { } utils.Stdout(log.Writer()) utils.Stderr(log.Writer()) + + provider, err := orchestrator.NewOrchestratorSpecificConfigProvider() + if err != nil { + log.Entry().WithError(err).Warning(err) + provider = &orchestrator.UnknownOrchestratorConfigProvider{} + } + + utils.provider = provider + return &utils } @@ -159,8 +174,10 @@ func runDetect(ctx context.Context, config detectExecuteScanOptions, utils detec } } + blackduckSystem := newBlackduckSystem(config) + args := []string{"./detect.sh"} - args, err = addDetectArgs(args, config, utils) + args, err = addDetectArgs(args, config, utils, blackduckSystem) if err != nil { return err } @@ -173,7 +190,6 @@ func runDetect(ctx context.Context, config detectExecuteScanOptions, utils detec utils.SetEnv(envs) err = utils.RunShell("/bin/bash", script) - blackduckSystem := newBlackduckSystem(config) reportingErr := postScanChecksAndReporting(ctx, config, influx, utils, blackduckSystem) if reportingErr != nil { if strings.Contains(reportingErr.Error(), "License Policy Violations found") { @@ -295,7 +311,7 @@ func getDetectScript(config detectExecuteScanOptions, utils detectUtils) error { return utils.DownloadFile("https://detect.synopsys.com/detect7.sh", "detect.sh", nil, nil) } -func addDetectArgs(args []string, config detectExecuteScanOptions, utils detectUtils) ([]string, error) { +func addDetectArgs(args []string, config detectExecuteScanOptions, utils detectUtils, sys *blackduckSystem) ([]string, error) { detectVersionName := getVersionName(config) // Split on spaces, the scanPropeties, so that each property is available as a single string // instead of all properties being part of a single string @@ -390,6 +406,18 @@ func addDetectArgs(args []string, config detectExecuteScanOptions, utils detectU args = append(args, fmt.Sprintf("\"--detect.maven.build.command='%v'\"", strings.Join(mavenArgs, " "))) } + // rapid scan on pull request + if utils.GetProvider().IsPullRequest() { + log.Entry().Debug("pull request detected") + args = append(args, "--detect.blackduck.scan.mode='RAPID'") + _, err := sys.Client.GetProjectVersion(config.ProjectName, config.Version) + if err == nil { + args = append(args, "--detect.blackduck.rapid.compare.mode='BOM_COMPARE_STRICT'") + } + args = append(args, "--detect.cleanup=false") + args = append(args, "--detect.output.path='report'") + } + return args, nil } @@ -498,6 +526,33 @@ func isMajorVulnerability(v bd.Vulnerability) bool { } func postScanChecksAndReporting(ctx context.Context, config detectExecuteScanOptions, influx *detectExecuteScanInflux, utils detectUtils, sys *blackduckSystem) error { + + if utils.GetProvider().IsPullRequest() { + issueNumber, err := strconv.Atoi(utils.GetProvider().GetPullRequestConfig().Key) + if err != nil { + log.Entry().Warning("Can not get issue number ", err) + return nil + } + commentBody, err := reporting.RapidScanResult("./report") + if err != nil { + log.Entry().Warning("Couldn't read file of report of rapid scan, error: ", err) + return nil + } + _, _, err = utils.GetIssueService().CreateComment(ctx, + config.Owner, + config.Repository, + issueNumber, + &github.IssueComment{ + Body: &commentBody, + }) + if err != nil { + log.Entry().Warning("Can send request to github ", err) + return nil + } + + return nil + } + errorsOccured := []string{} vulns, err := getVulnerabilitiesWithComponents(config, influx, sys) if err != nil { diff --git a/cmd/detectExecuteScan_test.go b/cmd/detectExecuteScan_test.go index 9d9b2c84a..f745325a7 100644 --- a/cmd/detectExecuteScan_test.go +++ b/cmd/detectExecuteScan_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "io/ioutil" "net/http" @@ -15,6 +16,7 @@ import ( piperGithub "github.com/SAP/jenkins-library/pkg/github" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/mock" + "github.com/SAP/jenkins-library/pkg/orchestrator" "github.com/google/go-github/v45/github" "github.com/stretchr/testify/assert" @@ -25,7 +27,12 @@ type detectTestUtilsBundle struct { downloadedFiles map[string]string // src, dest *mock.ShellMockRunner *mock.FilesMock - customEnv []string + customEnv []string + orchestrator *orchestratorConfigProviderMock +} + +func (d *detectTestUtilsBundle) GetProvider() orchestrator.OrchestratorSpecificConfigProviding { + return d.orchestrator } func (d *detectTestUtilsBundle) GetIssueService() *github.IssuesService { @@ -36,6 +43,15 @@ func (d *detectTestUtilsBundle) GetSearchService() *github.SearchService { return nil } +type orchestratorConfigProviderMock struct { + orchestrator.UnknownOrchestratorConfigProvider + isPullRequest bool +} + +func (o *orchestratorConfigProviderMock) IsPullRequest() bool { + return o.isPullRequest +} + type httpMockClient struct { responseBodyForURL map[string]string errorMessageForURL map[string]string @@ -73,6 +89,8 @@ func newBlackduckMockSystem(config detectExecuteScanOptions) blackduckSystem { "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, + "https://my.blackduck.system/api/projects?q=name%3ARapid_scan_on_PRs": projectContentRapidScan, + "https://my.blackduck.system/api/projects/654ggfdgf-1983-4e7b-97d4-eb1a0aeffbbf/versions?limit=100&offset=0": projectVersionContentRapid, }, header: map[string]http.Header{}, } @@ -193,6 +211,48 @@ const ( "severityLevels": [{"name":"BLOCKER", "value": 1}, {"name": "CRITICAL", "value": 1}] } }` + projectContentRapidScan = `{ + "totalCount": 1, + "items": [ + { + "name": "Rapid_scan_on_PRs", + "_meta": { + "href": "https://my.blackduck.system/api/projects/654ggfdgf-1983-4e7b-97d4-eb1a0aeffbbf", + "links": [ + { + "rel": "versions", + "href": "https://my.blackduck.system/api/projects/654ggfdgf-1983-4e7b-97d4-eb1a0aeffbbf/versions" + } + ] + } + } + ] + }` + projectVersionContentRapid = `{ + "totalCount": 1, + "items": [ + { + "versionName": "1.0", + "_meta": { + "href": "https://my.blackduck.system/api/projects/654ggfdgf-1983-4e7b-97d4-eb1a0aeffbbf/versions/54357fds-0ee6-414f-9054-90d549c69c36", + "links": [ + { + "rel": "components", + "href": "https://my.blackduck.system/api/projects/5ca86e11/versions/654784382/components" + }, + { + "rel": "vulnerable-components", + "href": "https://my.blackduck.system/api/projects/5ca86e11/versions/654784382/vunlerable-bom-components" + }, + { + "rel": "policy-status", + "href": "https://my.blackduck.system/api/projects/5ca86e11/versions/654784382/policy-status" + } + ] + } + } + ] + }` ) func (c *detectTestUtilsBundle) RunExecutable(string, ...string) error { @@ -222,10 +282,11 @@ func (w *detectTestUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.Cr return nil } -func newDetectTestUtilsBundle() *detectTestUtilsBundle { +func newDetectTestUtilsBundle(isPullRequest bool) *detectTestUtilsBundle { utilsBundle := detectTestUtilsBundle{ ShellMockRunner: &mock.ShellMockRunner{}, FilesMock: &mock.FilesMock{}, + orchestrator: &orchestratorConfigProviderMock{isPullRequest: isPullRequest}, } return &utilsBundle } @@ -235,7 +296,7 @@ func TestRunDetect(t *testing.T) { t.Run("success case", func(t *testing.T) { t.Parallel() ctx := context.Background() - utilsMock := newDetectTestUtilsBundle() + utilsMock := newDetectTestUtilsBundle(false) utilsMock.AddFile("detect.sh", []byte("")) err := runDetect(ctx, detectExecuteScanOptions{}, utilsMock, &detectExecuteScanInflux{}) @@ -251,7 +312,7 @@ func TestRunDetect(t *testing.T) { t.Run("success case detect 6", func(t *testing.T) { t.Parallel() ctx := context.Background() - utilsMock := newDetectTestUtilsBundle() + utilsMock := newDetectTestUtilsBundle(false) utilsMock.AddFile("detect.sh", []byte("")) options := detectExecuteScanOptions{ CustomEnvironmentVariables: []string{"DETECT_LATEST_RELEASE_VERSION=6.8.0"}, @@ -270,7 +331,7 @@ func TestRunDetect(t *testing.T) { t.Run("success case detect 6 from OS env", func(t *testing.T) { t.Parallel() ctx := context.Background() - utilsMock := newDetectTestUtilsBundle() + utilsMock := newDetectTestUtilsBundle(false) utilsMock.AddFile("detect.sh", []byte("")) utilsMock.customEnv = []string{"DETECT_LATEST_RELEASE_VERSION=6.8.0"} err := runDetect(ctx, detectExecuteScanOptions{}, utilsMock, &detectExecuteScanInflux{}) @@ -287,7 +348,7 @@ func TestRunDetect(t *testing.T) { t.Run("failure case", func(t *testing.T) { t.Parallel() ctx := context.Background() - utilsMock := newDetectTestUtilsBundle() + utilsMock := newDetectTestUtilsBundle(false) utilsMock.ShouldFailOnCommand = map[string]error{"./detect.sh --blackduck.url= --blackduck.api.token= \"--detect.project.name=''\" \"--detect.project.version.name=''\" \"--detect.code.location.name=''\" --detect.source.path='.'": fmt.Errorf("")} utilsMock.ExitCode = 3 utilsMock.AddFile("detect.sh", []byte("")) @@ -300,7 +361,7 @@ func TestRunDetect(t *testing.T) { t.Run("maven parameters", func(t *testing.T) { t.Parallel() ctx := context.Background() - utilsMock := newDetectTestUtilsBundle() + utilsMock := newDetectTestUtilsBundle(false) utilsMock.CurrentDir = "root_folder" utilsMock.AddFile("detect.sh", []byte("")) err := runDetect(ctx, detectExecuteScanOptions{ @@ -322,9 +383,10 @@ func TestRunDetect(t *testing.T) { func TestAddDetectArgs(t *testing.T) { t.Parallel() testData := []struct { - args []string - options detectExecuteScanOptions - expected []string + args []string + options detectExecuteScanOptions + isPullRequest bool + expected []string }{ { args: []string{"--testProp1=1"}, @@ -645,13 +707,76 @@ func TestAddDetectArgs(t *testing.T) { "--detect.source.path='.'", }, }, + { + args: []string{"--testProp1=1"}, + options: detectExecuteScanOptions{ + ServerURL: "https://server.url", + Token: "apiToken", + ProjectName: "Rapid_scan_on_PRs", + Version: "1.0", + VersioningModel: "major-minor", + CodeLocation: "", + ScanPaths: []string{"path1", "path2"}, + MinScanInterval: 4, + CustomScanVersion: "1.0", + }, + isPullRequest: true, + expected: []string{ + "--testProp1=1", + "--detect.blackduck.signature.scanner.arguments='--min-scan-interval=4'", + "--blackduck.url=https://server.url", + "--blackduck.api.token=apiToken", + "\"--detect.project.name='Rapid_scan_on_PRs'\"", + "\"--detect.project.version.name='1.0'\"", + "\"--detect.code.location.name='Rapid_scan_on_PRs/1.0'\"", + "--detect.blackduck.signature.scanner.paths=path1,path2", + "--detect.source.path='.'", + "--detect.blackduck.scan.mode='RAPID'", + "--detect.blackduck.rapid.compare.mode='BOM_COMPARE_STRICT'", + "--detect.cleanup=false", + "--detect.output.path='report'", + }, + }, + { + args: []string{"--testProp1=1"}, + options: detectExecuteScanOptions{ + ServerURL: "https://server.url", + Token: "apiToken", + ProjectName: "Rapid_scan_on_PRs", + Version: "2.0", + VersioningModel: "major-minor", + CodeLocation: "", + ScanPaths: []string{"path1", "path2"}, + MinScanInterval: 4, + CustomScanVersion: "2.0", + }, + isPullRequest: true, + expected: []string{ + "--testProp1=1", + "--detect.blackduck.signature.scanner.arguments='--min-scan-interval=4'", + "--blackduck.url=https://server.url", + "--blackduck.api.token=apiToken", + "\"--detect.project.name='Rapid_scan_on_PRs'\"", + "\"--detect.project.version.name='2.0'\"", + "\"--detect.code.location.name='Rapid_scan_on_PRs/2.0'\"", + "--detect.blackduck.signature.scanner.paths=path1,path2", + "--detect.source.path='.'", + "--detect.blackduck.scan.mode='RAPID'", + "--detect.cleanup=false", + "--detect.output.path='report'", + }, + }, } for k, v := range testData { v := v t.Run(fmt.Sprintf("run %v", k), func(t *testing.T) { t.Parallel() - got, err := addDetectArgs(v.args, v.options, newDetectTestUtilsBundle()) + + config := detectExecuteScanOptions{Token: "token", ServerURL: "https://my.blackduck.system", ProjectName: v.options.ProjectName, Version: v.options.Version, CustomScanVersion: v.options.CustomScanVersion} + sys := newBlackduckMockSystem(config) + + got, err := addDetectArgs(v.args, v.options, newDetectTestUtilsBundle(v.isPullRequest), &sys) assert.NoError(t, err) assert.Equal(t, v.expected, got) }) @@ -681,7 +806,7 @@ func TestPostScanChecksAndReporting(t *testing.T) { t.Run("Reporting after scan", func(t *testing.T) { ctx := context.Background() config := detectExecuteScanOptions{Token: "token", ServerURL: "https://my.blackduck.system", ProjectName: "SHC-PiperTest", Version: "", CustomScanVersion: "1.0"} - utils := newDetectTestUtilsBundle() + utils := newDetectTestUtilsBundle(false) sys := newBlackduckMockSystem(config) err := postScanChecksAndReporting(ctx, config, &detectExecuteScanInflux{}, utils, &sys) diff --git a/documentation/docs/steps/detectExecuteScan.md b/documentation/docs/steps/detectExecuteScan.md index 4880f9f75..7f9362558 100644 --- a/documentation/docs/steps/detectExecuteScan.md +++ b/documentation/docs/steps/detectExecuteScan.md @@ -51,7 +51,7 @@ If you have configured your orchestrator to detect pull requests, then the `dete ``` 2. Enable detecExecuationScan in the orchestrator. - For example: + For example: ``` @Library('piper-lib') _ diff --git a/pkg/reporting/pullRequestReport.go b/pkg/reporting/pullRequestReport.go new file mode 100644 index 000000000..d40f0e783 --- /dev/null +++ b/pkg/reporting/pullRequestReport.go @@ -0,0 +1,420 @@ +package reporting + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/SAP/jenkins-library/pkg/log" +) + +// Components - for parsing from file +type Components []Component + +type Component struct { + ComponentName string `json:"componentName"` + ComponentVersion string `json:"versionName"` + ComponentIdentifier string `json:"componentIdentifier"` + ViolatingPolicyNames []string `json:"violatingPolicyNames"` + PolicyViolationVulnerabilities []PolicyViolationVulnerability `json:"policyViolationVulnerabilities"` + PolicyViolationLicenses []PolicyViolationLicense `json:"policyViolationLicenses"` + WarningMessage string `json:"warningMessage"` + ErrorMessage string `json:"errorMessage"` +} + +type PolicyViolationVulnerability struct { + Name string `json:"name"` + ViolatingPolicyNames []string `json:"ViolatingPolicyNames"` + WarningMessage string `json:"warningMessage"` + ErrorMessage string `json:"errorMessage"` + Meta Meta `json:"_meta"` +} + +type PolicyViolationLicense struct { + LicenseName string `json:"licenseName"` + ViolatingPolicyNames []string `json:"violatingPolicyNames"` + Meta Meta `json:"_meta"` +} + +type Meta struct { + Href string `json:"href"` +} + +// RapidScanReport - for commenting to pull requests +type RapidScanReport struct { + Success bool + + ExecutedTime string + + MainTableHeaders []string + MainTableValues [][]string + + VulnerabilitiesTable []Vulnerabilities + LicensesTable []Licenses + OtherViolationsTable []OtherViolations +} + +type Vulnerabilities struct { + PolicyViolationName string + Values []Vulnerability +} + +type Vulnerability struct { + VulnerabilityID string + VulnerabilityScore string + ComponentName string + VulnerabilityHref string +} + +type Licenses struct { + PolicyViolationName string + Values []License +} + +type License struct { + LicenseName string + ComponentName string + LicenseHref string +} + +type OtherViolations struct { + PolicyViolationName string + Values []OtherViolation +} + +type OtherViolation struct { + ComponentName string +} + +const rapidReportMdTemplate = ` +## {{if .Success}}:heavy_check_mark: OSS related checks passed successfully + ### :clipboard: OSS related checks executed by Black Duck - rapid scan passed successfully. +

RAPID SCAN

+ +{{else}} :x: OSS related checks failed + ### :clipboard: Policies violated by added OSS components + + {{range $s := .MainTableHeaders -}}{{- end}} + {{range $s := .MainTableValues -}}{{range $s1 := $s }}{{- end}} + {{- end}} +
{{$s}}
{{$s1}}
+ +{{range $index := .VulnerabilitiesTable -}} +
+{{$len := len $index.Values}} +{{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

+{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}} +
+ + + {{range $value := $index.Values -}} + + + + {{end -}} +
Vulnerability IDVulnerability ScoreComponent Name
{{$value.VulnerabilityID}} {{$value.VulnerabilityScore}}{{$value.ComponentName}}
+
+{{end -}} +{{range $index := .LicensesTable -}} +
+{{$len := len $index.Values}} +{{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

+{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}} +
+ + + {{range $value := $index.Values -}} + + {{end -}} +
License NameComponent Name
{{$value.LicenseName}} {{$value.ComponentName}}
+
+{{end -}} +{{range $index := .OtherViolationsTable -}} +
+{{$len := len $index.Values}} +{{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

+{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}} +
+ + + {{range $value := $index.Values -}} + + {{end -}} +
Component Name
{{$value.ComponentName}}
+
+{{end -}} +{{end}} +` + +// RapidScanResult reads result of Rapid scan from generated file +func RapidScanResult(dir string) (string, error) { + components, removeDir, err := findAndReadJsonFile(dir) + if err != nil { + return "", err + } + if components == nil { + return "", errors.New("couldn't parse info from file") + } + + buf, err := createMarkdownReport(components) + if err != nil { + return "", err + } + + err = os.RemoveAll(removeDir) + if err != nil { + log.Entry().Error("Couldn't remove report file", err) + } + + return buf.String(), nil +} + +type Files []os.DirEntry + +// findLastCreatedDir finds last created directory +func findLastCreatedDir(directories []os.DirEntry) os.DirEntry { + lastCreatedDir := directories[0] + for _, dir := range directories { + if dir.Name() > lastCreatedDir.Name() { + lastCreatedDir = dir + } + } + return lastCreatedDir +} + +// findAndReadJsonFile find file BlackDuck_DeveloperMode_Result.json generated by detectExecuteStep and read it +func findAndReadJsonFile(dir string) (*Components, string, error) { + var err error + filePath := dir + "/runs" + allFiles, err := os.ReadDir(filePath) + if err != nil { + return nil, "", err + } + if allFiles == nil { + return nil, "", errors.New("no report files") + } + lastDir := findLastCreatedDir(allFiles) + removeDir := filePath + "/" + lastDir.Name() + filePath = filePath + "/" + lastDir.Name() + "/scan" + files, err := os.ReadDir(filePath) + if err != nil { + return nil, "", err + } + if files == nil { + return nil, "", errors.New("no report files") + } + + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), "BlackDuck_DeveloperMode_Result.json") { + var result Components + jsonFile, err := os.Open(filePath + "/" + file.Name()) + if err != nil { + return nil, "", err + } + fileBody, err := io.ReadAll(jsonFile) + if err != nil { + return nil, "", err + } + err = json.Unmarshal(fileBody, &result) + if err != nil { + return nil, "", err + } + err = jsonFile.Close() + if err != nil { + log.Entry().Error(fmt.Sprintf("Couldn't close %s", jsonFile.Name()), err) + } + return &result, removeDir, nil + } + } + + return nil, "", nil +} + +// createMarkdownReport creates markdown report to upload it as GitHub PR comment +func createMarkdownReport(components *Components) (*bytes.Buffer, error) { + // preparing report + var scanReport RapidScanReport + scanReport.Success = true + + // getting reports to maps + allPolicyViolationsMapUsed := make(map[string]bool) + countPolicyViolationComponent := make(map[string]map[string]int) + vulnerabilities := make(map[string][]Vulnerability) + licenses := make(map[string][]License) + otherViolations := make(map[string][]OtherViolation) + componentNames := make([]string, len(*components)) + + for idx, component := range *components { + componentName := component.ComponentName + " " + component.ComponentVersion + " (" + component.ComponentIdentifier + ")" + componentNames[idx] = componentName + + // for others + for _, policyViolationName := range component.ViolatingPolicyNames { + if !allPolicyViolationsMapUsed[policyViolationName] { + allPolicyViolationsMapUsed[policyViolationName] = true + scanReport.MainTableHeaders = append(scanReport.MainTableHeaders, policyViolationName) + } + if countPolicyViolationComponent[policyViolationName] == nil { + countPolicyViolationComponent[policyViolationName] = make(map[string]int) + } + msg := component.ErrorMessage + " " + component.WarningMessage + if strings.Contains(msg, policyViolationName) { + countPolicyViolationComponent[policyViolationName][componentName]++ + otherViolations[policyViolationName] = append(otherViolations[policyViolationName], OtherViolation{ComponentName: componentName}) + } + } + + // for Vulnerabilities + for _, policyVulnerability := range component.PolicyViolationVulnerabilities { + for _, policyViolationName := range policyVulnerability.ViolatingPolicyNames { + if countPolicyViolationComponent[policyViolationName] == nil { + countPolicyViolationComponent[policyViolationName] = make(map[string]int) + } + countPolicyViolationComponent[policyViolationName][componentName]++ + vulnerabilities[policyViolationName] = append(vulnerabilities[policyViolationName], + Vulnerability{ + VulnerabilityID: policyVulnerability.Name, + VulnerabilityHref: policyVulnerability.Meta.Href, + VulnerabilityScore: getScore(policyVulnerability.ErrorMessage, "score") + " " + getScore(policyVulnerability.ErrorMessage, "severity"), + ComponentName: componentName, + }) + } + } + + // for Licenses + for _, policyViolationLicense := range component.PolicyViolationLicenses { + for _, policyViolationName := range policyViolationLicense.ViolatingPolicyNames { + if countPolicyViolationComponent[policyViolationName] == nil { + countPolicyViolationComponent[policyViolationName] = make(map[string]int) + } + countPolicyViolationComponent[policyViolationName][componentName]++ + licenses[policyViolationName] = append(licenses[policyViolationName], + License{ + LicenseName: policyViolationLicense.LicenseName, + LicenseHref: policyViolationLicense.Meta.Href + "/license-terms", + ComponentName: componentName, + }) + } + } + } + + if scanReport.MainTableHeaders != nil && componentNames != nil { + scanReport.Success = false + + // MainTable sort & copy + sort.Strings(scanReport.MainTableHeaders) + sort.Strings(componentNames) + scanReport.MainTableHeaders = append([]string{"Component name"}, scanReport.MainTableHeaders...) + for i := range componentNames { + scanReport.MainTableValues = append(scanReport.MainTableValues, []string{}) + scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], componentNames[i]) + for j := 1; j < len(scanReport.MainTableHeaders); j++ { + policyV := scanReport.MainTableHeaders[j] + comp := componentNames[i] + count := strconv.Itoa(countPolicyViolationComponent[policyV][comp]) + scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], count) + } + } + + // VulnerabilitiesTable sort & copy + for key := range vulnerabilities { + item := vulnerabilities[key] + sort.Slice(item, func(i, j int) bool { + return scoreLogicSort(item[i].VulnerabilityScore, item[j].VulnerabilityScore) + }) + scanReport.VulnerabilitiesTable = append(scanReport.VulnerabilitiesTable, Vulnerabilities{ + PolicyViolationName: key, + Values: item, + }) + } + sort.Slice(scanReport.VulnerabilitiesTable, func(i, j int) bool { + return scanReport.VulnerabilitiesTable[i].PolicyViolationName < scanReport.VulnerabilitiesTable[j].PolicyViolationName + }) + + // LicensesTable sort & copy + for key := range licenses { + item := licenses[key] + sort.Slice(item, func(i, j int) bool { + if item[i].LicenseName < item[j].LicenseName { + return true + } + if item[i].LicenseName > item[j].LicenseName { + return false + } + return item[i].ComponentName < item[j].ComponentName + }) + scanReport.LicensesTable = append(scanReport.LicensesTable, Licenses{ + PolicyViolationName: key, + Values: item, + }) + } + sort.Slice(scanReport.LicensesTable, func(i, j int) bool { + return scanReport.LicensesTable[i].PolicyViolationName < scanReport.LicensesTable[j].PolicyViolationName + }) + + // OtherViolationsTable sort & copy + for key := range otherViolations { + item := otherViolations[key] + sort.Slice(item, func(i, j int) bool { + return item[i].ComponentName < item[j].ComponentName + }) + scanReport.OtherViolationsTable = append(scanReport.OtherViolationsTable, OtherViolations{ + PolicyViolationName: key, + Values: item, + }) + } + sort.Slice(scanReport.OtherViolationsTable, func(i, j int) bool { + return scanReport.OtherViolationsTable[i].PolicyViolationName < scanReport.OtherViolationsTable[j].PolicyViolationName + }) + } + + tmpl, err := template.New("report").Parse(rapidReportMdTemplate) + if err != nil { + return nil, errors.New("failed to create Markdown report template err:" + err.Error()) + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, scanReport) + if err != nil { + return nil, errors.New("failed to create Markdown report template err:" + err.Error()) + } + + return buf, nil +} + +// getScore extracts score or severity from error message +func getScore(message, key string) string { + indx := strings.Index(message, key) + if indx == -1 { + return "" + } + var result string + var notFirstSpace bool + for _, s := range message[indx+len(key):] { + if s == ' ' && notFirstSpace { + break + } + notFirstSpace = true + result = result + string(s) + } + return strings.Trim(result, " ") +} + +// scoreLogicSort sorts two scores +func scoreLogicSort(iStr, jStr string) bool { + if strings.Contains(iStr, "10.0") { + return true + } else if strings.Contains(jStr, "10.0") { + return false + } + if iStr >= jStr { + return true + } + return false +} diff --git a/pkg/reporting/pullRequestReport_test.go b/pkg/reporting/pullRequestReport_test.go new file mode 100644 index 000000000..f77f99043 --- /dev/null +++ b/pkg/reporting/pullRequestReport_test.go @@ -0,0 +1,249 @@ +package reporting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Testing createMarkdownReport function +func TestCreateMarkdownReport(t *testing.T) { + t.Parallel() + + testCases := []struct { + testName string + components *Components + expectedErr error + expectedReport string + }{ + + { + testName: "Vulnerabilities were found", + components: &Components{ + { + ComponentName: "qs - QS Querystring", + ComponentVersion: "5.2.1", + ComponentIdentifier: "npmjs:qs/5.2.1", + ViolatingPolicyNames: []string{ + "High Vulnerability Security Issue", + }, + PolicyViolationVulnerabilities: []PolicyViolationVulnerability{ + { + Name: "CVE-2017-1000048", + ViolatingPolicyNames: []string{"High Vulnerability Security Issue"}, + WarningMessage: "", + ErrorMessage: "Component qs - QS Querystring version 5.2.1 with ID npmjs:qs/5.2.1 violates policy" + + " High Vulnerability Security Issue: found vulnerability CVE-2017-1000048 with severity HIGH and CVSS score 7.5", + Meta: Meta{ + Href: "https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2017-1000048", + }, + }, + }, + PolicyViolationLicenses: nil, + WarningMessage: "", + ErrorMessage: "", + }, + { + ComponentName: "Lodash", + ComponentVersion: "4.17.10", + ComponentIdentifier: "npmjs:lodash/4.17.10", + ViolatingPolicyNames: []string{ + "High Vulnerability Security Issue", + "Test High Severity Vuln Filter", + "OutdatedFOSSLibraries", + }, + PolicyViolationVulnerabilities: []PolicyViolationVulnerability{ + { + Name: "CVE-2019-10744", + ViolatingPolicyNames: []string{ + "High Vulnerability Security Issue", + "Test High Severity Vuln Filter", + }, + WarningMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy Test High Severity Vuln " + + "Filter: found vulnerability CVE-2019-10744 with severity CRITICAL and CVSS score 9.1", + ErrorMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy High Vulnerability " + + "Security Issue: found vulnerability CVE-2019-10744 with severity CRITICAL and CVSS score 9.1", + Meta: Meta{ + Href: "https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2019-10744"}, + }, + { + Name: "CVE-2020-8203", + ViolatingPolicyNames: []string{ + "High Vulnerability Security Issue", + "Test High Severity Vuln Filter", + }, + WarningMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy Test " + + "High Severity Vuln Filter: found vulnerability CVE-2020-8203 with severity HIGH and CVSS score 7.4", + ErrorMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy Test High Severity Vuln Filter: " + + "found vulnerability CVE-2020-8203 with severity HIGH and CVSS score 7.4", + Meta: Meta{ + Href: "https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2020-8203", + }, + }, + { + Name: "BDSA-2019-3842", + ViolatingPolicyNames: []string{ + "High Vulnerability Security Issue", + "Test High Severity Vuln Filter", + }, + WarningMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy Test High Severity Vuln Filter: found vulnerability BDSA-2019-3842 with severity HIGH and CVSS score 7.1", + ErrorMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy High Vulnerability Security Issue: found vulnerability BDSA-2019-3842 with severity HIGH and CVSS score 7.1", + Meta: Meta{ + Href: "https://sap-staging.app.blackduck.com/api/vulnerabilities/BDSA-2019-3842", + }, + }, + }, + PolicyViolationLicenses: nil, + WarningMessage: "Component Lodash version 4.17.10 with ID npmjs:lodash/4.17.10 violates policy OutdatedFOSSLibraries", + ErrorMessage: "", + }, + { + ComponentName: "Chalk", + ComponentVersion: "1.1.3", + ComponentIdentifier: "npmjs:chalk/1.1.3", + ViolatingPolicyNames: []string{ + "OutdatedFOSSLibraries", + }, + PolicyViolationVulnerabilities: nil, + PolicyViolationLicenses: nil, + WarningMessage: "Component Chalk version 1.1.3 with ID npmjs:chalk/1.1.3 violates policy OutdatedFOSSLibraries", + ErrorMessage: "", + }, + }, + expectedReport: "\n## :x: OSS related checks failed\n ### :clipboard: Policies violated by added OSS components\n " + + "\n \n \n
Component nameHigh Vulnerability Security IssueOutdatedFOSSLibraries" + + "Test High Severity Vuln Filter
Chalk 1.1.3 (npmjs:chalk/1.1.3)010
Lodash " + + "4.17.10 (npmjs:lodash/4.17.10)313
qs - QS Querystring 5.2.1 " + + "(npmjs:qs/5.2.1)100
\n\n
\n\n

4 Policy " + + "Violations of High Vulnerability Security Issue

\n
\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t
Vulnerability IDVulnerability" + + " ScoreComponent Name
CVE-2019-10744 9.1 CRITICALLodash 4.17.10 " + + "(npmjs:lodash/4.17.10)
" + + "CVE-2017-1000048 7.5 HIGHqs - QS Querystring 5.2.1 (npmjs:qs/5.2.1)
" + + " CVE-2020-8203 7.4 HIGHLodash " + + "4.17.10 (npmjs:lodash/4.17.10)
" + + "BDSA-2019-3842 7.1 HIGHLodash 4.17.10 (npmjs:lodash/4.17.10)
\n
\n
\n\n

" + + "3 Policy Violations of Test High Severity Vuln Filter

\n
\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t
Vulnerability IDVulnerability " + + "ScoreComponent Name
" + + "CVE-2019-10744 9.1 CRITICALLodash 4.17.10 (npmjs:lodash/4.17.10)
" + + " CVE-2020-8203 7.4 " + + "HIGHLodash 4.17.10 (npmjs:lodash/4.17.10)
" + + "BDSA-2019-3842 7.1 HIGHLodash 4.17.10 (npmjs:lodash/4.17.10)
\n
\n
\n\n

" + + "2 Policy Violations of OutdatedFOSSLibraries

\n
\n\t\n\t\t\n\t\t\n\t\t\n\t\t
Component Name
Chalk 1.1.3 " + + "(npmjs:chalk/1.1.3)
Lodash 4.17.10 (npmjs:lodash/4.17.10)
\n
\n\n", + }, + { + testName: "No vulnerabilities && successful build", + components: &Components{}, + expectedReport: "\n## :heavy_check_mark: OSS related checks passed successfully\n ### :clipboard: OSS related checks executed by Black Duck " + + "- rapid scan passed successfully.\n" + + " " + + "

RAPID SCAN

\n\n\n", + }, + } + + for _, c := range testCases { + t.Run(c.testName, func(t *testing.T) { + t.Parallel() + + buf, err := createMarkdownReport(c.components) + + assert.Equal(t, c.expectedErr, err) + assert.Equal(t, c.expectedReport, buf.String()) + }) + } +} + +// Testing getScore function +func TestGetScore(t *testing.T) { + t.Parallel() + + testCases := []struct { + testName string + message string + key string + expected string + }{ + { + testName: "Score 7.5", + message: "Component qs - QS Querystring version 5.2.1 with ID npmjs:qs/5.2.1 violates policy High " + + "Vulnerability Security Issue: found vulnerability CVE-2017-1000048 with severity HIGH and CVSS score 7.5", + key: "score", + expected: "7.5", + }, + { + testName: "CRITICAL severity", + message: "Component minimist version 0.0.8 with ID npmjs:minimist/0.0.8 violates policy High " + + "Vulnerability Security Issue: found vulnerability CVE-2021-44906 with severity CRITICAL and CVSS score 9.8", + key: "severity", + expected: "CRITICAL", + }, + { + testName: "No severity", + message: "Component minimist version 0.0.8 with ID npmjs:minimist/0.0.8 violates policy High " + + "Vulnerability Security Issue: found vulnerability CVE-2021-44906 with CVSS score 9.8", + key: "severity", + expected: "", + }, + } + + for _, c := range testCases { + t.Run(c.testName, func(t *testing.T) { + t.Parallel() + + got := getScore(c.message, c.key) + assert.Equal(t, c.expected, got) + }) + } +} + +// Testing scoreLogicSort function +func TestScoreLogicSort(t *testing.T) { + t.Parallel() + + testCases := []struct { + testName string + leftScore string + rightScore string + expected bool + }{ + { + testName: "left score is higher", + leftScore: "8.8 HIGH", + rightScore: "8.1 HIGH", + expected: true, + }, + { + testName: "right score is higher", + leftScore: "7.9 HIGH", + rightScore: "9.3 CRITICAL", + expected: false, + }, + { + testName: "left score equals 10.0", + leftScore: "10.0 CRITICAL", + rightScore: "8.1 HIGH", + expected: true, + }, + { + testName: "right score equals 10.0", + leftScore: "7.9 HIGH", + rightScore: "10.0 CRITICAL", + expected: false, + }, + { + testName: "both scores equal 10.0", + leftScore: "10.0 CRITICAL", + rightScore: "10.0 CRITICAL", + expected: true, + }, + } + + for _, c := range testCases { + t.Run(c.testName, func(t *testing.T) { + t.Parallel() + + got := scoreLogicSort(c.leftScore, c.rightScore) + assert.Equal(t, c.expected, got) + }) + } +}