From c61c3836db9fd472864bcf497d9fbd97269b02e3 Mon Sep 17 00:00:00 2001 From: Andreea-Lupu Date: Tue, 5 Oct 2021 12:12:22 +0300 Subject: [PATCH] implement scrub to check manifest/blob integrity Signed-off-by: Andreea-Lupu --- pkg/api/controller.go | 106 ++++++++------- pkg/api/controller_test.go | 42 ++++++ pkg/cli/root.go | 47 +++++++ pkg/cli/root_test.go | 188 +++++++++++++++++++++++++++ pkg/storage/scrub.go | 258 +++++++++++++++++++++++++++++++++++++ pkg/storage/scrub_test.go | 249 +++++++++++++++++++++++++++++++++++ 6 files changed, 841 insertions(+), 49 deletions(-) create mode 100644 pkg/storage/scrub.go create mode 100644 pkg/storage/scrub_test.go diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 99c042e6..a508a959 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -69,7 +69,6 @@ func DefaultHeaders() mux.MiddlewareFunc { } } -// nolint: gocyclo func (c *Controller) Run() error { // validate configuration if err := c.Config.Validate(c.Log); err != nil { @@ -102,6 +101,62 @@ func (c *Controller) Run() error { } c.Metrics = monitoring.NewMetricsServer(enabled, c.Log) + + if err := c.InitImageStore(); err != nil { + return err + } + + monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, c.Config.Version) + _ = NewRouteHandler(c) + + addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port) + server := &http.Server{ + Addr: addr, + Handler: c.Router, + IdleTimeout: idleTimeout, + } + c.Server = server + + // Create the listener + l, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { + if c.Config.HTTP.TLS.CACert != "" { + clientAuth := tls.VerifyClientCertIfGiven + if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") && !c.Config.HTTP.AllowReadAccess { + clientAuth = tls.RequireAndVerifyClientCert + } + + caCert, err := ioutil.ReadFile(c.Config.HTTP.TLS.CACert) + if err != nil { + panic(err) + } + + caCertPool := x509.NewCertPool() + + if !caCertPool.AppendCertsFromPEM(caCert) { + panic(errors.ErrBadCACert) + } + + server.TLSConfig = &tls.Config{ + ClientAuth: clientAuth, + ClientCAs: caCertPool, + PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, + } + server.TLSConfig.BuildNameToCertificate() // nolint: staticcheck + } + + return server.ServeTLS(l, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key) + } + + return server.Serve(l) +} + +func (c *Controller) InitImageStore() error { c.StoreController = storage.StoreController{} if c.Config.Storage.RootDirectory != "" { @@ -202,54 +257,7 @@ func (c *Controller) Run() error { ext.EnableSyncExtension(c.Config, c.wgShutDown, c.StoreController, c.Log) } - monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, c.Config.Version) - _ = NewRouteHandler(c) - - addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port) - server := &http.Server{ - Addr: addr, - Handler: c.Router, - IdleTimeout: idleTimeout, - } - c.Server = server - - // Create the listener - l, err := net.Listen("tcp", addr) - if err != nil { - return err - } - - if c.Config.HTTP.TLS != nil && c.Config.HTTP.TLS.Key != "" && c.Config.HTTP.TLS.Cert != "" { - if c.Config.HTTP.TLS.CACert != "" { - clientAuth := tls.VerifyClientCertIfGiven - if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") && !c.Config.HTTP.AllowReadAccess { - clientAuth = tls.RequireAndVerifyClientCert - } - - caCert, err := ioutil.ReadFile(c.Config.HTTP.TLS.CACert) - if err != nil { - panic(err) - } - - caCertPool := x509.NewCertPool() - - if !caCertPool.AppendCertsFromPEM(caCert) { - panic(errors.ErrBadCACert) - } - - server.TLSConfig = &tls.Config{ - ClientAuth: clientAuth, - ClientCAs: caCertPool, - PreferServerCipherSuites: true, - MinVersion: tls.VersionTLS12, - } - server.TLSConfig.BuildNameToCertificate() // nolint: staticcheck - } - - return server.ServeTLS(l, c.Config.HTTP.TLS.Cert, c.Config.HTTP.TLS.Key) - } - - return server.Serve(l) + return nil } func (c *Controller) Shutdown() { diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 146ccfe7..fe99c48f 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -95,6 +95,48 @@ func TestNew(t *testing.T) { }) } +func TestRunAlreadyRunningServer(t *testing.T) { + Convey("Run server on unavailable port", t, func() { + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + c := api.NewController(conf) + + globalDir, err := ioutil.TempDir("", "oci-repo-test") + if err != nil { + panic(err) + } + defer os.RemoveAll(globalDir) + + c.Config.Storage.RootDirectory = globalDir + + go func() { + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(baseURL) + if err == nil { + break + } + + time.Sleep(100 * time.Millisecond) + } + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + err = c.Run() + So(err, ShouldNotBeNil) + }) +} + func TestObjectStorageController(t *testing.T) { skipIt(t) Convey("Negative make a new object storage controller", t, func() { diff --git a/pkg/cli/root.go b/pkg/cli/root.go index a7bccfb1..78e34149 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -1,6 +1,9 @@ package cli import ( + "fmt" + "net/http" + glob "github.com/bmatcuk/doublestar/v4" "github.com/fsnotify/fsnotify" "github.com/mitchellh/mapstructure" @@ -80,6 +83,49 @@ func NewRootCmd() *cobra.Command { }, } + // "scrub" + scrubCmd := &cobra.Command{ + Use: "scrub ", + Aliases: []string{"scrub"}, + Short: "`scrub` checks manifest/blob integrity", + Long: "`scrub` checks manifest/blob integrity", + Run: func(cmd *cobra.Command, args []string) { + configuration := config.New() + + if len(args) > 0 { + LoadConfiguration(configuration, args[0]) + } else { + if err := cmd.Usage(); err != nil { + panic(err) + } + return + } + + // checking if the server is already running + response, err := http.Get(fmt.Sprintf("http://%s:%s/v2", configuration.HTTP.Address, configuration.HTTP.Port)) + + if err == nil { + response.Body.Close() + log.Info().Msg("The server is running, in order to perform the scrub command the server should be shut down") + panic("Error: server is running") + } else { + // server is down + c := api.NewController(configuration) + + if err := c.InitImageStore(); err != nil { + panic(err) + } + + result, err := c.StoreController.CheckAllBlobsIntegrity() + if err != nil { + panic(err) + } + + result.PrintScrubResults(cmd.OutOrStdout()) + } + }, + } + verifyCmd := &cobra.Command{ Use: "verify ", Aliases: []string{"verify"}, @@ -137,6 +183,7 @@ func NewRootCmd() *cobra.Command { } rootCmd.AddCommand(serveCmd) + rootCmd.AddCommand(scrubCmd) rootCmd.AddCommand(gcCmd) rootCmd.AddCommand(verifyCmd) diff --git a/pkg/cli/root_test.go b/pkg/cli/root_test.go index 77a1c2f6..963d9cbb 100644 --- a/pkg/cli/root_test.go +++ b/pkg/cli/root_test.go @@ -1,15 +1,23 @@ package cli_test import ( + "context" + "fmt" "io/ioutil" "os" "path" + + "gopkg.in/resty.v1" + "testing" + "time" . "github.com/smartystreets/goconvey/convey" "github.com/spf13/viper" + "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/cli" + . "zotregistry.io/zot/test" ) func TestUsage(t *testing.T) { @@ -189,3 +197,183 @@ func TestGC(t *testing.T) { So(err, ShouldBeNil) }) } + +func TestScrub(t *testing.T) { + oldArgs := os.Args + + defer func() { os.Args = oldArgs }() + + Convey("Test scrub help", t, func(c C) { + os.Args = []string{"cli_test", "scrub", "-h"} + err := cli.NewRootCmd().Execute() + So(err, ShouldBeNil) + }) + + Convey("Test scrub config", t, func(c C) { + Convey("non-existent config", func(c C) { + os.Args = []string{"cli_test", "scrub", path.Join(os.TempDir(), "/x.yaml")} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + }) + + Convey("unknown config", func(c C) { + os.Args = []string{"cli_test", "scrub", path.Join(os.TempDir(), "/x")} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + }) + + Convey("bad config", func(c C) { + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(`{"log":{}}`) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "scrub", tmpfile.Name()} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + }) + + Convey("server is running", func(c C) { + port := GetFreePort() + config := config.New() + config.HTTP.Port = port + controller := api.NewController(config) + + dir, err := ioutil.TempDir("", "scrub-test") + if err != nil { + panic(err) + } + + defer os.RemoveAll(dir) + + controller.Config.Storage.RootDirectory = dir + go func(controller *api.Controller) { + // this blocks + if err := controller.Run(); err != nil { + return + } + }(controller) + // wait till ready + for { + _, err := resty.R().Get(fmt.Sprintf("http://127.0.0.1:%s", port)) + if err == nil { + break + } + + time.Sleep(100 * time.Millisecond) + } + + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(fmt.Sprintf(`{ + "storage": { + "rootDirectory": "%s" + }, + "http": { + "port": %s + }, + "log": { + "level": "debug" + } + } + `, dir, port)) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "scrub", tmpfile.Name()} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + + defer func(controller *api.Controller) { + ctx := context.Background() + _ = controller.Server.Shutdown(ctx) + }(controller) + }) + + Convey("no image store provided", func(c C) { + port := GetFreePort() + + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(fmt.Sprintf(`{ + "storage": { + "rootDirectory": "" + }, + "http": { + "port": %s + }, + "log": { + "level": "debug" + } + } + `, port)) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + os.Args = []string{"cli_test", "scrub", tmpfile.Name()} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + }) + + Convey("bad index.json", func(c C) { + port := GetFreePort() + + dir, err := ioutil.TempDir("", "scrub-test") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + + repoName := "badIndex" + + repo, err := ioutil.TempDir(dir, repoName) + if err != nil { + panic(err) + } + + if err := os.MkdirAll(fmt.Sprintf("%s/blobs", repo), 0755); err != nil { + panic(err) + } + + if _, err = os.Stat(fmt.Sprintf("%s/oci-layout", repo)); err != nil { + content := []byte(`{"imageLayoutVersion": "1.0.0"}`) + if err = ioutil.WriteFile(fmt.Sprintf("%s/oci-layout", repo), content, 0600); err != nil { + panic(err) + } + } + + if _, err = os.Stat(fmt.Sprintf("%s/index.json", repo)); err != nil { + content := []byte(`not a JSON content`) + if err = ioutil.WriteFile(fmt.Sprintf("%s/index.json", repo), content, 0600); err != nil { + panic(err) + } + } + + tmpfile, err := ioutil.TempFile("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpfile.Name()) // clean up + content := []byte(fmt.Sprintf(`{ + "storage": { + "rootDirectory": "%s" + }, + "http": { + "port": %s + }, + "log": { + "level": "debug" + } + } + `, dir, port)) + _, err = tmpfile.Write(content) + So(err, ShouldBeNil) + err = tmpfile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "scrub", tmpfile.Name()} + So(func() { _ = cli.NewRootCmd().Execute() }, ShouldPanic) + }) + }) +} diff --git a/pkg/storage/scrub.go b/pkg/storage/scrub.go new file mode 100644 index 00000000..acdd884b --- /dev/null +++ b/pkg/storage/scrub.go @@ -0,0 +1,258 @@ +package storage + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/olekukonko/tablewriter" + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/opencontainers/umoci" + "github.com/opencontainers/umoci/oci/casext" + "zotregistry.io/zot/errors" +) + +const ( + colImageNameIndex = iota + colTagIndex + colStatusIndex + colErrorIndex + + imageNameWidth = 32 + tagWidth = 24 + statusWidth = 8 + errorWidth = 8 +) + +type ScrubImageResult struct { + ImageName string `json:"image_name"` + Tag string `json:"tag"` + Status string `json:"status"` + Error string `json:"error"` +} + +type ScrubResults struct { + ScrubResults []ScrubImageResult `json:"scrub_results"` +} + +func (sc StoreController) CheckAllBlobsIntegrity() (ScrubResults, error) { + results := ScrubResults{} + + imageStoreList := make(map[string]ImageStore) + if sc.SubStore != nil { + imageStoreList = sc.SubStore + } + + imageStoreList[""] = sc.DefaultStore + + for _, is := range imageStoreList { + images, err := is.GetRepositories() + + if err != nil { + return results, err + } + + for _, repo := range images { + imageResults, err := checkImage(repo, is) + + if err != nil { + return results, err + } + + results.ScrubResults = append(results.ScrubResults, imageResults...) + } + } + + return results, nil +} + +func checkImage(imageName string, is ImageStore) ([]ScrubImageResult, error) { + results := []ScrubImageResult{} + + dir := path.Join(is.RootDir(), imageName) + if !is.DirExists(dir) { + return results, errors.ErrRepoNotFound + } + + ctxUmoci := context.Background() + + oci, err := umoci.OpenLayout(dir) + if err != nil { + return results, err + } + + defer oci.Close() + + is.RLock() + defer is.RUnlock() + + buf, err := ioutil.ReadFile(path.Join(dir, "index.json")) + + if err != nil { + return results, err + } + + var index ispec.Index + if err := json.Unmarshal(buf, &index); err != nil { + 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) + } + } + + return results, nil +} + +func checkIntegrity(ctx context.Context, imageName, tagName string, oci casext.Engine, manifest ispec.Descriptor, + dir string) ScrubImageResult { + // check manifest and config + stat, err := umoci.Stat(ctx, oci, manifest) + + imageRes := ScrubImageResult{} + + 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.Hex()) + + _, err = os.Stat(layerPath) + if err != nil { + imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) + break + } + + f, err := os.Open(layerPath) + if err != nil { + imageRes = getResult(imageName, tagName, errors.ErrBlobNotFound) + break + } + + computedDigest, err := godigest.FromReader(f) + f.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 +} + +func getResult(imageName, tag string, err error) ScrubImageResult { + var status string + + var errField string + + if err != nil { + status = "affected" + errField = err.Error() + } else { + status = "ok" + errField = "" + } + + return ScrubImageResult{ + ImageName: imageName, + Tag: tag, + Status: status, + Error: errField, + } +} + +func getScrubTableWriter(writer io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(writer) + + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding(" ") + table.SetNoWhiteSpace(true) + table.SetColMinWidth(colImageNameIndex, imageNameWidth) + table.SetColMinWidth(colTagIndex, tagWidth) + table.SetColMinWidth(colStatusIndex, statusWidth) + table.SetColMinWidth(colErrorIndex, errorWidth) + + return table +} + +func printScrubTableHeader(writer io.Writer) { + table := getScrubTableWriter(writer) + + row := make([]string, 4) + + row[colImageNameIndex] = "IMAGE NAME" + row[colTagIndex] = "TAG" + row[colStatusIndex] = "STATUS" + row[colErrorIndex] = "ERROR" + + table.Append(row) + table.Render() +} + +func printImageResult(imageResult ScrubImageResult) string { + var builder strings.Builder + + table := getScrubTableWriter(&builder) + table.SetColMinWidth(colImageNameIndex, imageNameWidth) + table.SetColMinWidth(colTagIndex, tagWidth) + table.SetColMinWidth(colStatusIndex, statusWidth) + table.SetColMinWidth(colErrorIndex, errorWidth) + + row := make([]string, 4) + + row[colImageNameIndex] = imageResult.ImageName + row[colTagIndex] = imageResult.Tag + row[colStatusIndex] = imageResult.Status + row[colErrorIndex] = imageResult.Error + + table.Append(row) + table.Render() + + return builder.String() +} + +func (results ScrubResults) PrintScrubResults(resultWriter io.Writer) { + var builder strings.Builder + + printScrubTableHeader(&builder) + fmt.Fprint(resultWriter, builder.String()) + + for _, res := range results.ScrubResults { + imageResult := printImageResult(res) + fmt.Fprint(resultWriter, imageResult) + } +} diff --git a/pkg/storage/scrub_test.go b/pkg/storage/scrub_test.go new file mode 100644 index 00000000..7554887e --- /dev/null +++ b/pkg/storage/scrub_test.go @@ -0,0 +1,249 @@ +package storage_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "regexp" + "strings" + "testing" + "time" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + "zotregistry.io/zot/pkg/extensions/monitoring" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +const ( + repoName = "test" +) + +func TestCheckAllBlobsIntegrity(t *testing.T) { + dir, err := ioutil.TempDir("", "scrub-test") + if err != nil { + panic(err) + } + + defer os.RemoveAll(dir) + + log := log.NewLogger("debug", "") + + metrics := monitoring.NewMetricsServer(false, log) + + il := storage.NewImageStore(dir, true, true, log, metrics) + + Convey("Scrub only one repo", t, func(c C) { + // initialize repo + err = il.InitRepo(repoName) + So(err, ShouldBeNil) + ok := il.DirExists(path.Join(il.RootDir(), repoName)) + So(ok, ShouldBeTrue) + storeController := storage.StoreController{} + storeController.DefaultStore = il + So(storeController.GetImageStore(repoName), ShouldResemble, il) + + sc := storage.StoreController{} + sc.DefaultStore = il + + const tag = "1.0" + + var manifest string + var config string + var layer string + + // create layer digest + body := []byte("this is a blob") + buf := bytes.NewBuffer(body) + l := buf.Len() + d := godigest.FromBytes(body) + u, n, err := il.FullBlobUpload(repoName, buf, d.String()) + So(err, ShouldBeNil) + So(n, ShouldEqual, len(body)) + So(u, ShouldNotBeEmpty) + layer = d.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 := il.FullBlobUpload(repoName, configBuf, configDigest.String()) + 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 + m := 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: d, + Size: int64(l), + }, + }, + Annotations: annotationsMap, + } + + m.SchemaVersion = 2 + mb, _ := json.Marshal(m) + + manifest, err = il.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, mb) + So(err, ShouldBeNil) + + Convey("Blobs integrity not affected", func() { + buff := bytes.NewBufferString("") + + res, err := sc.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") + }) + + Convey("Manifest integrity affected", func() { + // get content of manifest file + content, _, _, err := il.GetImageManifest(repoName, manifest) + So(err, ShouldBeNil) + + // delete content of manifest file + manifest = strings.ReplaceAll(manifest, "sha256:", "") + manifestFile := path.Join(il.RootDir(), repoName, "/blobs/sha256", manifest) + err = os.Truncate(manifestFile, 0) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := sc.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") + // verify error message + So(actual, ShouldContainSubstring, "test 1.0 affected parse application/vnd.oci.image.manifest.v1+json") + + // put manifest content back to file + err = ioutil.WriteFile(manifestFile, content, 0600) + So(err, ShouldBeNil) + }) + + Convey("Config integrity affected", func() { + // get content of config file + content, err := il.GetBlobContent(repoName, config) + So(err, ShouldBeNil) + + // delete content of config file + config = strings.ReplaceAll(config, "sha256:", "") + configFile := path.Join(il.RootDir(), repoName, "/blobs/sha256", config) + err = os.Truncate(configFile, 0) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := sc.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 stat: parse application/vnd.oci.image.config.v1+json") + + // put config content back to file + err = ioutil.WriteFile(configFile, content, 0600) + So(err, ShouldBeNil) + }) + + Convey("Layers integrity affected", func() { + // get content of layer + content, err := il.GetBlobContent(repoName, layer) + So(err, ShouldBeNil) + + // delete content of layer file + layer = strings.ReplaceAll(layer, "sha256:", "") + layerFile := path.Join(il.RootDir(), repoName, "/blobs/sha256", layer) + err = os.Truncate(layerFile, 0) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := sc.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 blob: bad blob digest") + + // put layer content back to file + err = ioutil.WriteFile(layerFile, content, 0600) + So(err, ShouldBeNil) + }) + + Convey("Layer not found", func() { + // delete layer file + layer = strings.ReplaceAll(layer, "sha256:", "") + layerFile := path.Join(il.RootDir(), repoName, "/blobs/sha256", layer) + err = os.Remove(layerFile) + So(err, ShouldBeNil) + + buff := bytes.NewBufferString("") + + res, err := sc.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 blob: not found") + }) + }) +}