Skip to content

Commit

Permalink
feat: compliance support for cli
Browse files Browse the repository at this point in the history
Signed-off-by: chenk <hen.keinan@gmail.com>
  • Loading branch information
chen-keinan committed Mar 20, 2022
1 parent 60a5611 commit 78cc55c
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 13 deletions.
31 changes: 31 additions & 0 deletions embedded.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package starboard
import (
_ "embed"

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

corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
Expand All @@ -17,12 +19,20 @@ var (
configAuditReportsCRD []byte
//go:embed deploy/crd/clusterconfigauditreports.crd.yaml
clusterConfigAuditReportsCRD []byte
//go:embed deploy/crd/clustercompliancereports.crd.yaml
clusterComplianceReportsCRD []byte
//go:embed deploy/crd/clustercompliancedetailreports.crd.yaml
clusterComplianceDetailReportsCRD []byte
//go:embed deploy/crd/ciskubebenchreports.crd.yaml
kubeBenchReportsCRD []byte
//go:embed deploy/crd/kubehunterreports.crd.yaml
kubeHunterReportsCRD []byte
//go:embed deploy/static/04-starboard-operator.policies.yaml
policies []byte

// compliance reports
//go:embed deploy/specs/nsa-1.0.yaml
nsaSpecV10 []byte
)

func PoliciesConfigMap() (corev1.ConfigMap, error) {
Expand Down Expand Up @@ -50,6 +60,14 @@ func GetClusterConfigAuditReportsCRD() (apiextensionsv1.CustomResourceDefinition
return getCRDFromBytes(clusterConfigAuditReportsCRD)
}

func GetClusterComplianceReportsCRD() (apiextensionsv1.CustomResourceDefinition, error) {
return getCRDFromBytes(clusterComplianceReportsCRD)
}

func GetClusterComplianceDetailReportsCRD() (apiextensionsv1.CustomResourceDefinition, error) {
return getCRDFromBytes(clusterComplianceDetailReportsCRD)
}

func GetCISKubeBenchReportsCRD() (apiextensionsv1.CustomResourceDefinition, error) {
return getCRDFromBytes(kubeBenchReportsCRD)
}
Expand All @@ -58,6 +76,10 @@ func GetKubeHunterReportsCRD() (apiextensionsv1.CustomResourceDefinition, error)
return getCRDFromBytes(kubeHunterReportsCRD)
}

func GetNsaSpecV10() (v1alpha1.ClusterComplianceReport, error) {
return getComplianceSpec(nsaSpecV10)
}

func getCRDFromBytes(bytes []byte) (apiextensionsv1.CustomResourceDefinition, error) {
var crd apiextensionsv1.CustomResourceDefinition
_, _, err := scheme.Codecs.UniversalDecoder().Decode(bytes, nil, &crd)
Expand All @@ -66,3 +88,12 @@ func getCRDFromBytes(bytes []byte) (apiextensionsv1.CustomResourceDefinition, er
}
return crd, nil
}

func getComplianceSpec(bytes []byte) (v1alpha1.ClusterComplianceReport, error) {
var complianceReport v1alpha1.ClusterComplianceReport
_, _, err := scheme.Codecs.UniversalDecoder().Decode(bytes, nil, &complianceReport)
if err != nil {
return v1alpha1.ClusterComplianceReport{}, err
}
return complianceReport, nil
}
4 changes: 4 additions & 0 deletions pkg/apis/aquasecurity/v1alpha1/compliance_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
ClusterComplianceReportCRName = "clustercompliancereports.aquasecurity.github.io"
)

type ClusterComplianceSummary struct {
PassCount int `json:"passCount"`
FailCount int `json:"failCount"`
Expand Down
4 changes: 4 additions & 0 deletions pkg/apis/aquasecurity/v1alpha1/compliancedetail_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
ClusterComplianceDetailReportCRName = "clustercompliancedetailreports.aquasecurity.github.io"
)

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

Expand Down
14 changes: 14 additions & 0 deletions pkg/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package cmd

import (
"errors"
"fmt"
"strings"
"time"

"k8s.io/apimachinery/pkg/types"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"

Expand Down Expand Up @@ -52,6 +55,17 @@ func WorkloadFromArgs(mapper meta.RESTMapper, namespace string, args []string) (
return
}

func ComplianceNameFromArgs(args []string, suffix ...string) (types.NamespacedName, error) {
if len(args) < 1 {
return types.NamespacedName{}, fmt.Errorf("required compliance name not specified")
}
reportName := args[0]
if len(suffix) > 0 {
reportName = fmt.Sprintf("%s-%s", reportName, suffix[0])
}
return types.NamespacedName{Name: reportName}, nil
}

const (
scanJobTimeoutFlagName = "scan-job-timeout"
deleteScanJobFlagName = "delete-scan-job"
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func NewGetCmd(buildInfo starboard.BuildInfo, cf *genericclioptions.ConfigFlags,
}
getCmd.AddCommand(NewGetVulnerabilityReportsCmd(buildInfo.Executable, cf, outWriter))
getCmd.AddCommand(NewGetConfigAuditReportsCmd(buildInfo.Executable, cf, outWriter))
getCmd.AddCommand(NewGetClusterComplianceReportsCmd(buildInfo.Executable, cf, outWriter))
getCmd.PersistentFlags().StringP("output", "o", "", "Output format. One of yaml|json")

return getCmd
Expand Down
118 changes: 118 additions & 0 deletions pkg/cmd/get_clustercompliancereport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cmd

import (
"context"
"fmt"
"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
"github.com/aquasecurity/starboard/pkg/compliance"
"github.com/aquasecurity/starboard/pkg/starboard"
"github.com/spf13/cobra"
"io"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/genericclioptions"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"strconv"
)

func NewGetClusterComplianceReportsCmd(executable string, cf *genericclioptions.ConfigFlags, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "clustercompliancereports (NAME)",
Aliases: []string{"clustercompliance"},
Short: "Get cluster compliance reports",
Long: `Get cluster compliance report for pre-define spec`,
Example: fmt.Sprintf(`
# Get cluster compliance report for specifc spec in JSON output format
%[1]s get clustercompliancereports nsa -o json
# Get compliance detail report for control checks failure in JSON output format
%[1]s get clustercompliancereports nsa -o json --detail`, executable),
RunE: func(cmd *cobra.Command, args []string) error {
//init client dependencies
logger := ctrl.Log.WithName("reconciler").WithName("clustercompliancereport")
ctx := context.Background()
scheme := starboard.NewScheme()
kubeConfig, err := cf.ToRESTConfig()
if err != nil {
return fmt.Errorf("failed to create kubeConfig: %w", err)
}
// create k8s client
kubeClient, err := client.New(kubeConfig, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create kubernetes client: %w", err)
}
namespaceName, err := ComplianceNameFromArgs(args)
if err != nil {
return err
}
// check report if report with spec exist
var report v1alpha1.ClusterComplianceReport
err = GetComplianceReport(ctx, kubeClient, namespaceName, out, &report)
if err != nil {
return err
}
// generate compliance and compliance failure detail reports
complianceMgr := compliance.NewMgr(kubeClient, logger)
err = complianceMgr.GenerateComplianceReport(ctx, report.Spec)
if err != nil {
return fmt.Errorf("failed to generate report: %w", err)
}

format := cmd.Flag("output").Value.String()
printer, err := genericclioptions.NewPrintFlags("").
WithTypeSetter(scheme).
WithDefaultOutput(format).
ToPrinter()
if err != nil {
return fmt.Errorf("faild to create printer: %w", err)
}
// check report detail flag has been set
detailString := cmd.Flag("detail").Value.String()
detail, err := strconv.ParseBool(detailString)
if err != nil {
detail = false
}
if !detail {
// print cluster compliance report
var complianceReport v1alpha1.ClusterComplianceReport
err := GetComplianceReport(ctx, kubeClient, namespaceName, out, &complianceReport)
if err != nil {
return err
}
if err := printer.PrintObj(&complianceReport, out); err != nil {
return fmt.Errorf("print compliance reports: %w", err)
}
return nil
}
// print cluster compliance failure detail report
detailNamespaceName, err := ComplianceNameFromArgs(args, "details")
if err != nil {
return err
}
var complianceDetailReport v1alpha1.ClusterComplianceDetailReport
err = GetComplianceReport(ctx, kubeClient, detailNamespaceName, out, &complianceDetailReport)
if err != nil {
return err
}
if err := printer.PrintObj(&complianceDetailReport, out); err != nil {
return fmt.Errorf("print compliance reports: %w", err)
}
return nil
},
}
cmd.PersistentFlags().BoolP("detail", "d", false, "Get compliance detail report for control checks failure")
return cmd
}

func GetComplianceReport(ctx context.Context, client client.Client, namespaceName types.NamespacedName, out io.Writer, report client.Object) error {
err := client.Get(ctx, namespaceName, report)
if err != nil {
if errors.IsNotFound(err) {
fmt.Fprintf(out, "No complaince reports found with name: %s .\n", namespaceName.Name)
return err
}
return fmt.Errorf("failed getting report: %w", err)
}
return nil
}
51 changes: 51 additions & 0 deletions pkg/cmd/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"time"

"k8s.io/apimachinery/pkg/types"

embedded "github.com/aquasecurity/starboard"
"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
"github.com/aquasecurity/starboard/pkg/plugin"
Expand Down Expand Up @@ -198,9 +200,34 @@ func (m *Installer) Install(ctx context.Context) error {
if err != nil {
return err
}
clusterComplianceReportsCRD, err := embedded.GetClusterComplianceReportsCRD()
if err != nil {
return err
}
err = m.createOrUpdateCRD(ctx, &clusterComplianceReportsCRD)
if err != nil {
return err
}
clusterComplianceDetailReportsCRD, err := embedded.GetClusterComplianceDetailReportsCRD()
if err != nil {
return err
}
err = m.createOrUpdateCRD(ctx, &clusterComplianceDetailReportsCRD)
if err != nil {
return err
}

// TODO We should wait for CRD statuses and make sure that the names were accepted

// compliance report
clusterComplianceReportSpec, err := embedded.GetNsaSpecV10()
if err != nil {
return err
}
err = m.createOrUpdateComplianceSpec(ctx, clusterComplianceReportSpec)
if err != nil {
return err
}
err = m.createNamespaceIfNotFound(ctx, namespace)
if err != nil {
return err
Expand Down Expand Up @@ -387,6 +414,22 @@ func (m *Installer) createOrUpdateCRD(ctx context.Context, crd *ext.CustomResour
return
}

func (m *Installer) createOrUpdateComplianceSpec(ctx context.Context, spec v1alpha1.ClusterComplianceReport) error {
namespaceName := types.NamespacedName{Name: spec.Spec.Name}
err := m.client.Get(ctx, namespaceName, &spec)
switch {
case err == nil:
klog.V(3).Infof("Updating compliance spec %q", spec.Spec.Name)
deepCopy := spec.DeepCopy()
deepCopy.Spec = spec.Spec
return m.client.Update(ctx, deepCopy)
case errors.IsNotFound(err):
klog.V(3).Infof("Creating compliance spec %q", spec.Spec.Name)
return m.client.Create(ctx, &spec)
}
return nil
}

func (m *Installer) deleteCRD(ctx context.Context, name string) (err error) {
klog.V(3).Infof("Deleting CRD %q", name)
err = m.clientsetext.CustomResourceDefinitions().Delete(ctx, name, metav1.DeleteOptions{})
Expand Down Expand Up @@ -421,6 +464,14 @@ func (m *Installer) Uninstall(ctx context.Context) error {
if err != nil {
return err
}
err = m.deleteCRD(ctx, v1alpha1.ClusterComplianceReportCRName)
if err != nil {
return err
}
err = m.deleteCRD(ctx, v1alpha1.ClusterComplianceDetailReportCRName)
if err != nil {
return err
}
err = m.cleanupRBAC(ctx)
if err != nil {
return err
Expand Down
7 changes: 1 addition & 6 deletions pkg/compliance/clustercompliancereport.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,7 @@ func (r *ClusterComplianceReportReconciler) generateComplianceReport(ctx context
return fmt.Errorf("failed to check report cron expression %w", err)
}
if utils.DurationExceeded(durationToNextGeneration) {
updatedReport, err := r.Mgr.GenerateComplianceReport(ctx, report.Spec)
if err != nil {
return fmt.Errorf("failed to generate new report %w", err)
}
// update compliance report status
return r.Status().Update(ctx, updatedReport)
return r.Mgr.GenerateComplianceReport(ctx, report.Spec)
}
log.V(1).Info("RequeueAfter", "durationToNextGeneration", durationToNextGeneration)
ctrlResult.RequeueAfter = durationToNextGeneration
Expand Down
19 changes: 12 additions & 7 deletions pkg/compliance/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (
)

type Mgr interface {
GenerateComplianceReport(ctx context.Context, spec v1alpha1.ReportSpec) (*v1alpha1.ClusterComplianceReport, error)
GenerateComplianceReport(ctx context.Context, spec v1alpha1.ReportSpec) error
}

func NewMgr(client client.Client, log logr.Logger) Mgr {
Expand All @@ -47,27 +47,32 @@ type specDataMapping struct {
controlIdResources map[string][]string
}

func (w *cm) GenerateComplianceReport(ctx context.Context, spec v1alpha1.ReportSpec) (*v1alpha1.ClusterComplianceReport, error) {
func (w *cm) GenerateComplianceReport(ctx context.Context, spec v1alpha1.ReportSpec) error {
// map specs to key/value map for easy processing
smd := w.populateSpecDataToMaps(spec)
// map compliance scanner to resource data
scannerResourceMap := mapComplianceScannerToResource(w.client, ctx, smd.scannerResourceListNames)
// organized data by check id and it aggregated results
checkIdsToResults, err := w.checkIdsToResults(scannerResourceMap)
if err != nil {
return nil, err
return err
}
// map scanner checks results to control check results
controlChecks := w.controlChecksByScannerChecks(smd, checkIdsToResults)
// find summary totals
st := w.getTotals(controlChecks)
//publish compliance details report
//create cluster compliance details report
err = w.createComplianceDetailReport(ctx, spec, smd, checkIdsToResults, st)
if err != nil {
return nil, err
return err
}
//generate compliance details report
return w.createComplianceReport(ctx, spec, st, controlChecks)
//generate cluster compliance report
updatedReport, err := w.createComplianceReport(ctx, spec, st, controlChecks)
if err != nil {
return err
}
// update compliance report status
return w.client.Status().Update(ctx, updatedReport)

}

Expand Down

0 comments on commit 78cc55c

Please sign in to comment.