Compare commits

...

24 Commits

Author SHA1 Message Date
David Wertenteil
0af0c01ec0 Merge pull request #524 from armosec/dev
Quick fix
2022-05-26 12:21:29 +03:00
David Wertenteil
3ff2b0d6ff Merge pull request #523 from dwertent/master
Support client ID and secret key flags
2022-05-26 11:50:35 +03:00
David Wertenteil
35b2b350a0 print submit error 2022-05-26 11:42:24 +03:00
David Wertenteil
046ea1d79f support secret key and account ID from cmd 2022-05-26 11:01:26 +03:00
Rotem Refael
3081508863 Merge pull request #522 from dwertent/master
Adding http request logs
2022-05-25 17:56:50 +03:00
David Wertenteil
4a757c1bf1 adding logs 2022-05-25 17:26:12 +03:00
David Wertenteil
4f1971a63d Merge pull request #520 from armosec/dev
Extend microservice support
2022-05-24 11:07:24 +03:00
David Wertenteil
dec4bcca00 Merge pull request #521 from dwertent/master
Do not submit results every scan with Prometheus
2022-05-24 10:18:20 +03:00
David Wertenteil
95e68f49f3 do not submit results every scan with Prometheus 2022-05-24 09:33:40 +03:00
rcohencyberarmor
0c84c8f1f3 Merge pull request #518 from dwertent/master
Image vuln data integration
2022-05-23 10:51:31 +03:00
David Wertenteil
56b3239e30 loading from file fallback 2022-05-23 10:10:41 +03:00
David Wertenteil
f8e85941da update loading customer config 2022-05-23 09:47:40 +03:00
David Wertenteil
15081aa9c3 update auth url 2022-05-22 15:45:55 +03:00
David Wertenteil
ac03a2bda3 load data from config.json 2022-05-22 15:21:06 +03:00
rcohencyberarmor
b7ffa22f3a Merge pull request #517 from dwertent/master
httphandler using channel for queueing requests
2022-05-22 11:22:31 +03:00
David Wertenteil
ac5e7069da update readme 2022-05-22 10:59:48 +03:00
David Wertenteil
5a83f38bca send prometheus triggering to queue 2022-05-22 09:57:30 +03:00
David Wertenteil
bad2f54e72 Merge pull request #515 from dwertent/master
fixed triggerd all frameworks
2022-05-18 17:29:55 +03:00
rcohencyberarmor
245200840d Merge pull request #514 from dwertent/master
using Buildx in githubactions
2022-05-18 16:11:47 +03:00
David Wertenteil
c285cb1bcc Merge pull request #513 from dwertent/master
REST api support
2022-05-18 15:16:00 +03:00
Rotem Refael
afb6ea1d9c Merge pull request #507 from armosec/dev
- Adding Lens extension to readme
- Supporting --view flag
2022-05-12 10:26:14 +03:00
David Wertenteil
b3d16875d6 Merge pull request #493 from armosec/dev
Microservice support
2022-05-03 15:14:24 +03:00
Rotem Refael
cf086e6614 Merge pull request #487 from armosec/dev
Remove binary from repo
2022-04-11 17:20:18 +03:00
Rotem Refael
a27d2d41f2 Merge pull request #484 from armosec/dev
CLI improvement release
2022-04-11 15:48:10 +03:00
45 changed files with 536 additions and 212 deletions

View File

@@ -24,7 +24,9 @@ func GetDeleteCmd(ks meta.IKubescape) *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
},
}
deleteCmd.PersistentFlags().StringVarP(&deleteInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
deleteCmd.PersistentFlags().StringVarP(&deleteInfo.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
deleteCmd.PersistentFlags().StringVarP(&deleteInfo.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armo.cloud/docs/authentication")
deleteCmd.PersistentFlags().StringVarP(&deleteInfo.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armo.cloud/docs/authentication")
deleteCmd.AddCommand(getExceptionsCmd(ks, &deleteInfo))

View File

@@ -26,7 +26,7 @@ func getExceptionsCmd(ks meta.IKubescape, deleteInfo *v1.Delete) *cobra.Command
if len(exceptionsNames) == 0 {
logger.L().Fatal("missing exceptions names")
}
if err := ks.DeleteExceptions(&v1.DeleteExceptions{Account: deleteInfo.Account, Exceptions: exceptionsNames}); err != nil {
if err := ks.DeleteExceptions(&v1.DeleteExceptions{Credentials: deleteInfo.Credentials, Exceptions: exceptionsNames}); err != nil {
logger.L().Fatal(err.Error())
}
},

View File

@@ -72,7 +72,10 @@ func GeDownloadCmd(ks meta.IKubescape) *cobra.Command {
return nil
},
}
downloadCmd.PersistentFlags().StringVarP(&downloadInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
downloadCmd.PersistentFlags().StringVarP(&downloadInfo.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
downloadCmd.PersistentFlags().StringVarP(&downloadInfo.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armo.cloud/docs/authentication")
downloadCmd.PersistentFlags().StringVarP(&downloadInfo.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armo.cloud/docs/authentication")
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If not specified, will save in `~/.kubescape/<policy name>.json`")
return downloadCmd

View File

@@ -59,7 +59,9 @@ func GetListCmd(ks meta.IKubescape) *cobra.Command {
return nil
},
}
listCmd.PersistentFlags().StringVar(&listPolicies.Account, "account", "", "Armo portal account ID. Default will load account ID from configMap or config file")
listCmd.PersistentFlags().StringVarP(&listPolicies.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
listCmd.PersistentFlags().StringVarP(&listPolicies.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armo.cloud/docs/authentication")
listCmd.PersistentFlags().StringVarP(&listPolicies.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armo.cloud/docs/authentication")
listCmd.PersistentFlags().StringVar(&listPolicies.Format, "format", "pretty-print", "output format. supported: 'pretty-printer'/'json'")
listCmd.PersistentFlags().BoolVarP(&listPolicies.ListIDs, "id", "", false, "List control ID's instead of controls names")

View File

@@ -48,8 +48,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command {
rootCmd := &cobra.Command{
Use: "kubescape",
Version: cautils.BuildNumber,
Short: "Kubescape is a tool for testing Kubernetes security posture",
Long: `Based on NSA \ MITRE ATT&CK® and other frameworks specifications`,
Short: "Kubescape is a tool for testing Kubernetes security posture. Docs: https://hub.armo.cloud/docs",
Example: ksExamples,
}

View File

@@ -63,7 +63,9 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command {
},
}
scanCmd.PersistentFlags().StringVarP(&scanInfo.Account, "account", "", "", "ARMO portal account ID. Default will load account ID from configMap or config file")
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armo.cloud/docs/authentication")
scanCmd.PersistentFlags().StringVarP(&scanInfo.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armo.cloud/docs/authentication")
scanCmd.PersistentFlags().StringVarP(&scanInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context")
scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal")
scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from ARMO management portal")

View File

@@ -21,7 +21,7 @@ func getExceptionsCmd(ks meta.IKubescape, submitInfo *metav1.Submit) *cobra.Comm
return nil
},
Run: func(cmd *cobra.Command, args []string) {
if err := ks.SubmitExceptions(submitInfo.Account, args[0]); err != nil {
if err := ks.SubmitExceptions(&submitInfo.Credentials, args[0]); err != nil {
logger.L().Fatal(err.Error())
}
},

View File

@@ -27,7 +27,7 @@ func getRBACCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
k8s := k8sinterface.NewKubernetesApi()
// get config
clusterConfig := getTenantConfig(submitInfo.Account, "", k8s)
clusterConfig := getTenantConfig(&submitInfo.Credentials, "", k8s)
if err := clusterConfig.SetTenant(); err != nil {
logger.L().Error("failed setting account ID", helpers.Error(err))
}
@@ -60,9 +60,9 @@ func getKubernetesApi() *k8sinterface.KubernetesApi {
}
return k8sinterface.NewKubernetesApi()
}
func getTenantConfig(Account, clusterName string, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
func getTenantConfig(credentials *cautils.Credentials, clusterName string, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
if !k8sinterface.IsConnectedToCluster() || k8s == nil {
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), Account, clusterName)
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), credentials, clusterName)
}
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), Account, clusterName)
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), credentials, clusterName)
}

View File

@@ -69,7 +69,7 @@ func getResultsCmd(ks meta.IKubescape, submitInfo *v1.Submit) *cobra.Command {
k8s := getKubernetesApi()
// get config
clusterConfig := getTenantConfig(submitInfo.Account, "", k8s)
clusterConfig := getTenantConfig(&submitInfo.Credentials, "", k8s)
if err := clusterConfig.SetTenant(); err != nil {
logger.L().Error("failed setting account ID", helpers.Error(err))
}

View File

@@ -20,7 +20,9 @@ func GetSubmitCmd(ks meta.IKubescape) *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
},
}
submitCmd.PersistentFlags().StringVarP(&submitInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
submitCmd.PersistentFlags().StringVarP(&submitInfo.Credentials.Account, "account", "", "", "Kubescape SaaS account ID. Default will load account ID from cache")
submitCmd.PersistentFlags().StringVarP(&submitInfo.Credentials.ClientID, "client-id", "", "", "Kubescape SaaS client ID. Default will load client ID from cache, read more - https://hub.armo.cloud/docs/authentication")
submitCmd.PersistentFlags().StringVarP(&submitInfo.Credentials.SecretKey, "secret-key", "", "", "Kubescape SaaS secret key. Default will load secret key from cache, read more - https://hub.armo.cloud/docs/authentication")
submitCmd.AddCommand(getExceptionsCmd(ks, &submitInfo))
submitCmd.AddCommand(getResultsCmd(ks, &submitInfo))

View File

@@ -69,7 +69,10 @@ type ITenantConfig interface {
// getters
GetContextName() string
GetAccountID() string
GetTennatEmail() string
GetTenantEmail() string
GetToken() string
GetClientID() string
GetSecretKey() string
GetConfigObj() *ConfigObj
// GetBackendAPI() getter.IBackend
// GenerateURL()
@@ -87,8 +90,7 @@ type LocalConfig struct {
}
func NewLocalConfig(
backendAPI getter.IBackend, customerGUID, clusterName string) *LocalConfig {
var configObj *ConfigObj
backendAPI getter.IBackend, credentials *Credentials, clusterName string) *LocalConfig {
lc := &LocalConfig{
backendAPI: backendAPI,
@@ -96,20 +98,14 @@ func NewLocalConfig(
}
// get from configMap
if existsConfigFile() { // get from file
configObj, _ = loadConfigFromFile()
} else {
configObj = &ConfigObj{}
}
if configObj != nil {
lc.configObj = configObj
}
if customerGUID != "" {
lc.configObj.AccountID = customerGUID // override config customerGUID
loadConfigFromFile(lc.configObj)
}
updateCredentials(lc.configObj, credentials)
if clusterName != "" {
lc.configObj.ClusterName = AdoptClusterName(clusterName) // override config clusterName
}
getAccountFromEnv(lc.configObj)
lc.backendAPI.SetAccountID(lc.configObj.AccountID)
lc.backendAPI.SetClientID(lc.configObj.ClientID)
@@ -119,9 +115,12 @@ func NewLocalConfig(
}
func (lc *LocalConfig) GetConfigObj() *ConfigObj { return lc.configObj }
func (lc *LocalConfig) GetTennatEmail() string { return lc.configObj.CustomerAdminEMail }
func (lc *LocalConfig) GetTenantEmail() string { return lc.configObj.CustomerAdminEMail }
func (lc *LocalConfig) GetAccountID() string { return lc.configObj.AccountID }
func (lc *LocalConfig) GetClientID() string { return lc.configObj.ClientID }
func (lc *LocalConfig) GetSecretKey() string { return lc.configObj.SecretKey }
func (lc *LocalConfig) GetContextName() string { return lc.configObj.ClusterName }
func (lc *LocalConfig) GetToken() string { return lc.configObj.Token }
func (lc *LocalConfig) IsConfigFound() bool { return existsConfigFile() }
func (lc *LocalConfig) SetTenant() error {
@@ -190,8 +189,8 @@ type ClusterConfig struct {
configObj *ConfigObj
}
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend, customerGUID, clusterName string) *ClusterConfig {
var configObj *ConfigObj
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend, credentials *Credentials, clusterName string) *ClusterConfig {
// var configObj *ConfigObj
c := &ClusterConfig{
k8s: k8s,
backendAPI: backendAPI,
@@ -200,23 +199,20 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken
configMapNamespace: getConfigMapNamespace(),
}
// get from configMap
// first, load from configMap
if c.existsConfigMap() {
configObj, _ = c.loadConfigFromConfigMap()
c.loadConfigFromConfigMap()
}
if configObj == nil && existsConfigFile() { // get from file
configObj, _ = loadConfigFromFile()
}
if configObj != nil {
c.configObj = configObj
}
if customerGUID != "" {
c.configObj.AccountID = customerGUID // override config customerGUID
// second, load from file
if existsConfigFile() { // get from file
loadConfigFromFile(c.configObj)
}
updateCredentials(c.configObj, credentials)
if clusterName != "" {
c.configObj.ClusterName = AdoptClusterName(clusterName) // override config clusterName
}
getAccountFromEnv(c.configObj)
if c.configObj.ClusterName == "" {
c.configObj.ClusterName = AdoptClusterName(k8sinterface.GetContextName())
@@ -234,7 +230,10 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken
func (c *ClusterConfig) GetConfigObj() *ConfigObj { return c.configObj }
func (c *ClusterConfig) GetDefaultNS() string { return c.configMapNamespace }
func (c *ClusterConfig) GetAccountID() string { return c.configObj.AccountID }
func (c *ClusterConfig) GetTennatEmail() string { return c.configObj.CustomerAdminEMail }
func (c *ClusterConfig) GetClientID() string { return c.configObj.ClientID }
func (c *ClusterConfig) GetSecretKey() string { return c.configObj.SecretKey }
func (c *ClusterConfig) GetTenantEmail() string { return c.configObj.CustomerAdminEMail }
func (c *ClusterConfig) GetToken() string { return c.configObj.Token }
func (c *ClusterConfig) IsConfigFound() bool { return existsConfigFile() || c.existsConfigMap() }
func (c *ClusterConfig) SetTenant() error {
@@ -282,18 +281,26 @@ func (c *ClusterConfig) ToMapString() map[string]interface{} {
}
return m
}
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
func (c *ClusterConfig) loadConfigFromConfigMap() error {
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
if err != nil {
return nil, err
return err
}
if bData, err := json.Marshal(configMap.Data); err == nil {
return readConfig(bData)
}
return nil, nil
return loadConfigFromData(c.configObj, configMap.Data)
}
func loadConfigFromData(co *ConfigObj, data map[string]string) error {
var e error
if jsonConf, ok := data["config.json"]; ok {
e = readConfig([]byte(jsonConf), co)
}
if bData, err := json.Marshal(data); err == nil {
e = readConfig(bData, co)
}
return e
}
func (c *ClusterConfig) existsConfigMap() bool {
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.configMapNamespace).Get(context.Background(), c.configMapName, metav1.GetOptions{})
// TODO - check if has customerGUID
@@ -411,28 +418,27 @@ func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
}
}
}
func loadConfigFromFile() (*ConfigObj, error) {
func loadConfigFromFile(configObj *ConfigObj) error {
dat, err := os.ReadFile(ConfigFileFullPath())
if err != nil {
return nil, err
return err
}
return readConfig(dat)
return readConfig(dat, configObj)
}
func readConfig(dat []byte) (*ConfigObj, error) {
func readConfig(dat []byte, configObj *ConfigObj) error {
if len(dat) == 0 {
return nil, nil
return nil
}
configObj := &ConfigObj{}
if err := json.Unmarshal(dat, configObj); err != nil {
return nil, err
return err
}
if configObj.AccountID == "" {
configObj.AccountID = configObj.CustomerGUID
}
configObj.CustomerGUID = ""
return configObj, nil
return nil
}
// Check if the customer is submitted
@@ -479,15 +485,34 @@ func getConfigMapNamespace() string {
return "default"
}
func getAccountFromEnv(configObj *ConfigObj) {
func getAccountFromEnv(credentials *Credentials) {
// load from env
if accountID := os.Getenv("KS_ACCOUNT_ID"); accountID != "" {
configObj.AccountID = accountID
if accountID := os.Getenv("KS_ACCOUNT_ID"); credentials.Account != "" && accountID != "" {
credentials.Account = accountID
}
if clientID := os.Getenv("KS_CLIENT_ID"); clientID != "" {
configObj.ClientID = clientID
if clientID := os.Getenv("KS_CLIENT_ID"); credentials.ClientID != "" && clientID != "" {
credentials.ClientID = clientID
}
if secretKey := os.Getenv("KS_SECRET_KEY"); secretKey != "" {
configObj.SecretKey = secretKey
if secretKey := os.Getenv("KS_SECRET_KEY"); credentials.SecretKey != "" && secretKey != "" {
credentials.SecretKey = secretKey
}
}
func updateCredentials(configObj *ConfigObj, credentials *Credentials) {
if credentials == nil {
credentials = &Credentials{}
}
getAccountFromEnv(credentials)
if credentials.Account != "" {
configObj.AccountID = credentials.Account // override config Account
}
if credentials.ClientID != "" {
configObj.ClientID = credentials.ClientID // override config ClientID
}
if credentials.SecretKey != "" {
configObj.SecretKey = credentials.SecretKey // override config SecretKey
}
}

View File

@@ -0,0 +1,193 @@
package cautils
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)
func mockConfigObj() *ConfigObj {
return &ConfigObj{
AccountID: "aaa",
ClientID: "bbb",
SecretKey: "ccc",
ClusterName: "ddd",
CustomerAdminEMail: "ab@cd",
Token: "eee",
}
}
func mockLocalConfig() *LocalConfig {
return &LocalConfig{
backendAPI: nil,
configObj: mockConfigObj(),
}
}
func mockClusterConfig() *ClusterConfig {
return &ClusterConfig{
backendAPI: nil,
configObj: mockConfigObj(),
}
}
func TestConfig(t *testing.T) {
co := mockConfigObj()
cop := ConfigObj{}
assert.NoError(t, json.Unmarshal(co.Config(), &cop))
assert.Equal(t, co.AccountID, cop.AccountID)
assert.Equal(t, co.ClientID, cop.ClientID)
assert.Equal(t, co.SecretKey, cop.SecretKey)
assert.Equal(t, "", cop.ClusterName) // Not copied to bytes
assert.Equal(t, "", cop.CustomerAdminEMail) // Not copied to bytes
assert.Equal(t, "", cop.Token) // Not copied to bytes
}
func TestITenantConfig(t *testing.T) {
var lc ITenantConfig
var c ITenantConfig
lc = mockLocalConfig()
c = mockClusterConfig()
co := mockConfigObj()
// test LocalConfig methods
assert.Equal(t, co.AccountID, lc.GetAccountID())
assert.Equal(t, co.ClientID, lc.GetClientID())
assert.Equal(t, co.SecretKey, lc.GetSecretKey())
assert.Equal(t, co.ClusterName, lc.GetContextName())
assert.Equal(t, co.CustomerAdminEMail, lc.GetTenantEmail())
assert.Equal(t, co.Token, lc.GetToken())
// test ClusterConfig methods
assert.Equal(t, co.AccountID, c.GetAccountID())
assert.Equal(t, co.ClientID, c.GetClientID())
assert.Equal(t, co.SecretKey, c.GetSecretKey())
assert.Equal(t, co.ClusterName, c.GetContextName())
assert.Equal(t, co.CustomerAdminEMail, c.GetTenantEmail())
assert.Equal(t, co.Token, c.GetToken())
}
func TestUpdateConfigData(t *testing.T) {
c := mockClusterConfig()
configMap := &corev1.ConfigMap{}
c.updateConfigData(configMap)
assert.Equal(t, c.GetAccountID(), configMap.Data["accountID"])
assert.Equal(t, c.GetClientID(), configMap.Data["clientID"])
assert.Equal(t, c.GetSecretKey(), configMap.Data["secretKey"])
}
func TestReadConfig(t *testing.T) {
com := mockConfigObj()
co := &ConfigObj{}
b, e := json.Marshal(com)
assert.NoError(t, e)
readConfig(b, co)
assert.Equal(t, com.AccountID, co.AccountID)
assert.Equal(t, com.ClientID, co.ClientID)
assert.Equal(t, com.SecretKey, co.SecretKey)
assert.Equal(t, com.ClusterName, co.ClusterName)
assert.Equal(t, com.CustomerAdminEMail, co.CustomerAdminEMail)
assert.Equal(t, com.Token, co.Token)
}
func TestLoadConfigFromData(t *testing.T) {
// use case: all data is in base config
{
c := mockClusterConfig()
co := mockConfigObj()
configMap := &corev1.ConfigMap{}
c.updateConfigData(configMap)
c.configObj = &ConfigObj{}
loadConfigFromData(c.configObj, configMap.Data)
assert.Equal(t, c.GetAccountID(), co.AccountID)
assert.Equal(t, c.GetClientID(), co.ClientID)
assert.Equal(t, c.GetSecretKey(), co.SecretKey)
assert.Equal(t, c.GetContextName(), co.ClusterName)
assert.Equal(t, c.GetTenantEmail(), co.CustomerAdminEMail)
assert.Equal(t, c.GetToken(), co.Token)
}
// use case: all data is in config.json
{
c := mockClusterConfig()
co := mockConfigObj()
configMap := &corev1.ConfigMap{
Data: make(map[string]string),
}
configMap.Data["config.json"] = string(c.GetConfigObj().Config())
c.configObj = &ConfigObj{}
loadConfigFromData(c.configObj, configMap.Data)
assert.Equal(t, c.GetAccountID(), co.AccountID)
assert.Equal(t, c.GetClientID(), co.ClientID)
assert.Equal(t, c.GetSecretKey(), co.SecretKey)
}
// use case: some data is in config.json
{
c := mockClusterConfig()
configMap := &corev1.ConfigMap{
Data: make(map[string]string),
}
// add to map
configMap.Data["clientID"] = c.configObj.ClientID
configMap.Data["secretKey"] = c.configObj.SecretKey
// delete the content
c.configObj.ClientID = ""
c.configObj.SecretKey = ""
configMap.Data["config.json"] = string(c.GetConfigObj().Config())
loadConfigFromData(c.configObj, configMap.Data)
assert.NotEmpty(t, c.GetAccountID())
assert.NotEmpty(t, c.GetClientID())
assert.NotEmpty(t, c.GetSecretKey())
}
// use case: some data is in config.json
{
c := mockClusterConfig()
configMap := &corev1.ConfigMap{
Data: make(map[string]string),
}
c.configObj.AccountID = "tttt"
// add to map
configMap.Data["accountID"] = mockConfigObj().AccountID
configMap.Data["clientID"] = c.configObj.ClientID
configMap.Data["secretKey"] = c.configObj.SecretKey
// delete the content
c.configObj.ClientID = ""
c.configObj.SecretKey = ""
configMap.Data["config.json"] = string(c.GetConfigObj().Config())
loadConfigFromData(c.configObj, configMap.Data)
assert.Equal(t, mockConfigObj().AccountID, c.GetAccountID())
assert.NotEmpty(t, c.GetClientID())
assert.NotEmpty(t, c.GetSecretKey())
}
}

View File

@@ -147,7 +147,8 @@ func (armoAPI *ArmoAPI) IsLoggedIn() bool { return armoAPI.loggedIn
func (armoAPI *ArmoAPI) GetClientID() string { return armoAPI.clientID }
func (armoAPI *ArmoAPI) GetSecretKey() string { return armoAPI.secretKey }
func (armoAPI *ArmoAPI) GetFrontendURL() string { return armoAPI.feURL }
func (armoAPI *ArmoAPI) GetAPIURL() string { return armoAPI.apiURL }
func (armoAPI *ArmoAPI) GetApiURL() string { return armoAPI.apiURL }
func (armoAPI *ArmoAPI) GetAuthURL() string { return armoAPI.authURL }
func (armoAPI *ArmoAPI) GetReportReceiverURL() string { return armoAPI.erURL }
func (armoAPI *ArmoAPI) SetAccountID(accountID string) { armoAPI.accountID = accountID }
func (armoAPI *ArmoAPI) SetClientID(clientID string) { armoAPI.clientID = clientID }

View File

@@ -9,23 +9,11 @@ import (
"strings"
)
func parseHost(urlObj *url.URL) {
if strings.Contains(urlObj.Host, "http://") {
urlObj.Scheme = "http"
urlObj.Host = strings.Replace(urlObj.Host, "http://", "", 1)
} else {
urlObj.Scheme = "https"
urlObj.Host = strings.Replace(urlObj.Host, "https://", "", 1)
}
}
var NativeFrameworks = []string{"nsa", "mitre", "armobest", "devopsbest"}
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/armoFrameworks"
q := u.Query()
q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack())
@@ -42,9 +30,7 @@ func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
func (armoAPI *ArmoAPI) getListFrameworkURL() string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/armoFrameworks"
q := u.Query()
q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack())
@@ -54,8 +40,7 @@ func (armoAPI *ArmoAPI) getListFrameworkURL() string {
}
func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/armoPostureExceptions"
q := u.Query()
@@ -70,8 +55,7 @@ func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string {
func (armoAPI *ArmoAPI) exceptionsURL(exceptionsPolicyName string) string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/postureExceptionPolicy"
q := u.Query()
@@ -93,8 +77,7 @@ func (armoAPI *ArmoAPI) getAccountConfigDefault(clusterName string) string {
func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/armoCustomerConfiguration"
q := u.Query()
@@ -109,22 +92,21 @@ func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string {
func (armoAPI *ArmoAPI) getAccountURL() string {
u := url.URL{}
u.Host = armoAPI.GetAPIURL()
parseHost(&u)
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/createTenant"
return u.String()
}
func (armoAPI *ArmoAPI) getApiToken() string {
u := url.URL{}
u.Scheme = "https"
u.Path = "frontegg/identity/resources/auth/v1/api-token"
u.Scheme, u.Host = parseHost(armoAPI.GetAuthURL())
u.Path = "identity/resources/auth/v1/api-token"
return u.String()
}
func (armoAPI *ArmoAPI) getOpenidCustomers() string {
u := url.URL{}
u.Scheme = "https"
u.Scheme, u.Host = parseHost(armoAPI.GetApiURL())
u.Path = "api/v1/openid_customers"
return u.String()
}
@@ -183,3 +165,12 @@ func (armoAPI *ArmoAPI) getCustomerGUIDFallBack() string {
}
return "11111111-1111-1111-1111-111111111111"
}
func parseHost(host string) (string, string) {
if strings.HasPrefix(host, "http://") {
return "http", strings.Replace(host, "http://", "", 1)
}
// default scheme
return "https", strings.Replace(host, "https://", "", 1)
}

View File

@@ -8,6 +8,13 @@ type RootInfo struct {
ArmoBEURLs string // armo url
ArmoBEURLsDep string // armo url
}
type Credentials struct {
Account string
ClientID string
SecretKey string
}
// func (rootInfo *RootInfo) InitLogger() {

View File

@@ -107,7 +107,7 @@ type ScanInfo struct {
HostSensorEnabled BoolPtrFlag // Deploy ARMO K8s host scanner to collect data from certain controls
HostSensorYamlPath string // Path to hostsensor file
Local bool // Do not submit results
Account string // account ID
Credentials Credentials // account ID
KubeContext string // context name
FrameworkScan bool // false if scanning control
ScanAll bool // true if scan all frameworks

View File

@@ -8,7 +8,7 @@ import (
func (ks *Kubescape) SetCachedConfig(setConfig *metav1.SetConfig) error {
tenant := getTenantConfig("", "", getKubernetesApi())
tenant := getTenantConfig(nil, "", getKubernetesApi())
if setConfig.Account != "" {
tenant.GetConfigObj().AccountID = setConfig.Account
@@ -25,13 +25,13 @@ func (ks *Kubescape) SetCachedConfig(setConfig *metav1.SetConfig) error {
// View cached configurations
func (ks *Kubescape) ViewCachedConfig(viewConfig *metav1.ViewConfig) error {
tenant := getTenantConfig("", "", getKubernetesApi()) // change k8sinterface
tenant := getTenantConfig(nil, "", getKubernetesApi()) // change k8sinterface
fmt.Fprintf(viewConfig.Writer, "%s\n", tenant.GetConfigObj().Config())
return nil
}
func (ks *Kubescape) DeleteCachedConfig(deleteConfig *metav1.DeleteConfig) error {
tenant := getTenantConfig("", "", getKubernetesApi()) // change k8sinterface
tenant := getTenantConfig(nil, "", getKubernetesApi()) // change k8sinterface
return tenant.DeleteCachedConfig()
}

View File

@@ -12,7 +12,7 @@ import (
func (ks *Kubescape) DeleteExceptions(delExceptions *v1.DeleteExceptions) error {
// load cached config
getTenantConfig(delExceptions.Account, "", getKubernetesApi())
getTenantConfig(&delExceptions.Credentials, "", getKubernetesApi())
// login kubescape SaaS
armoAPI := getter.GetArmoAPIConnector()

View File

@@ -80,7 +80,7 @@ func downloadArtifacts(downloadInfo *metav1.DownloadInfo) error {
}
func downloadConfigInputs(downloadInfo *metav1.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
tenant := getTenantConfig(&downloadInfo.Credentials, "", getKubernetesApi())
controlsInputsGetter := getConfigInputsGetter(downloadInfo.Name, tenant.GetAccountID(), nil)
controlInputs, err := controlsInputsGetter.GetControlsInputs(tenant.GetContextName())
@@ -104,7 +104,7 @@ func downloadConfigInputs(downloadInfo *metav1.DownloadInfo) error {
func downloadExceptions(downloadInfo *metav1.DownloadInfo) error {
var err error
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
tenant := getTenantConfig(&downloadInfo.Credentials, "", getKubernetesApi())
exceptionsGetter := getExceptionsGetter("")
exceptions := []armotypes.PostureExceptionPolicy{}
@@ -128,9 +128,9 @@ func downloadExceptions(downloadInfo *metav1.DownloadInfo) error {
func downloadFramework(downloadInfo *metav1.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
tenant := getTenantConfig(&downloadInfo.Credentials, "", getKubernetesApi())
g := getPolicyGetter(nil, tenant.GetTennatEmail(), true, nil)
g := getPolicyGetter(nil, tenant.GetTenantEmail(), true, nil)
if downloadInfo.Name == "" {
// if framework name not specified - download all frameworks
@@ -170,9 +170,9 @@ func downloadFramework(downloadInfo *metav1.DownloadInfo) error {
func downloadControl(downloadInfo *metav1.DownloadInfo) error {
tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi())
tenant := getTenantConfig(&downloadInfo.Credentials, "", getKubernetesApi())
g := getPolicyGetter(nil, tenant.GetTennatEmail(), false, nil)
g := getPolicyGetter(nil, tenant.GetTenantEmail(), false, nil)
if downloadInfo.Name == "" {
// TODO - support

View File

@@ -23,11 +23,11 @@ func getKubernetesApi() *k8sinterface.KubernetesApi {
}
return k8sinterface.NewKubernetesApi()
}
func getTenantConfig(Account, clusterName string, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
func getTenantConfig(credentials *cautils.Credentials, clusterName string, k8s *k8sinterface.KubernetesApi) cautils.ITenantConfig {
if !k8sinterface.IsConnectedToCluster() || k8s == nil {
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), Account, clusterName)
return cautils.NewLocalConfig(getter.GetArmoAPIConnector(), credentials, clusterName)
}
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), Account, clusterName)
return cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector(), credentials, clusterName)
}
func getExceptionsGetter(useExceptions string) getter.IExceptionsGetter {

View File

@@ -44,16 +44,16 @@ func (ks *Kubescape) List(listPolicies *metav1.ListPolicies) error {
}
func listFrameworks(listPolicies *metav1.ListPolicies) ([]string, error) {
tenant := getTenantConfig(listPolicies.Account, "", getKubernetesApi()) // change k8sinterface
g := getPolicyGetter(nil, tenant.GetTennatEmail(), true, nil)
tenant := getTenantConfig(&listPolicies.Credentials, "", getKubernetesApi()) // change k8sinterface
g := getPolicyGetter(nil, tenant.GetTenantEmail(), true, nil)
return listFrameworksNames(g), nil
}
func listControls(listPolicies *metav1.ListPolicies) ([]string, error) {
tenant := getTenantConfig(listPolicies.Account, "", getKubernetesApi()) // change k8sinterface
tenant := getTenantConfig(&listPolicies.Credentials, "", getKubernetesApi()) // change k8sinterface
g := getPolicyGetter(nil, tenant.GetTennatEmail(), false, nil)
g := getPolicyGetter(nil, tenant.GetTenantEmail(), false, nil)
l := getter.ListName
if listPolicies.ListIDs {
l = getter.ListID
@@ -63,7 +63,7 @@ func listControls(listPolicies *metav1.ListPolicies) ([]string, error) {
func listExceptions(listPolicies *metav1.ListPolicies) ([]string, error) {
// load tenant metav1
getTenantConfig(listPolicies.Account, "", getKubernetesApi())
getTenantConfig(&listPolicies.Credentials, "", getKubernetesApi())
var exceptionsNames []string
armoAPI := getExceptionsGetter("")

View File

@@ -43,7 +43,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces {
// ================== setup tenant object ======================================
tenantConfig := getTenantConfig(scanInfo.Account, scanInfo.KubeContext, k8s)
tenantConfig := getTenantConfig(&scanInfo.Credentials, scanInfo.KubeContext, k8s)
// Set submit behavior AFTER loading tenant config
setSubmitBehavior(scanInfo, tenantConfig)
@@ -124,7 +124,7 @@ func (ks *Kubescape) Scan(scanInfo *cautils.ScanInfo) (*resultshandling.ResultsH
downloadReleasedPolicy := getter.NewDownloadReleasedPolicy() // download config inputs from github release
// set policy getter only after setting the customerGUID
scanInfo.Getters.PolicyGetter = getPolicyGetter(scanInfo.UseFrom, interfaces.tenantConfig.GetTennatEmail(), scanInfo.FrameworkScan, downloadReleasedPolicy)
scanInfo.Getters.PolicyGetter = getPolicyGetter(scanInfo.UseFrom, interfaces.tenantConfig.GetTenantEmail(), scanInfo.FrameworkScan, downloadReleasedPolicy)
scanInfo.Getters.ControlsInputsGetter = getConfigInputsGetter(scanInfo.ControlsInputs, interfaces.tenantConfig.GetAccountID(), downloadReleasedPolicy)
scanInfo.Getters.ExceptionsGetter = getExceptionsGetter(scanInfo.UseExceptions)

View File

@@ -29,11 +29,11 @@ func (ks *Kubescape) Submit(submitInterfaces cliinterfaces.SubmitInterfaces) err
return nil
}
func (ks *Kubescape) SubmitExceptions(accountID, excPath string) error {
func (ks *Kubescape) SubmitExceptions(credentials *cautils.Credentials, excPath string) error {
logger.L().Info("submitting exceptions", helpers.String("path", excPath))
// load cached config
tenantConfig := getTenantConfig(accountID, "", getKubernetesApi())
tenantConfig := getTenantConfig(credentials, "", getKubernetesApi())
if err := tenantConfig.SetTenant(); err != nil {
logger.L().Error("failed setting account ID", helpers.Error(err))
}

View File

@@ -1,6 +1,8 @@
package v1
import "github.com/armosec/kubescape/v2/core/cautils"
type DeleteExceptions struct {
Account string
Exceptions []string
Credentials cautils.Credentials
Exceptions []string
}

View File

@@ -1,9 +1,11 @@
package v1
import "github.com/armosec/kubescape/v2/core/cautils"
type DownloadInfo struct {
Path string // directory to save artifact. Default is "~/.kubescape/"
FileName string // can be empty
Target string // type of artifact to download
Name string // name of artifact to download
Account string // AccountID
Path string // directory to save artifact. Default is "~/.kubescape/"
FileName string // can be empty
Target string // type of artifact to download
Name string // name of artifact to download
Credentials cautils.Credentials
}

View File

@@ -1,10 +1,12 @@
package v1
import "github.com/armosec/kubescape/v2/core/cautils"
type ListPolicies struct {
Target string
ListIDs bool
Account string
Format string
Target string
ListIDs bool
Format string
Credentials cautils.Credentials
}
type ListResponse struct {

View File

@@ -1,9 +1,11 @@
package v1
import "github.com/armosec/kubescape/v2/core/cautils"
type Submit struct {
Account string
Credentials cautils.Credentials
}
type Delete struct {
Account string
Credentials cautils.Credentials
}

View File

@@ -15,8 +15,8 @@ type IKubescape interface {
Download(downloadInfo *metav1.DownloadInfo) error // TODO - return downloaded policies
// submit
Submit(submitInterfaces cliinterfaces.SubmitInterfaces) error // TODO - func should receive object
SubmitExceptions(accountID, excPath string) error // TODO - remove
Submit(submitInterfaces cliinterfaces.SubmitInterfaces) error // TODO - func should receive object
SubmitExceptions(credentials *cautils.Credentials, excPath string) error // TODO - remove
// config
SetCachedConfig(setConfig *metav1.SetConfig) error

View File

@@ -51,7 +51,7 @@ func (armoCivAdaptor *ArmoCivAdaptor) GetImageVulnerability(imageID *registryvul
pageNumber := 1
request := V2ListRequest{PageSize: &pageSize, PageNum: &pageNumber, InnerFilters: filter, OrderBy: "timestamp:desc"}
requestBody, _ := json.Marshal(request)
requestUrl := fmt.Sprintf("https://%s/api/v1/vulnerability/scanResultsDetails?customerGUID=%s", armoCivAdaptor.armoAPI.GetAPIURL(), armoCivAdaptor.armoAPI.GetAccountID())
requestUrl := fmt.Sprintf("https://%s/api/v1/vulnerability/scanResultsDetails?customerGUID=%s", armoCivAdaptor.armoAPI.GetApiURL(), armoCivAdaptor.armoAPI.GetAccountID())
resp, err := armoCivAdaptor.armoAPI.Post(requestUrl, map[string]string{"Content-Type": "application/json"}, requestBody)
if err != nil {
@@ -83,8 +83,7 @@ func (armoCivAdaptor *ArmoCivAdaptor) GetImageVulnerability(imageID *registryvul
}
func (armoCivAdaptor *ArmoCivAdaptor) DescribeAdaptor() string {
// TODO
return ""
return "armo image vulnerabilities scanner, docs: https://hub.armo.cloud/docs/cluster-vulnerability-scanning"
}
func (armoCivAdaptor *ArmoCivAdaptor) GetImagesInformation(imageIDs []registryvulnerabilities.ContainerImageIdentifier) ([]registryvulnerabilities.ContainerImageInformation, error) {

View File

@@ -14,7 +14,7 @@ func (armoCivAdaptor *ArmoCivAdaptor) getImageLastScanId(imageID *registryvulner
pageNumber := 1
request := V2ListRequest{PageSize: &pageSize, PageNum: &pageNumber, InnerFilters: filter, OrderBy: "timestamp:desc"}
requestBody, _ := json.Marshal(request)
requestUrl := fmt.Sprintf("https://%s/api/v1/vulnerability/scanResultsSumSummary?customerGUID=%s", armoCivAdaptor.armoAPI.GetAPIURL(), armoCivAdaptor.armoAPI.GetAccountID())
requestUrl := fmt.Sprintf("https://%s/api/v1/vulnerability/scanResultsSumSummary?customerGUID=%s", armoCivAdaptor.armoAPI.GetApiURL(), armoCivAdaptor.armoAPI.GetAccountID())
resp, err := armoCivAdaptor.armoAPI.Post(requestUrl, map[string]string{"Content-Type": "application/json"}, requestBody)
if err != nil {

View File

@@ -86,9 +86,11 @@ func (k8sHandler *K8sResourceHandler) GetResources(sessionObj *cautils.OPASessio
if len(imgVulnResources) > 0 {
if err := k8sHandler.registryAdaptors.collectImagesVulnerabilities(k8sResourcesMap, allResources, armoResourceMap); err != nil {
logger.L().Warning("failed to collect image vulnerabilities", helpers.Error(err))
}
if isEmptyImgVulns(*armoResourceMap) {
cautils.SetInfoMapForResources("image scanning not configured. For more information: https://hub.armo.cloud/docs/cluster-vulnerability-scanning", imgVulnResources, sessionObj.InfoMap)
cautils.SetInfoMapForResources(fmt.Sprintf("failed to pull image scanning data: %s", err.Error()), imgVulnResources, sessionObj.InfoMap)
} else {
if isEmptyImgVulns(*armoResourceMap) {
cautils.SetInfoMapForResources("image scanning is not configured. for more information: https://hub.armo.cloud/docs/cluster-vulnerability-scanning", imgVulnResources, sessionObj.InfoMap)
}
}
}

View File

@@ -1,12 +1,13 @@
package resourcehandler
import (
"fmt"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/k8s-interface/workloadinterface"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/cautils/getter"
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
armosecadaptorv1 "github.com/armosec/kubescape/v2/core/pkg/registryadaptors/armosec/v1"
"github.com/armosec/kubescape/v2/core/pkg/registryadaptors/registryvulnerabilities"
@@ -45,8 +46,9 @@ func (registryAdaptors *RegistryAdaptors) collectImagesVulnerabilities(k8sResour
for i := range registryAdaptors.adaptors { // login and and get vulnerabilities
if err := registryAdaptors.adaptors[i].Login(); err != nil {
logger.L().Error("failed to login", helpers.Error(err))
continue
if err != nil {
return fmt.Errorf("failed to login, adaptor: '%s', reason: '%s'", registryAdaptors.adaptors[i].DescribeAdaptor(), err.Error())
}
}
vulnerabilities, err := registryAdaptors.adaptors[i].GetImagesVulnerabilities(imagesIdentifiers)
if err != nil {

View File

@@ -57,8 +57,7 @@ func (report *ReportEventReceiver) Submit(opaSessionObj *cautils.OPASessionObj)
if err == nil {
report.generateMessage()
} else {
err = fmt.Errorf("failed to submit scan results. url: '%s'", report.GetURL())
err = fmt.Errorf("failed to submit scan results. url: '%s', reason: %s", report.GetURL(), err.Error())
}
logger.L().Debug("", helpers.String("account ID", report.customerGUID))

View File

@@ -1,26 +1,85 @@
# Kubescape HTTP Handler Package
> This is a beta version, we might make some changes before publishing the official Prometheus support
Running `kubescape` will start up a webserver on port `8080` which will serve the following paths:
Running `kubescape` will start up a webserver on port `8080` which will serve the following API's:
### Trigger scan
* POST `/v1/scan` - Trigger a kubescape scan. The server will return an ID and will execute the scanning asynchronously
* * `wait=true`: scan synchronously (return results and not ID). Use only in small clusters are with an increased timeout. default is `wait=false`
* * `keep=true`: Do not delete results from local storage after returning. default is `keep=false`
* POST `/v1/scan` - trigger a kubescape scan. The server will return an ID and will execute the scanning asynchronously. the request body should look [as followed](#trigger-scan-object).
* * `wait=true`: scan synchronously (return results and not ID). Use only in small clusters or with an increased timeout. default is `wait=false`
* * `keep=true`: do not delete results from local storage after returning. default is `keep=false`
* POST `/v1/metrics` - trigger kubescape for Prometheus support. [read more](examples/prometheus/README.md)
[Response](#response-object):
```
{
"id": <str>, // scan ID
"type": "busy", // response object type
"response": <message:string> // message indicating scanning is still in process
}
```
> When scanning was triggered with the `wait=true` query param, the response is like the [`/v1/results` API](#get-results) response
### Get results
* GET `/v1/results` - Request kubescape scan results
* * query `id=<string>` -> ID returned when triggering the scan action. If empty will return latest results
* * query `keep=true` -> Do not delete results from local storage after returning. default is `keep=false`
* GET `/v1/results` - request kubescape scan results
* * query `id=<string>` -> request results of a specific scan ID. If empty will return latest results
* * query `keep=true` -> keep the results in the local storage after returning. default is `keep=false` - the results will be deleted from local storage after they are returned
[Response](#response-object):
When scanning was done successfully
```
{
"id": <str>, // scan ID
"type": "v1results", // response object type
"response": <object:v1results> // v1 results payload
}
```
When scanning failed
```
{
"id": <str>, // scan ID
"type": "error", // response object type
"response": <error:string> // error string
}
```
When scanning is in progress
```
{
"id": <str>, // scan ID
"type": "busy", // response object type
"response": <message:string> // message indicating scanning is still in process
}
```
### Check scanning progress status
Check the scanning status - is the scanning in progress or done. This is meant for a waiting mechanize since the API does not return the entire results object when the scanning is done
* GET `/v1/status` - Request kubescape scan status
* * query `id=<string>` -> Check status of a specific scan. If empty will check if any scan is in progress
[Response](#response-object):
When scanning is in progress
```
{
"id": <str>, // scan ID
"type": "busy", // response object type
"response": <message:string> // message indicating scanning is still in process
}
```
When scanning is not in progress
```
{
"id": <str>, // scan ID
"type": "notBusy", // response object type
"response": <message:string> // message indicating scanning is done in process
}
```
### Delete cached results
* DELETE `/v1/results` - Delete kubescape scan results from storage. If empty will delete latest results
* * query `id=<string>`: Delete ID of specific results
@@ -32,10 +91,10 @@ Check the scanning status - is the scanning in progress or done. This is meant f
* `/livez` - will respond 200 is server is alive
* `/readyz` - will respond 200 if server can receive requests
## Trigger Kubescape scan
## Objects
### Trigger scan object
POST /v1/scan
body:
```
{
"format": <str>, // results format [default: json] (same as 'kubescape scan --format')
@@ -51,7 +110,8 @@ body:
}
```
Response body:
### Response object
```
{
"id": <str>, // scan ID
@@ -59,10 +119,12 @@ Response body:
"response": <object:interface> // response payload as list of bytes
}
```
#### Response object types
Response body types:
* "v1results" - v1 results object
* "id" - id string
* "busy" - server is busy processing previous requests
* "notBusy" - server is not busy processing previous requests
* "ready" - server is done processing request and results are ready
* "error" - error object
## API Examples

View File

@@ -110,13 +110,4 @@ kubescape_control_count_resources_excluded{name="<control name>",url="<docs url>
kubescape_control_count_resources_passed{name="<control name>",url="<docs url>",severity="<control severity>"} <counter>
```
#### Resources metrics
The resources metrics give you the ability to prioritize fixing the resources by the number of controls that failed
```
# Number of controls that failed for this particular resource
kubescape_resource_count_controls_failed{apiVersion="<>",kind="<>",namespace="<>",name="<>"} <counter>
# Number of controls that where excluded for this particular resource
kubescape_resource_count_controls_excluded{apiVersion="<>",kind="<>",namespace="<>",name="<>"} <counter>
```

View File

@@ -12,6 +12,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0
github.com/stretchr/testify v1.7.1
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
)
require (
@@ -129,7 +130,6 @@ require (
k8s.io/client-go v0.23.5 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/controller-runtime v0.11.1 // indirect
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect

View File

@@ -119,11 +119,9 @@ github.com/armosec/go-git-url v0.0.4 h1:emG9Yfl53rHpuX41fXLD92ehzhRoNSSnGT6Pr7og
github.com/armosec/go-git-url v0.0.4/go.mod h1:PJqdEyJyFxTQvawBcyOM0Ies6+uezire5gpwfr1XX5M=
github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU=
github.com/armosec/k8s-interface v0.0.70/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/k8s-interface v0.0.76 h1:pQaF+8BcNMm6GTYTjdG7vCM1l4BIk7oALXoT6v5gCAk=
github.com/armosec/k8s-interface v0.0.76/go.mod h1:8NX4xWXh8mwW7QyZdZea1czNdM2azCK9BbUNmiZYXW0=
github.com/armosec/opa-utils v0.0.64/go.mod h1:6tQP8UDq2EvEfSqh8vrUdr/9QVSCG4sJfju1SXQOn4c=
github.com/armosec/opa-utils v0.0.139/go.mod h1:VnRVJgDDPFAprGDcibTtKHf9wgkoyTU8wmX2BxEIwok=
github.com/armosec/opa-utils v0.0.140 h1:iv6inb6+D0qgeVkv7f+ZIHpy239IUpAwg6Dau0JAWzg=
github.com/armosec/opa-utils v0.0.140/go.mod h1:Hwm9ZkcW87mB2567WT6mBuSBEzaKowBNfrl3Q0IVsV8=
github.com/armosec/rbac-utils v0.0.1/go.mod h1:pQ8CBiij8kSKV7aeZm9FMvtZN28VgA7LZcYyTWimq40=

View File

@@ -17,7 +17,7 @@ func ToScanInfo(scanRequest *utilsmetav1.PostScanRequest) *cautils.ScanInfo {
setTargetInScanInfo(scanRequest, scanInfo)
if scanRequest.Account != "" {
scanInfo.Account = scanRequest.Account
scanInfo.Credentials.Account = scanRequest.Account
}
if len(scanRequest.ExcludedNamespaces) > 0 {
scanInfo.ExcludedNamespaces = strings.Join(scanRequest.ExcludedNamespaces, ",")

View File

@@ -21,7 +21,7 @@ func TestToScanInfo(t *testing.T) {
TargetNames: []string{"nsa", "mitre"},
}
s := ToScanInfo(req)
assert.Equal(t, "abc", s.Account)
assert.Equal(t, "abc", s.Credentials.Account)
assert.Equal(t, "v2", s.FormatVersion)
assert.Equal(t, "pdf", s.Format)
assert.Equal(t, 2, len(s.PolicyIdentifier))
@@ -72,6 +72,17 @@ func TestSetTargetInScanInfo(t *testing.T) {
assert.True(t, scanInfo.ScanAll)
assert.Equal(t, 0, len(scanInfo.PolicyIdentifier))
}
{
req := &utilsmetav1.PostScanRequest{
TargetType: apisv1.KindFramework,
TargetNames: []string{},
}
scanInfo := &cautils.ScanInfo{}
setTargetInScanInfo(req, scanInfo)
assert.True(t, scanInfo.FrameworkScan)
assert.True(t, scanInfo.ScanAll)
assert.Equal(t, 0, len(scanInfo.PolicyIdentifier))
}
{
req := &utilsmetav1.PostScanRequest{
TargetType: apisv1.KindFramework,

View File

@@ -9,46 +9,57 @@ import (
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
"github.com/armosec/kubescape/v2/core/core"
utilsapisv1 "github.com/armosec/opa-utils/httpserver/apis/v1"
"github.com/google/uuid"
)
// Metrics http listener for prometheus support
func (handler *HTTPHandler) Metrics(w http.ResponseWriter, r *http.Request) {
if handler.state.len() > 0 { // if already scanning the cluster
message := fmt.Sprintf("scan '%s' in action", handler.state.getLatestID())
logger.L().Info("server is busy", helpers.String("message", message), helpers.Time())
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(message))
return
}
scanID := uuid.NewString()
handler.state.setBusy(scanID)
defer handler.state.setNotBusy(scanID)
resultsFile := filepath.Join(OutputDir, scanID)
scanInfo := getPrometheusDefaultScanCommand(scanID, resultsFile)
// trigger scanning
logger.L().Info(scanID, helpers.String("action", "triggering scan"), helpers.Time())
scanParams := &scanRequestParams{
scanQueryParams: &ScanQueryParams{
ReturnResults: true,
KeepResults: false,
},
scanInfo: scanInfo,
scanID: scanID,
}
ks := core.NewKubescape()
results, err := ks.Scan(getPrometheusDefaultScanCommand(scanID, resultsFile))
if err != nil {
handler.scanResponseChan.set(scanID) // add scan to channel
defer handler.scanResponseChan.delete(scanID)
// send to scan queue
logger.L().Info("requesting scan", helpers.String("scanID", scanID), helpers.String("api", "v1/metrics"))
handler.scanRequestChan <- scanParams
// wait for scan to complete
results := <-handler.scanResponseChan.get(scanID)
defer removeResultsFile(scanID) // remove json format results file
defer os.Remove(resultsFile) // remove prometheus format results file
// handle response
if results.Type == utilsapisv1.ErrorScanResponseType {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("failed to complete scan. reason: %s", err.Error())))
w.Write(responseToBytes(results))
return
}
results.HandleResults()
logger.L().Info(scanID, helpers.String("action", "done scanning"), helpers.Time())
// read prometheus format results file
f, err := os.ReadFile(resultsFile)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("failed read results from file. reason: %s", err.Error())))
results.Type = utilsapisv1.ErrorScanResponseType
results.Response = fmt.Sprintf("failed read results from file. reason: %s", err.Error())
w.Write(responseToBytes(results))
return
}
os.Remove(resultsFile)
w.WriteHeader(http.StatusOK)
w.Write(f)
@@ -56,6 +67,8 @@ func (handler *HTTPHandler) Metrics(w http.ResponseWriter, r *http.Request) {
func getPrometheusDefaultScanCommand(scanID, resultsFile string) *cautils.ScanInfo {
scanInfo := defaultScanInfo()
scanInfo.Submit = false // do not submit results every scan
scanInfo.Local = true // do not submit results every scan
scanInfo.FrameworkScan = true
scanInfo.ScanAll = true // scan all frameworks
scanInfo.ScanID = scanID // scan ID

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"sync"
"github.com/armosec/kubescape/v2/core/cautils"
"github.com/armosec/kubescape/v2/core/cautils/logger"
"github.com/armosec/kubescape/v2/core/cautils/logger/helpers"
utilsmetav1 "github.com/armosec/opa-utils/httpserver/meta/v1"
@@ -71,9 +72,9 @@ type StatusQueryParams struct {
// scanRequestParams params passed to channel
type scanRequestParams struct {
scanRequest *utilsmetav1.PostScanRequest // request as received from api
scanQueryParams *ScanQueryParams // request as received from api
scanID string // generated scan ID
scanInfo *cautils.ScanInfo // request as received from api
scanQueryParams *ScanQueryParams // request as received from api
scanID string // generated scan ID
}
func getScanParamsFromRequest(r *http.Request, scanID string) (*scanRequestParams, error) {
@@ -99,9 +100,11 @@ func getScanParamsFromRequest(r *http.Request, scanID string) (*scanRequestParam
return scanRequestParams, fmt.Errorf("failed to parse request payload, reason: %s", err.Error())
}
scanInfo := getScanCommand(scanRequest, scanID)
scanRequestParams.scanID = scanID
scanRequestParams.scanQueryParams = scanQueryParams
scanRequestParams.scanRequest = scanRequest
scanRequestParams.scanInfo = scanInfo
return scanRequestParams, nil
}

View File

@@ -39,9 +39,9 @@ func TestGetScanParamsFromRequest(t *testing.T) {
assert.Equal(t, scanID, req.scanID)
assert.True(t, req.scanQueryParams.KeepResults)
assert.True(t, req.scanQueryParams.ReturnResults)
assert.True(t, *req.scanRequest.HostScanner)
assert.True(t, *req.scanRequest.Submit)
assert.Equal(t, "aaaaaaaaaa", req.scanRequest.Account)
assert.True(t, req.scanInfo.HostSensorEnabled.GetBool())
assert.True(t, req.scanInfo.Submit)
assert.Equal(t, "aaaaaaaaaa", req.scanInfo.Credentials.Account)
}
{
@@ -69,8 +69,8 @@ func TestGetScanParamsFromRequest(t *testing.T) {
assert.Equal(t, scanID, req.scanID)
assert.False(t, req.scanQueryParams.KeepResults)
assert.False(t, req.scanQueryParams.ReturnResults)
assert.False(t, *req.scanRequest.HostScanner)
assert.False(t, *req.scanRequest.Submit)
assert.Equal(t, "aaaaaaaaaa", req.scanRequest.Account)
assert.False(t, req.scanInfo.HostSensorEnabled.GetBool())
assert.False(t, req.scanInfo.Submit)
assert.Equal(t, "aaaaaaaaaa", req.scanInfo.Credentials.Account)
}
}

View File

@@ -48,12 +48,16 @@ func (handler *HTTPHandler) Status(w http.ResponseWriter, r *http.Request) {
statusQueryParams := &StatusQueryParams{}
if err := schema.NewDecoder().Decode(statusQueryParams, r.URL.Query()); err != nil {
w.WriteHeader(http.StatusInternalServerError)
handler.writeError(w, fmt.Errorf("failed to parse query params, reason: %s", err.Error()), "")
return
}
logger.L().Info("requesting status", helpers.String("scanID", statusQueryParams.ScanID), helpers.String("api", "v1/status"))
w.WriteHeader(http.StatusOK)
if !handler.state.isBusy(statusQueryParams.ScanID) {
response.Type = utilsapisv1.NotBusyScanResponseType
logger.L().Debug("status: not busy", helpers.String("ID", statusQueryParams.ScanID))
w.Write(responseToBytes(&response))
return
}
@@ -65,6 +69,8 @@ func (handler *HTTPHandler) Status(w http.ResponseWriter, r *http.Request) {
response.Response = statusQueryParams.ScanID
response.ID = statusQueryParams.ScanID
response.Type = utilsapisv1.BusyScanResponseType
logger.L().Debug("status: busy", helpers.String("ID", statusQueryParams.ScanID))
w.Write(responseToBytes(&response))
}
@@ -102,6 +108,7 @@ func (handler *HTTPHandler) Scan(w http.ResponseWriter, r *http.Request) {
// you must use a goroutine since the executeScan function is not always listening to the channel
go func() {
// send to scanning handler
logger.L().Info("requesting scan", helpers.String("scanID", scanID), helpers.String("api", "v1/scan"))
handler.scanRequestChan <- scanRequestParams
}()
@@ -141,6 +148,7 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) {
handler.writeError(w, fmt.Errorf("failed to parse query params, reason: %s", err.Error()), "")
return
}
logger.L().Info("requesting results", helpers.String("scanID", resultsQueryParams.ScanID), helpers.String("api", "v1/results"), helpers.String("method", r.Method))
if resultsQueryParams.ScanID == "" {
resultsQueryParams.ScanID = handler.state.getLatestID()
@@ -148,8 +156,8 @@ func (handler *HTTPHandler) Results(w http.ResponseWriter, r *http.Request) {
if resultsQueryParams.ScanID == "" { // if no scan found
logger.L().Info("empty scan ID")
w.WriteHeader(http.StatusBadRequest) // Should we return ok?
response.Response = "latest scan not found. trigger again"
w.WriteHeader(http.StatusBadRequest)
response.Response = "latest scan not found"
response.Type = utilsapisv1.ErrorScanResponseType
w.Write(responseToBytes(&response))
return

View File

@@ -11,7 +11,7 @@ import (
func TestDefaultScanInfo(t *testing.T) {
s := defaultScanInfo()
assert.Equal(t, "", s.Account)
assert.Equal(t, "", s.Credentials.Account)
assert.Equal(t, "v2", s.FormatVersion)
assert.Equal(t, "json", s.Format)
assert.False(t, s.HostSensorEnabled.GetBool())
@@ -24,7 +24,7 @@ func TestGetScanCommand(t *testing.T) {
TargetType: apisv1.KindFramework,
}
s := getScanCommand(&req, "abc")
assert.Equal(t, "", s.Account)
assert.Equal(t, "", s.Credentials.Account)
assert.Equal(t, "abc", s.ScanID)
assert.Equal(t, "v2", s.FormatVersion)
assert.Equal(t, "json", s.Format)

View File

@@ -23,10 +23,12 @@ func (handler *HTTPHandler) executeScan() {
for {
scanReq := <-handler.scanRequestChan
logger.L().Info("triggering scan", helpers.String("scanID", scanReq.scanID))
response := &utilsmetav1.Response{}
logger.L().Info("scan triggered", helpers.String("ID", scanReq.scanID))
results, err := scan(scanReq.scanRequest, scanReq.scanID)
results, err := scan(scanReq.scanInfo, scanReq.scanID)
if err != nil {
logger.L().Error("scanning failed", helpers.String("ID", scanReq.scanID), helpers.Error(err))
if scanReq.scanQueryParams.ReturnResults {
@@ -48,8 +50,7 @@ func (handler *HTTPHandler) executeScan() {
}
}
func scan(scanRequest *utilsmetav1.PostScanRequest, scanID string) (*reporthandlingv2.PostureReport, error) {
scanInfo := getScanCommand(scanRequest, scanID)
func scan(scanInfo *cautils.ScanInfo, scanID string) (*reporthandlingv2.PostureReport, error) {
ks := core.NewKubescape()
result, err := ks.Scan(scanInfo)
@@ -136,15 +137,15 @@ func getScanCommand(scanRequest *utilsmetav1.PostScanRequest, scanID string) *ca
func defaultScanInfo() *cautils.ScanInfo {
scanInfo := &cautils.ScanInfo{}
scanInfo.FailThreshold = 100
scanInfo.Account = envToString("KS_ACCOUNT", "") // publish results to Kubescape SaaS
scanInfo.ExcludedNamespaces = envToString("KS_EXCLUDE_NAMESPACES", "") // namespace to exclude
scanInfo.HostSensorYamlPath = envToString("KS_HOST_SCAN_YAML", "") // namespace to exclude
scanInfo.IncludeNamespaces = envToString("KS_INCLUDE_NAMESPACES", "") // namespace to include
scanInfo.Credentials.Account = envToString("KS_ACCOUNT", "") // publish results to Kubescape SaaS
scanInfo.ExcludedNamespaces = envToString("KS_EXCLUDE_NAMESPACES", "") // namespaces to exclude
scanInfo.IncludeNamespaces = envToString("KS_INCLUDE_NAMESPACES", "") // namespaces to include
scanInfo.HostSensorYamlPath = envToString("KS_HOST_SCAN_YAML", "") // path to host scan YAML
scanInfo.FormatVersion = envToString("KS_FORMAT_VERSION", "v2") // output format version
scanInfo.Format = envToString("KS_FORMAT", "json") // default output should be json
scanInfo.Submit = envToBool("KS_SUBMIT", false) // publish results to Kubescape SaaS
scanInfo.HostSensorEnabled.SetBool(envToBool("KS_ENABLE_HOST_SCANNER", false)) // enable host scanner
scanInfo.Local = envToBool("KS_KEEP_LOCAL", false) // do not publish results to Kubescape SaaS
scanInfo.HostSensorEnabled.SetBool(envToBool("KS_ENABLE_HOST_SCANNER", false)) // enable host scanner
if !envToBool("KS_DOWNLOAD_ARTIFACTS", false) {
scanInfo.UseArtifactsFrom = getter.DefaultLocalStore // Load files from cache (this will prevent kubescape fom downloading the artifacts every time)
}