diff --git a/pkg/storage/scrub.go b/pkg/storage/scrub.go index fb3ad108..1a8073e8 100644 --- a/pkg/storage/scrub.go +++ b/pkg/storage/scrub.go @@ -115,67 +115,103 @@ func CheckRepo(imageName string, imgStore ImageStore) ([]ScrubImageResult, error return results, errors.ErrRepoNotFound } - for _, m := range index.Manifests { - tag, ok := m.Annotations[ispec.AnnotationRefName] - if ok { - imageResult := checkIntegrity(ctxUmoci, imageName, tag, oci, m, dir) - results = append(results, imageResult) + listOfManifests := []ispec.Descriptor{} + + for _, manifest := range index.Manifests { + if manifest.MediaType == ispec.MediaTypeImageIndex { + buf, err := os.ReadFile(path.Join(dir, "blobs", manifest.Digest.Algorithm().String(), manifest.Digest.Encoded())) + if err != nil { + tagName := manifest.Annotations[ispec.AnnotationRefName] + imgRes := getResult(imageName, tagName, errors.ErrBadBlobDigest) + results = append(results, imgRes) + + continue + } + + var idx ispec.Index + if err := json.Unmarshal(buf, &idx); err != nil { + tagName := manifest.Annotations[ispec.AnnotationRefName] + imgRes := getResult(imageName, tagName, errors.ErrBadBlobDigest) + results = append(results, imgRes) + + continue + } + + listOfManifests = append(listOfManifests, idx.Manifests...) + } else if manifest.MediaType == ispec.MediaTypeImageManifest { + listOfManifests = append(listOfManifests, manifest) } } + for _, m := range listOfManifests { + tag := m.Annotations[ispec.AnnotationRefName] + imageResult := CheckIntegrity(ctxUmoci, imageName, tag, oci, m, dir) + results = append(results, imageResult) + } + return results, nil } -func checkIntegrity(ctx context.Context, imageName, tagName string, oci casext.Engine, manifest ispec.Descriptor, dir string) ScrubImageResult { //nolint: lll +func CheckIntegrity(ctx context.Context, imageName, tagName string, oci casext.Engine, manifest ispec.Descriptor, dir string) ScrubImageResult { //nolint: lll // check manifest and config - stat, err := umoci.Stat(ctx, oci, manifest) + if _, err := umoci.Stat(ctx, oci, manifest); err != nil { + return getResult(imageName, tagName, err) + } + // check layers + return CheckLayers(imageName, tagName, dir, manifest) +} + +func CheckLayers(imageName, tagName, dir string, manifest ispec.Descriptor) ScrubImageResult { imageRes := ScrubImageResult{} + buf, err := os.ReadFile(path.Join(dir, "blobs", manifest.Digest.Algorithm().String(), manifest.Digest.Encoded())) if err != nil { imageRes = getResult(imageName, tagName, err) - } else { - // check layers - for _, s := range stat.History { - layer := s.Layer - if layer == nil { - continue - } - // check layer - layerPath := path.Join(dir, "blobs", layer.Digest.Algorithm().String(), layer.Digest.Encoded()) + return imageRes + } - _, err = os.Stat(layerPath) - if err != nil { - imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) + var man ispec.Manifest + if err := json.Unmarshal(buf, &man); err != nil { + imageRes = getResult(imageName, tagName, err) - break - } + return imageRes + } - layerFh, err := os.Open(layerPath) - if err != nil { - imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) + for _, layer := range man.Layers { + layerPath := path.Join(dir, "blobs", layer.Digest.Algorithm().String(), layer.Digest.Encoded()) - break - } + _, err = os.Stat(layerPath) + if err != nil { + imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) - computedDigest, err := godigest.FromReader(layerFh) - layerFh.Close() - - if err != nil { - imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest) - - break - } - - if computedDigest != layer.Digest { - imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest) - - break - } - - imageRes = getResult(imageName, tagName, nil) + break } + + layerFh, err := os.Open(layerPath) + if err != nil { + imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) + + break + } + + computedDigest, err := godigest.FromReader(layerFh) + layerFh.Close() + + if err != nil { + imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest) + + break + } + + if computedDigest != layer.Digest { + imageRes = getResult(imageName, tagName, errors.ErrBadBlobDigest) + + break + } + + imageRes = getResult(imageName, tagName, nil) } return imageRes diff --git a/pkg/storage/scrub_test.go b/pkg/storage/scrub_test.go index 45c95dc7..0e1de4a2 100644 --- a/pkg/storage/scrub_test.go +++ b/pkg/storage/scrub_test.go @@ -3,13 +3,11 @@ package storage_test import ( "bytes" "encoding/json" - "fmt" "os" "path" "regexp" "strings" "testing" - "time" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -20,10 +18,12 @@ import ( "zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage/cache" "zotregistry.io/zot/pkg/storage/local" + "zotregistry.io/zot/pkg/test" ) const ( repoName = "test" + tag = "1.0" ) func TestCheckAllBlobsIntegrity(t *testing.T) { @@ -53,89 +53,25 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { storeCtlr := storage.StoreController{} storeCtlr.DefaultStore = imgStore - const tag = "1.0" - - var manifestDigest godigest.Digest - var configDigest godigest.Digest - var layerDigest godigest.Digest - var manifest string - var config string - var layer string - - // create layer digest - body := []byte("this is a blob") - buf := bytes.NewBuffer(body) - buflen := buf.Len() - digest := godigest.FromBytes(body) - upload, n, err := imgStore.FullBlobUpload(repoName, buf, digest) - So(err, ShouldBeNil) - So(n, ShouldEqual, len(body)) - So(upload, ShouldNotBeEmpty) - layerDigest = digest - layer = digest.String() - - // create config digest - created := time.Now().Format("2006-01-02T15:04:05Z") - configBody := []byte(fmt.Sprintf(`{ - "created": "%v", - "architecture": "amd64", - "os": "linux", - "rootfs": { - "type": "layers", - "diff_ids": [ - "", - "" - ] - }, - "history": [ - { - "created": "%v", - "created_by": "" - }, - { - "created": "%v", - "created_by": "", - "empty_layer": true - } - ] - }`, created, created, created)) - configBuf := bytes.NewBuffer(configBody) - configLen := configBuf.Len() - configDigest = godigest.FromBytes(configBody) - uConfig, nConfig, err := imgStore.FullBlobUpload(repoName, configBuf, configDigest) - So(err, ShouldBeNil) - So(nConfig, ShouldEqual, len(configBody)) - So(uConfig, ShouldNotBeEmpty) - config = configDigest.String() - - // create manifest and add it to the repository - annotationsMap := make(map[string]string) - annotationsMap[ispec.AnnotationRefName] = tag - mnfst := ispec.Manifest{ - Config: ispec.Descriptor{ - MediaType: "application/vnd.oci.image.config.v1+json", - Digest: configDigest, - Size: int64(configLen), - }, - Layers: []ispec.Descriptor{ - { - MediaType: "application/vnd.oci.image.layer.v1.tar", - Digest: digest, - Size: int64(buflen), - }, - }, - Annotations: annotationsMap, - } - - mnfst.SchemaVersion = 2 - mbytes, err := json.Marshal(mnfst) + config, layers, manifest, err := test.GetImageComponents(1000) So(err, ShouldBeNil) - manifestDigest, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, - mbytes) + layerReader := bytes.NewReader(layers[0]) + layerDigest := godigest.FromBytes(layers[0]) + _, _, err = imgStore.FullBlobUpload(repoName, layerReader, layerDigest) So(err, ShouldBeNil) - manifest = manifestDigest.String() + configBlob, err := json.Marshal(config) + So(err, ShouldBeNil) + configReader := bytes.NewReader(configBlob) + configDigest := godigest.FromBytes(configBlob) + _, _, err = imgStore.FullBlobUpload(repoName, configReader, configDigest) + So(err, ShouldBeNil) + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + manifestDigest, err := imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBlob) + So(err, ShouldBeNil) Convey("Blobs integrity not affected", func() { buff := bytes.NewBufferString("") @@ -153,12 +89,12 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { Convey("Manifest integrity affected", func() { // get content of manifest file - content, _, _, err := imgStore.GetImageManifest(repoName, manifest) + content, _, _, err := imgStore.GetImageManifest(repoName, manifestDigest.String()) So(err, ShouldBeNil) // delete content of manifest file - manifest = strings.ReplaceAll(manifest, "sha256:", "") - manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifest) + manifestDig := manifestDigest.Encoded() + manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig) err = os.Truncate(manifestFile, 0) So(err, ShouldBeNil) @@ -175,6 +111,17 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { // verify error message So(actual, ShouldContainSubstring, "test 1.0 affected parse application/vnd.oci.image.manifest.v1+json") + index, err := storage.GetIndex(imgStore, repoName, log.With().Caller().Logger()) + So(err, ShouldBeNil) + + So(len(index.Manifests), ShouldEqual, 1) + manifestDescriptor := index.Manifests[0] + + repoDir := path.Join(dir, repoName) + imageRes := storage.CheckLayers(repoName, tag, repoDir, manifestDescriptor) + So(imageRes.Status, ShouldEqual, "affected") + So(imageRes.Error, ShouldEqual, "unexpected end of JSON input") + // put manifest content back to file err = os.WriteFile(manifestFile, content, 0o600) So(err, ShouldBeNil) @@ -186,8 +133,8 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { So(err, ShouldBeNil) // delete content of config file - config = strings.ReplaceAll(config, "sha256:", "") - configFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", config) + configDig := configDigest.Encoded() + configFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", configDig) err = os.Truncate(configFile, 0) So(err, ShouldBeNil) @@ -214,8 +161,8 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { So(err, ShouldBeNil) // delete content of layer file - layer = strings.ReplaceAll(layer, "sha256:", "") - layerFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", layer) + layerDig := layerDigest.Encoded() + layerFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", layerDig) err = os.Truncate(layerFile, 0) So(err, ShouldBeNil) @@ -237,9 +184,26 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { }) Convey("Layer not found", func() { + // change layer file permissions + layerDig := layerDigest.Encoded() + repoDir := path.Join(dir, repoName) + layerFile := path.Join(repoDir, "/blobs/sha256", layerDig) + err = os.Chmod(layerFile, 0x0200) + So(err, ShouldBeNil) + + index, err := storage.GetIndex(imgStore, repoName, log.With().Caller().Logger()) + So(err, ShouldBeNil) + + So(len(index.Manifests), ShouldEqual, 1) + manifestDescriptor := index.Manifests[0] + + imageRes := storage.CheckLayers(repoName, tag, repoDir, manifestDescriptor) + So(imageRes.Status, ShouldEqual, "affected") + So(imageRes.Error, ShouldEqual, "blob: not found") + err = os.Chmod(layerFile, 0x0600) + So(err, ShouldBeNil) + // delete layer file - layer = strings.ReplaceAll(layer, "sha256:", "") - layerFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", layer) err = os.Remove(layerFile) So(err, ShouldBeNil) @@ -255,5 +219,122 @@ func TestCheckAllBlobsIntegrity(t *testing.T) { So(actual, ShouldContainSubstring, "IMAGE NAME TAG STATUS ERROR") So(actual, ShouldContainSubstring, "test 1.0 affected blob: not found") }) + + Convey("Scrub index", func() { + newConfig, newLayers, newManifest, err := test.GetImageComponents(10) + So(err, ShouldBeNil) + + newLayerReader := bytes.NewReader(newLayers[0]) + newLayerDigest := godigest.FromBytes(newLayers[0]) + _, _, err = imgStore.FullBlobUpload(repoName, newLayerReader, newLayerDigest) + So(err, ShouldBeNil) + + newConfigBlob, err := json.Marshal(newConfig) + So(err, ShouldBeNil) + newConfigReader := bytes.NewReader(newConfigBlob) + newConfigDigest := godigest.FromBytes(newConfigBlob) + _, _, err = imgStore.FullBlobUpload(repoName, newConfigReader, newConfigDigest) + So(err, ShouldBeNil) + + newManifestBlob, err := json.Marshal(newManifest) + So(err, ShouldBeNil) + newManifestReader := bytes.NewReader(newManifestBlob) + newManifestDigest := godigest.FromBytes(newManifestBlob) + _, _, err = imgStore.FullBlobUpload(repoName, newManifestReader, newManifestDigest) + So(err, ShouldBeNil) + + var index ispec.Index + index.SchemaVersion = 2 + index.Manifests = []ispec.Descriptor{ + { + MediaType: ispec.MediaTypeImageManifest, + Digest: newManifestDigest, + Size: int64(len(newManifestBlob)), + }, + } + + indexBlob, err := json.Marshal(index) + So(err, ShouldBeNil) + indexDigest, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := storeCtlr.CheckAllBlobsIntegrity() + res.PrintScrubResults(buff) + So(err, ShouldBeNil) + + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "IMAGE NAME TAG STATUS ERROR") + So(actual, ShouldContainSubstring, "test 1.0 ok") + So(actual, ShouldContainSubstring, "test ok") + + // test scrub index - errors + indexFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", indexDigest.Encoded()) + err = os.Chmod(indexFile, 0o000) + So(err, ShouldBeNil) + + buff = bytes.NewBufferString("") + + res, err = storeCtlr.CheckAllBlobsIntegrity() + res.PrintScrubResults(buff) + So(err, ShouldBeNil) + + str = space.ReplaceAllString(buff.String(), " ") + actual = strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "IMAGE NAME TAG STATUS ERROR") + So(actual, ShouldContainSubstring, "test affected") + + err = os.Chmod(indexFile, 0o600) + So(err, ShouldBeNil) + + err = os.Truncate(indexFile, 0) + So(err, ShouldBeNil) + + buff = bytes.NewBufferString("") + + res, err = storeCtlr.CheckAllBlobsIntegrity() + res.PrintScrubResults(buff) + So(err, ShouldBeNil) + + str = space.ReplaceAllString(buff.String(), " ") + actual = strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "IMAGE NAME TAG STATUS ERROR") + So(actual, ShouldContainSubstring, "test affected") + }) + + Convey("Manifest not found", func() { + // delete manifest file + manifestDig := manifestDigest.Encoded() + manifestFile := path.Join(imgStore.RootDir(), repoName, "/blobs/sha256", manifestDig) + err = os.Remove(manifestFile) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := storeCtlr.CheckAllBlobsIntegrity() + res.PrintScrubResults(buff) + So(err, ShouldBeNil) + + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "IMAGE NAME TAG STATUS ERROR") + So(actual, ShouldContainSubstring, "test 1.0 affected") + So(actual, ShouldContainSubstring, "no such file or directory") + + index, err := storage.GetIndex(imgStore, repoName, log.With().Caller().Logger()) + So(err, ShouldBeNil) + + So(len(index.Manifests), ShouldEqual, 2) + manifestDescriptor := index.Manifests[0] + + repoDir := path.Join(dir, repoName) + imageRes := storage.CheckLayers(repoName, tag, repoDir, manifestDescriptor) + So(imageRes.Status, ShouldEqual, "affected") + So(imageRes.Error, ShouldContainSubstring, "no such file or directory") + }) }) }