diff --git a/cmd/cosign/cli/verify_manifest.go b/cmd/cosign/cli/verify_manifest.go index ea22b8c20ca..76f9345c963 100644 --- a/cmd/cosign/cli/verify_manifest.go +++ b/cmd/cosign/cli/verify_manifest.go @@ -27,21 +27,15 @@ import ( "github.com/peterbourgon/ff/v3/ffcli" "github.com/pkg/errors" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - "k8s.io/api/batch/v1beta1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/yaml" ) -// VerifyCommand verifies all image signatures on a supplied k8s resource +// VerifyManifestCommand verifies all image signatures on a supplied k8s resource type VerifyManifestCommand struct { VerifyCommand } -// Verify builds and returns an ffcli command +// VerifyManifest builds and returns an ffcli command func VerifyManifest() *ffcli.Command { cmd := VerifyManifestCommand{VerifyCommand: VerifyCommand{}} flagset := flag.NewFlagSet("cosign verify-manifest", flag.ExitOnError) @@ -116,95 +110,81 @@ func (c *VerifyManifestCommand) Exec(ctx context.Context, args []string) error { return c.VerifyCommand.Exec(ctx, images) } -func getImagesFromYamlManifest(manifest []byte) ([]string, error) { - dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 4096) - cScheme := runtime.NewScheme() - var images []string - if err := corev1.AddToScheme(cScheme); err != nil { - return images, err - } - if err := appsv1.AddToScheme(cScheme); err != nil { - return images, err - } - if err := batchv1.AddToScheme(cScheme); err != nil { - return images, err - } - - deserializer := serializer.NewCodecFactory(cScheme).UniversalDeserializer() - for { - ext := runtime.RawExtension{} - if err := dec.Decode(&ext); err != nil { - if errors.Is(err, io.EOF) { - break +// unionImagesKind is the union type that match PodSpec, PodSpecTemplate, and +// JobSpecTemplate; but filtering all keys except for `Image`. +type unionImagesKind struct { + Spec struct { + // PodSpec + imageContainers `json:",inline"` + // PodSpecTemplate + Template struct { + Spec struct { + imageContainers `json:",inline"` } - return images, errors.New("unable to decode the manifest") } - - ext.Raw = bytes.TrimSpace(ext.Raw) - if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) { - continue + // JobSpecTemplate + JobTemplate struct { + Spec struct { + Template struct { + Spec struct { + imageContainers `json:",inline"` + } + } + } } + } +} - decoded, _, err := deserializer.Decode(ext.Raw, nil, nil) - if err != nil { - return images, errors.New("unable to decode the manifest") - } +// imageContainers is a wrapper for `containers[].image` and `initContainers[].image` +type imageContainers struct { + Containers []struct { + Image string + } + InitContainers []struct { + Image string + } +} - var ( - d *appsv1.Deployment - rs *appsv1.ReplicaSet - ss *appsv1.StatefulSet - ds *appsv1.DaemonSet - job *v1beta1.CronJob - pod *corev1.Pod - ) - containers := make([]corev1.Container, 0) - switch obj := decoded.(type) { - case *appsv1.Deployment: - d = obj - containers = append(containers, d.Spec.Template.Spec.Containers...) - containers = append(containers, d.Spec.Template.Spec.InitContainers...) - for _, c := range containers { - images = append(images, c.Image) - } - case *appsv1.DaemonSet: - ds = obj - containers = append(containers, ds.Spec.Template.Spec.Containers...) - containers = append(containers, ds.Spec.Template.Spec.InitContainers...) - for _, c := range containers { - images = append(images, c.Image) - } - case *appsv1.ReplicaSet: - rs = obj - containers = append(containers, rs.Spec.Template.Spec.Containers...) - containers = append(containers, rs.Spec.Template.Spec.InitContainers...) - for _, c := range containers { +func (uik *unionImagesKind) images() []string { + images := []string(nil) + var addImage = func(ic *imageContainers) { + for _, c := range ic.InitContainers { + if len(c.Image) > 0 { images = append(images, c.Image) } - case *appsv1.StatefulSet: - ss = obj - containers = append(containers, ss.Spec.Template.Spec.Containers...) - containers = append(containers, ss.Spec.Template.Spec.InitContainers...) - for _, c := range containers { + } + for _, c := range ic.Containers { + if len(c.Image) > 0 { images = append(images, c.Image) } + } + } - case *v1beta1.CronJob: - job = obj - containers = append(containers, job.Spec.JobTemplate.Spec.Template.Spec.Containers...) - containers = append(containers, job.Spec.JobTemplate.Spec.Template.Spec.InitContainers...) - for _, c := range containers { - images = append(images, c.Image) - } - case *corev1.Pod: - pod = obj - containers = append(containers, pod.Spec.Containers...) - containers = append(containers, pod.Spec.InitContainers...) + // Pod + addImage(&uik.Spec.imageContainers) - for _, c := range containers { - images = append(images, c.Image) + // Deployment, ReplicaSet, StatefulSet, DaemonSet, Job + addImage(&uik.Spec.Template.Spec.imageContainers) + + // CronJob + addImage(&uik.Spec.JobTemplate.Spec.Template.Spec.imageContainers) + + return images +} + +func getImagesFromYamlManifest(manifest []byte) ([]string, error) { + dec := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 4096) + var images []string + + for { + ic := unionImagesKind{} + if err := dec.Decode(&ic); err != nil { + if errors.Is(err, io.EOF) { + break } + return images, errors.New("unable to decode the manifest") } + images = append(images, ic.images()...) } return images, nil diff --git a/cmd/cosign/cli/verify_manifest_test.go b/cmd/cosign/cli/verify_manifest_test.go index 73818a44f0b..f280d5bb589 100644 --- a/cmd/cosign/cli/verify_manifest_test.go +++ b/cmd/cosign/cli/verify_manifest_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -const SingleContainerManifest = ` +const singleContainerManifest = ` apiVersion: v1 kind: Pod metadata: @@ -31,7 +31,22 @@ spec: image: nginx:1.21.1 ` -const MultiContainerManifest = ` +const initContainerManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: single-pod +spec: + restartPolicy: Never + initContainers: + - name: preflight + image: preflight:3.2.1 + containers: + - name: nginx-container + image: nginx:1.21.1 +` + +const multiContainerManifest = ` apiVersion: v1 kind: Pod metadata: @@ -55,7 +70,8 @@ spec: command: ["/bin/sh"] args: ["-c", "echo Hello, World > /pod-data/index.html"] ` -const MultiResourceContainerManifest = ` + +const multiResourceContainerManifest = ` apiVersion: apps/v1 kind: Deployment metadata: @@ -102,33 +118,158 @@ spec: args: ["-c", "echo Hello, World > /pod-data/index.html"] ` +const customContainerManifest = ` +apiVersion: v42 +kind: PodSpec +metadata: + name: custom-pod +spec: + restartPolicy: Never + containers: + - name: nginx-container + image: nginx:1.21.1 +` + +const daemonsetManifest = ` +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fluentd-elasticsearch + namespace: kube-system + labels: + k8s-app: fluentd-logging +spec: + selector: + matchLabels: + name: fluentd-elasticsearch + template: + metadata: + labels: + name: fluentd-elasticsearch + spec: + tolerations: + # this toleration is to have the daemonset runnable on master nodes + # remove it if your masters can't run pods + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + initContainers: + - name: py + image: python + command: ["python", "-c", "import math;print(math.sin(1))"] + containers: + - name: fluentd-elasticsearch + image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2 + resources: + limits: + memory: 200Mi + requests: + cpu: 100m + memory: 200Mi + volumeMounts: + - name: varlog + mountPath: /var/log + - name: varlibdockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + terminationGracePeriodSeconds: 30 + volumes: + - name: varlog + hostPath: + path: /var/log + - name: varlibdockercontainers + hostPath: + path: /var/lib/docker/containers +` + +const jobManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: pi +spec: + template: + spec: + initContainers: + - name: py + image: python + command: ["python", "-c", "import math;print(math.sin(1))"] + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + backoffLimit: 4 +` + +const cronJobManifest = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello +spec: + schedule: "*/1 * * * *" + jobTemplate: + spec: + template: + spec: + initContainers: + - name: py + image: python + command: ["python", "-c", "booting up"] + containers: + - name: hello + image: busybox + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure +` + func TestGetImagesFromYamlManifest(t *testing.T) { testCases := []struct { name string fileContents []byte expected []string - }{ - { - name: "single image", - fileContents: []byte(SingleContainerManifest), - expected: []string{"nginx:1.21.1"}, - }, - { - name: "multi image", - fileContents: []byte(MultiContainerManifest), - expected: []string{"nginx:1.21.1", "ubuntu:21.10"}, - }, - { - name: "multiple resources and images within a document", - fileContents: []byte(MultiResourceContainerManifest), - expected: []string{"nginx:1.14.2", "nginx:1.21.1", "ubuntu:21.10"}, - }, - { - name: "no images found", - fileContents: []byte(``), - expected: nil, - }, - } + }{{ + name: "single image", + fileContents: []byte(singleContainerManifest), + expected: []string{"nginx:1.21.1"}, + }, { + name: "init and container images", + fileContents: []byte(initContainerManifest), + expected: []string{"preflight:3.2.1", "nginx:1.21.1"}, + }, { + name: "daemonsets", + fileContents: []byte(daemonsetManifest), + expected: []string{"python", "quay.io/fluentd_elasticsearch/fluentd:v2.5.2"}, + }, { + name: "jobs", + fileContents: []byte(jobManifest), + expected: []string{"python", "perl"}, + }, { + name: "cronjobs", + fileContents: []byte(cronJobManifest), + expected: []string{"python", "busybox"}, + }, { + name: "multi image", + fileContents: []byte(multiContainerManifest), + expected: []string{"nginx:1.21.1", "ubuntu:21.10"}, + }, { + name: "multiple resources and images within a document", + fileContents: []byte(multiResourceContainerManifest), + expected: []string{"nginx:1.14.2", "nginx:1.21.1", "ubuntu:21.10"}, + }, { + name: "no images found", + fileContents: []byte(``), + expected: nil, + }, { + name: "custom type single image", + fileContents: []byte(customContainerManifest), + expected: []string{"nginx:1.21.1"}, + }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got, err := getImagesFromYamlManifest(tc.fileContents)