2025-02-03 19:12:40 +05:30
|
|
|
package imagedataloader
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2025-02-10 18:33:01 +05:30
|
|
|
"io"
|
2025-02-03 19:12:40 +05:30
|
|
|
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
2025-02-10 18:33:01 +05:30
|
|
|
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
2025-02-03 19:12:40 +05:30
|
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
2025-02-26 06:26:17 +05:30
|
|
|
"github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
|
2025-02-10 18:33:01 +05:30
|
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
2025-02-03 19:12:40 +05:30
|
|
|
k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
|
|
)
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
var (
|
|
|
|
maxReferrersCount = 50
|
|
|
|
maxPayloadSize = int64(10 * 1000 * 1000) // 10 MB
|
|
|
|
)
|
|
|
|
|
2025-02-03 19:12:40 +05:30
|
|
|
type imagedatafetcher struct {
|
|
|
|
lister k8scorev1.SecretInterface
|
|
|
|
defaultOptions []remote.Option
|
|
|
|
}
|
|
|
|
|
|
|
|
type Fetcher interface {
|
|
|
|
FetchImageData(ctx context.Context, image string, options ...Option) (*ImageData, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
func New(lister k8scorev1.SecretInterface, opts ...Option) (*imagedatafetcher, error) {
|
|
|
|
remoteOpts, err := makeDefaultOpts(lister, opts...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &imagedatafetcher{
|
|
|
|
lister: lister,
|
|
|
|
defaultOptions: remoteOpts,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2025-02-26 06:26:17 +05:30
|
|
|
func (i *imagedatafetcher) ParseImageReference(image string, options ...Option) (ImageReference, error) {
|
|
|
|
var img ImageReference
|
|
|
|
nameOpts := nameOptions(options...)
|
|
|
|
ref, err := name.ParseReference(image, nameOpts...)
|
|
|
|
if err != nil {
|
|
|
|
return ImageReference{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
img.Image = image
|
|
|
|
img.Registry = ref.Context().RegistryStr()
|
|
|
|
img.Repository = ref.Context().RepositoryStr()
|
|
|
|
|
|
|
|
if _, ok := ref.(name.Tag); ok {
|
|
|
|
img.Tag = ref.Identifier()
|
|
|
|
} else {
|
|
|
|
img.Digest = ref.Identifier()
|
|
|
|
}
|
|
|
|
|
|
|
|
return img, nil
|
|
|
|
}
|
|
|
|
|
2025-02-03 19:12:40 +05:30
|
|
|
func (i *imagedatafetcher) FetchImageData(ctx context.Context, image string, options ...Option) (*ImageData, error) {
|
|
|
|
img := ImageData{
|
2025-02-18 12:53:39 +05:30
|
|
|
referrersData: make(map[string]referrerData),
|
2025-02-03 19:12:40 +05:30
|
|
|
}
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
var err error
|
2025-02-21 06:33:53 +05:30
|
|
|
img.RemoteOpts, err = i.remoteOptions(ctx, i.lister, options...)
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-26 06:26:17 +05:30
|
|
|
img.ImageReference, err = i.ParseImageReference(image, options...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
img.NameOpts = nameOptions(options...)
|
|
|
|
ref, err := name.ParseReference(image, img.NameOpts...)
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-02-21 06:33:53 +05:30
|
|
|
img.NameRef = ref
|
2025-02-03 19:12:40 +05:30
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
remoteImg, err := remote.Image(ref, img.RemoteOpts...)
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
img.Manifest, err = remoteImg.Manifest()
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
img.ConfigData, err = remoteImg.ConfigFile()
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
desc, err := remote.Get(ref, img.RemoteOpts...)
|
2025-02-03 19:12:40 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-02-10 18:33:01 +05:30
|
|
|
img.desc = desc
|
2025-02-03 19:12:40 +05:30
|
|
|
|
|
|
|
if len(img.Digest) == 0 {
|
|
|
|
img.Digest = desc.Digest.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(img.Tag) > 0 {
|
|
|
|
img.ResolvedImage = fmt.Sprintf("%s:%s@%s", ref.Context().Name(), img.Tag, img.Digest)
|
|
|
|
} else {
|
|
|
|
img.ResolvedImage = fmt.Sprintf("%s@%s", ref.Context().Name(), img.Digest)
|
|
|
|
}
|
|
|
|
|
|
|
|
// error returned means no image index
|
|
|
|
imgIndex, _ := desc.ImageIndex()
|
|
|
|
if imgIndex != nil {
|
|
|
|
imgIndexBytes, err := imgIndex.RawManifest()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(imgIndexBytes, &img.ImageIndex); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &img, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *imagedatafetcher) remoteOptions(ctx context.Context, lister k8scorev1.SecretInterface, options ...Option) ([]remote.Option, error) {
|
|
|
|
var opts []remote.Option
|
|
|
|
opts = append(opts, i.defaultOptions...)
|
|
|
|
|
|
|
|
authOpts, err := makeAuthOptions(lister, options...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
opts = append(opts, authOpts...)
|
|
|
|
opts = append(opts, remote.WithContext(ctx))
|
|
|
|
|
|
|
|
return opts, nil
|
|
|
|
}
|
2025-02-10 18:33:01 +05:30
|
|
|
|
2025-02-26 06:26:17 +05:30
|
|
|
type ImageReference struct {
|
|
|
|
Image string `json:"image,omitempty"`
|
|
|
|
ResolvedImage string `json:"resolvedImage,omitempty"`
|
|
|
|
Registry string `json:"registry,omitempty"`
|
|
|
|
Repository string `json:"repository,omitempty"`
|
|
|
|
Tag string `json:"tag,omitempty"`
|
|
|
|
Digest string `json:"digest,omitempty"`
|
|
|
|
}
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
type ImageData struct {
|
2025-02-21 06:33:53 +05:30
|
|
|
RemoteOpts []remote.Option
|
|
|
|
NameOpts []name.Option
|
2025-02-10 18:33:01 +05:30
|
|
|
|
2025-02-26 06:26:17 +05:30
|
|
|
ImageReference `json:",inline"`
|
|
|
|
ImageIndex interface{} `json:"imageIndex,omitempty"`
|
|
|
|
Manifest *gcrv1.Manifest `json:"manifest,omitempty"`
|
|
|
|
ConfigData *gcrv1.ConfigFile `json:"config,omitempty"`
|
2025-02-10 18:33:01 +05:30
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
NameRef name.Reference
|
|
|
|
desc *remote.Descriptor
|
|
|
|
referrersManifest *gcrv1.IndexManifest
|
|
|
|
referrersData map[string]referrerData
|
2025-02-26 06:26:17 +05:30
|
|
|
verifiedReferrers map[string]gcrv1.Descriptor
|
2025-02-21 06:33:53 +05:30
|
|
|
verifiedIntotoPayloads map[string][]byte
|
2025-02-10 18:33:01 +05:30
|
|
|
}
|
|
|
|
|
2025-02-18 12:53:39 +05:30
|
|
|
type referrerData struct {
|
|
|
|
layerDescriptor *gcrv1.Descriptor
|
|
|
|
data []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) FetchReference(identifier string) (ocispec.Descriptor, error) {
|
|
|
|
if identifier == i.Digest {
|
|
|
|
return GCRtoOCISpecDesc(i.desc.Descriptor), nil
|
|
|
|
}
|
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
d, err := remote.Head(i.NameRef.Context().Digest(identifier), i.RemoteOpts...)
|
2025-02-18 12:53:39 +05:30
|
|
|
if err != nil {
|
|
|
|
return ocispec.Descriptor{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return GCRtoOCISpecDesc(*d), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) WithDigest(digest string) string {
|
2025-02-21 06:33:53 +05:30
|
|
|
return i.NameRef.Context().Digest(digest).String()
|
2025-02-10 18:33:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) loadReferrers() error {
|
|
|
|
if i.referrersManifest != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-02-18 12:53:39 +05:30
|
|
|
referrersDescs, err := i.fetchReferrersFromRemote(i.Digest)
|
2025-02-10 18:33:01 +05:30
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-02-18 12:53:39 +05:30
|
|
|
i.referrersManifest = referrersDescs
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) fetchReferrersFromRemote(digest string) (*gcrv1.IndexManifest, error) {
|
2025-02-21 06:33:53 +05:30
|
|
|
referrers, err := remote.Referrers(i.NameRef.Context().Digest(digest), i.RemoteOpts...)
|
2025-02-18 12:53:39 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-10 18:33:01 +05:30
|
|
|
referrersDescs, err := referrers.IndexManifest()
|
|
|
|
if err != nil {
|
2025-02-18 12:53:39 +05:30
|
|
|
return nil, err
|
2025-02-10 18:33:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
// This check ensures that the manifest does not have an abnormal amount of referrers attached to it to protect against compromised images
|
|
|
|
if len(referrersDescs.Manifests) > maxReferrersCount {
|
2025-02-18 12:53:39 +05:30
|
|
|
return nil, fmt.Errorf("failed to fetch referrers: to many referrers found, max limit is %d", maxReferrersCount)
|
2025-02-10 18:33:01 +05:30
|
|
|
}
|
|
|
|
|
2025-02-18 12:53:39 +05:30
|
|
|
return referrersDescs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) FetchRefererrsForDigest(digest string, artifactType string) ([]gcrv1.Descriptor, error) {
|
|
|
|
// If the call is for image referrers, return prefetched referrers
|
|
|
|
if digest == i.Digest {
|
|
|
|
return i.FetchRefererrs(artifactType)
|
|
|
|
}
|
|
|
|
|
|
|
|
// this is most likely a call to fetch notary signatures for an attesatation
|
|
|
|
idx, err := i.fetchReferrersFromRemote(digest)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
refList := make([]gcrv1.Descriptor, 0)
|
|
|
|
for _, ref := range idx.Manifests {
|
|
|
|
if ref.ArtifactType == artifactType {
|
|
|
|
refList = append(refList, ref)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return refList, nil
|
2025-02-10 18:33:01 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) FetchRefererrs(artifactType string) ([]gcrv1.Descriptor, error) {
|
|
|
|
if err := i.loadReferrers(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
refList := make([]gcrv1.Descriptor, 0)
|
|
|
|
for _, ref := range i.referrersManifest.Manifests {
|
|
|
|
if ref.ArtifactType == artifactType {
|
|
|
|
refList = append(refList, ref)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return refList, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *ImageData) FetchReferrerData(desc gcrv1.Descriptor) ([]byte, *gcrv1.Descriptor, error) {
|
2025-02-18 12:53:39 +05:30
|
|
|
if v, found := i.referrersData[desc.Digest.String()]; found {
|
|
|
|
return v.data, v.layerDescriptor, nil
|
|
|
|
}
|
|
|
|
|
2025-02-21 06:33:53 +05:30
|
|
|
img, err := remote.Image(i.NameRef.Context().Digest(desc.Digest.String()), i.RemoteOpts...)
|
2025-02-10 18:33:01 +05:30
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
layers, err := img.Layers()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(layers) != 1 {
|
|
|
|
return nil, nil, fmt.Errorf("invalid referrer descriptor, must have only one layer")
|
|
|
|
}
|
|
|
|
layer := layers[0]
|
|
|
|
|
|
|
|
size, err := layer.Size()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
digest, err := layer.Digest()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
mediaType, err := layer.MediaType()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
layerDesc := &gcrv1.Descriptor{
|
|
|
|
MediaType: mediaType,
|
|
|
|
Digest: digest,
|
|
|
|
Size: size,
|
|
|
|
}
|
|
|
|
|
|
|
|
reader, err := layer.Uncompressed()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := io.ReadAll(io.LimitReader(reader, maxPayloadSize))
|
|
|
|
|
2025-02-18 12:53:39 +05:30
|
|
|
i.referrersData[desc.Digest.String()] = referrerData{
|
|
|
|
data: b,
|
|
|
|
layerDescriptor: layerDesc,
|
|
|
|
}
|
2025-02-10 18:33:01 +05:30
|
|
|
return b, layerDesc, err
|
|
|
|
}
|
2025-02-18 12:53:39 +05:30
|
|
|
|
|
|
|
func (i *ImageData) AddVerifiedReferrer(desc gcrv1.Descriptor) {
|
|
|
|
if i.verifiedReferrers == nil {
|
2025-02-26 06:26:17 +05:30
|
|
|
i.verifiedReferrers = make(map[string]gcrv1.Descriptor, 0)
|
2025-02-18 12:53:39 +05:30
|
|
|
}
|
|
|
|
|
2025-02-26 06:26:17 +05:30
|
|
|
i.verifiedReferrers[desc.ArtifactType] = desc
|
2025-02-18 12:53:39 +05:30
|
|
|
}
|
2025-02-21 06:33:53 +05:30
|
|
|
|
|
|
|
func (i *ImageData) AddVerifiedIntotoPayloads(predicateType string, data []byte) {
|
|
|
|
if i.verifiedIntotoPayloads == nil {
|
|
|
|
i.verifiedIntotoPayloads = make(map[string][]byte, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
i.verifiedIntotoPayloads[predicateType] = data
|
|
|
|
}
|
2025-02-26 06:26:17 +05:30
|
|
|
|
|
|
|
func (i *ImageData) GetPayload(a v1alpha1.Attestation) (interface{}, error) {
|
|
|
|
var b []byte
|
|
|
|
if a.IsInToto() {
|
|
|
|
var ok bool
|
|
|
|
b, ok = i.verifiedIntotoPayloads[a.InToto.Type]
|
|
|
|
if !ok {
|
|
|
|
// download this using cosign cli
|
|
|
|
return nil, fmt.Errorf("intoto attestation payload cannot be fetch before verifying intoto attestation")
|
|
|
|
}
|
|
|
|
} else if a.IsReferrer() {
|
|
|
|
var err error
|
|
|
|
if i.verifiedReferrers != nil {
|
|
|
|
desc, ok := i.verifiedReferrers[a.Referrer.Type]
|
|
|
|
if ok {
|
|
|
|
b, _, err = i.FetchReferrerData(desc)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(b) == 0 {
|
|
|
|
descs, err := i.FetchRefererrs(a.Referrer.Type)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(descs) == 0 {
|
|
|
|
return nil, fmt.Errorf("artifact of type %s not found", a.Referrer.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
b, _, err = i.FetchReferrerData(descs[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(b) == 0 {
|
|
|
|
return nil, fmt.Errorf("could not find attestation %s", a.GetKey())
|
|
|
|
}
|
|
|
|
|
|
|
|
var payload interface{}
|
|
|
|
|
|
|
|
if err := json.Unmarshal(b, &payload); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal json for attestation %s", a.GetKey())
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload, nil
|
|
|
|
}
|