Skip to content
This repository has been archived by the owner on Oct 1, 2023. It is now read-only.

Update component usage and add action pattern #85

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ go 1.16
require (
github.com/aquasecurity/starboard v0.11.0
github.com/stretchr/testify v1.7.0
github.com/vmware-tanzu/octant v0.23.0
github.com/vmware-tanzu/octant v0.24.0
k8s.io/api v0.21.3
k8s.io/apiextensions-apiserver v0.21.3
k8s.io/apimachinery v0.21.3
k8s.io/cli-runtime v0.21.3
k8s.io/client-go v0.21.3
)
452 changes: 440 additions & 12 deletions go.sum

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions pkg/plugin/actions/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package actions

import (
"context"
"fmt"
"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
"github.com/aquasecurity/starboard/pkg/kube"
"github.com/aquasecurity/starboard/pkg/kubehunter"
"github.com/aquasecurity/starboard/pkg/starboard"
"github.com/vmware-tanzu/octant/pkg/action"
"github.com/vmware-tanzu/octant/pkg/plugin/service"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"os"
"time"
)

const (
StarboardKubeHunterScan = "starboard.octant.dev/scanKubeHunter"

kubeHunterReportName = "cluster"
)

func ActionHandler(request *service.ActionRequest) error {
actionName, err := request.Payload.String("action")
if err != nil {
return err
}

switch actionName {
case StarboardKubeHunterScan:
alert := action.CreateAlert(action.AlertTypeInfo, "Creating kube-hunter report...", time.Second * 15)
request.DashboardClient.SendAlert(request.Context(), request.ClientState.ClientID(), alert)
return startKubeHunterScan(request.Context())
default:
// no-op
}
return nil
}

func startKubeHunterScan(ctx context.Context) error {
configFlags := genericclioptions.NewConfigFlags(true)
// TODO: Since DashboardClient does not provide a RESTConfig, don't scan if kubeconfigs are different
if *configFlags.KubeConfig != os.Getenv("KUBECONFIG") {
return fmt.Errorf("kubeconfig mismatch")
}

restconfig, err := configFlags.ToRESTConfig()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this in general may be a different client config than the one used by octant process. Is that correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a good point and leaves more to be desired in the plugin interface.

A workaround is to see if $KUBECONFIG is set and see if it matches the one from config flags since we can assume Octant looks at the same spot by default via clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarification. I think it's a good start and it should work in most cases. We can also leave a TODO comment explaining when this approach may not work.

if err != nil {
return err
}
kubeClientset, err := kubernetes.NewForConfig(restconfig)
if err != nil {
return err
}
opts := kube.ScannerOpts{ScanJobTimeout: time.Minute}
config, err := starboard.NewConfigManager(kubeClientset, starboard.NamespaceName).Read(ctx)
if err != nil {
return err
}
scanner := kubehunter.NewScanner(starboard.NewScheme(), kubeClientset, config, opts)
report, err := scanner.Scan(ctx)
if err != nil {
return err
}
starboardClientset, err := versioned.NewForConfig(restconfig)
if err != nil {
return err
}
return kubehunter.NewWriter(starboardClientset).Write(ctx, report, kubeHunterReportName)
}
97 changes: 92 additions & 5 deletions pkg/plugin/controller/printers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package controller
import (
"errors"
"fmt"

"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/model"
"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/view/configaudit"
"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/view/kubebench"
Expand All @@ -15,6 +14,7 @@ import (
"github.com/vmware-tanzu/octant/pkg/view/component"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"strconv"
)

// ResourceTabPrinter is called when Octant wants to add new tab for the underlying resource.
Expand Down Expand Up @@ -130,11 +130,98 @@ func ResourcePrinter(request *service.PrintRequest) (plugin.PrintResponse, error
return plugin.PrintResponse{
Status: vulnerabilities.NewSummarySections(summary),
Config: configaudit.NewSummarySections(configAuditSummary),
Items: []component.FlexLayoutItem{
{
Width: component.WidthFull,
View: configaudit.NewReport(workload, configAuditReportsDefined, configAuditReport),
}, nil
}

func ResourceReportTabPrinter(request *service.PrintRequest) (plugin.TabResponse, error) {
if request.Object == nil {
return plugin.TabResponse{}, errors.New("object is nil")
}

workload, err := getWorkloadFromObject(request.Object)
Copy link
Contributor

@danielpacak danielpacak Sep 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generate ConfigAuditReports for a subset of K8s resources (GVKs). For example, we'll never generate it for Nodes because for Nodes we generate CISKubeBenchReports.
That said, I was wondering if we can skip printing a Tab for certain GVKs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking if the object type is a node and returning an empty tab response is one way, but I don't like having that error message.

Going to patch upstream to skip empty tab responses without errors.

https://github.com/vmware-tanzu/octant/blob/5a8648921cc2779eb62a0ac11147f12aa29f831c/pkg/plugin/grpc.go#L557-L559

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty tab response sounds good. We can improve error handling by upgrading Octant whenever a new version is available.

if err != nil {
return plugin.TabResponse{}, err
}

if workload.Kind == kube.KindNode {
return plugin.TabResponse{}, nil
}

repository := model.NewRepository(request.DashboardClient)

_, err = repository.GetCustomResourceDefinitionByName(request.Context(), v1alpha1.ConfigAuditReportCRName)
configAuditReportsDefined := err == nil

var configAuditReport *v1alpha1.ConfigAuditReport
if configAuditReportsDefined {
configAuditReport, err = repository.GetConfigAuditReportByOwner(request.Context(), workload)
if err != nil {
return plugin.TabResponse{}, err
}
}

return plugin.TabResponse{
Tab: component.NewTabWithContents(*configaudit.NewReport(workload, configAuditReportsDefined, configAuditReport)),
}, nil
}

// ResourceObjectStatus is called when Octant wants to determine the status (icon color) of an object
func ResourceObjectStatus(request *service.PrintRequest) (plugin.ObjectStatusResponse, error) {
if request.Object == nil {
return plugin.ObjectStatusResponse{}, errors.New("object is nil")
}

workload, err := getWorkloadFromObject(request.Object)
if err != nil {
return plugin.ObjectStatusResponse{}, err
}

repository := model.NewRepository(request.DashboardClient)

_, err = repository.GetCustomResourceDefinitionByName(request.Context(), v1alpha1.VulnerabilityReportsCRName)
vulnerabilityReportsDefined := err == nil

var summary *v1alpha1.VulnerabilitySummary
if vulnerabilityReportsDefined {
summary, err = repository.GetVulnerabilitiesSummary(request.Context(), workload)
if err != nil {
return plugin.ObjectStatusResponse{}, err
}
}
// summary could be nil due to delays in fetching the CRDs
if summary == nil {
return plugin.ObjectStatusResponse{}, nil
}

status := vulnerabilities.NewSummaryStatus(summary)

return plugin.ObjectStatusResponse{
ObjectStatus: component.PodSummary{
Details: []component.Component{
component.NewLabels(map[string]string{
"Critical Vulnerabilities": strconv.Itoa(summary.CriticalCount),
}),
component.NewLabels(map[string]string{
"High Vulnerabilities": strconv.Itoa(summary.HighCount),
}),
component.NewLabels(map[string]string{
"Medium Vulnerabilities": strconv.Itoa(summary.MediumCount),
}),
component.NewLabels(map[string]string{
"Low Vulnerabilities": strconv.Itoa(summary.LowCount),
}),
component.NewLabels(map[string]string{
"Unknown Vulnerabilities": strconv.Itoa(summary.UnknownCount),
}),
},
Properties: []component.Property{
{Label: "Critical Vulnerabilities", Value: component.NewText(strconv.Itoa(summary.CriticalCount))},
{Label: "High Vulnerabilities", Value: component.NewText(strconv.Itoa(summary.HighCount))},
{Label: "Medium Vulnerabilities", Value: component.NewText(strconv.Itoa(summary.MediumCount))},
{Label: "Low Vulnerabilities", Value: component.NewText(strconv.Itoa(summary.LowCount))},
{Label: "Unknown Vulnerabilities", Value: component.NewText(strconv.Itoa(summary.UnknownCount))},
},
Status: status,
},
}, nil
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/plugin/settings/capabilities.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package settings

import (
"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/actions"
"github.com/vmware-tanzu/octant/pkg/plugin"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Expand Down Expand Up @@ -40,6 +41,18 @@ func GetCapabilities() *plugin.Capabilities {
cronJobGVK,
jobGVK,
},
SupportsObjectStatus: []schema.GroupVersionKind{
podGVK,
deploymentGVK,
daemonSetGVK,
statefulSetGVK,
replicaSetGVK,
replicationControllerGVK,
cronJobGVK,
jobGVK,
nodeGVK,
},
IsModule: true,
ActionNames: []string{actions.StarboardKubeHunterScan},
}
}
5 changes: 4 additions & 1 deletion pkg/plugin/settings/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package settings

import (
"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/actions"
"strings"

"github.com/aquasecurity/starboard-octant-plugin/pkg/plugin/controller"
Expand All @@ -10,8 +11,10 @@ import (

func GetOptions() []service.PluginOption {
return []service.PluginOption{
service.WithTabPrinter(controller.ResourceTabPrinter),
service.WithTabPrinter(controller.ResourceTabPrinter, controller.ResourceReportTabPrinter),
service.WithPrinter(controller.ResourcePrinter),
service.WithObjectStatus(controller.ResourceObjectStatus),
service.WithActionHandler(actions.ActionHandler),
service.WithNavigation(
func(_ *service.NavigationRequest) (nav navigation.Navigation, err error) {
nav = navigation.Navigation{
Expand Down
30 changes: 16 additions & 14 deletions pkg/plugin/view/configaudit/report_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
)

func NewReport(workload kube.Object, isConfigAuditReportsCRDefined bool, report *v1alpha1.ConfigAuditReport) *component.FlexLayout {
flexLayout := component.NewFlexLayout("")
flexLayout := component.NewFlexLayout("Audit Reports")

flexLayout.AddSections(component.FlexLayoutSection{
{
Width: component.WidthFull,
View: component.NewMarkdownText("#### Config Audit Reports"),
View: component.NewMarkdownText("#### Config"),
},
})

Expand Down Expand Up @@ -86,7 +86,7 @@ func NewReport(workload kube.Object, isConfigAuditReportsCRDefined bool, report

var items []component.FlexLayoutItem
items = append(items, component.FlexLayoutItem{
Width: component.WidthThird,
Width: component.WidthHalf,
View: createCardComponent("Pod Template", report.Report.PodChecks),
})

Expand All @@ -100,7 +100,7 @@ func NewReport(workload kube.Object, isConfigAuditReportsCRDefined bool, report

for _, containerName := range containerNames {
items = append(items, component.FlexLayoutItem{
Width: component.WidthThird,
Width: component.WidthHalf,
View: createCardComponent(fmt.Sprintf("Container %s", containerName), report.Report.ContainerChecks[containerName]),
})
}
Expand Down Expand Up @@ -131,25 +131,27 @@ func createChecksTable(checks []v1alpha1.Check) component.Component {
table.Add(tr)
}

sort.Slice(table.Rows(), func(i, j int) bool {
return table.Rows()[i]["ID"].(*component.Text).Config.Status > table.Rows()[j]["ID"].(*component.Text).Config.Status
})

return table
}

func CheckIDWithIcon(check v1alpha1.Check) component.Component {
iconShape := "check-circle"
iconClass := "is-success"

status := component.TextStatusOK
if !check.Success && check.Severity == "warning" {
iconShape = "info-circle"
iconClass = "is-warning"
status = component.TextStatusWarning
}
if !check.Success && check.Severity == "danger" {
iconShape = "exclamation-circle"
iconClass = "is-danger"
status = component.TextStatusError
}

status := component.NewMarkdownText(fmt.Sprintf(`<clr-icon shape="%s" class="is-solid %s"></clr-icon>&nbsp;%s`, iconShape, iconClass, check.ID))
status.EnableTrustedContent()
return status
text := component.NewText(check.ID, func(t *component.Text) {
t.SetStatus(status)
})

return text
}

func NewSummary(report v1alpha1.ConfigAuditReportData) *component.Summary {
Expand Down
23 changes: 15 additions & 8 deletions pkg/plugin/view/configaudit/report_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,54 @@ import (
func TestCheckIDWithIcon(t *testing.T) {
testCases := []struct {
check v1alpha1.Check
expectedMarkdownText string
expectedTextComponent *component.Text
}{
{
check: v1alpha1.Check{
ID: "check-id",
Success: true,
Severity: "warning",
},
expectedMarkdownText: `<clr-icon shape="check-circle" class="is-solid is-success"></clr-icon>&nbsp;check-id`,
expectedTextComponent: component.NewText("check-id", func(t *component.Text) {
t.SetStatus(component.TextStatusOK)
}),
},
{
check: v1alpha1.Check{
ID: "check-id",
Success: false,
Severity: "warning",
},
expectedMarkdownText: `<clr-icon shape="info-circle" class="is-solid is-warning"></clr-icon>&nbsp;check-id`,
expectedTextComponent: component.NewText("check-id", func(t *component.Text) {
t.SetStatus(component.TextStatusWarning)
}),
},
{
check: v1alpha1.Check{
ID: "check-id",
Success: true,
Severity: "danger",
},
expectedMarkdownText: `<clr-icon shape="check-circle" class="is-solid is-success"></clr-icon>&nbsp;check-id`,
expectedTextComponent: component.NewText("check-id", func(t *component.Text) {
t.SetStatus(component.TextStatusOK)
}),
},
{
check: v1alpha1.Check{
ID: "check-id",
Success: false,
Severity: "danger",
},
expectedMarkdownText: `<clr-icon shape="exclamation-circle" class="is-solid is-danger"></clr-icon>&nbsp;check-id`,
expectedTextComponent: component.NewText("check-id", func(t *component.Text) {
t.SetStatus(component.TextStatusError)
}),
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("Should return markdown text component for severity %s and success status %t", tc.check.Severity, tc.check.Success), func(t *testing.T) {
t.Run(fmt.Sprintf("Should return text component for severity %s and success status %t", tc.check.Severity, tc.check.Success), func(t *testing.T) {
c, ok := configaudit.CheckIDWithIcon(tc.check).(*component.Text)
assert.True(t, ok)
assert.True(t, c.TrustedContent())
assert.Equal(t, tc.expectedMarkdownText, c.Config.Text)
assert.Equal(t, tc.expectedTextComponent, c)
})
}
}
Loading