Support control cluster from cli (#1391)

* adding operator CLI to kubescape

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* support http requet for trigger in cluster operator

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* create interface for create request payload

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* logs + go mod update

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* docs

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* add relevant system tests

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* linter corrections

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* code review corrections

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* remove non relevant system tests - after code review corrections

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* PR corrections

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* PR corrections

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* change log

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* remove from examples

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* change log

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

* test correction

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>

---------

Signed-off-by: rcohencyberarmor <rcohen@armosec.io>
Co-authored-by: rcohencyberarmor <rcohen@armosec.io>
This commit is contained in:
rcohencyberarmor
2023-09-27 16:31:04 +03:00
committed by GitHub
parent 3e7a6b516b
commit 884af50c0b
17 changed files with 884 additions and 1 deletions

View File

@@ -38,7 +38,7 @@ on:
BINARY_TESTS:
type: string
required: false
default: '[ "scan_nsa", "scan_mitre", "scan_with_exceptions", "scan_repository", "scan_local_file", "scan_local_glob_files", "scan_local_list_of_files", "scan_nsa_and_submit_to_backend", "scan_mitre_and_submit_to_backend", "scan_local_repository_and_submit_to_backend", "scan_repository_from_url_and_submit_to_backend", "scan_with_exception_to_backend", "scan_with_custom_framework", "scan_customer_configuration", "host_scanner", "scan_compliance_score" ]'
default: '[ "scan_nsa", "scan_mitre", "scan_with_exceptions", "scan_repository", "scan_local_file", "scan_local_glob_files", "scan_local_list_of_files", "scan_nsa_and_submit_to_backend", "scan_mitre_and_submit_to_backend", "scan_local_repository_and_submit_to_backend", "scan_repository_from_url_and_submit_to_backend", "scan_with_exception_to_backend", "scan_with_custom_framework", "scan_customer_configuration", "host_scanner", "scan_compliance_score", "control_cluster_from_CLI_config_scan_exclude_namespaces", "control_cluster_from_CLI_config_scan_include_namespaces", "control_cluster_from_CLI_config_scan_host_scanner_enabled", "control_cluster_from_CLI_config_scan_MITRE_framework", "control_cluster_from_CLI_vulnerabilities_scan_default", "control_cluster_from_CLI_vulnerabilities_scan_include_namespaces" ]'
workflow_call:
inputs:

View File

@@ -0,0 +1,56 @@
package operator
import (
"fmt"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/core"
"github.com/kubescape/kubescape/v2/core/meta"
"github.com/spf13/cobra"
)
var operatorScanConfigExamples = fmt.Sprintf(`
# Run a configuration scan
%[1]s operator scan configurations
`, cautils.ExecName())
func getOperatorScanConfigCmd(ks meta.IKubescape, operatorInfo cautils.OperatorInfo) *cobra.Command {
configCmd := &cobra.Command{
Use: "configurations",
Short: "Trigger configuration scanning from the Kubescape-Operator microservice",
Long: ``,
Example: operatorScanConfigExamples,
Args: func(cmd *cobra.Command, args []string) error {
operatorInfo.Subcommands = append(operatorInfo.Subcommands, "config")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
operatorAdapter, err := core.NewOperatorAdapter(operatorInfo.OperatorScanInfo)
if err != nil {
return err
}
logger.L().Start("Kubescape-Operator Triggering for configuration scanning")
_, err = operatorAdapter.OperatorScan()
if err != nil {
logger.L().StopError("Failed to triggering Kubescape-Operator for configuration scanning", helpers.Error(err))
return err
}
logger.L().StopSuccess("Triggered Kubescape-Operator for configuration scanning")
return nil
},
}
configScanInfo := &cautils.ConfigScanInfo{}
operatorInfo.OperatorScanInfo = configScanInfo
configCmd.PersistentFlags().StringSliceVar(&configScanInfo.IncludedNamespaces, "include-namespaces", nil, "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b")
configCmd.PersistentFlags().StringSliceVar(&configScanInfo.ExcludedNamespaces, "exclude-namespaces", nil, "Namespaces to exclude from scanning. e.g: --exclude-namespaces ns-a,ns-b. Notice, when running with `exclude-namespace` kubescape does not scan cluster-scoped objects.")
configCmd.PersistentFlags().StringSliceVar(&configScanInfo.Frameworks, "frameworks", nil, "Load frameworks for configuration scanning")
configCmd.PersistentFlags().BoolVarP(&configScanInfo.HostScanner, "enable-host-scan", "", false, "Deploy Kubescape host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valuable data from cluster nodes for certain controls. Yaml file: https://github.com/kubescape/kubescape/blob/master/core/pkg/hostsensorutils/hostsensor.yaml")
return configCmd
}

56
cmd/operator/operator.go Normal file
View File

@@ -0,0 +1,56 @@
package operator
import (
"errors"
"fmt"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/meta"
"github.com/spf13/cobra"
)
const (
scanSubCommand string = "scan"
)
var operatorExamples = fmt.Sprintf(`
# Trigger a configuration scan
%[1]s operator scan configurations
# Trigger a vulnerabilities scan
%[1]s operator scan vulnerabilities
`, cautils.ExecName())
func GetOperatorCmd(ks meta.IKubescape) *cobra.Command {
var operatorInfo cautils.OperatorInfo
operatorCmd := &cobra.Command{
Use: "operator",
Short: "The operator is used to communicate with the Kubescape-Operator within the cluster components.",
Long: ``,
Example: operatorExamples,
Args: func(cmd *cobra.Command, args []string) error {
operatorInfo.Subcommands = append(operatorInfo.Subcommands, "operator")
if len(args) < 2 {
return errors.New("For the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above.")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return errors.New("For the operator sub-command, you need to provide at least one additional sub-command. Refer to the examples above.")
}
if args[0] != scanSubCommand {
return errors.New(fmt.Sprintf("For the operator sub-command, only %s is supported. Refer to the examples above.", scanSubCommand))
}
return nil
},
}
operatorCmd.AddCommand(getOperatorScanCmd(ks, operatorInfo))
return operatorCmd
}

45
cmd/operator/scan.go Normal file
View File

@@ -0,0 +1,45 @@
package operator
import (
"errors"
"fmt"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/meta"
"github.com/spf13/cobra"
)
const (
vulnerabilitiesSubCommand string = "vulnerabilities"
configurationsSubCommand string = "configurations"
)
func getOperatorScanCmd(ks meta.IKubescape, operatorInfo cautils.OperatorInfo) *cobra.Command {
operatorCmd := &cobra.Command{
Use: "scan",
Short: "Scan your cluster using the Kubescape-operator within the cluster components",
Long: ``,
Example: operatorExamples,
Args: func(cmd *cobra.Command, args []string) error {
operatorInfo.Subcommands = append(operatorInfo.Subcommands, "scan")
if len(args) < 1 {
return errors.New("for operator scan sub command, you must pass at least 1 more sub commands, see above examples")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("for operator scan sub command, you must pass at least 1 more sub commands, see above examples")
}
if (args[0] != vulnerabilitiesSubCommand) && (args[0] != configurationsSubCommand) {
return errors.New(fmt.Sprintf("For the operator sub-command, only %s and %s are supported. Refer to the examples above.", vulnerabilitiesSubCommand, configurationsSubCommand))
}
return nil
},
}
operatorCmd.AddCommand(getOperatorScanConfigCmd(ks, operatorInfo))
operatorCmd.AddCommand(getOperatorScanVulnerabilitiesCmd(ks, operatorInfo))
return operatorCmd
}

View File

@@ -0,0 +1,56 @@
package operator
import (
"fmt"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/core"
"github.com/kubescape/kubescape/v2/core/meta"
"github.com/spf13/cobra"
)
var operatorScanVulnerabilitiesExamples = fmt.Sprintf(`
# Trigger a vulnerabilities scan
%[1]s operator scan vulnerabilities
`, cautils.ExecName())
func getOperatorScanVulnerabilitiesCmd(ks meta.IKubescape, operatorInfo cautils.OperatorInfo) *cobra.Command {
configCmd := &cobra.Command{
Use: "vulnerabilities",
Short: "Vulnerabilities use for scan your cluster vulnerabilities using Kubescape operator in the in cluster components",
Long: ``,
Example: operatorScanVulnerabilitiesExamples,
Args: func(cmd *cobra.Command, args []string) error {
operatorInfo.Subcommands = append(operatorInfo.Subcommands, "vulnerabilities")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
operatorAdapter, err := core.NewOperatorAdapter(operatorInfo.OperatorScanInfo)
if err != nil {
return err
}
logger.L().Start("Triggering the Kubescape-Operator for vulnerability scanning")
_, err = operatorAdapter.OperatorScan()
if err != nil {
logger.L().StopError("Failed to trigger the Kubescape-Operator for vulnerability scanning", helpers.Error(err))
return err
}
logger.L().StopSuccess("Triggered Kubescape-Operator for vulnerability scanning. View the scanning results once they are ready using the following command: \"kubectl get vulnerabilitysummaries\"")
return err
},
}
vulnerabilitiesScanInfo := &cautils.VulnerabilitiesScanInfo{
ClusterName: k8sinterface.GetContextName(),
}
operatorInfo.OperatorScanInfo = vulnerabilitiesScanInfo
configCmd.PersistentFlags().StringSliceVar(&vulnerabilitiesScanInfo.IncludeNamespaces, "include-namespaces", nil, "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b")
return configCmd
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/kubescape/kubescape/v2/cmd/download"
"github.com/kubescape/kubescape/v2/cmd/fix"
"github.com/kubescape/kubescape/v2/cmd/list"
"github.com/kubescape/kubescape/v2/cmd/operator"
"github.com/kubescape/kubescape/v2/cmd/scan"
"github.com/kubescape/kubescape/v2/cmd/update"
"github.com/kubescape/kubescape/v2/cmd/version"
@@ -94,6 +95,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
rootCmd.AddCommand(config.GetConfigCmd(ks))
rootCmd.AddCommand(update.GetUpdateCmd())
rootCmd.AddCommand(fix.GetFixCmd(ks))
rootCmd.AddCommand(operator.GetOperatorCmd(ks))
// deprecated commands
rootCmd.AddCommand(&cobra.Command{

View File

@@ -0,0 +1,144 @@
package cautils
import (
"errors"
"testing"
"github.com/armosec/armoapi-go/apis"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
utilsmetav1 "github.com/kubescape/opa-utils/httpserver/meta/v1"
"github.com/stretchr/testify/assert"
)
func newFalse() *bool {
f := false
return &f
}
func Test_GetRequestPayload(t *testing.T) {
testCases := []struct {
name string
clusterName string
OperatorScanInfo
result *apis.Commands
}{
{
name: "scan kubescape config",
OperatorScanInfo: &ConfigScanInfo{
ExcludedNamespaces: []string{"1111"},
IncludedNamespaces: []string{"2222"},
HostScanner: false,
Frameworks: []string{"any", "many"},
},
result: &apis.Commands{
Commands: []apis.Command{
{
CommandName: apis.TypeRunKubescape,
Args: map[string]interface{}{
KubescapeScanV1: utilsmetav1.PostScanRequest{
ExcludedNamespaces: []string{"1111"},
IncludeNamespaces: []string{"2222"},
TargetType: apisv1.KindFramework,
TargetNames: []string{"any", "many"},
HostScanner: newFalse(),
},
},
},
},
},
},
{
name: "scan kubescape vulns",
OperatorScanInfo: &VulnerabilitiesScanInfo{
ClusterName: "any",
IncludeNamespaces: []string{""},
},
result: &apis.Commands{
Commands: []apis.Command{
{
CommandName: apis.TypeScanImages,
WildWlid: "wlid://cluster-any",
},
},
},
},
{
name: "scan kubescape vulns with namespace",
OperatorScanInfo: &VulnerabilitiesScanInfo{
ClusterName: "any",
IncludeNamespaces: []string{"123"},
},
result: &apis.Commands{
Commands: []apis.Command{
{
CommandName: apis.TypeScanImages,
WildWlid: "wlid://cluster-any/namespace-123",
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.OperatorScanInfo.GetRequestPayload()
assert.Equal(t, tc.result, result)
})
}
}
func Test_ValidatePayload(t *testing.T) {
testCases := []struct {
name string
clusterName string
OperatorScanInfo
result error
}{
{
name: "ConfigScanInfo first happy case",
OperatorScanInfo: &ConfigScanInfo{
ExcludedNamespaces: []string{"1111"},
IncludedNamespaces: []string{},
HostScanner: false,
Frameworks: []string{"any", "many"},
},
result: nil,
},
{
name: "ConfigScanInfo second happy case",
OperatorScanInfo: &ConfigScanInfo{
ExcludedNamespaces: []string{},
IncludedNamespaces: []string{"1111"},
HostScanner: false,
Frameworks: []string{"any", "many"},
},
result: nil,
},
{
name: "ConfigScanInfo returned error",
OperatorScanInfo: &ConfigScanInfo{
ExcludedNamespaces: []string{"1111"},
IncludedNamespaces: []string{"2222"},
HostScanner: false,
Frameworks: []string{"any", "many"},
},
result: errors.New("invalid arguments: include-namespaces and exclude-namespaces can't pass together to the CLI"),
},
{
name: "VulnerabilitiesScanInfo happy case",
OperatorScanInfo: &VulnerabilitiesScanInfo{
ClusterName: "any",
IncludeNamespaces: []string{""},
},
result: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
payload := tc.OperatorScanInfo.GetRequestPayload()
result := tc.OperatorScanInfo.ValidatePayload(payload)
assert.Equal(t, tc.result, result)
})
}
}

View File

@@ -0,0 +1,107 @@
package cautils
import (
"errors"
"github.com/armosec/armoapi-go/apis"
"github.com/armosec/utils-k8s-go/wlid"
apisv1 "github.com/kubescape/opa-utils/httpserver/apis/v1"
utilsmetav1 "github.com/kubescape/opa-utils/httpserver/meta/v1"
)
type OperatorSubCommand string
const (
ScanCommand OperatorSubCommand = "scan"
ScanConfigCommand OperatorSubCommand = "config"
ScanVulnerabilitiesCommand OperatorSubCommand = "vulnerabilities"
KubescapeScanV1 string = "scanV1"
)
type VulnerabilitiesScanInfo struct {
IncludeNamespaces []string
ClusterName string
}
type ConfigScanInfo struct {
ExcludedNamespaces []string
IncludedNamespaces []string
HostScanner bool
Frameworks []string // Load frameworks for config scan
}
type OperatorInfo struct {
Subcommands []OperatorSubCommand
OperatorScanInfo
}
type OperatorConnector interface {
StartPortForwarder() error
StopPortForwarder()
GetPortForwardLocalhost() string
}
type OperatorScanInfo interface {
GetRequestPayload() *apis.Commands
ValidatePayload(*apis.Commands) error
}
func (v *VulnerabilitiesScanInfo) ValidatePayload(commands *apis.Commands) error {
return nil
}
func (v *VulnerabilitiesScanInfo) GetRequestPayload() *apis.Commands {
var commands []apis.Command
clusterName := v.ClusterName
if len(v.IncludeNamespaces) == 0 {
wildWlid := wlid.GetWLID(clusterName, "", "", "")
command := apis.Command{
CommandName: apis.TypeScanImages,
WildWlid: wildWlid,
}
commands = append(commands, command)
} else {
for i := range v.IncludeNamespaces {
wildWlid := wlid.GetWLID(clusterName, v.IncludeNamespaces[i], "", "")
command := apis.Command{
CommandName: apis.TypeScanImages,
WildWlid: wildWlid,
}
commands = append(commands, command)
}
}
return &apis.Commands{
Commands: commands,
}
}
func (c *ConfigScanInfo) ValidatePayload(commands *apis.Commands) error {
if len(c.IncludedNamespaces) != 0 && len(c.ExcludedNamespaces) != 0 {
return errors.New("invalid arguments: include-namespaces and exclude-namespaces can't pass together to the CLI")
}
return nil
}
func (c *ConfigScanInfo) GetRequestPayload() *apis.Commands {
if len(c.Frameworks) == 0 {
c.Frameworks = append(c.Frameworks, "all")
}
return &apis.Commands{
Commands: []apis.Command{
{
CommandName: apis.TypeRunKubescape,
Args: map[string]interface{}{
KubescapeScanV1: utilsmetav1.PostScanRequest{
ExcludedNamespaces: c.ExcludedNamespaces,
IncludeNamespaces: c.IncludedNamespaces,
TargetType: apisv1.KindFramework,
TargetNames: c.Frameworks,
HostScanner: &c.HostScanner,
},
},
},
},
}
}

View File

@@ -0,0 +1,86 @@
package cautils
import (
"bytes"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/kubescape/k8s-interface/k8sinterface"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
)
const (
DefaultPortForwardPortEnv string = "DEFAULT_PORT_FORWARDER_PORT"
DefaultPortForwardPortValue string = "4444"
)
type portForward struct {
*portforward.PortForwarder
localPort string
stopChan chan struct{}
readyChan chan struct{}
out *bytes.Buffer
errOut *bytes.Buffer
}
func getPortForwardingPort() string {
if port, exist := os.LookupEnv(DefaultPortForwardPortEnv); exist {
return port
}
return DefaultPortForwardPortValue
}
func CreatePortForwarder(k8sClient *k8sinterface.KubernetesApi, pod *v1.Pod, forwardingPort, namespace string) (OperatorConnector, error) {
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, pod.Name)
hostIP := strings.TrimLeft(k8sClient.K8SConfig.Host, "htps:/")
serverURL := &url.URL{Scheme: "https", Path: path, Host: hostIP}
roundTripper, upgrader, err := spdy.RoundTripperFor(k8sClient.K8SConfig)
if err != nil {
return nil, err
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL)
stopChan, readyChan := make(chan struct{}, 1), make(chan struct{})
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
forwarder, err := portforward.NewOnAddresses(dialer, []string{"localhost"}, []string{fmt.Sprintf("%s:%s", getPortForwardingPort(), forwardingPort)}, stopChan, readyChan, out, errOut)
if err != nil {
return nil, err
}
return &portForward{
PortForwarder: forwarder,
localPort: getPortForwardingPort(),
stopChan: stopChan,
readyChan: readyChan,
out: out,
errOut: errOut,
}, nil
}
func (p *portForward) waitForPortForwardReadiness() struct{} {
return <-p.readyChan
}
func (p *portForward) GetPortForwardLocalhost() string {
return "localhost:" + getPortForwardingPort()
}
func (p *portForward) StopPortForwarder() {
p.stopChan <- struct{}{}
}
func (p *portForward) StartPortForwarder() error {
go func() {
p.PortForwarder.ForwardPorts()
}()
p.waitForPortForwardReadiness()
return nil
}

View File

@@ -0,0 +1,134 @@
package cautils
import (
"context"
"testing"
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"
)
type FakeCachedDiscoveryClient struct {
discovery.DiscoveryInterface
Groups []*metav1.APIGroup
Resources []*metav1.APIResourceList
PreferredResources []*metav1.APIResourceList
Invalidations int
}
func Test_getPortForwardingPort(t *testing.T) {
testCases := []struct {
name string
createNewPort bool
port string
expectedPort string
}{
{
name: "test default port",
port: "",
expectedPort: DefaultPortForwardPortValue,
},
{
name: "test set port",
createNewPort: true,
port: "1234",
expectedPort: "1234",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.createNewPort {
t.Setenv(DefaultPortForwardPortEnv, tc.port)
}
assert.Equal(t, tc.expectedPort, getPortForwardingPort())
})
}
}
func Test_CreatePortForwarder(t *testing.T) {
testCases := []struct {
name string
expectedError error
}{
{
name: "test creation",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewSimpleClientset(),
K8SConfig: &rest.Config{
Host: "any",
},
Context: context.TODO(),
}
operatorPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "first",
Labels: map[string]string{
"app": "operator",
},
},
}
createdOperatorPod, err := k8sClient.KubernetesClient.CoreV1().Pods(kubescapeNamespace).Create(k8sClient.Context, &operatorPod, metav1.CreateOptions{})
assert.Equal(t, nil, err)
_, err = CreatePortForwarder(&k8sClient, createdOperatorPod, "1234", "any")
assert.Equal(t, nil, err)
})
}
}
func Test_GetPortForwardLocalhost(t *testing.T) {
testCases := []struct {
name string
port string
result string
}{
{
name: "test creation",
port: "1234",
result: "localhost",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewSimpleClientset(),
K8SConfig: &rest.Config{
Host: "any",
},
Context: context.TODO(),
}
operatorPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "first",
Labels: map[string]string{
"app": "operator",
},
},
}
createdOperatorPod, err := k8sClient.KubernetesClient.CoreV1().Pods(kubescapeNamespace).Create(k8sClient.Context, &operatorPod, metav1.CreateOptions{})
assert.Equal(t, nil, err)
t.Setenv(DefaultPortForwardPortEnv, tc.port)
pf, err := CreatePortForwarder(&k8sClient, createdOperatorPod, "1234", "any")
assert.Equal(t, nil, err)
result := pf.GetPortForwardLocalhost()
assert.Equal(t, tc.result+":"+getPortForwardingPort(), result)
})
}
}

View File

@@ -0,0 +1,99 @@
package core
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/armosec/armoapi-go/apis"
"github.com/armosec/utils-go/httputils"
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/kubescape/kubescape/v2/core/cautils"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
operatorServicePort string = "4002"
operatorTriggerPath string = "v1/triggerAction"
kubescapeNamespace string = "kubescape"
)
type OperatorAdapter struct {
httpPostFunc func(httputils.IHttpClient, string, map[string]string, []byte) (*http.Response, error)
cautils.OperatorScanInfo
cautils.OperatorConnector
}
func getOperatorPod(k8sClient *k8sinterface.KubernetesApi) (*v1.Pod, error) {
listOptions := metav1.ListOptions{
LabelSelector: "app=operator",
}
pods, err := k8sClient.KubernetesClient.CoreV1().Pods(kubescapeNamespace).List(k8sClient.Context, listOptions)
if err != nil {
return nil, err
}
if len(pods.Items) != 1 {
return nil, errors.New("Could not find the Kubescape-Operator chart, please validate that the Kubescape-Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts")
}
return &pods.Items[0], nil
}
func NewOperatorAdapter(scanInfo cautils.OperatorScanInfo) (*OperatorAdapter, error) {
k8sClient := getKubernetesApi()
pod, err := getOperatorPod(k8sClient)
if err != nil {
return nil, err
}
operatorConnector, err := cautils.CreatePortForwarder(k8sClient, pod, operatorServicePort, kubescapeNamespace)
if err != nil {
return nil, err
}
return &OperatorAdapter{
httpPostFunc: httputils.HttpPost,
OperatorScanInfo: scanInfo,
OperatorConnector: operatorConnector,
}, nil
}
func (a *OperatorAdapter) httpPostOperatorScanRequest(body apis.Commands) (string, error) {
reqBody, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("in 'httpPostOperatorScanRequest' failed to json.Marshal, reason: %v", err)
}
err = a.StartPortForwarder()
if err != nil {
return "", err
}
defer a.StopPortForwarder()
urlQuery := url.URL{
Scheme: "http",
Host: a.GetPortForwardLocalhost(),
Path: operatorTriggerPath,
}
resp, err := a.httpPostFunc(http.DefaultClient, urlQuery.String(), map[string]string{"Content-Type": "application/json"}, reqBody)
if err != nil {
return "", err
}
defer resp.Body.Close()
return httputils.HttpRespToString(resp)
}
func (a *OperatorAdapter) OperatorScan() (string, error) {
payload := a.OperatorScanInfo.GetRequestPayload()
if err := a.OperatorScanInfo.ValidatePayload(payload); err != nil {
return "", err
}
res, err := a.httpPostOperatorScanRequest(*payload)
if err != nil {
return "", err
}
return res, nil
}

View File

@@ -0,0 +1,83 @@
package core
import (
"context"
"fmt"
"testing"
"github.com/kubescape/k8s-interface/k8sinterface"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func Test_getOperatorPod(t *testing.T) {
testCases := []struct {
name string
createOperatorPod bool
createAnotherOperatorPodWithSameLabel bool
expectedError error
}{
{
name: "test error no operator exist",
createOperatorPod: false,
createAnotherOperatorPodWithSameLabel: false,
expectedError: fmt.Errorf("Could not find the Kubescape-Operator chart, please validate that the Kubescape-Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
},
{
name: "test error several operators exist",
createOperatorPod: true,
createAnotherOperatorPodWithSameLabel: true,
expectedError: fmt.Errorf("Could not find the Kubescape-Operator chart, please validate that the Kubescape-Operator helm chart is installed and running -> https://github.com/kubescape/helm-charts"),
},
{
name: "test no error",
createOperatorPod: true,
createAnotherOperatorPodWithSameLabel: false,
expectedError: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
k8sClient := k8sinterface.KubernetesApi{
KubernetesClient: fake.NewSimpleClientset(),
Context: context.TODO(),
}
var createdOperatorPod *v1.Pod
if tc.createOperatorPod {
operatorPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "first",
Labels: map[string]string{
"app": "operator",
},
},
}
var err error
createdOperatorPod, err = k8sClient.KubernetesClient.CoreV1().Pods(kubescapeNamespace).Create(k8sClient.Context, &operatorPod, metav1.CreateOptions{})
assert.Equal(t, nil, err)
}
if tc.createAnotherOperatorPodWithSameLabel {
operatorPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "second",
Labels: map[string]string{
"app": "operator",
},
},
}
_, err := k8sClient.KubernetesClient.CoreV1().Pods(kubescapeNamespace).Create(k8sClient.Context, &operatorPod, metav1.CreateOptions{})
assert.Equal(t, nil, err)
}
pod, err := getOperatorPod(&k8sClient)
assert.Equal(t, err, tc.expectedError)
if tc.expectedError == nil {
assert.Equal(t, pod, createdOperatorPod)
}
})
}
}

View File

@@ -116,6 +116,15 @@ _Some documentation on using Kubescape is yet to move here from the [ARMO Platfo
> **Note**
> Kubescape will generate Kubernetes YAML objects using a `kustomize` file and scan them for security.
* Trigger in cluster components for scanning your cluster:
If kubescape helm chart is install in your cluster we can trigger scanning of the in cluster components from the kubescape CLI.
```sh
kubescape operator scan config
```
```sh
kubescape operator scan vulnerabilities
```
* Compliance Score

1
go.mod
View File

@@ -270,6 +270,7 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect

2
go.sum
View File

@@ -1399,6 +1399,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=

View File

@@ -273,6 +273,7 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect

View File

@@ -1405,6 +1405,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=