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

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 <andrei.kireev@sap.com>
Co-authored-by: ffeldmann <f.feldmann@sap.com>
Co-authored-by: sumeet patil <sumeet.patil@sap.com>
This commit is contained in:
Akramdzhon Azamov 2023-03-02 15:04:21 +05:00 committed by GitHub
parent cc3bc76943
commit f4fbf0f1ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 873 additions and 24 deletions

View File

@ -14,20 +14,20 @@ import (
"time" "time"
bd "github.com/SAP/jenkins-library/pkg/blackduck" bd "github.com/SAP/jenkins-library/pkg/blackduck"
"github.com/SAP/jenkins-library/pkg/command"
piperGithub "github.com/SAP/jenkins-library/pkg/github" piperGithub "github.com/SAP/jenkins-library/pkg/github"
piperhttp "github.com/SAP/jenkins-library/pkg/http" 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/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/piperutils"
"github.com/SAP/jenkins-library/pkg/reporting"
"github.com/SAP/jenkins-library/pkg/telemetry" "github.com/SAP/jenkins-library/pkg/telemetry"
"github.com/SAP/jenkins-library/pkg/toolrecord" "github.com/SAP/jenkins-library/pkg/toolrecord"
"github.com/SAP/jenkins-library/pkg/versioning"
"github.com/google/go-github/v45/github" "github.com/google/go-github/v45/github"
"github.com/pkg/errors"
) )
type detectUtils interface { type detectUtils interface {
@ -46,14 +46,16 @@ type detectUtils interface {
GetIssueService() *github.IssuesService GetIssueService() *github.IssuesService
GetSearchService() *github.SearchService GetSearchService() *github.SearchService
GetProvider() orchestrator.OrchestratorSpecificConfigProviding
} }
type detectUtilsBundle struct { type detectUtilsBundle struct {
*command.Command *command.Command
*piperutils.Files *piperutils.Files
*piperhttp.Client *piperhttp.Client
issues *github.IssuesService issues *github.IssuesService
search *github.SearchService search *github.SearchService
provider orchestrator.OrchestratorSpecificConfigProviding
} }
func (d *detectUtilsBundle) GetIssueService() *github.IssuesService { func (d *detectUtilsBundle) GetIssueService() *github.IssuesService {
@ -64,6 +66,10 @@ func (d *detectUtilsBundle) GetSearchService() *github.SearchService {
return d.search return d.search
} }
func (d *detectUtilsBundle) GetProvider() orchestrator.OrchestratorSpecificConfigProviding {
return d.provider
}
type blackduckSystem struct { type blackduckSystem struct {
Client bd.Client Client bd.Client
} }
@ -104,6 +110,15 @@ func newDetectUtils(client *github.Client) detectUtils {
} }
utils.Stdout(log.Writer()) utils.Stdout(log.Writer())
utils.Stderr(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 return &utils
} }
@ -159,8 +174,10 @@ func runDetect(ctx context.Context, config detectExecuteScanOptions, utils detec
} }
} }
blackduckSystem := newBlackduckSystem(config)
args := []string{"./detect.sh"} args := []string{"./detect.sh"}
args, err = addDetectArgs(args, config, utils) args, err = addDetectArgs(args, config, utils, blackduckSystem)
if err != nil { if err != nil {
return err return err
} }
@ -173,7 +190,6 @@ func runDetect(ctx context.Context, config detectExecuteScanOptions, utils detec
utils.SetEnv(envs) utils.SetEnv(envs)
err = utils.RunShell("/bin/bash", script) err = utils.RunShell("/bin/bash", script)
blackduckSystem := newBlackduckSystem(config)
reportingErr := postScanChecksAndReporting(ctx, config, influx, utils, blackduckSystem) reportingErr := postScanChecksAndReporting(ctx, config, influx, utils, blackduckSystem)
if reportingErr != nil { if reportingErr != nil {
if strings.Contains(reportingErr.Error(), "License Policy Violations found") { 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) 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) detectVersionName := getVersionName(config)
// Split on spaces, the scanPropeties, so that each property is available as a single string // 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 // 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, " "))) 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 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 { 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{} errorsOccured := []string{}
vulns, err := getVulnerabilitiesWithComponents(config, influx, sys) vulns, err := getVulnerabilitiesWithComponents(config, influx, sys)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -15,6 +16,7 @@ import (
piperGithub "github.com/SAP/jenkins-library/pkg/github" piperGithub "github.com/SAP/jenkins-library/pkg/github"
piperhttp "github.com/SAP/jenkins-library/pkg/http" piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/mock" "github.com/SAP/jenkins-library/pkg/mock"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/google/go-github/v45/github" "github.com/google/go-github/v45/github"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -25,7 +27,12 @@ type detectTestUtilsBundle struct {
downloadedFiles map[string]string // src, dest downloadedFiles map[string]string // src, dest
*mock.ShellMockRunner *mock.ShellMockRunner
*mock.FilesMock *mock.FilesMock
customEnv []string customEnv []string
orchestrator *orchestratorConfigProviderMock
}
func (d *detectTestUtilsBundle) GetProvider() orchestrator.OrchestratorSpecificConfigProviding {
return d.orchestrator
} }
func (d *detectTestUtilsBundle) GetIssueService() *github.IssuesService { func (d *detectTestUtilsBundle) GetIssueService() *github.IssuesService {
@ -36,6 +43,15 @@ func (d *detectTestUtilsBundle) GetSearchService() *github.SearchService {
return nil return nil
} }
type orchestratorConfigProviderMock struct {
orchestrator.UnknownOrchestratorConfigProvider
isPullRequest bool
}
func (o *orchestratorConfigProviderMock) IsPullRequest() bool {
return o.isPullRequest
}
type httpMockClient struct { type httpMockClient struct {
responseBodyForURL map[string]string responseBodyForURL map[string]string
errorMessageForURL 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/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/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/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{}, header: map[string]http.Header{},
} }
@ -193,6 +211,48 @@ const (
"severityLevels": [{"name":"BLOCKER", "value": 1}, {"name": "CRITICAL", "value": 1}] "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 { func (c *detectTestUtilsBundle) RunExecutable(string, ...string) error {
@ -222,10 +282,11 @@ func (w *detectTestUtilsBundle) CreateIssue(ghCreateIssueOptions *piperGithub.Cr
return nil return nil
} }
func newDetectTestUtilsBundle() *detectTestUtilsBundle { func newDetectTestUtilsBundle(isPullRequest bool) *detectTestUtilsBundle {
utilsBundle := detectTestUtilsBundle{ utilsBundle := detectTestUtilsBundle{
ShellMockRunner: &mock.ShellMockRunner{}, ShellMockRunner: &mock.ShellMockRunner{},
FilesMock: &mock.FilesMock{}, FilesMock: &mock.FilesMock{},
orchestrator: &orchestratorConfigProviderMock{isPullRequest: isPullRequest},
} }
return &utilsBundle return &utilsBundle
} }
@ -235,7 +296,7 @@ func TestRunDetect(t *testing.T) {
t.Run("success case", func(t *testing.T) { t.Run("success case", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
utilsMock := newDetectTestUtilsBundle() utilsMock := newDetectTestUtilsBundle(false)
utilsMock.AddFile("detect.sh", []byte("")) utilsMock.AddFile("detect.sh", []byte(""))
err := runDetect(ctx, detectExecuteScanOptions{}, utilsMock, &detectExecuteScanInflux{}) 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.Run("success case detect 6", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
utilsMock := newDetectTestUtilsBundle() utilsMock := newDetectTestUtilsBundle(false)
utilsMock.AddFile("detect.sh", []byte("")) utilsMock.AddFile("detect.sh", []byte(""))
options := detectExecuteScanOptions{ options := detectExecuteScanOptions{
CustomEnvironmentVariables: []string{"DETECT_LATEST_RELEASE_VERSION=6.8.0"}, 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.Run("success case detect 6 from OS env", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
utilsMock := newDetectTestUtilsBundle() utilsMock := newDetectTestUtilsBundle(false)
utilsMock.AddFile("detect.sh", []byte("")) utilsMock.AddFile("detect.sh", []byte(""))
utilsMock.customEnv = []string{"DETECT_LATEST_RELEASE_VERSION=6.8.0"} utilsMock.customEnv = []string{"DETECT_LATEST_RELEASE_VERSION=6.8.0"}
err := runDetect(ctx, detectExecuteScanOptions{}, utilsMock, &detectExecuteScanInflux{}) err := runDetect(ctx, detectExecuteScanOptions{}, utilsMock, &detectExecuteScanInflux{})
@ -287,7 +348,7 @@ func TestRunDetect(t *testing.T) {
t.Run("failure case", func(t *testing.T) { t.Run("failure case", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() 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.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.ExitCode = 3
utilsMock.AddFile("detect.sh", []byte("")) utilsMock.AddFile("detect.sh", []byte(""))
@ -300,7 +361,7 @@ func TestRunDetect(t *testing.T) {
t.Run("maven parameters", func(t *testing.T) { t.Run("maven parameters", func(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
utilsMock := newDetectTestUtilsBundle() utilsMock := newDetectTestUtilsBundle(false)
utilsMock.CurrentDir = "root_folder" utilsMock.CurrentDir = "root_folder"
utilsMock.AddFile("detect.sh", []byte("")) utilsMock.AddFile("detect.sh", []byte(""))
err := runDetect(ctx, detectExecuteScanOptions{ err := runDetect(ctx, detectExecuteScanOptions{
@ -322,9 +383,10 @@ func TestRunDetect(t *testing.T) {
func TestAddDetectArgs(t *testing.T) { func TestAddDetectArgs(t *testing.T) {
t.Parallel() t.Parallel()
testData := []struct { testData := []struct {
args []string args []string
options detectExecuteScanOptions options detectExecuteScanOptions
expected []string isPullRequest bool
expected []string
}{ }{
{ {
args: []string{"--testProp1=1"}, args: []string{"--testProp1=1"},
@ -645,13 +707,76 @@ func TestAddDetectArgs(t *testing.T) {
"--detect.source.path='.'", "--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 { for k, v := range testData {
v := v v := v
t.Run(fmt.Sprintf("run %v", k), func(t *testing.T) { t.Run(fmt.Sprintf("run %v", k), func(t *testing.T) {
t.Parallel() 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.NoError(t, err)
assert.Equal(t, v.expected, got) assert.Equal(t, v.expected, got)
}) })
@ -681,7 +806,7 @@ func TestPostScanChecksAndReporting(t *testing.T) {
t.Run("Reporting after scan", func(t *testing.T) { t.Run("Reporting after scan", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
config := detectExecuteScanOptions{Token: "token", ServerURL: "https://my.blackduck.system", ProjectName: "SHC-PiperTest", Version: "", CustomScanVersion: "1.0"} config := detectExecuteScanOptions{Token: "token", ServerURL: "https://my.blackduck.system", ProjectName: "SHC-PiperTest", Version: "", CustomScanVersion: "1.0"}
utils := newDetectTestUtilsBundle() utils := newDetectTestUtilsBundle(false)
sys := newBlackduckMockSystem(config) sys := newBlackduckMockSystem(config)
err := postScanChecksAndReporting(ctx, config, &detectExecuteScanInflux{}, utils, &sys) err := postScanChecksAndReporting(ctx, config, &detectExecuteScanInflux{}, utils, &sys)

View File

@ -51,7 +51,7 @@ If you have configured your orchestrator to detect pull requests, then the `dete
``` ```
2. Enable detecExecuationScan in the orchestrator. 2. Enable detecExecuationScan in the orchestrator.
For example: For example:
``` ```
@Library('piper-lib') _ @Library('piper-lib') _

View File

@ -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.
<a href="https://community.synopsys.com/s/document-item?bundleId=integrations-detect&topicId=downloadingandrunning%2Frapidscan.html&_LANG=enus"><h3>RAPID SCAN</h3> </a>
{{else}} :x: OSS related checks failed
### :clipboard: Policies violated by added OSS components
<table>
<tr>{{range $s := .MainTableHeaders -}}<td><b>{{$s}}</b></td>{{- end}}</tr>
{{range $s := .MainTableValues -}}<tr>{{range $s1 := $s }}<td>{{$s1}}</td>{{- end}}</tr>
{{- end}}
</table>
{{range $index := .VulnerabilitiesTable -}}
<details><summary>
{{$len := len $index.Values}}
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
</summary>
<table>
<tr><td><b>Vulnerability ID</b></td><td><b>Vulnerability Score</b></td><td><b>Component Name</b></td></tr>
{{range $value := $index.Values -}}
<tr>
<td> <a href="{{$value.VulnerabilityHref}}"> {{$value.VulnerabilityID}} </a> </td><td>{{$value.VulnerabilityScore}}</td><td>{{$value.ComponentName}}</td>
</tr>
{{end -}}
</table>
</details>
{{end -}}
{{range $index := .LicensesTable -}}
<details><summary>
{{$len := len $index.Values}}
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
</summary>
<table>
<tr><td><b>License Name</b></td><td><b>Component Name</b></td></tr>
{{range $value := $index.Values -}}
<tr><td> <a href="{{$value.LicenseHref}}"> {{$value.LicenseName}} </a> </td><td>{{$value.ComponentName}}</td></tr>
{{end -}}
</table>
</details>
{{end -}}
{{range $index := .OtherViolationsTable -}}
<details><summary>
{{$len := len $index.Values}}
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
</summary>
<table>
<tr><td><b>Component Name</b></td></tr>
{{range $value := $index.Values -}}
<tr><td>{{$value.ComponentName}}</td></tr>
{{end -}}
</table>
</details>
{{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
}

View File

@ -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 " +
"<table>\n <tr><td><b>Component name</b></td><td><b>High Vulnerability Security Issue</b></td><td><b>OutdatedFOSSLibraries</b></td><td><b>" +
"Test High Severity Vuln Filter</b></td></tr>\n <tr><td>Chalk 1.1.3 (npmjs:chalk/1.1.3)</td><td>0</td><td>1</td><td>0</td></tr><tr><td>Lodash " +
"4.17.10 (npmjs:lodash/4.17.10)</td><td>3</td><td>1</td><td>3</td></tr><tr><td>qs - QS Querystring 5.2.1 " +
"(npmjs:qs/5.2.1)</td><td>1</td><td>0</td><td>0</td></tr>\n </table>\n\n<details><summary>\n\n<h3> 4 Policy " +
"Violations of High Vulnerability Security Issue </h3> \n</summary>\n\t<table>\n\t\t<tr><td><b>Vulnerability ID</b></td><td><b>Vulnerability" +
" Score</b></td><td><b>Component Name</b></td></tr>\n\t\t<tr>\n\t\t\t<td> <a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2019-10744\"> CVE-2019-10744 </a> </td><td>9.1 CRITICAL</td><td>Lodash 4.17.10 " +
"(npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t<tr>\n\t\t\t<td> <a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2017-1000048\"> " +
"CVE-2017-1000048 </a> </td><td>7.5 HIGH</td><td>qs - QS Querystring 5.2.1 (npmjs:qs/5.2.1)</td>\n\t\t\t</tr>\n\t\t<tr>\n\t\t\t<td> " +
"<a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2020-8203\"> CVE-2020-8203 </a> </td><td>7.4 HIGH</td><td>Lodash " +
"4.17.10 (npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t<tr>\n\t\t\t<td> <a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/BDSA-2019-3842\"> " +
"BDSA-2019-3842 </a> </td><td>7.1 HIGH</td><td>Lodash 4.17.10 (npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t</table>\n</details>\n<details><summary>\n\n<h3> " +
"3 Policy Violations of Test High Severity Vuln Filter </h3> \n</summary>\n\t<table>\n\t\t<tr><td><b>Vulnerability ID</b></td><td><b>Vulnerability " +
"Score</b></td><td><b>Component Name</b></td></tr>\n\t\t<tr>\n\t\t\t<td> <a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2019-10744\"> " +
"CVE-2019-10744 </a> </td><td>9.1 CRITICAL</td><td>Lodash 4.17.10 (npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t<tr>\n\t\t\t<td> " +
"<a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/CVE-2020-8203\"> CVE-2020-8203 </a> </td><td>7.4 " +
"HIGH</td><td>Lodash 4.17.10 (npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t<tr>\n\t\t\t<td> <a href=\"https://sap-staging.app.blackduck.com/api/vulnerabilities/BDSA-2019-3842\"> " +
"BDSA-2019-3842 </a> </td><td>7.1 HIGH</td><td>Lodash 4.17.10 (npmjs:lodash/4.17.10)</td>\n\t\t\t</tr>\n\t\t</table>\n</details>\n<details><summary>\n\n<h3> " +
"2 Policy Violations of OutdatedFOSSLibraries </h3> \n</summary>\n\t<table>\n\t\t<tr><td><b>Component Name</b></td></tr>\n\t\t<tr><td>Chalk 1.1.3 " +
"(npmjs:chalk/1.1.3)</td></tr>\n\t\t<tr><td>Lodash 4.17.10 (npmjs:lodash/4.17.10)</td></tr>\n\t\t</table>\n</details>\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" +
" <a href=\"https://community.synopsys.com/s/document-item?bundleId=integrations-detect&topicId=downloadingandrunning%2Frapidscan.html&_LANG=enus\">" +
"<h3>RAPID SCAN</h3> </a>\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)
})
}
}