diff --git a/Dockerfile.imgcollector.tilt b/Dockerfile.imgcollector.tilt new file mode 100644 index 00000000..cae15e41 --- /dev/null +++ b/Dockerfile.imgcollector.tilt @@ -0,0 +1,5 @@ +FROM alpine:3.17 + +COPY ./bin/castai-imgcollector /usr/local/bin/castai-imgcollector + +CMD ["/usr/local/bin/castai-imgcollector"] diff --git a/Dockerfile.mockapi b/Dockerfile.mockapi index 9735cb7c..8274dfe8 100644 --- a/Dockerfile.mockapi +++ b/Dockerfile.mockapi @@ -1,3 +1,5 @@ -FROM gcr.io/distroless/static-debian11 +FROM alpine:3.17 + COPY ./bin/mockapi /usr/local/bin/mockapi + CMD ["/usr/local/bin/mockapi"] diff --git a/Tiltfile b/Tiltfile index aba78c50..a5291160 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,5 +1,5 @@ if config.tilt_subcommand == "down": - fail("consider using `kubectl delete ns castai-sec") + fail("consider using `kubectl delete ns kvisor") load('ext://restart_process', 'docker_build_with_restart') load('ext://namespace', 'namespace_create') @@ -44,7 +44,7 @@ local_resource( local_resource( 'imgcollector-docker-build', - 'docker build -t localhost:5000/kvisor-imgcollector . -f Dockerfile.imgcollector && docker push localhost:5000/kvisor-imgcollector', + 'docker build -t localhost:5000/kvisor-imgcollector . -f Dockerfile.imgcollector.tilt && docker push localhost:5000/kvisor-imgcollector', deps=[ './bin/castai-imgcollector' ], diff --git a/castai/imagemeta_types.go b/castai/imagemeta_types.go index c7b47426..4ca9b2a0 100644 --- a/castai/imagemeta_types.go +++ b/castai/imagemeta_types.go @@ -11,8 +11,11 @@ type ImageMetadata struct { ResourceIDs []string `json:"resourceIDs,omitempty"` BlobsInfo []types.BlobInfo `json:"blobsInfo,omitempty"` ConfigFile *v1.ConfigFile `json:"configFile,omitempty"` - Manifest *v1.Manifest `json:"manifest,omitempty"` - OsInfo *OsInfo `json:"osInfo,omitempty"` + // Manifest specification can be found here: https://github.com/opencontainers/image-spec/blob/main/manifest.md + Manifest *v1.Manifest `json:"manifest,omitempty"` + // Index specification can be found here: https://github.com/opencontainers/image-spec/blob/main/image-index.md + Index *v1.IndexManifest `json:"index,omitempty"` + OsInfo *OsInfo `json:"osInfo,omitempty"` } // nolint:musttag diff --git a/cmd/imgcollector/collector/collector.go b/cmd/imgcollector/collector/collector.go index a6ee705e..fae05c95 100644 --- a/cmd/imgcollector/collector/collector.go +++ b/cmd/imgcollector/collector/collector.go @@ -104,13 +104,13 @@ func (c *Collector) Collect(ctx context.Context) error { if err != nil { return err } - + manifest, err := img.Manifest() if err != nil { return fmt.Errorf("extract manifest: %w", err) } - if err := c.client.SendImageMetadata(ctx, &castai.ImageMetadata{ + metadata := &castai.ImageMetadata{ ImageName: c.cfg.ImageName, ImageID: c.cfg.ImageID, ResourceIDs: strings.Split(c.cfg.ResourceIDs, ","), @@ -121,14 +121,20 @@ func (c *Collector) Collect(ctx context.Context) error { ArtifactInfo: arRef.ArtifactInfo, OS: arRef.OsInfo, }, - }); err != nil { + } + + if index := img.Index(); index != nil { + metadata.Index = index + } + + if err := c.client.SendImageMetadata(ctx, metadata); err != nil { return err } return nil } -func (c *Collector) getImage(ctx context.Context) (image.Image, func(), error) { +func (c *Collector) getImage(ctx context.Context) (image.ImageWithIndex, func(), error) { imgRef, err := name.ParseReference(c.cfg.ImageName) if err != nil { return nil, nil, err diff --git a/cmd/imgcollector/image/blob.go b/cmd/imgcollector/image/blob.go index 3a830755..0e4f8850 100644 --- a/cmd/imgcollector/image/blob.go +++ b/cmd/imgcollector/image/blob.go @@ -1,11 +1,10 @@ package image import ( - "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/castai/kvisor/cmd/imgcollector/image/hostfs" ) -func NewFromContainerdHostFS(imageID string, config hostfs.ContainerdHostFSConfig) (types.Image, func(), error) { +func NewFromContainerdHostFS(imageID string, config hostfs.ContainerdHostFSConfig) (ImageWithIndex, func(), error) { hash, err := hostfs.NewImageHash(imageID) if err != nil { return nil, nil, err diff --git a/cmd/imgcollector/image/daemon.go b/cmd/imgcollector/image/daemon.go index b26c004a..9ae5140b 100644 --- a/cmd/imgcollector/image/daemon.go +++ b/cmd/imgcollector/image/daemon.go @@ -3,15 +3,12 @@ package image import ( "context" - "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/google/go-containerregistry/pkg/name" "github.com/castai/kvisor/cmd/imgcollector/image/daemon" ) -type Image = types.Image - -func NewFromContainerdDaemon(ctx context.Context, imageName string) (types.Image, func(), error) { +func NewFromContainerdDaemon(ctx context.Context, imageName string) (ImageWithIndex, func(), error) { img, cleanup, err := daemon.ContainerdImage(ctx, imageName) if err != nil { return nil, nil, err @@ -22,7 +19,7 @@ func NewFromContainerdDaemon(ctx context.Context, imageName string) (types.Image }, cleanup, nil } -func NewFromDockerDaemon(imageName string, ref name.Reference) (types.Image, func(), error) { +func NewFromDockerDaemon(imageName string, ref name.Reference) (ImageWithIndex, func(), error) { img, cleanup, err := daemon.DockerImage(ref) if err != nil { return nil, nil, err @@ -33,7 +30,7 @@ func NewFromDockerDaemon(imageName string, ref name.Reference) (types.Image, fun }, cleanup, nil } -func NewFromDockerDaemonTarFile(imageName, localTarPath string, ref name.Reference) (types.Image, func(), error) { +func NewFromDockerDaemonTarFile(imageName, localTarPath string, ref name.Reference) (ImageWithIndex, func(), error) { img, cleanup, err := daemon.DockerTarImage(ref, localTarPath) if err != nil { return nil, nil, err diff --git a/cmd/imgcollector/image/daemon/image.go b/cmd/imgcollector/image/daemon/image.go index 8ab53b26..c0240d07 100644 --- a/cmd/imgcollector/image/daemon/image.go +++ b/cmd/imgcollector/image/daemon/image.go @@ -26,6 +26,7 @@ type Image interface { v1.Image RepoTags() []string RepoDigests() []string + Index() *v1.IndexManifest } var mu sync.Mutex @@ -99,6 +100,10 @@ func (img *image) Manifest() (*v1.Manifest, error) { return img.Image.Manifest() } +func (img *image) Index() *v1.IndexManifest { + return nil +} + func (img *image) ConfigFile() (*v1.ConfigFile, error) { if len(img.inspect.RootFS.Layers) == 0 { // Podman doesn't return RootFS... diff --git a/cmd/imgcollector/image/hostfs/containerd_image.go b/cmd/imgcollector/image/hostfs/containerd_image.go index b8ab4ee5..313af5d5 100644 --- a/cmd/imgcollector/image/hostfs/containerd_image.go +++ b/cmd/imgcollector/image/hostfs/containerd_image.go @@ -25,34 +25,36 @@ const ( ) func NewContainerdImage(hash v1.Hash, cfg ContainerdHostFSConfig) (Image, error) { - manifestReader := newContainerdManifestReader(hash, cfg) - manifest, err := manifestReader.resolveManifest() + metadataReader := newContainerdMetadataReader(hash, cfg) + metadata, err := metadataReader.readMetadata() if err != nil { return nil, fmt.Errorf("resolving manifest: %w", err) } - config, configBytes, err := manifestReader.readConfig(manifest.Config.Digest.String()) + config, configBytes, err := metadataReader.readConfig(metadata.Manifest.Config.Digest.String()) if err != nil { return nil, fmt.Errorf("reading config file: %w", err) } - img := &containerdBlobImage{ - manifest: manifest, + return &containerdBlobImage{ + manifest: metadata.Manifest, + index: metadata.Index, config: config, configBytes: configBytes, contentDir: cfg.ContentDir, - } - return img, nil + }, nil } -func newContainerdManifestReader(hash v1.Hash, cfg ContainerdHostFSConfig) *containerdManifestReader { - return &containerdManifestReader{ +func newContainerdMetadataReader(hash v1.Hash, cfg ContainerdHostFSConfig) *containerdMetadataReader { + return &containerdMetadataReader{ imgHash: hash, cfg: cfg, } } -type containerdManifestReader struct { +// containerdMetadataReader is used to follow image references as described here: +// https://github.com/google/go-containerregistry/blob/main/images/ociimage.jpeg +type containerdMetadataReader struct { cfg ContainerdHostFSConfig imgHash v1.Hash } @@ -62,6 +64,11 @@ type ContainerdHostFSConfig struct { ContentDir string } +type containerdMetadata struct { + Index *v1.IndexManifest + Manifest *v1.Manifest +} + type manifestOrIndex struct { SchemaVersion int64 `json:"schemaVersion"` MediaType types.MediaType `json:"mediaType,omitempty"` @@ -74,6 +81,16 @@ type manifestOrIndex struct { Manifests []v1.Descriptor `json:"manifests"` } +func readManifest(atPath string, into *manifestOrIndex) error { + fileBytes, err := os.ReadFile(atPath) + if err != nil { + return err + } + + return json.Unmarshal(fileBytes, into) +} + +// manifest part of the sum type func (mi *manifestOrIndex) manifest() *v1.Manifest { return &v1.Manifest{ SchemaVersion: mi.SchemaVersion, @@ -84,62 +101,69 @@ func (mi *manifestOrIndex) manifest() *v1.Manifest { } } -func (h *containerdManifestReader) resolveManifest() (*v1.Manifest, error) { - // Try to find manifest file. In most cases image id digest will point to manifest or index. - var mi manifestOrIndex - readManifest := func(manifestPath string) error { - var err error - fileBytes, err := os.ReadFile(manifestPath) - if err != nil { - return err - } - err = json.Unmarshal(fileBytes, &mi) - if err != nil { - return err - } - return nil +// index part of the sum type +func (mi *manifestOrIndex) index() *v1.IndexManifest { + return &v1.IndexManifest{ + SchemaVersion: mi.SchemaVersion, + MediaType: mi.MediaType, + Manifests: mi.Manifests, + Annotations: mi.Annotations, } - if err := readManifest(path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm, h.imgHash.Hex)); err != nil { +} + +func (h *containerdMetadataReader) readMetadata() (*containerdMetadata, error) { + var ( + metadata containerdMetadata + manOrIdx manifestOrIndex + ) + if err := readManifest( + path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm, h.imgHash.Hex), &manOrIdx, + ); err != nil { return nil, err } // This case indicates that image id digest points to config file. // In such case we need to find manifest by iterating all files and searching for // config file digest hash inside files content. - if len(mi.Layers) == 0 && len(mi.Manifests) == 0 { + if len(manOrIdx.Layers) == 0 && len(manOrIdx.Manifests) == 0 { manifestPath, err := h.searchManifestPath() if err != nil { return nil, fmt.Errorf("searching manifest path: %w", err) } - if err := readManifest(manifestPath); err != nil { + if err := readManifest(manifestPath, &manOrIdx); err != nil { return nil, err } } - if len(mi.Layers) > 0 { - return mi.manifest(), nil + if len(manOrIdx.Layers) > 0 { + metadata.Manifest = manOrIdx.manifest() + return &metadata, nil } // Search manifest from index manifest. - if len(mi.Manifests) > 0 { - for _, m := range mi.Manifests { - if matchingPlatform(h.cfg.Platform, *m.Platform) { - if err := readManifest(path.Join(h.cfg.ContentDir, blobs, m.Digest.Algorithm, m.Digest.Hex)); err != nil { + if len(manOrIdx.Manifests) > 0 { + metadata.Index = manOrIdx.index() + for _, manifest := range manOrIdx.Manifests { + if matchingPlatform(h.cfg.Platform, *manifest.Platform) { + if err := readManifest( + path.Join(h.cfg.ContentDir, blobs, manifest.Digest.Algorithm, manifest.Digest.Hex), &manOrIdx, + ); err != nil { return nil, err } - if len(mi.Layers) == 0 { + if len(manOrIdx.Layers) == 0 { return nil, errors.New("invalid manifest, no layers") } - return mi.manifest(), nil + metadata.Manifest = manOrIdx.manifest() + return &metadata, nil } } return nil, fmt.Errorf("manifest not found for platform: %s %s", h.cfg.Platform.Architecture, h.cfg.Platform.OS) } - return nil, fmt.Errorf("unrecognised manifest mediatype %q", string(mi.MediaType)) + return nil, fmt.Errorf("unrecognised manifest mediatype %q", string(manOrIdx.MediaType)) } -func (h *containerdManifestReader) readConfig(configID string) (*v1.ConfigFile, []byte, error) { +func (h *containerdMetadataReader) readConfig(configID string) (*v1.ConfigFile, []byte, error) { p := strings.Split(configID, ":") if len(p) < 2 { return nil, nil, fmt.Errorf("invalid configID: %s", configID) @@ -160,7 +184,7 @@ func (h *containerdManifestReader) readConfig(configID string) (*v1.ConfigFile, return &cfg, configBytes, nil } -func (h *containerdManifestReader) searchManifestPath() (string, error) { +func (h *containerdMetadataReader) searchManifestPath() (string, error) { root := path.Join(h.cfg.ContentDir, blobs, h.imgHash.Algorithm) var manifestPath string digestBytes := []byte(h.imgHash.Hex) @@ -184,13 +208,14 @@ func (h *containerdManifestReader) searchManifestPath() (string, error) { return "", err } if manifestPath == "" { - return "", errors.New("manifest not find by searching in blobs content") + return "", errors.New("manifest not found by searching in blobs content") } return manifestPath, nil } type containerdBlobImage struct { manifest *v1.Manifest + index *v1.IndexManifest config *v1.ConfigFile configBytes []byte imgHash v1.Hash @@ -223,6 +248,10 @@ func (b *containerdBlobImage) Manifest() (*v1.Manifest, error) { return b.manifest, nil } +func (b *containerdBlobImage) Index() *v1.IndexManifest { + return b.index +} + func (b *containerdBlobImage) RawConfigFile() ([]byte, error) { return b.configBytes, nil } diff --git a/cmd/imgcollector/image/hostfs/containerd_image_test.go b/cmd/imgcollector/image/hostfs/containerd_image_test.go index 77fb8a52..0ab99efd 100644 --- a/cmd/imgcollector/image/hostfs/containerd_image_test.go +++ b/cmd/imgcollector/image/hostfs/containerd_image_test.go @@ -59,3 +59,29 @@ func TestContainerdImage(t *testing.T) { }) } } + +func TestContainerdImageWithIndex(t *testing.T) { + r := require.New(t) + img, err := NewContainerdImage(v1.Hash{ + Algorithm: "sha256", + Hex: "211a3be9e15e1e4ccd75220aa776d92e06235552351464db2daf043bd30a0ac0", + }, + ContainerdHostFSConfig{ + Platform: v1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + ContentDir: "./testdata/containerd_content", + }, + ) + r.NoError(err) + + index := img.Index() + r.NotNil(index) + r.Len(index.Manifests, 2) + + manifest, err := img.Manifest() + r.NoError(err) + r.NotNil(manifest) + r.Len(manifest.Layers, 2) +} diff --git a/cmd/imgcollector/image/hostfs/image.go b/cmd/imgcollector/image/hostfs/image.go index 35413bce..27f312b3 100644 --- a/cmd/imgcollector/image/hostfs/image.go +++ b/cmd/imgcollector/image/hostfs/image.go @@ -10,6 +10,7 @@ type Image interface { v1.Image RepoTags() []string RepoDigests() []string + Index() *v1.IndexManifest } // NewImageHash returns image hash from string in format: diff --git a/cmd/imgcollector/image/image.go b/cmd/imgcollector/image/image.go index 51171121..542edee0 100644 --- a/cmd/imgcollector/image/image.go +++ b/cmd/imgcollector/image/image.go @@ -3,9 +3,17 @@ package image import ( "fmt" + "github.com/aquasecurity/trivy/pkg/fanal/types" v1 "github.com/google/go-containerregistry/pkg/v1" ) +type Image = types.Image + +type ImageWithIndex interface { + Image + Index() *v1.IndexManifest +} + func ID(img v1.Image) (string, error) { h, err := img.ConfigName() if err != nil { diff --git a/cmd/imgcollector/image/remote.go b/cmd/imgcollector/image/remote.go index 64998a0f..960cc1b4 100644 --- a/cmd/imgcollector/image/remote.go +++ b/cmd/imgcollector/image/remote.go @@ -37,7 +37,7 @@ type DockerOption struct { NonSSL bool `yaml:"non_ssl"` } -func NewFromRemote(ctx context.Context, imageName string, option DockerOption) (types.Image, error) { +func NewFromRemote(ctx context.Context, imageName string, option DockerOption) (ImageWithIndex, error) { var nameOpts []name.Option if option.NonSSL { nameOpts = append(nameOpts, name.Insecure) @@ -65,7 +65,7 @@ func NewFromRemote(ctx context.Context, imageName string, option DockerOption) ( return img, nil } -func tryRemote(ctx context.Context, imageName string, ref name.Reference, option types.DockerOption) (types.Image, error) { +func tryRemote(ctx context.Context, imageName string, ref name.Reference, option types.DockerOption) (ImageWithIndex, error) { var remoteOpts []remote.Option if option.InsecureSkipTLSVerify { t := &http.Transport{ @@ -138,6 +138,10 @@ func (img remoteImage) RepoDigests() []string { return []string{repoDigest} } +func (img remoteImage) Index() *v1.IndexManifest { + return nil +} + type implicitReference struct { ref name.Reference }