Skip to content

Commit

Permalink
feat: Save historical reports of running kube-bench (#5)
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Pacak <[email protected]>
  • Loading branch information
danielpacak authored May 13, 2020
1 parent a4f150f commit cc1deb2
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 78 deletions.
11 changes: 7 additions & 4 deletions pkg/cmd/kube_bench.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package cmd

import (
"github.com/aquasecurity/starboard/pkg/ext"
starboard "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
"github.com/aquasecurity/starboard/pkg/kubebench"
"github.com/aquasecurity/starboard/pkg/kubebench/crd"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
)

func GetKubeBenchCmd(cf *genericclioptions.ConfigFlags) *cobra.Command {
Expand All @@ -16,19 +19,19 @@ func GetKubeBenchCmd(cf *genericclioptions.ConfigFlags) *cobra.Command {
if err != nil {
return
}
scanner, err := kubebench.NewScanner(config)
kubernetesClientset, err := kubernetes.NewForConfig(config)
if err != nil {
return
}
report, node, err := scanner.Scan()
report, node, err := kubebench.NewScanner(kubernetesClientset).Scan()
if err != nil {
return
}
writer, err := crd.NewWriter(config)
starboardClientset, err := starboard.NewForConfig(config)
if err != nil {
return
}
err = writer.Write(report, node)
err = crd.NewWriter(ext.NewSystemClock(), starboardClientset).Write(report, node)
return
},
}
Expand Down
15 changes: 14 additions & 1 deletion pkg/kube/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ import (
)

const (
LabelWorkloadKind = "starboard.workload.kind"
// Deprecated use LabelResourceKind instead, which is more generic
LabelWorkloadKind = "starboard.workload.kind"
// Deprecated use LabelResourceName instead, which is more generic
LabelWorkloadName = "starboard.workload.name"
LabelContainerName = "starboard.container.name"
LabelResourceKind = "starboard.resource.kind"
LabelResourceName = "starboard.resource.name"

LabelScannerName = "starboard.scanner.name"
LabelScannerVendor = "starboard.scanner.vendor"

LabelHistoryLatest = "starboard.history.latest"
)

const (
AnnotationHistoryLimit = "starboard.history.limit"
)

// WorkloadKind is an enum defining the different kinds of Kubernetes workloads.
Expand Down
48 changes: 48 additions & 0 deletions pkg/kubebench/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kubebench

import (
"encoding/json"
starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
"github.com/aquasecurity/starboard/pkg/ext"
"io"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type Converter interface {
Convert(reader io.Reader) (report starboard.CISKubernetesBenchmarkReport, err error)
}

var DefaultConverter Converter = &converter{
clock: ext.NewSystemClock(),
}

type converter struct {
clock ext.Clock
}

func (c *converter) Convert(reader io.Reader) (report starboard.CISKubernetesBenchmarkReport, err error) {
decoder := json.NewDecoder(reader)
report = starboard.CISKubernetesBenchmarkReport{
GeneratedAt: meta.NewTime(c.clock.Now()),
Scanner: starboard.Scanner{
Name: "kube-bench",
Vendor: "Aqua Security",
Version: "latest",
},
Sections: []starboard.CISKubernetesBenchmarkSection{},
}

for {
var section starboard.CISKubernetesBenchmarkSection
de := decoder.Decode(&section)
if de == io.EOF {
break
}
if de != nil {
err = de
break
}
report.Sections = append(report.Sections, section)
}
return
}
1 change: 1 addition & 0 deletions pkg/kubebench/converter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package kubebench
141 changes: 120 additions & 21 deletions pkg/kubebench/crd/writer.go
Original file line number Diff line number Diff line change
@@ -1,44 +1,143 @@
package crd

import (
"errors"
"strings"
"fmt"
"strconv"

sec "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
secapi "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
"github.com/aquasecurity/starboard/pkg/ext"
"github.com/aquasecurity/starboard/pkg/kube"

"k8s.io/klog"

core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/utils/pointer"

starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
starboardapi "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
"github.com/aquasecurity/starboard/pkg/kubebench"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
)

const (
defaultHistoryLimit = 10
)

type writer struct {
client *secapi.Clientset
clock ext.Clock
clientset starboardapi.Interface
}

func NewWriter(config *rest.Config) (w kubebench.Writer, err error) {
client, err := secapi.NewForConfig(config)
func NewWriter(clock ext.Clock, clientset starboardapi.Interface) kubebench.Writer {
return &writer{
clock: clock,
clientset: clientset,
}
}

func (w *writer) Write(report starboard.CISKubernetesBenchmarkReport, node *core.Node) (err error) {
reports, err := w.getReportsByNodeName(node.GetName())
if err != nil {
return
}
w = &writer{
client: client,
err = w.removeHistoryLatestLabel(reports)
if err != nil {
return
}
return
}

func (w *writer) Write(report sec.CISKubernetesBenchmarkReport, node string) (err error) {
if strings.TrimSpace(node) == "" {
err = errors.New("node name must not be blank")
err = w.removeReportsWithHistoryLimitExceeded(reports)
if err != nil {
return
}
// TODO Check if an instance of the report with the given name already exists.
// TODO If exists just update it, create new instance otherwise
_, err = w.client.AquasecurityV1alpha1().CISKubernetesBenchmarks().Create(&sec.CISKubernetesBenchmark{

_, err = w.clientset.AquasecurityV1alpha1().CISKubernetesBenchmarks().Create(&starboard.CISKubernetesBenchmark{
ObjectMeta: meta.ObjectMeta{
Name: node,
Labels: map[string]string{},
Name: fmt.Sprintf("%s-%d", node.Name, w.clock.Now().Unix()),
Labels: map[string]string{
kube.LabelResourceKind: "Node", // TODO Why node.Kind is nil?
kube.LabelResourceName: node.Name,
kube.LabelScannerName: "kube-bench",
kube.LabelScannerVendor: "aqua",
kube.LabelHistoryLatest: "true",
},
Annotations: map[string]string{
// TODO Make this history limit configurable somehow, e.g. $ starboard kube-bench --history-limit=7
kube.AnnotationHistoryLimit: strconv.Itoa(10),
},
OwnerReferences: []meta.OwnerReference{
{
APIVersion: "v1", // TODO Why node.APIVersion is nil?
Kind: "Node", // TODO Why node.Kind is nil?
Name: node.Name,
UID: node.UID,
Controller: pointer.BoolPtr(false),
},
},
},
Report: report,
})
return
}

func (w *writer) getReportsByNodeName(name string) (reports []starboard.CISKubernetesBenchmark, err error) {
list, err := w.clientset.AquasecurityV1alpha1().CISKubernetesBenchmarks().List(meta.ListOptions{
LabelSelector: labels.Set{
kube.LabelResourceKind: "Node",
kube.LabelResourceName: name,
}.String(),
})
if err != nil {
return
}
reports = list.Items
return
}

func (w *writer) removeHistoryLatestLabel(reports []starboard.CISKubernetesBenchmark) (err error) {
for _, report := range reports {
if value, ok := report.Labels[kube.LabelHistoryLatest]; !ok || value != "true" {
continue
}
clone := report.DeepCopy()
delete(clone.Labels, kube.LabelHistoryLatest)
klog.V(3).Infof("Removing %s label from %s report", kube.LabelHistoryLatest, clone.Name)
_, err = w.clientset.AquasecurityV1alpha1().CISKubernetesBenchmarks().Update(clone)
if err != nil {
return
}
}
return
}

func (w *writer) removeReportsWithHistoryLimitExceeded(reports []starboard.CISKubernetesBenchmark) (err error) {
limit := w.getHistoryLimit(reports)
diff := len(reports) - limit
if diff < 0 {
return
}
for _, r := range reports[0 : diff+1] {
klog.V(3).Infof("Removing %s report which exceeded history limit of %d", r.GetName(), limit)
err = w.clientset.AquasecurityV1alpha1().CISKubernetesBenchmarks().Delete(r.GetName(), &meta.DeleteOptions{
GracePeriodSeconds: pointer.Int64Ptr(0),
})
if err != nil {
return
}
}
return
}

func (w *writer) getHistoryLimit(reports []starboard.CISKubernetesBenchmark) int {
if len(reports) == 0 {
return defaultHistoryLimit
}
latestReport := reports[len(reports)-1]
if value, ok := latestReport.Annotations[kube.AnnotationHistoryLimit]; ok {
limit, err := strconv.Atoi(value)
if err != nil {
klog.V(3).Infof("Error while parsing value %s of %s annotation", value, kube.AnnotationHistoryLimit)
return defaultHistoryLimit
}
return limit
}
return defaultHistoryLimit
}
38 changes: 0 additions & 38 deletions pkg/kubebench/model.go

This file was deleted.

20 changes: 8 additions & 12 deletions pkg/kubebench/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (

"k8s.io/klog"

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

"github.com/aquasecurity/starboard/pkg/kube"
"github.com/aquasecurity/starboard/pkg/kube/pod"
Expand All @@ -19,7 +19,6 @@ import (
"time"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

const (
Expand All @@ -34,20 +33,18 @@ var (
type Scanner struct {
clientset kubernetes.Interface
pods *pod.Manager
converter Converter
}

func NewScanner(config *rest.Config) (*Scanner, error) {
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
func NewScanner(clientset kubernetes.Interface) *Scanner {
return &Scanner{
clientset: clientset,
pods: pod.NewPodManager(clientset),
}, nil
converter: DefaultConverter,
}
}

func (s *Scanner) Scan() (report sec.CISKubernetesBenchmarkReport, node string, err error) {
func (s *Scanner) Scan() (report starboard.CISKubernetesBenchmarkReport, node *core.Node, err error) {
// 1. Prepare descriptor for the Kubernetes Job which will run kube-bench
kubeBenchJob := s.prepareKubeBenchJob()

Expand Down Expand Up @@ -75,8 +72,6 @@ func (s *Scanner) Scan() (report sec.CISKubernetesBenchmarkReport, node string,
return
}

node = kubeBenchPod.Spec.NodeName

// 4. Get kube-bench JSON output from the kube-bench Pod
klog.V(3).Infof("Getting logs for %s container in job: %s/%s", kubeBenchContainerName,
kubeBenchJob.Namespace, kubeBenchJob.Name)
Expand All @@ -90,12 +85,13 @@ func (s *Scanner) Scan() (report sec.CISKubernetesBenchmarkReport, node string,
}()

// 5. Parse the CISBenchmarkReport from the logs Reader
report, err = CISBenchmarkReportFrom(logsReader)
report, err = s.converter.Convert(logsReader)
if err != nil {
err = fmt.Errorf("parsing CIS benchmark report: %w", err)
return
}

node, err = s.clientset.CoreV1().Nodes().Get(kubeBenchPod.Spec.NodeName, meta.GetOptions{})
return
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/kubebench/writer.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package kubebench

import (
sec "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
core "k8s.io/api/core/v1"
)

type Writer interface {
Write(report sec.CISKubernetesBenchmarkReport, node string) error
Write(report starboard.CISKubernetesBenchmarkReport, node *core.Node) error
}

0 comments on commit cc1deb2

Please sign in to comment.