0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat: include PackagePath data in CVEs for image queries (#2241)

Signed-off-by: Vishwas Rajashekar <vrajashe@cisco.com>
This commit is contained in:
Vishwas R 2024-02-16 02:49:49 +05:30 committed by GitHub
parent cc2eda0335
commit 0aa6bf0fff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 227 additions and 17 deletions

View file

@ -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}

View file

@ -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)
})

View file

@ -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,

View file

@ -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"`
}

View file

@ -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")

View file

@ -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"`
}

View file

@ -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,
},

View file

@ -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) {

View file

@ -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":

View file

@ -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

View file

@ -241,6 +241,7 @@ func getCVEListForImage(
pkgList = append(pkgList,
&gql_generated.PackageInfo{
Name: &pkg.Name,
PackagePath: &pkg.PackagePath,
InstalledVersion: &pkg.InstalledVersion,
FixedVersion: &pkg.FixedVersion,
},

View file

@ -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

View file

@ -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"
absoluteBlobPath, _ := filepath.Abs(filepath.Join(projectRootDir, pathToLayerBlob))
absoluteVulnBlobPath, _ := filepath.Abs(filepath.Join(projectRootDir, vulnBlobPath))
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 {