mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-19 20:39:56 +00:00
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cfcc6d930 | ||
|
|
8c6f618743 | ||
|
|
6adf1c3162 | ||
|
|
9f5f4f1832 | ||
|
|
9f49cc83e9 | ||
|
|
67972199ce | ||
|
|
9d5db86bf3 | ||
|
|
39efed5fc1 | ||
|
|
21c2bf22dd | ||
|
|
6c94b3a423 | ||
|
|
683248db0b | ||
|
|
9032400528 | ||
|
|
1a925b1acf | ||
|
|
dadc8c2c60 | ||
|
|
01c1b44bfc | ||
|
|
a394a99d8f | ||
|
|
4213707b7f | ||
|
|
d53b8272ee | ||
|
|
fdd688ac68 | ||
|
|
5bb961bdc6 | ||
|
|
9e7cc06f97 | ||
|
|
1d184d9000 | ||
|
|
a5e2ebf647 | ||
|
|
29eb573de5 | ||
|
|
ec6c3da5ec | ||
|
|
e2d4f8961e | ||
|
|
a48c680201 | ||
|
|
c869f2c962 | ||
|
|
f77fc9a06d | ||
|
|
e12eae93b9 | ||
|
|
d92fb32574 | ||
|
|
541dba3d79 | ||
|
|
033ed17125 | ||
|
|
aaeb663d15 | ||
|
|
c337005985 | ||
|
|
192eeee348 | ||
|
|
27c97684b9 | ||
|
|
41d5fa70ed | ||
|
|
4206e9c175 | ||
|
|
8658bb05dd | ||
|
|
9b707016a9 | ||
|
|
4b02826883 | ||
|
|
b29774ea71 | ||
|
|
bf68e90a8e | ||
|
|
cc5cdcd831 | ||
|
|
07f23ff7d9 | ||
|
|
2985da6dc9 | ||
|
|
1523973749 | ||
|
|
ccafd78a14 | ||
|
|
4f71fe0d55 | ||
|
|
7bd6b6b4d1 | ||
|
|
2b976489a2 | ||
|
|
1440f20f95 | ||
|
|
941e7e27c0 | ||
|
|
5428c6ab2f | ||
|
|
851bb65d17 | ||
|
|
a3ce04b7e8 | ||
|
|
4d68ca6aa2 | ||
|
|
29c6767d3c | ||
|
|
d8f5f7975c | ||
|
|
4cd8476837 | ||
|
|
112257449f | ||
|
|
ff9cf4adf0 | ||
|
|
f2d387bc9c | ||
|
|
2ceb5150e2 | ||
|
|
00006ec721 | ||
|
|
00aa6948ab | ||
|
|
aad32ec965 | ||
|
|
bd24ed3af7 | ||
|
|
9fc455bcec | ||
|
|
0f8ba1e7e8 | ||
|
|
d3137af3d7 | ||
|
|
775dd037d6 | ||
|
|
49cbfe130c | ||
|
|
9cd61dd996 | ||
|
|
292c4aa060 | ||
|
|
56a265930d | ||
|
|
4171d110a4 | ||
|
|
2568241ef8 | ||
|
|
d0775565e9 | ||
|
|
3c6b2db919 | ||
|
|
ca4d4a096c | ||
|
|
ff27db6b83 | ||
|
|
c9ecb6c563 | ||
|
|
b9e5782264 | ||
|
|
d852f81cb0 | ||
|
|
6137aa5d8e | ||
|
|
131b67ee83 | ||
|
|
db6f00be08 | ||
|
|
2ecc80985a | ||
|
|
93dbfd5110 | ||
|
|
ae0c384c85 | ||
|
|
f60ff1fb26 | ||
|
|
08a81696a1 | ||
|
|
8375a8ae63 | ||
|
|
31d8cf5118 | ||
|
|
597b967e55 | ||
|
|
679238ec13 | ||
|
|
94884ac3d7 | ||
|
|
0ef8f20c50 | ||
|
|
82f3d62de5 | ||
|
|
46f1e6a83b | ||
|
|
65841a014f | ||
|
|
985c6868c1 | ||
|
|
fca862b2c7 | ||
|
|
77a9956d91 | ||
|
|
3a4a58fdd5 | ||
|
|
a1e639453d | ||
|
|
7da23c111e | ||
|
|
768556251d | ||
|
|
00fcc565b5 | ||
|
|
9c74e5c93b | ||
|
|
6a0ee6e0d7 | ||
|
|
93bb09d78e | ||
|
|
228e7703a8 | ||
|
|
4b15a3b8e0 | ||
|
|
80c5fd7439 | ||
|
|
504c4acc42 | ||
|
|
573d85d770 | ||
|
|
4247f66378 | ||
|
|
7d6a10e787 | ||
|
|
bad303692e | ||
|
|
af3b33f7b0 | ||
|
|
fd66b2eba5 | ||
|
|
157ba1a08d | ||
|
|
6b15e6575b | ||
|
|
53f3229e9f | ||
|
|
186435de69 | ||
|
|
4d027d691f | ||
|
|
3f84ee3fcc | ||
|
|
38103ac90b | ||
|
|
13d27697e1 | ||
|
|
942f356d19 | ||
|
|
b87b687e2f | ||
|
|
2e313719bb | ||
|
|
0c5eb48fdb | ||
|
|
2ae2c81e0b | ||
|
|
23090fbb9f | ||
|
|
fcb6255f75 | ||
|
|
abbc9c3e2e | ||
|
|
75f76fcecd | ||
|
|
a00bba5fe4 | ||
|
|
96473188ed | ||
|
|
56264df047 | ||
|
|
c3008981e4 | ||
|
|
572130d797 | ||
|
|
7d8cf37532 | ||
|
|
ebd9661255 | ||
|
|
05e108b47b | ||
|
|
8cb6824f3c | ||
|
|
304017fc41 | ||
|
|
b82673f694 | ||
|
|
ac7e5219f1 | ||
|
|
a4c4f9c6ed | ||
|
|
222b154505 | ||
|
|
67c2de74f1 | ||
|
|
4a9b36807a | ||
|
|
c6241fab38 | ||
|
|
afbc69c6d2 | ||
|
|
571a15bee8 | ||
|
|
8a00a5c54b | ||
|
|
8f8aaf70d9 | ||
|
|
2779cb4e25 | ||
|
|
f46ee93539 | ||
|
|
3eb087e5c1 | ||
|
|
59c935e723 | ||
|
|
bae45d277f | ||
|
|
0b6dfa9cd0 | ||
|
|
1ff3a6c92c | ||
|
|
f75cee0d78 | ||
|
|
229f16cb01 | ||
|
|
2c6b1a440f | ||
|
|
37afc1352f | ||
|
|
9943119033 | ||
|
|
41457ff551 | ||
|
|
82b64b5828 | ||
|
|
229e8acc74 | ||
|
|
30324e1c01 | ||
|
|
8ca356eae7 | ||
|
|
29f4ae368d | ||
|
|
409080f51b | ||
|
|
0b24c46279 | ||
|
|
49596c5ac1 | ||
|
|
9bf79db8f8 |
12
.github/workflows/build.yaml
vendored
12
.github/workflows/build.yaml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [ closed ]
|
||||
|
||||
jobs:
|
||||
once:
|
||||
name: Create release
|
||||
@@ -38,11 +37,16 @@ jobs:
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
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
|
||||
env:
|
||||
RELEASE: v1.0.${{ github.run_number }}
|
||||
ArmoBEServer: api.armo.cloud
|
||||
ArmoERServer: report.euprod1.cyberarmorsoft.com
|
||||
ArmoWebsite: portal.armo.cloud
|
||||
CGO_ENABLED: 0
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s -X github.com/armosec/kubescape/cmd.BuildNumber=$RELEASE -X github.com/armosec/kubescape/cautils/getter.ArmoBEURL=$ArmoBEServer -X github.com/armosec/kubescape/cautils/getter.ArmoERURL=$ArmoERServer -X github.com/armosec/kubescape/cautils/getter.ArmoFEURL=$ArmoWebsite" -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:
|
||||
|
||||
36
.github/workflows/build_dev.yaml
vendored
Normal file
36
.github/workflows/build_dev.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: build-dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ dev ]
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
types: [ closed ]
|
||||
jobs:
|
||||
build:
|
||||
name: Create cross-platform dev build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
- name: Build
|
||||
env:
|
||||
RELEASE: v1.0.${{ github.run_number }}
|
||||
ArmoBEServer: api.armo.cloud
|
||||
ArmoERServer: report.euprod1.cyberarmorsoft.com
|
||||
ArmoWebsite: portal.armo.cloud
|
||||
CGO_ENABLED: 0
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s -X github.com/armosec/kubescape/cmd.BuildNumber=$RELEASE -X github.com/armosec/kubescape/cautils/getter.ArmoBEURL=$ArmoBEServer -X github.com/armosec/kubescape/cautils/getter.ArmoERURL=$ArmoERServer -X github.com/armosec/kubescape/cautils/getter.ArmoFEURL=$ArmoWebsite" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: kubescape-${{ matrix.os }}
|
||||
path: build/${{ matrix.os }}/kubescape
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*.vs*
|
||||
*go.sum*
|
||||
*kubescape*
|
||||
*debug*
|
||||
*debug*
|
||||
.idea
|
||||
81
README.md
81
README.md
@@ -1,7 +1,6 @@
|
||||
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
|
||||
|
||||
[](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
|
||||
[](https://github.com/armosec/kubescape)
|
||||
[](https://goreportcard.com/report/github.com/armosec/kubescape)
|
||||
|
||||
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
@@ -28,20 +27,22 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
|
||||
<img src="docs/summary.png">
|
||||
|
||||
|
||||
### Flags
|
||||
|
||||
| flag | default | description | options |
|
||||
| --- | --- | --- | --- |
|
||||
| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces |
|
||||
| `-s`/`--silent` | Display progress messages | Silent progress messages |
|
||||
| `-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). If not set will download exceptions from Armo management portal |
|
||||
| `--results-locally` | `false` | Kubescape sends scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features | `true`/`false`|
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
### Pre-Deployment Testing
|
||||
Check your YAML files before you're deploying, simply add them at the end of command line:
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
### Integration with other tools
|
||||
|
||||
Kubescape can produce output fitting for later processing:
|
||||
* JSON (`-f json`)
|
||||
* JUnit XML (`-f junit`)
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
|
||||
@@ -49,32 +50,37 @@ Kubescape can produce output fitting for later processing:
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files
|
||||
* Scan local `yaml`/`json` files before deploying
|
||||
```
|
||||
kubescape scan framework nsa examples/online-boutique/*
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
|
||||
* Scan `yaml`/`json` files from url
|
||||
* 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
|
||||
* Output in `json` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
|
||||
```
|
||||
|
||||
* Output in `junit xml` format
|
||||
* 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 template and pass as stdout
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
```
|
||||
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
|
||||
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
for example:
|
||||
@@ -82,6 +88,23 @@ for example:
|
||||
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
### Offline Support <img src="docs/new-feature.svg">
|
||||
|
||||
It is possible to run Kubescape offline!
|
||||
|
||||
First download the framework and then scan with `--use-from` flag
|
||||
|
||||
* Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
|
||||
```
|
||||
kubescape download framework nsa --output nsa.json
|
||||
```
|
||||
|
||||
* Scan using the downloaded framework
|
||||
```
|
||||
kubescape scan framework nsa --use-from nsa.json
|
||||
```
|
||||
|
||||
|
||||
# How to build
|
||||
|
||||
Note: development (and the release process) is done with Go `1.16`
|
||||
@@ -103,6 +126,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
|
||||
@@ -126,7 +161,9 @@ Kubescape is running the following tests according to what is defined by [Kubern
|
||||
* Linux hardening
|
||||
* Ingress and Egress blocked
|
||||
* Container hostPort
|
||||
* Anonymous requests
|
||||
* Network policies
|
||||
* Symlink Exchange Can Allow Host Filesystem Access (CVE-2021-25741)
|
||||
|
||||
|
||||
|
||||
## Technology
|
||||
|
||||
13
build/Dockerfile
Normal file
13
build/Dockerfile
Normal 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"]
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package armotypes
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
CostumerGuidQuery = "costumerGUID"
|
||||
ClusterNameQuery = "cluster"
|
||||
@@ -22,6 +24,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>
|
||||
@@ -41,10 +44,16 @@ const (
|
||||
DesignatorSid DesignatorType = "Sid" // secret id
|
||||
)
|
||||
|
||||
func (dt DesignatorType) ToLower() DesignatorType {
|
||||
return DesignatorType(strings.ToLower(string(dt)))
|
||||
}
|
||||
|
||||
// attributes
|
||||
const (
|
||||
AttributeCluster = "cluster"
|
||||
AttributeNamespace = "namespace"
|
||||
AttributeKind = "kind"
|
||||
AttributeName = "name"
|
||||
)
|
||||
|
||||
// PortalDesignator represented single designation options
|
||||
|
||||
@@ -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.ToLower() {
|
||||
case DesignatorAttributes.ToLower(), DesignatorAttribute.ToLower():
|
||||
return designator.DigestAttributesDesignator()
|
||||
case DesignatorWlid.ToLower(), DesignatorWildWlid.ToLower():
|
||||
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
|
||||
}
|
||||
|
||||
42
cautils/armotypes/postureexceptionpolicytypes.go
Normal file
42
cautils/armotypes/postureexceptionpolicytypes.go
Normal 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
|
||||
}
|
||||
1
cautils/armotypes/postureexceptionpolicytypesutils.go
Normal file
1
cautils/armotypes/postureexceptionpolicytypesutils.go
Normal file
@@ -0,0 +1 @@
|
||||
package armotypes
|
||||
325
cautils/customerloader.go
Normal file
325
cautils/customerloader.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
configMapName = "kubescape"
|
||||
ConfigFileName = "config"
|
||||
)
|
||||
|
||||
type ConfigObj struct {
|
||||
CustomerGUID string `json:"customerGUID"`
|
||||
Token string `json:"invitationParam"`
|
||||
CustomerAdminEMail string `json:"adminMail"`
|
||||
}
|
||||
|
||||
func (co *ConfigObj) Json() []byte {
|
||||
if b, err := json.Marshal(co); err == nil {
|
||||
return b
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
type IClusterConfig interface {
|
||||
SetCustomerGUID() error
|
||||
GetCustomerGUID() string
|
||||
GenerateURL()
|
||||
}
|
||||
|
||||
type ClusterConfig struct {
|
||||
k8s *k8sinterface.KubernetesApi
|
||||
defaultNS string
|
||||
armoAPI *getter.ArmoAPI
|
||||
configObj *ConfigObj
|
||||
}
|
||||
|
||||
type EmptyConfig struct {
|
||||
}
|
||||
|
||||
func (c *EmptyConfig) GenerateURL() {
|
||||
}
|
||||
|
||||
func (c *EmptyConfig) SetCustomerGUID() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *EmptyConfig) GetCustomerGUID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func NewEmptyConfig() *EmptyConfig {
|
||||
return &EmptyConfig{}
|
||||
}
|
||||
|
||||
func NewClusterConfig(k8s *k8sinterface.KubernetesApi, armoAPI *getter.ArmoAPI) *ClusterConfig {
|
||||
return &ClusterConfig{
|
||||
k8s: k8s,
|
||||
armoAPI: armoAPI,
|
||||
defaultNS: k8sinterface.GetDefaultNamespace(),
|
||||
}
|
||||
}
|
||||
func createConfigJson() {
|
||||
ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), nil, 0664)
|
||||
|
||||
}
|
||||
|
||||
func update(configObj *ConfigObj) {
|
||||
ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), configObj.Json(), 0664)
|
||||
}
|
||||
func (c *ClusterConfig) GenerateURL() {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = getter.ArmoFEURL
|
||||
if c.configObj == nil {
|
||||
return
|
||||
}
|
||||
if c.configObj.CustomerAdminEMail != "" {
|
||||
msgStr := fmt.Sprintf("To view all controls and get remediations ask access permissions to %s from %s", u.String(), c.configObj.CustomerAdminEMail)
|
||||
InfoTextDisplay(os.Stdout, msgStr+"\n")
|
||||
return
|
||||
}
|
||||
u.Path = "account/sign-up"
|
||||
q := u.Query()
|
||||
q.Add("invitationToken", c.configObj.Token)
|
||||
q.Add("customerGUID", c.configObj.CustomerGUID)
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
fmt.Println("To view all controls and get remediations visit:")
|
||||
InfoTextDisplay(os.Stdout, u.String()+"\n")
|
||||
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetCustomerGUID() string {
|
||||
if c.configObj != nil {
|
||||
return c.configObj.CustomerGUID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) GetValueByKeyFromConfigMap(key string) (string, error) {
|
||||
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if val, ok := configMap.Data[key]; ok {
|
||||
return val, nil
|
||||
} else {
|
||||
return "", fmt.Errorf("value does not exist")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetValueFromConfigJson(key string) (string, error) {
|
||||
data, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
if val, ok := obj[key]; ok {
|
||||
return fmt.Sprint(val), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("value does not exist")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SetKeyValueInConfigJson(key string, value string) error {
|
||||
data, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var obj map[string]interface{}
|
||||
err = json.Unmarshal(data, &obj)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj[key] = value
|
||||
newData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), newData, 0664)
|
||||
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error {
|
||||
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
configMap = &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if len(configMap.Data) == 0 {
|
||||
configMap.Data = make(map[string]string)
|
||||
}
|
||||
|
||||
configMap.Data[key] = value
|
||||
|
||||
if err != nil {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
} else {
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) SetCustomerGUID() error {
|
||||
|
||||
// get from file
|
||||
if existsConfigJson() {
|
||||
c.configObj, _ = loadConfigFromFile()
|
||||
} else if c.existsConfigMap() {
|
||||
c.configObj, _ = c.loadConfigFromConfigMap()
|
||||
} else {
|
||||
c.createConfigMap()
|
||||
createConfigJson()
|
||||
}
|
||||
|
||||
customerGUID := c.GetCustomerGUID()
|
||||
// get from armoBE
|
||||
tenantResponse, err := c.armoAPI.GetCustomerGUID(customerGUID)
|
||||
|
||||
if err == nil && tenantResponse != nil {
|
||||
if tenantResponse.AdminMail != "" { // this customer already belongs to some user
|
||||
if existsConfigJson() {
|
||||
update(&ConfigObj{CustomerGUID: customerGUID, CustomerAdminEMail: tenantResponse.AdminMail})
|
||||
}
|
||||
if c.existsConfigMap() {
|
||||
c.configObj.CustomerAdminEMail = tenantResponse.AdminMail
|
||||
c.updateConfigMap()
|
||||
}
|
||||
} else {
|
||||
if existsConfigJson() {
|
||||
update(&ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token})
|
||||
}
|
||||
if c.existsConfigMap() {
|
||||
c.configObj = &ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token}
|
||||
c.updateConfigMap()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err != nil && strings.Contains(err.Error(), "Invitation for tenant already exists") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) {
|
||||
if c.k8s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bData, err := json.Marshal(configMap.Data); err == nil {
|
||||
return readConfig(bData)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) existsConfigMap() bool {
|
||||
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func existsConfigJson() bool {
|
||||
_, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
|
||||
|
||||
return err == nil
|
||||
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) createConfigMap() error {
|
||||
if c.k8s == nil {
|
||||
return nil
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
},
|
||||
}
|
||||
c.updateConfigData(configMap)
|
||||
|
||||
_, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Create(context.Background(), configMap, metav1.CreateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) updateConfigMap() error {
|
||||
if c.k8s == nil {
|
||||
return nil
|
||||
}
|
||||
configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.updateConfigData(configMap)
|
||||
|
||||
_, err = c.k8s.KubernetesClient.CoreV1().ConfigMaps(configMap.Namespace).Update(context.Background(), configMap, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) {
|
||||
if len(configMap.Data) == 0 {
|
||||
configMap.Data = make(map[string]string)
|
||||
}
|
||||
m := c.ToMapString()
|
||||
for k, v := range m {
|
||||
if s, ok := v.(string); ok {
|
||||
configMap.Data[k] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
func loadConfigFromFile() (*ConfigObj, error) {
|
||||
dat, err := ioutil.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return readConfig(dat)
|
||||
}
|
||||
func readConfig(dat []byte) (*ConfigObj, error) {
|
||||
|
||||
if len(dat) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
configObj := &ConfigObj{}
|
||||
err := json.Unmarshal(dat, configObj)
|
||||
|
||||
return configObj, err
|
||||
}
|
||||
func (c *ClusterConfig) ToMapString() map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
bc, _ := json.Marshal(c.configObj)
|
||||
json.Unmarshal(bc, &m)
|
||||
return m
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
6
cautils/downloadinfo.go
Normal file
6
cautils/downloadinfo.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package cautils
|
||||
|
||||
type DownloadInfo struct {
|
||||
Path string
|
||||
FrameworkName string
|
||||
}
|
||||
88
cautils/getter/armoapi.go
Normal file
88
cautils/getter/armoapi.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// =======================================================================================================================
|
||||
// =============================================== ArmoAPI ===============================================================
|
||||
// =======================================================================================================================
|
||||
|
||||
var (
|
||||
// ATTENTION!!!
|
||||
// Changes in this URLs variable names, or in the usage is affecting the build process! BE CAREFULL
|
||||
ArmoBEURL = "eggdashbe.eudev3.cyberarmorsoft.com"
|
||||
ArmoERURL = "report.eudev3.cyberarmorsoft.com"
|
||||
ArmoFEURL = "armoui.eudev3.cyberarmorsoft.com"
|
||||
// ArmoURL = "https://dashbe.euprod1.cyberarmorsoft.com"
|
||||
)
|
||||
|
||||
// Armo API for downloading policies
|
||||
type ArmoAPI struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
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+".json"))
|
||||
|
||||
return framework, err
|
||||
}
|
||||
|
||||
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) GetCustomerGUID(customerGUID string) (*TenantResponse, error) {
|
||||
url := armoAPI.getCustomerURL()
|
||||
if customerGUID != "" {
|
||||
url = fmt.Sprintf("%s?customerGUID=%s", url, customerGUID)
|
||||
}
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenant := &TenantResponse{}
|
||||
if err = JSONDecoder(respStr).Decode(tenant); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
type TenantResponse struct {
|
||||
TenantID string `json:"tenantId"`
|
||||
Token string `json:"token"`
|
||||
Expires string `json:"expires"`
|
||||
AdminMail string `json:"adminMail,omitempty"`
|
||||
}
|
||||
44
cautils/getter/armoapiutils.go
Normal file
44
cautils/getter/armoapiutils.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "v1/armoFrameworks"
|
||||
q := u.Query()
|
||||
q.Add("customerGUID", "11111111-1111-1111-1111-111111111111")
|
||||
q.Add("frameworkName", strings.ToUpper(frameworkName))
|
||||
q.Add("getRules", "true")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "api/v1/armoPostureExceptions"
|
||||
|
||||
q := u.Query()
|
||||
q.Add("customerGUID", customerGUID)
|
||||
// if clusterName != "" { // TODO - fix customer name support in Armo BE
|
||||
// q.Add("clusterName", clusterName)
|
||||
// }
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getCustomerURL() string {
|
||||
u := url.URL{}
|
||||
u.Scheme = "https"
|
||||
u.Host = ArmoBEURL
|
||||
u.Path = "api/v1/createTenant"
|
||||
return u.String()
|
||||
}
|
||||
85
cautils/getter/downloadreleasedpolicy.go
Normal file
85
cautils/getter/downloadreleasedpolicy.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"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) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
if err := drp.setURL(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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+".json"))
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to download '%s' - not found", frameworkName)
|
||||
|
||||
}
|
||||
14
cautils/getter/getpolicies.go
Normal file
14
cautils/getter/getpolicies.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type IPolicyGetter interface {
|
||||
GetFramework(name string) (*opapolicy.Framework, error)
|
||||
}
|
||||
|
||||
type IExceptionsGetter interface {
|
||||
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
}
|
||||
114
cautils/getter/getpoliciesutils.go
Normal file
114
cautils/getter/getpoliciesutils.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func GetDefaultPath(name string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, name)
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
defaultfilePath = filepath.Join(homeDir, defaultfilePath)
|
||||
}
|
||||
return defaultfilePath
|
||||
}
|
||||
|
||||
func SaveFrameworkInFile(framework *opapolicy.Framework, path string) error {
|
||||
encodedData, err := json.Marshal(framework)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(path, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONDecoder returns JSON decoder for given string
|
||||
func JSONDecoder(origin string) *json.Decoder {
|
||||
dec := json.NewDecoder(strings.NewReader(origin))
|
||||
dec.UseNumber()
|
||||
return dec
|
||||
}
|
||||
|
||||
func HttpGetter(httpClient *http.Client, fullURL string) (string, error) {
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
respStr, err := httpRespToString(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return respStr, nil
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
bytesNum, err := io.Copy(&strBuilder, resp.Body)
|
||||
respStr := strBuilder.String()
|
||||
if err != nil {
|
||||
respStrNewLen := len(respStr)
|
||||
if respStrNewLen > 1024 {
|
||||
respStrNewLen = 1024
|
||||
}
|
||||
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
respStrNewLen := len(respStr)
|
||||
if respStrNewLen > 1024 {
|
||||
respStrNewLen = 1024
|
||||
}
|
||||
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
|
||||
}
|
||||
|
||||
return respStr, err
|
||||
}
|
||||
|
||||
// URLEncoder encode url
|
||||
func urlEncoder(oldURL string) string {
|
||||
fullURL := strings.Split(oldURL, "?")
|
||||
baseURL, err := url.Parse(fullURL[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prepare Query Parameters
|
||||
if len(fullURL) > 1 {
|
||||
params := url.Values{}
|
||||
queryParams := strings.Split(fullURL[1], "&")
|
||||
for _, i := range queryParams {
|
||||
queryParam := strings.Split(i, "=")
|
||||
val := ""
|
||||
if len(queryParam) > 1 {
|
||||
val = queryParam[1]
|
||||
}
|
||||
params.Add(queryParam[0], val)
|
||||
}
|
||||
baseURL.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
return baseURL.String()
|
||||
}
|
||||
54
cautils/getter/loadpolicy.go
Normal file
54
cautils/getter/loadpolicy.go
Normal 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
23
cautils/jsonutils.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
empty = ""
|
||||
tab = " "
|
||||
)
|
||||
|
||||
func PrettyJson(data interface{}) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetIndent(empty, tab)
|
||||
|
||||
err := encoder.Encode(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
@@ -4,16 +4,20 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// DO NOT REMOVE - load cloud providers auth
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
var ConnectedToCluster = true
|
||||
|
||||
// K8SConfig pointer to k8s config
|
||||
var K8SConfig *restclient.Config
|
||||
|
||||
@@ -26,8 +30,15 @@ type KubernetesApi struct {
|
||||
|
||||
// NewKubernetesApi -
|
||||
func NewKubernetesApi() *KubernetesApi {
|
||||
var kubernetesClient *kubernetes.Clientset
|
||||
var err error
|
||||
|
||||
kubernetesClient, err := kubernetes.NewForConfig(GetK8sConfig())
|
||||
if !IsConnectedToCluster() {
|
||||
fmt.Println(fmt.Errorf("failed to load kubernetes config: no configuration has been provided, try setting KUBECONFIG environment variable"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
kubernetesClient, err = kubernetes.NewForConfig(GetK8sConfig())
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to load config file, reason: %s", err.Error())
|
||||
os.Exit(1)
|
||||
@@ -52,23 +63,54 @@ var RunningIncluster bool
|
||||
func LoadK8sConfig() error {
|
||||
kubeconfig, err := config.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load kubernetes config: %s\n", err)
|
||||
return fmt.Errorf("failed to load kubernetes config: %s", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
}
|
||||
if _, err := restclient.InClusterConfig(); err == nil {
|
||||
RunningIncluster = true
|
||||
}
|
||||
|
||||
K8SConfig = kubeconfig
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetK8sConfig get config. load if not loaded yer
|
||||
// GetK8sConfig get config. load if not loaded yet
|
||||
func GetK8sConfig() *restclient.Config {
|
||||
if K8SConfig == nil {
|
||||
if err := LoadK8sConfig(); err != nil {
|
||||
// print error
|
||||
fmt.Printf("%s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if !IsConnectedToCluster() {
|
||||
return nil
|
||||
}
|
||||
return K8SConfig
|
||||
}
|
||||
|
||||
func IsConnectedToCluster() bool {
|
||||
if K8SConfig == nil {
|
||||
if err := LoadK8sConfig(); err != nil {
|
||||
ConnectedToCluster = false
|
||||
}
|
||||
}
|
||||
return ConnectedToCluster
|
||||
}
|
||||
func GetClusterName() string {
|
||||
if !ConnectedToCluster {
|
||||
return ""
|
||||
}
|
||||
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{})
|
||||
config, err := kubeConfig.RawConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// TODO - Handle if empty
|
||||
return config.CurrentContext
|
||||
}
|
||||
|
||||
func GetDefaultNamespace() string {
|
||||
clientCfg, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
|
||||
if err != nil {
|
||||
return "default"
|
||||
}
|
||||
namespace := clientCfg.Contexts[clientCfg.CurrentContext].Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package opapolicy
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
armotypes "github.com/armosec/kubescape/cautils/armotypes"
|
||||
@@ -17,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 {
|
||||
@@ -33,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"`
|
||||
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:"-"`
|
||||
@@ -93,6 +101,7 @@ type PolicyRule struct {
|
||||
// Control represents a collection of rules which are combined together to single purpose
|
||||
type Control struct {
|
||||
armotypes.PortalBase `json:",inline"`
|
||||
ControlID string `json:"id"`
|
||||
CreationTime string `json:"creationTime"`
|
||||
Description string `json:"description"`
|
||||
Remediation string `json:"remediation"`
|
||||
@@ -150,43 +159,3 @@ type PolicyIdentifier struct {
|
||||
Kind NotificationPolicyKind `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ScanInfo struct {
|
||||
PolicyIdentifier PolicyIdentifier
|
||||
Format string
|
||||
Output string
|
||||
ExcludedNamespaces string
|
||||
InputPatterns []string
|
||||
Silent bool
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) Init() {
|
||||
// scanInfo.setSilentMode()
|
||||
scanInfo.setOutputFile()
|
||||
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setSilentMode() {
|
||||
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
|
||||
scanInfo.Silent = true
|
||||
}
|
||||
if scanInfo.Output != "" {
|
||||
scanInfo.Silent = true
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setOutputFile() {
|
||||
if scanInfo.Output == "" {
|
||||
return
|
||||
}
|
||||
if scanInfo.Format == "json" {
|
||||
if filepath.Ext(scanInfo.Output) != "json" {
|
||||
scanInfo.Output += ".json"
|
||||
}
|
||||
}
|
||||
if scanInfo.Format == "junit" {
|
||||
if filepath.Ext(scanInfo.Output) != "xml" {
|
||||
scanInfo.Output += ".xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ func MockFrameworkReportA() *FrameworkReport {
|
||||
Name: AMockFrameworkName,
|
||||
ControlReports: []ControlReport{
|
||||
{
|
||||
Name: AMockControlName,
|
||||
ControlID: "C-0010",
|
||||
Name: AMockControlName,
|
||||
RuleReports: []RuleReport{
|
||||
{
|
||||
Name: AMockRuleName,
|
||||
|
||||
@@ -3,10 +3,8 @@ package opapolicy
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
)
|
||||
|
||||
func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
|
||||
@@ -17,6 +15,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 +37,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 +51,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 +82,155 @@ func (controlReport *ControlReport) ListControlsInputKinds() []string {
|
||||
}
|
||||
return listControlsInputKinds
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Passed() bool {
|
||||
for i := range controlReport.RuleReports {
|
||||
if len(controlReport.RuleReports[i].RuleResponses) > 0 {
|
||||
if len(controlReport.RuleReports[i].RuleResponses) != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Failed() bool {
|
||||
return !controlReport.Passed()
|
||||
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 {
|
||||
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 := len(ruleReport.RuleResponses) - 1; i >= 0; i-- {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
|
||||
if !ruleReport.DeleteIfRedundantResponse(&ruleReport.RuleResponses[i], i) {
|
||||
sum++
|
||||
}
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) DeleteIfRedundantResponse(RuleResponse *RuleResponse, index int) bool {
|
||||
if b, rr := ruleReport.IsDuplicateResponseOfResource(RuleResponse, index); b {
|
||||
rr.AddMessageToResponse(RuleResponse.AlertMessage)
|
||||
ruleReport.RuleResponses = removeResponse(ruleReport.RuleResponses, index)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ruleResponse *RuleResponse) AddMessageToResponse(message string) {
|
||||
ruleResponse.AlertMessage += message
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) IsDuplicateResponseOfResource(RuleResponse *RuleResponse, index int) (bool, *RuleResponse) {
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if i != index {
|
||||
for j := range ruleReport.RuleResponses[i].AlertObject.K8SApiObjects {
|
||||
for k := range RuleResponse.AlertObject.K8SApiObjects {
|
||||
w1 := k8sinterface.NewWorkloadObj(ruleReport.RuleResponses[i].AlertObject.K8SApiObjects[j])
|
||||
w2 := k8sinterface.NewWorkloadObj(RuleResponse.AlertObject.K8SApiObjects[k])
|
||||
if w1.GetName() == w2.GetName() && w1.GetNamespace() == w2.GetNamespace() && w1.GetKind() != "Role" && w1.GetKind() != "ClusterRole" {
|
||||
return true, &ruleReport.RuleResponses[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func removeResponse(slice []RuleResponse, index int) []RuleResponse {
|
||||
return append(slice[:index], slice[index+1:]...)
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfWarningResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "warning" {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (postureReport *PostureReport) RemoveData() {
|
||||
for i := range postureReport.FrameworkReports {
|
||||
postureReport.FrameworkReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (frameworkReport *FrameworkReport) RemoveData() {
|
||||
for i := range frameworkReport.ControlReports {
|
||||
frameworkReport.ControlReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
func (controlReport *ControlReport) RemoveData() {
|
||||
for i := range controlReport.RuleReports {
|
||||
controlReport.RuleReports[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) RemoveData() {
|
||||
for i := range ruleReport.RuleResponses {
|
||||
ruleReport.RuleResponses[i].RemoveData()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RuleResponse) RemoveData() {
|
||||
r.AlertObject.ExternalObjects = nil
|
||||
|
||||
keepFields := []string{"kind", "apiVersion", "metadata"}
|
||||
keepMetadataFields := []string{"name", "namespace", "labels"}
|
||||
|
||||
for i := range r.AlertObject.K8SApiObjects {
|
||||
deleteFromMap(r.AlertObject.K8SApiObjects[i], keepFields)
|
||||
for k := range r.AlertObject.K8SApiObjects[i] {
|
||||
if k == "metadata" {
|
||||
if b, ok := r.AlertObject.K8SApiObjects[i][k].(map[string]interface{}); ok {
|
||||
deleteFromMap(b, keepMetadataFields)
|
||||
r.AlertObject.K8SApiObjects[i][k] = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFromMap(m map[string]interface{}, keepFields []string) {
|
||||
for k := range m {
|
||||
if StringInSlice(keepFields, k) {
|
||||
continue
|
||||
}
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
func StringInSlice(strSlice []string, str string) bool {
|
||||
for i := range strSlice {
|
||||
if strSlice[i] == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,9 +42,12 @@ func NewRegoDependenciesDataMock() *RegoDependenciesData {
|
||||
|
||||
func NewRegoDependenciesData(k8sConfig *rest.Config) *RegoDependenciesData {
|
||||
|
||||
regoDependenciesData := RegoDependenciesData{
|
||||
K8sConfig: *NewRegoK8sConfig(k8sConfig),
|
||||
regoDependenciesData := RegoDependenciesData{}
|
||||
|
||||
if k8sConfig != nil {
|
||||
regoDependenciesData.K8sConfig = *NewRegoK8sConfig(k8sConfig)
|
||||
}
|
||||
|
||||
return ®oDependenciesData
|
||||
}
|
||||
func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
|
||||
@@ -61,19 +64,9 @@ func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
|
||||
token = fmt.Sprintf("Bearer %s", k8sConfig.BearerToken)
|
||||
}
|
||||
|
||||
// crtFile := os.Getenv("KUBERNETES_CRT_PATH")
|
||||
// if crtFile == "" {
|
||||
// crtFile = k8sConfig.CAFile
|
||||
// // crtFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
// }
|
||||
|
||||
// glog.Infof("===========================================================================")
|
||||
// glog.Infof(fmt.Sprintf("%v", k8sConfig.String()))
|
||||
// glog.Infof("===========================================================================")
|
||||
|
||||
regoK8sConfig := RegoK8sConfig{
|
||||
Token: token,
|
||||
Host: k8sConfig.Host,
|
||||
Host: host,
|
||||
CrtFile: k8sConfig.CAFile,
|
||||
ClientCrtFile: k8sConfig.CertFile,
|
||||
ClientKeyFile: k8sConfig.KeyFile,
|
||||
|
||||
88
cautils/scaninfo.go
Normal file
88
cautils/scaninfo.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ScanInfo struct {
|
||||
Getters
|
||||
PolicyIdentifier opapolicy.PolicyIdentifier
|
||||
UseExceptions string
|
||||
UseFrom string
|
||||
UseDefault bool
|
||||
Format string
|
||||
Output string
|
||||
ExcludedNamespaces string
|
||||
InputPatterns []string
|
||||
Silent bool
|
||||
FailThreshold uint16
|
||||
DoNotSendResults bool
|
||||
}
|
||||
|
||||
type Getters struct {
|
||||
ExceptionsGetter getter.IExceptionsGetter
|
||||
PolicyGetter getter.IPolicyGetter
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) Init() {
|
||||
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 != "" {
|
||||
return
|
||||
}
|
||||
if scanInfo.UseDefault {
|
||||
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
|
||||
}
|
||||
|
||||
}
|
||||
func (scanInfo *ScanInfo) setGetter() {
|
||||
if scanInfo.UseFrom != "" {
|
||||
// load from file
|
||||
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
|
||||
} else {
|
||||
scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setOutputFile() {
|
||||
if scanInfo.Output == "" {
|
||||
return
|
||||
}
|
||||
if scanInfo.Format == "json" {
|
||||
if filepath.Ext(scanInfo.Output) != ".json" {
|
||||
scanInfo.Output += ".json"
|
||||
}
|
||||
}
|
||||
if scanInfo.Format == "junit" {
|
||||
if filepath.Ext(scanInfo.Output) != ".xml" {
|
||||
scanInfo.Output += ".xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) ScanRunningCluster() bool {
|
||||
return len(scanInfo.InputPatterns) == 0
|
||||
}
|
||||
|
||||
// func (scanInfo *ScanInfo) ConnectedToCluster(k8s k8sinterface.) bool {
|
||||
// _, err := k8s.KubernetesClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
|
||||
// return err == nil
|
||||
// }
|
||||
18
cmd/cluster.go
Normal file
18
cmd/cluster.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// clusterCmd represents the cluster command
|
||||
var clusterCmd = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: "Set configuration for cluster",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(clusterCmd)
|
||||
}
|
||||
50
cmd/cluster_get.go
Normal file
50
cmd/cluster_get.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get configuration in cluster",
|
||||
Long: ``,
|
||||
ValidArgs: supportedFrameworks,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
clusterConfig := cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
|
||||
val, err := clusterConfig.GetValueByKeyFromConfigMap(key)
|
||||
if err != nil {
|
||||
if err.Error() == "value does not exist." {
|
||||
fmt.Printf("Could net get value from configmap, reason: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Println(key + "=" + val)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(getCmd)
|
||||
}
|
||||
44
cmd/cluster_set.go
Normal file
44
cmd/cluster_set.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var setCmd = &cobra.Command{
|
||||
Use: "set <key>=<value>",
|
||||
Short: "Set configuration in cluster",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 2 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
data := keyValue[1]
|
||||
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
clusterConfig := cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
|
||||
if err := clusterConfig.SetKeyValueInConfigmap(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Value added successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
clusterCmd.AddCommand(setCmd)
|
||||
}
|
||||
19
cmd/config.go
Normal file
19
cmd/config.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Set configuration",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
}
|
||||
45
cmd/download.go
Normal file
45
cmd/download.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var downloadInfo cautils.DownloadInfo
|
||||
|
||||
var downloadCmd = &cobra.Command{
|
||||
Use: "download framework <framework-name>",
|
||||
Short: "Download framework controls",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("requires two arguments : framework <framework-name>")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
downloadInfo.FrameworkName = args[1]
|
||||
g := getter.NewDownloadReleasedPolicy()
|
||||
if downloadInfo.Path == "" {
|
||||
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName + ".json")
|
||||
}
|
||||
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = getter.SaveFrameworkInFile(frameworks, downloadInfo.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(downloadCmd)
|
||||
downloadInfo = cautils.DownloadInfo{}
|
||||
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If specified, will store save to `~/.kubescape/<framework name>.json`")
|
||||
}
|
||||
132
cmd/framework.go
132
cmd/framework.go
@@ -10,21 +10,24 @@ import (
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/opaprocessor"
|
||||
"github.com/armosec/kubescape/policyhandler"
|
||||
"github.com/armosec/kubescape/printer"
|
||||
"github.com/armosec/kubescape/resultshandling"
|
||||
"github.com/armosec/kubescape/resultshandling/printer"
|
||||
"github.com/armosec/kubescape/resultshandling/reporter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var scanInfo opapolicy.ScanInfo
|
||||
var scanInfo cautils.ScanInfo
|
||||
var supportedFrameworks = []string{"nsa"}
|
||||
|
||||
type CLIHandler struct {
|
||||
policyHandler *policyhandler.PolicyHandler
|
||||
scanInfo *opapolicy.ScanInfo
|
||||
scanInfo *cautils.ScanInfo
|
||||
}
|
||||
|
||||
var frameworkCmd = &cobra.Command{
|
||||
@@ -33,37 +36,45 @@ var frameworkCmd = &cobra.Command{
|
||||
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
|
||||
ValidArgs: supportedFrameworks,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
|
||||
return fmt.Errorf("requires at least one argument")
|
||||
}
|
||||
if !isValidFramework(args[0]) {
|
||||
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
|
||||
} else if len(args) > 0 {
|
||||
if !isValidFramework(args[0]) {
|
||||
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
scanInfo.PolicyIdentifier = opapolicy.PolicyIdentifier{}
|
||||
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
|
||||
scanInfo.PolicyIdentifier.Name = args[0]
|
||||
|
||||
if len(args[1:]) == 0 || args[1] != "-" {
|
||||
scanInfo.InputPatterns = args[1:]
|
||||
} else { // store stout to file
|
||||
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
if !(cmd.Flags().Lookup("use-from").Changed) {
|
||||
scanInfo.PolicyIdentifier.Name = args[0]
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if len(args[1:]) == 0 || args[1] != "-" {
|
||||
scanInfo.InputPatterns = args[1:]
|
||||
} else { // store stout to file
|
||||
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
|
||||
return err
|
||||
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
scanInfo.InputPatterns = []string{tempFile.Name()}
|
||||
}
|
||||
scanInfo.InputPatterns = []string{tempFile.Name()}
|
||||
}
|
||||
scanInfo.Init()
|
||||
cautils.SetSilentMode(scanInfo.Silent)
|
||||
CliSetup()
|
||||
|
||||
err := CliSetup()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -74,17 +85,32 @@ func isValidFramework(framework string) bool {
|
||||
|
||||
func init() {
|
||||
scanCmd.AddCommand(frameworkCmd)
|
||||
scanInfo = opapolicy.ScanInfo{}
|
||||
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")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "silent progress output")
|
||||
scanInfo = cautils.ScanInfo{}
|
||||
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")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages")
|
||||
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armosec backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features")
|
||||
}
|
||||
|
||||
func CliSetup() error {
|
||||
flag.Parse()
|
||||
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
if 100 < scanInfo.FailThreshold {
|
||||
fmt.Println("bad argument: out of range threshold")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var k8s *k8sinterface.KubernetesApi
|
||||
if !scanInfo.ScanRunningCluster() {
|
||||
k8sinterface.ConnectedToCluster = false
|
||||
} else {
|
||||
k8s = k8sinterface.NewKubernetesApi()
|
||||
}
|
||||
|
||||
processNotification := make(chan *cautils.OPASessionObj)
|
||||
reportResults := make(chan *cautils.OPASessionObj)
|
||||
@@ -92,19 +118,45 @@ func CliSetup() error {
|
||||
// policy handler setup
|
||||
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
|
||||
|
||||
// cli handler setup
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
panic(err)
|
||||
// load cluster config
|
||||
var clusterConfig cautils.IClusterConfig
|
||||
if !scanInfo.DoNotSendResults && k8sinterface.ConnectedToCluster {
|
||||
clusterConfig = cautils.NewClusterConfig(k8s, getter.NewArmoAPI())
|
||||
} else {
|
||||
clusterConfig = cautils.NewEmptyConfig()
|
||||
}
|
||||
|
||||
if err := clusterConfig.SetCustomerGUID(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
cautils.CustomerGUID = clusterConfig.GetCustomerGUID()
|
||||
cautils.ClusterName = k8sinterface.GetClusterName()
|
||||
|
||||
// cli handler setup
|
||||
go func() {
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// processor setup - rego run
|
||||
go func() {
|
||||
reporterObj := opaprocessor.NewOPAProcessor(&processNotification, &reportResults)
|
||||
reporterObj.ProcessRulesListenner()
|
||||
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
|
||||
opaprocessorObj.ProcessRulesListenner()
|
||||
}()
|
||||
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
|
||||
p.ActionPrint()
|
||||
|
||||
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
|
||||
score := resultsHandling.HandleResults()
|
||||
|
||||
// print report url
|
||||
clusterConfig.GenerateURL()
|
||||
|
||||
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
|
||||
if score < adjustedFailThreshold {
|
||||
return fmt.Errorf("Scan score is bellow threshold")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -127,12 +179,10 @@ func (clihandler *CLIHandler) Scan() error {
|
||||
}
|
||||
switch policyNotification.NotificationType {
|
||||
case opapolicy.TypeExecPostureScan:
|
||||
go func() {
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
//
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
|
||||
}
|
||||
|
||||
17
cmd/local.go
Normal file
17
cmd/local.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localCmd = &cobra.Command{
|
||||
Use: "local",
|
||||
Short: "Set configuration locally (for config.json)",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(localCmd)
|
||||
}
|
||||
46
cmd/local_get.go
Normal file
46
cmd/local_get.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localGetCmd = &cobra.Command{
|
||||
Use: "get <key>",
|
||||
Short: "Get configuration locally",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 1 {
|
||||
return fmt.Errorf("requires one argument")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
|
||||
val, err := cautils.GetValueFromConfigJson(key)
|
||||
if err != nil {
|
||||
if err.Error() == "value does not exist." {
|
||||
fmt.Printf("Could net get value from: %s, reason: %s\n", getter.GetDefaultPath(cautils.ConfigFileName+".json"), err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Println(key + "=" + val)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
localCmd.AddCommand(localGetCmd)
|
||||
}
|
||||
40
cmd/local_set.go
Normal file
40
cmd/local_set.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var localSetCmd = &cobra.Command{
|
||||
Use: "set <key>=<value>",
|
||||
Short: "Set configuration locally",
|
||||
Long: ``,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 || len(args) > 1 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
if len(keyValue) != 2 {
|
||||
return fmt.Errorf("requires one argument: <key>=<value>")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
keyValue := strings.Split(args[0], "=")
|
||||
key := keyValue[0]
|
||||
data := keyValue[1]
|
||||
|
||||
if err := cautils.SetKeyValueInConfigJson(key, data); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Value added successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
localCmd.AddCommand(localSetCmd)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ var cfgFile string
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kubescape",
|
||||
Short: "Kubescape is a tool for testing Kubernetes security posture",
|
||||
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA and MITRE specifications.`,
|
||||
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA specifications.`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
49
cmd/version.go
Normal file
49
cmd/version.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var BuildNumber string
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Get current version",
|
||||
Long: ``,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Your current version is: " + BuildNumber)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func GetLatestVersion() (string, error) {
|
||||
latestVersion := "https://api.github.com/repos/armosec/kubescape/releases/latest"
|
||||
resp, err := http.Get(latestVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, 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", latestVersion, 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", latestVersion, err.Error())
|
||||
}
|
||||
return fmt.Sprintf("%v", data["tag_name"]), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
@@ -50,6 +50,18 @@ kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatfo
|
||||
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
### Scan on-prem (offline)
|
||||
|
||||
* Scan using a framework from the local file system
|
||||
```
|
||||
kubescape scan framework --use-from <path>
|
||||
```
|
||||
|
||||
* Scan using the framework from the default location in file system
|
||||
```
|
||||
kubescape scan framework --use-default
|
||||
```
|
||||
|
||||
## Output formats
|
||||
|
||||
By default, the output is user friendly.
|
||||
@@ -64,4 +76,16 @@ kubescape scan framework nsa --format json --output results.json
|
||||
* Output in `junit xml` format <img src="new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa --format junit --output results.xml
|
||||
```
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
* Download and save in file <img src="new-feature.svg">
|
||||
```
|
||||
kubescape download framework nsa --output nsa.json
|
||||
```
|
||||
|
||||
* Download and save in default file (`~/.kubescape/<framework name>.json`)
|
||||
```
|
||||
kubescape download framework nsa
|
||||
```
|
||||
|
||||
BIN
docs/summary.png
Executable file → Normal file
BIN
docs/summary.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 65 KiB |
34
examples/exceptions.json
Normal file
34
examples/exceptions.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"name": "ignore-kube-namespaces",
|
||||
"policyType": "postureExceptionPolicy",
|
||||
"actions": [
|
||||
"alertOnly"
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-system"
|
||||
}
|
||||
},
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-public"
|
||||
}
|
||||
},
|
||||
{
|
||||
"designatorType": "Attributes",
|
||||
"attributes": {
|
||||
"namespace": "kube-node-lease"
|
||||
}
|
||||
}
|
||||
],
|
||||
"posturePolicies": [
|
||||
{
|
||||
"frameworkName": "NSA"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
41
install.sh
41
install.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Installing Kubescape..."
|
||||
echo -e "\033[0;36mInstalling Kubescape..."
|
||||
echo
|
||||
|
||||
BASE_DIR=~/.kubescape
|
||||
@@ -27,15 +27,38 @@ mkdir -p $BASE_DIR
|
||||
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
|
||||
rm -rf $BASE_DIR
|
||||
# Checking if SUDO needed/exists
|
||||
SUDO=
|
||||
if [ "$(id -u)" -ne 0 ] && [ -n "$(which sudo)" ]; then
|
||||
SUDO=sudo
|
||||
fi
|
||||
|
||||
echo -e "[V] Finished Installation"
|
||||
|
||||
# Find install dir
|
||||
install_dir=/usr/local/bin #default
|
||||
for pdir in ${PATH//:/ }; do
|
||||
edir="${pdir/#\~/$HOME}"
|
||||
if [[ $edir == $HOME/* ]]; then
|
||||
install_dir=$edir
|
||||
mkdir -p $install_dir 2>/dev/null || true
|
||||
SUDO=
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
chmod +x $OUTPUT 2>/dev/null
|
||||
$SUDO rm -f /usr/local/bin/$KUBESCAPE_EXEC 2>/dev/null || true # clearning up old install
|
||||
$SUDO cp $OUTPUT $install_dir/$KUBESCAPE_EXEC
|
||||
rm -rf $OUTPUT
|
||||
|
||||
echo
|
||||
echo -e "\033[32mFinished Installation."
|
||||
|
||||
echo -e "\033[0m"
|
||||
$KUBESCAPE_EXEC version
|
||||
echo
|
||||
|
||||
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
|
||||
echo
|
||||
echo -e "\033[35mUsage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
|
||||
|
||||
echo -e "\033[0m"
|
||||
|
||||
18
main.go
18
main.go
@@ -1,7 +1,23 @@
|
||||
package main
|
||||
|
||||
import "github.com/armosec/kubescape/cmd"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/armosec/kubescape/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
CheckLatestVersion()
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
func CheckLatestVersion() {
|
||||
latest, err := cmd.GetLatestVersion()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
} else if latest != cmd.BuildNumber {
|
||||
fmt.Println("Warning: You are not updated to the latest release: " + latest)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,105 +6,160 @@ 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"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy/resources"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
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.GetK8sConfig())
|
||||
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.ReportID = uuid.NewV4().String()
|
||||
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.ControlID = control.ControlID
|
||||
|
||||
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 +202,52 @@ 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() {
|
||||
|
||||
if !k8sinterface.ConnectedToCluster {
|
||||
return
|
||||
}
|
||||
|
||||
// calculate score
|
||||
s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath)
|
||||
s.Calculate(opap.PostureReport.FrameworkReports)
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) updateResults() {
|
||||
for f, frameworkReport := range opap.PostureReport.FrameworkReports {
|
||||
sumFailed := 0
|
||||
sumTotal := 0
|
||||
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)
|
||||
}
|
||||
sumFailed += controlReport.GetNumberOfFailedResources()
|
||||
sumTotal += controlReport.GetNumberOfResources()
|
||||
opap.PostureReport.FrameworkReports[f].ControlReports[c].Score = float32(percentage(controlReport.GetNumberOfResources(), controlReport.GetNumberOfFailedResources()))
|
||||
}
|
||||
opap.PostureReport.FrameworkReports[f].Score = float32(percentage(sumTotal, sumTotal-sumFailed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -67,3 +139,13 @@ func listMatchKinds(match []opapolicy.RuleMatchObjects) []string {
|
||||
}
|
||||
return matchKinds
|
||||
}
|
||||
|
||||
func percentage(big, small int) int {
|
||||
if big == 0 {
|
||||
if small == 0 {
|
||||
return 100
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return int(float64(float64(big-small)/float64(big)) * 100)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
JSON_FILE_FORMAT FileFormat = "json"
|
||||
)
|
||||
|
||||
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
|
||||
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
|
||||
workloads := []k8sinterface.IWorkload{}
|
||||
|
||||
// load resource from local file system
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
@@ -15,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
|
||||
@@ -25,13 +27,14 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
|
||||
}
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *opapolicy.ScanInfo) error {
|
||||
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *cautils.ScanInfo) error {
|
||||
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
|
||||
// validate notification
|
||||
// TODO
|
||||
policyHandler.getters = &scanInfo.Getters
|
||||
|
||||
// get policies
|
||||
frameworks, err := policyHandler.getPolicies(notification)
|
||||
frameworks, exceptions, err := policyHandler.getPolicies(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -39,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 {
|
||||
@@ -54,33 +58,31 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
|
||||
return nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
|
||||
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
|
||||
|
||||
cautils.ProgressTextDisplay("Downloading framework definitions")
|
||||
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
|
||||
|
||||
// TODO - support load policies from local file
|
||||
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
|
||||
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 framework")
|
||||
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
|
||||
|
||||
return frameworks, nil
|
||||
return frameworks, exceptions, nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
|
||||
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
|
||||
var k8sResources *cautils.K8SResources
|
||||
var err error
|
||||
if len(scanInfo.InputPatterns) > 0 {
|
||||
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
} else {
|
||||
if k8sinterface.ConnectedToCluster {
|
||||
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces)
|
||||
|
||||
} else {
|
||||
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
}
|
||||
|
||||
return k8sResources, err
|
||||
|
||||
@@ -1,152 +1,51 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// URLEncoder encode url
|
||||
func URLEncoder(oldURL string) string {
|
||||
fullURL := strings.Split(oldURL, "?")
|
||||
baseURL, err := url.Parse(fullURL[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Prepare Query Parameters
|
||||
if len(fullURL) > 1 {
|
||||
params := url.Values{}
|
||||
queryParams := strings.Split(fullURL[1], "&")
|
||||
for _, i := range queryParams {
|
||||
queryParam := strings.Split(i, "=")
|
||||
val := ""
|
||||
if len(queryParam) > 1 {
|
||||
val = queryParam[1]
|
||||
}
|
||||
params.Add(queryParam[0], val)
|
||||
}
|
||||
baseURL.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
return baseURL.String()
|
||||
}
|
||||
|
||||
type IArmoAPI interface {
|
||||
OPAFRAMEWORKGet(string) ([]opapolicy.Framework, error)
|
||||
}
|
||||
|
||||
type ArmoAPI struct {
|
||||
httpClient *http.Client
|
||||
hostURL string
|
||||
}
|
||||
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
hostURL: "https://dashbe.eustage2.cyberarmorsoft.com",
|
||||
}
|
||||
}
|
||||
func (db *ArmoAPI) GetServerAddress() string {
|
||||
return db.hostURL
|
||||
}
|
||||
func (db *ArmoAPI) GetHttpClient() *http.Client {
|
||||
return db.httpClient
|
||||
}
|
||||
func (db *ArmoAPI) OPAFRAMEWORKGet(name string) ([]opapolicy.Framework, error) {
|
||||
requestURI := "v1/armoFrameworks"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
|
||||
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(name))
|
||||
requestURI += "&getRules=true"
|
||||
|
||||
fullURL := URLEncoder(fmt.Sprintf("%s/%s", db.GetServerAddress(), requestURI))
|
||||
frameworkList := []opapolicy.Framework{}
|
||||
|
||||
req, err := http.NewRequest("GET", fullURL, nil)
|
||||
if err != nil {
|
||||
return frameworkList, err
|
||||
}
|
||||
c := http.Client{}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return frameworkList, err
|
||||
}
|
||||
respStr, err := HTTPRespToString(resp)
|
||||
if err != nil {
|
||||
return frameworkList, err
|
||||
}
|
||||
if name != "" {
|
||||
frameworkSingle := opapolicy.Framework{}
|
||||
err = JSONDecoder(respStr).Decode(&frameworkSingle)
|
||||
frameworkList = append(frameworkList, frameworkSingle)
|
||||
} else {
|
||||
err = JSONDecoder(respStr).Decode(&frameworkList)
|
||||
}
|
||||
return frameworkList, err
|
||||
}
|
||||
|
||||
// JSONDecoder returns JSON decoder for given string
|
||||
func JSONDecoder(origin string) *json.Decoder {
|
||||
dec := json.NewDecoder(strings.NewReader(origin))
|
||||
dec.UseNumber()
|
||||
return dec
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
bytesNum, err := io.Copy(&strBuilder, resp.Body)
|
||||
respStr := strBuilder.String()
|
||||
if err != nil {
|
||||
respStrNewLen := len(respStr)
|
||||
if respStrNewLen > 1024 {
|
||||
respStrNewLen = 1024
|
||||
}
|
||||
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
respStrNewLen := len(respStr)
|
||||
if respStrNewLen > 1024 {
|
||||
respStrNewLen = 1024
|
||||
}
|
||||
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
|
||||
}
|
||||
|
||||
return respStr, err
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
|
||||
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
|
||||
var errs error
|
||||
d := NewArmoAPI()
|
||||
frameworks := []opapolicy.Framework{}
|
||||
exceptionPolicies := []armotypes.PostureExceptionPolicy{}
|
||||
|
||||
// Get - cacli opa get
|
||||
for _, rule := range notification.Rules {
|
||||
switch rule.Kind {
|
||||
case opapolicy.KindFramework:
|
||||
// backend
|
||||
receivedFrameworks, err := d.OPAFRAMEWORKGet(rule.Name)
|
||||
receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
|
||||
if err != nil {
|
||||
errs = fmt.Errorf("Could not download framework, please check if this framework exists")
|
||||
return nil, nil, fmt.Errorf("kind: %v, name: %s, error: %s", rule.Kind, rule.Name, err.Error())
|
||||
}
|
||||
if receivedFramework != nil {
|
||||
frameworks = append(frameworks, *receivedFramework)
|
||||
if recExceptionPolicies != nil {
|
||||
exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
|
||||
}
|
||||
}
|
||||
frameworks = append(frameworks, receivedFrameworks...)
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("Missing rule kind, expected: %s", opapolicy.KindFramework)
|
||||
err := fmt.Errorf("missing rule kind, expected: %s", opapolicy.KindFramework)
|
||||
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(cautils.CustomerGUID, cautils.ClusterName)
|
||||
if err != nil {
|
||||
return receivedFramework, nil, err
|
||||
}
|
||||
|
||||
return receivedFramework, receivedException, nil
|
||||
}
|
||||
|
||||
@@ -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,58 +26,74 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) ActionPrint() {
|
||||
|
||||
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)
|
||||
func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
|
||||
totalResources := 0
|
||||
totalFailed := 0
|
||||
for _, frameworkReport := range postureReport.FrameworkReports {
|
||||
for _, controlReport := range frameworkReport.ControlReports {
|
||||
for _, ruleReport := range controlReport.RuleReports {
|
||||
for _, ruleResponses := range ruleReport.RuleResponses {
|
||||
totalFailed += len(ruleResponses.AlertObject.K8SApiObjects)
|
||||
totalFailed += len(ruleResponses.AlertObject.ExternalObjects)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if !k8sinterface.RunningIncluster {
|
||||
break
|
||||
totalResources += controlReport.GetNumberOfResources()
|
||||
}
|
||||
}
|
||||
if totalResources == 0 {
|
||||
return float32(0)
|
||||
}
|
||||
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
|
||||
}
|
||||
|
||||
func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 {
|
||||
var score float32
|
||||
|
||||
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")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
score = calculatePostureScore(opaSessionObj.PostureReport)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
|
||||
@@ -92,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,
|
||||
@@ -101,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]]
|
||||
@@ -120,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 {
|
||||
@@ -133,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)
|
||||
@@ -161,6 +178,10 @@ func (printer *Printer) printResult(controlName string, controlSummary *ControlS
|
||||
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintUrl(url string) {
|
||||
cautils.InfoTextDisplay(printer.writer, url)
|
||||
}
|
||||
|
||||
func generateRow(control string, cs ControlSummary) []string {
|
||||
row := []string{control}
|
||||
row = append(row, cs.ToSlice()...)
|
||||
@@ -173,7 +194,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 {
|
||||
@@ -185,11 +206,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), "%"))
|
||||
@@ -206,14 +228,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()
|
||||
}
|
||||
|
||||
@@ -227,7 +251,7 @@ func (printer *Printer) getSortedControlsNames() []string {
|
||||
}
|
||||
|
||||
func getWriter(outputFile string) *os.File {
|
||||
|
||||
os.Remove(outputFile)
|
||||
if outputFile != "" {
|
||||
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
@@ -2,6 +2,8 @@ package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
type Summary map[string]ControlSummary
|
||||
@@ -13,6 +15,7 @@ func NewSummary() Summary {
|
||||
type ControlSummary struct {
|
||||
TotalResources int
|
||||
TotalFailed int
|
||||
TotalWarnign int
|
||||
Description string
|
||||
Remediation string
|
||||
ListInputKinds []string
|
||||
@@ -24,11 +27,13 @@ type WorkloadSummary struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Group string
|
||||
Exception *armotypes.PostureExceptionPolicy
|
||||
}
|
||||
|
||||
func (controlSummary *ControlSummary) ToSlice() []string {
|
||||
s := []string{}
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarnign))
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
|
||||
return s
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func listResultSummary(ruleReports []opapolicy.RuleReport) []WorkloadSummary {
|
||||
|
||||
// add resource only once
|
||||
for i := range resource {
|
||||
resource[i].Exception = ruleReport.Exception
|
||||
if ok := track[resource[i].ToString()]; !ok {
|
||||
track[resource[i].ToString()] = true
|
||||
workloadsSummary = append(workloadsSummary, resource[i])
|
||||
@@ -51,6 +52,7 @@ func ruleResultSummary(obj opapolicy.AlertObject) ([]WorkloadSummary, error) {
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
resource = append(resource, *r)
|
||||
}
|
||||
|
||||
58
resultshandling/reporter/reporteventreceiver.go
Normal file
58
resultshandling/reporter/reporteventreceiver.go
Normal file
@@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
//Add score
|
||||
opaSessionObj.PostureReport.RemoveData()
|
||||
if err := report.Send(opaSessionObj.PostureReport); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
func (report *ReportEventReceiver) Send(postureReport *opapolicy.PostureReport) error {
|
||||
|
||||
reqBody, err := json.Marshal(*postureReport)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err)
|
||||
}
|
||||
host := hostToString(&report.host, postureReport.ReportID)
|
||||
|
||||
req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send', http.NewRequest failed, host: %s, reason: %v", host, err)
|
||||
}
|
||||
res, err := report.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpClient.Do failed: %v", err)
|
||||
}
|
||||
msg, err := httpRespToString(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %v:%s", host, err, msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
56
resultshandling/reporter/reporteventreceiverutils.go
Normal file
56
resultshandling/reporter/reporteventreceiverutils.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/getter"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
|
||||
func httpRespToString(resp *http.Response) (string, error) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
return "", nil
|
||||
}
|
||||
strBuilder := strings.Builder{}
|
||||
defer resp.Body.Close()
|
||||
if resp.ContentLength > 0 {
|
||||
strBuilder.Grow(int(resp.ContentLength))
|
||||
}
|
||||
_, err := io.Copy(&strBuilder, resp.Body)
|
||||
if err != nil {
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
err = fmt.Errorf("response status: %d. Content: %s", resp.StatusCode, strBuilder.String())
|
||||
}
|
||||
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
|
||||
func initEventReceiverURL() *url.URL {
|
||||
urlObj := url.URL{}
|
||||
|
||||
urlObj.Scheme = "https"
|
||||
urlObj.Host = getter.ArmoERURL
|
||||
urlObj.Path = "/k8s/postureReport"
|
||||
q := urlObj.Query()
|
||||
q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
|
||||
q.Add("clusterName", cautils.ClusterName)
|
||||
|
||||
urlObj.RawQuery = q.Encode()
|
||||
|
||||
return &urlObj
|
||||
}
|
||||
|
||||
func hostToString(host *url.URL, reportID string) string {
|
||||
q := host.Query()
|
||||
q.Add("reportID", reportID) // TODO - do we add the reportID?
|
||||
host.RawQuery = q.Encode()
|
||||
return host.String()
|
||||
}
|
||||
20
resultshandling/reporter/reporteventreceiverutils_test.go
Normal file
20
resultshandling/reporter/reporteventreceiverutils_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHostToString(t *testing.T) {
|
||||
host := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "report.eudev3.cyberarmorsoft.com",
|
||||
Path: "k8srestapi/v1/postureReport",
|
||||
RawQuery: "cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af",
|
||||
}
|
||||
expectedHost := "https://report.eudev3.cyberarmorsoft.com/k8srestapi/v1/postureReport?cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af&reportID=ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41"
|
||||
receivedHost := hostToString(&host, "ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41")
|
||||
if receivedHost != expectedHost {
|
||||
t.Errorf("%s != %s", receivedHost, expectedHost)
|
||||
}
|
||||
}
|
||||
32
resultshandling/results.go
Normal file
32
resultshandling/results.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package resultshandling
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/resultshandling/printer"
|
||||
"github.com/armosec/kubescape/resultshandling/reporter"
|
||||
)
|
||||
|
||||
type ResultsHandler struct {
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
reporterObj *reporter.ReportEventReceiver
|
||||
printerObj *printer.Printer
|
||||
}
|
||||
|
||||
func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *reporter.ReportEventReceiver, printerObj *printer.Printer) *ResultsHandler {
|
||||
return &ResultsHandler{
|
||||
opaSessionObj: opaSessionObj,
|
||||
reporterObj: reporterObj,
|
||||
printerObj: printerObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (resultsHandler *ResultsHandler) HandleResults() float32 {
|
||||
|
||||
opaSessionObj := <-*resultsHandler.opaSessionObj
|
||||
|
||||
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
|
||||
|
||||
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
|
||||
|
||||
return score
|
||||
}
|
||||
141
scapepkg/exceptions/exceptionprocessor.go
Normal file
141
scapepkg/exceptions/exceptionprocessor.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"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)
|
||||
ns := r.GetNamespace()
|
||||
if ns != "" {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 != "" && cautils.ClusterName != "" && cluster != cautils.ClusterName { // TODO - where do we receive cluster name from?
|
||||
return false // cluster name does not match
|
||||
}
|
||||
|
||||
if namespace != "" && !compareNamespace(workload, namespace) {
|
||||
return false // namespaces do not match
|
||||
}
|
||||
|
||||
if kind != "" && !compareKind(workload, kind) {
|
||||
return false // kinds do not match
|
||||
}
|
||||
|
||||
if name != "" && !compareName(workload, name) {
|
||||
return false // names do not match
|
||||
}
|
||||
if len(labels) > 0 && !compareLabels(workload, labels) {
|
||||
return false // labels do not match
|
||||
}
|
||||
|
||||
return true // no mismatch found -> the workload has an exception
|
||||
}
|
||||
|
||||
func compareNamespace(workload k8sinterface.IWorkload, namespace string) bool {
|
||||
if workload.GetKind() == "Namespace" {
|
||||
return namespace == workload.GetName()
|
||||
}
|
||||
return namespace == workload.GetNamespace()
|
||||
}
|
||||
|
||||
func compareKind(workload k8sinterface.IWorkload, kind string) bool {
|
||||
return kind == workload.GetKind()
|
||||
}
|
||||
|
||||
func compareName(workload k8sinterface.IWorkload, name string) bool {
|
||||
return name == workload.GetName()
|
||||
}
|
||||
|
||||
func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string) bool {
|
||||
workloadLabels := labels.Set(workload.GetLabels())
|
||||
designators := labels.Set(attributes).AsSelector()
|
||||
|
||||
return designators.Matches(workloadLabels)
|
||||
}
|
||||
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
func PostureExceptionPolicyDisableMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{}
|
||||
}
|
||||
|
||||
func PostureExceptionPolicyAlertOnlyMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{
|
||||
PortalBase: armotypes.PortalBase{
|
||||
Name: "postureExceptionPolicyAlertOnlyMock",
|
||||
},
|
||||
PolicyType: "postureExceptionPolicy",
|
||||
Actions: []armotypes.PostureExceptionPolicyActions{armotypes.AlertOnly},
|
||||
Resources: []armotypes.PortalDesignator{
|
||||
{
|
||||
DesignatorType: armotypes.DesignatorAttributes,
|
||||
Attributes: map[string]string{
|
||||
armotypes.AttributeNamespace: "default",
|
||||
armotypes.AttributeCluster: "unittest",
|
||||
},
|
||||
},
|
||||
},
|
||||
PosturePolicies: []armotypes.PosturePolicy{
|
||||
{
|
||||
FrameworkName: "MITRE",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRuleExceptions(t *testing.T) {
|
||||
exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
if len(res1) != 1 {
|
||||
t.Errorf("expecting 1 exception")
|
||||
}
|
||||
res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
if len(res2) != 0 {
|
||||
t.Errorf("expecting 0 exception")
|
||||
}
|
||||
}
|
||||
|
||||
// func TestGetException(t *testing.T) {
|
||||
// exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
// res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
// if len(res1) != 1 {
|
||||
// t.Errorf("expecting 1 exception")
|
||||
// }
|
||||
// res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
// if len(res2) != 0 {
|
||||
// t.Errorf("expecting 0 exception")
|
||||
// }
|
||||
// }
|
||||
232
scapepkg/score/frameworkdict.json
Normal file
232
scapepkg/score/frameworkdict.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"developer_framework": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"MITRE": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 8.0,
|
||||
"improvementRatio": 0.5
|
||||
},
|
||||
"Sidecar injection": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access tiller endpoint": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Data Destruction": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Resource Hijacking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access the Kubernetes API server": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Backdoor container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Images from private registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Mount service principal": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (validating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Vulnerable application": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Application exploit (RCE)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"List Kubernetes secrets": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed sensitive interfaces": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Bash/cmd inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Clear container logs": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"New container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"CoreDNS poisoning": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (mutating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exec into container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Delete Kubernetes events": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"NSA": {
|
||||
"Control plane hardening": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Immutable container filesystem": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Non-root containers": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Host PID/IPC privileges": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
1214
scapepkg/score/frameworkmock.json
Normal file
1214
scapepkg/score/frameworkmock.json
Normal file
File diff suppressed because it is too large
Load Diff
2161
scapepkg/score/resourcemocks.json
Normal file
2161
scapepkg/score/resourcemocks.json
Normal file
File diff suppressed because one or more lines are too long
22
scapepkg/score/resourcesdict.json
Normal file
22
scapepkg/score/resourcesdict.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"pod": 1.0,
|
||||
"service": 1.0,
|
||||
"daemonset": 1.0,
|
||||
"deployment": 1.0,
|
||||
"replicaset": 1.1,
|
||||
"statefulset": 1.0,
|
||||
"job": 1.0,
|
||||
"secret": 1.0,
|
||||
"cronjob": 1.0,
|
||||
"clusterrolebinding": 1.0,
|
||||
"clusterrole": 1.0,
|
||||
"rolebinding": 1.0,
|
||||
"role": 1.0,
|
||||
"networkpolicy": 1.0,
|
||||
"controllerrevision": 1.0,
|
||||
"namespace": 1.0,
|
||||
"serviceaccount": 1.0,
|
||||
"configmap": 1.0,
|
||||
"node": 1.0
|
||||
|
||||
}
|
||||
201
scapepkg/score/score.go
Normal file
201
scapepkg/score/score.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
||||
// corev1 "k8s.io/api/core/v1"
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ControlScoreWeights struct {
|
||||
BaseScore float32 `json:"baseScore"`
|
||||
RuntimeImprovementMultiplier float32 `json:"improvementRatio"`
|
||||
}
|
||||
|
||||
type ScoreUtil struct {
|
||||
ResourceTypeScores map[string]float32
|
||||
FrameworksScore map[string]map[string]ControlScoreWeights
|
||||
K8SApoObj *k8sinterface.KubernetesApi
|
||||
configPath string
|
||||
}
|
||||
|
||||
var postureScore *ScoreUtil
|
||||
|
||||
func (su *ScoreUtil) Calculate(frameworksReports []opapolicy.FrameworkReport) error {
|
||||
for i := range frameworksReports {
|
||||
su.CalculateFrameworkScore(&frameworksReports[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) CalculateFrameworkScore(framework *opapolicy.FrameworkReport) error {
|
||||
for i := range framework.ControlReports {
|
||||
framework.WCSScore += su.ControlScore(&framework.ControlReports[i], framework.Name)
|
||||
framework.Score += framework.ControlReports[i].Score
|
||||
framework.ARMOImprovement += framework.ControlReports[i].ARMOImprovement
|
||||
}
|
||||
if framework.WCSScore > 0 {
|
||||
framework.Score = (framework.Score * 100) / framework.WCSScore
|
||||
framework.ARMOImprovement = (framework.ARMOImprovement * 100) / framework.WCSScore
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to calculate score for framework %s due to bad wcs score", framework.Name)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
daemonset: daemonsetscore*#nodes
|
||||
workloads: if replicas:
|
||||
replicascore*workloadkindscore*#replicas
|
||||
else:
|
||||
regular
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) resourceRules(resources []map[string]interface{}) float32 {
|
||||
var weight float32 = 0
|
||||
|
||||
for _, v := range resources {
|
||||
var score float32 = 0
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
kind := ""
|
||||
if wl != nil {
|
||||
kind = strings.ToLower(wl.GetKind())
|
||||
replicas := wl.GetReplicas()
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if replicas > 1 {
|
||||
score *= su.ResourceTypeScores["replicaset"] * float32(replicas)
|
||||
}
|
||||
|
||||
} else {
|
||||
epsilon := float32(0.00001)
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
kind = keys[0]
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if score == 0.0 || (score > -1*epsilon && score < epsilon) {
|
||||
score = 1
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "daemonset" {
|
||||
b, err := json.Marshal(v)
|
||||
if err == nil {
|
||||
dmnset := appsv1.DaemonSet{}
|
||||
json.Unmarshal(b, &dmnset)
|
||||
score *= float32(dmnset.Status.DesiredNumberScheduled)
|
||||
}
|
||||
}
|
||||
weight += score
|
||||
}
|
||||
|
||||
return weight
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) externalResourceConverter(rscs map[string]interface{}) []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
for atype, v := range rscs {
|
||||
resources = append(resources, map[string]interface{}{atype: v})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
/*
|
||||
ControlScore:
|
||||
@input:
|
||||
ctrlReport - opapolicy.ControlReport object, must contain down the line the Input resources and the output resources
|
||||
frameworkName - calculate this control according to a given framework weights
|
||||
|
||||
ctrl.score = baseScore * SUM_resource (resourceWeight*min(#replicas*replicaweight,1)(nodes if daemonset)
|
||||
|
||||
returns control score ***for the input resources***
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) ControlScore(ctrlReport *opapolicy.ControlReport, frameworkName string) float32 {
|
||||
|
||||
aggregatedInputs := make([]map[string]interface{}, 0)
|
||||
aggregatedResponses := make([]map[string]interface{}, 0)
|
||||
for _, ruleReport := range ctrlReport.RuleReports {
|
||||
status, _, _ := ruleReport.GetRuleStatus()
|
||||
if status != "warning" {
|
||||
for _, ruleResponse := range ruleReport.RuleResponses {
|
||||
aggregatedResponses = append(aggregatedResponses, ruleResponse.AlertObject.K8SApiObjects...)
|
||||
aggregatedResponses = append(aggregatedResponses, su.externalResourceConverter(ruleResponse.AlertObject.ExternalObjects)...)
|
||||
}
|
||||
}
|
||||
|
||||
aggregatedInputs = append(aggregatedInputs, ruleReport.ListInputResources...)
|
||||
|
||||
}
|
||||
improvementRatio := float32(1)
|
||||
if ctrls, isOk := su.FrameworksScore[frameworkName]; isOk {
|
||||
if scoreobj, isOk2 := ctrls[ctrlReport.Name]; isOk2 {
|
||||
ctrlReport.BaseScore = scoreobj.BaseScore
|
||||
improvementRatio -= scoreobj.RuntimeImprovementMultiplier
|
||||
}
|
||||
} else {
|
||||
ctrlReport.BaseScore = 1.0
|
||||
}
|
||||
|
||||
ctrlReport.Score = ctrlReport.BaseScore * su.resourceRules(aggregatedResponses)
|
||||
ctrlReport.ARMOImprovement = ctrlReport.Score * improvementRatio
|
||||
|
||||
return ctrlReport.BaseScore * su.resourceRules(aggregatedInputs)
|
||||
|
||||
}
|
||||
|
||||
func getPostureFrameworksScores(weightPath string) map[string]map[string]ControlScoreWeights {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
frameworksScoreMap := make(map[string]map[string]ControlScoreWeights)
|
||||
dat, err := ioutil.ReadFile(weightPath + "frameworkdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &frameworksScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return frameworksScoreMap
|
||||
|
||||
}
|
||||
|
||||
func getPostureResourceScores(weightPath string) map[string]float32 {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
resourceScoreMap := make(map[string]float32)
|
||||
dat, err := ioutil.ReadFile(weightPath + "resourcesdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resourceScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resourceScoreMap
|
||||
|
||||
}
|
||||
|
||||
func NewScore(k8sapiobj *k8sinterface.KubernetesApi, configPath string) *ScoreUtil {
|
||||
if postureScore == nil {
|
||||
|
||||
postureScore = &ScoreUtil{
|
||||
ResourceTypeScores: getPostureResourceScores(configPath),
|
||||
FrameworksScore: getPostureFrameworksScores(configPath),
|
||||
configPath: configPath,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return postureScore
|
||||
}
|
||||
77
scapepkg/score/score_mocks.go
Normal file
77
scapepkg/score/score_mocks.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func loadResourcesMock() []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
|
||||
dat, err := ioutil.ReadFile("resourcemocks.json")
|
||||
|
||||
if err != nil {
|
||||
return resources
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resources); err != nil {
|
||||
return resources
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func getResouceByType(desiredType string) map[string]interface{} {
|
||||
rsrcs := loadResourcesMock()
|
||||
if rsrcs == nil {
|
||||
return nil
|
||||
}
|
||||
for _, v := range rsrcs {
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
if wl != nil {
|
||||
if strings.ToLower(wl.GetKind()) == desiredType {
|
||||
return v
|
||||
}
|
||||
continue
|
||||
|
||||
} else {
|
||||
for k := range v {
|
||||
if k == desiredType {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFrameworkMock() *opapolicy.FrameworkReport {
|
||||
report := &opapolicy.FrameworkReport{}
|
||||
|
||||
dat, err := ioutil.ReadFile("frameworkmock.json")
|
||||
|
||||
if err != nil {
|
||||
return report
|
||||
}
|
||||
if err := json.Unmarshal(dat, &report); err != nil {
|
||||
return report
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
func getMITREFrameworkResultMock() []opapolicy.FrameworkReport {
|
||||
l := make([]opapolicy.FrameworkReport, 0)
|
||||
report := loadFrameworkMock()
|
||||
resources := loadResourcesMock()
|
||||
if report != nil && resources != nil {
|
||||
|
||||
report.ControlReports[0].RuleReports[0].ListInputResources = resources
|
||||
l = append(l, *report)
|
||||
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
65
scapepkg/score/score_test.go
Normal file
65
scapepkg/score/score_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFrameworkMock(t *testing.T) {
|
||||
r := getMITREFrameworkResultMock()
|
||||
su := NewScore(nil, "")
|
||||
var epsilon float32 = 0.001
|
||||
su.Calculate(r)
|
||||
var sumweights float32 = 0.0
|
||||
for _, v := range su.ResourceTypeScores {
|
||||
sumweights += v
|
||||
}
|
||||
|
||||
for _, framework := range r {
|
||||
if framework.Score < 1 {
|
||||
t.Errorf("framework %s invalid calculation1: %v", framework.Name, framework)
|
||||
}
|
||||
|
||||
if framework.Score > framework.WCSScore+epsilon {
|
||||
t.Errorf("framework %s invalid calculation2: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ARMOImprovement > framework.Score+epsilon {
|
||||
t.Errorf("framework %s invalid calculation3: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ControlReports[0].Score*sumweights <= 0+epsilon {
|
||||
t.Errorf("framework %s invalid calculation4: %v", framework.Name, framework)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
func TestDaemonsetRule(t *testing.T) {
|
||||
desiredType := "daemonset"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 13 * su.ResourceTypeScores[desiredType]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleReplicasRule(t *testing.T) {
|
||||
desiredType := "deployment"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 3 * su.ResourceTypeScores[desiredType] * su.ResourceTypeScores["replicaset"]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
1
scapepkg/score/scoremethods.go
Normal file
1
scapepkg/score/scoremethods.go
Normal file
@@ -0,0 +1 @@
|
||||
package score
|
||||
Reference in New Issue
Block a user