mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-28 16:50:40 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9e5782264 | ||
|
|
6137aa5d8e | ||
|
|
597b967e55 | ||
|
|
679238ec13 | ||
|
|
94884ac3d7 | ||
|
|
0ef8f20c50 | ||
|
|
82f3d62de5 | ||
|
|
46f1e6a83b | ||
|
|
65841a014f | ||
|
|
985c6868c1 | ||
|
|
fca862b2c7 | ||
|
|
3a4a58fdd5 | ||
|
|
a1e639453d | ||
|
|
7da23c111e | ||
|
|
768556251d | ||
|
|
00fcc565b5 | ||
|
|
9c74e5c93b | ||
|
|
6a0ee6e0d7 | ||
|
|
93bb09d78e | ||
|
|
228e7703a8 | ||
|
|
4b15a3b8e0 | ||
|
|
80c5fd7439 | ||
|
|
504c4acc42 | ||
|
|
573d85d770 | ||
|
|
4247f66378 | ||
|
|
7d6a10e787 | ||
|
|
bad303692e | ||
|
|
af3b33f7b0 | ||
|
|
fd66b2eba5 | ||
|
|
157ba1a08d | ||
|
|
6b15e6575b | ||
|
|
53f3229e9f | ||
|
|
186435de69 | ||
|
|
4d027d691f | ||
|
|
3f84ee3fcc | ||
|
|
38103ac90b | ||
|
|
13d27697e1 | ||
|
|
942f356d19 |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -40,9 +40,9 @@ jobs:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Build
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
|
||||
|
||||
- name: Upload Release Asset
|
||||
- name: Upload Release binaries
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
19
README.md
19
README.md
@@ -1,7 +1,6 @@
|
||||
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
|
||||
|
||||
[](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
|
||||
[](https://github.com/armosec/kubescape)
|
||||
[](https://goreportcard.com/report/github.com/armosec/kubescape)
|
||||
|
||||
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
@@ -37,6 +36,9 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
|
||||
| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` |
|
||||
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
|
||||
| `-o`/`--output` | print to stdout | Save scan result in file |
|
||||
| `--use-from` | | Load local framework object from specified path. If not used will download latest |
|
||||
| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` |
|
||||
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json) |
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
@@ -47,30 +49,35 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files before deploying <img src="docs/new-feature.svg">
|
||||
* Scan local `yaml`/`json` files before deploying
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
|
||||
* Scan `yaml`/`json` files from url <img src="docs/new-feature.svg">
|
||||
* Scan `yaml`/`json` files from url
|
||||
```
|
||||
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
|
||||
```
|
||||
|
||||
* Output in `json` format <img src="docs/new-feature.svg">
|
||||
* Output in `json` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
|
||||
```
|
||||
|
||||
* Output in `junit xml` format <img src="docs/new-feature.svg">
|
||||
* Output in `junit xml` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
|
||||
```
|
||||
|
||||
* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` <img src="docs/new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa --exceptions examples/exceptions.json
|
||||
```
|
||||
|
||||
### Helm Support
|
||||
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout <img src="docs/new-feature.svg">
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
```
|
||||
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
169
cautils/customerloader.go
Normal file
169
cautils/customerloader.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
configMapName = "kubescape"
|
||||
configFileName = "config"
|
||||
)
|
||||
|
||||
type ConfigObj struct {
|
||||
CustomerGUID string `json:"customerGUID"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (co *ConfigObj) Json() []byte {
|
||||
if b, err := json.Marshal(co); err == nil {
|
||||
return b
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
type IClusterConfig interface {
|
||||
SetCustomerGUID()
|
||||
GetCustomerGUID()
|
||||
GenerateURL() string
|
||||
}
|
||||
|
||||
type ClusterConfig struct {
|
||||
k8s *k8sinterface.KubernetesApi
|
||||
defaultNS string
|
||||
armoAPI *getter.ArmoAPI
|
||||
configObj *ConfigObj
|
||||
}
|
||||
|
||||
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, armoAPI *getter.ArmoAPI) *ClusterConfig {
|
||||
return &ClusterConfig{
|
||||
k8s: k8s,
|
||||
armoAPI: armoAPI,
|
||||
defaultNS: "default", // TODO - load default namespace from k8s api
|
||||
}
|
||||
}
|
||||
func (c *ClusterConfig) update(configObj *ConfigObj) {
|
||||
c.configObj = configObj
|
||||
ioutil.WriteFile(getter.GetDefaultPath(configFileName+".json"), c.configObj.Json(), 0664)
|
||||
}
|
||||
func (c *ClusterConfig) GenerateURL() string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = getter.ArmoFEURL
|
||||
u.Path = "account/signup"
|
||||
q := u.Query()
|
||||
q.Add("invitationToken", c.configObj.Token)
|
||||
q.Add("customerGUID", c.configObj.CustomerGUID)
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetCustomerGUID() string {
|
||||
return c.configObj.CustomerGUID
|
||||
}
|
||||
func (c *ClusterConfig) SetCustomerGUID() error {
|
||||
|
||||
// get from configMap
|
||||
if configObj, _ := c.loadConfigFromConfigMap(); configObj != nil {
|
||||
c.update(configObj)
|
||||
return nil
|
||||
}
|
||||
|
||||
// get from file
|
||||
if configObj, _ := c.loadConfigFromFile(); configObj != nil {
|
||||
c.update(configObj)
|
||||
c.updateConfigMap()
|
||||
return nil
|
||||
}
|
||||
|
||||
// get from armoBE
|
||||
if tenantResponse, err := c.armoAPI.GetCustomerGUID(); tenantResponse != nil {
|
||||
c.update(&ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token})
|
||||
return c.updateConfigMap()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
|
||||
if c.k8s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bData, err := json.Marshal(configMap.Data); err == nil {
|
||||
return readConfig(bData)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) updateConfigMap() error {
|
||||
if c.k8s == nil {
|
||||
return nil
|
||||
}
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
configMap = &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.updateConfigData(configMap)
|
||||
|
||||
if err != nil {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
} else {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
|
||||
if len(configMap.Data) == 0 {
|
||||
configMap.Data = make(map[string]string)
|
||||
}
|
||||
m := c.ToMapString()
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
configMap.Data[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *ClusterConfig) loadConfigFromFile() (*ConfigObj, error) {
|
||||
dat, err := ioutil.ReadFile(getter.GetDefaultPath(configFileName + ".json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return readConfig(dat)
|
||||
}
|
||||
func readConfig(dat []byte) (*ConfigObj, error) {
|
||||
|
||||
if len(dat) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
configObj := &ConfigObj{}
|
||||
err := json.Unmarshal(dat, configObj)
|
||||
|
||||
return configObj, err
|
||||
}
|
||||
func (c *ClusterConfig) ToMapString() map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
bc, _ := json.Marshal(c.configObj)
|
||||
json.Unmarshal(bc, &m)
|
||||
return m
|
||||
}
|
||||
@@ -21,6 +21,7 @@ func IsSilent() bool {
|
||||
}
|
||||
|
||||
var FailureDisplay = color.New(color.Bold, color.FgHiRed).FprintfFunc()
|
||||
var WarningDisplay = color.New(color.Bold, color.FgCyan).FprintfFunc()
|
||||
var FailureTextDisplay = color.New(color.Faint, color.FgHiRed).FprintfFunc()
|
||||
var InfoDisplay = color.New(color.Bold, color.FgHiYellow).FprintfFunc()
|
||||
var InfoTextDisplay = color.New(color.Faint, color.FgHiYellow).FprintfFunc()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
@@ -13,21 +11,25 @@ import (
|
||||
// =============================================== ArmoAPI ===============================================================
|
||||
// =======================================================================================================================
|
||||
|
||||
const (
|
||||
ArmoBEURL = "eggdashbe.eudev3.cyberarmorsoft.com"
|
||||
ArmoERURL = "report.eudev3.cyberarmorsoft.com"
|
||||
ArmoFEURL = "armoui.eudev3.cyberarmorsoft.com"
|
||||
// ArmoURL = "https://dashbe.euprod1.cyberarmorsoft.com"
|
||||
)
|
||||
|
||||
// Armo API for downloading policies
|
||||
type ArmoAPI struct {
|
||||
httpClient *http.Client
|
||||
hostURL string
|
||||
}
|
||||
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
hostURL: "https://dashbe.eustage2.cyberarmorsoft.com",
|
||||
}
|
||||
}
|
||||
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
armoAPI.setURL(name)
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.hostURL)
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -36,20 +38,43 @@ func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error)
|
||||
if err = JSONDecoder(respStr).Decode(framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
|
||||
|
||||
return framework, err
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) setURL(frameworkName string) {
|
||||
requestURI := "v1/armoFrameworks"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
|
||||
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(frameworkName))
|
||||
requestURI += "&getRules=true"
|
||||
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
exceptions := []armotypes.PostureExceptionPolicy{}
|
||||
if customerGUID == "" {
|
||||
return exceptions, nil
|
||||
}
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
armoAPI.hostURL = urlEncoder(fmt.Sprintf("%s/%s", armoAPI.hostURL, requestURI))
|
||||
if err = JSONDecoder(respStr).Decode(&exceptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exceptions, nil
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetExceptions(scope, customerName, namespace string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
return []armotypes.PostureExceptionPolicy{}, nil
|
||||
func (armoAPI *ArmoAPI) GetCustomerGUID() (*TenantResponse, error) {
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getCustomerURL())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant := &TenantResponse{}
|
||||
if err = JSONDecoder(respStr).Decode(tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
type TenantResponse struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
Token string `json:"token"`
|
||||
Expires string `json:"expires"`
|
||||
}
|
||||
|
||||
44
cautils/getter/armoapiutils.go
Normal file
44
cautils/getter/armoapiutils.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "v1/armoFrameworks"
|
||||
q := u.Query()
|
||||
q.Add("customerGUID", "11111111-1111-1111-1111-111111111111")
|
||||
q.Add("frameworkName", strings.ToUpper(frameworkName))
|
||||
q.Add("getRules", "true")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "api/v1/armoPostureExceptions"
|
||||
|
||||
q := u.Query()
|
||||
q.Add("customerGUID", customerGUID)
|
||||
if clusterName != "" {
|
||||
q.Add("clusterName", clusterName)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getCustomerURL() string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "api/v1/createTenant"
|
||||
return u.String()
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) GetExceptions(policyType, customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
func (drp *DownloadReleasedPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
return []armotypes.PostureExceptionPolicy{}, nil
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framewo
|
||||
return framework, err
|
||||
}
|
||||
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name+".json"))
|
||||
return framework, err
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ import (
|
||||
|
||||
type IPolicyGetter interface {
|
||||
GetFramework(name string) (*opapolicy.Framework, error)
|
||||
GetExceptions(policyType, customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
// GetScores(scope, customerName, namespace string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func GetDefaultPath(frameworkName string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, frameworkName+".json")
|
||||
func GetDefaultPath(name string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, name)
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
defaultfilePath = filepath.Join(homeDir, defaultfilePath)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,14 @@ func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework,
|
||||
return framework, err
|
||||
}
|
||||
|
||||
func (lp *LoadPolicy) GetExceptions(policyType, customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
return []armotypes.PostureExceptionPolicy{}, nil
|
||||
func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
|
||||
exception := []armotypes.PostureExceptionPolicy{}
|
||||
f, err := ioutil.ReadFile(lp.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(f, &exception)
|
||||
return exception, err
|
||||
}
|
||||
|
||||
23
cautils/jsonutils.go
Normal file
23
cautils/jsonutils.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
empty = ""
|
||||
tab = " "
|
||||
)
|
||||
|
||||
func PrettyJson(data interface{}) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent(empty, tab)
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
|
||||
// K8SConfig pointer to k8s config
|
||||
var K8SConfig *restclient.Config
|
||||
var K8SCmdConfig clientcmd.ClientConfig
|
||||
|
||||
// KubernetesApi -
|
||||
type KubernetesApi struct {
|
||||
@@ -51,13 +54,15 @@ var RunningIncluster bool
|
||||
|
||||
// LoadK8sConfig load config from local file or from cluster
|
||||
func LoadK8sConfig() error {
|
||||
|
||||
kubeconfig, err := config.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubernetes config: %s\n", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
return fmt.Errorf("failed to load kubernetes config: %s", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
}
|
||||
if _, err := restclient.InClusterConfig(); err == nil {
|
||||
RunningIncluster = true
|
||||
}
|
||||
|
||||
K8SConfig = kubeconfig
|
||||
return nil
|
||||
}
|
||||
@@ -66,8 +71,7 @@ func LoadK8sConfig() error {
|
||||
func GetK8sConfig() *restclient.Config {
|
||||
if K8SConfig == nil {
|
||||
if err := LoadK8sConfig(); err != nil {
|
||||
// print error
|
||||
fmt.Printf("%s", err.Error())
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +53,26 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
|
||||
func (controlReport *ControlReport) GetNumberOfResources() int {
|
||||
sum := 0
|
||||
for i := range controlReport.RuleReports {
|
||||
if controlReport.RuleReports[i].ListInputResources != nil {
|
||||
sum += len(controlReport.RuleReports[i].ListInputResources)
|
||||
}
|
||||
sum += controlReport.RuleReports[i].GetNumberOfResources()
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) GetNumberOfFailedResources() int {
|
||||
sum := 0
|
||||
for i := range controlReport.RuleReports {
|
||||
sum += controlReport.RuleReports[i].GetNumberOfFailedResources()
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) GetNumberOfWarningResources() int {
|
||||
sum := 0
|
||||
for i := range controlReport.RuleReports {
|
||||
sum += controlReport.RuleReports[i].GetNumberOfWarningResources()
|
||||
}
|
||||
return sum
|
||||
}
|
||||
func (controlReport *ControlReport) ListControlsInputKinds() []string {
|
||||
listControlsInputKinds := []string{}
|
||||
for i := range controlReport.RuleReports {
|
||||
@@ -100,3 +113,86 @@ func (controlReport *ControlReport) Failed() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfResources() int {
|
||||
return len(ruleReport.ListInputResources)
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfFailedResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfWarningResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "warning" {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (postureReport *PostureReport) RemoveData() {
|
||||
for i := range postureReport.FrameworkReports {
|
||||
postureReport.FrameworkReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (frameworkReport *FrameworkReport) RemoveData() {
|
||||
for i := range frameworkReport.ControlReports {
|
||||
frameworkReport.ControlReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (controlReport *ControlReport) RemoveData() {
|
||||
for i := range controlReport.RuleReports {
|
||||
controlReport.RuleReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) RemoveData() {
|
||||
for i := range ruleReport.RuleResponses {
|
||||
ruleReport.RuleResponses[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuleResponse) RemoveData() {
|
||||
r.AlertObject.ExternalObjects = nil
|
||||
|
||||
keepFields := []string{"kind", "apiVersion", "metadata"}
|
||||
keepMetadataFields := []string{"name", "namespace", "labels"}
|
||||
|
||||
for i := range r.AlertObject.K8SApiObjects {
|
||||
deleteFromMap(r.AlertObject.K8SApiObjects[i], keepFields)
|
||||
for k := range r.AlertObject.K8SApiObjects[i] {
|
||||
if k == "metadata" {
|
||||
if b, ok := r.AlertObject.K8SApiObjects[i][k].(map[string]interface{}); ok {
|
||||
deleteFromMap(b, keepMetadataFields)
|
||||
r.AlertObject.K8SApiObjects[i][k] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFromMap(m map[string]interface{}, keepFields []string) {
|
||||
for k := range m {
|
||||
if StringInSlice(keepFields, k) {
|
||||
continue
|
||||
}
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
func StringInSlice(strSlice []string, str string) bool {
|
||||
for i := range strSlice {
|
||||
if strSlice[i] == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ type Getters struct {
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) Init() {
|
||||
// scanInfo.setSilentMode()
|
||||
scanInfo.setUseFrom()
|
||||
scanInfo.setUseExceptions()
|
||||
scanInfo.setOutputFile()
|
||||
@@ -49,7 +48,7 @@ func (scanInfo *ScanInfo) setUseFrom() {
|
||||
return
|
||||
}
|
||||
if scanInfo.UseDefault {
|
||||
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name)
|
||||
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -58,16 +57,7 @@ func (scanInfo *ScanInfo) setGetter() {
|
||||
// load from file
|
||||
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
|
||||
} else {
|
||||
scanInfo.PolicyGetter = getter.NewArmoAPI()
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setSilentMode() {
|
||||
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
|
||||
scanInfo.Silent = true
|
||||
}
|
||||
if scanInfo.Output != "" {
|
||||
scanInfo.Silent = true
|
||||
scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ var downloadCmd = &cobra.Command{
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
downloadInfo.FrameworkName = args[1]
|
||||
g := getter.NewArmoAPI()
|
||||
g := getter.NewDownloadReleasedPolicy()
|
||||
if downloadInfo.Path == "" {
|
||||
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName)
|
||||
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName + ".json")
|
||||
}
|
||||
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,16 +5,20 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/opaprocessor"
|
||||
"github.com/armosec/kubescape/policyhandler"
|
||||
"github.com/armosec/kubescape/printer"
|
||||
"github.com/armosec/kubescape/resultshandling"
|
||||
"github.com/armosec/kubescape/resultshandling/printer"
|
||||
"github.com/armosec/kubescape/resultshandling/reporter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -113,19 +117,34 @@ func CliSetup() error {
|
||||
// policy handler setup
|
||||
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
|
||||
|
||||
// cli handler setup
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
panic(err)
|
||||
// load cluster config
|
||||
clusterConfig := cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
|
||||
if err := clusterConfig.SetCustomerGUID(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
cautils.CustomerGUID = clusterConfig.GetCustomerGUID()
|
||||
cautils.ClusterName = generateClusterName()
|
||||
|
||||
// cli handler setup
|
||||
go func() {
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// processor setup - rego run
|
||||
go func() {
|
||||
reporterObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
|
||||
reporterObj.ProcessRulesListenner()
|
||||
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
|
||||
opaprocessorObj.ProcessRulesListenner()
|
||||
}()
|
||||
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
|
||||
score := p.ActionPrint()
|
||||
|
||||
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
|
||||
score := resultsHandling.HandleResults()
|
||||
|
||||
// print report url
|
||||
fmt.Println(clusterConfig.GenerateURL())
|
||||
|
||||
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
|
||||
if score < adjustedFailThreshold {
|
||||
@@ -153,14 +172,30 @@ func (clihandler *CLIHandler) Scan() error {
|
||||
}
|
||||
switch policyNotification.NotificationType {
|
||||
case opapolicy.TypeExecPostureScan:
|
||||
go func() {
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
//
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateClusterName() string {
|
||||
name := fmt.Sprintf("%d", rand.Int())
|
||||
if k8sinterface.K8SConfig == nil {
|
||||
return name
|
||||
}
|
||||
if k8sinterface.K8SConfig.Host != "" {
|
||||
name = k8sinterface.K8SConfig.Host
|
||||
} else if k8sinterface.K8SConfig.ServerName != "" {
|
||||
name = k8sinterface.K8SConfig.ServerName
|
||||
}
|
||||
|
||||
name = strings.ReplaceAll(name, ".", "-")
|
||||
name = strings.ReplaceAll(name, " ", "-")
|
||||
name = strings.ReplaceAll(name, "https://", "")
|
||||
name = strings.ReplaceAll(name, ":", "-")
|
||||
return name
|
||||
}
|
||||
|
||||
BIN
docs/summary.png
Executable file → Normal file
BIN
docs/summary.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 65 KiB |
34
examples/exceptions.json
Normal file
34
examples/exceptions.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"name": "ignore-kube-namespaces",
|
||||
"policyType": "postureExceptionPolicy",
|
||||
"actions": [
|
||||
"alertOnly"
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-system"
|
||||
}
|
||||
},
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-public"
|
||||
}
|
||||
},
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-node-lease"
|
||||
}
|
||||
}
|
||||
],
|
||||
"posturePolicies": [
|
||||
{
|
||||
"frameworkName": "NSA"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -29,6 +29,9 @@ OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
|
||||
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
|
||||
echo -e "\033[32m[V] Downloaded Kubescape"
|
||||
|
||||
# Ping download counter
|
||||
curl --silent https://us-central1-elated-pottery-310110.cloudfunctions.net/kubescape-download-counter -o /dev/null
|
||||
|
||||
chmod +x $OUTPUT || sudo chmod +x $OUTPUT
|
||||
rm -f /usr/local/bin/$KUBESCAPE_EXEC || sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
|
||||
cp $OUTPUT /usr/local/bin || sudo cp $OUTPUT /usr/local/bin
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy/resources"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
const ScoreConfigPath = "/resources/config"
|
||||
@@ -56,12 +56,6 @@ func NewOPAProcessorHandler(processedPolicy, reportResults *chan *cautils.OPASes
|
||||
}
|
||||
|
||||
func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
|
||||
// recover
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
glog.Errorf("RECOVER in ProcessRulesListenner, reason: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
opaSessionObj := <-*opaHandler.processedPolicy
|
||||
@@ -98,6 +92,7 @@ func (opap *OPAProcessor) Process() error {
|
||||
}
|
||||
|
||||
opap.PostureReport.FrameworkReports = frameworkReports
|
||||
opap.PostureReport.ReportID = uuid.NewV4().String()
|
||||
opap.PostureReport.ReportGenerationTime = time.Now().UTC()
|
||||
// glog.Infof(fmt.Sprintf("Done 'Process'. reportID: %s", opap.PostureReport.ReportID))
|
||||
cautils.StopSpinner()
|
||||
@@ -110,6 +105,7 @@ func (opap *OPAProcessor) processFramework(framework *opapolicy.Framework) (*opa
|
||||
|
||||
frameworkReport := opapolicy.FrameworkReport{}
|
||||
frameworkReport.Name = framework.Name
|
||||
|
||||
controlReports := []opapolicy.ControlReport{}
|
||||
for i := range framework.Controls {
|
||||
controlReport, err := opap.processControl(&framework.Controls[i])
|
||||
|
||||
@@ -41,7 +41,7 @@ func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*op
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "", "")
|
||||
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "")
|
||||
if err != nil {
|
||||
return receivedFramework, nil, err
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
"github.com/enescakir/emoji"
|
||||
@@ -27,19 +26,17 @@ const (
|
||||
)
|
||||
|
||||
type Printer struct {
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
writer *os.File
|
||||
summary Summary
|
||||
sortedControlNames []string
|
||||
printerType string
|
||||
}
|
||||
|
||||
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj, printerType, outputFile string) *Printer {
|
||||
func NewPrinter(printerType, outputFile string) *Printer {
|
||||
return &Printer{
|
||||
opaSessionObj: opaSessionObj,
|
||||
summary: NewSummary(),
|
||||
printerType: printerType,
|
||||
writer: getWriter(outputFile),
|
||||
summary: NewSummary(),
|
||||
writer: getWriter(outputFile),
|
||||
printerType: printerType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,45 +60,39 @@ func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
|
||||
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
|
||||
}
|
||||
|
||||
func (printer *Printer) ActionPrint() float32 {
|
||||
func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 {
|
||||
var score float32
|
||||
for {
|
||||
opaSessionObj := <-*printer.opaSessionObj
|
||||
if printer.printerType == PrettyPrinter {
|
||||
printer.SummarySetup(opaSessionObj.PostureReport)
|
||||
printer.PrintResults()
|
||||
printer.PrintSummaryTable()
|
||||
} else if printer.printerType == JsonPrinter {
|
||||
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
printer.writer.Write(postureReportStr)
|
||||
} else if printer.printerType == JunitResultPrinter {
|
||||
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
postureReportStr, err := xml.Marshal(junitResult)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
printer.writer.Write(postureReportStr)
|
||||
} else if !cautils.IsSilent() {
|
||||
fmt.Println("unknown output printer")
|
||||
|
||||
if printer.printerType == PrettyPrinter {
|
||||
printer.SummarySetup(opaSessionObj.PostureReport)
|
||||
printer.PrintResults()
|
||||
printer.PrintSummaryTable()
|
||||
} else if printer.printerType == JsonPrinter {
|
||||
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
score = calculatePostureScore(opaSessionObj.PostureReport)
|
||||
|
||||
if !k8sinterface.RunningIncluster {
|
||||
break
|
||||
printer.writer.Write(postureReportStr)
|
||||
} else if printer.printerType == JunitResultPrinter {
|
||||
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
postureReportStr, err := xml.Marshal(junitResult)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to convert posture report object!")
|
||||
os.Exit(1)
|
||||
}
|
||||
printer.writer.Write(postureReportStr)
|
||||
} else if !cautils.IsSilent() {
|
||||
fmt.Println("unknown output printer")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
score = calculatePostureScore(opaSessionObj.PostureReport)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
@@ -116,7 +107,8 @@ func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
|
||||
|
||||
printer.summary[cr.Name] = ControlSummary{
|
||||
TotalResources: cr.GetNumberOfResources(),
|
||||
TotalFailed: len(workloadsSummary),
|
||||
TotalFailed: cr.GetNumberOfFailedResources(),
|
||||
TotalWarnign: cr.GetNumberOfWarningResources(),
|
||||
WorkloadSummary: mapResources,
|
||||
Description: cr.Description,
|
||||
Remediation: cr.Remediation,
|
||||
@@ -125,9 +117,7 @@ func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
|
||||
}
|
||||
}
|
||||
printer.sortedControlNames = printer.getSortedControlsNames()
|
||||
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintResults() {
|
||||
for i := 0; i < len(printer.sortedControlNames); i++ {
|
||||
controlSummary := printer.summary[printer.sortedControlNames[i]]
|
||||
@@ -144,6 +134,7 @@ func (printer *Printer) PrintResults() {
|
||||
func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
|
||||
cautils.SimpleDisplay(printer.writer, "Summary - ")
|
||||
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed)
|
||||
cautils.WarningDisplay(printer.writer, "Warning:%v ", controlSummary.TotalWarnign)
|
||||
cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
|
||||
cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
|
||||
if controlSummary.TotalFailed > 0 {
|
||||
@@ -157,10 +148,12 @@ func (printer *Printer) printTitle(controlName string, controlSummary *ControlSu
|
||||
cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName)
|
||||
if controlSummary.TotalResources == 0 && len(controlSummary.ListInputKinds) > 0 {
|
||||
cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace)
|
||||
} else if controlSummary.TotalFailed == 0 {
|
||||
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
|
||||
} else {
|
||||
} else if controlSummary.TotalFailed != 0 {
|
||||
cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
|
||||
} else if controlSummary.TotalWarnign != 0 {
|
||||
cautils.WarningDisplay(printer.writer, "warning %v\n", emoji.NeutralFace)
|
||||
} else {
|
||||
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
|
||||
}
|
||||
|
||||
cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
|
||||
@@ -197,7 +190,7 @@ func generateRow(control string, cs ControlSummary) []string {
|
||||
}
|
||||
|
||||
func generateHeader() []string {
|
||||
return []string{"Control Name", "Failed Resources", "All Resources", "% success"}
|
||||
return []string{"Control Name", "Failed Resources", "Warning Resources", "All Resources", "% success"}
|
||||
}
|
||||
|
||||
func percentage(big, small int) int {
|
||||
@@ -209,11 +202,12 @@ func percentage(big, small int) int {
|
||||
}
|
||||
return int(float64(float64(big-small)/float64(big)) * 100)
|
||||
}
|
||||
func generateFooter(numControlers, sumFailed, sumTotal int) []string {
|
||||
func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string {
|
||||
// Control name | # failed resources | all resources | % success
|
||||
row := []string{}
|
||||
row = append(row, fmt.Sprintf("%d", numControlers))
|
||||
row = append(row, fmt.Sprintf("%d", sumFailed))
|
||||
row = append(row, fmt.Sprintf("%d", sumWarning))
|
||||
row = append(row, fmt.Sprintf("%d", sumTotal))
|
||||
if sumTotal != 0 {
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
|
||||
@@ -230,14 +224,16 @@ func (printer *Printer) PrintSummaryTable() {
|
||||
summaryTable.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
sumTotal := 0
|
||||
sumFailed := 0
|
||||
sumWarning := 0
|
||||
|
||||
for i := 0; i < len(printer.sortedControlNames); i++ {
|
||||
controlSummary := printer.summary[printer.sortedControlNames[i]]
|
||||
summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
|
||||
sumFailed += controlSummary.TotalFailed
|
||||
sumWarning += controlSummary.TotalWarnign
|
||||
sumTotal += controlSummary.TotalResources
|
||||
}
|
||||
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, sumTotal))
|
||||
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, sumWarning, sumTotal))
|
||||
summaryTable.Render()
|
||||
}
|
||||
|
||||
@@ -251,7 +247,7 @@ func (printer *Printer) getSortedControlsNames() []string {
|
||||
}
|
||||
|
||||
func getWriter(outputFile string) *os.File {
|
||||
|
||||
os.Remove(outputFile)
|
||||
if outputFile != "" {
|
||||
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
@@ -2,6 +2,8 @@ package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
type Summary map[string]ControlSummary
|
||||
@@ -13,6 +15,7 @@ func NewSummary() Summary {
|
||||
type ControlSummary struct {
|
||||
TotalResources int
|
||||
TotalFailed int
|
||||
TotalWarnign int
|
||||
Description string
|
||||
Remediation string
|
||||
ListInputKinds []string
|
||||
@@ -24,11 +27,13 @@ type WorkloadSummary struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Group string
|
||||
Exception *armotypes.PostureExceptionPolicy
|
||||
}
|
||||
|
||||
func (controlSummary *ControlSummary) ToSlice() []string {
|
||||
s := []string{}
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarnign))
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
|
||||
return s
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func listResultSummary(ruleReports []opapolicy.RuleReport) []WorkloadSummary {
|
||||
|
||||
// add resource only once
|
||||
for i := range resource {
|
||||
resource[i].Exception = ruleReport.Exception
|
||||
if ok := track[resource[i].ToString()]; !ok {
|
||||
track[resource[i].ToString()] = true
|
||||
workloadsSummary = append(workloadsSummary, resource[i])
|
||||
@@ -51,6 +52,7 @@ func ruleResultSummary(obj opapolicy.AlertObject) ([]WorkloadSummary, error) {
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
resource = append(resource, *r)
|
||||
}
|
||||
|
||||
57
resultshandling/reporter/reporteventreceiver.go
Normal file
57
resultshandling/reporter/reporteventreceiver.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ReportEventReceiver struct {
|
||||
httpClient http.Client
|
||||
host url.URL
|
||||
}
|
||||
|
||||
func NewReportEventReceiver() *ReportEventReceiver {
|
||||
hostURL := initEventReceiverURL()
|
||||
return &ReportEventReceiver{
|
||||
httpClient: http.Client{},
|
||||
host: *hostURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *cautils.OPASessionObj) {
|
||||
if cautils.CustomerGUID == "" {
|
||||
return
|
||||
}
|
||||
opaSessionObj.PostureReport.RemoveData()
|
||||
if err := report.Send(opaSessionObj.PostureReport); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
func (report *ReportEventReceiver) Send(postureReport *opapolicy.PostureReport) error {
|
||||
|
||||
reqBody, err := json.Marshal(*postureReport)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err)
|
||||
}
|
||||
host := hostToString(&report.host, postureReport.ReportID)
|
||||
|
||||
req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send', http.NewRequest failed, host: %s, reason: %v", host, err)
|
||||
}
|
||||
res, err := report.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpClient.Do failed: %v", err)
|
||||
}
|
||||
msg, err := httpRespToString(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %v:%s", host, err, msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
56
resultshandling/reporter/reporteventreceiverutils.go
Normal file
56
resultshandling/reporter/reporteventreceiverutils.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
|
||||
func httpRespToString(resp *http.Response) (string, error) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
return "", nil
|
||||
}
|
||||
strBuilder := strings.Builder{}
|
||||
defer resp.Body.Close()
|
||||
if resp.ContentLength > 0 {
|
||||
strBuilder.Grow(int(resp.ContentLength))
|
||||
}
|
||||
_, err := io.Copy(&strBuilder, resp.Body)
|
||||
if err != nil {
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
err = fmt.Errorf("response status: %d. Content: %s", resp.StatusCode, strBuilder.String())
|
||||
}
|
||||
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
|
||||
func initEventReceiverURL() *url.URL {
|
||||
urlObj := url.URL{}
|
||||
|
||||
urlObj.Scheme = "https"
|
||||
urlObj.Host = getter.ArmoERURL
|
||||
urlObj.Path = "/k8s/postureReport"
|
||||
q := urlObj.Query()
|
||||
q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
|
||||
q.Add("clusterName", cautils.ClusterName)
|
||||
|
||||
urlObj.RawQuery = q.Encode()
|
||||
|
||||
return &urlObj
|
||||
}
|
||||
|
||||
func hostToString(host *url.URL, reportID string) string {
|
||||
q := host.Query()
|
||||
q.Add("reportID", reportID) // TODO - do we add the reportID?
|
||||
host.RawQuery = q.Encode()
|
||||
return host.String()
|
||||
}
|
||||
20
resultshandling/reporter/reporteventreceiverutils_test.go
Normal file
20
resultshandling/reporter/reporteventreceiverutils_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostToString(t *testing.T) {
|
||||
host := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "report.eudev3.cyberarmorsoft.com",
|
||||
Path: "k8srestapi/v1/postureReport",
|
||||
RawQuery: "cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af",
|
||||
}
|
||||
expectedHost := "https://report.eudev3.cyberarmorsoft.com/k8srestapi/v1/postureReport?cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af&reportID=ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41"
|
||||
receivedHost := hostToString(&host, "ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41")
|
||||
if receivedHost != expectedHost {
|
||||
t.Errorf("%s != %s", receivedHost, expectedHost)
|
||||
}
|
||||
}
|
||||
32
resultshandling/results.go
Normal file
32
resultshandling/results.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package resultshandling
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/resultshandling/printer"
|
||||
"github.com/armosec/kubescape/resultshandling/reporter"
|
||||
)
|
||||
|
||||
type ResultsHandler struct {
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
reporterObj *reporter.ReportEventReceiver
|
||||
printerObj *printer.Printer
|
||||
}
|
||||
|
||||
func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *reporter.ReportEventReceiver, printerObj *printer.Printer) *ResultsHandler {
|
||||
return &ResultsHandler{
|
||||
opaSessionObj: opaSessionObj,
|
||||
reporterObj: reporterObj,
|
||||
printerObj: printerObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (resultsHandler *ResultsHandler) HandleResults() float32 {
|
||||
|
||||
opaSessionObj := <-*resultsHandler.opaSessionObj
|
||||
|
||||
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
|
||||
|
||||
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
|
||||
|
||||
return score
|
||||
}
|
||||
136
scapepkg/exceptions/exceptionprocessor.go
Normal file
136
scapepkg/exceptions/exceptionprocessor.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
func ListRuleExceptions(exceptionPolicies []armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) []armotypes.PostureExceptionPolicy {
|
||||
ruleExceptions := []armotypes.PostureExceptionPolicy{}
|
||||
for i := range exceptionPolicies {
|
||||
if ruleHasExceptions(&exceptionPolicies[i], frameworkName, controlName, ruleName) {
|
||||
ruleExceptions = append(ruleExceptions, exceptionPolicies[i])
|
||||
}
|
||||
}
|
||||
|
||||
return ruleExceptions
|
||||
|
||||
}
|
||||
|
||||
func ruleHasExceptions(exceptionPolicy *armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) bool {
|
||||
for _, posturePolicy := range exceptionPolicy.PosturePolicies {
|
||||
if posturePolicy.FrameworkName == "" && posturePolicy.ControlName == "" && posturePolicy.RuleName == "" {
|
||||
continue // empty policy -> ignore
|
||||
}
|
||||
if posturePolicy.FrameworkName != "" && posturePolicy.FrameworkName != frameworkName {
|
||||
continue // policy does not match
|
||||
}
|
||||
if posturePolicy.ControlName != "" && posturePolicy.ControlName != controlName {
|
||||
continue // policy does not match
|
||||
}
|
||||
if posturePolicy.RuleName != "" && posturePolicy.RuleName != ruleName {
|
||||
continue // policy does not match
|
||||
}
|
||||
return true // policies match
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func AddExceptionsToRuleResponses(results []opapolicy.RuleResponse, ruleExceptions []armotypes.PostureExceptionPolicy) {
|
||||
if len(ruleExceptions) == 0 {
|
||||
return
|
||||
}
|
||||
for i := range results {
|
||||
workloads := alertObjectToWorkloads(&results[i].AlertObject)
|
||||
if len(workloads) == 0 {
|
||||
continue
|
||||
}
|
||||
for w := range workloads {
|
||||
if exception := getException(ruleExceptions, workloads[w]); exception != nil {
|
||||
results[i].Exception = exception
|
||||
}
|
||||
}
|
||||
results[i].RuleStatus = results[i].GetSingleResultStatus()
|
||||
}
|
||||
}
|
||||
|
||||
func alertObjectToWorkloads(obj *opapolicy.AlertObject) []k8sinterface.IWorkload {
|
||||
resource := []k8sinterface.IWorkload{}
|
||||
|
||||
for i := range obj.K8SApiObjects {
|
||||
r := k8sinterface.NewWorkloadObj(obj.K8SApiObjects[i])
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
resource = append(resource, r)
|
||||
}
|
||||
|
||||
return resource
|
||||
}
|
||||
func getException(ruleExceptions []armotypes.PostureExceptionPolicy, workload k8sinterface.IWorkload) *armotypes.PostureExceptionPolicy {
|
||||
for e := range ruleExceptions {
|
||||
for _, resource := range ruleExceptions[e].Resources {
|
||||
if hasException(&resource, workload) {
|
||||
return &ruleExceptions[e] // TODO - return disable exception out of all exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compareMetadata - compare namespace and kind
|
||||
func hasException(designator *armotypes.PortalDesignator, workload k8sinterface.IWorkload) bool {
|
||||
cluster, namespace, kind, name, labels := designator.DigestPortalDesignator()
|
||||
|
||||
if cluster == "" && namespace == "" && kind == "" && name == "" && len(labels) == 0 {
|
||||
return false // if designators are empty
|
||||
}
|
||||
|
||||
// if cluster != "" && cluster != ClusterName { // TODO - where do we receive cluster name from?
|
||||
// return false // cluster name does not match
|
||||
// }
|
||||
|
||||
if namespace != "" && !compareNamespace(workload, namespace) {
|
||||
return false // namespaces do not match
|
||||
}
|
||||
|
||||
if kind != "" && !compareKind(workload, kind) {
|
||||
return false // kinds do not match
|
||||
}
|
||||
|
||||
if name != "" && !compareName(workload, name) {
|
||||
return false // names do not match
|
||||
}
|
||||
if len(labels) > 0 && !compareLabels(workload, labels) {
|
||||
return false // labels do not match
|
||||
}
|
||||
|
||||
return true // no mismatch found -> the workload has an exception
|
||||
}
|
||||
|
||||
func compareNamespace(workload k8sinterface.IWorkload, namespace string) bool {
|
||||
if workload.GetKind() == "Namespace" {
|
||||
return namespace == workload.GetName()
|
||||
}
|
||||
return namespace == workload.GetNamespace()
|
||||
}
|
||||
|
||||
func compareKind(workload k8sinterface.IWorkload, kind string) bool {
|
||||
return kind == workload.GetKind()
|
||||
}
|
||||
|
||||
func compareName(workload k8sinterface.IWorkload, name string) bool {
|
||||
return name == workload.GetName()
|
||||
}
|
||||
|
||||
func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string) bool {
|
||||
workloadLabels := labels.Set(workload.GetLabels())
|
||||
designators := labels.Set(attributes).AsSelector()
|
||||
|
||||
return designators.Matches(workloadLabels)
|
||||
}
|
||||
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
func PostureExceptionPolicyDisableMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{}
|
||||
}
|
||||
|
||||
func PostureExceptionPolicyAlertOnlyMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{
|
||||
PortalBase: armotypes.PortalBase{
|
||||
Name: "postureExceptionPolicyAlertOnlyMock",
|
||||
},
|
||||
PolicyType: "postureExceptionPolicy",
|
||||
Actions: []armotypes.PostureExceptionPolicyActions{armotypes.AlertOnly},
|
||||
Resources: []armotypes.PortalDesignator{
|
||||
{
|
||||
DesignatorType: armotypes.DesignatorAttributes,
|
||||
Attributes: map[string]string{
|
||||
armotypes.AttributeNamespace: "default",
|
||||
armotypes.AttributeCluster: "unittest",
|
||||
},
|
||||
},
|
||||
},
|
||||
PosturePolicies: []armotypes.PosturePolicy{
|
||||
{
|
||||
FrameworkName: "MITRE",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRuleExceptions(t *testing.T) {
|
||||
exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
if len(res1) != 1 {
|
||||
t.Errorf("expecting 1 exception")
|
||||
}
|
||||
res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
if len(res2) != 0 {
|
||||
t.Errorf("expecting 0 exception")
|
||||
}
|
||||
}
|
||||
|
||||
// func TestGetException(t *testing.T) {
|
||||
// exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
// res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
// if len(res1) != 1 {
|
||||
// t.Errorf("expecting 1 exception")
|
||||
// }
|
||||
// res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
// if len(res2) != 0 {
|
||||
// t.Errorf("expecting 0 exception")
|
||||
// }
|
||||
// }
|
||||
232
scapepkg/score/frameworkdict.json
Normal file
232
scapepkg/score/frameworkdict.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"developer_framework": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"MITRE": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 8.0,
|
||||
"improvementRatio": 0.5
|
||||
},
|
||||
"Sidecar injection": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access tiller endpoint": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Data Destruction": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Resource Hijacking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access the Kubernetes API server": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Backdoor container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Images from private registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Mount service principal": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (validating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Vulnerable application": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Application exploit (RCE)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"List Kubernetes secrets": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed sensitive interfaces": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Bash/cmd inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Clear container logs": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"New container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"CoreDNS poisoning": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (mutating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exec into container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Delete Kubernetes events": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"NSA": {
|
||||
"Control plane hardening": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Immutable container filesystem": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Non-root containers": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Host PID/IPC privileges": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
1214
scapepkg/score/frameworkmock.json
Normal file
1214
scapepkg/score/frameworkmock.json
Normal file
File diff suppressed because it is too large
Load Diff
2161
scapepkg/score/resourcemocks.json
Normal file
2161
scapepkg/score/resourcemocks.json
Normal file
File diff suppressed because one or more lines are too long
22
scapepkg/score/resourcesdict.json
Normal file
22
scapepkg/score/resourcesdict.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"pod": 1.0,
|
||||
"service": 1.0,
|
||||
"daemonset": 1.0,
|
||||
"deployment": 1.0,
|
||||
"replicaset": 1.1,
|
||||
"statefulset": 1.0,
|
||||
"job": 1.0,
|
||||
"secret": 1.0,
|
||||
"cronjob": 1.0,
|
||||
"clusterrolebinding": 1.0,
|
||||
"clusterrole": 1.0,
|
||||
"rolebinding": 1.0,
|
||||
"role": 1.0,
|
||||
"networkpolicy": 1.0,
|
||||
"controllerrevision": 1.0,
|
||||
"namespace": 1.0,
|
||||
"serviceaccount": 1.0,
|
||||
"configmap": 1.0,
|
||||
"node": 1.0
|
||||
|
||||
}
|
||||
201
scapepkg/score/score.go
Normal file
201
scapepkg/score/score.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
||||
// corev1 "k8s.io/api/core/v1"
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ControlScoreWeights struct {
|
||||
BaseScore float32 `json:"baseScore"`
|
||||
RuntimeImprovementMultiplier float32 `json:"improvementRatio"`
|
||||
}
|
||||
|
||||
type ScoreUtil struct {
|
||||
ResourceTypeScores map[string]float32
|
||||
FrameworksScore map[string]map[string]ControlScoreWeights
|
||||
K8SApoObj *k8sinterface.KubernetesApi
|
||||
configPath string
|
||||
}
|
||||
|
||||
var postureScore *ScoreUtil
|
||||
|
||||
func (su *ScoreUtil) Calculate(frameworksReports []opapolicy.FrameworkReport) error {
|
||||
for i := range frameworksReports {
|
||||
su.CalculateFrameworkScore(&frameworksReports[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) CalculateFrameworkScore(framework *opapolicy.FrameworkReport) error {
|
||||
for i := range framework.ControlReports {
|
||||
framework.WCSScore += su.ControlScore(&framework.ControlReports[i], framework.Name)
|
||||
framework.Score += framework.ControlReports[i].Score
|
||||
framework.ARMOImprovement += framework.ControlReports[i].ARMOImprovement
|
||||
}
|
||||
if framework.WCSScore > 0 {
|
||||
framework.Score = (framework.Score * 100) / framework.WCSScore
|
||||
framework.ARMOImprovement = (framework.ARMOImprovement * 100) / framework.WCSScore
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to calculate score for framework %s due to bad wcs score", framework.Name)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
daemonset: daemonsetscore*#nodes
|
||||
workloads: if replicas:
|
||||
replicascore*workloadkindscore*#replicas
|
||||
else:
|
||||
regular
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) resourceRules(resources []map[string]interface{}) float32 {
|
||||
var weight float32 = 0
|
||||
|
||||
for _, v := range resources {
|
||||
var score float32 = 0
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
kind := ""
|
||||
if wl != nil {
|
||||
kind = strings.ToLower(wl.GetKind())
|
||||
replicas := wl.GetReplicas()
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if replicas > 1 {
|
||||
score *= su.ResourceTypeScores["replicaset"] * float32(replicas)
|
||||
}
|
||||
|
||||
} else {
|
||||
epsilon := float32(0.00001)
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
kind = keys[0]
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if score == 0.0 || (score > -1*epsilon && score < epsilon) {
|
||||
score = 1
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "daemonset" {
|
||||
b, err := json.Marshal(v)
|
||||
if err == nil {
|
||||
dmnset := appsv1.DaemonSet{}
|
||||
json.Unmarshal(b, &dmnset)
|
||||
score *= float32(dmnset.Status.DesiredNumberScheduled)
|
||||
}
|
||||
}
|
||||
weight += score
|
||||
}
|
||||
|
||||
return weight
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) externalResourceConverter(rscs map[string]interface{}) []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
for atype, v := range rscs {
|
||||
resources = append(resources, map[string]interface{}{atype: v})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
/*
|
||||
ControlScore:
|
||||
@input:
|
||||
ctrlReport - opapolicy.ControlReport object, must contain down the line the Input resources and the output resources
|
||||
frameworkName - calculate this control according to a given framework weights
|
||||
|
||||
ctrl.score = baseScore * SUM_resource (resourceWeight*min(#replicas*replicaweight,1)(nodes if daemonset)
|
||||
|
||||
returns control score ***for the input resources***
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) ControlScore(ctrlReport *opapolicy.ControlReport, frameworkName string) float32 {
|
||||
|
||||
aggregatedInputs := make([]map[string]interface{}, 0)
|
||||
aggregatedResponses := make([]map[string]interface{}, 0)
|
||||
for _, ruleReport := range ctrlReport.RuleReports {
|
||||
status, _, _ := ruleReport.GetRuleStatus()
|
||||
if status != "warning" {
|
||||
for _, ruleResponse := range ruleReport.RuleResponses {
|
||||
aggregatedResponses = append(aggregatedResponses, ruleResponse.AlertObject.K8SApiObjects...)
|
||||
aggregatedResponses = append(aggregatedResponses, su.externalResourceConverter(ruleResponse.AlertObject.ExternalObjects)...)
|
||||
}
|
||||
}
|
||||
|
||||
aggregatedInputs = append(aggregatedInputs, ruleReport.ListInputResources...)
|
||||
|
||||
}
|
||||
improvementRatio := float32(1)
|
||||
if ctrls, isOk := su.FrameworksScore[frameworkName]; isOk {
|
||||
if scoreobj, isOk2 := ctrls[ctrlReport.Name]; isOk2 {
|
||||
ctrlReport.BaseScore = scoreobj.BaseScore
|
||||
improvementRatio -= scoreobj.RuntimeImprovementMultiplier
|
||||
}
|
||||
} else {
|
||||
ctrlReport.BaseScore = 1.0
|
||||
}
|
||||
|
||||
ctrlReport.Score = ctrlReport.BaseScore * su.resourceRules(aggregatedResponses)
|
||||
ctrlReport.ARMOImprovement = ctrlReport.Score * improvementRatio
|
||||
|
||||
return ctrlReport.BaseScore * su.resourceRules(aggregatedInputs)
|
||||
|
||||
}
|
||||
|
||||
func getPostureFrameworksScores(weightPath string) map[string]map[string]ControlScoreWeights {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
frameworksScoreMap := make(map[string]map[string]ControlScoreWeights)
|
||||
dat, err := ioutil.ReadFile(weightPath + "frameworkdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &frameworksScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return frameworksScoreMap
|
||||
|
||||
}
|
||||
|
||||
func getPostureResourceScores(weightPath string) map[string]float32 {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
resourceScoreMap := make(map[string]float32)
|
||||
dat, err := ioutil.ReadFile(weightPath + "resourcesdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resourceScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resourceScoreMap
|
||||
|
||||
}
|
||||
|
||||
func NewScore(k8sapiobj *k8sinterface.KubernetesApi, configPath string) *ScoreUtil {
|
||||
if postureScore == nil {
|
||||
|
||||
postureScore = &ScoreUtil{
|
||||
ResourceTypeScores: getPostureResourceScores(configPath),
|
||||
FrameworksScore: getPostureFrameworksScores(configPath),
|
||||
configPath: configPath,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return postureScore
|
||||
}
|
||||
77
scapepkg/score/score_mocks.go
Normal file
77
scapepkg/score/score_mocks.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func loadResourcesMock() []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
|
||||
dat, err := ioutil.ReadFile("resourcemocks.json")
|
||||
|
||||
if err != nil {
|
||||
return resources
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resources); err != nil {
|
||||
return resources
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func getResouceByType(desiredType string) map[string]interface{} {
|
||||
rsrcs := loadResourcesMock()
|
||||
if rsrcs == nil {
|
||||
return nil
|
||||
}
|
||||
for _, v := range rsrcs {
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
if wl != nil {
|
||||
if strings.ToLower(wl.GetKind()) == desiredType {
|
||||
return v
|
||||
}
|
||||
continue
|
||||
|
||||
} else {
|
||||
for k := range v {
|
||||
if k == desiredType {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFrameworkMock() *opapolicy.FrameworkReport {
|
||||
report := &opapolicy.FrameworkReport{}
|
||||
|
||||
dat, err := ioutil.ReadFile("frameworkmock.json")
|
||||
|
||||
if err != nil {
|
||||
return report
|
||||
}
|
||||
if err := json.Unmarshal(dat, &report); err != nil {
|
||||
return report
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
func getMITREFrameworkResultMock() []opapolicy.FrameworkReport {
|
||||
l := make([]opapolicy.FrameworkReport, 0)
|
||||
report := loadFrameworkMock()
|
||||
resources := loadResourcesMock()
|
||||
if report != nil && resources != nil {
|
||||
|
||||
report.ControlReports[0].RuleReports[0].ListInputResources = resources
|
||||
l = append(l, *report)
|
||||
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
65
scapepkg/score/score_test.go
Normal file
65
scapepkg/score/score_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFrameworkMock(t *testing.T) {
|
||||
r := getMITREFrameworkResultMock()
|
||||
su := NewScore(nil, "")
|
||||
var epsilon float32 = 0.001
|
||||
su.Calculate(r)
|
||||
var sumweights float32 = 0.0
|
||||
for _, v := range su.ResourceTypeScores {
|
||||
sumweights += v
|
||||
}
|
||||
|
||||
for _, framework := range r {
|
||||
if framework.Score < 1 {
|
||||
t.Errorf("framework %s invalid calculation1: %v", framework.Name, framework)
|
||||
}
|
||||
|
||||
if framework.Score > framework.WCSScore+epsilon {
|
||||
t.Errorf("framework %s invalid calculation2: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ARMOImprovement > framework.Score+epsilon {
|
||||
t.Errorf("framework %s invalid calculation3: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ControlReports[0].Score*sumweights <= 0+epsilon {
|
||||
t.Errorf("framework %s invalid calculation4: %v", framework.Name, framework)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
func TestDaemonsetRule(t *testing.T) {
|
||||
desiredType := "daemonset"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 13 * su.ResourceTypeScores[desiredType]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleReplicasRule(t *testing.T) {
|
||||
desiredType := "deployment"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 3 * su.ResourceTypeScores[desiredType] * su.ResourceTypeScores["replicaset"]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
1
scapepkg/score/scoremethods.go
Normal file
1
scapepkg/score/scoremethods.go
Normal file
@@ -0,0 +1 @@
|
||||
package score
|
||||
Reference in New Issue
Block a user