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:
parent
cc2eda0335
commit
0aa6bf0fff
13 changed files with 227 additions and 17 deletions
1
Makefile
1
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}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -241,6 +241,7 @@ func getCVEListForImage(
|
|||
pkgList = append(pkgList,
|
||||
&gql_generated.PackageInfo{
|
||||
Name: &pkg.Name,
|
||||
PackagePath: &pkg.PackagePath,
|
||||
InstalledVersion: &pkg.InstalledVersion,
|
||||
FixedVersion: &pkg.FixedVersion,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue