Compare commits

...

37 Commits

Author SHA1 Message Date
lalafi@cyberarmor.io
aad32ec965 added controlID 2021-09-12 18:43:45 +03:00
David Wertenteil
d3137af3d7 skip score updating 2021-09-12 17:36:23 +03:00
Benyamin Hirschberg
2568241ef8 Merge pull request #69 from pettersolberg88/master
fix: Fixed Docker build not working
2021-09-12 10:19:18 +03:00
Petter Solberg
3c6b2db919 fix: Fixed Docker build not working
Building the docker image does currently not work because go.mod does not exist.
By running: `docker build -t kubescape -f build/Dockerfile .`
It fails:
```
Step 7/10 : RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-s -w " -installsuffix cgo  -o kubescape .
 ---> Running in 3e7d4a124446
cautils/k8sinterface/cloudvendorregistrycreds.go:14:2: missing go.sum entry for module providing package github.com/aws/aws-sdk-go/aws (imported by github.com/armosec/kubescape/cautils/k8sinterface); to add:
        go get github.com/armosec/kubescape/cautils/k8sinterface
cautils/k8sinterface/cloudvendorregistrycreds.go:15:2: missing go.sum entry for module providing package github.com/aws/aws-sdk-go/aws/session (imported by github.com/armosec/kubescape/cautils/k8sinterface); to add:
        go get github.com/armosec/kubescape/cautils/k8sinterface
...
```
By changing mod download to go mod tidy, it creates go.sum and the docker build works.
2021-09-10 14:10:09 +02:00
Benyamin Hirschberg
f60ff1fb26 Merge pull request #63 from zc2638/feat/dockerfile
add docker build
2021-09-09 14:59:46 +03:00
dwertent
679238ec13 download from release 2021-09-09 09:53:08 +03:00
dwertent
94884ac3d7 Merge branch 'master' into dev 2021-09-09 09:50:45 +03:00
Benyamin Hirschberg
0ef8f20c50 Merge pull request #65 from BenHirschbergCa/master
Cleanup in build file
2021-09-08 21:30:22 +03:00
Ben Hirschberg
82f3d62de5 clean up build file 2021-09-08 21:29:21 +03:00
Benyamin Hirschberg
46f1e6a83b Merge pull request #7 from armosec/master
rebase
2021-09-08 21:27:16 +03:00
Benyamin Hirschberg
65841a014f Merge branch 'master' into master 2021-09-08 21:27:02 +03:00
Benyamin Hirschberg
985c6868c1 Fixing URL typo 2021-09-08 21:21:25 +03:00
Shauli Rozen
fca862b2c7 Update README.md 2021-09-07 21:10:27 +03:00
zc
77a9956d91 add docker build 2021-09-07 14:21:39 +08:00
David Wertenteil
3a4a58fdd5 remove deffer func (#60) 2021-09-05 17:36:41 +03:00
dwertent
a1e639453d remove deffer func 2021-09-05 17:35:56 +03:00
dwertent
7da23c111e adding exceptions after merge 2021-09-05 17:29:53 +03:00
dwertent
768556251d support exceptions
use rego store
2021-09-05 17:22:47 +03:00
dwertent
00fcc565b5 ignore md5sum 2021-09-05 17:14:00 +03:00
Ben Hirschberg
9c74e5c93b Merge branch 'master' of github.com:BenHirschbergCa/kubescape 2021-09-05 17:00:37 +03:00
Ben Hirschberg
6a0ee6e0d7 specific upload files 2021-09-05 16:59:55 +03:00
Benyamin Hirschberg
93bb09d78e Merge pull request #6 from BenHirschbergCa/dev
removing unneeded fields
2021-09-05 16:51:40 +03:00
Ben Hirschberg
228e7703a8 removing unneeded fields 2021-09-05 16:51:00 +03:00
Benyamin Hirschberg
4b15a3b8e0 Merge pull request #5 from BenHirschbergCa/dev
moving to alexellis/upload-assets
2021-09-05 16:47:11 +03:00
Ben Hirschberg
80c5fd7439 moving to alexellis/upload-assets 2021-09-05 16:46:13 +03:00
Benyamin Hirschberg
504c4acc42 Merge pull request #4 from BenHirschbergCa/dev
returning master push run
2021-09-05 15:38:09 +03:00
Ben Hirschberg
573d85d770 returning master push run 2021-09-05 15:37:18 +03:00
Benyamin Hirschberg
4247f66378 Merge pull request #3 from BenHirschbergCa/dev
fixing upload file list
2021-09-05 15:34:37 +03:00
Benyamin Hirschberg
7d6a10e787 Merge pull request #59 from BenHirschbergCa/dev
Dev
2021-09-05 15:29:19 +03:00
Ben Hirschberg
bad303692e fixing upload file list 2021-09-05 15:28:33 +03:00
Benyamin Hirschberg
af3b33f7b0 Merge pull request #2 from BenHirschbergCa/dev
Dev
2021-09-05 15:23:12 +03:00
Ben Hirschberg
fd66b2eba5 build on pull requests only! 2021-09-05 15:22:02 +03:00
Ben Hirschberg
157ba1a08d ws 2021-09-05 15:20:13 +03:00
Benyamin Hirschberg
6b15e6575b Merge pull request #1 from BenHirschbergCa/dev
Dev
2021-09-05 15:18:48 +03:00
Ben Hirschberg
53f3229e9f adding m5sum 2021-09-05 15:17:55 +03:00
Ben Hirschberg
186435de69 test pinging 2021-09-05 15:03:39 +03:00
David Wertenteil
4d027d691f Support exceptions (#58)
* support exceptions

* update screenshot

* update summary
2021-09-05 14:44:55 +03:00
47 changed files with 5249 additions and 546 deletions

View File

@@ -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:

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.vs*
*go.sum*
*kubescape*
*debug*
*debug*
.idea

View File

@@ -1,7 +1,6 @@
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
[![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
[![Github All Releases](https://img.shields.io/github/downloads/armosec/kubescape/total.svg)](https://github.com/armosec/kubescape)
[![Go Report Card](https://goreportcard.com/badge/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 -
```
@@ -118,6 +125,18 @@ go mod tidy && go build -o kubescape .
4. Enjoy :zany_face:
# How to build in Docker
1. Clone Project
```
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
```
2. Build
```
docker build -t kubescape -f build/Dockerfile .
```
# Under the hood
## Tests

13
build/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM golang:1.16-alpine as builder
ENV GOPROXY=https://goproxy.io,direct
ENV GO111MODULE=on
WORKDIR /work
ADD . .
RUN go mod tidy
RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-s -w " -installsuffix cgo -o kubescape .
FROM alpine
COPY --from=builder /work/kubescape /usr/bin/kubescape
CMD ["kubescape"]

View File

@@ -1,6 +1,6 @@
package armotypes
type EnforcmentsRule struct {
type EnforcementsRule struct {
MonitoredObject []string `json:"monitoredObject"`
MonitoredObjectExistence []string `json:"objectExistence"`
MonitoredObjectEvent []string `json:"event"`
@@ -12,5 +12,5 @@ type ExecutionPolicy struct {
Designators []PortalDesignator `json:"designators"`
PolicyType string `json:"policyType"`
CreationTime string `json:"creation_time"`
ExecutionEnforcmentsRules []EnforcmentsRule `json:"enforcementRules"`
ExecutionEnforcementsRule []EnforcementsRule `json:"enforcementRules"`
}

View File

@@ -22,6 +22,7 @@ type DesignatorType string
// Supported designators
const (
DesignatorAttributes DesignatorType = "Attributes"
DesignatorAttribute DesignatorType = "Attribute" // Deprecated
/*
WorkloadID format.
k8s format: wlid://cluster-<cluster>/namespace-<namespace>/<kind>-<name>
@@ -45,6 +46,8 @@ const (
const (
AttributeCluster = "cluster"
AttributeNamespace = "namespace"
AttributeKind = "kind"
AttributeName = "name"
)
// PortalDesignator represented single designation options

View File

@@ -1,24 +1,100 @@
package armotypes
import (
"github.com/armosec/kubescape/cautils/cautils"
"github.com/golang/glog"
)
var IgnoreLabels = []string{AttributeCluster, AttributeNamespace}
func (designator *PortalDesignator) GetCluster() string {
cluster, _, _, _, _ := designator.DigestPortalDesignator()
return cluster
}
func (designator *PortalDesignator) GetNamespace() string {
_, namespace, _, _, _ := designator.DigestPortalDesignator()
return namespace
}
func (designator *PortalDesignator) GetKind() string {
_, _, kind, _, _ := designator.DigestPortalDesignator()
return kind
}
func (designator *PortalDesignator) GetName() string {
_, _, _, name, _ := designator.DigestPortalDesignator()
return name
}
func (designator *PortalDesignator) GetLabels() map[string]string {
_, _, _, _, labels := designator.DigestPortalDesignator()
return labels
}
// DigestPortalDesignator - get cluster namespace and labels from designator
func (designator *PortalDesignator) DigestPortalDesignator() (string, string, string, string, map[string]string) {
switch designator.DesignatorType {
case DesignatorAttributes, DesignatorAttribute:
return designator.DigestAttributesDesignator()
case DesignatorWlid, DesignatorWildWlid:
return cautils.GetClusterFromWlid(designator.WLID), cautils.GetNamespaceFromWlid(designator.WLID), cautils.GetKindFromWlid(designator.WLID), cautils.GetNameFromWlid(designator.WLID), map[string]string{}
// case DesignatorSid: // TODO
default:
glog.Warningf("in 'digestPortalDesignator' designator type: '%v' not yet supported. please contact Armo team", designator.DesignatorType)
}
return "", "", "", "", nil
}
func (designator *PortalDesignator) DigestAttributesDesignator() (string, string, string, string, map[string]string) {
cluster := ""
namespace := ""
kind := ""
name := ""
labels := map[string]string{}
attributes := designator.Attributes
if attributes == nil {
return cluster, namespace, kind, name, labels
}
for k, v := range attributes {
labels[k] = v
}
if v, ok := attributes[AttributeNamespace]; ok {
namespace = v
delete(labels, AttributeNamespace)
}
if v, ok := attributes[AttributeCluster]; ok {
cluster = v
delete(labels, AttributeCluster)
}
if v, ok := attributes[AttributeKind]; ok {
kind = v
delete(labels, AttributeKind)
}
if v, ok := attributes[AttributeName]; ok {
name = v
delete(labels, AttributeName)
}
return cluster, namespace, kind, name, labels
}
// DigestPortalDesignator DEPRECATED. use designator.DigestPortalDesignator() - get cluster namespace and labels from designator
func DigestPortalDesignator(designator *PortalDesignator) (string, string, map[string]string) {
switch designator.DesignatorType {
case DesignatorAttributes:
case DesignatorAttributes, DesignatorAttribute:
return DigestAttributesDesignator(designator.Attributes)
// case DesignatorWlid: TODO
// case DesignatorWildWlid: TODO
case DesignatorWlid, DesignatorWildWlid:
return cautils.GetClusterFromWlid(designator.WLID), cautils.GetNamespaceFromWlid(designator.WLID), map[string]string{}
// case DesignatorSid: // TODO
default:
glog.Warningf("in 'digestPortalDesignator' designator type: '%v' not yet supported. please contact Armo team", designator.DesignatorType)
}
return "", "", nil
}
func DigestAttributesDesignator(attributes map[string]string) (string, string, map[string]string) {
cluster := ""
namespace := ""
labels := map[string]string{}
if attributes == nil || len(attributes) == 0 {
if attributes == nil {
return cluster, namespace, labels
}
for k, v := range attributes {
@@ -32,5 +108,6 @@ func DigestAttributesDesignator(attributes map[string]string) (string, string, m
cluster = v
delete(labels, AttributeCluster)
}
return cluster, namespace, labels
}

View File

@@ -0,0 +1,42 @@
package armotypes
type PostureExceptionPolicyActions string
const AlertOnly PostureExceptionPolicyActions = "alertOnly"
const Disable PostureExceptionPolicyActions = "disable"
type PostureExceptionPolicy struct {
PortalBase `json:",inline"`
PolicyType string `json:"policyType"`
CreationTime string `json:"creationTime"`
Actions []PostureExceptionPolicyActions `json:"actions"`
Resources []PortalDesignator `json:"resources"`
PosturePolicies []PosturePolicy `json:"posturePolicies"`
}
type PosturePolicy struct {
FrameworkName string `json:"frameworkName"`
ControlName string `json:"controlName"`
RuleName string `json:"ruleName"`
}
func (exceptionPolicy *PostureExceptionPolicy) IsAlertOnly() bool {
if exceptionPolicy.IsDisable() {
return false
}
for i := range exceptionPolicy.Actions {
if exceptionPolicy.Actions[i] == AlertOnly {
return true
}
}
return false
}
func (exceptionPolicy *PostureExceptionPolicy) IsDisable() bool {
for i := range exceptionPolicy.Actions {
if exceptionPolicy.Actions[i] == Disable {
return true
}
}
return false
}

View File

@@ -0,0 +1 @@
package armotypes

View File

@@ -1,6 +1,7 @@
package cautils
import (
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
@@ -10,6 +11,7 @@ type K8SResources map[string]interface{}
type OPASessionObj struct {
Frameworks []opapolicy.Framework
K8SResources *K8SResources
Exceptions []armotypes.PostureExceptionPolicy
PostureReport *opapolicy.PostureReport
}

View File

@@ -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()

76
cautils/getter/armoapi.go Normal file
View File

@@ -0,0 +1,76 @@
package getter
import (
"fmt"
"net/http"
"strings"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// =======================================================================================================================
// =============================================== ArmoAPI ===============================================================
// =======================================================================================================================
// Armo API for downloading policies
type ArmoAPI struct {
httpClient *http.Client
baseURL string
}
func NewArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{},
baseURL: "https://dashbe.euprod1.cyberarmorsoft.com",
}
}
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return nil, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
return framework, err
}
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) 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"
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
}
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
}
if err = JSONDecoder(respStr).Decode(&exceptions); err != nil {
return nil, err
}
return exceptions, nil
}
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
requestURI := "api/v1/armoPostureExceptions"
requestURI += fmt.Sprintf("?customerGUID=%s", customerGUID)
if clusterName != "" {
requestURI += fmt.Sprintf("&clusterName=%s", clusterName)
}
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
}

View File

@@ -0,0 +1,87 @@
package getter
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// =======================================================================================================================
// ======================================== DownloadReleasedPolicy =======================================================
// =======================================================================================================================
// Download released version
type DownloadReleasedPolicy struct {
hostURL string
httpClient *http.Client
}
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
return &DownloadReleasedPolicy{
hostURL: "",
httpClient: &http.Client{},
}
}
func (drp *DownloadReleasedPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
return []armotypes.PostureExceptionPolicy{}, nil
}
func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framework, error) {
drp.setURL(name)
respStr, err := HttpGetter(drp.httpClient, drp.hostURL)
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return framework, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
return framework, err
}
func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
latestReleases := "https://api.github.com/repos/armosec/regolibrary/releases/latest"
resp, err := http.Get(latestReleases)
if err != nil {
return fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestReleases, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode < 200 || 301 < resp.StatusCode {
return fmt.Errorf("failed to download file, status code: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body from '%s', reason: %s", latestReleases, err.Error())
}
var data map[string]interface{}
err = json.Unmarshal(body, &data)
if err != nil {
return fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestReleases, err.Error())
}
if assets, ok := data["assets"].([]interface{}); ok {
for i := range assets {
if asset, ok := assets[i].(map[string]interface{}); ok {
if name, ok := asset["name"].(string); ok {
if name == frameworkName {
if url, ok := asset["browser_download_url"].(string); ok {
drp.hostURL = url
}
}
}
}
}
}
return nil
}

View File

@@ -1,160 +1,12 @@
package getter
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
const DefaultLocalStore = ".kubescape"
type IPolicyGetter interface {
GetFramework(name string) (*opapolicy.Framework, error)
}
// =======================================================================================================================
// ======================================== DownloadReleasedPolicy =======================================================
// =======================================================================================================================
// Download released version
type DownloadReleasedPolicy struct {
hostURL string
httpClient *http.Client
}
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
return &DownloadReleasedPolicy{
hostURL: "",
httpClient: &http.Client{},
}
}
func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framework, error) {
drp.setURL(name)
respStr, err := HttpGetter(drp.httpClient, drp.hostURL)
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return framework, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
return framework, err
}
func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
latestReleases := "https://api.github.com/repos/armosec/regolibrary/releases/latest"
resp, err := http.Get(latestReleases)
if err != nil {
return fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestReleases, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode < 200 || 301 < resp.StatusCode {
return fmt.Errorf("failed to download file, status code: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body from '%s', reason: %s", latestReleases, err.Error())
}
var data map[string]interface{}
err = json.Unmarshal(body, &data)
if err != nil {
return fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestReleases, err.Error())
}
if assets, ok := data["assets"].([]interface{}); ok {
for i := range assets {
if asset, ok := assets[i].(map[string]interface{}); ok {
if name, ok := asset["name"].(string); ok {
if name == frameworkName {
if url, ok := asset["browser_download_url"].(string); ok {
drp.hostURL = url
}
}
}
}
}
}
return nil
}
// =======================================================================================================================
// ============================================== LoadPolicy =============================================================
// =======================================================================================================================
// Load policies from a local repository
type LoadPolicy struct {
filePath string
}
func NewLoadPolicy(filePath string) *LoadPolicy {
return &LoadPolicy{
filePath: filePath,
}
}
func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework, error) {
framework := &opapolicy.Framework{}
f, err := ioutil.ReadFile(lp.filePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(f, framework)
if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) {
return nil, fmt.Errorf("framework from file not matching")
}
return framework, err
}
// =======================================================================================================================
// =============================================== ArmoAPI ===============================================================
// =======================================================================================================================
// 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)
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return nil, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
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"
armoAPI.hostURL = urlEncoder(fmt.Sprintf("%s/%s", armoAPI.hostURL, requestURI))
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
// GetScores(scope, customerName, namespace string) ([]armotypes.PostureExceptionPolicy, error)
}

View File

@@ -0,0 +1,54 @@
package getter
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// =======================================================================================================================
// ============================================== LoadPolicy =============================================================
// =======================================================================================================================
const DefaultLocalStore = ".kubescape"
// Load policies from a local repository
type LoadPolicy struct {
filePath string
}
func NewLoadPolicy(filePath string) *LoadPolicy {
return &LoadPolicy{
filePath: filePath,
}
}
func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework, error) {
framework := &opapolicy.Framework{}
f, err := ioutil.ReadFile(lp.filePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(f, framework)
if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) {
return nil, fmt.Errorf("framework from file not matching")
}
return framework, err
}
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
View 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
}

View File

@@ -16,14 +16,15 @@ const (
// RegoResponse the expected response of single run of rego policy
type RuleResponse struct {
AlertMessage string `json:"alertMessage"`
PackageName string `json:"packagename"`
AlertScore AlertScore `json:"alertScore"`
// AlertObject AlertObject `json:"alertObject"`
AlertObject AlertObject `json:"alertObject"` // TODO - replace interface to AlertObject
Context []string `json:"context"` // TODO - Remove
Rulename string `json:"rulename"` // TODO - Remove
ExceptionName string `json:"exceptionName"`
AlertMessage string `json:"alertMessage"`
RuleStatus string `json:"ruleStatus"`
PackageName string `json:"packagename"`
AlertScore AlertScore `json:"alertScore"`
AlertObject AlertObject `json:"alertObject"`
Context []string `json:"context,omitempty"` // TODO - Remove
Rulename string `json:"rulename,omitempty"` // TODO - Remove
ExceptionName string `json:"exceptionName,omitempty"` // Not in use
Exception *armotypes.PostureExceptionPolicy `json:"exception,omitempty"`
}
type AlertObject struct {
@@ -32,19 +33,27 @@ type AlertObject struct {
}
type FrameworkReport struct {
Name string `json:"name"`
ControlReports []ControlReport `json:"controlReports"`
Name string `json:"name"`
ControlReports []ControlReport `json:"controlReports"`
Score float32 `json:"score,omitempty"`
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
WCSScore float32 `json:"wcsScore,omitempty"`
}
type ControlReport struct {
Name string `json:"name"`
RuleReports []RuleReport `json:"ruleReports"`
Remediation string `json:"remediation"`
Description string `json:"description"`
armotypes.PortalBase `json:",inline"`
ControlID string `json:"id"`
Name string `json:"name"`
RuleReports []RuleReport `json:"ruleReports"`
Remediation string `json:"remediation"`
Description string `json:"description"`
Score float32 `json:"score,omitempty"`
BaseScore float32 `json:"baseScore,omitempty"`
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
}
type RuleReport struct {
Name string `json:"name"`
Remediation string `json:"remediation"`
RuleStatus RuleStatus `json:"ruleStatus"`
RuleStatus RuleStatus `json:"ruleStatus"` // did we run the rule or not (if there where compile errors, the value will be failed)
RuleResponses []RuleResponse `json:"ruleResponses"`
ListInputResources []map[string]interface{} `json:"-"`
ListInputKinds []string `json:"-"`
@@ -92,10 +101,12 @@ type PolicyRule struct {
// Control represents a collection of rules which are combined together to single purpose
type Control struct {
armotypes.PortalBase `json:",inline"`
CreationTime string `json:"creationTime"`
Description string `json:"description"`
Remediation string `json:"remediation"`
Rules []PolicyRule `json:"rules"`
ControlID string `json:"id"`
CreationTime string `json:"creationTime"`
Description string `json:"description"`
Remediation string `json:"remediation"`
Rules []PolicyRule `json:"rules"`
// for new list of rules in POST/UPADTE requests
RulesIDs *[]string `json:"rulesIDs,omitempty"`
}

View File

@@ -3,10 +3,6 @@ package opapolicy
import (
"bytes"
"encoding/json"
"fmt"
"github.com/golang/glog"
"github.com/open-policy-agent/opa/rego"
)
func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
@@ -17,6 +13,19 @@ func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
return bytes.NewBuffer(res), err
}
func (RuleResponse *RuleResponse) GetSingleResultStatus() string {
if RuleResponse.Exception != nil {
if RuleResponse.Exception.IsAlertOnly() {
return "warning"
}
if RuleResponse.Exception.IsDisable() {
return "ignore"
}
}
return "failed"
}
func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleResponse) {
if len(ruleReport.RuleResponses) == 0 {
return "success", nil, nil
@@ -26,9 +35,11 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
for _, rule := range ruleReport.RuleResponses {
if rule.ExceptionName != "" {
failed = append(failed, rule)
} else {
exceptions = append(exceptions, rule)
} else if rule.Exception != nil {
exceptions = append(exceptions, rule)
} else {
failed = append(failed, rule)
}
}
@@ -38,46 +49,30 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
}
return status, failed, exceptions
}
func ParseRegoResult(regoResult *rego.ResultSet) ([]RuleResponse, error) {
var errs error
ruleResponses := []RuleResponse{}
for _, result := range *regoResult {
for desicionIdx := range result.Expressions {
if resMap, ok := result.Expressions[desicionIdx].Value.(map[string]interface{}); ok {
for objName := range resMap {
jsonBytes, err := json.Marshal(resMap[objName])
if err != nil {
err = fmt.Errorf("in parseRegoResult, json.Marshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
desObj := make([]RuleResponse, 0)
if err := json.Unmarshal(jsonBytes, &desObj); err != nil {
err = fmt.Errorf("in parseRegoResult, json.Unmarshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
ruleResponses = append(ruleResponses, desObj...)
}
}
}
}
return ruleResponses, errs
}
func (controlReport *ControlReport) GetNumberOfResources() int {
sum := 0
for i := range controlReport.RuleReports {
if controlReport.RuleReports[i].ListInputResources == nil {
continue
}
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 {
@@ -85,15 +80,60 @@ func (controlReport *ControlReport) ListControlsInputKinds() []string {
}
return listControlsInputKinds
}
func (controlReport *ControlReport) Passed() bool {
for i := range controlReport.RuleReports {
if len(controlReport.RuleReports[i].RuleResponses) > 0 {
return false
if len(controlReport.RuleReports[i].RuleResponses) == 0 {
return true
}
}
return true
return false
}
func (controlReport *ControlReport) Warning() bool {
if controlReport.Passed() || controlReport.Failed() {
return false
}
for i := range controlReport.RuleReports {
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "warning" {
return true
}
}
return false
}
func (controlReport *ControlReport) Failed() bool {
return !controlReport.Passed()
if controlReport.Passed() {
return false
}
for i := range controlReport.RuleReports {
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "failed" {
return true
}
}
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
}

View File

@@ -8,8 +8,9 @@ import (
)
type ScanInfo struct {
PolicyGetter getter.IPolicyGetter
Getters
PolicyIdentifier opapolicy.PolicyIdentifier
UseExceptions string
UseFrom string
UseDefault bool
Format string
@@ -20,12 +21,27 @@ type ScanInfo struct {
FailThreshold uint16
}
type Getters struct {
ExceptionsGetter getter.IPolicyGetter
PolicyGetter getter.IPolicyGetter
}
func (scanInfo *ScanInfo) Init() {
// scanInfo.setSilentMode()
scanInfo.setUseFrom()
scanInfo.setUseExceptions()
scanInfo.setOutputFile()
scanInfo.setGetter()
}
func (scanInfo *ScanInfo) setUseExceptions() {
if scanInfo.UseExceptions != "" {
// load exceptions from file
scanInfo.ExceptionsGetter = getter.NewLoadPolicy(scanInfo.UseExceptions)
} else {
scanInfo.ExceptionsGetter = getter.NewArmoAPI()
}
}
func (scanInfo *ScanInfo) setUseFrom() {
if scanInfo.UseFrom != "" {
@@ -41,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()
}
}

View File

@@ -22,7 +22,7 @@ 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)
}

View File

@@ -14,7 +14,9 @@ import (
"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"
)
@@ -83,8 +85,9 @@ func isValidFramework(framework string) bool {
func init() {
scanCmd.AddCommand(frameworkCmd)
scanInfo = cautils.ScanInfo{}
frameworkCmd.Flags().StringVarP(&scanInfo.UseFrom, "use-from", "", "", "Path to load framework from")
frameworkCmd.Flags().BoolVarP(&scanInfo.UseDefault, "use-default", "", false, "Load framework from default path")
frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Path to load framework from")
frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load framework from default path")
frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to file containing list of exceptions")
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout")
@@ -120,11 +123,12 @@ func CliSetup() error {
// processor setup - rego run
go func() {
reporterObj := opaprocessor.NewOPAProcessor(&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()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {

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
View 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"
}
]
}
]

View File

@@ -29,9 +29,12 @@ OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
echo -e "\033[32m[V] Downloaded Kubescape"
sudo chmod +x $OUTPUT
sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
sudo cp $OUTPUT /usr/local/bin
# 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
rm -rf $OUTPUT
echo -e "[V] Finished Installation"

View File

@@ -6,6 +6,8 @@ import (
"time"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/scapepkg/exceptions"
"github.com/armosec/kubescape/scapepkg/score"
"github.com/armosec/kubescape/cautils/k8sinterface"
@@ -18,93 +20,143 @@ import (
"github.com/open-policy-agent/opa/storage"
)
type OPAProcessor struct {
processedPolicy *chan *cautils.OPASessionObj
reportResults *chan *cautils.OPASessionObj
regoK8sCredentials storage.Store
const ScoreConfigPath = "/resources/config"
var RegoK8sCredentials storage.Store
type OPAProcessorHandler struct {
processedPolicy *chan *cautils.OPASessionObj
reportResults *chan *cautils.OPASessionObj
// componentConfig cautils.ComponentConfig
}
func NewOPAProcessor(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessor {
type OPAProcessor struct {
*cautils.OPASessionObj
}
func NewOPAProcessor(sessionObj *cautils.OPASessionObj) *OPAProcessor {
return &OPAProcessor{
OPASessionObj: sessionObj,
}
}
func NewOPAProcessorHandler(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessorHandler {
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.K8SConfig)
store, err := regoDependenciesData.TOStorage()
if err != nil {
panic(err)
}
return &OPAProcessor{
processedPolicy: processedPolicy,
reportResults: reportResults,
regoK8sCredentials: store,
RegoK8sCredentials = store
return &OPAProcessorHandler{
processedPolicy: processedPolicy,
reportResults: reportResults,
}
}
func (opap *OPAProcessor) ProcessRulesListenner() {
func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
for {
// recover
defer func() {
if err := recover(); err != nil {
glog.Errorf("RECOVER in ProcessRulesListenner, reason: %v", err)
}
}()
opaSessionObj := <-*opap.processedPolicy
go func() {
if err := opap.ProcessRulesHandler(opaSessionObj); err != nil {
// opaSessionObj.Reporter.SendError(nil, true, true)
}
*opap.reportResults <- opaSessionObj
}()
opaSessionObj := <-*opaHandler.processedPolicy
opap := NewOPAProcessor(opaSessionObj)
// process
if err := opap.Process(); err != nil {
fmt.Println(err)
}
// edit results
opap.updateResults()
// update score
// opap.updateScore()
// report
*opaHandler.reportResults <- opaSessionObj
}
}
func (opap *OPAProcessor) ProcessRulesHandler(opaSessionObj *cautils.OPASessionObj) error {
func (opap *OPAProcessor) Process() error {
// glog.Infof(fmt.Sprintf("Starting 'Process'. reportID: %s", opap.PostureReport.ReportID))
cautils.ProgressTextDisplay(fmt.Sprintf("Scanning cluster %s", cautils.ClusterName))
cautils.StartSpinner()
frameworkReports := []opapolicy.FrameworkReport{}
var errs error
for _, framework := range opaSessionObj.Frameworks {
frameworkReport := opapolicy.FrameworkReport{}
frameworkReport.Name = framework.Name
controlReports := []opapolicy.ControlReport{}
for _, control := range framework.Controls {
// cautils.SimpleDisplay(os.Stdout, fmt.Sprintf("\033[2K\r%s", control.Name))
controlReport := opapolicy.ControlReport{}
controlReport.Name = control.Name
controlReport.Description = control.Description
controlReport.Remediation = control.Remediation
ruleReports := []opapolicy.RuleReport{}
for _, rule := range control.Rules {
if ruleWithArmoOpaDependency(rule.Attributes) {
continue
}
k8sObjects := getKubernetesObjects(opaSessionObj.K8SResources, rule.Match)
ruleReport, err := opap.runOPAOnSingleRule(&rule, k8sObjects)
if err != nil {
ruleReport.RuleStatus.Status = "failure"
ruleReport.RuleStatus.Message = err.Error()
glog.Error(err)
errs = fmt.Errorf("%v\n%s", errs, err.Error())
} else {
ruleReport.RuleStatus.Status = "success"
}
ruleReport.ListInputResources = k8sObjects
ruleReport.ListInputKinds = listMatchKinds(rule.Match)
ruleReports = append(ruleReports, ruleReport)
}
controlReport.RuleReports = ruleReports
controlReports = append(controlReports, controlReport)
for i := range opap.Frameworks {
frameworkReport, err := opap.processFramework(&opap.Frameworks[i])
if err != nil {
errs = fmt.Errorf("%v\n%s", errs, err.Error())
}
frameworkReport.ControlReports = controlReports
frameworkReports = append(frameworkReports, frameworkReport)
frameworkReports = append(frameworkReports, *frameworkReport)
}
opaSessionObj.PostureReport.FrameworkReports = frameworkReports
opaSessionObj.PostureReport.ReportGenerationTime = time.Now().UTC()
opap.PostureReport.FrameworkReports = frameworkReports
opap.PostureReport.ReportGenerationTime = time.Now().UTC()
// glog.Infof(fmt.Sprintf("Done 'Process'. reportID: %s", opap.PostureReport.ReportID))
cautils.StopSpinner()
cautils.SuccessTextDisplay(fmt.Sprintf("Done scanning cluster %s", cautils.ClusterName))
return errs
}
func (opap *OPAProcessor) processFramework(framework *opapolicy.Framework) (*opapolicy.FrameworkReport, error) {
var errs error
frameworkReport := opapolicy.FrameworkReport{}
frameworkReport.Name = framework.Name
controlReports := []opapolicy.ControlReport{}
for i := range framework.Controls {
controlReport, err := opap.processControl(&framework.Controls[i])
if err != nil {
errs = fmt.Errorf("%v\n%s", errs, err.Error())
}
controlReports = append(controlReports, *controlReport)
}
frameworkReport.ControlReports = controlReports
return &frameworkReport, errs
}
func (opap *OPAProcessor) processControl(control *opapolicy.Control) (*opapolicy.ControlReport, error) {
var errs error
controlReport := opapolicy.ControlReport{}
controlReport.PortalBase = control.PortalBase
controlReport.Name = control.Name
controlReport.Description = control.Description
controlReport.Remediation = control.Remediation
ruleReports := []opapolicy.RuleReport{}
for i := range control.Rules {
ruleReport, err := opap.processRule(&control.Rules[i])
if err != nil {
errs = fmt.Errorf("%v\n%s", errs, err.Error())
}
if ruleReport != nil {
ruleReports = append(ruleReports, *ruleReport)
}
}
controlReport.RuleReports = ruleReports
return &controlReport, errs
}
func (opap *OPAProcessor) processRule(rule *opapolicy.PolicyRule) (*opapolicy.RuleReport, error) {
if ruleWithArmoOpaDependency(rule.Attributes) {
return nil, nil
}
k8sObjects := getKubernetesObjects(opap.K8SResources, rule.Match)
ruleReport, err := opap.runOPAOnSingleRule(rule, k8sObjects)
if err != nil {
ruleReport.RuleStatus.Status = "failure"
ruleReport.RuleStatus.Message = err.Error()
glog.Error(err)
} else {
ruleReport.RuleStatus.Status = "success"
}
ruleReport.ListInputResources = k8sObjects
return &ruleReport, err
}
func (opap *OPAProcessor) runOPAOnSingleRule(rule *opapolicy.PolicyRule, k8sObjects []map[string]interface{}) (opapolicy.RuleReport, error) {
switch rule.RuleLanguage {
case opapolicy.RegoLanguage, opapolicy.RegoLanguage2:
@@ -147,18 +199,42 @@ func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRe
rego.Query("data.armo_builtins"), // get package name from rule
rego.Compiler(compiledRego),
rego.Input(inputObj),
rego.Store(opap.regoK8sCredentials),
rego.Store(RegoK8sCredentials),
)
// Run evaluation
resultSet, err := rego.Eval(context.Background())
if err != nil {
return nil, fmt.Errorf("In 'regoEval', failed to evaluate rule, reason: %s", err.Error())
return nil, fmt.Errorf("in 'regoEval', failed to evaluate rule, reason: %s", err.Error())
}
results, err := opapolicy.ParseRegoResult(&resultSet)
results, err := parseRegoResult(&resultSet)
// results, err := ParseRegoResult(&resultSet)
if err != nil {
return results, err
}
return results, nil
}
func (opap *OPAProcessor) updateScore() {
// calculate score
s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath)
s.Calculate(opap.PostureReport.FrameworkReports)
}
func (opap *OPAProcessor) updateResults() {
for f, frameworkReport := range opap.PostureReport.FrameworkReports {
for c, controlReport := range opap.PostureReport.FrameworkReports[f].ControlReports {
for r, ruleReport := range opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports {
// editing the responses -> removing duplications, clearing secret data, etc.
opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports[r].RuleResponses = editRuleResponses(ruleReport.RuleResponses)
// adding exceptions to the rules
ruleExceptions := exceptions.ListRuleExceptions(opap.Exceptions, frameworkReport.Name, controlReport.Name, ruleReport.Name)
exceptions.AddExceptionsToRuleResponses(opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports[r].RuleResponses, ruleExceptions)
}
}
}
}

View File

@@ -1,44 +1,21 @@
package opaprocessor
import (
"context"
"encoding/json"
"os"
"path"
"strings"
"testing"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
// _ "k8s.io/client-go/plugin/pkg/client/auth"
restclient "k8s.io/client-go/rest"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/opapolicy/resources"
"github.com/open-policy-agent/opa/ast"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
func NewOPAProcessorMock() *OPAProcessor {
c := make(chan *cautils.OPASessionObj)
deps := resources.NewRegoDependenciesDataMock()
storage, err := deps.TOStorage()
if err != nil {
panic(err)
}
return &OPAProcessor{
processedPolicy: &c,
reportResults: &c,
regoK8sCredentials: storage,
}
return &OPAProcessor{}
}
func TestProcessRulesHandler(t *testing.T) {
func TestProcess(t *testing.T) {
// set k8s
k8sResources := make(cautils.K8SResources)
k8sResources["/v1/pods"] = k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.V1KubeSystemNamespaceMock().Items)
@@ -47,149 +24,21 @@ func TestProcessRulesHandler(t *testing.T) {
opaSessionObj := cautils.NewOPASessionObjMock()
opaSessionObj.Frameworks = []opapolicy.Framework{*opapolicy.MockFrameworkA()}
opaSessionObj.K8SResources = &k8sResources
k8sinterface.K8SConfig = &restclient.Config{}
// run test
processor := NewOPAProcessorMock()
if err := processor.ProcessRulesHandler(opaSessionObj); err != nil {
t.Errorf("%v", err)
}
// bla, _ := json.Marshal(opaSessionObj.PostureReport)
// t.Errorf("%v", string(bla))
}
func TestRunRegoOnK8s(t *testing.T) {
// set k8s
k8sResources := make(cautils.K8SResources)
// k8sResources["/v1/pods"] = k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.V1KubeSystemNamespaceMock().Items)
k8sResources["/v1/pods"] = k8sinterface.V1KubeSystemNamespaceMock().Items
k8sinterface.K8SConfig = &restclient.Config{}
// run test
processor := NewOPAProcessorMock()
report, err := processor.runRegoOnK8s(opapolicy.MockRuleA(), []map[string]interface{}{k8sResources})
if err != nil {
t.Errorf("%v", err)
}
if len(report.RuleResponses) == 0 {
t.Errorf("len(report.RuleResponses) == 0")
}
}
func TestCompromisedRegistries(t *testing.T) {
// set k8s
k8sResources := make(cautils.K8SResources)
// k8sResources["/v1/pods"] = k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.V1KubeSystemNamespaceMock().Items)
k8sResources["/v1/pods"] = k8sinterface.V1AllClusterWithCompromisedRegistriesMock().Items
wd, _ := os.Getwd()
baseDirName := "kubescape"
idx := strings.Index(wd, baseDirName)
wd = wd[0:idx]
resources.RegoDependenciesPath = path.Join(wd, "/kubescape/vendor/asterix.cyberarmor.io/cyberarmor/capacketsgo/opapolicy/resources/rego/dependencies")
k8sinterface.K8SConfig = &restclient.Config{}
opaProcessor := NewOPAProcessorMock()
// run test
reportB, errB := opaProcessor.runRegoOnK8s(opapolicy.MockRuleUntrustedRegistries(), []map[string]interface{}{k8sResources})
if errB != nil {
t.Errorf("%v", errB)
}
if len(reportB.RuleResponses) == 0 {
t.Errorf("len(report.RuleResponses) == 0")
return
}
// bla, _ := json.Marshal(reportB.RuleResponses[0])
// t.Errorf("%s", bla)
}
// func TestForLior(t *testing.T) {
// // set k8s
// k8sResources := make(cautils.K8SResources)
// // k8sResources["/v1/pods"] = k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.V1KubeSystemNamespaceMock().Items)
// k8sResources["/v1/pods"] = k8sinterface.V1KubeSystemNamespaceMock().Items
// resources.RegoDependenciesPath = "/home/david/go/src/kubescape/vendor/asterix.cyberarmor.io/cyberarmor/capacketsgo/opapolicy/resources/rego/dependencies"
// opaProcessor := NewOPAProcessorMock()
// // set opaSessionObj
// opaSessionObj := cautils.NewOPASessionObjMock()
// opaSessionObj.K8SResources = &k8sResources
// opaSessionObj.Frameworks = []opapolicy.Framework{*opapolicy.MockFrameworkA()}
// opaSessionObj.Frameworks[0].Controls[0].Rules[0] = *opapolicy.MockRuleB()
// // run test
// reportB, errB := opaProcessor.runRegoOnK8s(opapolicy.MockRuleB(), opaSessionObj)
// if errB != nil {
// t.Errorf("%v", errB)
// return
// }
// if len(reportB.RuleResponses) == 0 {
// t.Errorf("len(report.RuleResponses) == 0")
// return
// }
// bla, _ := json.Marshal(reportB.RuleResponses[0])
// t.Errorf("%s", bla)
// }
func TestNewRego(t *testing.T) {
// TODO - remove before testing
return
// k8sConfig := k8sinterface.GetK8sConfig()
// t.Errorf(fmt.Sprintf("%v", k8sConfig.String()))
// t.Errorf(fmt.Sprintf("%v", k8sConfig.AuthProvider.Config))
// return
ruleName := "some rule"
rule := opapolicy.MockTemp()
allResources := []schema.GroupVersionResource{
{Group: "api-versions", Version: "", Resource: ""},
}
namespace := ""
k8sinterface.K8SConfig = nil
// compile modules
modules, err := getRuleDependencies()
if err != nil {
t.Errorf("err: %v", err)
return
}
modules[ruleName] = rule
compiled, err := ast.CompileModules(modules)
if err != nil {
t.Errorf("err: %v", err)
return
}
opaProcessor := NewOPAProcessorMock()
k8s := k8sinterface.NewKubernetesApi()
// set dynamic object
var clientResource dynamic.ResourceInterface
recourceList := []unstructured.Unstructured{}
for i := range allResources {
if namespace != "" {
clientResource = k8s.DynamicClient.Resource(allResources[i]).Namespace(namespace)
} else {
clientResource = k8s.DynamicClient.Resource(allResources[i])
opap := NewOPAProcessor(opaSessionObj)
opap.Process()
opap.updateResults()
for _, f := range opap.PostureReport.FrameworkReports {
for _, c := range f.ControlReports {
for _, r := range c.RuleReports {
for _, rr := range r.RuleResponses {
// t.Errorf("AlertMessage: %v", rr.AlertMessage)
if rr.Exception != nil {
t.Errorf("Exception: %v", rr.Exception)
}
}
}
}
l, err := clientResource.List(context.Background(), metav1.ListOptions{})
if err != nil {
t.Errorf("err: %v", err)
return
}
recourceList = append(recourceList, l.Items...)
}
inputObj := k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.FilterOutOwneredResources(recourceList))
// inputObj := k8sinterface.ConvertUnstructuredSliceToMap(l.Items)
result, err := opaProcessor.regoEval(inputObj, compiled)
if err != nil {
t.Errorf("%v", err)
return
}
resb, _ := json.Marshal(result)
t.Errorf("result: %s", resb)
}

View File

@@ -1,12 +1,17 @@
package opaprocessor
import (
"github.com/armosec/kubescape/cautils"
"encoding/json"
"fmt"
pkgcautils "github.com/armosec/kubescape/cautils/cautils"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
resources "github.com/armosec/kubescape/cautils/opapolicy/resources"
"github.com/open-policy-agent/opa/rego"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -49,6 +54,73 @@ func getRuleDependencies() (map[string]string, error) {
}
return modules, nil
}
func parseRegoResult(regoResult *rego.ResultSet) ([]opapolicy.RuleResponse, error) {
var errs error
ruleResponses := []opapolicy.RuleResponse{}
for _, result := range *regoResult {
for desicionIdx := range result.Expressions {
if resMap, ok := result.Expressions[desicionIdx].Value.(map[string]interface{}); ok {
for objName := range resMap {
jsonBytes, err := json.Marshal(resMap[objName])
if err != nil {
err = fmt.Errorf("in parseRegoResult, json.Marshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
desObj := make([]opapolicy.RuleResponse, 0)
if err := json.Unmarshal(jsonBytes, &desObj); err != nil {
err = fmt.Errorf("in parseRegoResult, json.Unmarshal failed. name: %s, obj: %v, reason: %s", objName, resMap[objName], err)
glog.Error(err)
errs = fmt.Errorf("%s\n%s", errs, err)
continue
}
ruleResponses = append(ruleResponses, desObj...)
}
}
}
}
return ruleResponses, errs
}
//editRuleResponses editing the responses -> removing duplications, clearing secret data, etc.
func editRuleResponses(ruleResponses []opapolicy.RuleResponse) []opapolicy.RuleResponse {
uniqueRuleResponses := map[string]bool{}
lenRuleResponses := len(ruleResponses)
for i := 0; i < lenRuleResponses; i++ {
for j := range ruleResponses[i].AlertObject.K8SApiObjects {
w := k8sinterface.NewWorkloadObj(ruleResponses[i].AlertObject.K8SApiObjects[j])
if w == nil {
continue
}
resourceID := fmt.Sprintf("%s/%s/%s/%s", w.GetApiVersion(), w.GetNamespace(), w.GetKind(), w.GetName())
if found := uniqueRuleResponses[resourceID]; found {
// resource found -> remove from slice
ruleResponses = removeFromSlice(ruleResponses, i)
lenRuleResponses -= 1
break
} else {
cleanRuleResponses(w)
ruleResponses[i].AlertObject.K8SApiObjects[j] = w.GetWorkload()
uniqueRuleResponses[resourceID] = true
}
}
}
return ruleResponses
}
func cleanRuleResponses(workload k8sinterface.IWorkload) {
if workload.GetKind() == "Secret" {
workload.RemoveSecretData()
}
}
func removeFromSlice(ruleResponses []opapolicy.RuleResponse, i int) []opapolicy.RuleResponse {
if i != len(ruleResponses)-1 {
ruleResponses[i] = ruleResponses[len(ruleResponses)-1]
}
return ruleResponses[:len(ruleResponses)-1]
}
func ruleWithArmoOpaDependency(annotations map[string]interface{}) bool {
if annotations == nil {

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
@@ -16,6 +16,7 @@ type PolicyHandler struct {
k8s *k8sinterface.KubernetesApi
// we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func
processPolicy *chan *cautils.OPASessionObj
getters *cautils.Getters
}
// CreatePolicyHandler Create ws-handler obj
@@ -30,9 +31,10 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
// validate notification
// TODO
policyHandler.getters = &scanInfo.Getters
// get policies
frameworks, err := policyHandler.getPolicies(notification, scanInfo.PolicyGetter)
frameworks, exceptions, err := policyHandler.getPolicies(notification)
if err != nil {
return err
}
@@ -40,6 +42,7 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return fmt.Errorf("empty list of frameworks")
}
opaSessionObj.Frameworks = frameworks
opaSessionObj.Exceptions = exceptions
k8sResources, err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
if err != nil {
@@ -55,22 +58,22 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return nil
}
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification, policyGetter getter.IPolicyGetter) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
frameworks, err := policyHandler.GetPoliciesFromBackend(notification, policyGetter)
frameworks, exceptions, err := policyHandler.GetPoliciesFromBackend(notification)
if err != nil {
return frameworks, err
return frameworks, exceptions, err
}
if len(frameworks) == 0 {
err := fmt.Errorf("could not download any policies, please check previous logs")
return frameworks, err
return frameworks, exceptions, err
}
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
return frameworks, nil
return frameworks, exceptions, nil
}
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {

View File

@@ -3,25 +3,28 @@ package policyhandler
import (
"fmt"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification, getPolicies getter.IPolicyGetter) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
var errs error
// d := getter.NewArmoAPI()
frameworks := []opapolicy.Framework{}
exceptionPolicies := []armotypes.PostureExceptionPolicy{}
// Get - cacli opa get
for _, rule := range notification.Rules {
switch rule.Kind {
case opapolicy.KindFramework:
// backend
receivedFramework, err := getPolicies.GetFramework(rule.Name)
receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
if err != nil {
errs = err
errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
}
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
if recExceptionPolicies != nil {
exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
}
}
default:
@@ -29,5 +32,19 @@ func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapoli
errs = fmt.Errorf("%s", err.Error())
}
}
return frameworks, errs
return frameworks, exceptionPolicies, errs
}
func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(policyName)
if err != nil {
return nil, nil, err
}
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "")
if err != nil {
return receivedFramework, nil, err
}
return receivedFramework, receivedException, nil
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -0,0 +1,56 @@
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
}
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
}

View File

@@ -0,0 +1,57 @@
package reporter
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/armosec/kubescape/cautils"
"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 = "report.euprod1.cyberarmorsoft.com"
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()
if reportID != "" {
q.Add("reportID", reportID) // TODO - do we add the reportID?
}
host.RawQuery = q.Encode()
return host.String()
}

View 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)
}
}

View 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
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
return score
}

View 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)
}

View 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")
// }
// }

View 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
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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
View 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
}

View 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
}

View 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)
}
}

View File

@@ -0,0 +1 @@
package score