Skip to content

Commit

Permalink
feat: compliance support for cli (#1039)
Browse files Browse the repository at this point in the history
* feat: compliance support for cli

Signed-off-by: chenk <[email protected]>
  • Loading branch information
chen-keinan authored Mar 21, 2022
1 parent 60a5611 commit 1f24058
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 15 deletions.
30 changes: 30 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,19 @@ 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

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

func PoliciesConfigMap() (corev1.ConfigMap, error) {
Expand Down Expand Up @@ -50,6 +59,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 +75,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 +87,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
}
107 changes: 105 additions & 2 deletions itest/starboard/starboard_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@ var _ = Describe("Starboard CLI", func() {
"Scope": Equal(apiextensionsv1beta1.ClusterScoped),
}),
}),
"clustercompliancereports.aquasecurity.github.io": MatchFields(IgnoreExtras, Fields{
"Spec": MatchFields(IgnoreExtras, Fields{
"Group": Equal("aquasecurity.github.io"),
"Version": Equal("v1alpha1"),
"Names": Equal(apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "clustercompliancereports",
Singular: "clustercompliancereport",
ShortNames: []string{"compliance"},
Kind: "ClusterComplianceReport",
ListKind: "ClusterComplianceReportList",
}),
"Scope": Equal(apiextensionsv1beta1.ClusterScoped),
}),
}),
"clustercompliancedetailreports.aquasecurity.github.io": MatchFields(IgnoreExtras, Fields{
"Spec": MatchFields(IgnoreExtras, Fields{
"Group": Equal("aquasecurity.github.io"),
"Version": Equal("v1alpha1"),
"Names": Equal(apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "clustercompliancedetailreports",
Singular: "clustercompliancedetailreport",
ShortNames: []string{"compliancedetail"},
Kind: "ClusterComplianceDetailReport",
ListKind: "ClusterComplianceDetailReportList",
}),
"Scope": Equal(apiextensionsv1beta1.ClusterScoped),
}),
}),
"configauditreports.aquasecurity.github.io": MatchFields(IgnoreExtras, Fields{
"Spec": MatchFields(IgnoreExtras, Fields{
"Group": Equal("aquasecurity.github.io"),
Expand Down Expand Up @@ -173,8 +201,19 @@ var _ = Describe("Starboard CLI", func() {
}, &corev1.ServiceAccount{})
Expect(err).ToNot(HaveOccurred())
})
It("should deploy nsa report", func() {
nsaSpec := &v1alpha1.ClusterComplianceReport{}
err := kubeClient.Get(context.TODO(), types.NamespacedName{
Name: "nsa",
}, nsaSpec)
Expect(err).ToNot(HaveOccurred())
Expect(nsaSpec.Spec.Name == "nsa").To(BeTrue())
Expect(nsaSpec.Spec.Description == "National Security Agency - Kubernetes Hardening Guidance").To(BeTrue())
Expect(nsaSpec.Spec.Cron == "0 */3 * * *").To(BeTrue())
Expect(nsaSpec.Spec.Version == "1.0").To(BeTrue())
Expect(len(nsaSpec.Spec.Controls) == 27).To(BeTrue())
})
})

Describe("Command version", func() {

It("should print the current version of the executable binary", func() {
Expand All @@ -186,7 +225,6 @@ var _ = Describe("Starboard CLI", func() {
Expect(err).ToNot(HaveOccurred())
Eventually(out).Should(Say("Starboard Version: {Version:dev Commit:none Date:unknown}"))
})

})

Describe("Command scan vulnerabilityreports", func() {
Expand Down Expand Up @@ -1234,6 +1272,71 @@ var _ = Describe("Starboard CLI", func() {
})
})

Describe("Command get nsa compliance report", func() {

It("should create nsa compliance report", func() {
// create ciskubebenchreports
err := cmd.Run(versionInfo, []string{
"starboard",
"scan", "ciskubebenchreports",
"-v", starboardCLILogLevel,
}, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())

// create configauditreports
ctx := context.TODO()
object := helper.NewPod().
WithRandomName("nginx-polaris").
WithNamespace(testNamespace.Name).
WithContainer("nginx-container", "nginx:1.16").
Build()
err = kubeClient.Create(ctx, object)
Expect(err).ToNot(HaveOccurred())

err = cmd.Run(versionInfo, []string{
"starboard",
"scan", "configauditreports", "pod" + "/" + object.GetName(),
"--namespace", object.GetNamespace(),
"-v", starboardCLILogLevel,
}, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())

// get cluster compliance report
stdout := NewBuffer()
stderr := NewBuffer()
err = cmd.Run(versionInfo, []string{
"starboard", "get", "clustercompliancereports",
"nsa", "--output", "yaml",
"-v", starboardCLILogLevel,
}, stdout, stderr)
Expect(err).ToNot(HaveOccurred())

var ccr v1alpha1.ClusterComplianceReport
err = yaml.Unmarshal(stdout.Contents(), &ccr)
Expect(err).ToNot(HaveOccurred())
Expect(ccr.Status.Summary.PassCount > 0).To(BeTrue())
Expect(ccr.Status.Summary.FailCount > 0).To(BeTrue())
Expect(len(ccr.Status.ControlChecks) > 0).To(BeTrue())

// get cluster compliance detail report
stdout = NewBuffer()
stderr = NewBuffer()
err = cmd.Run(versionInfo, []string{
"starboard", "get", "clustercompliancereports",
"nsa", "--output", "yaml", "--detail",
"-v", starboardCLILogLevel,
}, stdout, stderr)
Expect(err).ToNot(HaveOccurred())

var ccdr v1alpha1.ClusterComplianceDetailReport
err = yaml.Unmarshal(stdout.Contents(), &ccdr)
Expect(err).ToNot(HaveOccurred())
Expect(ccdr.Report.Summary.PassCount > 0).To(BeTrue())
Expect(ccdr.Report.Summary.FailCount > 0).To(BeTrue())
Expect(len(ccdr.Report.ControlChecks) > 0).To(BeTrue())
})
})

Describe("Command scan kubehunterreports", func() {

BeforeEach(func() {
Expand Down
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
113 changes: 113 additions & 0 deletions pkg/cmd/get_clustercompliancereport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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"
)

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-defined 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 {
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)
}

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
}

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)
}

detail, err := cmd.Flags().GetBool("detail")
if err != nil {
return fmt.Errorf("detail flag is not set correctly, check flag usage: %w", err)
}
if !detail {
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
}

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
}
Loading

0 comments on commit 1f24058

Please sign in to comment.