mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-19 20:39:56 +00:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ceb5150e2 | ||
|
|
aad32ec965 | ||
|
|
d3137af3d7 | ||
|
|
2568241ef8 | ||
|
|
3c6b2db919 | ||
|
|
f60ff1fb26 | ||
|
|
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 | ||
|
|
23090fbb9f | ||
|
|
fcb6255f75 | ||
|
|
abbc9c3e2e | ||
|
|
75f76fcecd | ||
|
|
a00bba5fe4 | ||
|
|
96473188ed | ||
|
|
56264df047 | ||
|
|
c3008981e4 | ||
|
|
572130d797 | ||
|
|
7d8cf37532 | ||
|
|
ebd9661255 | ||
|
|
05e108b47b | ||
|
|
8cb6824f3c | ||
|
|
304017fc41 | ||
|
|
b82673f694 | ||
|
|
ac7e5219f1 | ||
|
|
a4c4f9c6ed | ||
|
|
571a15bee8 | ||
|
|
8a00a5c54b | ||
|
|
8f8aaf70d9 | ||
|
|
d3f4af0f9c | ||
|
|
f069955231 | ||
|
|
d5290a6671 | ||
|
|
a54dac51af | ||
|
|
823455846f | ||
|
|
39c7bfeed9 | ||
|
|
67fc9832f8 | ||
|
|
559c4f3f15 | ||
|
|
187f517f58 | ||
|
|
c963b19364 | ||
|
|
2d7c5fd1ce | ||
|
|
e728b3ae37 | ||
|
|
01bc5345ab | ||
|
|
fc2374a690 | ||
|
|
00314be32a | ||
|
|
c8068a8d90 | ||
|
|
44803ab915 | ||
|
|
b50a665920 | ||
|
|
2d5ed19d6d | ||
|
|
81e5bc3991 | ||
|
|
d6a9e50626 | ||
|
|
7500438991 | ||
|
|
328265559c | ||
|
|
1fae5755fe | ||
|
|
96148ac6fd | ||
|
|
b2fcf295ce | ||
|
|
89ad5c1df0 | ||
|
|
798994850d | ||
|
|
f9ab72d595 | ||
|
|
8f4ac5dd87 | ||
|
|
5a5f2b408c | ||
|
|
615c1af63e | ||
|
|
c67e584cfb | ||
|
|
6414be3c6f | ||
|
|
3d9f98e866 | ||
|
|
aba67cc596 | ||
|
|
0d43fec008 | ||
|
|
2574f1954d | ||
|
|
b97f7d6e36 | ||
|
|
ce908aa748 | ||
|
|
707a2c78fe | ||
|
|
5f1bb0ce0a | ||
|
|
4683b56ce0 | ||
|
|
a7e69c8096 | ||
|
|
3735f4fcc2 | ||
|
|
2606be8ecd | ||
|
|
c51ea6bafd | ||
|
|
6588507e6f | ||
|
|
b9da2380fa | ||
|
|
e162d5e8b2 | ||
|
|
5bec5b0075 | ||
|
|
747940d66c | ||
|
|
789902f534 | ||
|
|
140ef8ac91 | ||
|
|
4b4b05caba | ||
|
|
abbb14c571 | ||
|
|
93a5f133c3 | ||
|
|
50f99f4719 | ||
|
|
ba45dd9f1c | ||
|
|
7669f791d5 | ||
|
|
c592728f62 | ||
|
|
ea0d48bf6d | ||
|
|
b8a9dd9359 | ||
|
|
dba4520f27 | ||
|
|
111d04dabe | ||
|
|
5f5215ff8e | ||
|
|
12a6229193 | ||
|
|
0992e6edd7 | ||
|
|
12294ad23c | ||
|
|
b2d14a778a | ||
|
|
b92a44dd7c | ||
|
|
35ea718080 | ||
|
|
47ed057e66 | ||
|
|
5e0e5c6231 | ||
|
|
4f9a4a6c61 | ||
|
|
22b3544243 | ||
|
|
297a4fc42b | ||
|
|
405cd837a1 | ||
|
|
e6b2688462 | ||
|
|
e5b35fcb55 | ||
|
|
35449e3d4e | ||
|
|
9509c69d87 | ||
|
|
34170faae9 | ||
|
|
d5d0da8ac3 | ||
|
|
8b7a4b1e48 | ||
|
|
d5383fe218 | ||
|
|
46b9cf35ac | ||
|
|
329d341fbf | ||
|
|
6be692c66f | ||
|
|
3c062238ad | ||
|
|
954224e9f6 | ||
|
|
a5f99e0a8d | ||
|
|
d484aeb62c | ||
|
|
8c3eeab7ed | ||
|
|
cea8266734 | ||
|
|
eefaf7b23c | ||
|
|
bc61755f67 | ||
|
|
c462d1ec2f | ||
|
|
203d43347e | ||
|
|
d102789a35 | ||
|
|
28b431c623 | ||
|
|
2fb1fef6d5 | ||
|
|
091a811fa1 |
79
.github/workflows/build.yaml
vendored
79
.github/workflows/build.yaml
vendored
@@ -5,49 +5,50 @@ on:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
types: [ closed ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
once:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Create a release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v1.0.${{ github.run_number }}
|
||||
release_name: Release v1.0.${{ github.run_number }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
build:
|
||||
name: Create cross-platform release build, tag and upload binaries
|
||||
needs: once
|
||||
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: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Build
|
||||
run: mkdir build && go mod tidy && go build -o build/kubescape
|
||||
|
||||
- name: Chmod
|
||||
run: chmod +x build/kubescape
|
||||
|
||||
- name: List
|
||||
run: ls -la
|
||||
- name: Build
|
||||
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v0.0.${{ github.run_number }}
|
||||
release_name: Release v0.0.${{ github.run_number }}
|
||||
body: |
|
||||
Changes in this Release
|
||||
- First Change
|
||||
- Second Change
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: build/kubescape
|
||||
asset_name: kubescape
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload Release binaries
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.once.outputs.upload_url }}
|
||||
asset_path: build/${{ matrix.os }}/kubescape
|
||||
asset_name: kubescape-${{ matrix.os }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
*.vs*
|
||||
*go.sum*
|
||||
*kubescape*
|
||||
*kubescape*
|
||||
*debug*
|
||||
.idea
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "vendor/github.com/armosec/capacketsgo"]
|
||||
path = vendor/github.com/armosec/capacketsgo
|
||||
url = git@github.com:armosec/capacketsgo.git
|
||||
branch = master
|
||||
1
LICENSE
1
LICENSE
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
171
README.md
171
README.md
@@ -1,39 +1,176 @@
|
||||
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
|
||||
|
||||
kubescape is a tool for testing Kubernetes clusters against industry accepted security standards and recomendations like:
|
||||
* NSA hardening for Kubernetes operators [see here](https://media.defense.gov/2021/Aug/03/2002820425/-1/-1/1/CTR_KUBERNETES%20HARDENING%20GUIDANCE.PDF)
|
||||
* MITRE threat matrix for Kubernetes [see here](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/)
|
||||
[](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
|
||||
[](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/)
|
||||
|
||||
Use Kubescape to test clusters or scan single YAML files and integrate it to your processes.
|
||||
|
||||
<img src="docs/demo.gif">
|
||||
|
||||
# TL;DR
|
||||
## Installation
|
||||
To install the tool locally, run this:
|
||||
## Install & Run
|
||||
|
||||
`curl -s https://raw.githubusercontent.com/armosec/kubescape/master/install.sh | /bin/bash`
|
||||
### Install:
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/armosec/kubescape/master/install.sh | /bin/bash
|
||||
```
|
||||
|
||||
<img src="docs/install.jpeg">
|
||||
### Run:
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
## Run
|
||||
To get a fast check of the security posture of your Kubernetes cluster, run this:
|
||||
If you wish to scan all namespaces in your cluster, remove the `--exclude-namespaces` flag.
|
||||
|
||||
`kubescape scan framework nsa`
|
||||
|
||||
<img src="docs/run.jpeg">
|
||||
<img src="docs/summary.png">
|
||||
|
||||
|
||||
# Status
|
||||
[](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
|
||||
### 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) |
|
||||
|
||||
## Usage & Examples
|
||||
|
||||
### 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
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files before deploying
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
|
||||
* 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
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
|
||||
```
|
||||
|
||||
* Output in `junit xml` format
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
|
||||
```
|
||||
|
||||
* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` <img src="docs/new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa --exceptions examples/exceptions.json
|
||||
```
|
||||
|
||||
### Helm Support
|
||||
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
|
||||
```
|
||||
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
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
|
||||
`go mod tidy && go build -o kubescape` :zany_face:
|
||||
|
||||
Note: development (and the release process) is done with Go `1.16`
|
||||
|
||||
1. Clone Project
|
||||
```
|
||||
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
|
||||
```
|
||||
|
||||
2. Build
|
||||
```
|
||||
go mod tidy && go build -o kubescape .
|
||||
```
|
||||
|
||||
3. Run
|
||||
```
|
||||
./kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
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
|
||||
Defining the tests here...
|
||||
Kubescape is running the following tests according to what is defined by [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/)
|
||||
* Non-root containers
|
||||
* Immutable container filesystem
|
||||
* Privileged containers
|
||||
* hostPID, hostIPC privileges
|
||||
* hostNetwork access
|
||||
* allowedHostPaths field
|
||||
* Protecting pod service account tokens
|
||||
* Resource policies
|
||||
* Control plane hardening
|
||||
* Exposed dashboard
|
||||
* Allow privilege escalation
|
||||
* Applications credentials in configuration files
|
||||
* Cluster-admin binding
|
||||
* Exec into container
|
||||
* Dangerous capabilities
|
||||
* Insecure capabilities
|
||||
* Linux hardening
|
||||
* Ingress and Egress blocked
|
||||
* Container hostPort
|
||||
* Network policies
|
||||
|
||||
|
||||
|
||||
## Technology
|
||||
Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls.
|
||||
|
||||
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by (ARMO)[https://www.armosec.io/].
|
||||
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/).
|
||||
|
||||
The results by default printed in a pretty "console friendly" manner, but they can be retrieved in JSON format for further processing.
|
||||
|
||||
Kubescape is an open source project, we welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
|
||||
|
||||
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type DesignatorType string
|
||||
// Supported designators
|
||||
const (
|
||||
DesignatorAttributes DesignatorType = "Attributes"
|
||||
DesignatorAttribute DesignatorType = "Attribute" // Deprecated
|
||||
/*
|
||||
WorkloadID format.
|
||||
k8s format: wlid://cluster-<cluster>/namespace-<namespace>/<kind>-<name>
|
||||
@@ -45,6 +46,8 @@ const (
|
||||
const (
|
||||
AttributeCluster = "cluster"
|
||||
AttributeNamespace = "namespace"
|
||||
AttributeKind = "kind"
|
||||
AttributeName = "name"
|
||||
)
|
||||
|
||||
// PortalDesignator represented single designation options
|
||||
|
||||
@@ -1,27 +1,100 @@
|
||||
package armotypes
|
||||
|
||||
import "github.com/golang/glog"
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils/cautils"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var IgnoreLabels = []string{AttributeCluster, AttributeNamespace}
|
||||
|
||||
func (designator *PortalDesignator) GetCluster() string {
|
||||
cluster, _, _, _, _ := designator.DigestPortalDesignator()
|
||||
return cluster
|
||||
}
|
||||
|
||||
func (designator *PortalDesignator) GetNamespace() string {
|
||||
_, namespace, _, _, _ := designator.DigestPortalDesignator()
|
||||
return namespace
|
||||
}
|
||||
|
||||
func (designator *PortalDesignator) GetKind() string {
|
||||
_, _, kind, _, _ := designator.DigestPortalDesignator()
|
||||
return kind
|
||||
}
|
||||
|
||||
func (designator *PortalDesignator) GetName() string {
|
||||
_, _, _, name, _ := designator.DigestPortalDesignator()
|
||||
return name
|
||||
}
|
||||
func (designator *PortalDesignator) GetLabels() map[string]string {
|
||||
_, _, _, _, labels := designator.DigestPortalDesignator()
|
||||
return labels
|
||||
}
|
||||
|
||||
// DigestPortalDesignator - get cluster namespace and labels from designator
|
||||
func (designator *PortalDesignator) DigestPortalDesignator() (string, string, string, string, map[string]string) {
|
||||
switch designator.DesignatorType {
|
||||
case DesignatorAttributes, DesignatorAttribute:
|
||||
return designator.DigestAttributesDesignator()
|
||||
case DesignatorWlid, DesignatorWildWlid:
|
||||
return cautils.GetClusterFromWlid(designator.WLID), cautils.GetNamespaceFromWlid(designator.WLID), cautils.GetKindFromWlid(designator.WLID), cautils.GetNameFromWlid(designator.WLID), map[string]string{}
|
||||
// case DesignatorSid: // TODO
|
||||
default:
|
||||
glog.Warningf("in 'digestPortalDesignator' designator type: '%v' not yet supported. please contact Armo team", designator.DesignatorType)
|
||||
}
|
||||
return "", "", "", "", nil
|
||||
}
|
||||
|
||||
func (designator *PortalDesignator) DigestAttributesDesignator() (string, string, string, string, map[string]string) {
|
||||
cluster := ""
|
||||
namespace := ""
|
||||
kind := ""
|
||||
name := ""
|
||||
labels := map[string]string{}
|
||||
attributes := designator.Attributes
|
||||
if attributes == nil {
|
||||
return cluster, namespace, kind, name, labels
|
||||
}
|
||||
for k, v := range attributes {
|
||||
labels[k] = v
|
||||
}
|
||||
if v, ok := attributes[AttributeNamespace]; ok {
|
||||
namespace = v
|
||||
delete(labels, AttributeNamespace)
|
||||
}
|
||||
if v, ok := attributes[AttributeCluster]; ok {
|
||||
cluster = v
|
||||
delete(labels, AttributeCluster)
|
||||
}
|
||||
if v, ok := attributes[AttributeKind]; ok {
|
||||
kind = v
|
||||
delete(labels, AttributeKind)
|
||||
}
|
||||
if v, ok := attributes[AttributeName]; ok {
|
||||
name = v
|
||||
delete(labels, AttributeName)
|
||||
}
|
||||
return cluster, namespace, kind, name, labels
|
||||
}
|
||||
|
||||
// DigestPortalDesignator DEPRECATED. use designator.DigestPortalDesignator() - get cluster namespace and labels from designator
|
||||
func DigestPortalDesignator(designator *PortalDesignator) (string, string, map[string]string) {
|
||||
switch designator.DesignatorType {
|
||||
case DesignatorAttributes:
|
||||
case DesignatorAttributes, DesignatorAttribute:
|
||||
return DigestAttributesDesignator(designator.Attributes)
|
||||
// case DesignatorWlid: TODO
|
||||
// case DesignatorWildWlid: TODO
|
||||
case DesignatorWlid, DesignatorWildWlid:
|
||||
return cautils.GetClusterFromWlid(designator.WLID), cautils.GetNamespaceFromWlid(designator.WLID), map[string]string{}
|
||||
// case DesignatorSid: // TODO
|
||||
default:
|
||||
glog.Warningf("in 'digestPortalDesignator' designator type: '%v' not yet supported. please contact Armo team", designator.DesignatorType)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func DigestAttributesDesignator(attributes map[string]string) (string, string, map[string]string) {
|
||||
cluster := ""
|
||||
namespace := ""
|
||||
labels := map[string]string{}
|
||||
if attributes == nil || len(attributes) == 0 {
|
||||
if attributes == nil {
|
||||
return cluster, namespace, labels
|
||||
}
|
||||
for k, v := range attributes {
|
||||
@@ -35,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
|
||||
@@ -1,7 +1,8 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// K8SResources map[<api group>/<api version>/<resource>]<resource object>
|
||||
@@ -10,6 +11,7 @@ type K8SResources map[string]interface{}
|
||||
type OPASessionObj struct {
|
||||
Frameworks []opapolicy.Framework
|
||||
K8SResources *K8SResources
|
||||
Exceptions []armotypes.PostureExceptionPolicy
|
||||
PostureReport *opapolicy.PostureReport
|
||||
}
|
||||
|
||||
|
||||
@@ -10,28 +10,62 @@ import (
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
var silent = false
|
||||
|
||||
func SetSilentMode(s bool) {
|
||||
silent = s
|
||||
}
|
||||
|
||||
func IsSilent() bool {
|
||||
return silent
|
||||
}
|
||||
|
||||
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()
|
||||
var SimpleDisplay = color.New(color.Bold, color.FgHiWhite).FprintfFunc()
|
||||
var SuccessDisplay = color.New(color.Bold, color.FgHiGreen).FprintfFunc()
|
||||
var DescriptionDisplay = color.New(color.Faint, color.FgWhite).FprintfFunc()
|
||||
|
||||
var Spinner *spinner.Spinner
|
||||
|
||||
func ScanStartDisplay() {
|
||||
if IsSilent() {
|
||||
return
|
||||
}
|
||||
InfoDisplay(os.Stdout, "ARMO security scanner starting\n")
|
||||
}
|
||||
|
||||
func SuccessTextDisplay(str string) {
|
||||
if IsSilent() {
|
||||
return
|
||||
}
|
||||
SuccessDisplay(os.Stdout, "[success] ")
|
||||
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
|
||||
|
||||
}
|
||||
|
||||
func ErrorDisplay(str string) {
|
||||
if IsSilent() {
|
||||
return
|
||||
}
|
||||
SuccessDisplay(os.Stdout, "[Error] ")
|
||||
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
|
||||
|
||||
}
|
||||
|
||||
func ProgressTextDisplay(str string) {
|
||||
if IsSilent() {
|
||||
return
|
||||
}
|
||||
InfoDisplay(os.Stdout, "[progress] ")
|
||||
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
|
||||
|
||||
}
|
||||
func StartSpinner() {
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
if !IsSilent() && isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) // Build our new spinner
|
||||
Spinner.Start()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// CA environment vars
|
||||
var (
|
||||
CustomerGUID = ""
|
||||
@@ -13,12 +9,3 @@ var (
|
||||
DashboardBackendURL = ""
|
||||
RestAPIPort = "4001"
|
||||
)
|
||||
|
||||
func SetupDefaultEnvs() {
|
||||
if os.Getenv("CA_DASHBOARD_BACKEND") == "" {
|
||||
os.Setenv("CA_DASHBOARD_BACKEND", "https://dashbe.eudev3.cyberarmorsoft.com") // use prod
|
||||
}
|
||||
if os.Getenv("CA_CUSTOMER_GUID") == "" {
|
||||
os.Setenv("CA_CUSTOMER_GUID", "11111111-1111-1111-1111-111111111111")
|
||||
}
|
||||
}
|
||||
|
||||
76
cautils/getter/armoapi.go
Normal file
76
cautils/getter/armoapi.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// =======================================================================================================================
|
||||
// =============================================== ArmoAPI ===============================================================
|
||||
// =======================================================================================================================
|
||||
|
||||
// Armo API for downloading policies
|
||||
type ArmoAPI struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
baseURL: "https://dashbe.euprod1.cyberarmorsoft.com",
|
||||
}
|
||||
}
|
||||
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
framework := &opapolicy.Framework{}
|
||||
if err = JSONDecoder(respStr).Decode(framework); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
|
||||
return framework, err
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
|
||||
requestURI := "v1/armoFrameworks"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
|
||||
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(frameworkName))
|
||||
requestURI += "&getRules=true"
|
||||
|
||||
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
exceptions := []armotypes.PostureExceptionPolicy{}
|
||||
if customerGUID == "" {
|
||||
return exceptions, nil
|
||||
}
|
||||
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = JSONDecoder(respStr).Decode(&exceptions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exceptions, nil
|
||||
}
|
||||
|
||||
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
|
||||
requestURI := "api/v1/armoPostureExceptions"
|
||||
requestURI += fmt.Sprintf("?customerGUID=%s", customerGUID)
|
||||
if clusterName != "" {
|
||||
requestURI += fmt.Sprintf("&clusterName=%s", clusterName)
|
||||
}
|
||||
return urlEncoder(fmt.Sprintf("%s/%s", armoAPI.baseURL, requestURI))
|
||||
}
|
||||
87
cautils/getter/downloadreleasedpolicy.go
Normal file
87
cautils/getter/downloadreleasedpolicy.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// =======================================================================================================================
|
||||
// ======================================== DownloadReleasedPolicy =======================================================
|
||||
// =======================================================================================================================
|
||||
|
||||
// Download released version
|
||||
type DownloadReleasedPolicy struct {
|
||||
hostURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
|
||||
return &DownloadReleasedPolicy{
|
||||
hostURL: "",
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
|
||||
return []armotypes.PostureExceptionPolicy{}, nil
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framework, error) {
|
||||
drp.setURL(name)
|
||||
respStr, err := HttpGetter(drp.httpClient, drp.hostURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
framework := &opapolicy.Framework{}
|
||||
if err = JSONDecoder(respStr).Decode(framework); err != nil {
|
||||
return framework, err
|
||||
}
|
||||
|
||||
SaveFrameworkInFile(framework, GetDefaultPath(name))
|
||||
return framework, err
|
||||
}
|
||||
|
||||
func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
|
||||
|
||||
latestReleases := "https://api.github.com/repos/armosec/regolibrary/releases/latest"
|
||||
resp, err := http.Get(latestReleases)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestReleases, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || 301 < resp.StatusCode {
|
||||
return fmt.Errorf("failed to download file, status code: %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body from '%s', reason: %s", latestReleases, err.Error())
|
||||
}
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestReleases, err.Error())
|
||||
}
|
||||
|
||||
if assets, ok := data["assets"].([]interface{}); ok {
|
||||
for i := range assets {
|
||||
if asset, ok := assets[i].(map[string]interface{}); ok {
|
||||
if name, ok := asset["name"].(string); ok {
|
||||
if name == frameworkName {
|
||||
if url, ok := asset["browser_download_url"].(string); ok {
|
||||
drp.hostURL = url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
12
cautils/getter/getpolicies.go
Normal file
12
cautils/getter/getpolicies.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package getter
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type IPolicyGetter interface {
|
||||
GetFramework(name string) (*opapolicy.Framework, error)
|
||||
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
|
||||
// GetScores(scope, customerName, namespace 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(frameworkName string) string {
|
||||
defaultfilePath := filepath.Join(DefaultLocalStore, frameworkName+".json")
|
||||
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,15 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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"
|
||||
)
|
||||
|
||||
// K8SConfig pointer to k8s config
|
||||
@@ -27,13 +27,16 @@ type KubernetesApi struct {
|
||||
|
||||
// NewKubernetesApi -
|
||||
func NewKubernetesApi() *KubernetesApi {
|
||||
|
||||
kubernetesClient, err := kubernetes.NewForConfig(GetK8sConfig())
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("kubernetes.NewForConfig - Failed to load config file, reason: %s", err.Error()))
|
||||
fmt.Printf("Failed to load config file, reason: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(GetK8sConfig())
|
||||
dynamicClient, err := dynamic.NewForConfig(K8SConfig)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("dynamic.NewForConfig - Failed to load config file, reason: %s", err.Error()))
|
||||
fmt.Printf("Failed to load config file, reason: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return &KubernetesApi{
|
||||
@@ -43,30 +46,29 @@ func NewKubernetesApi() *KubernetesApi {
|
||||
}
|
||||
}
|
||||
|
||||
var ConfigPath = filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
// RunningIncluster whether running in cluster
|
||||
var RunningIncluster bool
|
||||
|
||||
// LoadK8sConfig load config from local file or from cluster
|
||||
func LoadK8sConfig() error {
|
||||
kubeconfig, err := clientcmd.BuildConfigFromFlags("", ConfigPath)
|
||||
kubeconfig, err := config.GetConfig()
|
||||
if err != nil {
|
||||
kubeconfig, err = restclient.InClusterConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load kubernetes config from file: '%s', err: %v", ConfigPath, err)
|
||||
}
|
||||
return fmt.Errorf("failed to load kubernetes config: %s\n", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
|
||||
}
|
||||
if _, err := restclient.InClusterConfig(); err == nil {
|
||||
RunningIncluster = true
|
||||
} else {
|
||||
RunningIncluster = false
|
||||
}
|
||||
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 {
|
||||
return nil
|
||||
// print error
|
||||
fmt.Printf("%s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
return K8SConfig
|
||||
|
||||
@@ -3,7 +3,7 @@ package k8sinterface
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"kube-escape/cautils/cautils"
|
||||
"github.com/armosec/kubescape/cautils/cautils"
|
||||
)
|
||||
|
||||
func TestGetGroupVersionResource(t *testing.T) {
|
||||
@@ -23,4 +23,12 @@ func TestGetGroupVersionResource(t *testing.T) {
|
||||
t.Errorf("wrong Resource")
|
||||
}
|
||||
|
||||
r2, err := GetGroupVersionResource("NetworkPolicy")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if r2.Resource != "networkpolicies" {
|
||||
t.Errorf("wrong Resource")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/cautils"
|
||||
"github.com/armosec/kubescape/cautils/cautils"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
@@ -3,7 +3,7 @@ package k8sinterface
|
||||
import (
|
||||
"context"
|
||||
|
||||
"kube-escape/cautils/cautils"
|
||||
"github.com/armosec/kubescape/cautils/cautils"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
@@ -46,10 +46,7 @@ var GroupsClusterScope = []string{}
|
||||
var ResourceClusterScope = []string{"nodes", "namespaces", "clusterroles", "clusterrolebindings"}
|
||||
|
||||
func GetGroupVersionResource(resource string) (schema.GroupVersionResource, error) {
|
||||
resource = strings.ToLower(resource)
|
||||
if resource != "" && !strings.HasSuffix(resource, "s") {
|
||||
resource = fmt.Sprintf("%ss", resource) // add 's' at the end of a resource
|
||||
}
|
||||
resource = updateResourceKind(resource)
|
||||
if r, ok := ResourceGroupMapping[resource]; ok {
|
||||
gv := strings.Split(r, "/")
|
||||
return schema.GroupVersionResource{Group: gv[0], Version: gv[1], Resource: resource}, nil
|
||||
@@ -116,10 +113,7 @@ func ResourceGroupToString(group, version, resource string) []string {
|
||||
if resource == "*" {
|
||||
resource = ""
|
||||
}
|
||||
resource = strings.ToLower(resource)
|
||||
if resource != "" && !strings.HasSuffix(resource, "s") {
|
||||
resource = fmt.Sprintf("%ss", resource) // add 's' at the end of a resource
|
||||
}
|
||||
resource = updateResourceKind(resource)
|
||||
return GetResourceTriplets(group, version, resource)
|
||||
}
|
||||
|
||||
@@ -132,3 +126,17 @@ func StringToResourceGroup(str string) (string, string, string) {
|
||||
}
|
||||
return splitted[0], splitted[1], splitted[2]
|
||||
}
|
||||
|
||||
func updateResourceKind(resource string) string {
|
||||
resource = strings.ToLower(resource)
|
||||
|
||||
if resource != "" && !strings.HasSuffix(resource, "s") {
|
||||
if strings.HasSuffix(resource, "y") {
|
||||
return fmt.Sprintf("%sies", strings.TrimSuffix(resource, "y")) // e.g. NetworkPolicy -> networkpolicies
|
||||
} else {
|
||||
return fmt.Sprintf("%ss", resource) // add 's' at the end of a resource
|
||||
}
|
||||
}
|
||||
return resource
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package k8sinterface
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"kube-escape/cautils/apis"
|
||||
"github.com/armosec/kubescape/cautils/apis"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
@@ -15,9 +14,16 @@ import (
|
||||
type IWorkload interface {
|
||||
IBasicWorkload
|
||||
|
||||
// Convert
|
||||
ToUnstructured() (*unstructured.Unstructured, error)
|
||||
ToString() string
|
||||
Json() string // DEPRECATED
|
||||
|
||||
// GET
|
||||
GetWlid() string
|
||||
GetJobID() *apis.JobTracking
|
||||
GetVersion() string
|
||||
GetGroup() string
|
||||
|
||||
// SET
|
||||
SetWlid(string)
|
||||
@@ -27,6 +33,7 @@ type IWorkload interface {
|
||||
SetJobID(apis.JobTracking)
|
||||
SetCompatible()
|
||||
SetIncompatible()
|
||||
SetReplaceheaders()
|
||||
|
||||
// EXIST
|
||||
IsIgnore() bool
|
||||
@@ -37,6 +44,7 @@ type IWorkload interface {
|
||||
|
||||
// REMOVE
|
||||
RemoveWlid()
|
||||
RemoveSecretData()
|
||||
RemoveInject()
|
||||
RemoveIgnore()
|
||||
RemoveUpdateTime()
|
||||
@@ -62,8 +70,8 @@ type IBasicWorkload interface {
|
||||
GetGenerateName() string
|
||||
GetApiVersion() string
|
||||
GetKind() string
|
||||
GetInnerAnnotation() (string, bool)
|
||||
GetPodAnnotation() (string, bool)
|
||||
GetInnerAnnotation(string) (string, bool)
|
||||
GetPodAnnotation(string) (string, bool)
|
||||
GetAnnotation(string) (string, bool)
|
||||
GetLabel(string) (string, bool)
|
||||
GetAnnotations() map[string]string
|
||||
@@ -72,16 +80,17 @@ type IBasicWorkload interface {
|
||||
GetLabels() map[string]string
|
||||
GetInnerLabels() map[string]string
|
||||
GetPodLabels() map[string]string
|
||||
GetJobLabels() map[string]string
|
||||
GetVolumes() []corev1.Volume
|
||||
GetContainers() []corev1.Container
|
||||
GetInitContainers() []corev1.Container
|
||||
GetVolumes() ([]corev1.Volume, error)
|
||||
GetReplicas() int
|
||||
GetContainers() ([]corev1.Container, error)
|
||||
GetInitContainers() ([]corev1.Container, error)
|
||||
GetOwnerReferences() ([]metav1.OwnerReference, error)
|
||||
GetImagePullSecret() ([]corev1.LocalObjectReference, error)
|
||||
GetServiceAccountName() string
|
||||
GetSelector() (*metav1.LabelSelector, error)
|
||||
GetResourceVersion() string
|
||||
GetUID() string
|
||||
GetPodSpec() (*corev1.PodSpec, error)
|
||||
|
||||
GetWorkload() map[string]interface{}
|
||||
|
||||
@@ -115,14 +124,17 @@ func NewWorkloadObj(workload map[string]interface{}) *Workload {
|
||||
}
|
||||
|
||||
func (w *Workload) Json() string {
|
||||
if w.workload == nil {
|
||||
return w.ToString()
|
||||
}
|
||||
func (w *Workload) ToString() string {
|
||||
if w.GetWorkload() == nil {
|
||||
return ""
|
||||
}
|
||||
bWorkload, err := json.Marshal(w.workload)
|
||||
bWorkload, err := json.Marshal(w.GetWorkload())
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return fmt.Sprintf("%s", bWorkload)
|
||||
return string(bWorkload)
|
||||
}
|
||||
|
||||
func (workload *Workload) DeepCopy(w map[string]interface{}) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kube-escape/cautils/apis"
|
||||
"kube-escape/cautils/cautils"
|
||||
"github.com/armosec/kubescape/cautils/apis"
|
||||
"github.com/armosec/kubescape/cautils/cautils"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -117,6 +117,10 @@ func (w *Workload) RemoveUpdateTime() {
|
||||
w.RemoveAnnotation(cautils.CAUpdate) // DEPRECATED
|
||||
w.RemoveAnnotation(cautils.ArmoUpdate)
|
||||
}
|
||||
func (w *Workload) RemoveSecretData() {
|
||||
w.RemoveAnnotation("kubectl.kubernetes.io/last-applied-configuration")
|
||||
delete(w.workload, "data")
|
||||
}
|
||||
|
||||
func (w *Workload) RemovePodStatus() {
|
||||
delete(w.workload, "status")
|
||||
@@ -268,6 +272,26 @@ func (w *Workload) GetApiVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *Workload) GetVersion() string {
|
||||
apiVersion := w.GetApiVersion()
|
||||
splitted := strings.Split(apiVersion, "/")
|
||||
if len(splitted) == 1 {
|
||||
return splitted[0]
|
||||
} else if len(splitted) == 2 {
|
||||
return splitted[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *Workload) GetGroup() string {
|
||||
apiVersion := w.GetApiVersion()
|
||||
splitted := strings.Split(apiVersion, "/")
|
||||
if len(splitted) == 2 {
|
||||
return splitted[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *Workload) GetGenerateName() string {
|
||||
if v, ok := InspectWorkload(w.workload, "metadata", "generateName"); ok {
|
||||
return v.(string)
|
||||
@@ -275,6 +299,16 @@ func (w *Workload) GetGenerateName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *Workload) GetReplicas() int {
|
||||
if v, ok := InspectWorkload(w.workload, "spec", "replicas"); ok {
|
||||
replicas, isok := v.(float64)
|
||||
if isok {
|
||||
return int(replicas)
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (w *Workload) GetKind() string {
|
||||
if v, ok := InspectWorkload(w.workload, "kind"); ok {
|
||||
return v.(string)
|
||||
@@ -453,7 +487,7 @@ func (w *Workload) GetContainers() ([]corev1.Container, error) {
|
||||
return containers, err
|
||||
}
|
||||
|
||||
// GetContainers -
|
||||
// GetInitContainers -
|
||||
func (w *Workload) GetInitContainers() ([]corev1.Container, error) {
|
||||
containers := []corev1.Container{}
|
||||
|
||||
@@ -606,52 +640,3 @@ func InspectWorkload(workload interface{}, scopes ...string) (val interface{}, k
|
||||
return val, k
|
||||
|
||||
}
|
||||
|
||||
// // InspectWorkload -
|
||||
// func InjectWorkload(workload interface{}, scopes []string, val string) {
|
||||
|
||||
// if len(scopes) == 0 {
|
||||
|
||||
// }
|
||||
// if data, ok := workload.(map[string]interface{}); ok {
|
||||
// InjectWorkload(data[scopes[0]], scopes[1:], val)
|
||||
// } else {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// InjectWorkload -
|
||||
// func InjectWorkload(workload interface{}, scopes []string, val string) {
|
||||
|
||||
// if len(scopes) == 0 {
|
||||
// workload = ""
|
||||
// }
|
||||
// if data, ok := workload.(map[string]interface{}); ok {
|
||||
// d := InjectWorkload(data[scopes[0]], scopes[1:], val)
|
||||
// data[scopes[0]] = d
|
||||
// return data
|
||||
// } else {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
// func (w *Workload) SetNamespace(ns string) {
|
||||
|
||||
// if v, k := w.workload["metadata"]; k {
|
||||
// if vv, kk := v.(map[string]interface{}); kk {
|
||||
// vv["namespace"] = ""
|
||||
// // if v3, k3 := w.workload["namespace"]; k3 {
|
||||
// // if v4, k4 := v.(map[string]interface{}); kk {
|
||||
|
||||
// // }
|
||||
// // }
|
||||
// v = vv
|
||||
// }
|
||||
// w.workload = v
|
||||
// }
|
||||
// // if data, ok := w.workload.(map[string]interface{}); ok {
|
||||
// // val, k = InspectWorkload(data[scopes[0]], scopes[1:]...)
|
||||
// // }
|
||||
|
||||
// }
|
||||
|
||||
@@ -3,7 +3,7 @@ package opapolicy
|
||||
import (
|
||||
"time"
|
||||
|
||||
armotypes "kube-escape/cautils/armotypes"
|
||||
armotypes "github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
type AlertScore float32
|
||||
@@ -16,37 +16,47 @@ 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 {
|
||||
K8SApiObjects []map[string]interface{} `json:"k8sApiObjects,omitempty"`
|
||||
ExternalObjects []map[string]interface{} `json:"externalObjects,omitempty"`
|
||||
ExternalObjects map[string]interface{} `json:"externalObjects,omitempty"`
|
||||
}
|
||||
|
||||
type FrameworkReport struct {
|
||||
Name string `json:"name"`
|
||||
ControlReports []ControlReport `json:"controlReports"`
|
||||
Name string `json:"name"`
|
||||
ControlReports []ControlReport `json:"controlReports"`
|
||||
Score float32 `json:"score,omitempty"`
|
||||
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
|
||||
WCSScore float32 `json:"wcsScore,omitempty"`
|
||||
}
|
||||
type ControlReport struct {
|
||||
Name string `json:"name"`
|
||||
RuleReports []RuleReport `json:"ruleReports"`
|
||||
Remediation string `json:"remediation"`
|
||||
Description string `json:"description"`
|
||||
armotypes.PortalBase `json:",inline"`
|
||||
ControlID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
RuleReports []RuleReport `json:"ruleReports"`
|
||||
Remediation string `json:"remediation"`
|
||||
Description string `json:"description"`
|
||||
Score float32 `json:"score,omitempty"`
|
||||
BaseScore float32 `json:"baseScore,omitempty"`
|
||||
ARMOImprovement float32 `json:"ARMOImprovement,omitempty"`
|
||||
}
|
||||
type RuleReport struct {
|
||||
Name string `json:"name"`
|
||||
Remediation string `json:"remediation"`
|
||||
RuleStatus RuleStatus `json:"ruleStatus"`
|
||||
RuleResponses []RuleResponse `json:"ruleResponses"`
|
||||
NumOfResources int
|
||||
Name string `json:"name"`
|
||||
Remediation string `json:"remediation"`
|
||||
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:"-"`
|
||||
}
|
||||
type RuleStatus struct {
|
||||
Status string `json:"status"`
|
||||
@@ -91,10 +101,12 @@ type PolicyRule struct {
|
||||
// Control represents a collection of rules which are combined together to single purpose
|
||||
type Control struct {
|
||||
armotypes.PortalBase `json:",inline"`
|
||||
CreationTime string `json:"creationTime"`
|
||||
Description string `json:"description"`
|
||||
Remediation string `json:"remediation"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
ControlID string `json:"id"`
|
||||
|
||||
CreationTime string `json:"creationTime"`
|
||||
Description string `json:"description"`
|
||||
Remediation string `json:"remediation"`
|
||||
Rules []PolicyRule `json:"rules"`
|
||||
// for new list of rules in POST/UPADTE requests
|
||||
RulesIDs *[]string `json:"rulesIDs,omitempty"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package opapolicy
|
||||
import (
|
||||
"time"
|
||||
|
||||
armotypes "kube-escape/cautils/armotypes"
|
||||
armotypes "github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
// Mock A
|
||||
@@ -33,7 +33,8 @@ func MockFrameworkReportA() *FrameworkReport {
|
||||
Name: AMockFrameworkName,
|
||||
ControlReports: []ControlReport{
|
||||
{
|
||||
Name: AMockControlName,
|
||||
ControlID: "testctrl",
|
||||
Name: AMockControlName,
|
||||
RuleReports: []RuleReport{
|
||||
{
|
||||
Name: AMockRuleName,
|
||||
|
||||
@@ -3,10 +3,6 @@ package opapolicy
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
)
|
||||
|
||||
func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
|
||||
@@ -17,6 +13,19 @@ func (pn *PolicyNotification) ToJSONBytesBuffer() (*bytes.Buffer, error) {
|
||||
return bytes.NewBuffer(res), err
|
||||
}
|
||||
|
||||
func (RuleResponse *RuleResponse) GetSingleResultStatus() string {
|
||||
if RuleResponse.Exception != nil {
|
||||
if RuleResponse.Exception.IsAlertOnly() {
|
||||
return "warning"
|
||||
}
|
||||
if RuleResponse.Exception.IsDisable() {
|
||||
return "ignore"
|
||||
}
|
||||
}
|
||||
return "failed"
|
||||
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleResponse) {
|
||||
if len(ruleReport.RuleResponses) == 0 {
|
||||
return "success", nil, nil
|
||||
@@ -26,9 +35,11 @@ func (ruleReport *RuleReport) GetRuleStatus() (string, []RuleResponse, []RuleRes
|
||||
|
||||
for _, rule := range ruleReport.RuleResponses {
|
||||
if rule.ExceptionName != "" {
|
||||
failed = append(failed, rule)
|
||||
} else {
|
||||
exceptions = append(exceptions, rule)
|
||||
} else if rule.Exception != nil {
|
||||
exceptions = append(exceptions, rule)
|
||||
} else {
|
||||
failed = append(failed, rule)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,52 +49,91 @@ 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 {
|
||||
sum += controlReport.RuleReports[i].NumOfResources
|
||||
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 {
|
||||
listControlsInputKinds = append(listControlsInputKinds, controlReport.RuleReports[i].ListInputKinds...)
|
||||
}
|
||||
return listControlsInputKinds
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Passed() bool {
|
||||
for i := range controlReport.RuleReports {
|
||||
if len(controlReport.RuleReports[i].RuleResponses) > 0 {
|
||||
return false
|
||||
if len(controlReport.RuleReports[i].RuleResponses) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Warning() bool {
|
||||
if controlReport.Passed() || controlReport.Failed() {
|
||||
return false
|
||||
}
|
||||
for i := range controlReport.RuleReports {
|
||||
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "warning" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (controlReport *ControlReport) Failed() bool {
|
||||
return !controlReport.Passed()
|
||||
if controlReport.Passed() {
|
||||
return false
|
||||
}
|
||||
for i := range controlReport.RuleReports {
|
||||
if status, _, _ := controlReport.RuleReports[i].GetRuleStatus(); status == "failed" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfResources() int {
|
||||
return len(ruleReport.ListInputResources)
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfFailedResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (ruleReport *RuleReport) GetNumberOfWarningResources() int {
|
||||
sum := 0
|
||||
for i := range ruleReport.RuleResponses {
|
||||
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "warning" {
|
||||
sum += 1
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
@@ -196,6 +196,21 @@ query_all(resource) = http.send({
|
||||
"raise_error": true,
|
||||
})
|
||||
|
||||
|
||||
|
||||
# Query for all resources of type resource in all namespaces - without authentication
|
||||
# Example: query_all("deployments")
|
||||
query_all_no_auth(resource) = http.send({
|
||||
"url": sprintf("%v/%v/namespaces/default/%v", [
|
||||
host,
|
||||
resource_group_mapping[resource],
|
||||
resource,
|
||||
]),
|
||||
"method": "get",
|
||||
"raise_error": true,
|
||||
"tls_insecure_skip_verify" : true,
|
||||
})
|
||||
|
||||
field_transform_to_qry_param(field,map) = finala {
|
||||
mid := {concat(".",[field,key]): val | val := map[key]}
|
||||
finala := label_map_to_query_string(mid)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
@@ -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,
|
||||
|
||||
82
cautils/scaninfo.go
Normal file
82
cautils/scaninfo.go
Normal file
@@ -0,0 +1,82 @@
|
||||
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
|
||||
}
|
||||
|
||||
type Getters struct {
|
||||
ExceptionsGetter getter.IPolicyGetter
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
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`")
|
||||
}
|
||||
169
cmd/framework.go
Normal file
169
cmd/framework.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"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/resultshandling"
|
||||
"github.com/armosec/kubescape/resultshandling/printer"
|
||||
"github.com/armosec/kubescape/resultshandling/reporter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var scanInfo cautils.ScanInfo
|
||||
var supportedFrameworks = []string{"nsa"}
|
||||
|
||||
type CLIHandler struct {
|
||||
policyHandler *policyhandler.PolicyHandler
|
||||
scanInfo *cautils.ScanInfo
|
||||
}
|
||||
|
||||
var frameworkCmd = &cobra.Command{
|
||||
Use: "framework <framework name> [`<glob patter>`/`-`] [flags]",
|
||||
Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(supportedFrameworks, ", ")),
|
||||
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 && !(cmd.Flags().Lookup("use-from").Changed) {
|
||||
return fmt.Errorf("requires at least one argument")
|
||||
} 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
|
||||
|
||||
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
|
||||
}
|
||||
scanInfo.InputPatterns = []string{tempFile.Name()}
|
||||
}
|
||||
}
|
||||
scanInfo.Init()
|
||||
cautils.SetSilentMode(scanInfo.Silent)
|
||||
err := CliSetup()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func isValidFramework(framework string) bool {
|
||||
return cautils.StringInSlice(supportedFrameworks, framework) != cautils.ValueNotFound
|
||||
}
|
||||
|
||||
func init() {
|
||||
scanCmd.AddCommand(frameworkCmd)
|
||||
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")
|
||||
|
||||
}
|
||||
|
||||
func CliSetup() error {
|
||||
flag.Parse()
|
||||
|
||||
if 100 < scanInfo.FailThreshold {
|
||||
fmt.Println("bad argument: out of range threshold")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var k8s *k8sinterface.KubernetesApi
|
||||
if scanInfo.ScanRunningCluster() {
|
||||
k8s = k8sinterface.NewKubernetesApi()
|
||||
}
|
||||
|
||||
processNotification := make(chan *cautils.OPASessionObj)
|
||||
reportResults := make(chan *cautils.OPASessionObj)
|
||||
|
||||
// policy handler setup
|
||||
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
|
||||
|
||||
// cli handler setup
|
||||
cli := NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// processor setup - rego run
|
||||
go func() {
|
||||
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
|
||||
opaprocessorObj.ProcessRulesListenner()
|
||||
}()
|
||||
|
||||
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
|
||||
score := resultsHandling.HandleResults()
|
||||
|
||||
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
|
||||
if score < adjustedFailThreshold {
|
||||
return fmt.Errorf("Scan score is bellow threshold")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCLIHandler(policyHandler *policyhandler.PolicyHandler) *CLIHandler {
|
||||
return &CLIHandler{
|
||||
scanInfo: &scanInfo,
|
||||
policyHandler: policyHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (clihandler *CLIHandler) Scan() error {
|
||||
cautils.ScanStartDisplay()
|
||||
policyNotification := &opapolicy.PolicyNotification{
|
||||
NotificationType: opapolicy.TypeExecPostureScan,
|
||||
Rules: []opapolicy.PolicyIdentifier{
|
||||
clihandler.scanInfo.PolicyIdentifier,
|
||||
},
|
||||
Designators: armotypes.PortalDesignator{},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
cmd/root.go
Normal file
25
cmd/root.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
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 specifications.`,
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
}
|
||||
18
cmd/scan.go
Normal file
18
cmd/scan.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// scanCmd represents the scan command
|
||||
var scanCmd = &cobra.Command{
|
||||
Use: "scan",
|
||||
Short: "Scan the current running cluster or yaml files",
|
||||
Long: `The action you want to perform`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(scanCmd)
|
||||
}
|
||||
BIN
docs/demo.gif
Executable file
BIN
docs/demo.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 897 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 44 KiB |
35
docs/new-feature.svg
Normal file
35
docs/new-feature.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20">
|
||||
<defs>
|
||||
<linearGradient id="workflow-fill" x1="50%" y1="0%" x2="50%" y2="100%">
|
||||
<stop stop-color="#444D56" offset="0%"></stop>
|
||||
<stop stop-color="#24292E" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="state-fill" x1="50%" y1="0%" x2="50%" y2="100%">
|
||||
<stop stop-color="#34D058" offset="0%"></stop>
|
||||
<stop stop-color="#28A745" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g font-family="'DejaVu Sans',Verdana,Geneva,sans-serif" font-size="11">
|
||||
<path id="workflow-bg" d="M0,3 C0,1.3431 1.3552,0 3.02702703,0 L54,0 L54,20 L3.02702703,20 C1.3552,20 0,18.6569 0,17 L0,3 Z" fill="url(#workflow-fill)" fill-rule="nonzero"></path>
|
||||
<text fill="#010101" fill-opacity=".3">
|
||||
<tspan x="22.1981982" y="15">new</tspan>
|
||||
</text>
|
||||
<text fill="#FFFFFF">
|
||||
<tspan x="22.1981982" y="14">new</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g transform="translate(54)" font-family="'DejaVu Sans',Verdana,Geneva,sans-serif" font-size="11">
|
||||
<path d="M0 0h46.939C48.629 0 50 1.343 50 3v14c0 1.657-1.37 3-3.061 3H0V0z" id="state-bg" fill="url(#state-fill)" fill-rule="nonzero"></path>
|
||||
<text fill="#010101" fill-opacity=".3">
|
||||
<tspan x="4" y="15">feature</tspan>
|
||||
</text>
|
||||
<text fill="#FFFFFF">
|
||||
<tspan x="4" y="14">feature</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<path fill="#959DA5" d="M11 3c-3.868 0-7 3.132-7 7a6.996 6.996 0 0 0 4.786 6.641c.35.062.482-.148.482-.332 0-.166-.01-.718-.01-1.304-1.758.324-2.213-.429-2.353-.822-.079-.202-.42-.823-.717-.99-.245-.13-.595-.454-.01-.463.552-.009.946.508 1.077.718.63 1.058 1.636.76 2.039.577.061-.455.245-.761.446-.936-1.557-.175-3.185-.779-3.185-3.456 0-.762.271-1.392.718-1.882-.07-.175-.315-.892.07-1.855 0 0 .586-.183 1.925.718a6.5 6.5 0 0 1 1.75-.236 6.5 6.5 0 0 1 1.75.236c1.338-.91 1.925-.718 1.925-.718.385.963.14 1.68.07 1.855.446.49.717 1.112.717 1.882 0 2.686-1.636 3.28-3.194 3.456.254.219.473.639.473 1.295 0 .936-.009 1.689-.009 1.925 0 .184.131.402.481.332A7.011 7.011 0 0 0 18 10c0-3.867-3.133-7-7-7z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
91
docs/release.md
Normal file
91
docs/release.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Kubescape Release
|
||||
|
||||
|
||||
## Input
|
||||
|
||||
### Scan a running Kubernetes cluster
|
||||
|
||||
* Scan your Kubernetes cluster. Ignore `kube-system` and `kube-public` namespaces
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan your Kubernetes cluster
|
||||
```
|
||||
kubescape scan framework nsa
|
||||
```
|
||||
|
||||
### Scan a local Kubernetes manifest
|
||||
|
||||
* Scan single Kubernetes manifest file <img src="new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa <my-workload.yaml>
|
||||
```
|
||||
|
||||
* Scan many Kubernetes manifest files <img src="new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa <my-workload-1.yaml> <my-workload-2.yaml>
|
||||
```
|
||||
|
||||
* Scan all Kubernetes manifest files in directory <img src="new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
* Scan Kubernetes manifest from stdout <img src="new-feature.svg">
|
||||
```
|
||||
cat <my-workload.yaml> | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
|
||||
* Scan Kubernetes manifest url <img src="new-feature.svg">
|
||||
```
|
||||
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
|
||||
```
|
||||
|
||||
### Scan HELM chart
|
||||
|
||||
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout <img src="new-feature.svg">
|
||||
```
|
||||
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.
|
||||
|
||||
For the sake of automation, it is possible to receive the result in a `json` or `junit xml` format.
|
||||
|
||||
* Output in `json` format <img src="new-feature.svg">
|
||||
```
|
||||
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
Normal file
BIN
docs/summary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/using-mov.gif
Executable file
BIN
docs/using-mov.gif
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
5
examples/example.md
Normal file
5
examples/example.md
Normal file
@@ -0,0 +1,5 @@
|
||||
#! /bin/bash
|
||||
|
||||
echo "Testing Online Boutique yamls (https://github.com/GoogleCloudPlatform/microservices-demo)"
|
||||
|
||||
kubescape scan framework nsa online-boutique/*
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
73
examples/online-boutique/adservice.yaml
Normal file
73
examples/online-boutique/adservice.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: adservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: adservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: adservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: adservice
|
||||
ports:
|
||||
- containerPort: 9555
|
||||
env:
|
||||
- name: PORT
|
||||
value: "9555"
|
||||
# - name: DISABLE_STATS
|
||||
# value: "1"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
#- name: JAEGER_SERVICE_ADDR
|
||||
# value: "jaeger-collector:14268"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 180Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 300Mi
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:9555"]
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:9555"]
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: adservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: adservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 9555
|
||||
targetPort: 9555
|
||||
66
examples/online-boutique/cartservice.yaml
Normal file
66
examples/online-boutique/cartservice.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cartservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cartservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cartservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: cartservice
|
||||
ports:
|
||||
- containerPort: 7070
|
||||
env:
|
||||
- name: REDIS_ADDR
|
||||
value: "redis-cart:6379"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 300m
|
||||
memory: 128Mi
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 15
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:7070", "-rpc-timeout=5s"]
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:7070", "-rpc-timeout=5s"]
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: cartservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: cartservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 7070
|
||||
targetPort: 7070
|
||||
82
examples/online-boutique/checkoutservice.yaml
Normal file
82
examples/online-boutique/checkoutservice.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: checkoutservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: checkoutservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: checkoutservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- name: server
|
||||
image: checkoutservice
|
||||
ports:
|
||||
- containerPort: 5050
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:5050"]
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:5050"]
|
||||
env:
|
||||
- name: PORT
|
||||
value: "5050"
|
||||
- name: PRODUCT_CATALOG_SERVICE_ADDR
|
||||
value: "productcatalogservice:3550"
|
||||
- name: SHIPPING_SERVICE_ADDR
|
||||
value: "shippingservice:50051"
|
||||
- name: PAYMENT_SERVICE_ADDR
|
||||
value: "paymentservice:50051"
|
||||
- name: EMAIL_SERVICE_ADDR
|
||||
value: "emailservice:5000"
|
||||
- name: CURRENCY_SERVICE_ADDR
|
||||
value: "currencyservice:7000"
|
||||
- name: CART_SERVICE_ADDR
|
||||
value: "cartservice:7070"
|
||||
# - name: DISABLE_STATS
|
||||
# value: "1"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: JAEGER_SERVICE_ADDR
|
||||
# value: "jaeger-collector:14268"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: checkoutservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: checkoutservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 5050
|
||||
targetPort: 5050
|
||||
70
examples/online-boutique/currencyservice.yaml
Normal file
70
examples/online-boutique/currencyservice.yaml
Normal file
@@ -0,0 +1,70 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: currencyservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: currencyservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: currencyservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: currencyservice
|
||||
ports:
|
||||
- name: grpc
|
||||
containerPort: 7000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "7000"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: DISABLE_DEBUGGER
|
||||
# value: "1"
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:7000"]
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:7000"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: currencyservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: currencyservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 7000
|
||||
targetPort: 7000
|
||||
69
examples/online-boutique/emailservice.yaml
Normal file
69
examples/online-boutique/emailservice.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: emailservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: emailservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: emailservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: emailservice
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
- name: DISABLE_PROFILER
|
||||
value: "1"
|
||||
readinessProbe:
|
||||
periodSeconds: 5
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8080"]
|
||||
livenessProbe:
|
||||
periodSeconds: 5
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8080"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: emailservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: emailservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 5000
|
||||
targetPort: 8080
|
||||
109
examples/online-boutique/frontend.yaml
Normal file
109
examples/online-boutique/frontend.yaml
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: frontend
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: frontend
|
||||
annotations:
|
||||
sidecar.istio.io/rewriteAppHTTPProbers: "true"
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- name: server
|
||||
image: frontend
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 10
|
||||
httpGet:
|
||||
path: "/_healthz"
|
||||
port: 8080
|
||||
httpHeaders:
|
||||
- name: "Cookie"
|
||||
value: "shop_session-id=x-readiness-probe"
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 10
|
||||
httpGet:
|
||||
path: "/_healthz"
|
||||
port: 8080
|
||||
httpHeaders:
|
||||
- name: "Cookie"
|
||||
value: "shop_session-id=x-liveness-probe"
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
- name: PRODUCT_CATALOG_SERVICE_ADDR
|
||||
value: "productcatalogservice:3550"
|
||||
- name: CURRENCY_SERVICE_ADDR
|
||||
value: "currencyservice:7000"
|
||||
- name: CART_SERVICE_ADDR
|
||||
value: "cartservice:7070"
|
||||
- name: RECOMMENDATION_SERVICE_ADDR
|
||||
value: "recommendationservice:8080"
|
||||
- name: SHIPPING_SERVICE_ADDR
|
||||
value: "shippingservice:50051"
|
||||
- name: CHECKOUT_SERVICE_ADDR
|
||||
value: "checkoutservice:5050"
|
||||
- name: AD_SERVICE_ADDR
|
||||
value: "adservice:9555"
|
||||
- name: ENV_PLATFORM
|
||||
value: "gcp"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: JAEGER_SERVICE_ADDR
|
||||
# value: "jaeger-collector:14268"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: frontend
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: frontend-external
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: frontend
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
47
examples/online-boutique/loadgenerator.yaml
Normal file
47
examples/online-boutique/loadgenerator.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: loadgenerator
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: loadgenerator
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: loadgenerator
|
||||
annotations:
|
||||
sidecar.istio.io/rewriteAppHTTPProbers: "true"
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
restartPolicy: Always
|
||||
containers:
|
||||
- name: main
|
||||
image: loadgenerator
|
||||
env:
|
||||
- name: FRONTEND_ADDR
|
||||
value: "frontend:80"
|
||||
- name: USERS
|
||||
value: "10"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 300m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
63
examples/online-boutique/paymentservice.yaml
Normal file
63
examples/online-boutique/paymentservice.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: paymentservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: paymentservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: paymentservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: paymentservice
|
||||
ports:
|
||||
- containerPort: 50051
|
||||
env:
|
||||
- name: PORT
|
||||
value: "50051"
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:50051"]
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:50051"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: paymentservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: paymentservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 50051
|
||||
targetPort: 50051
|
||||
71
examples/online-boutique/productcatalogservice.yaml
Normal file
71
examples/online-boutique/productcatalogservice.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: productcatalogservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: productcatalogservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: productcatalogservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: productcatalogservice
|
||||
ports:
|
||||
- containerPort: 3550
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3550"
|
||||
# - name: DISABLE_STATS
|
||||
# value: "1"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: JAEGER_SERVICE_ADDR
|
||||
# value: "jaeger-collector:14268"
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3550"]
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:3550"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: productcatalogservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: productcatalogservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 3550
|
||||
targetPort: 3550
|
||||
73
examples/online-boutique/recommendationservice.yaml
Normal file
73
examples/online-boutique/recommendationservice.yaml
Normal file
@@ -0,0 +1,73 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: recommendationservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: recommendationservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: recommendationservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
terminationGracePeriodSeconds: 5
|
||||
containers:
|
||||
- name: server
|
||||
image: recommendationservice
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
readinessProbe:
|
||||
periodSeconds: 5
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8080"]
|
||||
livenessProbe:
|
||||
periodSeconds: 5
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:8080"]
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
- name: PRODUCT_CATALOG_SERVICE_ADDR
|
||||
value: "productcatalogservice:3550"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: DISABLE_DEBUGGER
|
||||
# value: "1"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 220Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 450Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: recommendationservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: recommendationservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
66
examples/online-boutique/redis.yaml
Normal file
66
examples/online-boutique/redis.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis-cart
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis-cart
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis-cart
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
readinessProbe:
|
||||
periodSeconds: 5
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
livenessProbe:
|
||||
periodSeconds: 5
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: redis-data
|
||||
resources:
|
||||
limits:
|
||||
memory: 256Mi
|
||||
cpu: 125m
|
||||
requests:
|
||||
cpu: 70m
|
||||
memory: 200Mi
|
||||
volumes:
|
||||
- name: redis-data
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-cart
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: redis-cart
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
71
examples/online-boutique/shippingservice.yaml
Normal file
71
examples/online-boutique/shippingservice.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
# Copyright 2018 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: shippingservice
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: shippingservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: shippingservice
|
||||
spec:
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- name: server
|
||||
image: shippingservice
|
||||
ports:
|
||||
- containerPort: 50051
|
||||
env:
|
||||
- name: PORT
|
||||
value: "50051"
|
||||
# - name: DISABLE_STATS
|
||||
# value: "1"
|
||||
# - name: DISABLE_TRACING
|
||||
# value: "1"
|
||||
# - name: DISABLE_PROFILER
|
||||
# value: "1"
|
||||
# - name: JAEGER_SERVICE_ADDR
|
||||
# value: "jaeger-collector:14268"
|
||||
readinessProbe:
|
||||
periodSeconds: 5
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:50051"]
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["/bin/grpc_health_probe", "-addr=:50051"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: shippingservice
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: shippingservice
|
||||
ports:
|
||||
- name: grpc
|
||||
port: 50051
|
||||
targetPort: 50051
|
||||
20
go.mod
20
go.mod
@@ -1,9 +1,9 @@
|
||||
module kube-escape
|
||||
module github.com/armosec/kubescape
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.40.20
|
||||
github.com/aws/aws-sdk-go v1.40.30
|
||||
github.com/briandowns/spinner v1.16.0
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/docker/docker v20.10.8+incompatible
|
||||
@@ -13,18 +13,18 @@ require (
|
||||
github.com/fatih/color v1.12.0
|
||||
github.com/francoispqt/gojay v1.2.13
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529
|
||||
github.com/golang/glog v1.0.0
|
||||
github.com/mattn/go-isatty v0.0.13
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/open-policy-agent/opa v0.31.0
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
k8s.io/api v0.22.0
|
||||
k8s.io/apimachinery v0.22.0
|
||||
k8s.io/client-go v0.22.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.22.1
|
||||
k8s.io/apimachinery v0.22.1
|
||||
k8s.io/client-go v0.22.1
|
||||
sigs.k8s.io/controller-runtime v0.9.6
|
||||
)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package clihandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"kube-escape/policyhandler"
|
||||
"os"
|
||||
|
||||
"kube-escape/cautils/armotypes"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
type CLIHandler struct {
|
||||
policyHandler *policyhandler.PolicyHandler
|
||||
flagHandler FlagHandler
|
||||
}
|
||||
|
||||
func NewCLIHandler(policyHandler *policyhandler.PolicyHandler) *CLIHandler {
|
||||
return &CLIHandler{
|
||||
flagHandler: *NewFlagHandler(),
|
||||
policyHandler: policyHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (clihandler *CLIHandler) Scan() error {
|
||||
clihandler.flagHandler.ParseFlag()
|
||||
if !clihandler.flagHandler.ExecuteScan() {
|
||||
os.Exit(0)
|
||||
}
|
||||
cautils.InfoDisplay(os.Stdout, "ARMO security scanner starting\n")
|
||||
|
||||
policyNotification := &opapolicy.PolicyNotification{
|
||||
NotificationType: opapolicy.TypeExecPostureScan,
|
||||
Rules: []opapolicy.PolicyIdentifier{
|
||||
*clihandler.flagHandler.policyIdentifier,
|
||||
},
|
||||
Designators: armotypes.PortalDesignator{},
|
||||
}
|
||||
|
||||
switch policyNotification.NotificationType {
|
||||
case opapolicy.TypeExecPostureScan:
|
||||
go func() {
|
||||
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package clihandler
|
||||
@@ -1,97 +0,0 @@
|
||||
package clihandler
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type FlagHandler struct {
|
||||
policyIdentifier *opapolicy.PolicyIdentifier
|
||||
}
|
||||
|
||||
func NewFlagHandler() *FlagHandler {
|
||||
flag.Parse()
|
||||
return &FlagHandler{}
|
||||
}
|
||||
|
||||
func (flagHandler *FlagHandler) ExecuteScan() bool {
|
||||
return flagHandler.policyIdentifier != nil
|
||||
}
|
||||
|
||||
// SetupHTTPListener set up listening http servers
|
||||
func (flagHandler *FlagHandler) ParseFlag() {
|
||||
f := "help"
|
||||
if len(flag.Args()) >= 1 {
|
||||
f = strings.ToLower(flag.Arg(0))
|
||||
}
|
||||
switch f {
|
||||
case "scan":
|
||||
flagHandler.Scan()
|
||||
case "version":
|
||||
flagHandler.Version()
|
||||
case "help":
|
||||
flagHandler.Help()
|
||||
default:
|
||||
fmt.Println("unknown input argument")
|
||||
flagHandler.Help()
|
||||
}
|
||||
}
|
||||
|
||||
func (flagHandler *FlagHandler) Help() {
|
||||
fmt.Println("Run: kube-escape scan framework nsa")
|
||||
}
|
||||
|
||||
func (flagHandler *FlagHandler) Version() {
|
||||
fmt.Println("bla.bla.bla")
|
||||
}
|
||||
|
||||
func (flagHandler *FlagHandler) Scan() {
|
||||
f := "help"
|
||||
if len(flag.Args()) >= 2 {
|
||||
f = strings.ToLower(flag.Arg(1))
|
||||
}
|
||||
switch f {
|
||||
case "framework":
|
||||
flagHandler.ScanFramework()
|
||||
case "control":
|
||||
flagHandler.ScanControl()
|
||||
case "help":
|
||||
flagHandler.ScanHelp()
|
||||
default:
|
||||
fmt.Println("unknown input argument")
|
||||
flagHandler.ScanHelp()
|
||||
}
|
||||
}
|
||||
func (flagHandler *FlagHandler) ScanFramework() {
|
||||
frameworkName := strings.ToUpper(flag.Arg(2))
|
||||
// if cautils.StringInSlice(SupportedFrameworks(), frameworkName) == cautils.ValueNotFound {
|
||||
// fmt.Printf("framework %s not supported, supported frameworks: %v", frameworkName, SupportedFrameworks())
|
||||
// return
|
||||
// }
|
||||
flagHandler.policyIdentifier = &opapolicy.PolicyIdentifier{
|
||||
Kind: opapolicy.KindFramework,
|
||||
Name: frameworkName,
|
||||
}
|
||||
}
|
||||
func (flagHandler *FlagHandler) ScanControl() {
|
||||
flagHandler.policyIdentifier = &opapolicy.PolicyIdentifier{
|
||||
Kind: opapolicy.KindControl,
|
||||
Name: strings.ToUpper(flag.Arg(3)),
|
||||
}
|
||||
}
|
||||
func (flagHandler *FlagHandler) ScanHelp() {
|
||||
fmt.Println("")
|
||||
}
|
||||
func (flagHandler *FlagHandler) ScanFrameworkHelp() {
|
||||
fmt.Println("Run framework nsa or mitre")
|
||||
}
|
||||
func (flagHandler *FlagHandler) ScanControlHelp() {
|
||||
fmt.Println("not supported")
|
||||
}
|
||||
|
||||
func SupportedFrameworks() []string {
|
||||
return []string{"nsa", "mitre"} // TODO - get from BE
|
||||
}
|
||||
32
install.sh
32
install.sh
@@ -6,23 +6,39 @@ echo
|
||||
|
||||
BASE_DIR=~/.kubescape
|
||||
KUBESCAPE_EXEC=kubescape
|
||||
RELEASE=v0.0.12
|
||||
DOWNLOAD_URL="https://github.com/armosec/kubescape/releases/download/$RELEASE/kubescape"
|
||||
|
||||
osName=$(uname -s)
|
||||
if [[ $osName == *"MINGW"* ]]; then
|
||||
osName=windows-latest
|
||||
elif [[ $osName == *"Darwin"* ]]; then
|
||||
osName=macos-latest
|
||||
else
|
||||
osName=ubuntu-latest
|
||||
fi
|
||||
|
||||
GITHUB_OWNER=armosec
|
||||
|
||||
DOWNLOAD_URL=$(curl --silent "https://api.github.com/repos/$GITHUB_OWNER/kubescape/releases/latest" | grep -o "browser_download_url.*${osName}.*")
|
||||
DOWNLOAD_URL=${DOWNLOAD_URL//\"}
|
||||
DOWNLOAD_URL=${DOWNLOAD_URL/browser_download_url: /}
|
||||
|
||||
mkdir -p $BASE_DIR
|
||||
|
||||
OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
|
||||
|
||||
curl -sL $DOWNLOAD_URL -o $OUTPUT
|
||||
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
|
||||
# Ping download counter
|
||||
curl --silent https://us-central1-elated-pottery-310110.cloudfunctions.net/kubescape-download-counter -o /dev/null
|
||||
|
||||
chmod +x $OUTPUT || sudo chmod +x $OUTPUT
|
||||
rm -f /usr/local/bin/$KUBESCAPE_EXEC || sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
|
||||
cp $OUTPUT /usr/local/bin || sudo cp $OUTPUT /usr/local/bin
|
||||
rm -rf $OUTPUT
|
||||
|
||||
echo -e "[V] Finished Installation"
|
||||
echo
|
||||
|
||||
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa"
|
||||
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
|
||||
echo
|
||||
|
||||
47
main.go
47
main.go
@@ -1,50 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
k8sinterface "kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/inputhandler/clihandler"
|
||||
|
||||
"kube-escape/opaprocessor"
|
||||
"kube-escape/policyhandler"
|
||||
"kube-escape/printer"
|
||||
|
||||
"os"
|
||||
)
|
||||
import "github.com/armosec/kubescape/cmd"
|
||||
|
||||
func main() {
|
||||
|
||||
if err := CliSetup(); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func CliSetup() error {
|
||||
k8s := k8sinterface.NewKubernetesApi()
|
||||
processNotification := make(chan *cautils.OPASessionObj)
|
||||
reportResults := make(chan *cautils.OPASessionObj)
|
||||
|
||||
// policy handler setup
|
||||
cautils.SetupDefaultEnvs()
|
||||
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
|
||||
|
||||
// cli handler setup
|
||||
cli := clihandler.NewCLIHandler(policyHandler)
|
||||
if err := cli.Scan(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// processor setup - rego run
|
||||
go func() {
|
||||
reporterObj := opaprocessor.NewOPAProcessor(&processNotification, &reportResults)
|
||||
reporterObj.ProcessRulesListenner()
|
||||
}()
|
||||
|
||||
p := printer.NewPrinter(&reportResults)
|
||||
p.ActionPrint()
|
||||
|
||||
return nil
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
@@ -3,13 +3,16 @@ package opaprocessor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"time"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/scapepkg/exceptions"
|
||||
"github.com/armosec/kubescape/scapepkg/score"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"kube-escape/cautils/opapolicy/resources"
|
||||
"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"
|
||||
@@ -17,94 +20,144 @@ import (
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
)
|
||||
|
||||
type OPAProcessor struct {
|
||||
processedPolicy *chan *cautils.OPASessionObj
|
||||
reportResults *chan *cautils.OPASessionObj
|
||||
regoK8sCredentials storage.Store
|
||||
const ScoreConfigPath = "/resources/config"
|
||||
|
||||
var RegoK8sCredentials storage.Store
|
||||
|
||||
type OPAProcessorHandler struct {
|
||||
processedPolicy *chan *cautils.OPASessionObj
|
||||
reportResults *chan *cautils.OPASessionObj
|
||||
// componentConfig cautils.ComponentConfig
|
||||
}
|
||||
|
||||
func NewOPAProcessor(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessor {
|
||||
type OPAProcessor struct {
|
||||
*cautils.OPASessionObj
|
||||
}
|
||||
|
||||
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig())
|
||||
func NewOPAProcessor(sessionObj *cautils.OPASessionObj) *OPAProcessor {
|
||||
return &OPAProcessor{
|
||||
OPASessionObj: sessionObj,
|
||||
}
|
||||
}
|
||||
|
||||
func NewOPAProcessorHandler(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessorHandler {
|
||||
|
||||
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.K8SConfig)
|
||||
store, err := regoDependenciesData.TOStorage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &OPAProcessor{
|
||||
processedPolicy: processedPolicy,
|
||||
reportResults: reportResults,
|
||||
regoK8sCredentials: store,
|
||||
RegoK8sCredentials = store
|
||||
|
||||
return &OPAProcessorHandler{
|
||||
processedPolicy: processedPolicy,
|
||||
reportResults: reportResults,
|
||||
}
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) ProcessRulesListenner() {
|
||||
func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
|
||||
|
||||
for {
|
||||
// recover
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
glog.Errorf("RECOVER in ProcessRulesListenner, reason: %v", err)
|
||||
}
|
||||
}()
|
||||
opaSessionObj := <-*opap.processedPolicy
|
||||
go func() {
|
||||
if err := opap.ProcessRulesHandler(opaSessionObj); err != nil {
|
||||
// opaSessionObj.Reporter.SendError(nil, true, true)
|
||||
}
|
||||
*opap.reportResults <- opaSessionObj
|
||||
}()
|
||||
opaSessionObj := <-*opaHandler.processedPolicy
|
||||
opap := NewOPAProcessor(opaSessionObj)
|
||||
|
||||
// process
|
||||
if err := opap.Process(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// edit results
|
||||
opap.updateResults()
|
||||
|
||||
// update score
|
||||
// opap.updateScore()
|
||||
|
||||
// report
|
||||
*opaHandler.reportResults <- opaSessionObj
|
||||
}
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) ProcessRulesHandler(opaSessionObj *cautils.OPASessionObj) error {
|
||||
glog.Infof(fmt.Sprintf("Starting 'ProcessRulesHandler'. reportID: %s", opaSessionObj.PostureReport.ReportID))
|
||||
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.NumOfResources = len(k8sObjects)
|
||||
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()
|
||||
glog.Infof(fmt.Sprintf("Done 'ProcessRulesHandler'. reportID: %s", opaSessionObj.PostureReport.ReportID))
|
||||
opap.PostureReport.FrameworkReports = frameworkReports
|
||||
opap.PostureReport.ReportGenerationTime = time.Now().UTC()
|
||||
// glog.Infof(fmt.Sprintf("Done 'Process'. reportID: %s", opap.PostureReport.ReportID))
|
||||
cautils.StopSpinner()
|
||||
cautils.SuccessTextDisplay(fmt.Sprintf("Done scanning cluster %s", cautils.ClusterName))
|
||||
return errs
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) processFramework(framework *opapolicy.Framework) (*opapolicy.FrameworkReport, error) {
|
||||
var errs error
|
||||
|
||||
frameworkReport := opapolicy.FrameworkReport{}
|
||||
frameworkReport.Name = framework.Name
|
||||
controlReports := []opapolicy.ControlReport{}
|
||||
for i := range framework.Controls {
|
||||
controlReport, err := opap.processControl(&framework.Controls[i])
|
||||
if err != nil {
|
||||
errs = fmt.Errorf("%v\n%s", errs, err.Error())
|
||||
}
|
||||
controlReports = append(controlReports, *controlReport)
|
||||
}
|
||||
frameworkReport.ControlReports = controlReports
|
||||
return &frameworkReport, errs
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) processControl(control *opapolicy.Control) (*opapolicy.ControlReport, error) {
|
||||
var errs error
|
||||
|
||||
controlReport := opapolicy.ControlReport{}
|
||||
controlReport.PortalBase = control.PortalBase
|
||||
controlReport.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 +200,42 @@ func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRe
|
||||
rego.Query("data.armo_builtins"), // get package name from rule
|
||||
rego.Compiler(compiledRego),
|
||||
rego.Input(inputObj),
|
||||
rego.Store(opap.regoK8sCredentials),
|
||||
rego.Store(RegoK8sCredentials),
|
||||
)
|
||||
|
||||
// Run evaluation
|
||||
resultSet, err := rego.Eval(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("In 'regoEval', failed to evaluate rule, reason: %s", err.Error())
|
||||
return nil, fmt.Errorf("in 'regoEval', failed to evaluate rule, reason: %s", err.Error())
|
||||
}
|
||||
results, err := opapolicy.ParseRegoResult(&resultSet)
|
||||
results, err := parseRegoResult(&resultSet)
|
||||
|
||||
// results, err := ParseRegoResult(&resultSet)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) updateScore() {
|
||||
|
||||
// calculate score
|
||||
s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath)
|
||||
s.Calculate(opap.PostureReport.FrameworkReports)
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) updateResults() {
|
||||
for f, frameworkReport := range opap.PostureReport.FrameworkReports {
|
||||
for c, controlReport := range opap.PostureReport.FrameworkReports[f].ControlReports {
|
||||
for r, ruleReport := range opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports {
|
||||
// editing the responses -> removing duplications, clearing secret data, etc.
|
||||
opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports[r].RuleResponses = editRuleResponses(ruleReport.RuleResponses)
|
||||
|
||||
// adding exceptions to the rules
|
||||
ruleExceptions := exceptions.ListRuleExceptions(opap.Exceptions, frameworkReport.Name, controlReport.Name, ruleReport.Name)
|
||||
exceptions.AddExceptionsToRuleResponses(opap.PostureReport.FrameworkReports[f].ControlReports[c].RuleReports[r].RuleResponses, ruleExceptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
package opaprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"kube-escape/cautils"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"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"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"kube-escape/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"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -46,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 := "kube-escape"
|
||||
idx := strings.Index(wd, baseDirName)
|
||||
wd = wd[0:idx]
|
||||
resources.RegoDependenciesPath = path.Join(wd, "/kube-escape/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/kube-escape/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 (
|
||||
"kube-escape/cautils"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
pkgcautils "kube-escape/cautils/cautils"
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
resources "kube-escape/cautils/opapolicy/resources"
|
||||
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"
|
||||
@@ -22,7 +27,7 @@ func getKubernetesObjects(k8sResources *cautils.K8SResources, match []opapolicy.
|
||||
for _, groupResource := range groupResources {
|
||||
if k8sObj, ok := (*k8sResources)[groupResource]; ok {
|
||||
if k8sObj == nil {
|
||||
glog.Errorf("Resource '%s' is nil, probably failed to pull the resource", groupResource)
|
||||
// glog.Errorf("Resource '%s' is nil, probably failed to pull the resource", groupResource)
|
||||
} else if v, k := k8sObj.([]map[string]interface{}); k {
|
||||
k8sObjects = append(k8sObjects, v...)
|
||||
} else if v, k := k8sObj.(map[string]interface{}); k {
|
||||
@@ -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 {
|
||||
@@ -59,3 +131,11 @@ func ruleWithArmoOpaDependency(annotations map[string]interface{}) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func listMatchKinds(match []opapolicy.RuleMatchObjects) []string {
|
||||
matchKinds := []string{}
|
||||
for i := range match {
|
||||
matchKinds = append(matchKinds, match[i].Resources...)
|
||||
}
|
||||
return matchKinds
|
||||
}
|
||||
|
||||
Binary file not shown.
270
policyhandler/filesloader.go
Normal file
270
policyhandler/filesloader.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
YAML_PREFIX = []string{".yaml", ".yml"}
|
||||
JSON_PREFIX = []string{".json"}
|
||||
)
|
||||
|
||||
type FileFormat string
|
||||
|
||||
const (
|
||||
YAML_FILE_FORMAT FileFormat = "yaml"
|
||||
JSON_FILE_FORMAT FileFormat = "json"
|
||||
)
|
||||
|
||||
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
|
||||
workloads := []k8sinterface.IWorkload{}
|
||||
|
||||
// load resource from local file system
|
||||
w, err := loadResourcesFromFiles(scanInfo.InputPatterns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if w != nil {
|
||||
workloads = append(workloads, w...)
|
||||
}
|
||||
|
||||
// load resources from url
|
||||
w, err = loadResourcesFromUrl(scanInfo.InputPatterns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if w != nil {
|
||||
workloads = append(workloads, w...)
|
||||
}
|
||||
|
||||
if len(workloads) == 0 {
|
||||
return nil, fmt.Errorf("empty list of workloads - no workloads found")
|
||||
}
|
||||
|
||||
// map all resources: map["/group/version/kind"][]<k8s workloads>
|
||||
allResources := mapResources(workloads)
|
||||
|
||||
// build resources map
|
||||
// map resources based on framework required resources: map["/group/version/kind"][]<k8s workloads>
|
||||
k8sResources := setResourceMap(frameworks)
|
||||
|
||||
// save only relevant resources
|
||||
for i := range allResources {
|
||||
if _, ok := (*k8sResources)[i]; ok {
|
||||
(*k8sResources)[i] = allResources[i]
|
||||
}
|
||||
}
|
||||
|
||||
return k8sResources, nil
|
||||
|
||||
}
|
||||
|
||||
func loadResourcesFromFiles(inputPatterns []string) ([]k8sinterface.IWorkload, error) {
|
||||
files, errs := listFiles(inputPatterns)
|
||||
if len(errs) > 0 {
|
||||
cautils.ErrorDisplay(fmt.Sprintf("%v", errs)) // TODO - print error
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
workloads, errs := loadFiles(files)
|
||||
if len(errs) > 0 {
|
||||
cautils.ErrorDisplay(fmt.Sprintf("%v", errs)) // TODO - print error
|
||||
}
|
||||
return workloads, nil
|
||||
}
|
||||
|
||||
// build resources map
|
||||
func mapResources(workloads []k8sinterface.IWorkload) map[string][]map[string]interface{} {
|
||||
allResources := map[string][]map[string]interface{}{}
|
||||
for i := range workloads {
|
||||
groupVersionResource, err := k8sinterface.GetGroupVersionResource(workloads[i].GetKind())
|
||||
if err != nil {
|
||||
// TODO - print warning
|
||||
continue
|
||||
}
|
||||
if groupVersionResource.Group != workloads[i].GetGroup() || groupVersionResource.Version != workloads[i].GetVersion() {
|
||||
// TODO - print warning
|
||||
continue
|
||||
}
|
||||
resourceTriplets := k8sinterface.JoinResourceTriplets(groupVersionResource.Group, groupVersionResource.Version, groupVersionResource.Resource)
|
||||
if r, ok := allResources[resourceTriplets]; ok {
|
||||
r = append(r, workloads[i].GetWorkload())
|
||||
allResources[resourceTriplets] = r
|
||||
} else {
|
||||
allResources[resourceTriplets] = []map[string]interface{}{workloads[i].GetWorkload()}
|
||||
}
|
||||
}
|
||||
return allResources
|
||||
|
||||
}
|
||||
|
||||
func loadFiles(filePaths []string) ([]k8sinterface.IWorkload, []error) {
|
||||
workloads := []k8sinterface.IWorkload{}
|
||||
errs := []error{}
|
||||
for i := range filePaths {
|
||||
f, err := loadFile(filePaths[i])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
w, e := readFile(f, getFileFormat(filePaths[i]))
|
||||
errs = append(errs, e...)
|
||||
if w != nil {
|
||||
workloads = append(workloads, w...)
|
||||
}
|
||||
}
|
||||
return workloads, errs
|
||||
}
|
||||
|
||||
func loadFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
}
|
||||
func readFile(fileContent []byte, fileFromat FileFormat) ([]k8sinterface.IWorkload, []error) {
|
||||
|
||||
switch fileFromat {
|
||||
case YAML_FILE_FORMAT:
|
||||
return readYamlFile(fileContent)
|
||||
case JSON_FILE_FORMAT:
|
||||
return readJsonFile(fileContent)
|
||||
default:
|
||||
return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func listFiles(patterns []string) ([]string, []error) {
|
||||
files := []string{}
|
||||
errs := []error{}
|
||||
for i := range patterns {
|
||||
if strings.HasPrefix(patterns[i], "http") {
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(patterns[i]) {
|
||||
o, _ := os.Getwd()
|
||||
patterns[i] = filepath.Join(o, patterns[i])
|
||||
}
|
||||
f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
files = append(files, f...)
|
||||
}
|
||||
}
|
||||
return files, errs
|
||||
}
|
||||
|
||||
func readYamlFile(yamlFile []byte) ([]k8sinterface.IWorkload, []error) {
|
||||
errs := []error{}
|
||||
|
||||
r := bytes.NewReader(yamlFile)
|
||||
dec := yaml.NewDecoder(r)
|
||||
yamlObjs := []k8sinterface.IWorkload{}
|
||||
|
||||
var t interface{}
|
||||
for dec.Decode(&t) == nil {
|
||||
j := convertYamlToJson(t)
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
if obj, ok := j.(map[string]interface{}); ok {
|
||||
yamlObjs = append(yamlObjs, k8sinterface.NewWorkloadObj(obj))
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j))
|
||||
}
|
||||
}
|
||||
|
||||
return yamlObjs, errs
|
||||
}
|
||||
|
||||
func readJsonFile(jsonFile []byte) ([]k8sinterface.IWorkload, []error) {
|
||||
workloads := []k8sinterface.IWorkload{}
|
||||
var jsonObj interface{}
|
||||
if err := json.Unmarshal(jsonFile, &jsonObj); err != nil {
|
||||
return workloads, []error{err}
|
||||
}
|
||||
|
||||
convertJsonToWorkload(jsonObj, &workloads)
|
||||
|
||||
return workloads, nil
|
||||
}
|
||||
func convertJsonToWorkload(jsonObj interface{}, workloads *[]k8sinterface.IWorkload) {
|
||||
|
||||
switch x := jsonObj.(type) {
|
||||
case map[string]interface{}:
|
||||
(*workloads) = append(*workloads, k8sinterface.NewWorkloadObj(x))
|
||||
case []interface{}:
|
||||
for i := range x {
|
||||
convertJsonToWorkload(x[i], workloads)
|
||||
}
|
||||
}
|
||||
}
|
||||
func convertYamlToJson(i interface{}) interface{} {
|
||||
switch x := i.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
m2 := map[string]interface{}{}
|
||||
for k, v := range x {
|
||||
if s, ok := k.(string); ok {
|
||||
m2[s] = convertYamlToJson(v)
|
||||
}
|
||||
}
|
||||
return m2
|
||||
case []interface{}:
|
||||
for i, v := range x {
|
||||
x[i] = convertYamlToJson(v)
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func glob(root, pattern string) ([]string, error) {
|
||||
var matches []string
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {
|
||||
return err
|
||||
} else if matched {
|
||||
matches = append(matches, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
func isYaml(filePath string) bool {
|
||||
return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
|
||||
}
|
||||
|
||||
func isJson(filePath string) bool {
|
||||
return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
|
||||
}
|
||||
|
||||
func getFileFormat(filePath string) FileFormat {
|
||||
if isYaml(filePath) {
|
||||
return YAML_FILE_FORMAT
|
||||
} else if isJson(filePath) {
|
||||
return JSON_FILE_FORMAT
|
||||
} else {
|
||||
return FileFormat(filePath)
|
||||
}
|
||||
}
|
||||
64
policyhandler/filesloader_test.go
Normal file
64
policyhandler/filesloader_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
)
|
||||
|
||||
func combine(base, rel string) string {
|
||||
finalPath := []string{}
|
||||
sBase := strings.Split(base, "/")
|
||||
sRel := strings.Split(rel, "/")
|
||||
for i := range sBase {
|
||||
if cautils.StringInSlice(sRel, sBase[i]) != cautils.ValueNotFound {
|
||||
finalPath = append(finalPath, sRel...)
|
||||
break
|
||||
}
|
||||
finalPath = append(finalPath, sBase[i])
|
||||
}
|
||||
return fmt.Sprintf("/%s", filepath.Join(finalPath...))
|
||||
}
|
||||
func onlineBoutiquePath() string {
|
||||
o, _ := os.Getwd()
|
||||
return combine(o, "github.com/armosec/kubescape/examples/online-boutique/*")
|
||||
}
|
||||
func TestListFiles(t *testing.T) {
|
||||
files, errs := listFiles([]string{onlineBoutiquePath()})
|
||||
if len(errs) > 0 {
|
||||
t.Error(errs)
|
||||
}
|
||||
expected := 12
|
||||
if len(files) != expected {
|
||||
t.Errorf("wrong number of files, expected: %d, found: %d", expected, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFiles(t *testing.T) {
|
||||
files, _ := listFiles([]string{onlineBoutiquePath()})
|
||||
loadFiles(files)
|
||||
}
|
||||
|
||||
func TestLoadFile(t *testing.T) {
|
||||
files, _ := listFiles([]string{strings.Replace(onlineBoutiquePath(), "*", "bi-monitor.yaml", 1)})
|
||||
_, err := loadFile(files[0])
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}
|
||||
func TestLoadResources(t *testing.T) {
|
||||
|
||||
// k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
// files, _ := listFiles([]string{onlineBoutiquePath()})
|
||||
// bb, err := loadFile(files[0])
|
||||
// if len(err) > 0 {
|
||||
// t.Errorf("%v", err)
|
||||
// }
|
||||
// for i := range bb {
|
||||
// t.Errorf("%s", bb[i].ToString())
|
||||
// }
|
||||
}
|
||||
@@ -2,13 +2,13 @@ package policyhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// PolicyHandler -
|
||||
@@ -16,6 +16,7 @@ type PolicyHandler struct {
|
||||
k8s *k8sinterface.KubernetesApi
|
||||
// we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func
|
||||
processPolicy *chan *cautils.OPASessionObj
|
||||
getters *cautils.Getters
|
||||
}
|
||||
|
||||
// CreatePolicyHandler Create ws-handler obj
|
||||
@@ -26,37 +27,29 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
|
||||
}
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification) error {
|
||||
glog.Infof("Processing notification. reportID: %s", notification.ReportID)
|
||||
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
|
||||
glog.Infof(fmt.Sprintf("Getting %d policies from backend. reportID: %s", len(notification.Rules), notification.ReportID))
|
||||
cautils.ProgressTextDisplay("Downloading framework definitions")
|
||||
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
|
||||
frameworks, exceptions, err := policyHandler.getPolicies(notification)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(frameworks) == 0 {
|
||||
err := fmt.Errorf("Could not download any policies, please check previous logs")
|
||||
return err
|
||||
return fmt.Errorf("empty list of frameworks")
|
||||
}
|
||||
opaSessionObj.Frameworks = frameworks
|
||||
cautils.SuccessTextDisplay("Downloaded framework")
|
||||
// store policies as configmaps
|
||||
// TODO
|
||||
opaSessionObj.Exceptions = exceptions
|
||||
|
||||
// get k8s resources
|
||||
cautils.ProgressTextDisplay("Accessing Kubernetes objects")
|
||||
glog.Infof(fmt.Sprintf("Getting kubernetes objects. reportID: %s", notification.ReportID))
|
||||
k8sResources, err := policyHandler.getK8sResources(frameworks, ¬ification.Designators)
|
||||
if err != nil || len(*k8sResources) == 0 {
|
||||
glog.Error(err)
|
||||
} else {
|
||||
cautils.SuccessTextDisplay("Accessed successfully to Kubernetes objects, let’s start!!!")
|
||||
k8sResources, err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if k8sResources == nil || len(*k8sResources) == 0 {
|
||||
return fmt.Errorf("empty list of resources")
|
||||
}
|
||||
opaSessionObj.K8SResources = k8sResources
|
||||
|
||||
@@ -64,3 +57,33 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
|
||||
*policyHandler.processPolicy <- opaSessionObj
|
||||
return nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
|
||||
|
||||
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
|
||||
|
||||
frameworks, exceptions, err := policyHandler.GetPoliciesFromBackend(notification)
|
||||
if err != nil {
|
||||
return frameworks, exceptions, err
|
||||
}
|
||||
|
||||
if len(frameworks) == 0 {
|
||||
err := fmt.Errorf("could not download any policies, please check previous logs")
|
||||
return frameworks, exceptions, err
|
||||
}
|
||||
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
|
||||
|
||||
return frameworks, exceptions, nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
|
||||
var k8sResources *cautils.K8SResources
|
||||
var err error
|
||||
if scanInfo.ScanRunningCluster() {
|
||||
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces)
|
||||
} else {
|
||||
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
}
|
||||
|
||||
return k8sResources, err
|
||||
}
|
||||
|
||||
@@ -1,162 +1,50 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"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.eudev3.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", 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("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
|
||||
}
|
||||
frameworks = append(frameworks, receivedFrameworks...)
|
||||
case opapolicy.KindControl:
|
||||
receivedControls := []opapolicy.Control{} //, err := policyHandler.cacli.OPAFRAMEWORKGet(rule.Name, !k8sinterface.RunningIncluster)
|
||||
|
||||
// receivedControls, err := policyHandler.cacli.OPACONTROLGet(rule.Name)
|
||||
// if err != nil {
|
||||
// errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
|
||||
// }
|
||||
framework := opapolicy.Framework{ // TODO - wrap control by framework properly
|
||||
Controls: receivedControls,
|
||||
if receivedFramework != nil {
|
||||
frameworks = append(frameworks, *receivedFramework)
|
||||
if recExceptionPolicies != nil {
|
||||
exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
|
||||
}
|
||||
}
|
||||
frameworks = append(frameworks, framework)
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("missing rule kind, expected: %s", opapolicy.KindFramework)
|
||||
errs = fmt.Errorf("%v\nerror: %s", errs, err.Error())
|
||||
|
||||
errs = fmt.Errorf("%s", err.Error())
|
||||
}
|
||||
}
|
||||
return frameworks, errs
|
||||
return frameworks, exceptionPolicies, errs
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
|
||||
receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(policyName)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "")
|
||||
if err != nil {
|
||||
return receivedFramework, nil, err
|
||||
}
|
||||
|
||||
return receivedFramework, receivedException, nil
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package policyhandler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"kube-escape/cautils/armotypes"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -18,7 +20,10 @@ import (
|
||||
|
||||
const SelectAllResources = "*"
|
||||
|
||||
func (policyHandler *PolicyHandler) getK8sResources(frameworks []opapolicy.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) {
|
||||
func (policyHandler *PolicyHandler) getK8sResources(frameworks []opapolicy.Framework, designator *armotypes.PortalDesignator, excludedNamespaces string) (*cautils.K8SResources, error) {
|
||||
// get k8s resources
|
||||
cautils.ProgressTextDisplay("Accessing Kubernetes objects")
|
||||
|
||||
// build resources map
|
||||
k8sResourcesMap := setResourceMap(frameworks)
|
||||
|
||||
@@ -26,20 +31,21 @@ func (policyHandler *PolicyHandler) getK8sResources(frameworks []opapolicy.Frame
|
||||
_, namespace, labels := armotypes.DigestPortalDesignator(designator)
|
||||
|
||||
// pull k8s recourses
|
||||
if err := policyHandler.pullResources(k8sResourcesMap, namespace, labels); err != nil {
|
||||
if err := policyHandler.pullResources(k8sResourcesMap, namespace, labels, excludedNamespaces); err != nil {
|
||||
return k8sResourcesMap, err
|
||||
}
|
||||
|
||||
cautils.SuccessTextDisplay("Accessed successfully to Kubernetes objects, let’s start!!!")
|
||||
return k8sResourcesMap, nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string) error {
|
||||
func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string, excludedNamespaces string) error {
|
||||
|
||||
var errs error
|
||||
for groupResource := range *k8sResources {
|
||||
apiGroup, apiVersion, resource := k8sinterface.StringToResourceGroup(groupResource)
|
||||
gvr := schema.GroupVersionResource{Group: apiGroup, Version: apiVersion, Resource: resource}
|
||||
result, err := policyHandler.pullSingleResource(&gvr, namespace, labels)
|
||||
result, err := policyHandler.pullSingleResource(&gvr, namespace, labels, excludedNamespaces)
|
||||
if err != nil {
|
||||
// handle error
|
||||
if errs == nil {
|
||||
@@ -55,18 +61,23 @@ func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResou
|
||||
return errs
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string) ([]unstructured.Unstructured, error) {
|
||||
func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string, excludedNamespaces string) ([]unstructured.Unstructured, error) {
|
||||
|
||||
// set labels
|
||||
listOptions := metav1.ListOptions{}
|
||||
if labels != nil && len(labels) > 0 {
|
||||
if excludedNamespaces != "" && k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) {
|
||||
excludedNamespacesSlice := strings.Split(excludedNamespaces, ",")
|
||||
for _, excludedNamespace := range excludedNamespacesSlice {
|
||||
listOptions.FieldSelector += "metadata.namespace!=" + excludedNamespace + ","
|
||||
}
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
set := k8slabels.Set(labels)
|
||||
listOptions.LabelSelector = set.AsSelector().String()
|
||||
}
|
||||
|
||||
// set dynamic object
|
||||
var clientResource dynamic.ResourceInterface
|
||||
|
||||
if namespace != "" && k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) {
|
||||
clientResource = policyHandler.k8s.DynamicClient.Resource(*resource).Namespace(namespace)
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"kube-escape/cautils"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func setResourceMap(frameworks []opapolicy.Framework) *cautils.K8SResources {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
71
policyhandler/urlloader.go
Normal file
71
policyhandler/urlloader.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package policyhandler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
)
|
||||
|
||||
func loadResourcesFromUrl(inputPatterns []string) ([]k8sinterface.IWorkload, error) {
|
||||
urls := listUrls(inputPatterns)
|
||||
if len(urls) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
workloads, errs := downloadFiles(urls)
|
||||
if len(errs) > 0 {
|
||||
cautils.ErrorDisplay(fmt.Sprintf("%v", errs)) // TODO - print error
|
||||
}
|
||||
return workloads, nil
|
||||
}
|
||||
|
||||
func listUrls(patterns []string) []string {
|
||||
urls := []string{}
|
||||
for i := range patterns {
|
||||
if strings.HasPrefix(patterns[i], "http") {
|
||||
urls = append(urls, patterns[i])
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func downloadFiles(urls []string) ([]k8sinterface.IWorkload, []error) {
|
||||
workloads := []k8sinterface.IWorkload{}
|
||||
errs := []error{}
|
||||
for i := range urls {
|
||||
f, err := downloadFile(urls[i])
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
w, e := readFile(f, getFileFormat(urls[i]))
|
||||
errs = append(errs, e...)
|
||||
if w != nil {
|
||||
workloads = append(workloads, w...)
|
||||
}
|
||||
}
|
||||
return workloads, errs
|
||||
}
|
||||
|
||||
func downloadFile(url string) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || 301 < resp.StatusCode {
|
||||
return nil, fmt.Errorf("failed to download file, url: '%s', status code: %s", url, resp.Status)
|
||||
}
|
||||
return streamToByte(resp.Body), nil
|
||||
}
|
||||
|
||||
func streamToByte(stream io.Reader) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(stream)
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
|
||||
"github.com/enescakir/emoji"
|
||||
"github.com/golang/glog"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
var INDENT = " "
|
||||
|
||||
type Printer struct {
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
summery Summery
|
||||
}
|
||||
|
||||
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj) *Printer {
|
||||
return &Printer{
|
||||
opaSessionObj: opaSessionObj,
|
||||
summery: NewSummery(),
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) ActionPrint() {
|
||||
|
||||
// recover
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
glog.Errorf("RECOVER in ActionSendReportListenner, reason: %v", err)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
opaSessionObj := <-*printer.opaSessionObj
|
||||
|
||||
printer.SummerySetup(opaSessionObj.PostureReport)
|
||||
printer.PrintResults()
|
||||
printer.PrintSummaryTable()
|
||||
|
||||
if !k8sinterface.RunningIncluster {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) SummerySetup(postureReport *opapolicy.PostureReport) {
|
||||
for _, fr := range postureReport.FrameworkReports {
|
||||
for _, cr := range fr.ControlReports {
|
||||
if len(cr.RuleReports) == 0 {
|
||||
continue
|
||||
}
|
||||
workloadsSummery := listResultSummery(cr.RuleReports)
|
||||
mapResources := groupByNamespace(workloadsSummery)
|
||||
|
||||
printer.summery[cr.Name] = ControlSummery{
|
||||
TotalResources: cr.GetNumberOfResources(),
|
||||
TotalFailed: len(workloadsSummery),
|
||||
WorkloadSummery: mapResources,
|
||||
Description: strings.ReplaceAll(cr.Description, ". ", fmt.Sprintf(".\n%s%s", INDENT, INDENT)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintResults() {
|
||||
for control, controlSummery := range printer.summery {
|
||||
printer.printTitle(control, &controlSummery)
|
||||
printer.printResult(control, &controlSummery)
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) printTitle(controlName string, controlSummery *ControlSummery) {
|
||||
cautils.InfoDisplay(os.Stdout, "[control: %s] ", controlName)
|
||||
if controlSummery.TotalResources == 0 {
|
||||
cautils.InfoDisplay(os.Stdout, "resources not found %v\n", emoji.ConfusedFace)
|
||||
} else if controlSummery.TotalFailed == 0 {
|
||||
cautils.SuccessDisplay(os.Stdout, "passed %v\n", emoji.ThumbsUp)
|
||||
} else {
|
||||
cautils.FailureDisplay(os.Stdout, "failed %v\n", emoji.SadButRelievedFace)
|
||||
}
|
||||
|
||||
cautils.SimpleDisplay(os.Stdout, "%sDescription: %s\n", INDENT, controlSummery.Description)
|
||||
|
||||
}
|
||||
func (printer *Printer) printResult(controlName string, controlSummery *ControlSummery) {
|
||||
|
||||
indent := INDENT
|
||||
for ns, rsc := range controlSummery.WorkloadSummery {
|
||||
preIndent := indent
|
||||
indent += indent
|
||||
cautils.SimpleDisplay(os.Stdout, "%sNamespace %s\n", indent, ns)
|
||||
preIndent2 := indent
|
||||
for r := range rsc {
|
||||
indent += indent
|
||||
cautils.SimpleDisplay(os.Stdout, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
|
||||
indent = preIndent2
|
||||
}
|
||||
indent = preIndent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateRow(control string, cs ControlSummery) []string {
|
||||
row := []string{control}
|
||||
row = append(row, cs.ToSlice()...)
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
|
||||
return row
|
||||
}
|
||||
|
||||
func generateHeader() []string {
|
||||
return []string{"Control Name", "Failed Resources", "All Resources", "% success"}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
func generateFooter(numControlers, sumFailed, 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", sumTotal))
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
|
||||
return row
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintSummaryTable() {
|
||||
summaryTable := tablewriter.NewWriter(os.Stdout)
|
||||
summaryTable.SetAutoWrapText(false)
|
||||
summaryTable.SetHeader(generateHeader())
|
||||
summaryTable.SetHeaderLine(true)
|
||||
summaryTable.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||
sumTotal := 0
|
||||
sumFailed := 0
|
||||
|
||||
for k, v := range printer.summery {
|
||||
summaryTable.Append(generateRow(k, v))
|
||||
sumFailed += v.TotalFailed
|
||||
sumTotal += v.TotalResources
|
||||
}
|
||||
summaryTable.SetFooter(generateFooter(len(printer.summery), sumFailed, sumTotal))
|
||||
summaryTable.Render()
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Summery map[string]ControlSummery
|
||||
|
||||
func NewSummery() Summery {
|
||||
return make(map[string]ControlSummery)
|
||||
}
|
||||
|
||||
type ControlSummery struct {
|
||||
TotalResources int
|
||||
TotalFailed int
|
||||
Description string
|
||||
WorkloadSummery map[string][]WorkloadSummery
|
||||
}
|
||||
|
||||
type WorkloadSummery struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string
|
||||
Group string
|
||||
}
|
||||
|
||||
func (controlSummery *ControlSummery) ToSlice() []string {
|
||||
s := []string{}
|
||||
s = append(s, fmt.Sprintf("%d", controlSummery.TotalFailed))
|
||||
s = append(s, fmt.Sprintf("%d", controlSummery.TotalResources))
|
||||
return s
|
||||
}
|
||||
|
||||
func (workloadSummery *WorkloadSummery) ToString() string {
|
||||
return fmt.Sprintf("/%s/%s/%s/%s", workloadSummery.Group, workloadSummery.Namespace, workloadSummery.Kind, workloadSummery.Name)
|
||||
}
|
||||
79
resultshandling/printer/junit.go
Normal file
79
resultshandling/printer/junit.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type JUnitTestSuites struct {
|
||||
XMLName xml.Name `xml:"testsuites"`
|
||||
Suites []JUnitTestSuite `xml:"testsuite"`
|
||||
}
|
||||
|
||||
// JUnitTestSuite is a single JUnit test suite which may contain many
|
||||
// testcases.
|
||||
type JUnitTestSuite struct {
|
||||
XMLName xml.Name `xml:"testsuite"`
|
||||
Tests int `xml:"tests,attr"`
|
||||
Failures int `xml:"failures,attr"`
|
||||
Time string `xml:"time,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Properties []JUnitProperty `xml:"properties>property,omitempty"`
|
||||
TestCases []JUnitTestCase `xml:"testcase"`
|
||||
}
|
||||
|
||||
// JUnitTestCase is a single test case with its result.
|
||||
type JUnitTestCase struct {
|
||||
XMLName xml.Name `xml:"testcase"`
|
||||
Classname string `xml:"classname,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Time string `xml:"time,attr"`
|
||||
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
|
||||
Failure *JUnitFailure `xml:"failure,omitempty"`
|
||||
}
|
||||
|
||||
// JUnitSkipMessage contains the reason why a testcase was skipped.
|
||||
type JUnitSkipMessage struct {
|
||||
Message string `xml:"message,attr"`
|
||||
}
|
||||
|
||||
// JUnitProperty represents a key/value pair used to define properties.
|
||||
type JUnitProperty struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
}
|
||||
|
||||
// JUnitFailure contains data related to a failed test.
|
||||
type JUnitFailure struct {
|
||||
Message string `xml:"message,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
Contents string `xml:",chardata"`
|
||||
}
|
||||
|
||||
func convertPostureReportToJunitResult(postureResult *opapolicy.PostureReport) (*JUnitTestSuites, error) {
|
||||
juResult := JUnitTestSuites{XMLName: xml.Name{Local: "Kubescape scan results"}}
|
||||
for _, framework := range postureResult.FrameworkReports {
|
||||
suite := JUnitTestSuite{Name: framework.Name}
|
||||
for _, controlReports := range framework.ControlReports {
|
||||
suite.Tests = suite.Tests + 1
|
||||
testCase := JUnitTestCase{}
|
||||
testCase.Name = controlReports.Name
|
||||
testCase.Classname = "Kubescape"
|
||||
testCase.Time = "0"
|
||||
if 0 < len(controlReports.RuleReports[0].RuleResponses) {
|
||||
suite.Failures = suite.Failures + 1
|
||||
failure := JUnitFailure{}
|
||||
failure.Message = fmt.Sprintf("%d resources failed", len(controlReports.RuleReports[0].RuleResponses))
|
||||
for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses {
|
||||
failure.Contents = fmt.Sprintf("%s\n%s", failure.Contents, ruleResponses.AlertMessage)
|
||||
}
|
||||
testCase.Failure = &failure
|
||||
}
|
||||
suite.TestCases = append(suite.TestCases, testCase)
|
||||
}
|
||||
juResult.Suites = append(juResult.Suites, suite)
|
||||
}
|
||||
return &juResult, nil
|
||||
}
|
||||
261
resultshandling/printer/printresults.go
Normal file
261
resultshandling/printer/printresults.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
"github.com/enescakir/emoji"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
var INDENT = " "
|
||||
|
||||
const EmptyPercentage = "NaN"
|
||||
|
||||
const (
|
||||
PrettyPrinter string = "pretty-printer"
|
||||
JsonPrinter string = "json"
|
||||
JunitResultPrinter string = "junit"
|
||||
)
|
||||
|
||||
type Printer struct {
|
||||
writer *os.File
|
||||
summary Summary
|
||||
sortedControlNames []string
|
||||
printerType string
|
||||
}
|
||||
|
||||
func NewPrinter(printerType, outputFile string) *Printer {
|
||||
return &Printer{
|
||||
summary: NewSummary(),
|
||||
writer: getWriter(outputFile),
|
||||
printerType: printerType,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
for _, fr := range postureReport.FrameworkReports {
|
||||
for _, cr := range fr.ControlReports {
|
||||
if len(cr.RuleReports) == 0 {
|
||||
continue
|
||||
}
|
||||
workloadsSummary := listResultSummary(cr.RuleReports)
|
||||
mapResources := groupByNamespace(workloadsSummary)
|
||||
|
||||
printer.summary[cr.Name] = ControlSummary{
|
||||
TotalResources: cr.GetNumberOfResources(),
|
||||
TotalFailed: cr.GetNumberOfFailedResources(),
|
||||
TotalWarnign: cr.GetNumberOfWarningResources(),
|
||||
WorkloadSummary: mapResources,
|
||||
Description: cr.Description,
|
||||
Remediation: cr.Remediation,
|
||||
ListInputKinds: cr.ListControlsInputKinds(),
|
||||
}
|
||||
}
|
||||
}
|
||||
printer.sortedControlNames = printer.getSortedControlsNames()
|
||||
}
|
||||
func (printer *Printer) PrintResults() {
|
||||
for i := 0; i < len(printer.sortedControlNames); i++ {
|
||||
controlSummary := printer.summary[printer.sortedControlNames[i]]
|
||||
printer.printTitle(printer.sortedControlNames[i], &controlSummary)
|
||||
printer.printResult(printer.sortedControlNames[i], &controlSummary)
|
||||
|
||||
if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 {
|
||||
printer.printSummary(printer.sortedControlNames[i], &controlSummary)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation)
|
||||
}
|
||||
cautils.DescriptionDisplay(printer.writer, "\n")
|
||||
|
||||
}
|
||||
|
||||
func (printer *Printer) printTitle(controlName string, controlSummary *ControlSummary) {
|
||||
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.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)
|
||||
|
||||
}
|
||||
func (printer *Printer) printResult(controlName string, controlSummary *ControlSummary) {
|
||||
|
||||
indent := INDENT
|
||||
for ns, rsc := range controlSummary.WorkloadSummary {
|
||||
preIndent := indent
|
||||
if ns != "" {
|
||||
cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns)
|
||||
}
|
||||
preIndent2 := indent
|
||||
for r := range rsc {
|
||||
indent += indent
|
||||
cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
|
||||
indent = preIndent2
|
||||
}
|
||||
indent = preIndent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateRow(control string, cs ControlSummary) []string {
|
||||
row := []string{control}
|
||||
row = append(row, cs.ToSlice()...)
|
||||
if cs.TotalResources != 0 {
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
|
||||
} else {
|
||||
row = append(row, EmptyPercentage)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func generateHeader() []string {
|
||||
return []string{"Control Name", "Failed Resources", "Warning Resources", "All Resources", "% success"}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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), "%"))
|
||||
} else {
|
||||
row = append(row, EmptyPercentage)
|
||||
}
|
||||
return row
|
||||
}
|
||||
func (printer *Printer) PrintSummaryTable() {
|
||||
summaryTable := tablewriter.NewWriter(printer.writer)
|
||||
summaryTable.SetAutoWrapText(false)
|
||||
summaryTable.SetHeader(generateHeader())
|
||||
summaryTable.SetHeaderLine(true)
|
||||
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, sumWarning, sumTotal))
|
||||
summaryTable.Render()
|
||||
}
|
||||
|
||||
func (printer *Printer) getSortedControlsNames() []string {
|
||||
controlNames := make([]string, 0, len(printer.summary))
|
||||
for k := range printer.summary {
|
||||
controlNames = append(controlNames, k)
|
||||
}
|
||||
sort.Strings(controlNames)
|
||||
return controlNames
|
||||
}
|
||||
|
||||
func getWriter(outputFile string) *os.File {
|
||||
|
||||
if outputFile != "" {
|
||||
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file")
|
||||
return os.Stdout
|
||||
}
|
||||
return f
|
||||
}
|
||||
return os.Stdout
|
||||
|
||||
}
|
||||
43
resultshandling/printer/summary.go
Normal file
43
resultshandling/printer/summary.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
type Summary map[string]ControlSummary
|
||||
|
||||
func NewSummary() Summary {
|
||||
return make(map[string]ControlSummary)
|
||||
}
|
||||
|
||||
type ControlSummary struct {
|
||||
TotalResources int
|
||||
TotalFailed int
|
||||
TotalWarnign int
|
||||
Description string
|
||||
Remediation string
|
||||
ListInputKinds []string
|
||||
WorkloadSummary map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
|
||||
}
|
||||
|
||||
type WorkloadSummary struct {
|
||||
Kind string
|
||||
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
|
||||
}
|
||||
|
||||
func (workloadSummary *WorkloadSummary) ToString() string {
|
||||
return fmt.Sprintf("/%s/%s/%s/%s", workloadSummary.Group, workloadSummary.Namespace, workloadSummary.Kind, workloadSummary.Name)
|
||||
}
|
||||
@@ -3,30 +3,30 @@ package printer
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// Group workloads by namespace - return {"namespace": <[]WorkloadSummery>}
|
||||
func groupByNamespace(resources []WorkloadSummery) map[string][]WorkloadSummery {
|
||||
mapResources := make(map[string][]WorkloadSummery)
|
||||
// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>}
|
||||
func groupByNamespace(resources []WorkloadSummary) map[string][]WorkloadSummary {
|
||||
mapResources := make(map[string][]WorkloadSummary)
|
||||
for i := range resources {
|
||||
if r, ok := mapResources[resources[i].Namespace]; ok {
|
||||
r = append(r, resources[i])
|
||||
mapResources[resources[i].Namespace] = r
|
||||
} else {
|
||||
mapResources[resources[i].Namespace] = []WorkloadSummery{resources[i]}
|
||||
mapResources[resources[i].Namespace] = []WorkloadSummary{resources[i]}
|
||||
}
|
||||
}
|
||||
return mapResources
|
||||
}
|
||||
func listResultSummery(ruleReports []opapolicy.RuleReport) []WorkloadSummery {
|
||||
workloadsSummery := []WorkloadSummery{}
|
||||
func listResultSummary(ruleReports []opapolicy.RuleReport) []WorkloadSummary {
|
||||
workloadsSummary := []WorkloadSummary{}
|
||||
track := map[string]bool{}
|
||||
|
||||
for c := range ruleReports {
|
||||
for _, ruleReport := range ruleReports[c].RuleResponses {
|
||||
resource, err := ruleResultSummery(ruleReport.AlertObject)
|
||||
resource, err := ruleResultSummary(ruleReport.AlertObject)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
continue
|
||||
@@ -34,31 +34,33 @@ func listResultSummery(ruleReports []opapolicy.RuleReport) []WorkloadSummery {
|
||||
|
||||
// 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
|
||||
workloadsSummery = append(workloadsSummery, resource[i])
|
||||
workloadsSummary = append(workloadsSummary, resource[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return workloadsSummery
|
||||
return workloadsSummary
|
||||
}
|
||||
func ruleResultSummery(obj opapolicy.AlertObject) ([]WorkloadSummery, error) {
|
||||
resource := []WorkloadSummery{}
|
||||
func ruleResultSummary(obj opapolicy.AlertObject) ([]WorkloadSummary, error) {
|
||||
resource := []WorkloadSummary{}
|
||||
|
||||
for i := range obj.K8SApiObjects {
|
||||
r, err := newWorkloadSummery(obj.K8SApiObjects[i])
|
||||
r, err := newWorkloadSummary(obj.K8SApiObjects[i])
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
resource = append(resource, *r)
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
func newWorkloadSummery(obj map[string]interface{}) (*WorkloadSummery, error) {
|
||||
r := &WorkloadSummery{}
|
||||
func newWorkloadSummary(obj map[string]interface{}) (*WorkloadSummary, error) {
|
||||
r := &WorkloadSummary{}
|
||||
|
||||
workload := k8sinterface.NewWorkloadObj(obj)
|
||||
if workload == nil {
|
||||
56
resultshandling/reporter/reporteventreceiver.go
Normal file
56
resultshandling/reporter/reporteventreceiver.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ReportEventReceiver struct {
|
||||
httpClient http.Client
|
||||
host url.URL
|
||||
}
|
||||
|
||||
func NewReportEventReceiver() *ReportEventReceiver {
|
||||
hostURL := initEventReceiverURL()
|
||||
return &ReportEventReceiver{
|
||||
httpClient: http.Client{},
|
||||
host: *hostURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *cautils.OPASessionObj) {
|
||||
if cautils.CustomerGUID == "" {
|
||||
return
|
||||
}
|
||||
if err := report.Send(opaSessionObj.PostureReport); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
func (report *ReportEventReceiver) Send(postureReport *opapolicy.PostureReport) error {
|
||||
|
||||
reqBody, err := json.Marshal(*postureReport)
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err)
|
||||
}
|
||||
host := hostToString(&report.host, postureReport.ReportID)
|
||||
|
||||
req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("in 'Send', http.NewRequest failed, host: %s, reason: %v", host, err)
|
||||
}
|
||||
res, err := report.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpClient.Do failed: %v", err)
|
||||
}
|
||||
msg, err := httpRespToString(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, %v:%s", host, err, msg)
|
||||
}
|
||||
return err
|
||||
}
|
||||
57
resultshandling/reporter/reporteventreceiverutils.go
Normal file
57
resultshandling/reporter/reporteventreceiverutils.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
|
||||
func httpRespToString(resp *http.Response) (string, error) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
return "", nil
|
||||
}
|
||||
strBuilder := strings.Builder{}
|
||||
defer resp.Body.Close()
|
||||
if resp.ContentLength > 0 {
|
||||
strBuilder.Grow(int(resp.ContentLength))
|
||||
}
|
||||
_, err := io.Copy(&strBuilder, resp.Body)
|
||||
if err != nil {
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
err = fmt.Errorf("response status: %d. Content: %s", resp.StatusCode, strBuilder.String())
|
||||
}
|
||||
|
||||
return strBuilder.String(), err
|
||||
}
|
||||
|
||||
func initEventReceiverURL() *url.URL {
|
||||
urlObj := url.URL{}
|
||||
|
||||
urlObj.Scheme = "https"
|
||||
urlObj.Host = "report.euprod1.cyberarmorsoft.com"
|
||||
urlObj.Path = "/k8s/postureReport"
|
||||
|
||||
q := urlObj.Query()
|
||||
q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
|
||||
q.Add("clusterName", cautils.ClusterName)
|
||||
urlObj.RawQuery = q.Encode()
|
||||
|
||||
return &urlObj
|
||||
}
|
||||
|
||||
func hostToString(host *url.URL, reportID string) string {
|
||||
q := host.Query()
|
||||
if reportID != "" {
|
||||
q.Add("reportID", reportID) // TODO - do we add the reportID?
|
||||
}
|
||||
host.RawQuery = q.Encode()
|
||||
return host.String()
|
||||
}
|
||||
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
|
||||
|
||||
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
|
||||
|
||||
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
|
||||
|
||||
return score
|
||||
}
|
||||
136
scapepkg/exceptions/exceptionprocessor.go
Normal file
136
scapepkg/exceptions/exceptionprocessor.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
func ListRuleExceptions(exceptionPolicies []armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) []armotypes.PostureExceptionPolicy {
|
||||
ruleExceptions := []armotypes.PostureExceptionPolicy{}
|
||||
for i := range exceptionPolicies {
|
||||
if ruleHasExceptions(&exceptionPolicies[i], frameworkName, controlName, ruleName) {
|
||||
ruleExceptions = append(ruleExceptions, exceptionPolicies[i])
|
||||
}
|
||||
}
|
||||
|
||||
return ruleExceptions
|
||||
|
||||
}
|
||||
|
||||
func ruleHasExceptions(exceptionPolicy *armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) bool {
|
||||
for _, posturePolicy := range exceptionPolicy.PosturePolicies {
|
||||
if posturePolicy.FrameworkName == "" && posturePolicy.ControlName == "" && posturePolicy.RuleName == "" {
|
||||
continue // empty policy -> ignore
|
||||
}
|
||||
if posturePolicy.FrameworkName != "" && posturePolicy.FrameworkName != frameworkName {
|
||||
continue // policy does not match
|
||||
}
|
||||
if posturePolicy.ControlName != "" && posturePolicy.ControlName != controlName {
|
||||
continue // policy does not match
|
||||
}
|
||||
if posturePolicy.RuleName != "" && posturePolicy.RuleName != ruleName {
|
||||
continue // policy does not match
|
||||
}
|
||||
return true // policies match
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func AddExceptionsToRuleResponses(results []opapolicy.RuleResponse, ruleExceptions []armotypes.PostureExceptionPolicy) {
|
||||
if len(ruleExceptions) == 0 {
|
||||
return
|
||||
}
|
||||
for i := range results {
|
||||
workloads := alertObjectToWorkloads(&results[i].AlertObject)
|
||||
if len(workloads) == 0 {
|
||||
continue
|
||||
}
|
||||
for w := range workloads {
|
||||
if exception := getException(ruleExceptions, workloads[w]); exception != nil {
|
||||
results[i].Exception = exception
|
||||
}
|
||||
}
|
||||
results[i].RuleStatus = results[i].GetSingleResultStatus()
|
||||
}
|
||||
}
|
||||
|
||||
func alertObjectToWorkloads(obj *opapolicy.AlertObject) []k8sinterface.IWorkload {
|
||||
resource := []k8sinterface.IWorkload{}
|
||||
|
||||
for i := range obj.K8SApiObjects {
|
||||
r := k8sinterface.NewWorkloadObj(obj.K8SApiObjects[i])
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
resource = append(resource, r)
|
||||
}
|
||||
|
||||
return resource
|
||||
}
|
||||
func getException(ruleExceptions []armotypes.PostureExceptionPolicy, workload k8sinterface.IWorkload) *armotypes.PostureExceptionPolicy {
|
||||
for e := range ruleExceptions {
|
||||
for _, resource := range ruleExceptions[e].Resources {
|
||||
if hasException(&resource, workload) {
|
||||
return &ruleExceptions[e] // TODO - return disable exception out of all exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compareMetadata - compare namespace and kind
|
||||
func hasException(designator *armotypes.PortalDesignator, workload k8sinterface.IWorkload) bool {
|
||||
cluster, namespace, kind, name, labels := designator.DigestPortalDesignator()
|
||||
|
||||
if cluster == "" && namespace == "" && kind == "" && name == "" && len(labels) == 0 {
|
||||
return false // if designators are empty
|
||||
}
|
||||
|
||||
// if cluster != "" && cluster != ClusterName { // TODO - where do we receive cluster name from?
|
||||
// return false // cluster name does not match
|
||||
// }
|
||||
|
||||
if namespace != "" && !compareNamespace(workload, namespace) {
|
||||
return false // namespaces do not match
|
||||
}
|
||||
|
||||
if kind != "" && !compareKind(workload, kind) {
|
||||
return false // kinds do not match
|
||||
}
|
||||
|
||||
if name != "" && !compareName(workload, name) {
|
||||
return false // names do not match
|
||||
}
|
||||
if len(labels) > 0 && !compareLabels(workload, labels) {
|
||||
return false // labels do not match
|
||||
}
|
||||
|
||||
return true // no mismatch found -> the workload has an exception
|
||||
}
|
||||
|
||||
func compareNamespace(workload k8sinterface.IWorkload, namespace string) bool {
|
||||
if workload.GetKind() == "Namespace" {
|
||||
return namespace == workload.GetName()
|
||||
}
|
||||
return namespace == workload.GetNamespace()
|
||||
}
|
||||
|
||||
func compareKind(workload k8sinterface.IWorkload, kind string) bool {
|
||||
return kind == workload.GetKind()
|
||||
}
|
||||
|
||||
func compareName(workload k8sinterface.IWorkload, name string) bool {
|
||||
return name == workload.GetName()
|
||||
}
|
||||
|
||||
func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string) bool {
|
||||
workloadLabels := labels.Set(workload.GetLabels())
|
||||
designators := labels.Set(attributes).AsSelector()
|
||||
|
||||
return designators.Matches(workloadLabels)
|
||||
}
|
||||
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
59
scapepkg/exceptions/exceptionprocessor_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package exceptions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
func PostureExceptionPolicyDisableMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{}
|
||||
}
|
||||
|
||||
func PostureExceptionPolicyAlertOnlyMock() *armotypes.PostureExceptionPolicy {
|
||||
return &armotypes.PostureExceptionPolicy{
|
||||
PortalBase: armotypes.PortalBase{
|
||||
Name: "postureExceptionPolicyAlertOnlyMock",
|
||||
},
|
||||
PolicyType: "postureExceptionPolicy",
|
||||
Actions: []armotypes.PostureExceptionPolicyActions{armotypes.AlertOnly},
|
||||
Resources: []armotypes.PortalDesignator{
|
||||
{
|
||||
DesignatorType: armotypes.DesignatorAttributes,
|
||||
Attributes: map[string]string{
|
||||
armotypes.AttributeNamespace: "default",
|
||||
armotypes.AttributeCluster: "unittest",
|
||||
},
|
||||
},
|
||||
},
|
||||
PosturePolicies: []armotypes.PosturePolicy{
|
||||
{
|
||||
FrameworkName: "MITRE",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRuleExceptions(t *testing.T) {
|
||||
exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
if len(res1) != 1 {
|
||||
t.Errorf("expecting 1 exception")
|
||||
}
|
||||
res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
if len(res2) != 0 {
|
||||
t.Errorf("expecting 0 exception")
|
||||
}
|
||||
}
|
||||
|
||||
// func TestGetException(t *testing.T) {
|
||||
// exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
|
||||
// res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
|
||||
// if len(res1) != 1 {
|
||||
// t.Errorf("expecting 1 exception")
|
||||
// }
|
||||
// res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
|
||||
// if len(res2) != 0 {
|
||||
// t.Errorf("expecting 0 exception")
|
||||
// }
|
||||
// }
|
||||
232
scapepkg/score/frameworkdict.json
Normal file
232
scapepkg/score/frameworkdict.json
Normal file
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"developer_framework": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"MITRE": {
|
||||
"Writable hostPath mount": {
|
||||
"baseScore": 8.0,
|
||||
"improvementRatio": 0.5
|
||||
},
|
||||
"Sidecar injection": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Compromised images in registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access tiller endpoint": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Data Destruction": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Resource Hijacking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access the Kubernetes API server": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Backdoor container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Network mapping": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Images from private registry": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Mount service principal": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access container service account": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (validating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubelet API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Vulnerable application": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Application exploit (RCE)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster-admin binding": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Kubernetes CronJob": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"SSH server running inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"List Kubernetes secrets": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Pod / container name similarity": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Cluster internal networking": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exposed sensitive interfaces": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Bash/cmd inside container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Clear container logs": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Access Kubernetes dashboard": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"New container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Privileged container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"CoreDNS poisoning": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"hostPath mount": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Instance Metadata API": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Malicious admission controller (mutating)": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Exec into container": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Delete Kubernetes events": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Applications credentials in configuration files": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
},
|
||||
"NSA": {
|
||||
"Control plane hardening": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Immutable container filesystem": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Non-root containers": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
},
|
||||
"Host PID/IPC privileges": {
|
||||
"baseScore": 1.0,
|
||||
"improvementRatio": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
1214
scapepkg/score/frameworkmock.json
Normal file
1214
scapepkg/score/frameworkmock.json
Normal file
File diff suppressed because it is too large
Load Diff
2161
scapepkg/score/resourcemocks.json
Normal file
2161
scapepkg/score/resourcemocks.json
Normal file
File diff suppressed because one or more lines are too long
22
scapepkg/score/resourcesdict.json
Normal file
22
scapepkg/score/resourcesdict.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"pod": 1.0,
|
||||
"service": 1.0,
|
||||
"daemonset": 1.0,
|
||||
"deployment": 1.0,
|
||||
"replicaset": 1.1,
|
||||
"statefulset": 1.0,
|
||||
"job": 1.0,
|
||||
"secret": 1.0,
|
||||
"cronjob": 1.0,
|
||||
"clusterrolebinding": 1.0,
|
||||
"clusterrole": 1.0,
|
||||
"rolebinding": 1.0,
|
||||
"role": 1.0,
|
||||
"networkpolicy": 1.0,
|
||||
"controllerrevision": 1.0,
|
||||
"namespace": 1.0,
|
||||
"serviceaccount": 1.0,
|
||||
"configmap": 1.0,
|
||||
"node": 1.0
|
||||
|
||||
}
|
||||
201
scapepkg/score/score.go
Normal file
201
scapepkg/score/score.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
||||
// corev1 "k8s.io/api/core/v1"
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
type ControlScoreWeights struct {
|
||||
BaseScore float32 `json:"baseScore"`
|
||||
RuntimeImprovementMultiplier float32 `json:"improvementRatio"`
|
||||
}
|
||||
|
||||
type ScoreUtil struct {
|
||||
ResourceTypeScores map[string]float32
|
||||
FrameworksScore map[string]map[string]ControlScoreWeights
|
||||
K8SApoObj *k8sinterface.KubernetesApi
|
||||
configPath string
|
||||
}
|
||||
|
||||
var postureScore *ScoreUtil
|
||||
|
||||
func (su *ScoreUtil) Calculate(frameworksReports []opapolicy.FrameworkReport) error {
|
||||
for i := range frameworksReports {
|
||||
su.CalculateFrameworkScore(&frameworksReports[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) CalculateFrameworkScore(framework *opapolicy.FrameworkReport) error {
|
||||
for i := range framework.ControlReports {
|
||||
framework.WCSScore += su.ControlScore(&framework.ControlReports[i], framework.Name)
|
||||
framework.Score += framework.ControlReports[i].Score
|
||||
framework.ARMOImprovement += framework.ControlReports[i].ARMOImprovement
|
||||
}
|
||||
if framework.WCSScore > 0 {
|
||||
framework.Score = (framework.Score * 100) / framework.WCSScore
|
||||
framework.ARMOImprovement = (framework.ARMOImprovement * 100) / framework.WCSScore
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to calculate score for framework %s due to bad wcs score", framework.Name)
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
daemonset: daemonsetscore*#nodes
|
||||
workloads: if replicas:
|
||||
replicascore*workloadkindscore*#replicas
|
||||
else:
|
||||
regular
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) resourceRules(resources []map[string]interface{}) float32 {
|
||||
var weight float32 = 0
|
||||
|
||||
for _, v := range resources {
|
||||
var score float32 = 0
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
kind := ""
|
||||
if wl != nil {
|
||||
kind = strings.ToLower(wl.GetKind())
|
||||
replicas := wl.GetReplicas()
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if replicas > 1 {
|
||||
score *= su.ResourceTypeScores["replicaset"] * float32(replicas)
|
||||
}
|
||||
|
||||
} else {
|
||||
epsilon := float32(0.00001)
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
kind = keys[0]
|
||||
score = su.ResourceTypeScores[kind]
|
||||
if score == 0.0 || (score > -1*epsilon && score < epsilon) {
|
||||
score = 1
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "daemonset" {
|
||||
b, err := json.Marshal(v)
|
||||
if err == nil {
|
||||
dmnset := appsv1.DaemonSet{}
|
||||
json.Unmarshal(b, &dmnset)
|
||||
score *= float32(dmnset.Status.DesiredNumberScheduled)
|
||||
}
|
||||
}
|
||||
weight += score
|
||||
}
|
||||
|
||||
return weight
|
||||
}
|
||||
|
||||
func (su *ScoreUtil) externalResourceConverter(rscs map[string]interface{}) []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
for atype, v := range rscs {
|
||||
resources = append(resources, map[string]interface{}{atype: v})
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
/*
|
||||
ControlScore:
|
||||
@input:
|
||||
ctrlReport - opapolicy.ControlReport object, must contain down the line the Input resources and the output resources
|
||||
frameworkName - calculate this control according to a given framework weights
|
||||
|
||||
ctrl.score = baseScore * SUM_resource (resourceWeight*min(#replicas*replicaweight,1)(nodes if daemonset)
|
||||
|
||||
returns control score ***for the input resources***
|
||||
|
||||
*/
|
||||
func (su *ScoreUtil) ControlScore(ctrlReport *opapolicy.ControlReport, frameworkName string) float32 {
|
||||
|
||||
aggregatedInputs := make([]map[string]interface{}, 0)
|
||||
aggregatedResponses := make([]map[string]interface{}, 0)
|
||||
for _, ruleReport := range ctrlReport.RuleReports {
|
||||
status, _, _ := ruleReport.GetRuleStatus()
|
||||
if status != "warning" {
|
||||
for _, ruleResponse := range ruleReport.RuleResponses {
|
||||
aggregatedResponses = append(aggregatedResponses, ruleResponse.AlertObject.K8SApiObjects...)
|
||||
aggregatedResponses = append(aggregatedResponses, su.externalResourceConverter(ruleResponse.AlertObject.ExternalObjects)...)
|
||||
}
|
||||
}
|
||||
|
||||
aggregatedInputs = append(aggregatedInputs, ruleReport.ListInputResources...)
|
||||
|
||||
}
|
||||
improvementRatio := float32(1)
|
||||
if ctrls, isOk := su.FrameworksScore[frameworkName]; isOk {
|
||||
if scoreobj, isOk2 := ctrls[ctrlReport.Name]; isOk2 {
|
||||
ctrlReport.BaseScore = scoreobj.BaseScore
|
||||
improvementRatio -= scoreobj.RuntimeImprovementMultiplier
|
||||
}
|
||||
} else {
|
||||
ctrlReport.BaseScore = 1.0
|
||||
}
|
||||
|
||||
ctrlReport.Score = ctrlReport.BaseScore * su.resourceRules(aggregatedResponses)
|
||||
ctrlReport.ARMOImprovement = ctrlReport.Score * improvementRatio
|
||||
|
||||
return ctrlReport.BaseScore * su.resourceRules(aggregatedInputs)
|
||||
|
||||
}
|
||||
|
||||
func getPostureFrameworksScores(weightPath string) map[string]map[string]ControlScoreWeights {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
frameworksScoreMap := make(map[string]map[string]ControlScoreWeights)
|
||||
dat, err := ioutil.ReadFile(weightPath + "frameworkdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &frameworksScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return frameworksScoreMap
|
||||
|
||||
}
|
||||
|
||||
func getPostureResourceScores(weightPath string) map[string]float32 {
|
||||
if len(weightPath) != 0 {
|
||||
weightPath = weightPath + "/"
|
||||
}
|
||||
resourceScoreMap := make(map[string]float32)
|
||||
dat, err := ioutil.ReadFile(weightPath + "resourcesdict.json")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resourceScoreMap); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resourceScoreMap
|
||||
|
||||
}
|
||||
|
||||
func NewScore(k8sapiobj *k8sinterface.KubernetesApi, configPath string) *ScoreUtil {
|
||||
if postureScore == nil {
|
||||
|
||||
postureScore = &ScoreUtil{
|
||||
ResourceTypeScores: getPostureResourceScores(configPath),
|
||||
FrameworksScore: getPostureFrameworksScores(configPath),
|
||||
configPath: configPath,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return postureScore
|
||||
}
|
||||
77
scapepkg/score/score_mocks.go
Normal file
77
scapepkg/score/score_mocks.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
func loadResourcesMock() []map[string]interface{} {
|
||||
resources := make([]map[string]interface{}, 0)
|
||||
|
||||
dat, err := ioutil.ReadFile("resourcemocks.json")
|
||||
|
||||
if err != nil {
|
||||
return resources
|
||||
}
|
||||
if err := json.Unmarshal(dat, &resources); err != nil {
|
||||
return resources
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func getResouceByType(desiredType string) map[string]interface{} {
|
||||
rsrcs := loadResourcesMock()
|
||||
if rsrcs == nil {
|
||||
return nil
|
||||
}
|
||||
for _, v := range rsrcs {
|
||||
wl := k8sinterface.NewWorkloadObj(v)
|
||||
if wl != nil {
|
||||
if strings.ToLower(wl.GetKind()) == desiredType {
|
||||
return v
|
||||
}
|
||||
continue
|
||||
|
||||
} else {
|
||||
for k := range v {
|
||||
if k == desiredType {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadFrameworkMock() *opapolicy.FrameworkReport {
|
||||
report := &opapolicy.FrameworkReport{}
|
||||
|
||||
dat, err := ioutil.ReadFile("frameworkmock.json")
|
||||
|
||||
if err != nil {
|
||||
return report
|
||||
}
|
||||
if err := json.Unmarshal(dat, &report); err != nil {
|
||||
return report
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
func getMITREFrameworkResultMock() []opapolicy.FrameworkReport {
|
||||
l := make([]opapolicy.FrameworkReport, 0)
|
||||
report := loadFrameworkMock()
|
||||
resources := loadResourcesMock()
|
||||
if report != nil && resources != nil {
|
||||
|
||||
report.ControlReports[0].RuleReports[0].ListInputResources = resources
|
||||
l = append(l, *report)
|
||||
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
65
scapepkg/score/score_test.go
Normal file
65
scapepkg/score/score_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package score
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFrameworkMock(t *testing.T) {
|
||||
r := getMITREFrameworkResultMock()
|
||||
su := NewScore(nil, "")
|
||||
var epsilon float32 = 0.001
|
||||
su.Calculate(r)
|
||||
var sumweights float32 = 0.0
|
||||
for _, v := range su.ResourceTypeScores {
|
||||
sumweights += v
|
||||
}
|
||||
|
||||
for _, framework := range r {
|
||||
if framework.Score < 1 {
|
||||
t.Errorf("framework %s invalid calculation1: %v", framework.Name, framework)
|
||||
}
|
||||
|
||||
if framework.Score > framework.WCSScore+epsilon {
|
||||
t.Errorf("framework %s invalid calculation2: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ARMOImprovement > framework.Score+epsilon {
|
||||
t.Errorf("framework %s invalid calculation3: %v", framework.Name, framework)
|
||||
}
|
||||
if framework.ControlReports[0].Score*sumweights <= 0+epsilon {
|
||||
t.Errorf("framework %s invalid calculation4: %v", framework.Name, framework)
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
func TestDaemonsetRule(t *testing.T) {
|
||||
desiredType := "daemonset"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 13 * su.ResourceTypeScores[desiredType]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleReplicasRule(t *testing.T) {
|
||||
desiredType := "deployment"
|
||||
r := getResouceByType(desiredType)
|
||||
if r == nil {
|
||||
t.Errorf("no %v was found in the mock, should be 1", desiredType)
|
||||
}
|
||||
su := NewScore(nil, "")
|
||||
|
||||
resources := []map[string]interface{}{r}
|
||||
weights := su.resourceRules(resources)
|
||||
expecting := 3 * su.ResourceTypeScores[desiredType] * su.ResourceTypeScores["replicaset"]
|
||||
if weights != expecting {
|
||||
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
|
||||
}
|
||||
}
|
||||
1
scapepkg/score/scoremethods.go
Normal file
1
scapepkg/score/scoremethods.go
Normal file
@@ -0,0 +1 @@
|
||||
package score
|
||||
Reference in New Issue
Block a user