From 3917d89130453b1acc1265c3515644dc3c39f199 Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Fri, 12 Jan 2024 20:44:18 -0300 Subject: [PATCH 1/5] chore: implement PutVulnerabilityReport in saas.Client --- internal/saas/client.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/saas/client.go b/internal/saas/client.go index 72ddb01c..ec83f039 100644 --- a/internal/saas/client.go +++ b/internal/saas/client.go @@ -22,6 +22,8 @@ import ( "net/http" "net/url" "path" + + "github.com/undistro/zora/api/zora/v1alpha1" ) const ( @@ -42,6 +44,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 } type client struct { @@ -120,6 +123,26 @@ 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") + b, err := json.Marshal(vulnReport) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPut, u, bytes.NewReader(b)) + if err != nil { + return err + } + req.Header.Set("content-type", "application/json") + req.Header.Set(versionHeader, r.version) + res, err := r.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + return validateStatus(res) +} + func (r *client) DeleteClusterScan(ctx context.Context, namespace, name string) error { u := r.clusterURL(namespace, name, "scan") req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u, nil) From ef1ea0982ee34e89072b54a29f45222040b521ba Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Fri, 12 Jan 2024 20:45:19 -0300 Subject: [PATCH 2/5] chore: add status conditions in VulnerabilityReportStatus --- .../v1alpha1/vulnerabilityreport_types.go | 4 +- api/zora/v1alpha1/zz_generated.deepcopy.go | 3 +- ...zora.undistro.io_vulnerabilityreports.yaml | 76 +++++++++++++++++++ ...zora.undistro.io_vulnerabilityreports.yaml | 76 +++++++++++++++++++ 4 files changed, 157 insertions(+), 2 deletions(-) diff --git a/api/zora/v1alpha1/vulnerabilityreport_types.go b/api/zora/v1alpha1/vulnerabilityreport_types.go index e1aa88a5..7ed9329d 100644 --- a/api/zora/v1alpha1/vulnerabilityreport_types.go +++ b/api/zora/v1alpha1/vulnerabilityreport_types.go @@ -67,7 +67,9 @@ type VulnerabilitySummary struct { } // VulnerabilityReportStatus defines the observed state of VulnerabilityReport -type VulnerabilityReportStatus struct{} +type VulnerabilityReportStatus struct { + Status `json:",inline"` +} //+kubebuilder:object:root=true //+kubebuilder:subresource:status diff --git a/api/zora/v1alpha1/zz_generated.deepcopy.go b/api/zora/v1alpha1/zz_generated.deepcopy.go index 45d06296..0043cafa 100644 --- a/api/zora/v1alpha1/zz_generated.deepcopy.go +++ b/api/zora/v1alpha1/zz_generated.deepcopy.go @@ -770,7 +770,7 @@ func (in *VulnerabilityReport) DeepCopyInto(out *VulnerabilityReport) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VulnerabilityReport. @@ -875,6 +875,7 @@ func (in *VulnerabilityReportSpec) DeepCopy() *VulnerabilityReportSpec { // 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. diff --git a/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml b/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml index 2e3ed886..24e41cfc 100644 --- a/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml +++ b/charts/zora/crds/zora.undistro.io_vulnerabilityreports.yaml @@ -185,6 +185,82 @@ spec: 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. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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 diff --git a/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml b/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml index e80ff2d8..759b414a 100644 --- a/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml +++ b/config/crd/bases/zora.undistro.io_vulnerabilityreports.yaml @@ -171,6 +171,82 @@ spec: 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. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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 From d611d527f8f9097c11fa5d3a8cbb9f1fca0f92ba Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Fri, 12 Jan 2024 20:53:01 -0300 Subject: [PATCH 3/5] chore: grant vulnerabilityreports/status permission to zora-operator --- charts/zora/templates/operator/rbac.yaml | 20 +++++++++++++++++++ config/rbac/role.yaml | 2 ++ .../controller/zora/clusterscan_controller.go | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/charts/zora/templates/operator/rbac.yaml b/charts/zora/templates/operator/rbac.yaml index 88a5a8c7..f2f6e867 100644 --- a/charts/zora/templates/operator/rbac.yaml +++ b/charts/zora/templates/operator/rbac.yaml @@ -265,6 +265,26 @@ rules: - get - list - watch +- apiGroups: + - zora.undistro.io + resources: + - vulnerabilityreports + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - zora.undistro.io + resources: + - vulnerabilityreports/status + verbs: + - get + - patch + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1fbc95be..d10e1ae1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -219,3 +219,5 @@ rules: - vulnerabilityreports/status verbs: - get + - patch + - update diff --git a/internal/controller/zora/clusterscan_controller.go b/internal/controller/zora/clusterscan_controller.go index 52d8c8b4..f1696a49 100644 --- a/internal/controller/zora/clusterscan_controller.go +++ b/internal/controller/zora/clusterscan_controller.go @@ -74,7 +74,7 @@ type ClusterScanReconciler struct { //+kubebuilder:rbac:groups=zora.undistro.io,resources=clusterissues,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=zora.undistro.io,resources=clusterissues/status,verbs=get //+kubebuilder:rbac:groups=zora.undistro.io,resources=vulnerabilityreports,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=zora.undistro.io,resources=vulnerabilityreports/status,verbs=get +//+kubebuilder:rbac:groups=zora.undistro.io,resources=vulnerabilityreports/status,verbs=get;update;patch //+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=batch,resources=cronjobs/status,verbs=get //+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch From 78e5e08db3c5fc0b80bb9154757203a9626ca9dd Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Sat, 13 Jan 2024 11:36:37 -0300 Subject: [PATCH 4/5] feat: sending vulnerability reports to saas if enabled --- .../v1alpha1/vulnerabilityreport_types.go | 14 +++ internal/saas/hooks.go | 90 ++++++++++++++++--- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/api/zora/v1alpha1/vulnerabilityreport_types.go b/api/zora/v1alpha1/vulnerabilityreport_types.go index 7ed9329d..4f368b2d 100644 --- a/api/zora/v1alpha1/vulnerabilityreport_types.go +++ b/api/zora/v1alpha1/vulnerabilityreport_types.go @@ -71,6 +71,20 @@ 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} diff --git a/internal/saas/hooks.go b/internal/saas/hooks.go index c17853c1..e49d7c27 100644 --- a/internal/saas/hooks.go +++ b/internal/saas/hooks.go @@ -16,8 +16,10 @@ package saas import ( "context" + "errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/undistro/zora/api/zora/v1alpha1" @@ -76,6 +78,17 @@ func getClusterScans(ctx context.Context, c ctrlClient.Client, namespace, cluste } func updateClusterScan(saasClient Client, c ctrlClient.Client, ctx context.Context, clusterScan *v1alpha1.ClusterScan, scanList *v1alpha1.ClusterScanList) error { + if err := pushMisconfigs(saasClient, c, ctx, clusterScan, scanList); err != nil { + return err + } + + if err := pushVulns(saasClient, c, ctx, clusterScan); err != nil { + return err + } + return nil +} + +func pushMisconfigs(saasClient Client, c ctrlClient.Client, ctx context.Context, clusterScan *v1alpha1.ClusterScan, scanList *v1alpha1.ClusterScanList) error { clusterName := clusterScan.Spec.ClusterRef.Name var lastScanIDs []string for _, cs := range scanList.Items { @@ -85,22 +98,13 @@ func updateClusterScan(saasClient Client, c ctrlClient.Client, ctx context.Conte lastScanIDs = append(lastScanIDs, cs.Status.LastScanIDs(true)...) } - ls := &metav1.LabelSelector{ - MatchLabels: map[string]string{v1alpha1.LabelCluster: clusterName}, - } - if len(lastScanIDs) > 0 { - ls.MatchExpressions = []metav1.LabelSelectorRequirement{{ - Key: v1alpha1.LabelScanID, - Operator: metav1.LabelSelectorOpIn, - Values: lastScanIDs, - }} - } - lss, err := metav1.LabelSelectorAsSelector(ls) + ls, err := buildLabelSelector(clusterName, lastScanIDs) if err != nil { return err } + issueList := &v1alpha1.ClusterIssueList{} - if err := c.List(ctx, issueList, ctrlClient.MatchingLabelsSelector{Selector: lss}); err != nil { + if err := c.List(ctx, issueList, ctrlClient.MatchingLabelsSelector{Selector: ls}); err != nil { return err } @@ -108,8 +112,9 @@ func updateClusterScan(saasClient Client, c ctrlClient.Client, ctx context.Conte if status == nil { return nil } - if err := saasClient.PutClusterScan(ctx, clusterScan.Namespace, clusterName, status); err != nil { - if serr, ok := err.(*saasError); ok { + if err := saasClient.PutClusterScan(ctx, clusterScan.Namespace, clusterScan.Spec.ClusterRef.Name, status); err != nil { + var serr *saasError + if errors.As(err, &serr) { clusterScan.SetSaaSStatus(metav1.ConditionFalse, serr.Err, serr.Detail) return nil } @@ -119,3 +124,60 @@ func updateClusterScan(saasClient Client, c ctrlClient.Client, ctx context.Conte clusterScan.SetSaaSStatus(metav1.ConditionTrue, "OK", "cluster scan successfully synced with SaaS") return nil } + +func pushVulns(scl Client, cl ctrlClient.Client, ctx context.Context, cs *v1alpha1.ClusterScan) error { + successfulScanIDs := cs.Status.LastScanIDs(true) + if len(successfulScanIDs) == 0 { + return nil + } + ls, err := buildLabelSelector(cs.Spec.ClusterRef.Name, successfulScanIDs) + if err != nil { + return err + } + + metaList := &metav1.PartialObjectMetadataList{} + metaList.SetGroupVersionKind(v1alpha1.GroupVersion.WithKind("VulnerabilityReportList")) + if err := cl.List(ctx, metaList, ls); err != nil { + return err + } + if len(metaList.Items) == 0 { + return nil + } + for _, i := range metaList.Items { + vulnReport := &v1alpha1.VulnerabilityReport{} + if err := cl.Get(ctx, types.NamespacedName{Namespace: i.Namespace, Name: i.Name}, vulnReport); err != nil { + return err + } + if vulnReport.SaaSStatusIsTrue() { + continue + } + if err := scl.PutVulnerabilityReport(ctx, cs.Namespace, cs.Spec.ClusterRef.Name, *vulnReport); err != nil { + cs.SetSaaSStatus(metav1.ConditionFalse, "Error", err.Error()) + vulnReport.SetSaaSStatus(metav1.ConditionTrue, "Error", err.Error()) + _ = cl.Status().Update(ctx, vulnReport) + return err + } + vulnReport.SetSaaSStatus(metav1.ConditionTrue, "OK", "VulnerabilityReport successfully pushed to SaaS") + if err := cl.Status().Update(ctx, vulnReport); err != nil { + return err + } + } + cs.SetSaaSStatus(metav1.ConditionTrue, "OK", "cluster scan successfully synced with SaaS") + return nil +} + +func buildLabelSelector(clusterName string, scanIDs []string) (*ctrlClient.MatchingLabelsSelector, error) { + sel := &metav1.LabelSelector{MatchLabels: map[string]string{v1alpha1.LabelCluster: clusterName}} + if len(scanIDs) > 0 { + sel.MatchExpressions = []metav1.LabelSelectorRequirement{{ + Key: v1alpha1.LabelScanID, + Operator: metav1.LabelSelectorOpIn, + Values: scanIDs, + }} + } + ls, err := metav1.LabelSelectorAsSelector(sel) + if err != nil { + return nil, err + } + return &ctrlClient.MatchingLabelsSelector{Selector: ls}, nil +} From c0fe7406c2fe203a3a81b3f6a6da6d7085f8f8f5 Mon Sep 17 00:00:00 2001 From: Matheus Moraes Date: Mon, 15 Jan 2024 06:41:40 -0300 Subject: [PATCH 5/5] chore: add scanID in scan status to be sent to SaaS --- internal/saas/clusters.go | 2 ++ internal/saas/hooks.go | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/saas/clusters.go b/internal/saas/clusters.go index 804d0c41..4b1003bf 100644 --- a/internal/saas/clusters.go +++ b/internal/saas/clusters.go @@ -80,6 +80,7 @@ type ScanStatus struct { Status ScanStatusType `json:"status"` Message string `json:"message"` Suspend bool `json:"suspend"` + ID string `json:"id"` } type ConnectionStatus struct { @@ -156,6 +157,7 @@ func NewScanStatus(scans []v1alpha1.ClusterScan) (map[string]*PluginStatus, *int } pluginStatus[p].Scan.Suspend = pointer.BoolDeref(cs.Spec.Suspend, false) pluginStatus[p].Schedule = cs.Spec.Schedule + pluginStatus[p].Scan.ID = s.LastScanID if s.TotalIssues != nil { if pluginStatus[p].IssueCount == nil { diff --git a/internal/saas/hooks.go b/internal/saas/hooks.go index e49d7c27..330183a8 100644 --- a/internal/saas/hooks.go +++ b/internal/saas/hooks.go @@ -78,11 +78,10 @@ func getClusterScans(ctx context.Context, c ctrlClient.Client, namespace, cluste } func updateClusterScan(saasClient Client, c ctrlClient.Client, ctx context.Context, clusterScan *v1alpha1.ClusterScan, scanList *v1alpha1.ClusterScanList) error { - if err := pushMisconfigs(saasClient, c, ctx, clusterScan, scanList); err != nil { + if err := pushVulns(saasClient, c, ctx, clusterScan); err != nil { return err } - - if err := pushVulns(saasClient, c, ctx, clusterScan); err != nil { + if err := pushMisconfigs(saasClient, c, ctx, clusterScan, scanList); err != nil { return err } return nil