Skip to content

Commit

Permalink
feat: add metrics for configauditreport summary (giantswarm#72)
Browse files Browse the repository at this point in the history
* feat: add metrics for configauditreport summary

* add finalizer and cleanup logic

* add kubebuilder annotations

* fix imports

* fix imports on vuln metrics

* remove binary

* Update controllers/configauditreport/configauditreport_controller.go

Co-authored-by: Zach Stone <[email protected]>

* use new starboard upstream severity for configauditreport

* upgrade dependencies inc starboard

* fix dockerfile missing utils dir

Co-authored-by: Zach Stone <[email protected]>
  • Loading branch information
mycodeself and stone-z authored Apr 12, 2022
1 parent 75ca9ea commit 0c43bbc
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 57 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RUN go mod download
COPY main.go main.go
# COPY api/ api/
COPY controllers/ controllers/
COPY utils/ utils/

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go
Expand Down
5 changes: 5 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ resources:
group: aquasecurity.github.io
kind: VulnerabilityReport
version: v1alpha1
- controller: true
domain: giantswarm
group: aquasecurity.github.io
kind: ConfigAuditReport
version: v1alpha1
version: "3"
187 changes: 187 additions & 0 deletions controllers/configauditreport/configauditreport_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
Copyright 2021.
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 configauditreport

import (
"context"
"fmt"
"time"

"github.com/go-logr/logr"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

aqua "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"

"github.com/giantswarm/starboard-exporter/utils"
)

const (
ConfigAuditReportFinalizer = "starboard-exporter.giantswarm.io/configauditreport"
)

// ConfigAuditReportReconciler reconciles a ConfigAuditReport object
type ConfigAuditReportReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports/finalizers,verbs=update
func (r *ConfigAuditReportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
_ = r.Log.WithValues("configauditreport", req.NamespacedName)

report := &aqua.ConfigAuditReport{}
if err := r.Client.Get(ctx, req.NamespacedName, report); err != nil {
if apierrors.IsNotFound(err) {
// Most likely the report was deleted.
return ctrl.Result{}, nil
}

// Error reading the object.
r.Log.Error(err, "Unable to read configauditreport")
return ctrl.Result{}, err
}

if report.DeletionTimestamp.IsZero() {
// Give the report our finalizer if it doesn't have one.
if !utils.SliceContains(report.GetFinalizers(), ConfigAuditReportFinalizer) {
ctrlutil.AddFinalizer(report, ConfigAuditReportFinalizer)
if err := r.Update(ctx, report); err != nil {
return ctrl.Result{}, err
}
}

r.Log.Info(fmt.Sprintf("Reconciled %s || Found (C/H/M/L): %d/%d/%d/%d",
req.NamespacedName,
report.Report.Summary.CriticalCount,
report.Report.Summary.HighCount,
report.Report.Summary.MediumCount,
report.Report.Summary.LowCount,
))

// Publish summary metrics for this report.
publishSummaryMetrics(report)

} else {

if utils.SliceContains(report.GetFinalizers(), ConfigAuditReportFinalizer) {
// Unfortunately, we can't just clear the series based on one label value,
// we have to reconstruct all of the label values to delete the series.
// That's the only reason the finalizer is needed at all.
r.clearImageMetrics(report)

ctrlutil.RemoveFinalizer(report, ConfigAuditReportFinalizer)
if err := r.Update(ctx, report); err != nil {
return ctrl.Result{}, err
}
}
}

return defaultRequeue(), nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
err := ctrl.NewControllerManagedBy(mgr).
For(&aqua.ConfigAuditReport{}).
Complete(r)
if err != nil {
return errors.Wrap(err, "failed setting up controller with controller manager")
}

return nil
}

func (r *ConfigAuditReportReconciler) clearImageMetrics(report *aqua.ConfigAuditReport) {
// clear summary metrics
summaryValues := valuesForReport(report, metricLabels)

// Delete the series for each severity.
for severity := range getCountPerSeverity(report) {
v := summaryValues
v["severity"] = severity

// Delete the metric.
ConfigAuditSummary.Delete(
v,
)
}
}

func getCountPerSeverity(report *aqua.ConfigAuditReport) map[string]float64 {
// Format is e.g. {CRITICAL: 10}.
return map[string]float64{
string(aqua.SeverityCritical): float64(report.Report.Summary.CriticalCount),
string(aqua.SeverityHigh): float64(report.Report.Summary.HighCount),
string(aqua.SeverityMedium): float64(report.Report.Summary.MediumCount),
string(aqua.SeverityLow): float64(report.Report.Summary.LowCount),
}
}

func publishSummaryMetrics(report *aqua.ConfigAuditReport) {
summaryValues := valuesForReport(report, metricLabels)

// Add the severity label after the standard labels and expose each severity metric.
for severity, count := range getCountPerSeverity(report) {
v := summaryValues
v["severity"] = severity

// Expose the metric.
ConfigAuditSummary.With(
v,
).Set(count)
}
}

func valuesForReport(report *aqua.ConfigAuditReport, labels []string) map[string]string {
result := map[string]string{}
for _, label := range labels {
result[label] = reportValueFor(label, report)
}
return result
}

func reportValueFor(field string, report *aqua.ConfigAuditReport) string {
switch field {
case "resource_name":
return report.Name
case "resource_namespace":
return report.Namespace
case "severity":
return "" // this value will be overwritten on publishSummaryMetrics
default:
// Error?
return ""
}
}

func defaultRequeue() reconcile.Result {
return ctrl.Result{
Requeue: true,
RequeueAfter: time.Minute * 5,
}
}
35 changes: 35 additions & 0 deletions controllers/configauditreport/configauditreport_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package configauditreport

import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)

const (
metricNamespace = "starboard_exporter"
metricSubsystem = "configauditreport"
)

var metricLabels = []string{
"resource_name",
"resource_namespace",
"severity",
}

// Gauge for the count of all config audit rules summary
var (
ConfigAuditSummary = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Subsystem: metricSubsystem,
Name: "resource_checks_summary_count",
Help: "Exposes the number of checks of a particular severity.",
},
metricLabels,
)
)

func init() {
// Register custom metrics with the global prometheus registry
metrics.Registry.MustRegister(ConfigAuditSummary)
}
6 changes: 4 additions & 2 deletions controllers/vulnerabilityreport_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

aqua "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"

"github.com/giantswarm/starboard-exporter/utils"
)

const (
Expand Down Expand Up @@ -77,7 +79,7 @@ func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.
if report.DeletionTimestamp.IsZero() {

// Give the report our finalizer if it doesn't have one.
if !sliceContains(report.GetFinalizers(), VulnerabilityReportFinalizer) {
if !utils.SliceContains(report.GetFinalizers(), VulnerabilityReportFinalizer) {
ctrlutil.AddFinalizer(report, VulnerabilityReportFinalizer)
if err := r.Update(ctx, report); err != nil {
return ctrl.Result{}, err
Expand All @@ -99,7 +101,7 @@ func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.

} else {

if sliceContains(report.GetFinalizers(), VulnerabilityReportFinalizer) {
if utils.SliceContains(report.GetFinalizers(), VulnerabilityReportFinalizer) {
// Unfortunately, we can't just clear the series based on one label value,
// we have to reconstruct all of the label values to delete the series.
// That's the only reason the finalizer is needed at all.
Expand Down
15 changes: 4 additions & 11 deletions controllers/vulnerabilityreport_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package controllers
import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"

"github.com/giantswarm/starboard-exporter/utils"
)

const (
Expand Down Expand Up @@ -105,19 +107,10 @@ func LabelWithName(name string) (label VulnerabilityLabel, ok bool) {
return VulnerabilityLabel{}, false
}

func sliceContains(s []string, value string) bool {
for _, item := range s {
if item == value {
return true
}
}
return false
}

func LabelsForGroup(group string) []VulnerabilityLabel {
l := []VulnerabilityLabel{}
for _, label := range metricLabels {
if sliceContains(label.Groups, group) {
if utils.SliceContains(label.Groups, group) {
l = append(l, label)
}
}
Expand All @@ -127,7 +120,7 @@ func LabelsForGroup(group string) []VulnerabilityLabel {
func labelNamesForGroup(group string) []string {
l := []string{}
for _, label := range metricLabels {
if sliceContains(label.Groups, group) {
if utils.SliceContains(label.Groups, group) {
l = append(l, label.Name)
}
}
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ module github.com/giantswarm/starboard-exporter
go 1.16

require (
github.com/aquasecurity/starboard v0.14.1
github.com/aquasecurity/starboard v0.15.3
github.com/go-logr/logr v1.2.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.1
k8s.io/apimachinery v0.23.2
k8s.io/client-go v0.23.2
sigs.k8s.io/controller-runtime v0.11.1
k8s.io/apimachinery v0.23.5
k8s.io/client-go v0.23.5
sigs.k8s.io/controller-runtime v0.11.2
)

replace (
Expand Down
Loading

0 comments on commit 0c43bbc

Please sign in to comment.