Files
zot/pkg/extensions/search/common/common_test.go
T
Shivam Mishra b61aff62cd check notary v2 signature while looking for available signatures
expanded repo info also provides information if manifests of repo is signed or not
previously it was looking for only cosign signature.

Signed-off-by: Shivam Mishra <shimish2@cisco.com>
2022-06-03 17:45:22 -07:00

662 lines
18 KiB
Go

//go:build extended
// +build extended
package common_test
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"testing"
"time"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sigstore/cosign/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search/common"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/storage"
. "zotregistry.io/zot/pkg/test"
)
const (
graphqlQueryPrefix = constants.ExtSearchPrefix
)
// nolint:gochecknoglobals
var (
rootDir string
subRootDir string
)
type ImgResponsWithLatestTag struct {
ImgListWithLatestTag ImgListWithLatestTag `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type ExpandedRepoInfoResp struct {
ExpandedRepoInfo ExpandedRepoInfo `json:"data"`
Errors []ErrorGQL `json:"errors"`
}
type ExpandedRepoInfo struct {
RepoInfo common.RepoInfo `json:"expandedRepoInfo"`
}
//nolint:tagliatelle // graphQL schema
type ImgListWithLatestTag struct {
Images []ImageInfo `json:"ImageListWithLatestTag"`
}
type ErrorGQL struct {
Message string `json:"message"`
Path []string `json:"path"`
}
type ImageInfo struct {
Name string
Latest string
LastUpdated time.Time
Description string
Licenses string
Vendor string
Size string
Labels string
}
func testSetup(t *testing.T, subpath string) error {
t.Helper()
dir := t.TempDir()
subDir := t.TempDir()
rootDir = dir
subRootDir = path.Join(subDir, subpath)
err := CopyFiles("../../../../test/data", rootDir)
if err != nil {
return err
}
return CopyFiles("../../../../test/data", subRootDir)
}
func signUsingCosign(port string) error {
cwd, err := os.Getwd()
So(err, ShouldBeNil)
defer func() { _ = os.Chdir(cwd) }()
tdir, err := ioutil.TempDir("", "cosign")
if err != nil {
return err
}
defer os.RemoveAll(tdir)
_ = os.Chdir(tdir)
// generate a keypair
os.Setenv("COSIGN_PASSWORD", "")
err = generate.GenerateKeyPairCmd(context.TODO(), "", nil)
if err != nil {
return err
}
imageURL := fmt.Sprintf("localhost:%s/%s@%s", port, "zot-cve-test",
"sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29")
// sign the image
return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
options.RegistryOptions{AllowInsecure: true},
map[string]interface{}{"tag": "1.0"},
[]string{imageURL},
"", "", true, "", "", "", false, false, "")
}
func signUsingNotary(port string) error {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer func() { _ = os.Chdir(cwd) }()
tdir, err := ioutil.TempDir("", "notation")
if err != nil {
return err
}
defer os.RemoveAll(tdir)
_ = os.Chdir(tdir)
_, err = exec.LookPath("notation")
if err != nil {
return err
}
os.Setenv("XDG_CONFIG_HOME", tdir)
// generate a keypair
cmd := exec.Command("notation", "cert", "generate-test", "--trust", "notation-sign-test")
err = cmd.Run()
if err != nil {
return err
}
// sign the image
image := fmt.Sprintf("localhost:%s/%s:%s", port, "zot-cve-test", "0.0.1")
cmd = exec.Command("notation", "sign", "--key", "notation-sign-test", "--plain-http", image)
return cmd.Run()
}
func getTags() ([]common.TagInfo, []common.TagInfo) {
tags := make([]common.TagInfo, 0)
firstTag := common.TagInfo{
Name: "1.0.0",
Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb",
Timestamp: time.Now(),
}
secondTag := common.TagInfo{
Name: "1.0.1",
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
Timestamp: time.Now(),
}
thirdTag := common.TagInfo{
Name: "1.0.2",
Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb",
Timestamp: time.Now(),
}
fourthTag := common.TagInfo{
Name: "1.0.3",
Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb",
Timestamp: time.Now(),
}
tags = append(tags, firstTag, secondTag, thirdTag, fourthTag)
infectedTags := make([]common.TagInfo, 0)
infectedTags = append(infectedTags, secondTag)
return tags, infectedTags
}
func TestImageFormat(t *testing.T) {
Convey("Test valid image", t, func() {
log := log.NewLogger("debug", "")
dbDir := "../../../../test/data"
metrics := monitoring.NewMetricsServer(false, log)
defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay, false, false, log, metrics)
storeController := storage.StoreController{DefaultStore: defaultStore}
olu := common.NewOciLayoutUtils(storeController, log)
isValidImage, err := olu.IsValidImageFormat("zot-test")
So(err, ShouldBeNil)
So(isValidImage, ShouldEqual, true)
isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.1")
So(err, ShouldBeNil)
So(isValidImage, ShouldEqual, true)
isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.")
So(err, ShouldBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-noindex-test")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot--tet")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-noindex-test")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-noblobs")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-index")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-blob")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-test:0.3.22-squashfs")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
isValidImage, err = olu.IsValidImageFormat("zot-nonreadable-test")
So(err, ShouldNotBeNil)
So(isValidImage, ShouldEqual, false)
})
}
func TestLatestTagSearchHTTP(t *testing.T) {
Convey("Test latest image search by timestamp", t, func() {
subpath := "/a"
err := testSetup(t, subpath)
if err != nil {
panic(err)
}
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = rootDir
conf.Storage.SubPaths = make(map[string]config.StorageConfig)
conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir}
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go func() {
// this blocks
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(baseURL)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
// shut down server
defer func() {
ctx := context.Background()
_ = ctlr.Server.Shutdown(ctx)
}()
resp, err := resty.R().Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
var responseStruct ImgResponsWithLatestTag
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ImgListWithLatestTag.Images), ShouldEqual, 4)
images := responseStruct.ImgListWithLatestTag.Images
So(images[0].Latest, ShouldEqual, "0.0.1")
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
err = os.Chmod(rootDir, 0o000)
if err != nil {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ImgListWithLatestTag.Images), ShouldEqual, 0)
err = os.Chmod(rootDir, 0o755)
if err != nil {
panic(err)
}
var manifestDigest digest.Digest
var configDigest digest.Digest
manifestDigest, configDigest, _ = GetOciLayoutDigests("../../../../test/data/zot-test")
// Delete config blob and try.
err = os.Remove(path.Join(subRootDir, "zot-test/blobs/sha256", configDigest.Encoded()))
if err != nil {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = os.Remove(path.Join(subRootDir, "zot-test/blobs/sha256",
manifestDigest.Encoded()))
if err != nil {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", configDigest.Encoded()))
if err != nil {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
// Delete manifest blob also and try
err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded()))
if err != nil {
panic(err)
}
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={ImageListWithLatestTag(){Name%20Latest}}")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
})
}
func TestExpandedRepoInfo(t *testing.T) {
Convey("Test expanded repo info", t, func() {
subpath := "/a"
err := testSetup(t, subpath)
if err != nil {
panic(err)
}
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
conf.Storage.RootDirectory = rootDir
conf.Storage.SubPaths = make(map[string]config.StorageConfig)
conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir}
defaultVal := true
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{Enable: &defaultVal},
}
conf.Extensions.Search.CVE = nil
ctlr := api.NewController(conf)
go func() {
// this blocks
if err := ctlr.Run(context.Background()); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(baseURL)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
// shut down server
defer func() {
ctx := context.Background()
_ = ctlr.Server.Shutdown(ctx)
}()
resp, err := resty.R().Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
query := "{ExpandedRepoInfo(repo:\"zot-cve-test\"){Manifests%20{Digest%20IsSigned%20Tag%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := &ExpandedRepoInfoResp{}
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
So(m.IsSigned, ShouldEqual, false)
}
}
err = signUsingCosign(port)
So(err, ShouldBeNil)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
if m.Digest == "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" {
So(m.IsSigned, ShouldEqual, true)
}
}
query = "{ExpandedRepoInfo(repo:\"\"){Manifests%20{Digest%20Tag%20IsSigned%20Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
query = "{ExpandedRepoInfo(repo:\"zot-test\"){Manifests%20{Digest%20Tag%20IsSigned%20%Layers%20{Size%20Digest}}}}"
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
So(m.IsSigned, ShouldEqual, false)
}
}
err = signUsingNotary(port)
So(err, ShouldBeNil)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "/query?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests), ShouldNotEqual, 0)
So(len(responseStruct.ExpandedRepoInfo.RepoInfo.Manifests[0].Layers), ShouldNotEqual, 0)
for _, m := range responseStruct.ExpandedRepoInfo.RepoInfo.Manifests {
if m.Digest == "2bacca16b9df395fc855c14ccf50b12b58d35d468b8e7f25758aff90f89bf396" {
So(m.IsSigned, ShouldEqual, true)
}
}
var manifestDigest digest.Digest
manifestDigest, _, _ = GetOciLayoutDigests("../../../../test/data/zot-test")
err = os.Remove(path.Join(rootDir, "zot-test/blobs/sha256", manifestDigest.Encoded()))
So(err, ShouldBeNil)
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + query)
So(resp, ShouldNotBeNil)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), responseStruct)
So(err, ShouldBeNil)
})
}
func TestUtilsMethod(t *testing.T) {
Convey("Test utils", t, func() {
// Test GetRepo method
repo := common.GetRepo("test")
So(repo, ShouldEqual, "test")
repo = common.GetRepo(":")
So(repo, ShouldEqual, "")
repo = common.GetRepo("")
So(repo, ShouldEqual, "")
repo = common.GetRepo("test:123")
So(repo, ShouldEqual, "test")
repo = common.GetRepo("a/test:123")
So(repo, ShouldEqual, "a/test")
repo = common.GetRepo("a/test:123:456")
So(repo, ShouldEqual, "a/test")
// Test various labels
labels := make(map[string]string)
desc := common.GetDescription(labels)
So(desc, ShouldEqual, "")
license := common.GetLicense(labels)
So(license, ShouldEqual, "")
vendor := common.GetVendor(labels)
So(vendor, ShouldEqual, "")
categories := common.GetCategories(labels)
So(categories, ShouldEqual, "")
labels[ispec.AnnotationVendor] = "zot"
labels[ispec.AnnotationDescription] = "zot-desc"
labels[ispec.AnnotationLicenses] = "zot-license"
labels[common.AnnotationLabels] = "zot-labels"
desc = common.GetDescription(labels)
So(desc, ShouldEqual, "zot-desc")
license = common.GetLicense(labels)
So(license, ShouldEqual, "zot-license")
vendor = common.GetVendor(labels)
So(vendor, ShouldEqual, "zot")
categories = common.GetCategories(labels)
So(categories, ShouldEqual, "zot-labels")
labels = make(map[string]string)
// Use diff key
labels[common.LabelAnnotationVendor] = "zot-vendor"
labels[common.LabelAnnotationDescription] = "zot-label-desc"
labels[common.LabelAnnotationLicenses] = "zot-label-license"
desc = common.GetDescription(labels)
So(desc, ShouldEqual, "zot-label-desc")
license = common.GetLicense(labels)
So(license, ShouldEqual, "zot-label-license")
vendor = common.GetVendor(labels)
So(vendor, ShouldEqual, "zot-vendor")
routePrefix := common.GetRoutePrefix("test:latest")
So(routePrefix, ShouldEqual, "/")
routePrefix = common.GetRoutePrefix("a/test:latest")
So(routePrefix, ShouldEqual, "/a")
routePrefix = common.GetRoutePrefix("a/b/test:latest")
So(routePrefix, ShouldEqual, "/a")
allTags, infectedTags := getTags()
latestTag := common.GetLatestTag(allTags)
So(latestTag.Name, ShouldEqual, "1.0.3")
fixedTags := common.GetFixedTags(allTags, infectedTags)
So(len(fixedTags), ShouldEqual, 2)
log := log.NewLogger("debug", "")
rootDir := t.TempDir()
subRootDir := t.TempDir()
metrics := monitoring.NewMetricsServer(false, log)
defaultStore := storage.NewImageStore(rootDir, false, storage.DefaultGCDelay, false, false, log, metrics)
subStore := storage.NewImageStore(subRootDir, false, storage.DefaultGCDelay, false, false, log, metrics)
subStoreMap := make(map[string]storage.ImageStore)
subStoreMap["/b"] = subStore
storeController := storage.StoreController{DefaultStore: defaultStore, SubStore: subStoreMap}
dir := common.GetRootDir("a/zot-cve-test", storeController)
So(dir, ShouldEqual, rootDir)
dir = common.GetRootDir("b/zot-cve-test", storeController)
So(dir, ShouldEqual, subRootDir)
})
}