From e7e761c372eb303757b4b816555aad9571bf636d Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Mon, 3 Jun 2024 10:53:48 -0300 Subject: [PATCH] VulnerabilityReport v1alpha2 with grouped packages and webhook conversion (#277) * create resource VulnerabilityReport v1alpha2 * add VulnerabilityReport v1alpha2 fields * generating clientset for vulnerabilityreport v1alpha2 * fix custom checks paths in Makefile * parse trivy results to v1alpha2 vulnerability reports * send v1alpha2 vulnerability reports to SaaS * update vulnerabilityreport samples * preserve unknown fields in .spec.vulnerabilities objects of v1alpha2 VulnerabilityReports * deprecate VulnerabilityReport v1alpha1 * Setup VulnerabilityReport webhook conversion (#283) * implement conversion.Hub in VulnerabilityReport v1alpha1 * move summarize function to v1alpha2 VulnerabilityReport * implement conversion.Convertible in VulnerabilityReport v1alpha2 * set imagePullPolicy to IfNotPresent in config/manager/manager.yaml * split install and install-crds targets in Makefile * logging vulnerability reports conversions * remove x-kubernetes-preserve-unknown-fields from v1alpha2 vulnerability report * setup webhook with cert-manager * setting TLSOpts in webhook server * generating CRDs with kustomize for applying webhook patch * installing yq in local binaries directory * fix CRD generation * updating CRD annotations * annotate CRD for injecting conversion * inject webhook conversion in annotated CRDs * add flag for enabling/disabling conversion * update helm chart for generating and mounting certificates if webhook is enabled * set annotation with jq instead of kustomize * remove unnecessary base64 encoding * bump chart version to 0.8.5-rc3 * move NAMESPACE var in Makefile * delete unused cert-manager files in config/ directory * remove logs from vulnerabilityreport_conversion.go * using certificates from existing secret if it already exists (#284) * Add fields `totalPackages` and `totalUniquePackages` in VulnerabilityReport (#285) * add fields `totalPackages` and `totalUniquePackages` in v1alpha2 VulnerabilityReport * bump chart version to 0.8.5-rc4 * remove TODO --- Makefile | 20 +- PROJECT | 11 + .../v1alpha1/vulnerabilityreport_types.go | 89 ++- .../v1alpha1/vulnerabilityreport_webhook.go | 26 + api/zora/v1alpha1/zz_generated.deepcopy.go | 108 ++- .../vulnerabilityreport_conversion.go | 68 ++ .../vulnerabilityreport_conversion_test.go | 170 +++++ .../v1alpha2/vulnerabilityreport_types.go | 124 +++ api/zora/v1alpha2/zz_generated.deepcopy.go | 120 +++ charts/zora/Chart.yaml | 4 +- charts/zora/README.md | 5 +- ...zora.undistro.io_vulnerabilityreports.yaml | 706 ++++++++++++------ .../zora/templates/operator/deployment.yaml | 44 +- .../templates/operator/webhook-service.yaml | 29 + charts/zora/values.yaml | 4 +- cmd/main.go | 37 +- ...zora.undistro.io_vulnerabilityreports.yaml | 706 ++++++++++++------ config/crd/kustomization.yaml | 8 +- .../webhook_in_zora_vulnerabilityreports.yaml | 16 + config/default/kustomization.yaml | 36 +- config/default/manager_webhook_patch.yaml | 23 + config/manager/manager.yaml | 1 + config/samples/kustomization.yaml | 1 + .../zora_v1alpha1_vulnerabilityreport.yaml | 74 +- .../zora_v1alpha2_vulnerabilityreport.yaml | 66 ++ config/webhook/kustomization.yaml | 2 + config/webhook/service.yaml | 15 + go.mod | 2 +- internal/saas/client.go | 28 +- internal/saas/hooks.go | 6 +- .../v1alpha2/fake/fake_vulnerabilityreport.go | 125 ++++ .../zora/v1alpha2/fake/fake_zora_client.go | 4 + .../zora/v1alpha2/generated_expansion.go | 2 + .../zora/v1alpha2/vulnerabilityreport.go | 179 +++++ .../typed/zora/v1alpha2/zora_client.go | 5 + pkg/crds/crds.go | 95 ++- pkg/crds/crds_test.go | 100 ++- pkg/informers/externalversions/generic.go | 2 + .../zora/v1alpha2/interface.go | 7 + .../zora/v1alpha2/vulnerabilityreport.go | 74 ++ .../zora/v1alpha2/expansion_generated.go | 8 + .../zora/v1alpha2/vulnerabilityreport.go | 83 ++ pkg/worker/report/trivy/parse.go | 98 +-- pkg/worker/report/trivy/parse_test.go | 446 ++++++----- pkg/worker/report/trivy/testdata/report.json | 226 ++++++ pkg/worker/vuln.go | 17 +- pkg/worker/vuln_test.go | 461 +++++++----- 47 files changed, 3431 insertions(+), 1050 deletions(-) create mode 100644 api/zora/v1alpha1/vulnerabilityreport_webhook.go create mode 100644 api/zora/v1alpha2/vulnerabilityreport_conversion.go create mode 100644 api/zora/v1alpha2/vulnerabilityreport_conversion_test.go create mode 100644 api/zora/v1alpha2/vulnerabilityreport_types.go create mode 100644 charts/zora/templates/operator/webhook-service.yaml create mode 100644 config/crd/patches/webhook_in_zora_vulnerabilityreports.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/samples/zora_v1alpha2_vulnerabilityreport.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/service.yaml create mode 100644 pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_vulnerabilityreport.go create mode 100644 pkg/clientset/versioned/typed/zora/v1alpha2/vulnerabilityreport.go create mode 100644 pkg/informers/externalversions/zora/v1alpha2/vulnerabilityreport.go create mode 100644 pkg/listers/zora/v1alpha2/vulnerabilityreport.go diff --git a/Makefile b/Makefile index 9396bf78..c8f485d7 100644 --- a/Makefile +++ b/Makefile @@ -46,8 +46,9 @@ help: ## Display this help. ##@ Development .PHONY: manifests -manifests: controller-gen addlicense ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. +manifests: controller-gen addlicense yq ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(YQ) -i '.metadata.annotations."zora.undistro.io/inject-conversion" = "true"' config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml @cp -r config/crd/bases/*.yaml charts/zora/crds/ $(ADDLICENSE) -c "Undistro Authors" -l "apache" -ignore ".github/**" -ignore ".idea/**" -ignore "dist/**" -ignore "site/**" -ignore "config/**" -ignore "docs/overrides/**" -ignore "docs/stylesheets/**" . @@ -146,15 +147,19 @@ ifndef ignore-not-found ignore-not-found = false endif +.PHONY: install-crds +install-crds: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + NAMESPACE ?= zora-system .PHONY: install -install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - +install: install-crds ## Install CRs (plugins, custom checks, cluster, and scan) into the K8s cluster specified in ~/.kube/config. @$(KUBECTL) create namespace $(NAMESPACE) || true @$(KUBECTL) apply -f config/samples/zora_v1alpha1_plugin_popeye_all.yaml -n $(NAMESPACE) @$(KUBECTL) apply -f config/samples/zora_v1alpha1_plugin_marvin.yaml -n $(NAMESPACE) @$(KUBECTL) apply -f config/samples/zora_v1alpha1_plugin_trivy.yaml -n $(NAMESPACE) - @$(KUBECTL) apply -f config/samples/zora_v1alpha1_customcheck_labels.yaml -n $(NAMESPACE) + @$(KUBECTL) apply -f config/samples/zora_v1alpha1_customcheck_replicas.yaml -n $(NAMESPACE) + @$(KUBECTL) apply -f config/samples/zora_v1alpha2_customcheck_labels.yaml -n $(NAMESPACE) @$(KUBECTL) apply -f config/rbac/zora_plugins_role.yaml @$(KUBECTL) create -f config/rbac/zora_plugins_role_binding.yaml || true @$(KUBECTL) apply -f config/samples/zora_v1alpha1_cluster.yaml -n $(NAMESPACE) @@ -206,6 +211,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint-$(GOLANGCI_LINT_VERSION) ADDLICENSE ?= $(LOCALBIN)/addlicense-$(ADDLICENSE_VERSION) HELM_DOCS ?= $(LOCALBIN)/helm-docs-$(HELM_DOCS_VERSION) KIND ?= $(LOCALBIN)/kind-$(KIND_VERSION) +YQ ?= $(LOCALBIN)/yq-$(YQ_VERSION) ## Tool Versions KUSTOMIZE_VERSION ?= v5.3.0 @@ -215,6 +221,7 @@ GOLANGCI_LINT_VERSION ?= v1.54.2 HELM_DOCS_VERSION ?= v1.13.1 ADDLICENSE_VERSION ?= v1.1.1 KIND_VERSION ?= v0.22.0 +YQ_VERSION ?= v4.43.1 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -251,6 +258,11 @@ kind: $(KIND) ## Download kind locally if necessary $(KIND): $(LOCALBIN) $(call go-install-tool,$(KIND),sigs.k8s.io/kind,${KIND_VERSION}) +.PHONY: yq +yq: $(YQ) ## Download yq locally if necessary +$(YQ): $(LOCALBIN) + $(call go-install-tool,$(YQ),github.com/mikefarah/yq/v4,${YQ_VERSION}) + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary (ideally with version) # $2 - package url which can be installed diff --git a/PROJECT b/PROJECT index 5ed28f6b..0321c2d2 100644 --- a/PROJECT +++ b/PROJECT @@ -60,6 +60,9 @@ resources: kind: VulnerabilityReport path: github.com/undistro/zora/api/zora/v1alpha1 version: v1alpha1 + webhooks: + conversion: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true @@ -68,4 +71,12 @@ resources: kind: CustomCheck path: github.com/undistro/zora/api/zora/v1alpha2 version: v1alpha2 +- api: + crdVersion: v1 + namespaced: true + domain: undistro.io + group: zora + kind: VulnerabilityReport + path: github.com/undistro/zora/api/zora/v1alpha2 + version: v1alpha2 version: "3" diff --git a/api/zora/v1alpha1/vulnerabilityreport_types.go b/api/zora/v1alpha1/vulnerabilityreport_types.go index 008747e7..b63dc5ca 100644 --- a/api/zora/v1alpha1/vulnerabilityreport_types.go +++ b/api/zora/v1alpha1/vulnerabilityreport_types.go @@ -15,37 +15,53 @@ package v1alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // VulnerabilityReportSpec defines the desired state of VulnerabilityReport type VulnerabilityReportSpec struct { - Cluster string `json:"cluster"` - Image string `json:"image"` - Digest string `json:"digest"` - Tags []string `json:"tags,omitempty"` - Architecture string `json:"architecture,omitempty"` - OS string `json:"os,omitempty"` - Distro *Distro `json:"distro,omitempty"` - - TotalResources int `json:"totalResources"` - Resources map[string][]string `json:"resources"` - Vulnerabilities []Vulnerability `json:"vulnerabilities"` - - Summary VulnerabilitySummary `json:"summary"` + VulnerabilityReportCommon `json:",inline"` + Vulnerabilities []Vulnerability `json:"vulnerabilities"` +} + +type VulnerabilityReportCommon struct { + Cluster string `json:"cluster"` + Image string `json:"image"` + Digest string `json:"digest"` + Tags []string `json:"tags,omitempty"` + Architecture string `json:"architecture,omitempty"` + OS string `json:"os,omitempty"` + Distro *Distro `json:"distro,omitempty"` + TotalResources int `json:"totalResources"` + Resources map[string][]string `json:"resources"` + Summary VulnerabilitySummary `json:"summary"` } type Vulnerability struct { + VulnerabilityCommon `json:",inline"` + Package `json:",inline"` +} + +type Package struct { + Package string `json:"package"` + Version string `json:"version"` + FixVersion string `json:"fixVersion,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` +} + +func (r *Package) String() string { + return fmt.Sprintf("Package=%s, Version=%s, FixVersion=%s, Status=%s, Type=%s", r.Package, r.Version, r.FixVersion, r.Status, r.Type) +} + +type VulnerabilityCommon struct { ID string `json:"id"` Severity string `json:"severity"` Title string `json:"title"` Description string `json:"description,omitempty"` - Package string `json:"package"` - Version string `json:"version"` - FixVersion string `json:"fixVersion,omitempty"` URL string `json:"url,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` Score string `json:"score,omitempty"` PublishedDate *metav1.Time `json:"publishedDate,omitempty"` LastModifiedDate *metav1.Time `json:"lastModifiedDate,omitempty"` @@ -66,25 +82,31 @@ type VulnerabilitySummary struct { Unknown int `json:"unknown"` } +func (in *VulnerabilityReportSpec) Summarize() { + s := &VulnerabilitySummary{} + for _, v := range in.Vulnerabilities { + s.Total++ + switch v.Severity { + case "CRITICAL": + s.Critical++ + case "HIGH": + s.High++ + case "MEDIUM": + s.Medium++ + case "LOW": + s.Low++ + default: + s.Unknown++ + } + } + in.Summary = *s +} + // VulnerabilityReportStatus defines the observed state of VulnerabilityReport type VulnerabilityReportStatus struct { Status `json:",inline"` } -func (in *VulnerabilityReport) SetSaaSStatus(status metav1.ConditionStatus, reason, msg string) { - in.Status.SetCondition(metav1.Condition{ - Type: "SaaS", - Status: status, - ObservedGeneration: in.Generation, - Reason: reason, - Message: msg, - }) -} - -func (in *VulnerabilityReport) SaaSStatusIsTrue() bool { - return in.Status.ConditionIsTrue("SaaS") -} - //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:shortName={vuln,vulns,vulnerabilities} @@ -97,6 +119,7 @@ func (in *VulnerabilityReport) SaaSStatusIsTrue() bool { //+kubebuilder:printcolumn:name="Low",type="string",JSONPath=".spec.summary.low",priority=1 //+kubebuilder:printcolumn:name="Unknown",type="string",JSONPath=".spec.summary.unknown",priority=1 //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",priority=0 +//+kubebuilder:deprecatedversion:warning="The v1alpha1 version of VulnerabilityReport has been deprecated. Please use v1alpha2 instead." // VulnerabilityReport is the Schema for the vulnerabilityreports API // +genclient @@ -108,6 +131,8 @@ type VulnerabilityReport struct { Status VulnerabilityReportStatus `json:"status,omitempty"` } +func (in *VulnerabilityReport) Hub() {} + //+kubebuilder:object:root=true // VulnerabilityReportList contains a list of VulnerabilityReport diff --git a/api/zora/v1alpha1/vulnerabilityreport_webhook.go b/api/zora/v1alpha1/vulnerabilityreport_webhook.go new file mode 100644 index 00000000..94747566 --- /dev/null +++ b/api/zora/v1alpha1/vulnerabilityreport_webhook.go @@ -0,0 +1,26 @@ +// Copyright 2024 Undistro Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha1 + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *VulnerabilityReport) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} diff --git a/api/zora/v1alpha1/zz_generated.deepcopy.go b/api/zora/v1alpha1/zz_generated.deepcopy.go index f524ab88..082c0407 100644 --- a/api/zora/v1alpha1/zz_generated.deepcopy.go +++ b/api/zora/v1alpha1/zz_generated.deepcopy.go @@ -574,6 +574,21 @@ func (in *Match) DeepCopy() *Match { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Package) DeepCopyInto(out *Package) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Package. +func (in *Package) DeepCopy() *Package { + if in == nil { + return nil + } + out := new(Package) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Plugin) DeepCopyInto(out *Plugin) { *out = *in @@ -875,6 +890,23 @@ func (in *Validation) DeepCopy() *Validation { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Vulnerability) DeepCopyInto(out *Vulnerability) { + *out = *in + in.VulnerabilityCommon.DeepCopyInto(&out.VulnerabilityCommon) + out.Package = in.Package +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vulnerability. +func (in *Vulnerability) DeepCopy() *Vulnerability { + if in == nil { + return nil + } + out := new(Vulnerability) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityCommon) DeepCopyInto(out *VulnerabilityCommon) { *out = *in if in.PublishedDate != nil { in, out := &in.PublishedDate, &out.PublishedDate @@ -886,12 +918,12 @@ func (in *Vulnerability) DeepCopyInto(out *Vulnerability) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vulnerability. -func (in *Vulnerability) DeepCopy() *Vulnerability { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityCommon. +func (in *VulnerabilityCommon) DeepCopy() *VulnerabilityCommon { if in == nil { return nil } - out := new(Vulnerability) + out := new(VulnerabilityCommon) in.DeepCopyInto(out) return out } @@ -923,6 +955,48 @@ func (in *VulnerabilityReport) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityReportCommon) DeepCopyInto(out *VulnerabilityReportCommon) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Distro != nil { + in, out := &in.Distro, &out.Distro + *out = new(Distro) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + out.Summary = in.Summary +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReportCommon. +func (in *VulnerabilityReportCommon) DeepCopy() *VulnerabilityReportCommon { + if in == nil { + return nil + } + out := new(VulnerabilityReportCommon) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VulnerabilityReportList) DeepCopyInto(out *VulnerabilityReportList) { *out = *in @@ -958,32 +1032,7 @@ func (in *VulnerabilityReportList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VulnerabilityReportSpec) DeepCopyInto(out *VulnerabilityReportSpec) { *out = *in - if in.Tags != nil { - in, out := &in.Tags, &out.Tags - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Distro != nil { - in, out := &in.Distro, &out.Distro - *out = new(Distro) - **out = **in - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make(map[string][]string, len(*in)) - for key, val := range *in { - var outVal []string - if val == nil { - (*out)[key] = nil - } else { - inVal := (*in)[key] - in, out := &inVal, &outVal - *out = make([]string, len(*in)) - copy(*out, *in) - } - (*out)[key] = outVal - } - } + in.VulnerabilityReportCommon.DeepCopyInto(&out.VulnerabilityReportCommon) if in.Vulnerabilities != nil { in, out := &in.Vulnerabilities, &out.Vulnerabilities *out = make([]Vulnerability, len(*in)) @@ -991,7 +1040,6 @@ func (in *VulnerabilityReportSpec) DeepCopyInto(out *VulnerabilityReportSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.Summary = in.Summary } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReportSpec. diff --git a/api/zora/v1alpha2/vulnerabilityreport_conversion.go b/api/zora/v1alpha2/vulnerabilityreport_conversion.go new file mode 100644 index 00000000..f018b225 --- /dev/null +++ b/api/zora/v1alpha2/vulnerabilityreport_conversion.go @@ -0,0 +1,68 @@ +// Copyright 2024 Undistro Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha2 + +import ( + "fmt" + + "github.com/undistro/zora/api/zora/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +// ConvertTo converts this VulnerabilityReport (v1alpha2) to the Hub version (v1alpha1) +func (src *VulnerabilityReport) ConvertTo(dstRaw conversion.Hub) error { + dst, ok := dstRaw.(*v1alpha1.VulnerabilityReport) + if !ok { + return fmt.Errorf("unsupported type") + } + dst.ObjectMeta = src.ObjectMeta + dst.Status.Status = src.Status.Status + dst.Spec.VulnerabilityReportCommon = src.Spec.VulnerabilityReportCommon + + for _, vuln := range src.Spec.Vulnerabilities { + for _, pkg := range vuln.Packages { + dst.Spec.Vulnerabilities = append(dst.Spec.Vulnerabilities, v1alpha1.Vulnerability{ + VulnerabilityCommon: vuln.VulnerabilityCommon, + Package: pkg, + }) + } + } + dst.Spec.Summarize() + return nil +} + +// ConvertFrom converts from the Hub version (v1alpha1) to this VulnerabilityReport (v1alpha2) +func (dst *VulnerabilityReport) ConvertFrom(srcRaw conversion.Hub) error { + src, ok := srcRaw.(*v1alpha1.VulnerabilityReport) + if !ok { + return fmt.Errorf("unsupported type") + } + dst.ObjectMeta = src.ObjectMeta + dst.Status.Status = src.Status.Status + dst.Spec.VulnerabilityReportCommon = src.Spec.VulnerabilityReportCommon + + vulnsByID := make(map[string]*Vulnerability) + for _, vuln := range src.Spec.Vulnerabilities { + if _, ok := vulnsByID[vuln.ID]; !ok { + vulnsByID[vuln.ID] = &Vulnerability{VulnerabilityCommon: vuln.VulnerabilityCommon} + } + vulnsByID[vuln.ID].Packages = append(vulnsByID[vuln.ID].Packages, vuln.Package) + } + for _, vuln := range vulnsByID { + dst.Spec.Vulnerabilities = append(dst.Spec.Vulnerabilities, *vuln) + } + dst.Spec.Summarize() + return nil +} diff --git a/api/zora/v1alpha2/vulnerabilityreport_conversion_test.go b/api/zora/v1alpha2/vulnerabilityreport_conversion_test.go new file mode 100644 index 00000000..4c92416c --- /dev/null +++ b/api/zora/v1alpha2/vulnerabilityreport_conversion_test.go @@ -0,0 +1,170 @@ +// Copyright 2024 Undistro Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha2 + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/undistro/zora/api/zora/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" +) + +var ( + now = metav1.Now() + meta = metav1.ObjectMeta{ + Name: "v1a2report", + Namespace: "namespace", + UID: "v1a2report-uid", + ResourceVersion: "123", + Generation: 1, + CreationTimestamp: now, + Labels: map[string]string{"foo": "bar"}, + Annotations: map[string]string{"bar": "foo"}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "Owner", + Name: "owner", + UID: "owner-uid", + Controller: pointer.Bool(true), + BlockOwnerDeletion: pointer.Bool(true), + }}, + } + vulnCommon = v1alpha1.VulnerabilityCommon{ + ID: "CVE-2022-4450", + Severity: "HIGH", + Title: "double free after calling PEM_read_bio_ex", + Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and ...", + URL: "https://avd.aquasec.com/nvd/cve-2022-4450", + Score: "7.5", + } + status = v1alpha1.Status{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{{ + Type: "Ready", + Status: "True", + ObservedGeneration: 1, + LastTransitionTime: now, + Reason: "reason", + Message: "message", + }}, + } +) + +func TestVulnerabilityReportConversion(t *testing.T) { + tests := []struct { + v1a2 *VulnerabilityReport + wantErr bool + v1a1 *v1alpha1.VulnerabilityReport + }{ + { + v1a2: &VulnerabilityReport{ + ObjectMeta: meta, + Spec: VulnerabilityReportSpec{ + VulnerabilityReportCommon: newVulnerabilityReportCommon(v1alpha1.VulnerabilitySummary{Total: 1, High: 1}), + TotalPackages: 2, + TotalUniquePackages: 2, + Vulnerabilities: []Vulnerability{{ + VulnerabilityCommon: vulnCommon, + Packages: []v1alpha1.Package{ + { + FixVersion: "1.1.1t-r0", + Package: "libcrypto1.1", + Status: "fixed", + Type: "alpine", + Version: "1.1.1s-r0", + }, + { + FixVersion: "1.1.1t-r0", + Package: "libssl1.1", + Status: "fixed", + Type: "alpine", + Version: "1.1.1s-r0", + }, + }, + }}, + }, + Status: VulnerabilityReportStatus{Status: status}, + }, + wantErr: false, + v1a1: &v1alpha1.VulnerabilityReport{ + ObjectMeta: meta, + Spec: v1alpha1.VulnerabilityReportSpec{ + VulnerabilityReportCommon: newVulnerabilityReportCommon(v1alpha1.VulnerabilitySummary{Total: 2, High: 2}), + Vulnerabilities: []v1alpha1.Vulnerability{ + { + VulnerabilityCommon: vulnCommon, + Package: v1alpha1.Package{ + FixVersion: "1.1.1t-r0", + Package: "libcrypto1.1", + Status: "fixed", + Type: "alpine", + Version: "1.1.1s-r0", + }, + }, + { + VulnerabilityCommon: vulnCommon, + Package: v1alpha1.Package{ + FixVersion: "1.1.1t-r0", + Package: "libssl1.1", + Status: "fixed", + Type: "alpine", + Version: "1.1.1s-r0", + }, + }, + }, + }, + Status: v1alpha1.VulnerabilityReportStatus{Status: status}, + }, + }, + } + for _, tt := range tests { + t.Run("ConvertTo", func(t *testing.T) { + got := &v1alpha1.VulnerabilityReport{} + if err := tt.v1a2.ConvertTo(got); (err != nil) != tt.wantErr { + t.Errorf("ConvertTo() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.v1a1) { + t.Errorf("ConvertTo() mismatch (-want +got):\n%s", cmp.Diff(tt.v1a1, got)) + } + }) + t.Run("ConvertFrom", func(t *testing.T) { + got := &VulnerabilityReport{} + if err := got.ConvertFrom(tt.v1a1); (err != nil) != tt.wantErr { + t.Errorf("ConvertFrom() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(tt.v1a2, got) { + t.Errorf("ConvertFrom() mismatch (-want +got):\n%s", cmp.Diff(tt.v1a2, got)) + } + }) + } +} + +func newVulnerabilityReportCommon(summary v1alpha1.VulnerabilitySummary) v1alpha1.VulnerabilityReportCommon { + return v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "image:tag", + Digest: "image@sha254:uuid", + Tags: []string{"image:tag"}, + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "alpine", Version: "3.16.3"}, + TotalResources: 1, + Resources: map[string][]string{"Deployment": {"foo/bar"}}, + Summary: summary, + } +} diff --git a/api/zora/v1alpha2/vulnerabilityreport_types.go b/api/zora/v1alpha2/vulnerabilityreport_types.go new file mode 100644 index 00000000..8d77745a --- /dev/null +++ b/api/zora/v1alpha2/vulnerabilityreport_types.go @@ -0,0 +1,124 @@ +// Copyright 2024 Undistro Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1alpha2 + +import ( + "github.com/undistro/zora/api/zora/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VulnerabilityReportSpec defines the desired state of VulnerabilityReport +type VulnerabilityReportSpec struct { + v1alpha1.VulnerabilityReportCommon `json:",inline"` + + // TotalPackages represents the total number of affected packages in this image. + // A package affected by two vulnerabilities is counted twice. + TotalPackages int `json:"totalPackages"` + + // TotalUniquePackages represents the total number of unique affected packages in this image. + // A package affected by multiple vulnerabilities is counted only once. + TotalUniquePackages int `json:"totalUniquePackages"` + + Vulnerabilities []Vulnerability `json:"vulnerabilities"` +} + +func (in *VulnerabilityReportSpec) Summarize() { + total := 0 + unique := make(map[string]bool) + s := &v1alpha1.VulnerabilitySummary{} + for _, v := range in.Vulnerabilities { + s.Total++ + switch v.Severity { + case "CRITICAL": + s.Critical++ + case "HIGH": + s.High++ + case "MEDIUM": + s.Medium++ + case "LOW": + s.Low++ + default: + s.Unknown++ + } + for _, p := range v.Packages { + total++ + unique[p.String()] = true + } + } + in.Summary = *s + in.TotalPackages = total + in.TotalUniquePackages = len(unique) +} + +type Vulnerability struct { + v1alpha1.VulnerabilityCommon `json:",inline"` + Packages []v1alpha1.Package `json:"packages"` +} + +// VulnerabilityReportStatus defines the observed state of VulnerabilityReport +type VulnerabilityReportStatus struct { + v1alpha1.Status `json:",inline"` +} + +func (in *VulnerabilityReport) SetSaaSStatus(status metav1.ConditionStatus, reason, msg string) { + in.Status.SetCondition(metav1.Condition{ + Type: "SaaS", + Status: status, + ObservedGeneration: in.Generation, + Reason: reason, + Message: msg, + }) +} + +func (in *VulnerabilityReport) SaaSStatusIsTrue() bool { + return in.Status.ConditionIsTrue("SaaS") +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:storageversion +//+kubebuilder:resource:shortName={vuln,vulns,vulnerabilities} +//+kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".spec.cluster",priority=0 +//+kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",priority=0 +//+kubebuilder:printcolumn:name="Total",type="string",JSONPath=".spec.summary.total",priority=0 +//+kubebuilder:printcolumn:name="Critical",type="string",JSONPath=".spec.summary.critical",priority=0 +//+kubebuilder:printcolumn:name="High",type="string",JSONPath=".spec.summary.high",priority=0 +//+kubebuilder:printcolumn:name="Medium",type="string",JSONPath=".spec.summary.medium",priority=1 +//+kubebuilder:printcolumn:name="Low",type="string",JSONPath=".spec.summary.low",priority=1 +//+kubebuilder:printcolumn:name="Unknown",type="string",JSONPath=".spec.summary.unknown",priority=1 +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",priority=0 + +// VulnerabilityReport is the Schema for the vulnerabilityreports API +// +genclient +type VulnerabilityReport struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VulnerabilityReportSpec `json:"spec,omitempty"` + Status VulnerabilityReportStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// VulnerabilityReportList contains a list of VulnerabilityReport +type VulnerabilityReportList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VulnerabilityReport `json:"items"` +} + +func init() { + SchemeBuilder.Register(&VulnerabilityReport{}, &VulnerabilityReportList{}) +} diff --git a/api/zora/v1alpha2/zz_generated.deepcopy.go b/api/zora/v1alpha2/zz_generated.deepcopy.go index 8078e15d..bf5eaae7 100644 --- a/api/zora/v1alpha2/zz_generated.deepcopy.go +++ b/api/zora/v1alpha2/zz_generated.deepcopy.go @@ -19,6 +19,7 @@ package v1alpha2 import ( + "github.com/undistro/zora/api/zora/v1alpha1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -132,3 +133,122 @@ func (in *Variable) DeepCopy() *Variable { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Vulnerability) DeepCopyInto(out *Vulnerability) { + *out = *in + in.VulnerabilityCommon.DeepCopyInto(&out.VulnerabilityCommon) + if in.Packages != nil { + in, out := &in.Packages, &out.Packages + *out = make([]v1alpha1.Package, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vulnerability. +func (in *Vulnerability) DeepCopy() *Vulnerability { + if in == nil { + return nil + } + out := new(Vulnerability) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityReport) DeepCopyInto(out *VulnerabilityReport) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReport. +func (in *VulnerabilityReport) DeepCopy() *VulnerabilityReport { + if in == nil { + return nil + } + out := new(VulnerabilityReport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VulnerabilityReport) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityReportList) DeepCopyInto(out *VulnerabilityReportList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VulnerabilityReport, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReportList. +func (in *VulnerabilityReportList) DeepCopy() *VulnerabilityReportList { + if in == nil { + return nil + } + out := new(VulnerabilityReportList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VulnerabilityReportList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityReportSpec) DeepCopyInto(out *VulnerabilityReportSpec) { + *out = *in + in.VulnerabilityReportCommon.DeepCopyInto(&out.VulnerabilityReportCommon) + if in.Vulnerabilities != nil { + in, out := &in.Vulnerabilities, &out.Vulnerabilities + *out = make([]Vulnerability, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReportSpec. +func (in *VulnerabilityReportSpec) DeepCopy() *VulnerabilityReportSpec { + if in == nil { + return nil + } + out := new(VulnerabilityReportSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VulnerabilityReportStatus) DeepCopyInto(out *VulnerabilityReportStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReportStatus. +func (in *VulnerabilityReportStatus) DeepCopy() *VulnerabilityReportStatus { + if in == nil { + return nil + } + out := new(VulnerabilityReportStatus) + in.DeepCopyInto(out) + return out +} diff --git a/charts/zora/Chart.yaml b/charts/zora/Chart.yaml index 29e03ceb..57a2cdae 100644 --- a/charts/zora/Chart.yaml +++ b/charts/zora/Chart.yaml @@ -17,7 +17,7 @@ name: zora description: A multi-plugin solution that reports misconfigurations and vulnerabilities by scanning your cluster at scheduled times. icon: https://zora-docs.undistro.io/v0.7/assets/logo.svg type: application -version: 0.8.5-rc2 -appVersion: "v0.8.5-rc2" +version: 0.8.5-rc4 +appVersion: "v0.8.5-rc4" sources: - https://github.com/undistro/zora diff --git a/charts/zora/README.md b/charts/zora/README.md index bd915f15..c6be42b2 100644 --- a/charts/zora/README.md +++ b/charts/zora/README.md @@ -1,6 +1,6 @@ # Zora Helm Chart -![Version: 0.8.5-rc2](https://img.shields.io/badge/Version-0.8.5--rc2-informational?style=flat-square&color=3CA9DD) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square&color=3CA9DD) ![AppVersion: v0.8.5-rc2](https://img.shields.io/badge/AppVersion-v0.8.5--rc2-informational?style=flat-square&color=3CA9DD) +![Version: 0.8.5-rc4](https://img.shields.io/badge/Version-0.8.5--rc4-informational?style=flat-square&color=3CA9DD) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square&color=3CA9DD) ![AppVersion: v0.8.5-rc4](https://img.shields.io/badge/AppVersion-v0.8.5--rc4-informational?style=flat-square&color=3CA9DD) A multi-plugin solution that reports misconfigurations and vulnerabilities by scanning your cluster at scheduled times. @@ -13,7 +13,7 @@ helm repo add undistro https://charts.undistro.io --force-update helm repo update undistro helm upgrade --install zora undistro/zora \ -n zora-system \ - --version 0.8.5-rc2 \ + --version 0.8.5-rc4 \ --create-namespace \ --wait \ --set clusterName="$(kubectl config current-context)" @@ -93,6 +93,7 @@ The following table lists the configurable parameters of the Zora chart and thei | operator.log.level | string | `"info"` | Log level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity | | operator.log.stacktraceLevel | string | `"error"` | Log level at and above which stacktraces are captured (one of 'info', 'error' or 'panic') | | operator.log.timeEncoding | string | `"rfc3339"` | Log time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano') | +| operator.webhook.enabled | bool | `true` | Specifies whether webhook server is enabled | | scan.misconfiguration.enabled | bool | `true` | Specifies whether misconfiguration scan is enabled | | scan.misconfiguration.schedule | string | Cron expression for every hour at the current minute + 5 minutes | Cluster scan schedule in Cron format for misconfiguration scan | | scan.misconfiguration.successfulScansHistoryLimit | int | `1` | The number of successful finished scans and their issues to retain. | diff --git a/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml b/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml index f507404c..e8670c31 100644 --- a/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml +++ b/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml @@ -18,6 +18,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.14.0 + zora.undistro.io/inject-conversion: "true" name: vulnerabilityreports.zora.undistro.io spec: group: zora.undistro.io @@ -26,251 +27,492 @@ spec: listKind: VulnerabilityReportList plural: vulnerabilityreports shortNames: - - vuln - - vulns - - vulnerabilities + - vuln + - vulns + - vulnerabilities singular: vulnerabilityreport scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .spec.cluster - name: Cluster - type: string - - jsonPath: .spec.image - name: Image - type: string - - jsonPath: .spec.summary.total - name: Total - type: string - - jsonPath: .spec.summary.critical - name: Critical - type: string - - jsonPath: .spec.summary.high - name: High - type: string - - jsonPath: .spec.summary.medium - name: Medium - priority: 1 - type: string - - jsonPath: .spec.summary.low - name: Low - priority: 1 - type: string - - jsonPath: .spec.summary.unknown - name: Unknown - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: VulnerabilityReport is the Schema for the vulnerabilityreports - API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport - properties: - architecture: - type: string - cluster: - type: string - digest: - type: string - distro: - properties: - name: - type: string - version: - type: string - type: object - image: - type: string - os: - type: string - resources: - additionalProperties: - items: - type: string - type: array - type: object - summary: - properties: - critical: - type: integer - high: - type: integer - low: - type: integer - medium: - type: integer - total: - type: integer - unknown: - type: integer - required: - - critical - - high - - low - - medium - - total - - unknown - type: object - tags: - items: + - additionalPrinterColumns: + - jsonPath: .spec.cluster + name: Cluster + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.summary.total + name: Total + type: string + - jsonPath: .spec.summary.critical + name: Critical + type: string + - jsonPath: .spec.summary.high + name: High + type: string + - jsonPath: .spec.summary.medium + name: Medium + priority: 1 + type: string + - jsonPath: .spec.summary.low + name: Low + priority: 1 + type: string + - jsonPath: .spec.summary.unknown + name: Unknown + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha1 version of VulnerabilityReport has been deprecated. Please use v1alpha2 instead. + name: v1alpha1 + schema: + openAPIV3Schema: + description: VulnerabilityReport is the Schema for the vulnerabilityreports API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport + properties: + architecture: + type: string + cluster: type: string - type: array - totalResources: - type: integer - vulnerabilities: - items: + digest: + type: string + distro: properties: - description: - type: string - fixVersion: - type: string - id: - type: string - lastModifiedDate: - format: date-time - type: string - package: - type: string - publishedDate: - format: date-time - type: string - score: - type: string - severity: - type: string - status: - type: string - title: - type: string - type: - type: string - url: + name: type: string version: type: string - required: - - id - - package - - severity - - title - - version type: object - type: array - required: - - cluster - - digest - - image - - resources - - summary - - totalResources - - vulnerabilities - type: object - status: - description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport - properties: - conditions: - description: Conditions the latest available observations of a resource's - current state. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 + image: + type: string + os: + type: string + resources: + additionalProperties: + items: type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 + type: array + type: object + summary: + properties: + critical: + type: integer + high: + type: integer + low: + type: integer + medium: type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + total: + type: integer + unknown: + type: integer + required: + - critical + - high + - low + - medium + - total + - unknown + type: object + tags: + items: + type: string + type: array + totalResources: + type: integer + vulnerabilities: + items: + properties: + description: + type: string + fixVersion: + type: string + id: + type: string + lastModifiedDate: + format: date-time + type: string + package: + type: string + publishedDate: + format: date-time + type: string + score: + type: string + severity: + type: string + status: + type: string + title: + type: string + type: + type: string + url: + type: string + version: + type: string + required: + - id + - package + - severity + - title + - version + type: object + type: array + required: + - cluster + - digest + - image + - resources + - summary + - totalResources + - vulnerabilities + type: object + status: + description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport + properties: + conditions: + description: Conditions the latest available observations of a resource's current state. + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration is the 'Generation' of the resource that + was last processed by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.cluster + name: Cluster + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.summary.total + name: Total + type: string + - jsonPath: .spec.summary.critical + name: Critical + type: string + - jsonPath: .spec.summary.high + name: High + type: string + - jsonPath: .spec.summary.medium + name: Medium + priority: 1 + type: string + - jsonPath: .spec.summary.low + name: Low + priority: 1 + type: string + - jsonPath: .spec.summary.unknown + name: Unknown + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: VulnerabilityReport is the Schema for the vulnerabilityreports API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport + properties: + architecture: + type: string + cluster: + type: string + digest: + type: string + distro: + properties: + name: type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown + version: type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: object + image: + type: string + os: + type: string + resources: + additionalProperties: + items: type: string + type: array + type: object + summary: + properties: + critical: + type: integer + high: + type: integer + low: + type: integer + medium: + type: integer + total: + type: integer + unknown: + type: integer required: - - lastTransitionTime - - message - - reason - - status - - type + - critical + - high + - low + - medium + - total + - unknown type: object - type: array - observedGeneration: - description: |- - ObservedGeneration is the 'Generation' of the resource that - was last processed by the controller. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} + tags: + items: + type: string + type: array + totalPackages: + description: |- + TotalPackages represents the total number of affected packages in this image. + A package affected by two vulnerabilities is counted twice. + type: integer + totalResources: + type: integer + totalUniquePackages: + description: |- + TotalUniquePackages represents the total number of unique affected packages in this image. + A package affected by multiple vulnerabilities is counted only once. + type: integer + vulnerabilities: + items: + properties: + description: + type: string + id: + type: string + lastModifiedDate: + format: date-time + type: string + packages: + items: + properties: + fixVersion: + type: string + package: + type: string + status: + type: string + type: + type: string + version: + type: string + required: + - package + - version + type: object + type: array + publishedDate: + format: date-time + type: string + score: + type: string + severity: + type: string + title: + type: string + url: + type: string + required: + - id + - packages + - severity + - title + type: object + type: array + required: + - cluster + - digest + - image + - resources + - summary + - totalPackages + - totalResources + - totalUniquePackages + - vulnerabilities + type: object + status: + description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport + properties: + conditions: + description: Conditions the latest available observations of a resource's current state. + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration is the 'Generation' of the resource that + was last processed by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/zora/templates/operator/deployment.yaml b/charts/zora/templates/operator/deployment.yaml index 3ae61817..57cc4b5b 100644 --- a/charts/zora/templates/operator/deployment.yaml +++ b/charts/zora/templates/operator/deployment.yaml @@ -11,7 +11,29 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +{{ $secretName := printf "%s-serving-cert" (include "zora.fullname" .) -}} +{{- $serviceName := printf "%s-webhook" (include "zora.fullname" .) -}} +{{- if .Values.operator.webhook.enabled -}} +{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace $secretName -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} +type: kubernetes.io/tls +data: +{{- if $existingSecret }} + {{- toYaml $existingSecret.data | nindent 2 }} +{{- else }} + {{- $cn := $serviceName }} + {{- $ca := genCA $cn 3650 }} + {{- $altNames := list ( printf "%s.%s" $serviceName .Release.Namespace ) ( printf "%s.%s.svc" $serviceName .Release.Namespace ) ( printf "%s.%s.svc.cluster.local" $serviceName .Release.Namespace ) }} + {{- $cert := genSignedCert $cn nil $altNames 3650 $ca }} + tls.key: {{ b64enc $cert.Key }} + tls.crt: {{ b64enc $cert.Cert }} + ca.crt: {{ b64enc $ca.Cert }} +{{- end }} +--- +{{- end -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -92,6 +114,9 @@ spec: - --checks-configmap-name={{ .Values.customChecksConfigMap }} - --kubexns-image={{ printf "%s:%s" .Values.kubexnsImage.repository .Values.kubexnsImage.tag }} - --update-crds={{ .Values.updateCRDs | default .Release.IsUpgrade }} + - --inject-conversion={{ .Values.operator.webhook.enabled }} + - --webhook-service-name={{ $serviceName }} + - --webhook-service-namespace={{ .Release.Namespace }} image: "{{ .Values.operator.image.repository }}:{{ .Values.operator.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.operator.image.pullPolicy }} ports: @@ -101,6 +126,15 @@ spec: - containerPort: 8080 protocol: TCP name: metrics + {{- if .Values.operator.webhook.enabled }} + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + {{- end }} livenessProbe: httpGet: path: /healthz @@ -117,6 +151,14 @@ spec: {{- toYaml .Values.operator.resources | nindent 12 }} securityContext: {{- toYaml .Values.operator.securityContext | nindent 12 }} + {{- if .Values.operator.webhook.enabled }} + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: {{ $secretName }} + optional: true + {{- end }} securityContext: {{- toYaml .Values.operator.podSecurityContext | nindent 8 }} serviceAccountName: {{ include "zora.operatorServiceAccountName" . }} diff --git a/charts/zora/templates/operator/webhook-service.yaml b/charts/zora/templates/operator/webhook-service.yaml new file mode 100644 index 00000000..3b86170f --- /dev/null +++ b/charts/zora/templates/operator/webhook-service.yaml @@ -0,0 +1,29 @@ +# Copyright 2024 Undistro Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.operator.webhook.enabled }} +apiVersion: v1 +kind: Service +metadata: + labels: + {{- include "zora.operatorLabels" . | nindent 4 }} + name: {{ include "zora.fullname" . }}-webhook +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + {{- include "zora.operatorSelectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/zora/values.yaml b/charts/zora/values.yaml index c67d50c4..28d997c2 100644 --- a/charts/zora/values.yaml +++ b/charts/zora/values.yaml @@ -141,7 +141,9 @@ operator: stacktraceLevel: error # -- Log time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano') timeEncoding: rfc3339 - + webhook: + # -- Specifies whether webhook server is enabled + enabled: true scan: misconfiguration: # -- Specifies whether misconfiguration scan is enabled diff --git a/cmd/main.go b/cmd/main.go index 8b9fecbc..ce116729 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,13 +24,14 @@ import ( "time" "github.com/undistro/zora/pkg/crds" - apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "go.uber.org/zap/zapcore" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes" @@ -39,6 +40,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" zorav1alpha1 "github.com/undistro/zora/api/zora/v1alpha1" zorav1alpha2 "github.com/undistro/zora/api/zora/v1alpha2" @@ -80,6 +82,11 @@ func main() { var kubexnsImage string var trivyPVC string var updateCRDs bool + var injectConversion bool + var caPath string + var webhookServiceName string + var webhookServiceNamespace string + var webhookServicePath string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -104,7 +111,17 @@ func main() { flag.StringVar(&kubexnsImage, "kubexns-image", "ghcr.io/undistro/kubexns:latest", "kubexns image") flag.StringVar(&trivyPVC, "trivy-db-pvc", "", "PersistentVolumeClaim name for Trivy DB") flag.BoolVar(&updateCRDs, "update-crds", false, - "If set, operator will update Zora CRDs if needed") + "If set to true, operator will update Zora CRDs if needed") + flag.BoolVar(&injectConversion, "inject-conversion", false, + "If set to true, operator will inject webhook conversion in annotated CRDs") + flag.StringVar(&caPath, "ca-path", "/tmp/k8s-webhook-server/serving-certs/ca.crt", + "Path of CA file to be injected in CRDs") + flag.StringVar(&webhookServiceName, "webhook-service-name", "zora-webhook", + "Webhook service name") + flag.StringVar(&webhookServiceNamespace, "webhook-service-namespace", "zora-system", + "Webhook service namespace") + flag.StringVar(&webhookServicePath, "webhook-service-path", "/convert", + "URL path for webhook conversion") opts := zap.Options{ Development: true, @@ -130,6 +147,9 @@ func main() { if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: tlsOpts, + }) restConfig := ctrl.GetConfigOrDie() mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ @@ -142,6 +162,7 @@ func main() { HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "e0f4eef4.zora.undistro.io", + WebhookServer: webhookServer, }) if err != nil { setupLog.Error(err, "unable to start manager") @@ -210,6 +231,12 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "CustomCheck") os.Exit(1) } + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = (&zorav1alpha1.VulnerabilityReport{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "VulnerabilityReport") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -222,8 +249,10 @@ func main() { } ctx := ctrl.SetupSignalHandler() - if updateCRDs { - if err := crds.Update(ctrllog.IntoContext(ctx, setupLog), apiextensionsv1client.NewForConfigOrDie(restConfig)); err != nil { + if updateCRDs || injectConversion { + extClient := apiextensionsv1client.NewForConfigOrDie(restConfig) + copts := crds.NewConversionOptions(injectConversion, webhookServiceName, webhookServiceNamespace, webhookServicePath, caPath) + if err := crds.Update(ctrllog.IntoContext(ctx, setupLog), extClient, *copts); err != nil { setupLog.Error(err, "unable to update CRDs") os.Exit(1) } diff --git a/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml b/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml index e6eb5bf5..5a08e1e5 100644 --- a/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml +++ b/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml @@ -4,6 +4,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.14.0 + zora.undistro.io/inject-conversion: "true" name: vulnerabilityreports.zora.undistro.io spec: group: zora.undistro.io @@ -12,251 +13,492 @@ spec: listKind: VulnerabilityReportList plural: vulnerabilityreports shortNames: - - vuln - - vulns - - vulnerabilities + - vuln + - vulns + - vulnerabilities singular: vulnerabilityreport scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .spec.cluster - name: Cluster - type: string - - jsonPath: .spec.image - name: Image - type: string - - jsonPath: .spec.summary.total - name: Total - type: string - - jsonPath: .spec.summary.critical - name: Critical - type: string - - jsonPath: .spec.summary.high - name: High - type: string - - jsonPath: .spec.summary.medium - name: Medium - priority: 1 - type: string - - jsonPath: .spec.summary.low - name: Low - priority: 1 - type: string - - jsonPath: .spec.summary.unknown - name: Unknown - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: VulnerabilityReport is the Schema for the vulnerabilityreports - API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport - properties: - architecture: - type: string - cluster: - type: string - digest: - type: string - distro: - properties: - name: - type: string - version: - type: string - type: object - image: - type: string - os: - type: string - resources: - additionalProperties: - items: - type: string - type: array - type: object - summary: - properties: - critical: - type: integer - high: - type: integer - low: - type: integer - medium: - type: integer - total: - type: integer - unknown: - type: integer - required: - - critical - - high - - low - - medium - - total - - unknown - type: object - tags: - items: + - additionalPrinterColumns: + - jsonPath: .spec.cluster + name: Cluster + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.summary.total + name: Total + type: string + - jsonPath: .spec.summary.critical + name: Critical + type: string + - jsonPath: .spec.summary.high + name: High + type: string + - jsonPath: .spec.summary.medium + name: Medium + priority: 1 + type: string + - jsonPath: .spec.summary.low + name: Low + priority: 1 + type: string + - jsonPath: .spec.summary.unknown + name: Unknown + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha1 version of VulnerabilityReport has been deprecated. Please use v1alpha2 instead. + name: v1alpha1 + schema: + openAPIV3Schema: + description: VulnerabilityReport is the Schema for the vulnerabilityreports API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport + properties: + architecture: + type: string + cluster: type: string - type: array - totalResources: - type: integer - vulnerabilities: - items: + digest: + type: string + distro: properties: - description: - type: string - fixVersion: - type: string - id: - type: string - lastModifiedDate: - format: date-time - type: string - package: - type: string - publishedDate: - format: date-time - type: string - score: - type: string - severity: - type: string - status: - type: string - title: - type: string - type: - type: string - url: + name: type: string version: type: string - required: - - id - - package - - severity - - title - - version type: object - type: array - required: - - cluster - - digest - - image - - resources - - summary - - totalResources - - vulnerabilities - type: object - status: - description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport - properties: - conditions: - description: Conditions the latest available observations of a resource's - current state. - items: - description: "Condition contains details for one aspect of the current - state of this API Resource.\n---\nThis struct is intended for - direct use as an array at the field path .status.conditions. For - example,\n\n\n\ttype FooStatus struct{\n\t // Represents the - observations of a foo's current state.\n\t // Known .status.conditions.type - are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // - +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t - \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" - patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t - \ // other fields\n\t}" - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 + image: + type: string + os: + type: string + resources: + additionalProperties: + items: type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 + type: array + type: object + summary: + properties: + critical: + type: integer + high: + type: integer + low: + type: integer + medium: type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + total: + type: integer + unknown: + type: integer + required: + - critical + - high + - low + - medium + - total + - unknown + type: object + tags: + items: + type: string + type: array + totalResources: + type: integer + vulnerabilities: + items: + properties: + description: + type: string + fixVersion: + type: string + id: + type: string + lastModifiedDate: + format: date-time + type: string + package: + type: string + publishedDate: + format: date-time + type: string + score: + type: string + severity: + type: string + status: + type: string + title: + type: string + type: + type: string + url: + type: string + version: + type: string + required: + - id + - package + - severity + - title + - version + type: object + type: array + required: + - cluster + - digest + - image + - resources + - summary + - totalResources + - vulnerabilities + type: object + status: + description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport + properties: + conditions: + description: Conditions the latest available observations of a resource's current state. + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration is the 'Generation' of the resource that + was last processed by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.cluster + name: Cluster + type: string + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .spec.summary.total + name: Total + type: string + - jsonPath: .spec.summary.critical + name: Critical + type: string + - jsonPath: .spec.summary.high + name: High + type: string + - jsonPath: .spec.summary.medium + name: Medium + priority: 1 + type: string + - jsonPath: .spec.summary.low + name: Low + priority: 1 + type: string + - jsonPath: .spec.summary.unknown + name: Unknown + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: VulnerabilityReport is the Schema for the vulnerabilityreports API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VulnerabilityReportSpec defines the desired state of VulnerabilityReport + properties: + architecture: + type: string + cluster: + type: string + digest: + type: string + distro: + properties: + name: type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown + version: type: string - type: - description: |- - type of condition in CamelCase or in foo.example.com/CamelCase. - --- - Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be - useful (see .node.status.conditions), the ability to deconflict is important. - The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: object + image: + type: string + os: + type: string + resources: + additionalProperties: + items: type: string + type: array + type: object + summary: + properties: + critical: + type: integer + high: + type: integer + low: + type: integer + medium: + type: integer + total: + type: integer + unknown: + type: integer required: - - lastTransitionTime - - message - - reason - - status - - type + - critical + - high + - low + - medium + - total + - unknown type: object - type: array - observedGeneration: - description: |- - ObservedGeneration is the 'Generation' of the resource that - was last processed by the controller. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} + tags: + items: + type: string + type: array + totalPackages: + description: |- + TotalPackages represents the total number of affected packages in this image. + A package affected by two vulnerabilities is counted twice. + type: integer + totalResources: + type: integer + totalUniquePackages: + description: |- + TotalUniquePackages represents the total number of unique affected packages in this image. + A package affected by multiple vulnerabilities is counted only once. + type: integer + vulnerabilities: + items: + properties: + description: + type: string + id: + type: string + lastModifiedDate: + format: date-time + type: string + packages: + items: + properties: + fixVersion: + type: string + package: + type: string + status: + type: string + type: + type: string + version: + type: string + required: + - package + - version + type: object + type: array + publishedDate: + format: date-time + type: string + score: + type: string + severity: + type: string + title: + type: string + url: + type: string + required: + - id + - packages + - severity + - title + type: object + type: array + required: + - cluster + - digest + - image + - resources + - summary + - totalPackages + - totalResources + - totalUniquePackages + - vulnerabilities + type: object + status: + description: VulnerabilityReportStatus defines the observed state of VulnerabilityReport + properties: + conditions: + description: Conditions the latest available observations of a resource's current state. + items: + description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: |- + ObservedGeneration is the 'Generation' of the resource that + was last processed by the controller. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 327bc7a3..c4d86ad6 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -1,3 +1,5 @@ +namespace: zora-system + # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default @@ -20,6 +22,7 @@ patches: #- path: patches/webhook_in_customchecks.yaml #- path: patches/webhook_in_vulnerabilityreports.yaml #- path: patches/webhook_in_zora_customchecks.yaml +#- path: patches/webhook_in_zora_vulnerabilityreports.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -31,10 +34,11 @@ patches: #- path: patches/cainjection_in_customchecks.yaml #- path: patches/cainjection_in_vulnerabilityreports.yaml #- path: patches/cainjection_in_zora_customchecks.yaml +#- path: patches/cainjection_in_zora_vulnerabilityreports.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. -#configurations: -#- kustomizeconfig.yaml +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/patches/webhook_in_zora_vulnerabilityreports.yaml b/config/crd/patches/webhook_in_zora_vulnerabilityreports.yaml new file mode 100644 index 00000000..10e579d7 --- /dev/null +++ b/config/crd/patches/webhook_in_zora_vulnerabilityreports.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: vulnerabilityreports.zora.undistro.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 5c186d5b..240807a7 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -20,7 +20,7 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -36,7 +36,7 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- path: manager_webhook_patch.yaml +- path: manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. @@ -54,22 +54,6 @@ patches: # fieldPath: .metadata.namespace # namespace of the certificate CR # targets: # - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: # kind: CustomResourceDefinition # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] @@ -85,22 +69,6 @@ patches: # fieldPath: .metadata.name # targets: # - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: # kind: CustomResourceDefinition # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..738de350 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 714456f0..68cbb9c4 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -71,6 +71,7 @@ spec: args: - --leader-elect image: controller:latest + imagePullPolicy: IfNotPresent name: manager securityContext: allowPrivilegeEscalation: false diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 3574961d..7cf7539b 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -8,4 +8,5 @@ resources: - zora_v1alpha1_customcheck_replicas.yaml.yaml - zora_v1alpha1_vulnerabilityreport.yaml - zora_v1alpha2_customcheck_labels.yaml.yaml +- zora_v1alpha2_vulnerabilityreport.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/zora_v1alpha1_vulnerabilityreport.yaml b/config/samples/zora_v1alpha1_vulnerabilityreport.yaml index ef79bb2f..deb6dfdd 100644 --- a/config/samples/zora_v1alpha1_vulnerabilityreport.yaml +++ b/config/samples/zora_v1alpha1_vulnerabilityreport.yaml @@ -2,11 +2,71 @@ apiVersion: zora.undistro.io/v1alpha1 kind: VulnerabilityReport metadata: labels: - app.kubernetes.io/name: vulnerabilityreport - app.kubernetes.io/instance: vulnerabilityreport-sample - app.kubernetes.io/part-of: zora - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: zora - name: vulnerabilityreport-sample + cluster: cluster + clusterUID: 9a1d324c-9170-4aa7-9f64-76f01c9d7989 + plugin: trivy + scanID: 50c8957e-c9e1-493a-9fa4-d0786deea017 + name: vulnerabilityreport-v1alpha1-sample spec: - # TODO(user): Add fields here + architecture: amd64 + cluster: cluster + digest: quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824 + distro: + name: alpine + version: 3.16.3 + image: quay.io/kiwigrid/k8s-sidecar:1.22.0 + os: linux + resources: + Deployment: + - ns/app1 + - ns/app2 + summary: + critical: 1 + high: 2 + low: 0 + medium: 0 + total: 3 + unknown: 0 + tags: + - quay.io/kiwigrid/k8s-sidecar:1.22.0 + totalResources: 2 + vulnerabilities: + - description: The function PEM_read_bio_ex() reads a PEM file from a BIO and ... + id: CVE-2022-4450 + lastModifiedDate: "2023-07-19T00:57:00Z" + fixVersion: 1.1.1t-r0 + package: libcrypto1.1 + status: fixed + type: alpine + version: 1.1.1s-r0 + publishedDate: "2023-02-08T20:15:00Z" + score: "7.5" + severity: HIGH + title: double free after calling PEM_read_bio_ex + url: https://avd.aquasec.com/nvd/cve-2022-4450 + - description: The function PEM_read_bio_ex() reads a PEM file from a BIO and ... + id: CVE-2022-4450 + lastModifiedDate: "2023-07-19T00:57:00Z" + fixVersion: 1.1.1t-r0 + package: libssl1.1 + status: fixed + type: alpine + version: 1.1.1s-r0 + publishedDate: "2023-02-08T20:15:00Z" + score: "7.5" + severity: HIGH + title: double free after calling PEM_read_bio_ex + url: https://avd.aquasec.com/nvd/cve-2022-4450 + - description: Certifi is a curated collection of Root Certificates for validating ... + id: CVE-2023-37920 + lastModifiedDate: "2023-08-12T06:16:00Z" + fixVersion: 2023.7.22 + package: certifi + status: fixed + type: python-pkg + version: 2022.12.7 + publishedDate: "2023-07-25T21:15:00Z" + score: "9.8" + severity: CRITICAL + title: Removal of e-Tugra root certificate + url: https://avd.aquasec.com/nvd/cve-2023-37920 diff --git a/config/samples/zora_v1alpha2_vulnerabilityreport.yaml b/config/samples/zora_v1alpha2_vulnerabilityreport.yaml new file mode 100644 index 00000000..41a490b9 --- /dev/null +++ b/config/samples/zora_v1alpha2_vulnerabilityreport.yaml @@ -0,0 +1,66 @@ +apiVersion: zora.undistro.io/v1alpha2 +kind: VulnerabilityReport +metadata: + labels: + cluster: cluster + clusterUID: 9a1d324c-9170-4aa7-9f64-76f01c9d7989 + plugin: trivy + scanID: 50c8957e-c9e1-493a-9fa4-d0786deea017 + name: vulnerabilityreport-v1alpha2-sample +spec: + architecture: amd64 + cluster: cluster + digest: quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824 + distro: + name: alpine + version: 3.16.3 + image: quay.io/kiwigrid/k8s-sidecar:1.22.0 + os: linux + resources: + Deployment: + - ns/app1 + - ns/app2 + summary: + critical: 1 + high: 1 + low: 0 + medium: 0 + total: 2 + unknown: 0 + tags: + - quay.io/kiwigrid/k8s-sidecar:1.22.0 + totalResources: 2 + vulnerabilities: + - description: The function PEM_read_bio_ex() reads a PEM file from a BIO and ... + id: CVE-2022-4450 + lastModifiedDate: "2023-07-19T00:57:00Z" + packages: + - fixVersion: 1.1.1t-r0 + package: libcrypto1.1 + status: fixed + type: alpine + version: 1.1.1s-r0 + - fixVersion: 1.1.1t-r0 + package: libssl1.1 + status: fixed + type: alpine + version: 1.1.1s-r0 + publishedDate: "2023-02-08T20:15:00Z" + score: "7.5" + severity: HIGH + title: double free after calling PEM_read_bio_ex + url: https://avd.aquasec.com/nvd/cve-2022-4450 + - description: Certifi is a curated collection of Root Certificates for validating ... + id: CVE-2023-37920 + lastModifiedDate: "2023-08-12T06:16:00Z" + packages: + - fixVersion: 2023.7.22 + package: certifi + status: fixed + type: python-pkg + version: 2022.12.7 + publishedDate: "2023-07-25T21:15:00Z" + score: "9.8" + severity: CRITICAL + title: Removal of e-Tugra root certificate + url: https://avd.aquasec.com/nvd/cve-2023-37920 diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..2da25882 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- service.yaml diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..1772e3f0 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: zora + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/go.mod b/go.mod index fadaf025..16e8e539 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 github.com/robfig/cron/v3 v3.0.1 + github.com/stretchr/testify v1.9.0 github.com/undistro/marvin v0.2.3 go.uber.org/zap v1.27.0 k8s.io/api v0.29.3 @@ -90,7 +91,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.etcd.io/bbolt v1.3.8 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect diff --git a/internal/saas/client.go b/internal/saas/client.go index 716c8520..8c1a24db 100644 --- a/internal/saas/client.go +++ b/internal/saas/client.go @@ -23,13 +23,12 @@ import ( "net/url" "path" - "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" ) const ( - workspacePathF = "zora/api/v1alpha1/workspaces/%s" - clusterPathF = "namespaces/%s/clusters/%s" - versionHeader = "x-zora-version" + clusterPathF = "zora/api/%s/workspaces/%s/namespaces/%s/clusters/%s" + versionHeader = "x-zora-version" ) var allowedStatus = []int{ @@ -44,7 +43,7 @@ type Client interface { DeleteCluster(ctx context.Context, namespace, name string) error PutClusterScan(ctx context.Context, namespace, name string, pluginStatus map[string]*PluginStatus) error DeleteClusterScan(ctx context.Context, namespace, name string) error - PutVulnerabilityReport(ctx context.Context, namespace, name string, vulnReport v1alpha1.VulnerabilityReport) error + PutVulnerabilityReport(ctx context.Context, namespace, name string, vulnReport v1alpha2.VulnerabilityReport) error PutClusterStatus(ctx context.Context, namespace, name string, pluginStatus map[string]*PluginStatus) error } @@ -60,7 +59,6 @@ func NewClient(baseURL, version, workspaceID string, httpclient *http.Client) (C if err != nil { return nil, err } - u.Path = path.Join(u.Path, fmt.Sprintf(workspacePathF, workspaceID)) return &client{ version: version, baseURL: u, @@ -70,7 +68,7 @@ func NewClient(baseURL, version, workspaceID string, httpclient *http.Client) (C } func (r *client) PutCluster(ctx context.Context, cluster Cluster) error { - u := r.clusterURL(cluster.Namespace, cluster.Name) + u := r.clusterURL("v1alpha1", cluster.Namespace, cluster.Name) b, err := json.Marshal(cluster) if err != nil { return err @@ -90,7 +88,7 @@ func (r *client) PutCluster(ctx context.Context, cluster Cluster) error { } func (r *client) DeleteCluster(ctx context.Context, namespace, name string) error { - u := r.clusterURL(namespace, name) + u := r.clusterURL("v1alpha1", namespace, name) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u, nil) if err != nil { return err @@ -105,7 +103,7 @@ func (r *client) DeleteCluster(ctx context.Context, namespace, name string) erro } func (r *client) PutClusterScan(ctx context.Context, namespace, name string, pluginStatus map[string]*PluginStatus) error { - u := r.clusterURL(namespace, name, "scan") + u := r.clusterURL("v1alpha1", namespace, name, "scan") b, err := json.Marshal(pluginStatus) if err != nil { return err @@ -124,8 +122,8 @@ func (r *client) PutClusterScan(ctx context.Context, namespace, name string, plu return validateStatus(res) } -func (r *client) PutVulnerabilityReport(ctx context.Context, namespace, name string, vulnReport v1alpha1.VulnerabilityReport) error { - u := r.clusterURL(namespace, name, "vulnerabilityreports") +func (r *client) PutVulnerabilityReport(ctx context.Context, namespace, name string, vulnReport v1alpha2.VulnerabilityReport) error { + u := r.clusterURL("v1alpha2", namespace, name, "vulnerabilityreports") b, err := json.Marshal(vulnReport) if err != nil { return err @@ -145,7 +143,7 @@ func (r *client) PutVulnerabilityReport(ctx context.Context, namespace, name str } func (r *client) DeleteClusterScan(ctx context.Context, namespace, name string) error { - u := r.clusterURL(namespace, name, "scan") + u := r.clusterURL("v1alpha1", namespace, name, "scan") req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u, nil) if err != nil { return err @@ -160,7 +158,7 @@ func (r *client) DeleteClusterScan(ctx context.Context, namespace, name string) } func (r *client) PutClusterStatus(ctx context.Context, namespace, name string, pluginStatus map[string]*PluginStatus) error { - u := r.clusterURL(namespace, name, "status") + u := r.clusterURL("v1alpha1", namespace, name, "status") b, err := json.Marshal(pluginStatus) if err != nil { return err @@ -179,8 +177,8 @@ func (r *client) PutClusterStatus(ctx context.Context, namespace, name string, p return validateStatus(res) } -func (r *client) clusterURL(namespace, name string, extra ...string) string { - p := path.Join(r.baseURL.Path, fmt.Sprintf(clusterPathF, namespace, name)) +func (r *client) clusterURL(version, namespace, name string, extra ...string) string { + p := path.Join(r.baseURL.Path, fmt.Sprintf(clusterPathF, version, r.workspaceID, namespace, name)) if len(extra) > 0 { tmp := []string{p} p = path.Join(append(tmp, extra...)...) diff --git a/internal/saas/hooks.go b/internal/saas/hooks.go index 6106a532..e59b4f8a 100644 --- a/internal/saas/hooks.go +++ b/internal/saas/hooks.go @@ -26,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" ) type ClusterHook func(ctx context.Context, cluster *v1alpha1.Cluster) error @@ -160,13 +161,12 @@ func pushVulns(scl Client, cl ctrlClient.Client, ctx context.Context, cs *v1alph pluginProcessedResources := getVulnerabilityProcessedResources(metaList.Items) if reflect.DeepEqual(pluginProcessedResources, cs.Status.ProcessedVulnerabilities) { - log := log.FromContext(ctx) - log.Info("Skipping vulnerabilities, no changes from processed vulnerabilities") + log.FromContext(ctx).Info("Skipping vulnerabilities, no changes from processed vulnerabilities") return nil } for _, i := range metaList.Items { - vulnReport := &v1alpha1.VulnerabilityReport{} + vulnReport := &v1alpha2.VulnerabilityReport{} if err := cl.Get(ctx, types.NamespacedName{Namespace: i.Namespace, Name: i.Name}, vulnReport); err != nil { return err } diff --git a/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_vulnerabilityreport.go b/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_vulnerabilityreport.go new file mode 100644 index 00000000..ce9c47bc --- /dev/null +++ b/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_vulnerabilityreport.go @@ -0,0 +1,125 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha2 "github.com/undistro/zora/api/zora/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeVulnerabilityReports implements VulnerabilityReportInterface +type FakeVulnerabilityReports struct { + Fake *FakeZoraV1alpha2 + ns string +} + +var vulnerabilityreportsResource = v1alpha2.SchemeGroupVersion.WithResource("vulnerabilityreports") + +var vulnerabilityreportsKind = v1alpha2.SchemeGroupVersion.WithKind("VulnerabilityReport") + +// Get takes name of the vulnerabilityReport, and returns the corresponding vulnerabilityReport object, and an error if there is any. +func (c *FakeVulnerabilityReports) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VulnerabilityReport, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(vulnerabilityreportsResource, c.ns, name), &v1alpha2.VulnerabilityReport{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VulnerabilityReport), err +} + +// List takes label and field selectors, and returns the list of VulnerabilityReports that match those selectors. +func (c *FakeVulnerabilityReports) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VulnerabilityReportList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(vulnerabilityreportsResource, vulnerabilityreportsKind, c.ns, opts), &v1alpha2.VulnerabilityReportList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.VulnerabilityReportList{ListMeta: obj.(*v1alpha2.VulnerabilityReportList).ListMeta} + for _, item := range obj.(*v1alpha2.VulnerabilityReportList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested vulnerabilityReports. +func (c *FakeVulnerabilityReports) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(vulnerabilityreportsResource, c.ns, opts)) + +} + +// Create takes the representation of a vulnerabilityReport and creates it. Returns the server's representation of the vulnerabilityReport, and an error, if there is any. +func (c *FakeVulnerabilityReports) Create(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.CreateOptions) (result *v1alpha2.VulnerabilityReport, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(vulnerabilityreportsResource, c.ns, vulnerabilityReport), &v1alpha2.VulnerabilityReport{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VulnerabilityReport), err +} + +// Update takes the representation of a vulnerabilityReport and updates it. Returns the server's representation of the vulnerabilityReport, and an error, if there is any. +func (c *FakeVulnerabilityReports) Update(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (result *v1alpha2.VulnerabilityReport, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(vulnerabilityreportsResource, c.ns, vulnerabilityReport), &v1alpha2.VulnerabilityReport{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VulnerabilityReport), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeVulnerabilityReports) UpdateStatus(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (*v1alpha2.VulnerabilityReport, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(vulnerabilityreportsResource, "status", c.ns, vulnerabilityReport), &v1alpha2.VulnerabilityReport{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VulnerabilityReport), err +} + +// Delete takes name of the vulnerabilityReport and deletes it. Returns an error if one occurs. +func (c *FakeVulnerabilityReports) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(vulnerabilityreportsResource, c.ns, name, opts), &v1alpha2.VulnerabilityReport{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeVulnerabilityReports) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(vulnerabilityreportsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.VulnerabilityReportList{}) + return err +} + +// Patch applies the patch and returns the patched vulnerabilityReport. +func (c *FakeVulnerabilityReports) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VulnerabilityReport, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(vulnerabilityreportsResource, c.ns, name, pt, data, subresources...), &v1alpha2.VulnerabilityReport{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.VulnerabilityReport), err +} diff --git a/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_zora_client.go b/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_zora_client.go index f8fbb14f..7e07920e 100644 --- a/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_zora_client.go +++ b/pkg/clientset/versioned/typed/zora/v1alpha2/fake/fake_zora_client.go @@ -16,6 +16,10 @@ func (c *FakeZoraV1alpha2) CustomChecks(namespace string) v1alpha2.CustomCheckIn return &FakeCustomChecks{c, namespace} } +func (c *FakeZoraV1alpha2) VulnerabilityReports(namespace string) v1alpha2.VulnerabilityReportInterface { + return &FakeVulnerabilityReports{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeZoraV1alpha2) RESTClient() rest.Interface { diff --git a/pkg/clientset/versioned/typed/zora/v1alpha2/generated_expansion.go b/pkg/clientset/versioned/typed/zora/v1alpha2/generated_expansion.go index b2745581..75440e10 100644 --- a/pkg/clientset/versioned/typed/zora/v1alpha2/generated_expansion.go +++ b/pkg/clientset/versioned/typed/zora/v1alpha2/generated_expansion.go @@ -3,3 +3,5 @@ package v1alpha2 type CustomCheckExpansion interface{} + +type VulnerabilityReportExpansion interface{} diff --git a/pkg/clientset/versioned/typed/zora/v1alpha2/vulnerabilityreport.go b/pkg/clientset/versioned/typed/zora/v1alpha2/vulnerabilityreport.go new file mode 100644 index 00000000..7f48c070 --- /dev/null +++ b/pkg/clientset/versioned/typed/zora/v1alpha2/vulnerabilityreport.go @@ -0,0 +1,179 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + "time" + + v1alpha2 "github.com/undistro/zora/api/zora/v1alpha2" + scheme "github.com/undistro/zora/pkg/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// VulnerabilityReportsGetter has a method to return a VulnerabilityReportInterface. +// A group's client should implement this interface. +type VulnerabilityReportsGetter interface { + VulnerabilityReports(namespace string) VulnerabilityReportInterface +} + +// VulnerabilityReportInterface has methods to work with VulnerabilityReport resources. +type VulnerabilityReportInterface interface { + Create(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.CreateOptions) (*v1alpha2.VulnerabilityReport, error) + Update(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (*v1alpha2.VulnerabilityReport, error) + UpdateStatus(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (*v1alpha2.VulnerabilityReport, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha2.VulnerabilityReport, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.VulnerabilityReportList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VulnerabilityReport, err error) + VulnerabilityReportExpansion +} + +// vulnerabilityReports implements VulnerabilityReportInterface +type vulnerabilityReports struct { + client rest.Interface + ns string +} + +// newVulnerabilityReports returns a VulnerabilityReports +func newVulnerabilityReports(c *ZoraV1alpha2Client, namespace string) *vulnerabilityReports { + return &vulnerabilityReports{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the vulnerabilityReport, and returns the corresponding vulnerabilityReport object, and an error if there is any. +func (c *vulnerabilityReports) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.VulnerabilityReport, err error) { + result = &v1alpha2.VulnerabilityReport{} + err = c.client.Get(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of VulnerabilityReports that match those selectors. +func (c *vulnerabilityReports) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.VulnerabilityReportList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.VulnerabilityReportList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested vulnerabilityReports. +func (c *vulnerabilityReports) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a vulnerabilityReport and creates it. Returns the server's representation of the vulnerabilityReport, and an error, if there is any. +func (c *vulnerabilityReports) Create(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.CreateOptions) (result *v1alpha2.VulnerabilityReport, err error) { + result = &v1alpha2.VulnerabilityReport{} + err = c.client.Post(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(vulnerabilityReport). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a vulnerabilityReport and updates it. Returns the server's representation of the vulnerabilityReport, and an error, if there is any. +func (c *vulnerabilityReports) Update(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (result *v1alpha2.VulnerabilityReport, err error) { + result = &v1alpha2.VulnerabilityReport{} + err = c.client.Put(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + Name(vulnerabilityReport.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(vulnerabilityReport). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *vulnerabilityReports) UpdateStatus(ctx context.Context, vulnerabilityReport *v1alpha2.VulnerabilityReport, opts v1.UpdateOptions) (result *v1alpha2.VulnerabilityReport, err error) { + result = &v1alpha2.VulnerabilityReport{} + err = c.client.Put(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + Name(vulnerabilityReport.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(vulnerabilityReport). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the vulnerabilityReport and deletes it. Returns an error if one occurs. +func (c *vulnerabilityReports) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *vulnerabilityReports) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("vulnerabilityreports"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched vulnerabilityReport. +func (c *vulnerabilityReports) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.VulnerabilityReport, err error) { + result = &v1alpha2.VulnerabilityReport{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("vulnerabilityreports"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/clientset/versioned/typed/zora/v1alpha2/zora_client.go b/pkg/clientset/versioned/typed/zora/v1alpha2/zora_client.go index c8404871..2fd936ae 100644 --- a/pkg/clientset/versioned/typed/zora/v1alpha2/zora_client.go +++ b/pkg/clientset/versioned/typed/zora/v1alpha2/zora_client.go @@ -13,6 +13,7 @@ import ( type ZoraV1alpha2Interface interface { RESTClient() rest.Interface CustomChecksGetter + VulnerabilityReportsGetter } // ZoraV1alpha2Client is used to interact with features provided by the zora group. @@ -24,6 +25,10 @@ func (c *ZoraV1alpha2Client) CustomChecks(namespace string) CustomCheckInterface return newCustomChecks(c, namespace) } +func (c *ZoraV1alpha2Client) VulnerabilityReports(namespace string) VulnerabilityReportInterface { + return newVulnerabilityReports(c, namespace) +} + // NewForConfig creates a new ZoraV1alpha2Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/crds/crds.go b/pkg/crds/crds.go index b1798ae7..bc7e1b45 100644 --- a/pkg/crds/crds.go +++ b/pkg/crds/crds.go @@ -17,6 +17,7 @@ package crds import ( "context" "fmt" + "os" "path/filepath" "sort" @@ -26,15 +27,34 @@ import ( "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) -var CRDs []apiextensionsv1.CustomResourceDefinition +const AnnotationInjectConversion = "zora.undistro.io/inject-conversion" + +var ( + log = logf.Log.WithName("crds") + CRDs []apiextensionsv1.CustomResourceDefinition + noneConversion = apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter} +) + +type ConversionOptions struct { + Enabled bool + WebhookServiceName string + WebhookServiceNamespace string + WebhookServicePath string + CAPath string + + conversion *apiextensionsv1.CustomResourceConversion +} + +func NewConversionOptions(enabled bool, svcName, svcNamespace, svcPath, caPath string) *ConversionOptions { + return &ConversionOptions{Enabled: enabled, WebhookServiceName: svcName, WebhookServiceNamespace: svcNamespace, WebhookServicePath: svcPath, CAPath: caPath} +} // Update updates Zora CRDs if needed -func Update(ctx context.Context, client *apiextensionsv1client.ApiextensionsV1Client) error { - log := ctrllog.FromContext(ctx) +func Update(ctx context.Context, client *apiextensionsv1client.ApiextensionsV1Client, opts ConversionOptions) error { for _, crd := range CRDs { existing, err := client.CustomResourceDefinitions().Get(ctx, crd.Name, metav1.GetOptions{}) if err != nil { @@ -44,7 +64,10 @@ func Update(ctx context.Context, client *apiextensionsv1client.ApiextensionsV1Cl } return err } - obj, updatedFields := merge(*existing, crd) + obj, updatedFields, err := merge(*existing, crd, opts) + if err != nil { + return err + } if len(updatedFields) == 0 { log.Info("Unchanged CRD", "name", crd.Name) continue @@ -57,7 +80,7 @@ func Update(ctx context.Context, client *apiextensionsv1client.ApiextensionsV1Cl return nil } -func merge(existing, desired apiextensionsv1.CustomResourceDefinition) (*apiextensionsv1.CustomResourceDefinition, []string) { +func merge(existing, desired apiextensionsv1.CustomResourceDefinition, opts ConversionOptions) (*apiextensionsv1.CustomResourceDefinition, []string, error) { existingVersions := make(map[string]apiextensionsv1.CustomResourceDefinitionVersion, len(existing.Spec.Versions)) for _, v := range existing.Spec.Versions { existingVersions[v.Name] = v @@ -65,13 +88,35 @@ func merge(existing, desired apiextensionsv1.CustomResourceDefinition) (*apiexte result := existing.DeepCopy() var updatedFields []string + if !equality.Semantic.DeepEqual(result.Annotations, desired.Annotations) { + for k, v := range desired.Annotations { + if result.Annotations == nil { + result.Annotations = make(map[string]string, len(desired.Annotations)) + } + result.Annotations[k] = v + } + updatedFields = append(updatedFields, "metadata.annotations") + } + if result.Spec.PreserveUnknownFields != desired.Spec.PreserveUnknownFields { result.Spec.PreserveUnknownFields = desired.Spec.PreserveUnknownFields updatedFields = append(updatedFields, "spec.preserveUnknownFields") } + conversionUpdated := false if !equality.Semantic.DeepEqual(conversionOrNone(result.Spec.Conversion), conversionOrNone(desired.Spec.Conversion)) { result.Spec.Conversion = desired.Spec.Conversion + conversionUpdated = true + } + if opts.shouldInjectConversion(result) { + c, err := opts.getConversion() + if err != nil { + return nil, nil, err + } + result.Spec.Conversion = c + conversionUpdated = true + } + if conversionUpdated { updatedFields = append(updatedFields, "spec.conversion") } @@ -117,14 +162,48 @@ func merge(existing, desired apiextensionsv1.CustomResourceDefinition) (*apiexte updatedFields = append(updatedFields, fmt.Sprintf(`spec.versions[?(@.name==%q)].deprecationWarning`, desiredVersion.Name)) } } - return result, updatedFields + return result, updatedFields, nil +} + +// shouldInjectConversion returns true if the given object is annotated for conversion injecting +func (opts ConversionOptions) shouldInjectConversion(object metav1.Object) bool { + a := object.GetAnnotations() + return opts.Enabled && a != nil && a[AnnotationInjectConversion] == "true" +} + +// getConversion returns a CustomResourceConversion from the given options +func (opts ConversionOptions) getConversion() (*apiextensionsv1.CustomResourceConversion, error) { + if opts.conversion != nil { + return opts.conversion, nil + } + ca, err := os.ReadFile(opts.CAPath) + if err != nil { + log.Error(err, "failed to read certificate") + return nil, err + } + opts.conversion = &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.WebhookConverter, + Webhook: &apiextensionsv1.WebhookConversion{ + ConversionReviewVersions: []string{"v1"}, + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + Service: &apiextensionsv1.ServiceReference{ + Namespace: opts.WebhookServiceNamespace, + Name: opts.WebhookServiceName, + Path: &opts.WebhookServicePath, + }, + CABundle: ca, + }, + }, + } + return opts.conversion, nil } +// conversionOrNone returns a CustomResourceConversion with None strategy if the given parameter is nil func conversionOrNone(c *apiextensionsv1.CustomResourceConversion) *apiextensionsv1.CustomResourceConversion { if c != nil { return c } - return &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter} + return &noneConversion } func init() { diff --git a/pkg/crds/crds_test.go b/pkg/crds/crds_test.go index 4cbcf9f1..6f522517 100644 --- a/pkg/crds/crds_test.go +++ b/pkg/crds/crds_test.go @@ -15,11 +15,14 @@ package crds import ( + "os" + "path/filepath" "reflect" "sort" "testing" "github.com/google/go-cmp/cmp" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" @@ -27,7 +30,10 @@ import ( var ( exampleCRD = v1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{Name: "examples.zora.undistro.io"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "examples.zora.undistro.io", + Annotations: map[string]string{"keep": "true"}, + }, Spec: v1.CustomResourceDefinitionSpec{ Group: "zora.undistro.io", Names: v1.CustomResourceDefinitionNames{ @@ -86,20 +92,24 @@ var ( ) func TestMergeCRDs(t *testing.T) { + tmpDir, caPath := setupTempCerts(t) type args struct { existing v1.CustomResourceDefinition updateFunc func(*v1.CustomResourceDefinition) + opts ConversionOptions } tests := []struct { - name string - args args - want *v1.CustomResourceDefinition - fields []string + name string + args args + want *v1.CustomResourceDefinition + fields []string + wantErr bool }{ { name: "equal", args: args{ existing: exampleCRD, + opts: ConversionOptions{Enabled: true}, updateFunc: func(crd *v1.CustomResourceDefinition) { // just sorting update crd.Spec.Names.ShortNames = []string{"exs", "ex"} @@ -112,10 +122,24 @@ func TestMergeCRDs(t *testing.T) { want: &exampleCRD, fields: nil, }, + { + name: "disabled injection and annotated CRD", + args: args{ + existing: exampleCRD, + opts: ConversionOptions{Enabled: false}, + updateFunc: func(crd *v1.CustomResourceDefinition) { + crd.Annotations[AnnotationInjectConversion] = "true" + }, + }, + want: annotateCRD(exampleCRD), + fields: []string{"metadata.annotations"}, + wantErr: false, + }, { name: "ignored fields", args: args{ existing: exampleCRD, + opts: ConversionOptions{Enabled: true}, updateFunc: func(crd *v1.CustomResourceDefinition) { crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["new"] = v1.JSONSchemaProps{Type: "string"} crd.Spec.Scope = "Cluster" @@ -133,7 +157,18 @@ func TestMergeCRDs(t *testing.T) { name: "allowed updates", args: args{ existing: exampleCRD, + opts: ConversionOptions{ + Enabled: true, + WebhookServiceName: "zora-webhook", + WebhookServiceNamespace: "zora-system", + WebhookServicePath: "/convert", + CAPath: caPath, + }, updateFunc: func(crd *v1.CustomResourceDefinition) { + crd.ObjectMeta.Annotations = map[string]string{ + "foo": "bar", + AnnotationInjectConversion: "true", + } crd.Spec.PreserveUnknownFields = true crd.Spec.Conversion = &v1.CustomResourceConversion{Strategy: v1.WebhookConverter} crd.Spec.Names.ShortNames = append(crd.Spec.Names.ShortNames, "new") @@ -147,6 +182,7 @@ func TestMergeCRDs(t *testing.T) { }, }, fields: []string{ + "metadata.annotations", "spec.preserveUnknownFields", "spec.conversion", "spec.names.shortNames", @@ -159,7 +195,14 @@ func TestMergeCRDs(t *testing.T) { `spec.versions[?(@.name=="v1alpha2")]`, }, want: &v1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{Name: "examples.zora.undistro.io"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "examples.zora.undistro.io", + Annotations: map[string]string{ + "keep": "true", + "foo": "bar", + AnnotationInjectConversion: "true", + }, + }, Spec: v1.CustomResourceDefinitionSpec{ Group: "zora.undistro.io", Names: v1.CustomResourceDefinitionNames{ @@ -193,17 +236,43 @@ func TestMergeCRDs(t *testing.T) { }, v1alpha2Version, }, - Conversion: &v1.CustomResourceConversion{Strategy: "Webhook"}, + Conversion: &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.WebhookConverter, + Webhook: &apiextensionsv1.WebhookConversion{ + ConversionReviewVersions: []string{"v1"}, + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + Service: &apiextensionsv1.ServiceReference{ + Name: "zora-webhook", + Namespace: "zora-system", + Path: pointer.String("/convert"), + }, + CABundle: []byte("test"), + }, + }, + }, PreserveUnknownFields: true, }, }, }, + { + name: "certificate file not found", + args: args{ + existing: *annotateCRD(exampleCRD), + updateFunc: func(crd *v1.CustomResourceDefinition) {}, + opts: ConversionOptions{Enabled: true, CAPath: filepath.Join(tmpDir, "foo.crt")}, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { desired := tt.args.existing.DeepCopy() tt.args.updateFunc(desired) - got, fields := merge(tt.args.existing, *desired) + got, fields, err := merge(tt.args.existing, *desired, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("merge() error = %v, wantErr %v", err, tt.wantErr) + return + } if !reflect.DeepEqual(got, tt.want) { t.Errorf("merge() mismatch (-want +got):\n%s", cmp.Diff(tt.want, got)) } @@ -215,3 +284,18 @@ func TestMergeCRDs(t *testing.T) { }) } } + +func setupTempCerts(t *testing.T) (string, string) { + tmpDir := t.TempDir() + caPath := filepath.Join(tmpDir, "ca.crt") + if err := os.WriteFile(caPath, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + return tmpDir, caPath +} + +func annotateCRD(crd v1.CustomResourceDefinition) *v1.CustomResourceDefinition { + c := crd.DeepCopy() + c.Annotations[AnnotationInjectConversion] = "true" + return c +} diff --git a/pkg/informers/externalversions/generic.go b/pkg/informers/externalversions/generic.go index 2da68de1..c46665d4 100644 --- a/pkg/informers/externalversions/generic.go +++ b/pkg/informers/externalversions/generic.go @@ -54,6 +54,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=zora, Version=v1alpha2 case v1alpha2.SchemeGroupVersion.WithResource("customchecks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Zora().V1alpha2().CustomChecks().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("vulnerabilityreports"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Zora().V1alpha2().VulnerabilityReports().Informer()}, nil } diff --git a/pkg/informers/externalversions/zora/v1alpha2/interface.go b/pkg/informers/externalversions/zora/v1alpha2/interface.go index 67127396..aff35ee9 100644 --- a/pkg/informers/externalversions/zora/v1alpha2/interface.go +++ b/pkg/informers/externalversions/zora/v1alpha2/interface.go @@ -10,6 +10,8 @@ import ( type Interface interface { // CustomChecks returns a CustomCheckInformer. CustomChecks() CustomCheckInformer + // VulnerabilityReports returns a VulnerabilityReportInformer. + VulnerabilityReports() VulnerabilityReportInformer } type version struct { @@ -27,3 +29,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (v *version) CustomChecks() CustomCheckInformer { return &customCheckInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// VulnerabilityReports returns a VulnerabilityReportInformer. +func (v *version) VulnerabilityReports() VulnerabilityReportInformer { + return &vulnerabilityReportInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/informers/externalversions/zora/v1alpha2/vulnerabilityreport.go b/pkg/informers/externalversions/zora/v1alpha2/vulnerabilityreport.go new file mode 100644 index 00000000..ee4b22cb --- /dev/null +++ b/pkg/informers/externalversions/zora/v1alpha2/vulnerabilityreport.go @@ -0,0 +1,74 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + time "time" + + zorav1alpha2 "github.com/undistro/zora/api/zora/v1alpha2" + versioned "github.com/undistro/zora/pkg/clientset/versioned" + internalinterfaces "github.com/undistro/zora/pkg/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/undistro/zora/pkg/listers/zora/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// VulnerabilityReportInformer provides access to a shared informer and lister for +// VulnerabilityReports. +type VulnerabilityReportInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.VulnerabilityReportLister +} + +type vulnerabilityReportInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewVulnerabilityReportInformer constructs a new informer for VulnerabilityReport type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewVulnerabilityReportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredVulnerabilityReportInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredVulnerabilityReportInformer constructs a new informer for VulnerabilityReport type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredVulnerabilityReportInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ZoraV1alpha2().VulnerabilityReports(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ZoraV1alpha2().VulnerabilityReports(namespace).Watch(context.TODO(), options) + }, + }, + &zorav1alpha2.VulnerabilityReport{}, + resyncPeriod, + indexers, + ) +} + +func (f *vulnerabilityReportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredVulnerabilityReportInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *vulnerabilityReportInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&zorav1alpha2.VulnerabilityReport{}, f.defaultInformer) +} + +func (f *vulnerabilityReportInformer) Lister() v1alpha2.VulnerabilityReportLister { + return v1alpha2.NewVulnerabilityReportLister(f.Informer().GetIndexer()) +} diff --git a/pkg/listers/zora/v1alpha2/expansion_generated.go b/pkg/listers/zora/v1alpha2/expansion_generated.go index 6ba69c9e..8c1e85ac 100644 --- a/pkg/listers/zora/v1alpha2/expansion_generated.go +++ b/pkg/listers/zora/v1alpha2/expansion_generated.go @@ -9,3 +9,11 @@ type CustomCheckListerExpansion interface{} // CustomCheckNamespaceListerExpansion allows custom methods to be added to // CustomCheckNamespaceLister. type CustomCheckNamespaceListerExpansion interface{} + +// VulnerabilityReportListerExpansion allows custom methods to be added to +// VulnerabilityReportLister. +type VulnerabilityReportListerExpansion interface{} + +// VulnerabilityReportNamespaceListerExpansion allows custom methods to be added to +// VulnerabilityReportNamespaceLister. +type VulnerabilityReportNamespaceListerExpansion interface{} diff --git a/pkg/listers/zora/v1alpha2/vulnerabilityreport.go b/pkg/listers/zora/v1alpha2/vulnerabilityreport.go new file mode 100644 index 00000000..e491a1d5 --- /dev/null +++ b/pkg/listers/zora/v1alpha2/vulnerabilityreport.go @@ -0,0 +1,83 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/undistro/zora/api/zora/v1alpha2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// VulnerabilityReportLister helps list VulnerabilityReports. +// All objects returned here must be treated as read-only. +type VulnerabilityReportLister interface { + // List lists all VulnerabilityReports in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VulnerabilityReport, err error) + // VulnerabilityReports returns an object that can list and get VulnerabilityReports. + VulnerabilityReports(namespace string) VulnerabilityReportNamespaceLister + VulnerabilityReportListerExpansion +} + +// vulnerabilityReportLister implements the VulnerabilityReportLister interface. +type vulnerabilityReportLister struct { + indexer cache.Indexer +} + +// NewVulnerabilityReportLister returns a new VulnerabilityReportLister. +func NewVulnerabilityReportLister(indexer cache.Indexer) VulnerabilityReportLister { + return &vulnerabilityReportLister{indexer: indexer} +} + +// List lists all VulnerabilityReports in the indexer. +func (s *vulnerabilityReportLister) List(selector labels.Selector) (ret []*v1alpha2.VulnerabilityReport, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VulnerabilityReport)) + }) + return ret, err +} + +// VulnerabilityReports returns an object that can list and get VulnerabilityReports. +func (s *vulnerabilityReportLister) VulnerabilityReports(namespace string) VulnerabilityReportNamespaceLister { + return vulnerabilityReportNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// VulnerabilityReportNamespaceLister helps list and get VulnerabilityReports. +// All objects returned here must be treated as read-only. +type VulnerabilityReportNamespaceLister interface { + // List lists all VulnerabilityReports in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.VulnerabilityReport, err error) + // Get retrieves the VulnerabilityReport from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha2.VulnerabilityReport, error) + VulnerabilityReportNamespaceListerExpansion +} + +// vulnerabilityReportNamespaceLister implements the VulnerabilityReportNamespaceLister +// interface. +type vulnerabilityReportNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all VulnerabilityReports in the indexer for a given namespace. +func (s vulnerabilityReportNamespaceLister) List(selector labels.Selector) (ret []*v1alpha2.VulnerabilityReport, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.VulnerabilityReport)) + }) + return ret, err +} + +// Get retrieves the VulnerabilityReport from the indexer for a given namespace and name. +func (s vulnerabilityReportNamespaceLister) Get(name string) (*v1alpha2.VulnerabilityReport, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("vulnerabilityreport"), name) + } + return obj.(*v1alpha2.VulnerabilityReport), nil +} diff --git a/pkg/worker/report/trivy/parse.go b/pkg/worker/report/trivy/parse.go index 89a140f7..34021c80 100644 --- a/pkg/worker/report/trivy/parse.go +++ b/pkg/worker/report/trivy/parse.go @@ -24,22 +24,24 @@ import ( "strings" "time" + "github.com/aquasecurity/trivy/pkg/fanal/types" trivyreport "github.com/aquasecurity/trivy/pkg/k8s/report" trivytypes "github.com/aquasecurity/trivy/pkg/types" "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" ) -func Parse(ctx context.Context, results io.Reader) ([]v1alpha1.VulnerabilityReportSpec, error) { +func Parse(ctx context.Context, results io.Reader) ([]v1alpha2.VulnerabilityReportSpec, error) { log := logr.FromContextOrDiscard(ctx) report := &trivyreport.Report{} if err := json.NewDecoder(results).Decode(report); err != nil { return nil, err } ignoreDescriptions, _ := strconv.ParseBool(os.Getenv("TRIVY_IGNORE_VULN_DESCRIPTIONS")) - vulnsByImage := make(map[string]*v1alpha1.VulnerabilityReportSpec) + vulnsByImage := make(map[string]*v1alpha2.VulnerabilityReportSpec) // map to control which image + class was parsed parsed := make(map[string]bool) @@ -74,26 +76,39 @@ func Parse(ctx context.Context, results io.Reader) ([]v1alpha1.VulnerabilityRepo } parsed[k] = true - for _, vuln := range result.Vulnerabilities { - spec.Vulnerabilities = append(spec.Vulnerabilities, newVulnerability(vuln, ignoreDescriptions, string(result.Type))) + vulnsByID := make(map[string]*v1alpha2.Vulnerability) + for _, v := range result.Vulnerabilities { + // we are assuming that a repeated CVE in this list refers to another package + if _, ok := vulnsByID[v.VulnerabilityID]; !ok { + vulnsByID[v.VulnerabilityID] = newVulnerability(v, ignoreDescriptions) + } + pkg := newPackage(v, result.Type) + vulnsByID[v.VulnerabilityID].Packages = append(vulnsByID[v.VulnerabilityID].Packages, pkg) + } + + // append all vulnerabilities to spec + for _, vuln := range vulnsByID { + spec.Vulnerabilities = append(spec.Vulnerabilities, *vuln) } } } - specs := make([]v1alpha1.VulnerabilityReportSpec, 0, len(vulnsByImage)) + specs := make([]v1alpha2.VulnerabilityReportSpec, 0, len(vulnsByImage)) for _, spec := range vulnsByImage { - summarize(spec) + spec.Summarize() specs = append(specs, *spec) } return specs, nil } -func newSpec(img string, resource trivyreport.Resource) *v1alpha1.VulnerabilityReportSpec { +func newSpec(img string, resource trivyreport.Resource) *v1alpha2.VulnerabilityReportSpec { meta := resource.Metadata - s := &v1alpha1.VulnerabilityReportSpec{ - Image: img, - Tags: meta.RepoTags, - Architecture: meta.ImageConfig.Architecture, - OS: meta.ImageConfig.OS, + s := &v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: img, + Tags: meta.RepoTags, + Architecture: meta.ImageConfig.Architecture, + OS: meta.ImageConfig.OS, + }, } if len(meta.RepoDigests) > 0 { s.Digest = meta.RepoDigests[0] @@ -107,26 +122,33 @@ func newSpec(img string, resource trivyreport.Resource) *v1alpha1.VulnerabilityR return s } -func newVulnerability(vuln trivytypes.DetectedVulnerability, ignoreDescription bool, t string) v1alpha1.Vulnerability { +func newVulnerability(vuln trivytypes.DetectedVulnerability, ignoreDescription bool) *v1alpha2.Vulnerability { description := "" if !ignoreDescription { description = vuln.Description } - return v1alpha1.Vulnerability{ - ID: vuln.VulnerabilityID, - Severity: vuln.Severity, - Title: vuln.Title, - Description: description, - Package: vuln.PkgName, - Version: vuln.InstalledVersion, - FixVersion: vuln.FixedVersion, - URL: vuln.PrimaryURL, - Status: vuln.Status.String(), - Score: getScore(vuln), - Type: t, - PublishedDate: parseTime(vuln.PublishedDate), - LastModifiedDate: parseTime(vuln.LastModifiedDate), + return &v1alpha2.Vulnerability{ + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: vuln.VulnerabilityID, + Severity: vuln.Severity, + Title: vuln.Title, + Description: description, + URL: vuln.PrimaryURL, + Score: getScore(vuln), + PublishedDate: parseTime(vuln.PublishedDate), + LastModifiedDate: parseTime(vuln.LastModifiedDate), + }, + } +} + +func newPackage(vuln trivytypes.DetectedVulnerability, t types.TargetType) v1alpha1.Package { + return v1alpha1.Package{ + Package: vuln.PkgName, + Status: vuln.Status.String(), + Version: vuln.InstalledVersion, + FixVersion: vuln.FixedVersion, + Type: string(t), } } @@ -164,7 +186,7 @@ func getImage(resource trivyreport.Resource) string { return "" } -func addResource(spec *v1alpha1.VulnerabilityReportSpec, kind, namespace, name string) { +func addResource(spec *v1alpha2.VulnerabilityReportSpec, kind, namespace, name string) { if spec.Resources == nil { spec.Resources = map[string][]string{} } @@ -182,23 +204,3 @@ func addResource(spec *v1alpha1.VulnerabilityReportSpec, kind, namespace, name s spec.Resources[kind] = append(spec.Resources[kind], id) spec.TotalResources++ } - -func summarize(spec *v1alpha1.VulnerabilityReportSpec) { - s := &v1alpha1.VulnerabilitySummary{} - for _, v := range spec.Vulnerabilities { - s.Total++ - switch v.Severity { - case "CRITICAL": - s.Critical++ - case "HIGH": - s.High++ - case "MEDIUM": - s.Medium++ - case "LOW": - s.Low++ - default: - s.Unknown++ - } - } - spec.Summary = *s -} diff --git a/pkg/worker/report/trivy/parse_test.go b/pkg/worker/report/trivy/parse_test.go index e075adb7..5028752b 100644 --- a/pkg/worker/report/trivy/parse_test.go +++ b/pkg/worker/report/trivy/parse_test.go @@ -27,233 +27,306 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" ) func TestParse(t *testing.T) { tests := []struct { name string testfile string - want []v1alpha1.VulnerabilityReportSpec + want []v1alpha2.VulnerabilityReportSpec wantErr bool }{ { name: "ok", testfile: "testdata/report.json", wantErr: false, - want: []v1alpha1.VulnerabilityReportSpec{ + want: []v1alpha2.VulnerabilityReportSpec{ { - Image: "registry.k8s.io/kube-apiserver:v1.27.3", - Tags: []string{"registry.k8s.io/kube-apiserver:v1.27.3"}, - Digest: "registry.k8s.io/kube-apiserver@sha256:fd03335dd2e7163e5e36e933a0c735d7fec6f42b33ddafad0bc54f333e4a23c0", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, - TotalResources: 1, - Resources: map[string][]string{"Pod": {"kube-system/kube-apiserver-kind-control-plane"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: "registry.k8s.io/kube-apiserver:v1.27.3", + Tags: []string{"registry.k8s.io/kube-apiserver:v1.27.3"}, + Digest: "registry.k8s.io/kube-apiserver@sha256:fd03335dd2e7163e5e36e933a0c735d7fec6f42b33ddafad0bc54f333e4a23c0", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, + TotalResources: 1, + Resources: map[string][]string{"Pod": {"kube-system/kube-apiserver-kind-control-plane"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 1, High: 1}, + }, + TotalPackages: 1, + TotalUniquePackages: 1, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2022-41723", - Severity: "HIGH", - Title: "avoid quadratic complexity in HPACK decoding", - Description: "A maliciously crafted HTTP/2 stream could cause excessive CPU consumption in the HPACK decoder, sufficient to cause a denial of service from a small number of small requests.", - Package: "golang.org/x/net", - Version: "v0.0.0-20220722155237-a158d28d115b", - FixVersion: "0.7.0", - URL: "https://avd.aquasec.com/nvd/cve-2022-41723", - Status: "fixed", - Type: "gobinary", - Score: "7.5", - PublishedDate: newTime("2023-02-28T18:15:00Z"), - LastModifiedDate: newTime("2023-05-16T10:50:00Z"), + Packages: []v1alpha1.Package{{ + Package: "golang.org/x/net", + Version: "v0.0.0-20220722155237-a158d28d115b", + FixVersion: "0.7.0", + Status: "fixed", + Type: "gobinary", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2022-41723", + Severity: "HIGH", + Title: "avoid quadratic complexity in HPACK decoding", + Description: "A maliciously crafted HTTP/2 stream could cause excessive CPU consumption in the HPACK decoder, sufficient to cause a denial of service from a small number of small requests.", + URL: "https://avd.aquasec.com/nvd/cve-2022-41723", + Score: "7.5", + PublishedDate: newTime("2023-02-28T18:15:00Z"), + LastModifiedDate: newTime("2023-05-16T10:50:00Z"), + }, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 1, High: 1}, }, { - Image: "quay.io/kiwigrid/k8s-sidecar:1.22.0", - Tags: []string{"quay.io/kiwigrid/k8s-sidecar:1.22.0"}, - Digest: "quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "alpine", Version: "3.16.3"}, - TotalResources: 2, - Resources: map[string][]string{"Deployment": {"apps/app1", "apps/app2"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: "quay.io/kiwigrid/k8s-sidecar:1.22.0", + Tags: []string{"quay.io/kiwigrid/k8s-sidecar:1.22.0"}, + Digest: "quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "alpine", Version: "3.16.3"}, + TotalResources: 2, + Resources: map[string][]string{"Deployment": {"apps/app1", "apps/app2"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 3, Critical: 1, High: 2}, + }, + TotalPackages: 4, + TotalUniquePackages: 3, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2022-4450", - Severity: "HIGH", - Title: "double free after calling PEM_read_bio_ex", - Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", - Package: "libssl1.1", - Version: "1.1.1s-r0", - FixVersion: "1.1.1t-r0", - URL: "https://avd.aquasec.com/nvd/cve-2022-4450", - Status: "fixed", - Type: "alpine", - Score: "7.5", - PublishedDate: newTime("2023-02-08T20:15:00Z"), - LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + Packages: []v1alpha1.Package{ + { + Package: "libssl1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + { + Package: "libcrypto1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + }, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2022-4450", + Severity: "HIGH", + Title: "double free after calling PEM_read_bio_ex", + Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", + URL: "https://avd.aquasec.com/nvd/cve-2022-4450", + Score: "7.5", + PublishedDate: newTime("2023-02-08T20:15:00Z"), + LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + }, }, { - ID: "CVE-2022-4450", - Severity: "HIGH", - Title: "double free after calling PEM_read_bio_ex", - Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", - Package: "libcrypto1.1", - Version: "1.1.1s-r0", - FixVersion: "1.1.1t-r0", - URL: "https://avd.aquasec.com/nvd/cve-2022-4450", - Status: "fixed", - Type: "alpine", - Score: "7.5", - PublishedDate: newTime("2023-02-08T20:15:00Z"), - LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + Packages: []v1alpha1.Package{{ + Package: "certifi", + Version: "2022.12.7", + FixVersion: "2023.7.22", + Status: "fixed", + Type: "python-pkg", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-37920", + Severity: "CRITICAL", + Title: "Removal of e-Tugra root certificate", + Description: "Certifi is a curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. Certifi prior to version 2023.07.22 recognizes \"e-Tugra\" root certificates. e-Tugra's root certificates were subject to an investigation prompted by reporting of security issues in their systems. Certifi 2023.07.22 removes root certificates from \"e-Tugra\" from the root store.", + URL: "https://avd.aquasec.com/nvd/cve-2023-37920", + Score: "9.8", + PublishedDate: newTime("2023-07-25T21:15:00Z"), + LastModifiedDate: newTime("2023-08-12T06:16:00Z"), + }, }, { - ID: "CVE-2023-37920", - Severity: "CRITICAL", - Title: "Removal of e-Tugra root certificate", - Description: "Certifi is a curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. Certifi prior to version 2023.07.22 recognizes \"e-Tugra\" root certificates. e-Tugra's root certificates were subject to an investigation prompted by reporting of security issues in their systems. Certifi 2023.07.22 removes root certificates from \"e-Tugra\" from the root store.", - Package: "certifi", - Version: "2022.12.7", - FixVersion: "2023.7.22", - URL: "https://avd.aquasec.com/nvd/cve-2023-37920", - Status: "fixed", - Type: "python-pkg", - Score: "9.8", - PublishedDate: newTime("2023-07-25T21:15:00Z"), - LastModifiedDate: newTime("2023-08-12T06:16:00Z"), + Packages: []v1alpha1.Package{ + { + Package: "libssl1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + }, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-0286", + Severity: "HIGH", + Title: "openssl: X.400 address type confusion in X.509 GeneralName", + Description: "There is a type confusion vulnerability relating to X.400 address processing\ninside an X.509 GeneralName. X.400 addresses were parsed as an ASN1_STRING but\nthe public structure definition for GENERAL_NAME incorrectly specified the type\nof the x400Address field as ASN1_TYPE. This field is subsequently interpreted by\nthe OpenSSL function GENERAL_NAME_cmp as an ASN1_TYPE rather than an\nASN1_STRING.\n\nWhen CRL checking is enabled (i.e. the application sets the\nX509_V_FLAG_CRL_CHECK flag), this vulnerability may allow an attacker to pass\narbitrary pointers to a memcmp call, enabling them to read memory contents or\nenact a denial of service. In most cases, the attack requires the attacker to\nprovide both the certificate chain and CRL, neither of which need to have a\nvalid signature. If the attacker only controls one of these inputs, the other\ninput must already contain an X.400 address as a CRL distribution point, which\nis uncommon. As such, this vulnerability is most likely to only affect\napplications which have implemented their own functionality for retrieving CRLs\nover a network.\n\n", + URL: "https://avd.aquasec.com/nvd/cve-2023-0286", + Score: "7.4", + PublishedDate: newTime("2023-02-08T20:15:24.267Z"), + LastModifiedDate: newTime("2024-02-04T09:15:09.113Z"), + }, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 3, Critical: 1, High: 2}, }, { - Image: "docker.io/istio/examples-bookinfo-ratings-v1:1.17.0", - Tags: []string{"istio/examples-bookinfo-ratings-v1:1.17.0"}, - Digest: "istio/examples-bookinfo-ratings-v1@sha256:b6a6b88d35785c19f6dcb6acf055aa585511f2126bb0b5802f3107b7d37ead0b", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "9.12"}, - TotalResources: 1, - Resources: map[string][]string{"Deployment": {"apps/app1"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: "docker.io/istio/examples-bookinfo-ratings-v1:1.17.0", + Tags: []string{"istio/examples-bookinfo-ratings-v1:1.17.0"}, + Digest: "istio/examples-bookinfo-ratings-v1@sha256:b6a6b88d35785c19f6dcb6acf055aa585511f2126bb0b5802f3107b7d37ead0b", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "9.12"}, + TotalResources: 1, + Resources: map[string][]string{"Deployment": {"apps/app1"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 3, High: 1, Medium: 1, Unknown: 1}, + }, + TotalPackages: 3, + TotalUniquePackages: 3, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "DLA-3051-1", - Severity: "UNKNOWN", - Title: "tzdata - new timezone database", - Description: "", - Package: "tzdata", - Version: "2019c-0+deb9u1", - FixVersion: "2021a-0+deb9u4", - URL: "", - Status: "fixed", - Type: "debian", - PublishedDate: nil, - LastModifiedDate: nil, + Packages: []v1alpha1.Package{{ + Package: "tzdata", + Version: "2019c-0+deb9u1", + FixVersion: "2021a-0+deb9u4", + Status: "fixed", + Type: "debian", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "DLA-3051-1", + Severity: "UNKNOWN", + Title: "tzdata - new timezone database", + Description: "", + URL: "", + Score: "", + PublishedDate: nil, + LastModifiedDate: nil, + }, }, { - ID: "CVE-2016-2779", - Severity: "HIGH", - Title: "util-linux: runuser tty hijack via TIOCSTI ioctl", - Description: "runuser in util-linux allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", - Package: "bsdutils", - Version: "1:2.29.2-1+deb9u1", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2016-2779", - Status: "affected", - Type: "debian", - Score: "7.8", - PublishedDate: newTime("2017-02-07T15:59:00Z"), - LastModifiedDate: newTime("2019-01-04T14:14:00Z"), + Packages: []v1alpha1.Package{{ + Package: "bsdutils", + Version: "1:2.29.2-1+deb9u1", + FixVersion: "", + Status: "affected", + Type: "debian", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2016-2779", + Severity: "HIGH", + Title: "util-linux: runuser tty hijack via TIOCSTI ioctl", + Description: "runuser in util-linux allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + URL: "https://avd.aquasec.com/nvd/cve-2016-2779", + Score: "7.8", + PublishedDate: newTime("2017-02-07T15:59:00Z"), + LastModifiedDate: newTime("2019-01-04T14:14:00Z"), + }, }, { - ID: "GHSA-jmqm-f2gx-4fjv", - Severity: "MEDIUM", - Title: "Sensitive information exposure through logs in npm-registry-fetch", - Description: "Affected versions of `npm-registry-fetch` are vulnerable to an information exposure vulnerability through log files. The cli supports URLs like `\u003cprotocol\u003e://[\u003cuser\u003e[:\u003cpassword\u003e]@]\u003chostname\u003e[:\u003cport\u003e][:][/]\u003cpath\u003e`. The password value is not redacted and is printed to stdout and also to any generated log files.", - Package: "npm-registry-fetch", - Version: "4.0.4", - FixVersion: "8.1.1, 4.0.5", - URL: "https://github.com/advisories/GHSA-jmqm-f2gx-4fjv", - Status: "fixed", - Type: "node-pkg", - Score: "5.3", - PublishedDate: nil, - LastModifiedDate: nil, + Packages: []v1alpha1.Package{{ + Package: "npm-registry-fetch", + Version: "4.0.4", + FixVersion: "8.1.1, 4.0.5", + Status: "fixed", + Type: "node-pkg", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "GHSA-jmqm-f2gx-4fjv", + Severity: "MEDIUM", + Title: "Sensitive information exposure through logs in npm-registry-fetch", + Description: "Affected versions of `npm-registry-fetch` are vulnerable to an information exposure vulnerability through log files. The cli supports URLs like `\u003cprotocol\u003e://[\u003cuser\u003e[:\u003cpassword\u003e]@]\u003chostname\u003e[:\u003cport\u003e][:][/]\u003cpath\u003e`. The password value is not redacted and is printed to stdout and also to any generated log files.", + URL: "https://github.com/advisories/GHSA-jmqm-f2gx-4fjv", + Score: "5.3", + PublishedDate: nil, + LastModifiedDate: nil, + }, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 3, High: 1, Medium: 1, Unknown: 1}, }, { - Image: "docker.io/istio/examples-bookinfo-details-v1:1.17.0", - Tags: []string{"istio/examples-bookinfo-details-v1:1.17.0"}, - Digest: "istio/examples-bookinfo-details-v1@sha256:2b081e3c86dd8105040ea1f2adcc94cb473f41249dc9c91ebc1c2885ddd56c13", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "10.5"}, - TotalResources: 1, - Resources: map[string][]string{"Deployment": {"apps/app2"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: "docker.io/istio/examples-bookinfo-details-v1:1.17.0", + Tags: []string{"istio/examples-bookinfo-details-v1:1.17.0"}, + Digest: "istio/examples-bookinfo-details-v1@sha256:2b081e3c86dd8105040ea1f2adcc94cb473f41249dc9c91ebc1c2885ddd56c13", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "10.5"}, + TotalResources: 1, + Resources: map[string][]string{"Deployment": {"apps/app2"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 2, High: 1, Low: 1}, + }, + TotalPackages: 2, + TotalUniquePackages: 2, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2016-2781", - Severity: "LOW", - Title: "coreutils: Non-privileged session can escape to the parent session in chroot", - Description: "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", - Package: "coreutils", - Version: "8.30-3", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2016-2781", - Status: "will_not_fix", - Type: "debian", - Score: "6.5", - PublishedDate: newTime("2017-02-07T15:59:00Z"), - LastModifiedDate: newTime("2021-02-25T17:15:00Z"), + Packages: []v1alpha1.Package{{ + Package: "coreutils", + Version: "8.30-3", + FixVersion: "", + Status: "will_not_fix", + Type: "debian", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2016-2781", + Severity: "LOW", + Title: "coreutils: Non-privileged session can escape to the parent session in chroot", + Description: "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + URL: "https://avd.aquasec.com/nvd/cve-2016-2781", + Score: "6.5", + PublishedDate: newTime("2017-02-07T15:59:00Z"), + LastModifiedDate: newTime("2021-02-25T17:15:00Z"), + }, }, { - ID: "CVE-2023-28755", - Severity: "HIGH", - Title: "ReDoS vulnerability in URI", - Description: "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", - Package: "uri", - Version: "0.10.0", - FixVersion: "~\u003e 0.10.0.1, ~\u003e 0.10.2, ~\u003e 0.11.1, \u003e= 0.12.1", - URL: "https://avd.aquasec.com/nvd/cve-2023-28755", - Status: "fixed", - Type: "gemspec", - Score: "5.3", - PublishedDate: newTime("2023-03-31T04:15:00Z"), - LastModifiedDate: newTime("2023-05-30T17:17:00Z"), + Packages: []v1alpha1.Package{{ + Package: "uri", + Version: "0.10.0", + FixVersion: "~\u003e 0.10.0.1, ~\u003e 0.10.2, ~\u003e 0.11.1, \u003e= 0.12.1", + Status: "fixed", + Type: "gemspec", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-28755", + Severity: "HIGH", + Title: "ReDoS vulnerability in URI", + Description: "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", + URL: "https://avd.aquasec.com/nvd/cve-2023-28755", + Score: "5.3", + PublishedDate: newTime("2023-03-31T04:15:00Z"), + LastModifiedDate: newTime("2023-05-30T17:17:00Z"), + }, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 2, High: 1, Low: 1}, }, { - Image: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", - Tags: []string{"nginx:1.25.0"}, - Digest: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, - TotalResources: 1, - Resources: map[string][]string{"Deployment": {"default/nginx"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Image: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", + Tags: []string{"nginx:1.25.0"}, + Digest: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, + TotalResources: 1, + Resources: map[string][]string{"Deployment": {"default/nginx"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 1, Medium: 1}, + }, + TotalPackages: 1, + TotalUniquePackages: 1, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2023-3446", - Severity: "MEDIUM", - Title: "Excessive time spent checking DH keys and parameters", - Description: "Issue summary: Checking excessively long DH keys or parameters may be very slow.\n\nImpact summary: Applications that use the functions DH_check(), DH_check_ex()\nor EVP_PKEY_param_check() to check a DH key or DH parameters may experience long\ndelays. Where the key or parameters that are being checked have been obtained\nfrom an untrusted source this may lead to a Denial of Service.\n\nThe function DH_check() performs various checks on DH parameters. One of those\nchecks confirms that the modulus ('p' parameter) is not too large. Trying to use\na very large modulus is slow and OpenSSL will not normally use a modulus which\nis over 10,000 bits in length.\n\nHowever the DH_check() function checks numerous aspects of the key or parameters\nthat have been supplied. Some of those checks use the supplied modulus value\neven if it has already been found to be too large.\n\nAn application that calls DH_check() and supplies a key or parameters obtained\nfrom an untrusted source could be vulernable to a Denial of Service attack.\n\nThe function DH_check() is itself called by a number of other OpenSSL functions.\nAn application calling any of those other functions may similarly be affected.\nThe other functions affected by this are DH_check_ex() and\nEVP_PKEY_param_check().\n\nAlso vulnerable are the OpenSSL dhparam and pkeyparam command line applications\nwhen using the '-check' option.\n\nThe OpenSSL SSL/TLS implementation is not affected by this issue.\nThe OpenSSL 3.0 and 3.1 FIPS providers are not affected by this issue.", - Package: "openssl", - Version: "1.1.1n-0+deb11u4", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2023-3446", - Status: "fix_deferred", - Type: "debian", - Score: "5.3", - PublishedDate: newTime("2023-07-19T12:15:00Z"), - LastModifiedDate: newTime("2023-08-16T08:15:00Z"), + Packages: []v1alpha1.Package{{ + Package: "openssl", + Version: "1.1.1n-0+deb11u4", + FixVersion: "", + Status: "fix_deferred", + Type: "debian", + }}, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-3446", + Severity: "MEDIUM", + Title: "Excessive time spent checking DH keys and parameters", + Description: "Issue summary: Checking excessively long DH keys or parameters may be very slow.\n\nImpact summary: Applications that use the functions DH_check(), DH_check_ex()\nor EVP_PKEY_param_check() to check a DH key or DH parameters may experience long\ndelays. Where the key or parameters that are being checked have been obtained\nfrom an untrusted source this may lead to a Denial of Service.\n\nThe function DH_check() performs various checks on DH parameters. One of those\nchecks confirms that the modulus ('p' parameter) is not too large. Trying to use\na very large modulus is slow and OpenSSL will not normally use a modulus which\nis over 10,000 bits in length.\n\nHowever the DH_check() function checks numerous aspects of the key or parameters\nthat have been supplied. Some of those checks use the supplied modulus value\neven if it has already been found to be too large.\n\nAn application that calls DH_check() and supplies a key or parameters obtained\nfrom an untrusted source could be vulernable to a Denial of Service attack.\n\nThe function DH_check() is itself called by a number of other OpenSSL functions.\nAn application calling any of those other functions may similarly be affected.\nThe other functions affected by this are DH_check_ex() and\nEVP_PKEY_param_check().\n\nAlso vulnerable are the OpenSSL dhparam and pkeyparam command line applications\nwhen using the '-check' option.\n\nThe OpenSSL SSL/TLS implementation is not affected by this issue.\nThe OpenSSL 3.0 and 3.1 FIPS providers are not affected by this issue.", + URL: "https://avd.aquasec.com/nvd/cve-2023-3446", + Score: "5.3", + PublishedDate: newTime("2023-07-19T12:15:00Z"), + LastModifiedDate: newTime("2023-08-16T08:15:00Z"), + }, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 1, Medium: 1}, }, }, }, @@ -278,13 +351,18 @@ func TestParse(t *testing.T) { } } -func sortVulns(specs []v1alpha1.VulnerabilityReportSpec) { +func sortVulns(specs []v1alpha2.VulnerabilityReportSpec) { sort.Slice(specs, func(i, j int) bool { return strings.Compare(specs[i].Image, specs[j].Image) == -1 }) for _, s := range specs { - for _, v := range s.Resources { - sort.Strings(v) + for _, r := range s.Resources { + sort.Strings(r) + } + for _, v := range s.Vulnerabilities { + sort.Slice(v.Packages, func(i, j int) bool { + return strings.Compare(v.Packages[i].String(), v.Packages[j].String()) == -1 + }) } sort.Slice(s.Vulnerabilities, func(i, j int) bool { return strings.Compare(s.Vulnerabilities[i].ID, s.Vulnerabilities[j].ID) == -1 diff --git a/pkg/worker/report/trivy/testdata/report.json b/pkg/worker/report/trivy/testdata/report.json index 303338ac..dec94ff4 100644 --- a/pkg/worker/report/trivy/testdata/report.json +++ b/pkg/worker/report/trivy/testdata/report.json @@ -964,6 +964,119 @@ ], "PublishedDate": "2023-02-08T20:15:00Z", "LastModifiedDate": "2023-07-19T00:57:00Z" + }, + { + "VulnerabilityID": "CVE-2023-0286", + "PkgID": "libssl1.1@1.1.1s-r0", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1s-r0?arch=x86_64\u0026distro=3.16.3" + }, + "InstalledVersion": "1.1.1s-r0", + "FixedVersion": "1.1.1t-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ca7dd9ec2225f2385955c43b2379305acd51543c28cf1d4e94522b3d94cce3ce", + "DiffID": "sha256:e5e13b0c77cbb769548077189c3da2f0a764ceca06af49d8d558e759f5c232bd" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-0286", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Title": "openssl: X.400 address type confusion in X.509 GeneralName", + "Description": "There is a type confusion vulnerability relating to X.400 address processing\ninside an X.509 GeneralName. X.400 addresses were parsed as an ASN1_STRING but\nthe public structure definition for GENERAL_NAME incorrectly specified the type\nof the x400Address field as ASN1_TYPE. This field is subsequently interpreted by\nthe OpenSSL function GENERAL_NAME_cmp as an ASN1_TYPE rather than an\nASN1_STRING.\n\nWhen CRL checking is enabled (i.e. the application sets the\nX509_V_FLAG_CRL_CHECK flag), this vulnerability may allow an attacker to pass\narbitrary pointers to a memcmp call, enabling them to read memory contents or\nenact a denial of service. In most cases, the attack requires the attacker to\nprovide both the certificate chain and CRL, neither of which need to have a\nvalid signature. If the attacker only controls one of these inputs, the other\ninput must already contain an X.400 address as a CRL distribution point, which\nis uncommon. As such, this vulnerability is most likely to only affect\napplications which have implemented their own functionality for retrieving CRLs\nover a network.\n\n", + "Severity": "HIGH", + "CweIDs": [ + "CWE-843" + ], + "VendorSeverity": { + "alma": 3, + "amazon": 3, + "cbl-mariner": 3, + "ghsa": 3, + "nvd": 3, + "oracle-oval": 3, + "photon": 3, + "redhat": 3, + "rocky": 3, + "ubuntu": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + }, + "nvd": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + } + }, + "References": [ + "https://access.redhat.com/errata/RHSA-2023:2165", + "https://access.redhat.com/security/cve/CVE-2023-0286", + "https://access.redhat.com/security/cve/cve-2023-0286", + "https://bugzilla.redhat.com/1960321", + "https://bugzilla.redhat.com/2164440", + "https://bugzilla.redhat.com/2164487", + "https://bugzilla.redhat.com/2164492", + "https://bugzilla.redhat.com/2164494", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144000", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144003", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144006", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144008", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144010", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144012", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144015", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144017", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144019", + "https://bugzilla.redhat.com/show_bug.cgi?id=2145170", + "https://bugzilla.redhat.com/show_bug.cgi?id=2158412", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164440", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164487", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164488", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164492", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164494", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164497", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164499", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164500", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4203", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4304", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4450", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0215", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0216", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0217", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0286", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0401", + "https://errata.almalinux.org/9/ALSA-2023-2165.html", + "https://errata.rockylinux.org/RLSA-2023:0946", + "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.6.2-relnotes.txt", + "https://ftp.openbsd.org/pub/OpenBSD/patches/7.2/common/018_x509.patch.sig", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=2c6c9d439b484e1ba9830d8454a34fa4f80fdfe9", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=2f7530077e0ef79d98718138716bc51ca0cad658", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=fd2af07dc083a350c959147097003a14a5e8ac4d", + "https://github.com/pyca/cryptography", + "https://github.com/pyca/cryptography/security/advisories/GHSA-x4qr-2fvf-3mr5", + "https://linux.oracle.com/cve/CVE-2023-0286.html", + "https://linux.oracle.com/errata/ELSA-2023-32791.html", + "https://nvd.nist.gov/vuln/detail/CVE-2023-0286", + "https://rustsec.org/advisories/RUSTSEC-2023-0006.html", + "https://security.gentoo.org/glsa/202402-08", + "https://ubuntu.com/security/notices/USN-5844-1", + "https://ubuntu.com/security/notices/USN-5845-1", + "https://ubuntu.com/security/notices/USN-5845-2", + "https://ubuntu.com/security/notices/USN-6564-1", + "https://www.cve.org/CVERecord?id=CVE-2023-0286", + "https://www.openssl.org/news/secadv/20230207.txt" + ], + "PublishedDate": "2023-02-08T20:15:24.267Z", + "LastModifiedDate": "2024-02-04T09:15:09.113Z" } ] }, @@ -1389,6 +1502,119 @@ ], "PublishedDate": "2023-02-08T20:15:00Z", "LastModifiedDate": "2023-07-19T00:57:00Z" + }, + { + "VulnerabilityID": "CVE-2023-0286", + "PkgID": "libssl1.1@1.1.1s-r0", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1s-r0?arch=x86_64\u0026distro=3.16.3" + }, + "InstalledVersion": "1.1.1s-r0", + "FixedVersion": "1.1.1t-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ca7dd9ec2225f2385955c43b2379305acd51543c28cf1d4e94522b3d94cce3ce", + "DiffID": "sha256:e5e13b0c77cbb769548077189c3da2f0a764ceca06af49d8d558e759f5c232bd" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-0286", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Title": "openssl: X.400 address type confusion in X.509 GeneralName", + "Description": "There is a type confusion vulnerability relating to X.400 address processing\ninside an X.509 GeneralName. X.400 addresses were parsed as an ASN1_STRING but\nthe public structure definition for GENERAL_NAME incorrectly specified the type\nof the x400Address field as ASN1_TYPE. This field is subsequently interpreted by\nthe OpenSSL function GENERAL_NAME_cmp as an ASN1_TYPE rather than an\nASN1_STRING.\n\nWhen CRL checking is enabled (i.e. the application sets the\nX509_V_FLAG_CRL_CHECK flag), this vulnerability may allow an attacker to pass\narbitrary pointers to a memcmp call, enabling them to read memory contents or\nenact a denial of service. In most cases, the attack requires the attacker to\nprovide both the certificate chain and CRL, neither of which need to have a\nvalid signature. If the attacker only controls one of these inputs, the other\ninput must already contain an X.400 address as a CRL distribution point, which\nis uncommon. As such, this vulnerability is most likely to only affect\napplications which have implemented their own functionality for retrieving CRLs\nover a network.\n\n", + "Severity": "HIGH", + "CweIDs": [ + "CWE-843" + ], + "VendorSeverity": { + "alma": 3, + "amazon": 3, + "cbl-mariner": 3, + "ghsa": 3, + "nvd": 3, + "oracle-oval": 3, + "photon": 3, + "redhat": 3, + "rocky": 3, + "ubuntu": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + }, + "nvd": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + } + }, + "References": [ + "https://access.redhat.com/errata/RHSA-2023:2165", + "https://access.redhat.com/security/cve/CVE-2023-0286", + "https://access.redhat.com/security/cve/cve-2023-0286", + "https://bugzilla.redhat.com/1960321", + "https://bugzilla.redhat.com/2164440", + "https://bugzilla.redhat.com/2164487", + "https://bugzilla.redhat.com/2164492", + "https://bugzilla.redhat.com/2164494", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144000", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144003", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144006", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144008", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144010", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144012", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144015", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144017", + "https://bugzilla.redhat.com/show_bug.cgi?id=2144019", + "https://bugzilla.redhat.com/show_bug.cgi?id=2145170", + "https://bugzilla.redhat.com/show_bug.cgi?id=2158412", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164440", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164487", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164488", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164492", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164494", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164497", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164499", + "https://bugzilla.redhat.com/show_bug.cgi?id=2164500", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4203", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4304", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-4450", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0215", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0216", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0217", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0286", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-0401", + "https://errata.almalinux.org/9/ALSA-2023-2165.html", + "https://errata.rockylinux.org/RLSA-2023:0946", + "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.6.2-relnotes.txt", + "https://ftp.openbsd.org/pub/OpenBSD/patches/7.2/common/018_x509.patch.sig", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=2c6c9d439b484e1ba9830d8454a34fa4f80fdfe9", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=2f7530077e0ef79d98718138716bc51ca0cad658", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=fd2af07dc083a350c959147097003a14a5e8ac4d", + "https://github.com/pyca/cryptography", + "https://github.com/pyca/cryptography/security/advisories/GHSA-x4qr-2fvf-3mr5", + "https://linux.oracle.com/cve/CVE-2023-0286.html", + "https://linux.oracle.com/errata/ELSA-2023-32791.html", + "https://nvd.nist.gov/vuln/detail/CVE-2023-0286", + "https://rustsec.org/advisories/RUSTSEC-2023-0006.html", + "https://security.gentoo.org/glsa/202402-08", + "https://ubuntu.com/security/notices/USN-5844-1", + "https://ubuntu.com/security/notices/USN-5845-1", + "https://ubuntu.com/security/notices/USN-5845-2", + "https://ubuntu.com/security/notices/USN-6564-1", + "https://www.cve.org/CVERecord?id=CVE-2023-0286", + "https://www.openssl.org/news/secadv/20230207.txt" + ], + "PublishedDate": "2023-02-08T20:15:24.267Z", + "LastModifiedDate": "2024-02-04T09:15:09.113Z" } ] }, diff --git a/pkg/worker/vuln.go b/pkg/worker/vuln.go index 0e94d01f..c698137a 100644 --- a/pkg/worker/vuln.go +++ b/pkg/worker/vuln.go @@ -25,17 +25,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" zora "github.com/undistro/zora/pkg/clientset/versioned" "github.com/undistro/zora/pkg/worker/report/trivy" ) -var vulnPlugins = map[string]func(ctx context.Context, reader io.Reader) ([]v1alpha1.VulnerabilityReportSpec, error){ +var vulnPlugins = map[string]func(ctx context.Context, reader io.Reader) ([]v1alpha2.VulnerabilityReportSpec, error){ "trivy": trivy.Parse, } var vulnReportTypeMeta = metav1.TypeMeta{ Kind: "VulnerabilityReport", - APIVersion: v1alpha1.SchemeGroupVersion.String(), + APIVersion: v1alpha2.SchemeGroupVersion.String(), } var nonAlphanumericRegex = regexp.MustCompile(`\W+`) @@ -47,7 +48,7 @@ func handleVulnerability(ctx context.Context, cfg *config, results io.Reader, cl return err } for _, vuln := range vulns { - v, err := client.ZoraV1alpha1().VulnerabilityReports(cfg.Namespace).Create(ctx, &vuln, createOpts) + v, err := client.ZoraV1alpha2().VulnerabilityReports(cfg.Namespace).Create(ctx, &vuln, createOpts) if err != nil { return fmt.Errorf("failed to create VulnerabilityReport %q: %v", vuln.Name, err) } @@ -56,7 +57,7 @@ func handleVulnerability(ctx context.Context, cfg *config, results io.Reader, cl return nil } -func parseVulnResults(ctx context.Context, cfg *config, results io.Reader) ([]v1alpha1.VulnerabilityReport, error) { +func parseVulnResults(ctx context.Context, cfg *config, results io.Reader) ([]v1alpha2.VulnerabilityReport, error) { parseFunc, ok := vulnPlugins[cfg.PluginName] if !ok { return nil, fmt.Errorf("invalid plugin %q", cfg.PluginName) @@ -66,16 +67,16 @@ func parseVulnResults(ctx context.Context, cfg *config, results io.Reader) ([]v1 return nil, fmt.Errorf("failed to parse %q results: %v", cfg.PluginName, err) } owner := ownerReference(cfg) - vulns := make([]v1alpha1.VulnerabilityReport, 0, len(specs)) + vulns := make([]v1alpha2.VulnerabilityReport, 0, len(specs)) for _, spec := range specs { vulns = append(vulns, newVulnReport(cfg, spec, owner)) } return vulns, nil } -func newVulnReport(cfg *config, spec v1alpha1.VulnerabilityReportSpec, owner metav1.OwnerReference) v1alpha1.VulnerabilityReport { +func newVulnReport(cfg *config, spec v1alpha2.VulnerabilityReportSpec, owner metav1.OwnerReference) v1alpha2.VulnerabilityReport { spec.Cluster = cfg.ClusterName - return v1alpha1.VulnerabilityReport{ + return v1alpha2.VulnerabilityReport{ TypeMeta: vulnReportTypeMeta, ObjectMeta: metav1.ObjectMeta{ Name: vulnReportName(cfg, spec), @@ -92,7 +93,7 @@ func newVulnReport(cfg *config, spec v1alpha1.VulnerabilityReportSpec, owner met } } -func vulnReportName(cfg *config, spec v1alpha1.VulnerabilityReportSpec) string { +func vulnReportName(cfg *config, spec v1alpha2.VulnerabilityReportSpec) string { return fmt.Sprintf("%s-%s-%s", cfg.ClusterName, strings.ToLower(cleanString(spec.Image)), cfg.suffix) } diff --git a/pkg/worker/vuln_test.go b/pkg/worker/vuln_test.go index ccfc9a0c..d0c4a4b1 100644 --- a/pkg/worker/vuln_test.go +++ b/pkg/worker/vuln_test.go @@ -30,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/undistro/zora/api/zora/v1alpha1" + "github.com/undistro/zora/api/zora/v1alpha2" ) var labels = map[string]string{ @@ -56,7 +57,7 @@ func TestParseVulnResults(t *testing.T) { tests := []struct { name string args args - want []v1alpha1.VulnerabilityReport + want []v1alpha2.VulnerabilityReport wantErr bool }{ { @@ -89,7 +90,7 @@ func TestParseVulnResults(t *testing.T) { }, filename: "report/trivy/testdata/report.json", }, - want: []v1alpha1.VulnerabilityReport{ + want: []v1alpha2.VulnerabilityReport{ { TypeMeta: vulnReportTypeMeta, ObjectMeta: metav1.ObjectMeta{ @@ -98,32 +99,40 @@ func TestParseVulnResults(t *testing.T) { OwnerReferences: owners, Labels: labels, }, - Spec: v1alpha1.VulnerabilityReportSpec{ - Cluster: "cluster", - Image: "registry.k8s.io/kube-apiserver:v1.27.3", - Tags: []string{"registry.k8s.io/kube-apiserver:v1.27.3"}, - Digest: "registry.k8s.io/kube-apiserver@sha256:fd03335dd2e7163e5e36e933a0c735d7fec6f42b33ddafad0bc54f333e4a23c0", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, - Resources: map[string][]string{"Pod": {"kube-system/kube-apiserver-kind-control-plane"}}, - TotalResources: 1, - Summary: v1alpha1.VulnerabilitySummary{Total: 1, High: 1}, - Vulnerabilities: []v1alpha1.Vulnerability{ + Spec: v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "registry.k8s.io/kube-apiserver:v1.27.3", + Tags: []string{"registry.k8s.io/kube-apiserver:v1.27.3"}, + Digest: "registry.k8s.io/kube-apiserver@sha256:fd03335dd2e7163e5e36e933a0c735d7fec6f42b33ddafad0bc54f333e4a23c0", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, + Resources: map[string][]string{"Pod": {"kube-system/kube-apiserver-kind-control-plane"}}, + TotalResources: 1, + Summary: v1alpha1.VulnerabilitySummary{Total: 1, High: 1}, + }, + TotalPackages: 1, + TotalUniquePackages: 1, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2022-41723", - Severity: "HIGH", - Title: "avoid quadratic complexity in HPACK decoding", - Description: "A maliciously crafted HTTP/2 stream could cause excessive CPU consumption in the HPACK decoder, sufficient to cause a denial of service from a small number of small requests.", - Package: "golang.org/x/net", - Version: "v0.0.0-20220722155237-a158d28d115b", - FixVersion: "0.7.0", - URL: "https://avd.aquasec.com/nvd/cve-2022-41723", - Status: "fixed", - Type: "gobinary", - Score: "7.5", - PublishedDate: newTime("2023-02-28T18:15:00Z"), - LastModifiedDate: newTime("2023-05-16T10:50:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2022-41723", + Severity: "HIGH", + Title: "avoid quadratic complexity in HPACK decoding", + Description: "A maliciously crafted HTTP/2 stream could cause excessive CPU consumption in the HPACK decoder, sufficient to cause a denial of service from a small number of small requests.", + URL: "https://avd.aquasec.com/nvd/cve-2022-41723", + Score: "7.5", + PublishedDate: newTime("2023-02-28T18:15:00Z"), + LastModifiedDate: newTime("2023-05-16T10:50:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "golang.org/x/net", + Version: "v0.0.0-20220722155237-a158d28d115b", + FixVersion: "0.7.0", + Status: "fixed", + Type: "gobinary", + }}, }, }, }, @@ -136,62 +145,89 @@ func TestParseVulnResults(t *testing.T) { OwnerReferences: owners, Labels: labels, }, - Spec: v1alpha1.VulnerabilityReportSpec{ - Cluster: "cluster", - Image: "quay.io/kiwigrid/k8s-sidecar:1.22.0", - Tags: []string{"quay.io/kiwigrid/k8s-sidecar:1.22.0"}, - Digest: "quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "alpine", Version: "3.16.3"}, - Resources: map[string][]string{"Deployment": {"apps/app1", "apps/app2"}}, - TotalResources: 2, - Summary: v1alpha1.VulnerabilitySummary{Total: 3, Critical: 1, High: 2}, - Vulnerabilities: []v1alpha1.Vulnerability{ + Spec: v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "quay.io/kiwigrid/k8s-sidecar:1.22.0", + Tags: []string{"quay.io/kiwigrid/k8s-sidecar:1.22.0"}, + Digest: "quay.io/kiwigrid/k8s-sidecar@sha256:eaa478cdd0b8e1be7a4813bc1b01948b838e2feaa6d999e60c997dc823013824", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "alpine", Version: "3.16.3"}, + Resources: map[string][]string{"Deployment": {"apps/app1", "apps/app2"}}, + TotalResources: 2, + Summary: v1alpha1.VulnerabilitySummary{Total: 3, Critical: 1, High: 2}, + }, + TotalPackages: 4, + TotalUniquePackages: 3, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2022-4450", - Severity: "HIGH", - Title: "double free after calling PEM_read_bio_ex", - Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", - Package: "libssl1.1", - Version: "1.1.1s-r0", - FixVersion: "1.1.1t-r0", - URL: "https://avd.aquasec.com/nvd/cve-2022-4450", - Status: "fixed", - Type: "alpine", - Score: "7.5", - PublishedDate: newTime("2023-02-08T20:15:00Z"), - LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2022-4450", + Severity: "HIGH", + Title: "double free after calling PEM_read_bio_ex", + Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", + URL: "https://avd.aquasec.com/nvd/cve-2022-4450", + Score: "7.5", + PublishedDate: newTime("2023-02-08T20:15:00Z"), + LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + }, + Packages: []v1alpha1.Package{ + { + Package: "libssl1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + { + Package: "libcrypto1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + }, }, { - ID: "CVE-2022-4450", - Severity: "HIGH", - Title: "double free after calling PEM_read_bio_ex", - Description: "The function PEM_read_bio_ex() reads a PEM file from a BIO and parses and decodes the \"name\" (e.g. \"CERTIFICATE\"), any header data and the payload data. If the function succeeds then the \"name_out\", \"header\" and \"data\" arguments are populated with pointers to buffers containing the relevant decoded data. The caller is responsible for freeing those buffers. It is possible to construct a PEM file that results in 0 bytes of payload data. In this case PEM_read_bio_ex() will return a failure code but will populate the header argument with a pointer to a buffer that has already been freed. If the caller also frees this buffer then a double free will occur. This will most likely lead to a crash. This could be exploited by an attacker who has the ability to supply malicious PEM files for parsing to achieve a denial of service attack. The functions PEM_read_bio() and PEM_read() are simple wrappers around PEM_read_bio_ex() and therefore these functions are also directly affected. These functions are also called indirectly by a number of other OpenSSL functions including PEM_X509_INFO_read_bio_ex() and SSL_CTX_use_serverinfo_file() which are also vulnerable. Some OpenSSL internal uses of these functions are not vulnerable because the caller does not free the header argument if PEM_read_bio_ex() returns a failure code. These locations include the PEM_read_bio_TYPE() functions as well as the decoders introduced in OpenSSL 3.0. The OpenSSL asn1parse command line application is also impacted by this issue.", - Package: "libcrypto1.1", - Version: "1.1.1s-r0", - FixVersion: "1.1.1t-r0", - URL: "https://avd.aquasec.com/nvd/cve-2022-4450", - Status: "fixed", - Type: "alpine", - Score: "7.5", - PublishedDate: newTime("2023-02-08T20:15:00Z"), - LastModifiedDate: newTime("2023-07-19T00:57:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-37920", + Severity: "CRITICAL", + Title: "Removal of e-Tugra root certificate", + Description: "Certifi is a curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. Certifi prior to version 2023.07.22 recognizes \"e-Tugra\" root certificates. e-Tugra's root certificates were subject to an investigation prompted by reporting of security issues in their systems. Certifi 2023.07.22 removes root certificates from \"e-Tugra\" from the root store.", + URL: "https://avd.aquasec.com/nvd/cve-2023-37920", + Score: "9.8", + PublishedDate: newTime("2023-07-25T21:15:00Z"), + LastModifiedDate: newTime("2023-08-12T06:16:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "certifi", + Version: "2022.12.7", + FixVersion: "2023.7.22", + Status: "fixed", + Type: "python-pkg", + }}, }, { - ID: "CVE-2023-37920", - Severity: "CRITICAL", - Title: "Removal of e-Tugra root certificate", - Description: "Certifi is a curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. Certifi prior to version 2023.07.22 recognizes \"e-Tugra\" root certificates. e-Tugra's root certificates were subject to an investigation prompted by reporting of security issues in their systems. Certifi 2023.07.22 removes root certificates from \"e-Tugra\" from the root store.", - Package: "certifi", - Version: "2022.12.7", - FixVersion: "2023.7.22", - URL: "https://avd.aquasec.com/nvd/cve-2023-37920", - Status: "fixed", - Type: "python-pkg", - Score: "9.8", - PublishedDate: newTime("2023-07-25T21:15:00Z"), - LastModifiedDate: newTime("2023-08-12T06:16:00Z"), + Packages: []v1alpha1.Package{ + { + Package: "libssl1.1", + Version: "1.1.1s-r0", + FixVersion: "1.1.1t-r0", + Status: "fixed", + Type: "alpine", + }, + }, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-0286", + Severity: "HIGH", + Title: "openssl: X.400 address type confusion in X.509 GeneralName", + Description: "There is a type confusion vulnerability relating to X.400 address processing\ninside an X.509 GeneralName. X.400 addresses were parsed as an ASN1_STRING but\nthe public structure definition for GENERAL_NAME incorrectly specified the type\nof the x400Address field as ASN1_TYPE. This field is subsequently interpreted by\nthe OpenSSL function GENERAL_NAME_cmp as an ASN1_TYPE rather than an\nASN1_STRING.\n\nWhen CRL checking is enabled (i.e. the application sets the\nX509_V_FLAG_CRL_CHECK flag), this vulnerability may allow an attacker to pass\narbitrary pointers to a memcmp call, enabling them to read memory contents or\nenact a denial of service. In most cases, the attack requires the attacker to\nprovide both the certificate chain and CRL, neither of which need to have a\nvalid signature. If the attacker only controls one of these inputs, the other\ninput must already contain an X.400 address as a CRL distribution point, which\nis uncommon. As such, this vulnerability is most likely to only affect\napplications which have implemented their own functionality for retrieving CRLs\nover a network.\n\n", + URL: "https://avd.aquasec.com/nvd/cve-2023-0286", + Score: "7.4", + PublishedDate: newTime("2023-02-08T20:15:24.267Z"), + LastModifiedDate: newTime("2024-02-04T09:15:09.113Z"), + }, }, }, }, @@ -204,61 +240,77 @@ func TestParseVulnResults(t *testing.T) { OwnerReferences: owners, Labels: labels, }, - Spec: v1alpha1.VulnerabilityReportSpec{ - Cluster: "cluster", - Image: "docker.io/istio/examples-bookinfo-ratings-v1:1.17.0", - Tags: []string{"istio/examples-bookinfo-ratings-v1:1.17.0"}, - Digest: "istio/examples-bookinfo-ratings-v1@sha256:b6a6b88d35785c19f6dcb6acf055aa585511f2126bb0b5802f3107b7d37ead0b", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "9.12"}, - Resources: map[string][]string{"Deployment": {"apps/app1"}}, - TotalResources: 1, - Summary: v1alpha1.VulnerabilitySummary{Total: 3, High: 1, Medium: 1, Unknown: 1}, - Vulnerabilities: []v1alpha1.Vulnerability{ + Spec: v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "docker.io/istio/examples-bookinfo-ratings-v1:1.17.0", + Tags: []string{"istio/examples-bookinfo-ratings-v1:1.17.0"}, + Digest: "istio/examples-bookinfo-ratings-v1@sha256:b6a6b88d35785c19f6dcb6acf055aa585511f2126bb0b5802f3107b7d37ead0b", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "9.12"}, + Resources: map[string][]string{"Deployment": {"apps/app1"}}, + TotalResources: 1, + Summary: v1alpha1.VulnerabilitySummary{Total: 3, High: 1, Medium: 1, Unknown: 1}, + }, + TotalPackages: 3, + TotalUniquePackages: 3, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "DLA-3051-1", - Severity: "UNKNOWN", - Title: "tzdata - new timezone database", - Description: "", - Package: "tzdata", - Version: "2019c-0+deb9u1", - FixVersion: "2021a-0+deb9u4", - URL: "", - Status: "fixed", - Type: "debian", - PublishedDate: nil, - LastModifiedDate: nil, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "DLA-3051-1", + Severity: "UNKNOWN", + Title: "tzdata - new timezone database", + Description: "", + URL: "", + PublishedDate: nil, + LastModifiedDate: nil, + }, + Packages: []v1alpha1.Package{{ + Package: "tzdata", + Version: "2019c-0+deb9u1", + FixVersion: "2021a-0+deb9u4", + Status: "fixed", + Type: "debian", + }}, }, { - ID: "CVE-2016-2779", - Severity: "HIGH", - Title: "util-linux: runuser tty hijack via TIOCSTI ioctl", - Description: "runuser in util-linux allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", - Package: "bsdutils", - Version: "1:2.29.2-1+deb9u1", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2016-2779", - Status: "affected", - Type: "debian", - Score: "7.8", - PublishedDate: newTime("2017-02-07T15:59:00Z"), - LastModifiedDate: newTime("2019-01-04T14:14:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2016-2779", + Severity: "HIGH", + Title: "util-linux: runuser tty hijack via TIOCSTI ioctl", + Description: "runuser in util-linux allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + URL: "https://avd.aquasec.com/nvd/cve-2016-2779", + Score: "7.8", + PublishedDate: newTime("2017-02-07T15:59:00Z"), + LastModifiedDate: newTime("2019-01-04T14:14:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "bsdutils", + Version: "1:2.29.2-1+deb9u1", + FixVersion: "", + Status: "affected", + Type: "debian", + }}, }, { - ID: "GHSA-jmqm-f2gx-4fjv", - Severity: "MEDIUM", - Title: "Sensitive information exposure through logs in npm-registry-fetch", - Description: "Affected versions of `npm-registry-fetch` are vulnerable to an information exposure vulnerability through log files. The cli supports URLs like `\u003cprotocol\u003e://[\u003cuser\u003e[:\u003cpassword\u003e]@]\u003chostname\u003e[:\u003cport\u003e][:][/]\u003cpath\u003e`. The password value is not redacted and is printed to stdout and also to any generated log files.", - Package: "npm-registry-fetch", - Version: "4.0.4", - FixVersion: "8.1.1, 4.0.5", - URL: "https://github.com/advisories/GHSA-jmqm-f2gx-4fjv", - Status: "fixed", - Type: "node-pkg", - Score: "5.3", - PublishedDate: nil, - LastModifiedDate: nil, + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "GHSA-jmqm-f2gx-4fjv", + Severity: "MEDIUM", + Title: "Sensitive information exposure through logs in npm-registry-fetch", + Description: "Affected versions of `npm-registry-fetch` are vulnerable to an information exposure vulnerability through log files. The cli supports URLs like `\u003cprotocol\u003e://[\u003cuser\u003e[:\u003cpassword\u003e]@]\u003chostname\u003e[:\u003cport\u003e][:][/]\u003cpath\u003e`. The password value is not redacted and is printed to stdout and also to any generated log files.", + URL: "https://github.com/advisories/GHSA-jmqm-f2gx-4fjv", + Score: "5.3", + PublishedDate: nil, + LastModifiedDate: nil, + }, + Packages: []v1alpha1.Package{{ + Package: "npm-registry-fetch", + Version: "4.0.4", + FixVersion: "8.1.1, 4.0.5", + Status: "fixed", + Type: "node-pkg", + }}, }, }, }, @@ -271,47 +323,59 @@ func TestParseVulnResults(t *testing.T) { OwnerReferences: owners, Labels: labels, }, - Spec: v1alpha1.VulnerabilityReportSpec{ - Cluster: "cluster", - Image: "docker.io/istio/examples-bookinfo-details-v1:1.17.0", - Tags: []string{"istio/examples-bookinfo-details-v1:1.17.0"}, - Digest: "istio/examples-bookinfo-details-v1@sha256:2b081e3c86dd8105040ea1f2adcc94cb473f41249dc9c91ebc1c2885ddd56c13", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "10.5"}, - Resources: map[string][]string{"Deployment": {"apps/app2"}}, - TotalResources: 1, - Summary: v1alpha1.VulnerabilitySummary{Total: 2, High: 1, Low: 1}, - Vulnerabilities: []v1alpha1.Vulnerability{ + Spec: v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "docker.io/istio/examples-bookinfo-details-v1:1.17.0", + Tags: []string{"istio/examples-bookinfo-details-v1:1.17.0"}, + Digest: "istio/examples-bookinfo-details-v1@sha256:2b081e3c86dd8105040ea1f2adcc94cb473f41249dc9c91ebc1c2885ddd56c13", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "10.5"}, + Resources: map[string][]string{"Deployment": {"apps/app2"}}, + TotalResources: 1, + Summary: v1alpha1.VulnerabilitySummary{Total: 2, High: 1, Low: 1}, + }, + TotalPackages: 2, + TotalUniquePackages: 2, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2016-2781", - Severity: "LOW", - Title: "coreutils: Non-privileged session can escape to the parent session in chroot", - Description: "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", - Package: "coreutils", - Version: "8.30-3", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2016-2781", - Status: "will_not_fix", - Type: "debian", - Score: "6.5", - PublishedDate: newTime("2017-02-07T15:59:00Z"), - LastModifiedDate: newTime("2021-02-25T17:15:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2016-2781", + Severity: "LOW", + Title: "coreutils: Non-privileged session can escape to the parent session in chroot", + Description: "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + URL: "https://avd.aquasec.com/nvd/cve-2016-2781", + Score: "6.5", + PublishedDate: newTime("2017-02-07T15:59:00Z"), + LastModifiedDate: newTime("2021-02-25T17:15:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "coreutils", + Version: "8.30-3", + FixVersion: "", + Status: "will_not_fix", + Type: "debian", + }}, }, { - ID: "CVE-2023-28755", - Severity: "HIGH", - Title: "ReDoS vulnerability in URI", - Description: "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", - Package: "uri", - Version: "0.10.0", - FixVersion: "~\u003e 0.10.0.1, ~\u003e 0.10.2, ~\u003e 0.11.1, \u003e= 0.12.1", - URL: "https://avd.aquasec.com/nvd/cve-2023-28755", - Status: "fixed", - Type: "gemspec", - Score: "5.3", - PublishedDate: newTime("2023-03-31T04:15:00Z"), - LastModifiedDate: newTime("2023-05-30T17:17:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-28755", + Severity: "HIGH", + Title: "ReDoS vulnerability in URI", + Description: "A ReDoS issue was discovered in the URI component through 0.12.0 in Ruby through 3.2.1. The URI parser mishandles invalid URLs that have specific characters. It causes an increase in execution time for parsing strings to URI objects. The fixed versions are 0.12.1, 0.11.1, 0.10.2 and 0.10.0.1.", + URL: "https://avd.aquasec.com/nvd/cve-2023-28755", + Score: "5.3", + PublishedDate: newTime("2023-03-31T04:15:00Z"), + LastModifiedDate: newTime("2023-05-30T17:17:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "uri", + Version: "0.10.0", + FixVersion: "~\u003e 0.10.0.1, ~\u003e 0.10.2, ~\u003e 0.11.1, \u003e= 0.12.1", + Status: "fixed", + Type: "gemspec", + }}, }, }, }, @@ -324,34 +388,42 @@ func TestParseVulnResults(t *testing.T) { OwnerReferences: owners, Labels: labels, }, - Spec: v1alpha1.VulnerabilityReportSpec{ - Cluster: "cluster", - Image: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", - Tags: []string{"nginx:1.25.0"}, - Digest: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", - Architecture: "amd64", - OS: "linux", - Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, - TotalResources: 1, - Resources: map[string][]string{"Deployment": {"default/nginx"}}, - Vulnerabilities: []v1alpha1.Vulnerability{ + Spec: v1alpha2.VulnerabilityReportSpec{ + VulnerabilityReportCommon: v1alpha1.VulnerabilityReportCommon{ + Cluster: "cluster", + Image: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", + Tags: []string{"nginx:1.25.0"}, + Digest: "nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305", + Architecture: "amd64", + OS: "linux", + Distro: &v1alpha1.Distro{Name: "debian", Version: "11.7"}, + TotalResources: 1, + Resources: map[string][]string{"Deployment": {"default/nginx"}}, + Summary: v1alpha1.VulnerabilitySummary{Total: 1, Medium: 1}, + }, + TotalPackages: 1, + TotalUniquePackages: 1, + Vulnerabilities: []v1alpha2.Vulnerability{ { - ID: "CVE-2023-3446", - Severity: "MEDIUM", - Title: "Excessive time spent checking DH keys and parameters", - Description: "Issue summary: Checking excessively long DH keys or parameters may be very slow.\n\nImpact summary: Applications that use the functions DH_check(), DH_check_ex()\nor EVP_PKEY_param_check() to check a DH key or DH parameters may experience long\ndelays. Where the key or parameters that are being checked have been obtained\nfrom an untrusted source this may lead to a Denial of Service.\n\nThe function DH_check() performs various checks on DH parameters. One of those\nchecks confirms that the modulus ('p' parameter) is not too large. Trying to use\na very large modulus is slow and OpenSSL will not normally use a modulus which\nis over 10,000 bits in length.\n\nHowever the DH_check() function checks numerous aspects of the key or parameters\nthat have been supplied. Some of those checks use the supplied modulus value\neven if it has already been found to be too large.\n\nAn application that calls DH_check() and supplies a key or parameters obtained\nfrom an untrusted source could be vulernable to a Denial of Service attack.\n\nThe function DH_check() is itself called by a number of other OpenSSL functions.\nAn application calling any of those other functions may similarly be affected.\nThe other functions affected by this are DH_check_ex() and\nEVP_PKEY_param_check().\n\nAlso vulnerable are the OpenSSL dhparam and pkeyparam command line applications\nwhen using the '-check' option.\n\nThe OpenSSL SSL/TLS implementation is not affected by this issue.\nThe OpenSSL 3.0 and 3.1 FIPS providers are not affected by this issue.", - Package: "openssl", - Version: "1.1.1n-0+deb11u4", - FixVersion: "", - URL: "https://avd.aquasec.com/nvd/cve-2023-3446", - Status: "fix_deferred", - Type: "debian", - Score: "5.3", - PublishedDate: newTime("2023-07-19T12:15:00Z"), - LastModifiedDate: newTime("2023-08-16T08:15:00Z"), + VulnerabilityCommon: v1alpha1.VulnerabilityCommon{ + ID: "CVE-2023-3446", + Severity: "MEDIUM", + Title: "Excessive time spent checking DH keys and parameters", + Description: "Issue summary: Checking excessively long DH keys or parameters may be very slow.\n\nImpact summary: Applications that use the functions DH_check(), DH_check_ex()\nor EVP_PKEY_param_check() to check a DH key or DH parameters may experience long\ndelays. Where the key or parameters that are being checked have been obtained\nfrom an untrusted source this may lead to a Denial of Service.\n\nThe function DH_check() performs various checks on DH parameters. One of those\nchecks confirms that the modulus ('p' parameter) is not too large. Trying to use\na very large modulus is slow and OpenSSL will not normally use a modulus which\nis over 10,000 bits in length.\n\nHowever the DH_check() function checks numerous aspects of the key or parameters\nthat have been supplied. Some of those checks use the supplied modulus value\neven if it has already been found to be too large.\n\nAn application that calls DH_check() and supplies a key or parameters obtained\nfrom an untrusted source could be vulernable to a Denial of Service attack.\n\nThe function DH_check() is itself called by a number of other OpenSSL functions.\nAn application calling any of those other functions may similarly be affected.\nThe other functions affected by this are DH_check_ex() and\nEVP_PKEY_param_check().\n\nAlso vulnerable are the OpenSSL dhparam and pkeyparam command line applications\nwhen using the '-check' option.\n\nThe OpenSSL SSL/TLS implementation is not affected by this issue.\nThe OpenSSL 3.0 and 3.1 FIPS providers are not affected by this issue.", + URL: "https://avd.aquasec.com/nvd/cve-2023-3446", + Score: "5.3", + PublishedDate: newTime("2023-07-19T12:15:00Z"), + LastModifiedDate: newTime("2023-08-16T08:15:00Z"), + }, + Packages: []v1alpha1.Package{{ + Package: "openssl", + Version: "1.1.1n-0+deb11u4", + FixVersion: "", + Status: "fix_deferred", + Type: "debian", + }}, }, }, - Summary: v1alpha1.VulnerabilitySummary{Total: 1, Medium: 1}, }, }, }, @@ -383,7 +455,7 @@ func TestParseVulnResults(t *testing.T) { } } -func sortVulns(vulns []v1alpha1.VulnerabilityReport) { +func sortVulns(vulns []v1alpha2.VulnerabilityReport) { sort.Slice(vulns, func(i, j int) bool { return strings.Compare(vulns[i].Spec.Image, vulns[j].Spec.Image) == -1 }) @@ -391,6 +463,11 @@ func sortVulns(vulns []v1alpha1.VulnerabilityReport) { for _, r := range v.Spec.Resources { sort.Strings(r) } + for _, vuln := range v.Spec.Vulnerabilities { + sort.Slice(vuln.Packages, func(i, j int) bool { + return strings.Compare(vuln.Packages[i].String(), vuln.Packages[j].String()) == -1 + }) + } sort.Slice(v.Spec.Vulnerabilities, func(i, j int) bool { return strings.Compare(v.Spec.Vulnerabilities[i].ID, v.Spec.Vulnerabilities[j].ID) == -1 })