From ce06b8245035d3bffbde565f7afa5d57f14b100e Mon Sep 17 00:00:00 2001 From: Sven Merk <33895725+nevskrem@users.noreply.github.com> Date: Mon, 17 May 2021 13:38:17 +0200 Subject: [PATCH] fix(protecodeExecuteScan): Handling of empty findings (#2818) * Don't fail if components list is empty. Resolves failures when scanning images from Crossplane. * Update formatting with go fmt * Update pkg/protecode/protecode.go Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> * Update pkg/protecode/protecode.go Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> * Fix change, make consistent Co-authored-by: d.small@sap.com Co-authored-by: dee0 Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> --- pkg/protecode/protecode.go | 36 ++++++++++++++++-- pkg/protecode/protecode_test.go | 67 ++++++++++++++------------------- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/pkg/protecode/protecode.go b/pkg/protecode/protecode.go index 41c167919..1e46fd2ff 100644 --- a/pkg/protecode/protecode.go +++ b/pkg/protecode/protecode.go @@ -89,6 +89,14 @@ type Protecode struct { logger *logrus.Entry } +// Just calls SetOptions which makes sure logger is set. +// Added to make test code more resilient +func makeProtecode(opts Options) Protecode { + ret := Protecode{} + ret.SetOptions(opts) + return ret +} + //Options struct which can be used to configure the Protecode struct type Options struct { ServerURL string @@ -326,6 +334,16 @@ func (pc *Protecode) DeclareFetchURL(cleanupMode, group, fetchURL string) *Resul return result } +// 2021-04-20 d : +// Found, via web search, an announcement that the set of status codes is expanding from +// B, R, F +// to +// B, R, F, S, D, P. +// Only R and F indicate work has completed. +func scanInProgress(status string) bool { + return status != statusReady && status != statusFailed +} + //PollForResult polls the protecode scan for the result scan func (pc *Protecode) PollForResult(productID int, timeOutInMinutes string) ResultData { @@ -351,7 +369,7 @@ func (pc *Protecode) PollForResult(productID int, timeOutInMinutes string) Resul i = 0 return response } - if len(response.Result.Components) > 0 && response.Result.Status != statusBusy { + if !scanInProgress(response.Result.Status) { ticker.Stop() i = 0 break @@ -363,10 +381,20 @@ func (pc *Protecode) PollForResult(productID int, timeOutInMinutes string) Resul } } - if len(response.Result.Components) == 0 || response.Result.Status == statusBusy { + if scanInProgress(response.Result.Status) { response, err = pc.pullResult(productID) - if err != nil || len(response.Result.Components) == 0 || response.Result.Status == statusBusy { - pc.logger.Fatal("No result after polling") + + if len(response.Result.Components) < 1 { + // 2020-04-20 d : + // We are required to scan all images including 3rd party ones. + // We have found that Crossplane makes use docker images that contain no + // executable code. + // So we can no longer treat an empty Components list as an error. + pc.logger.Warn("Protecode scan did not identify any components.") + } + + if err != nil || response.Result.Status == statusBusy { + pc.logger.Fatalf("No result after polling err: %v protecode status: %v", err, response.Result.Status) } } diff --git a/pkg/protecode/protecode_test.go b/pkg/protecode/protecode_test.go index 23b015968..9a1fcf89a 100644 --- a/pkg/protecode/protecode_test.go +++ b/pkg/protecode/protecode_test.go @@ -15,8 +15,6 @@ import ( "strings" "time" - piperHttp "github.com/SAP/jenkins-library/pkg/http" - "github.com/SAP/jenkins-library/pkg/log" "github.com/stretchr/testify/assert" ) @@ -34,7 +32,7 @@ func TestMapResponse(t *testing.T) { {`{"results": {"status": "B", "id": 209396, "product_id": 209396, "report_url": "https://protecode.c.eu-de-2.cloud.sap/products/209396/"}}`, new(ResultData), &ResultData{Result: Result{ProductID: 209396, Status: statusBusy, ReportURL: "https://protecode.c.eu-de-2.cloud.sap/products/209396/"}}}, {`{"products": [{"product_id": 1}]}`, new(ProductData), &ProductData{Products: []Product{{ProductID: 1}}}}, } - pc := Protecode{} + pc := makeProtecode(Options{}) for _, c := range cases { r := ioutil.NopCloser(bytes.NewReader([]byte(c.give))) @@ -63,7 +61,7 @@ func TestParseResultSuccess(t *testing.T) { }, }, } - pc := Protecode{} + pc := makeProtecode(Options{}) m, vulns := pc.ParseResultForInflux(result, "Excluded CVES: Cve4,") t.Run("Parse Protecode Results", func(t *testing.T) { assert.Equal(t, 1, m["historical_vulnerabilities"]) @@ -84,7 +82,7 @@ func TestParseResultViolations(t *testing.T) { if err != nil { t.Fatalf("failed reading %v", violations) } - pc := Protecode{} + pc := makeProtecode(Options{}) resultData := new(ResultData) pc.mapResponse(ioutil.NopCloser(strings.NewReader(string(byteContent))), resultData) @@ -110,7 +108,7 @@ func TestParseResultNoViolations(t *testing.T) { t.Fatalf("failed reading %v", noViolations) } - pc := Protecode{} + pc := makeProtecode(Options{}) resultData := new(ResultData) pc.mapResponse(ioutil.NopCloser(strings.NewReader(string(byteContent))), resultData) @@ -135,7 +133,7 @@ func TestParseResultTriaged(t *testing.T) { t.Fatalf("failed reading %v", triaged) } - pc := Protecode{} + pc := makeProtecode(Options{}) resultData := new(ResultData) pc.mapResponse(ioutil.NopCloser(strings.NewReader(string(byteContent))), resultData) @@ -168,17 +166,14 @@ func TestLoadExistingProductSuccess(t *testing.T) { // Close the server when test finishes defer server.Close() - client := &piperHttp.Client{} - client.SetOptions(piperHttp.ClientOptions{}) - cases := []struct { pc Protecode protecodeGroup string reuseExisting bool want int }{ - {Protecode{serverURL: server.URL, client: client, logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/protecode")}, "group", true, 1}, - {Protecode{serverURL: server.URL, client: client}, "group32", false, -1}, + {makeProtecode(Options{ServerURL: server.URL}), "group", true, 1}, + {makeProtecode(Options{ServerURL: server.URL}), "group32", false, -1}, } for _, c := range cases { @@ -194,15 +189,24 @@ func TestPollForResultSuccess(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestURI = req.RequestURI productID := 111 + + // 2021-04-20 d : + // Added case '333' to test proper handling of case where Component list is empty. if strings.Contains(requestURI, "222") { productID = 222 + } else if strings.Contains(requestURI, "333") { + productID = 333 } - response = ResultData{Result: Result{ProductID: productID, ReportURL: requestURI, Status: "D", Components: []Component{ - {Vulns: []Vulnerability{ - {Triage: []Triage{{ID: 1}}}}, - }}, - }} + var cmpnts []Component + if productID != 333 { + cmpnts = []Component{ + {Vulns: []Vulnerability{ + {Triage: []Triage{{ID: 1}}}}, + }} + } + + response = ResultData{Result: Result{ProductID: productID, ReportURL: requestURI, Status: "D", Components: cmpnts}} var b bytes.Buffer json.NewEncoder(&b).Encode(&response) @@ -224,13 +228,12 @@ func TestPollForResultSuccess(t *testing.T) { {Triage: []Triage{{ID: 1}}}}, }}, }}}, + {333, ResultData{Result: Result{ProductID: 333, ReportURL: "/api/product/333/", Status: "D"}}}, } // Close the server when test finishes defer server.Close() - client := &piperHttp.Client{} - client.SetOptions(piperHttp.ClientOptions{}) - pc := Protecode{serverURL: server.URL, client: client, duration: (time.Minute * 1), logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/protecode")} + pc := makeProtecode(Options{ServerURL: server.URL, Duration: (time.Minute * 1)}) for _, c := range cases { got := pc.PollForResult(c.productID, "1") @@ -264,16 +267,13 @@ func TestPullResultSuccess(t *testing.T) { // Close the server when test finishes defer server.Close() - client := &piperHttp.Client{} - client.SetOptions(piperHttp.ClientOptions{}) - cases := []struct { pc Protecode productID int want ResultData }{ - {Protecode{serverURL: server.URL, client: client}, 111, ResultData{Result: Result{ProductID: 111, ReportURL: "/api/product/111/"}}}, - {Protecode{serverURL: server.URL, client: client}, 222, ResultData{Result: Result{ProductID: 222, ReportURL: "/api/product/222/"}}}, + {makeProtecode(Options{ServerURL: server.URL}), 111, ResultData{Result: Result{ProductID: 111, ReportURL: "/api/product/111/"}}}, + {makeProtecode(Options{ServerURL: server.URL}), 222, ResultData{Result: Result{ProductID: 222, ReportURL: "/api/product/222/"}}}, } for _, c := range cases { @@ -306,10 +306,7 @@ func TestDeclareFetchURLSuccess(t *testing.T) { })) // Close the server when test finishes defer server.Close() - - pc := Protecode{} - po := Options{ServerURL: server.URL} - pc.SetOptions(po) + pc := makeProtecode(Options{ServerURL: server.URL}) cases := []struct { cleanupMode string @@ -369,10 +366,7 @@ func TestUploadScanFileSuccess(t *testing.T) { })) // Close the server when test finishes defer server.Close() - - pc := Protecode{} - po := Options{ServerURL: server.URL} - pc.SetOptions(po) + pc := makeProtecode(Options{ServerURL: server.URL}) testFile, err := ioutil.TempFile("", "testFileUpload") if err != nil { @@ -424,10 +418,7 @@ func TestLoadReportSuccess(t *testing.T) { // Close the server when test finishes defer server.Close() - client := &piperHttp.Client{} - client.SetOptions(piperHttp.ClientOptions{}) - - pc := Protecode{serverURL: server.URL, client: client} + pc := makeProtecode(Options{ServerURL: server.URL}) cases := []struct { productID int @@ -467,7 +458,7 @@ func TestDeleteScanSuccess(t *testing.T) { // Close the server when test finishes defer server.Close() - pc := Protecode{} + pc := makeProtecode(Options{}) po := Options{ServerURL: server.URL} pc.SetOptions(po)