mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-14 18:09:55 +00:00
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:
@@ -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:
|
||||
|
||||
56
cmd/operator/configscan.go
Normal file
56
cmd/operator/configscan.go
Normal 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
56
cmd/operator/operator.go
Normal 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
45
cmd/operator/scan.go
Normal 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
|
||||
}
|
||||
56
cmd/operator/vulnerabilitiesscan.go
Normal file
56
cmd/operator/vulnerabilitiesscan.go
Normal 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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
144
core/cautils/operartorscaninfo_test.go
Normal file
144
core/cautils/operartorscaninfo_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
107
core/cautils/operatorscaninfo.go
Normal file
107
core/cautils/operatorscaninfo.go
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
86
core/cautils/portforwarder.go
Normal file
86
core/cautils/portforwarder.go
Normal 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
|
||||
}
|
||||
134
core/cautils/portforwarder_test.go
Normal file
134
core/cautils/portforwarder_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
99
core/core/clusterconnector.go
Normal file
99
core/core/clusterconnector.go
Normal 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
|
||||
}
|
||||
83
core/core/clusterconnector_test.go
Normal file
83
core/core/clusterconnector_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user