From 0aa6bf0fffc12f90e564adb4db7a2fb78c44f755 Mon Sep 17 00:00:00 2001 From: Vishwas R <30438425+vrajashkr@users.noreply.github.com> Date: Fri, 16 Feb 2024 02:49:49 +0530 Subject: [PATCH] feat: include PackagePath data in CVEs for image queries (#2241) Signed-off-by: Vishwas Rajashekar --- Makefile | 1 + pkg/cli/client/cve_cmd_internal_test.go | 4 +- .../client/search_functions_internal_test.go | 47 +++++++++-- pkg/cli/client/service.go | 3 +- .../search/cve/cve_internal_test.go | 11 +++ pkg/extensions/search/cve/model/models.go | 4 +- pkg/extensions/search/cve/trivy/scanner.go | 9 +++ .../search/cve/trivy/scanner_test.go | 78 +++++++++++++++++++ .../search/gql_generated/generated.go | 57 ++++++++++++++ .../search/gql_generated/models_gen.go | 2 + pkg/extensions/search/resolver.go | 1 + pkg/extensions/search/schema.graphql | 4 + pkg/test/image-utils/utils.go | 23 ++++-- 13 files changed, 227 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index a61f6d7cb..669a36658 100644 --- a/Makefile +++ b/Makefile @@ -230,6 +230,7 @@ $(TESTDATA): check-skopeo skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1; \ skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/java:0.0.1 oci:${TESTDATA}/zot-cve-java-test:0.0.1; \ skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/alpine:3.17.3 oci:${TESTDATA}/alpine:3.17.3; \ + skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/spring-web:5.3.31 oci:${TESTDATA}/spring-web:5.3.31; \ chmod -R a=rwx ${TESTDATA} ls -R -l ${TESTDATA} diff --git a/pkg/cli/client/cve_cmd_internal_test.go b/pkg/cli/client/cve_cmd_internal_test.go index 1d45ee0cf..028fe229d 100644 --- a/pkg/cli/client/cve_cmd_internal_test.go +++ b/pkg/cli/client/cve_cmd_internal_test.go @@ -227,7 +227,7 @@ func TestSearchCVECmd(t *testing.T) { So(buff.String(), ShouldEqual, `{"Tag":"dummyImageName:tag","CVEList":`+ `[{"Id":"dummyCVEID","Severity":"HIGH","Title":"Title of that CVE",`+ `"Description":"Description of the CVE","PackageList":[{"Name":"packagename",`+ - `"InstalledVersion":"installedver","FixedVersion":"fixedver"}]}],"Summary":`+ + `"PackagePath":"","InstalledVersion":"installedver","FixedVersion":"fixedver"}]}],"Summary":`+ `{"maxSeverity":"HIGH","unknownCount":0,"lowCount":0,"mediumCount":0,"highCount":1,`+ `"criticalCount":0,"count":1}}`+"\n") So(err, ShouldBeNil) @@ -247,7 +247,7 @@ func TestSearchCVECmd(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `--- tag: dummyImageName:tag cvelist: - id: dummyCVEID`+ ` severity: HIGH title: Title of that CVE description: Description of the CVE packagelist: `+ - `- name: packagename installedversion: installedver fixedversion: fixedver `+ + `- name: packagename packagepath: "" installedversion: installedver fixedversion: fixedver `+ `summary: maxseverity: HIGH unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 1 criticalcount: 0 count: 1`) So(err, ShouldBeNil) }) diff --git a/pkg/cli/client/search_functions_internal_test.go b/pkg/cli/client/search_functions_internal_test.go index 21bb99216..36683b057 100644 --- a/pkg/cli/client/search_functions_internal_test.go +++ b/pkg/cli/client/search_functions_internal_test.go @@ -345,13 +345,33 @@ func TestSearchCVEForImageGQL(t *testing.T) { }, }, }, + { + ID: "test-cve-id2", + Description: "Test CVE ID 2", + Title: "Test CVE 2", + Severity: "HIGH", + PackageList: []packageList{ + { + Name: "packagename", + PackagePath: "/usr/bin/dummy.jar", + FixedVersion: "fixedver", + InstalledVersion: "installedver", + }, + { + Name: "packagename", + PackagePath: "/usr/bin/dummy.gem", + FixedVersion: "fixedver", + InstalledVersion: "installedver", + }, + }, + }, }, Summary: common.ImageVulnerabilitySummary{ - Count: 1, + Count: 2, UnknownCount: 0, LowCount: 0, MediumCount: 0, - HighCount: 1, + HighCount: 2, CriticalCount: 0, MaxSeverity: "HIGH", }, @@ -363,14 +383,27 @@ func TestSearchCVEForImageGQL(t *testing.T) { err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID") So(err, ShouldBeNil) + bufferContent := buff.String() + bufferLines := strings.Split(bufferContent, "\n") + + // Expected result - each row indicates a row of the table with reduced spaces + expected := []string{ + "CRITICAL 0, HIGH 2, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 2", + "", + "ID SEVERITY TITLE", + "dummyCVEID HIGH Title of that CVE", + "test-cve-id2 HIGH Test CVE 2", + } + space := regexp.MustCompile(`\s+`) - str := space.ReplaceAllString(buff.String(), " ") - actual := strings.TrimSpace(str) - So(actual, ShouldContainSubstring, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1") - So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") + + for lineIndex := 0; lineIndex < len(expected); lineIndex++ { + line := space.ReplaceAllString(bufferLines[lineIndex], " ") + So(line, ShouldEqualTrimSpace, expected[lineIndex]) + } }) - Convey("SearchCVEForImageGQL", t, func() { + Convey("SearchCVEForImageGQL with injected error", t, func() { buff := bytes.NewBufferString("") searchConfig := getMockSearchConfig(buff, mockService{ getCveByImageGQLFn: func(ctx context.Context, config SearchConfig, username string, password string, diff --git a/pkg/cli/client/service.go b/pkg/cli/client/service.go index a7039815a..8b9f8576d 100644 --- a/pkg/cli/client/service.go +++ b/pkg/cli/client/service.go @@ -308,7 +308,7 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config Search Tag CVEList { Id Title Severity Description - PackageList {Name InstalledVersion FixedVersion} + PackageList {Name PackagePath InstalledVersion FixedVersion} } Summary { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity @@ -732,6 +732,7 @@ type tagListResp struct { //nolint:tagliatelle // graphQL schema type packageList struct { Name string `json:"Name"` + PackagePath string `json:"PackagePath"` InstalledVersion string `json:"InstalledVersion"` FixedVersion string `json:"FixedVersion"` } diff --git a/pkg/extensions/search/cve/cve_internal_test.go b/pkg/extensions/search/cve/cve_internal_test.go index 930e1ac87..4f902cfdf 100644 --- a/pkg/extensions/search/cve/cve_internal_test.go +++ b/pkg/extensions/search/cve/cve_internal_test.go @@ -25,6 +25,13 @@ func TestUtils(t *testing.T) { PackageList: []cvemodel.Package{ { Name: "NameTest", + PackagePath: "/usr/bin/artifacts/dummy.jar", + FixedVersion: "FixedVersionTest", + InstalledVersion: "InstalledVersionTest", + }, + { + Name: "NameTest", + PackagePath: "/usr/local/artifacts/dummy.gem", FixedVersion: "FixedVersionTest", InstalledVersion: "InstalledVersionTest", }, @@ -34,6 +41,10 @@ func TestUtils(t *testing.T) { So(cve.ContainsStr("NameTest"), ShouldBeTrue) So(cve.ContainsStr("FixedVersionTest"), ShouldBeTrue) So(cve.ContainsStr("InstalledVersionTest"), ShouldBeTrue) + So(cve.ContainsStr("/usr/bin/artifacts/dummy.jar"), ShouldBeTrue) + So(cve.ContainsStr("dummy.jar"), ShouldBeTrue) + So(cve.ContainsStr("/usr/local/artifacts/dummy.gem"), ShouldBeTrue) + So(cve.ContainsStr("dummy.gem"), ShouldBeTrue) }) Convey("getConfigAndDigest", func() { _, _, err := getConfigAndDigest(mocks.MetaDBMock{}, "bad-digest") diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go index 7a0c8ef1d..30dcc2f0b 100644 --- a/pkg/extensions/search/cve/model/models.go +++ b/pkg/extensions/search/cve/model/models.go @@ -39,13 +39,15 @@ func (cve *CVE) ContainsStr(str string) bool { slices.ContainsFunc(cve.PackageList, func(pack Package) bool { return strings.Contains(strings.ToUpper(pack.Name), str) || strings.Contains(strings.ToUpper(pack.FixedVersion), str) || - strings.Contains(strings.ToUpper(pack.InstalledVersion), str) + strings.Contains(strings.ToUpper(pack.InstalledVersion), str) || + strings.Contains(strings.ToUpper(pack.PackagePath), str) }) } //nolint:tagliatelle // graphQL schema type Package struct { Name string `json:"Name"` + PackagePath string `json:"PackagePath"` InstalledVersion string `json:"InstalledVersion"` FixedVersion string `json:"FixedVersion"` } diff --git a/pkg/extensions/search/cve/trivy/scanner.go b/pkg/extensions/search/cve/trivy/scanner.go index 098b7164b..87e2f8d51 100644 --- a/pkg/extensions/search/cve/trivy/scanner.go +++ b/pkg/extensions/search/cve/trivy/scanner.go @@ -394,6 +394,13 @@ func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (m fixedVersion = "Not Specified" } + var packagePath string + if vulnerability.PkgPath != "" { + packagePath = vulnerability.PkgPath + } else { + packagePath = "Not Specified" + } + _, ok := cveidMap[vulnerability.VulnerabilityID] if ok { cveDetailStruct := cveidMap[vulnerability.VulnerabilityID] @@ -404,6 +411,7 @@ func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (m pkgList, cvemodel.Package{ Name: pkgName, + PackagePath: packagePath, InstalledVersion: installedVersion, FixedVersion: fixedVersion, }, @@ -419,6 +427,7 @@ func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (m newPkgList, cvemodel.Package{ Name: pkgName, + PackagePath: packagePath, InstalledVersion: installedVersion, FixedVersion: fixedVersion, }, diff --git a/pkg/extensions/search/cve/trivy/scanner_test.go b/pkg/extensions/search/cve/trivy/scanner_test.go index 54c7aabfa..00faebb10 100644 --- a/pkg/extensions/search/cve/trivy/scanner_test.go +++ b/pkg/extensions/search/cve/trivy/scanner_test.go @@ -206,6 +206,84 @@ func TestVulnerableLayer(t *testing.T) { So(cveMap, ShouldContainKey, "CVE-2023-3817") So(cveMap, ShouldContainKey, "CVE-2023-3446") }) + + Convey("Vulnerable layer with vulnerability in language-specific file", t, func() { + vulnerableLayer, err := GetLayerWithLanguageFileVulnerability() + So(err, ShouldBeNil) + + created, err := time.Parse(time.RFC3339, "2024-02-15T09:56:01.500079786Z") + So(err, ShouldBeNil) + + config := ispec.Image{ + Created: &created, + Platform: ispec.Platform{ + Architecture: "amd64", + OS: "linux", + }, + Config: ispec.ImageConfig{ + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + }, + RootFS: ispec.RootFS{ + Type: "layers", + DiffIDs: []godigest.Digest{"sha256:d789b0723f3e6e5064d612eb3c84071cc84a7cf7921d549642252c3295e5f937"}, + }, + } + + img := CreateImageWith(). + LayerBlobs([][]byte{vulnerableLayer}). + ImageConfig(config). + Build() + + tempDir := t.TempDir() + + log := log.NewLogger("debug", "") + imageStore := local.NewImageStore(tempDir, false, false, + log, monitoring.NewMetricsServer(false, log), nil, nil) + + storeController := storage.StoreController{ + DefaultStore: imageStore, + } + + err = WriteImageToFileSystem(img, "repo", img.DigestStr(), storeController) + So(err, ShouldBeNil) + + params := boltdb.DBParameters{ + RootDir: tempDir, + } + boltDriver, err := boltdb.GetBoltDriver(params) + So(err, ShouldBeNil) + + metaDB, err := boltdb.New(boltDriver, log) + So(err, ShouldBeNil) + + err = meta.ParseStorage(metaDB, storeController, log) + So(err, ShouldBeNil) + + scanner := trivy.NewScanner(storeController, metaDB, "ghcr.io/project-zot/trivy-db", + "ghcr.io/aquasecurity/trivy-java-db", log) + + err = scanner.UpdateDB(context.Background()) + So(err, ShouldBeNil) + + cveMap, err := scanner.ScanImage(context.Background(), "repo@"+img.DigestStr()) + So(err, ShouldBeNil) + t.Logf("cveMap: %v", cveMap) + + // As of Feb 15 2024, there is 1 CVE in this layer: + So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 1) + So(cveMap, ShouldContainKey, "CVE-2016-1000027") + + cveData := cveMap["CVE-2016-1000027"] + vulnerablePackages := cveData.PackageList + + // There is only 1 vulnerable package in this layer + So(len(vulnerablePackages), ShouldEqual, 1) + vulnerableSpringWebPackage := vulnerablePackages[0] + So(vulnerableSpringWebPackage.Name, ShouldEqual, "org.springframework:spring-web") + So(vulnerableSpringWebPackage.InstalledVersion, ShouldEqual, "5.3.31") + So(vulnerableSpringWebPackage.FixedVersion, ShouldEqual, "6.0.0") + So(vulnerableSpringWebPackage.PackagePath, ShouldEqual, "usr/local/artifacts/spring-web-5.3.31.jar") + }) } func TestScannerErrors(t *testing.T) { diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 675b17cf2..2e97e8ba8 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -145,6 +145,7 @@ type ComplexityRoot struct { FixedVersion func(childComplexity int) int InstalledVersion func(childComplexity int) int Name func(childComplexity int) int + PackagePath func(childComplexity int) int } PageInfo struct { @@ -737,6 +738,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PackageInfo.Name(childComplexity), true + case "PackageInfo.PackagePath": + if e.complexity.PackageInfo.PackagePath == nil { + break + } + + return e.complexity.PackageInfo.PackagePath(childComplexity), true + case "PageInfo.ItemCount": if e.complexity.PageInfo.ItemCount == nil { break @@ -1275,6 +1283,10 @@ type PackageInfo { """ Name: String """ + Path where the vulnerable package is located + """ + PackagePath: String + """ Current version of the package, typically affected by the CVE """ InstalledVersion: String @@ -2749,6 +2761,8 @@ func (ec *executionContext) fieldContext_CVE_PackageList(ctx context.Context, fi switch field.Name { case "Name": return ec.fieldContext_PackageInfo_Name(ctx, field) + case "PackagePath": + return ec.fieldContext_PackageInfo_PackagePath(ctx, field) case "InstalledVersion": return ec.fieldContext_PackageInfo_InstalledVersion(ctx, field) case "FixedVersion": @@ -5431,6 +5445,47 @@ func (ec *executionContext) fieldContext_PackageInfo_Name(ctx context.Context, f return fc, nil } +func (ec *executionContext) _PackageInfo_PackagePath(ctx context.Context, field graphql.CollectedField, obj *PackageInfo) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PackageInfo_PackagePath(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PackagePath, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2áš–string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PackageInfo_PackagePath(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PackageInfo", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _PackageInfo_InstalledVersion(ctx context.Context, field graphql.CollectedField, obj *PackageInfo) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PackageInfo_InstalledVersion(ctx, field) if err != nil { @@ -10318,6 +10373,8 @@ func (ec *executionContext) _PackageInfo(ctx context.Context, sel ast.SelectionS out.Values[i] = graphql.MarshalString("PackageInfo") case "Name": out.Values[i] = ec._PackageInfo_Name(ctx, field, obj) + case "PackagePath": + out.Values[i] = ec._PackageInfo_PackagePath(ctx, field, obj) case "InstalledVersion": out.Values[i] = ec._PackageInfo_InstalledVersion(ctx, field, obj) case "FixedVersion": diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 00a82c757..75483546f 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -209,6 +209,8 @@ type ManifestSummary struct { type PackageInfo struct { // Name of the package affected by a CVE Name *string `json:"Name,omitempty"` + // Path where the vulnerable package is located + PackagePath *string `json:"PackagePath,omitempty"` // Current version of the package, typically affected by the CVE InstalledVersion *string `json:"InstalledVersion,omitempty"` // Minimum version of the package in which the CVE is fixed diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index da7ea2c80..77dd17136 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -241,6 +241,7 @@ func getCVEListForImage( pkgList = append(pkgList, &gql_generated.PackageInfo{ Name: &pkg.Name, + PackagePath: &pkg.PackagePath, InstalledVersion: &pkg.InstalledVersion, FixedVersion: &pkg.FixedVersion, }, diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 5ea816296..e0cbe2f53 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -73,6 +73,10 @@ type PackageInfo { """ Name: String """ + Path where the vulnerable package is located + """ + PackagePath: String + """ Current version of the package, typically affected by the CVE """ InstalledVersion: String diff --git a/pkg/test/image-utils/utils.go b/pkg/test/image-utils/utils.go index 97f53150d..f44849375 100644 --- a/pkg/test/image-utils/utils.go +++ b/pkg/test/image-utils/utils.go @@ -29,23 +29,34 @@ func GetLayerWithVulnerability() ([]byte, error) { if vulnerableLayer != nil { return vulnerableLayer, nil } + // this is the path of the blob relative to the root of the zot folder + vulnBlobPath := "test/data/alpine/blobs/sha256/f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09" + vulnerableLayer, err := GetLayerRelativeToProjectRoot(vulnBlobPath) + + return vulnerableLayer, err +} + +func GetLayerWithLanguageFileVulnerability() ([]byte, error) { + vulnBlobPath := "test/data/spring-web/blobs/sha256/506c47a6827e325a63d4b38c7ce656e07d5e98a09d748ec7ac989a45af7d6567" + vulnerableLayerWithLanguageFile, err := GetLayerRelativeToProjectRoot(vulnBlobPath) + return vulnerableLayerWithLanguageFile, err +} + +func GetLayerRelativeToProjectRoot(pathToLayerBlob string) ([]byte, error) { projectRootDir, err := tcommon.GetProjectRootDir() if err != nil { return nil, err } - // this is the path of the blob relative to the root of the zot folder - vulnBlobPath := "test/data/alpine/blobs/sha256/f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09" - - absoluteVulnBlobPath, _ := filepath.Abs(filepath.Join(projectRootDir, vulnBlobPath)) + absoluteBlobPath, _ := filepath.Abs(filepath.Join(projectRootDir, pathToLayerBlob)) - vulnerableLayer, err := os.ReadFile(absoluteVulnBlobPath) //nolint: lll + layer, err := os.ReadFile(absoluteBlobPath) //nolint: lll if err != nil { return nil, err } - return vulnerableLayer, nil + return layer, nil } func GetDefaultLayers() []Layer {