mirror of
https://github.com/kubescape/kubescape.git
synced 2026-02-28 16:50:40 +00:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2779cb4e25 | ||
|
|
f46ee93539 | ||
|
|
3eb087e5c1 | ||
|
|
f069955231 | ||
|
|
d5290a6671 | ||
|
|
a54dac51af | ||
|
|
823455846f | ||
|
|
39c7bfeed9 | ||
|
|
67fc9832f8 | ||
|
|
559c4f3f15 | ||
|
|
187f517f58 | ||
|
|
c963b19364 | ||
|
|
2d7c5fd1ce | ||
|
|
59c935e723 | ||
|
|
bae45d277f | ||
|
|
e728b3ae37 | ||
|
|
01bc5345ab | ||
|
|
fc2374a690 | ||
|
|
00314be32a | ||
|
|
0b6dfa9cd0 | ||
|
|
1ff3a6c92c | ||
|
|
c8068a8d90 | ||
|
|
f75cee0d78 | ||
|
|
229f16cb01 | ||
|
|
44803ab915 | ||
|
|
2c6b1a440f | ||
|
|
37afc1352f | ||
|
|
9943119033 | ||
|
|
41457ff551 | ||
|
|
82b64b5828 | ||
|
|
229e8acc74 | ||
|
|
b50a665920 | ||
|
|
2d5ed19d6d | ||
|
|
30324e1c01 | ||
|
|
8ca356eae7 | ||
|
|
29f4ae368d | ||
|
|
409080f51b | ||
|
|
81e5bc3991 | ||
|
|
d6a9e50626 | ||
|
|
7500438991 | ||
|
|
0b24c46279 | ||
|
|
49596c5ac1 | ||
|
|
328265559c | ||
|
|
1fae5755fe | ||
|
|
96148ac6fd | ||
|
|
b2fcf295ce | ||
|
|
89ad5c1df0 | ||
|
|
798994850d | ||
|
|
9bf79db8f8 | ||
|
|
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
|
||||
|
||||
- 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 Asset
|
||||
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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
*.vs*
|
||||
*go.sum*
|
||||
*kubescape*
|
||||
*kubescape*
|
||||
*debug*
|
||||
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/
|
||||
|
||||
136
README.md
136
README.md
@@ -1,39 +1,141 @@
|
||||
<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://github.com/armosec/kubescape)
|
||||
[](https://goreportcard.com/report/github.com/armosec/kubescape)
|
||||
|
||||
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
|
||||
|
||||
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)
|
||||
## Usage & Examples
|
||||
|
||||
### Pre-Deployment Testing
|
||||
Check your YAML files before you're deploying, simply add them at the end of command line:
|
||||
```
|
||||
kubescape scan framework nsa *.yaml
|
||||
```
|
||||
|
||||
### Integration with other tools
|
||||
|
||||
Kubescape can produce output fitting for later processing:
|
||||
* JSON (`-f json`)
|
||||
* JUnit XML (`-f junit`)
|
||||
|
||||
### Examples
|
||||
|
||||
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
|
||||
```
|
||||
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
|
||||
```
|
||||
|
||||
* Scan local `yaml`/`json` files
|
||||
```
|
||||
kubescape scan framework nsa examples/online-boutique/*
|
||||
```
|
||||
|
||||
|
||||
* 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
|
||||
```
|
||||
|
||||
### Helm Support
|
||||
|
||||
Render the helm template and pass as stdout
|
||||
```
|
||||
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
for example:
|
||||
```
|
||||
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
|
||||
```
|
||||
|
||||
# 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:
|
||||
|
||||
# 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
|
||||
* Anonymous requests
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package armotypes
|
||||
|
||||
import "github.com/golang/glog"
|
||||
|
||||
var IgnoreLabels = []string{AttributeCluster, AttributeNamespace}
|
||||
|
||||
// DigestPortalDesignator - get cluster namespace and labels from designator
|
||||
@@ -12,7 +10,6 @@ func DigestPortalDesignator(designator *PortalDesignator) (string, string, map[s
|
||||
// case DesignatorWlid: TODO
|
||||
// case DesignatorWildWlid: TODO
|
||||
default:
|
||||
glog.Warningf("in 'digestPortalDesignator' designator type: '%v' not yet supported. please contact Armo team", designator.DesignatorType)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package cautils
|
||||
|
||||
import (
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// K8SResources map[<api group>/<api version>/<resource>]<resource object>
|
||||
|
||||
@@ -10,28 +10,61 @@ 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 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()
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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 +26,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,20 +45,17 @@ 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", err)
|
||||
}
|
||||
if _, err := restclient.InClusterConfig(); err == nil {
|
||||
RunningIncluster = true
|
||||
} else {
|
||||
RunningIncluster = false
|
||||
}
|
||||
K8SConfig = kubeconfig
|
||||
return nil
|
||||
@@ -66,7 +65,9 @@ func LoadK8sConfig() error {
|
||||
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:]...)
|
||||
// // }
|
||||
|
||||
// }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package opapolicy
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
armotypes "kube-escape/cautils/armotypes"
|
||||
armotypes "github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
type AlertScore float32
|
||||
@@ -28,7 +29,7 @@ type RuleResponse struct {
|
||||
|
||||
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 {
|
||||
@@ -42,11 +43,12 @@ type ControlReport struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
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"`
|
||||
RuleResponses []RuleResponse `json:"ruleResponses"`
|
||||
ListInputResources []map[string]interface{} `json:"-"`
|
||||
ListInputKinds []string `json:"-"`
|
||||
}
|
||||
type RuleStatus struct {
|
||||
Status string `json:"status"`
|
||||
@@ -148,3 +150,43 @@ type PolicyIdentifier struct {
|
||||
Kind NotificationPolicyKind `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ScanInfo struct {
|
||||
PolicyIdentifier PolicyIdentifier
|
||||
Format string
|
||||
Output string
|
||||
ExcludedNamespaces string
|
||||
InputPatterns []string
|
||||
Silent bool
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) Init() {
|
||||
// scanInfo.setSilentMode()
|
||||
scanInfo.setOutputFile()
|
||||
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setSilentMode() {
|
||||
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
|
||||
scanInfo.Silent = true
|
||||
}
|
||||
if scanInfo.Output != "" {
|
||||
scanInfo.Silent = true
|
||||
}
|
||||
}
|
||||
|
||||
func (scanInfo *ScanInfo) setOutputFile() {
|
||||
if scanInfo.Output == "" {
|
||||
return
|
||||
}
|
||||
if scanInfo.Format == "json" {
|
||||
if filepath.Ext(scanInfo.Output) != "json" {
|
||||
scanInfo.Output += ".json"
|
||||
}
|
||||
}
|
||||
if scanInfo.Format == "junit" {
|
||||
if filepath.Ext(scanInfo.Output) != "xml" {
|
||||
scanInfo.Output += ".xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package opapolicy
|
||||
import (
|
||||
"time"
|
||||
|
||||
armotypes "kube-escape/cautils/armotypes"
|
||||
armotypes "github.com/armosec/kubescape/cautils/armotypes"
|
||||
)
|
||||
|
||||
// Mock A
|
||||
|
||||
@@ -70,11 +70,21 @@ func ParseRegoResult(regoResult *rego.ResultSet) ([]RuleResponse, error) {
|
||||
func (controlReport *ControlReport) GetNumberOfResources() int {
|
||||
sum := 0
|
||||
for i := range controlReport.RuleReports {
|
||||
sum += controlReport.RuleReports[i].NumOfResources
|
||||
if controlReport.RuleReports[i].ListInputResources == nil {
|
||||
continue
|
||||
}
|
||||
sum += len(controlReport.RuleReports[i].ListInputResources)
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
140
cmd/framework.go
Normal file
140
cmd/framework.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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/printer"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var scanInfo opapolicy.ScanInfo
|
||||
var supportedFrameworks = []string{"nsa"}
|
||||
|
||||
type CLIHandler struct {
|
||||
policyHandler *policyhandler.PolicyHandler
|
||||
scanInfo *opapolicy.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 {
|
||||
return fmt.Errorf("requires at least one argument")
|
||||
}
|
||||
if !isValidFramework(args[0]) {
|
||||
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
scanInfo.PolicyIdentifier = opapolicy.PolicyIdentifier{}
|
||||
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
|
||||
scanInfo.PolicyIdentifier.Name = args[0]
|
||||
|
||||
if len(args[1:]) == 0 || args[1] != "-" {
|
||||
scanInfo.InputPatterns = args[1:]
|
||||
} else { // store stout to file
|
||||
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
scanInfo.InputPatterns = []string{tempFile.Name()}
|
||||
}
|
||||
scanInfo.Init()
|
||||
cautils.SetSilentMode(scanInfo.Silent)
|
||||
CliSetup()
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func isValidFramework(framework string) bool {
|
||||
return cautils.StringInSlice(supportedFrameworks, framework) != cautils.ValueNotFound
|
||||
}
|
||||
|
||||
func init() {
|
||||
scanCmd.AddCommand(frameworkCmd)
|
||||
scanInfo = opapolicy.ScanInfo{}
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "namespaces to exclude from check")
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `output format. supported formats: "pretty-printer"/"json"/"junit"`)
|
||||
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "output file. print output to file and not stdout")
|
||||
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "silent progress output")
|
||||
}
|
||||
|
||||
func CliSetup() error {
|
||||
flag.Parse()
|
||||
|
||||
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() {
|
||||
reporterObj := opaprocessor.NewOPAProcessor(&processNotification, &reportResults)
|
||||
reporterObj.ProcessRulesListenner()
|
||||
}()
|
||||
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
|
||||
p.ActionPrint()
|
||||
|
||||
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 and MITRE 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 |
67
docs/release.md
Normal file
67
docs/release.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 -
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
BIN
docs/summary.png
Executable file
BIN
docs/summary.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 206 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/*
|
||||
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
|
||||
}
|
||||
21
install.sh
21
install.sh
@@ -6,14 +6,27 @@ 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
|
||||
@@ -24,5 +37,5 @@ rm -rf $BASE_DIR
|
||||
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,14 @@ package opaprocessor
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"time"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"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"
|
||||
@@ -56,7 +57,6 @@ func (opap *OPAProcessor) ProcessRulesListenner() {
|
||||
}
|
||||
|
||||
func (opap *OPAProcessor) ProcessRulesHandler(opaSessionObj *cautils.OPASessionObj) error {
|
||||
glog.Infof(fmt.Sprintf("Starting 'ProcessRulesHandler'. reportID: %s", opaSessionObj.PostureReport.ReportID))
|
||||
cautils.ProgressTextDisplay(fmt.Sprintf("Scanning cluster %s", cautils.ClusterName))
|
||||
cautils.StartSpinner()
|
||||
frameworkReports := []opapolicy.FrameworkReport{}
|
||||
@@ -87,7 +87,8 @@ func (opap *OPAProcessor) ProcessRulesHandler(opaSessionObj *cautils.OPASessionO
|
||||
} else {
|
||||
ruleReport.RuleStatus.Status = "success"
|
||||
}
|
||||
ruleReport.NumOfResources = len(k8sObjects)
|
||||
ruleReport.ListInputResources = k8sObjects
|
||||
ruleReport.ListInputKinds = listMatchKinds(rule.Match)
|
||||
ruleReports = append(ruleReports, ruleReport)
|
||||
}
|
||||
controlReport.RuleReports = ruleReports
|
||||
@@ -99,7 +100,6 @@ func (opap *OPAProcessor) ProcessRulesHandler(opaSessionObj *cautils.OPASessionO
|
||||
|
||||
opaSessionObj.PostureReport.FrameworkReports = frameworkReports
|
||||
opaSessionObj.PostureReport.ReportGenerationTime = time.Now().UTC()
|
||||
glog.Infof(fmt.Sprintf("Done 'ProcessRulesHandler'. reportID: %s", opaSessionObj.PostureReport.ReportID))
|
||||
cautils.StopSpinner()
|
||||
cautils.SuccessTextDisplay(fmt.Sprintf("Done scanning cluster %s", cautils.ClusterName))
|
||||
return errs
|
||||
|
||||
@@ -3,18 +3,19 @@ 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/armosec/kubescape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy/resources"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -82,10 +83,10 @@ func TestCompromisedRegistries(t *testing.T) {
|
||||
// k8sResources["/v1/pods"] = k8sinterface.ConvertUnstructuredSliceToMap(k8sinterface.V1KubeSystemNamespaceMock().Items)
|
||||
k8sResources["/v1/pods"] = k8sinterface.V1AllClusterWithCompromisedRegistriesMock().Items
|
||||
wd, _ := os.Getwd()
|
||||
baseDirName := "kube-escape"
|
||||
baseDirName := "kubescape"
|
||||
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")
|
||||
resources.RegoDependenciesPath = path.Join(wd, "/kubescape/vendor/asterix.cyberarmor.io/cyberarmor/capacketsgo/opapolicy/resources/rego/dependencies")
|
||||
k8sinterface.K8SConfig = &restclient.Config{}
|
||||
|
||||
opaProcessor := NewOPAProcessorMock()
|
||||
@@ -108,7 +109,7 @@ func TestCompromisedRegistries(t *testing.T) {
|
||||
// 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"
|
||||
// resources.RegoDependenciesPath = "/home/david/go/src/kubescape/vendor/asterix.cyberarmor.io/cyberarmor/capacketsgo/opapolicy/resources/rego/dependencies"
|
||||
// opaProcessor := NewOPAProcessorMock()
|
||||
|
||||
// // set opaSessionObj
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package opaprocessor
|
||||
|
||||
import (
|
||||
"kube-escape/cautils"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
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/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
resources "github.com/armosec/kubescape/cautils/opapolicy/resources"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -22,7 +22,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 {
|
||||
@@ -59,3 +59,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 *opapolicy.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,12 @@ 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/k8sinterface"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// PolicyHandler -
|
||||
@@ -26,37 +25,27 @@ 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 *opapolicy.ScanInfo) error {
|
||||
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
|
||||
// validate notification
|
||||
// TODO
|
||||
|
||||
// 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, 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
|
||||
|
||||
// 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 +53,35 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
|
||||
*policyHandler.processPolicy <- opaSessionObj
|
||||
return nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
|
||||
|
||||
cautils.ProgressTextDisplay("Downloading framework definitions")
|
||||
|
||||
// TODO - support load policies from local file
|
||||
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
|
||||
if err != nil {
|
||||
return frameworks, err
|
||||
}
|
||||
|
||||
if len(frameworks) == 0 {
|
||||
err := fmt.Errorf("could not download any policies, please check previous logs")
|
||||
return frameworks, err
|
||||
}
|
||||
cautils.SuccessTextDisplay("Downloaded framework")
|
||||
|
||||
return frameworks, nil
|
||||
}
|
||||
|
||||
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
|
||||
var k8sResources *cautils.K8SResources
|
||||
var err error
|
||||
if len(scanInfo.InputPatterns) > 0 {
|
||||
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
|
||||
} else {
|
||||
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces)
|
||||
|
||||
}
|
||||
|
||||
return k8sResources, err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
)
|
||||
|
||||
// URLEncoder encode url
|
||||
@@ -49,7 +49,7 @@ type ArmoAPI struct {
|
||||
func NewArmoAPI() *ArmoAPI {
|
||||
return &ArmoAPI{
|
||||
httpClient: &http.Client{},
|
||||
hostURL: "https://dashbe.eudev3.cyberarmorsoft.com",
|
||||
hostURL: "https://dashbe.eustage2.cyberarmorsoft.com",
|
||||
}
|
||||
}
|
||||
func (db *ArmoAPI) GetServerAddress() string {
|
||||
@@ -61,7 +61,7 @@ func (db *ArmoAPI) GetHttpClient() *http.Client {
|
||||
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 += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(name))
|
||||
requestURI += "&getRules=true"
|
||||
|
||||
fullURL := URLEncoder(fmt.Sprintf("%s/%s", db.GetServerAddress(), requestURI))
|
||||
@@ -138,23 +138,13 @@ func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapoli
|
||||
// backend
|
||||
receivedFrameworks, err := d.OPAFRAMEWORKGet(rule.Name)
|
||||
if err != nil {
|
||||
errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
|
||||
errs = fmt.Errorf("Could not download framework, please check if this framework exists")
|
||||
}
|
||||
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,
|
||||
}
|
||||
frameworks = append(frameworks, framework)
|
||||
default:
|
||||
err := fmt.Errorf("missing rule kind, expected: %s", opapolicy.KindFramework)
|
||||
errs = fmt.Errorf("%v\nerror: %s", errs, err.Error())
|
||||
err := fmt.Errorf("Missing rule kind, expected: %s", opapolicy.KindFramework)
|
||||
errs = fmt.Errorf("%s", err.Error())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
79
printer/junit.go
Normal file
79
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
|
||||
}
|
||||
@@ -1,47 +1,79 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"kube-escape/cautils"
|
||||
"os"
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"kube-escape/cautils/k8sinterface"
|
||||
"kube-escape/cautils/opapolicy"
|
||||
"github.com/armosec/kubescape/cautils"
|
||||
|
||||
"github.com/armosec/kubescape/cautils/k8sinterface"
|
||||
"github.com/armosec/kubescape/cautils/opapolicy"
|
||||
|
||||
"github.com/enescakir/emoji"
|
||||
"github.com/golang/glog"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
var INDENT = " "
|
||||
var INDENT = " "
|
||||
|
||||
const EmptyPercentage = "NaN"
|
||||
|
||||
const (
|
||||
PrettyPrinter string = "pretty-printer"
|
||||
JsonPrinter string = "json"
|
||||
JunitResultPrinter string = "junit"
|
||||
)
|
||||
|
||||
type Printer struct {
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
summery Summery
|
||||
opaSessionObj *chan *cautils.OPASessionObj
|
||||
writer *os.File
|
||||
summary Summary
|
||||
sortedControlNames []string
|
||||
printerType string
|
||||
}
|
||||
|
||||
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj) *Printer {
|
||||
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj, printerType, outputFile string) *Printer {
|
||||
return &Printer{
|
||||
opaSessionObj: opaSessionObj,
|
||||
summery: NewSummery(),
|
||||
summary: NewSummary(),
|
||||
printerType: printerType,
|
||||
writer: getWriter(outputFile),
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
if !k8sinterface.RunningIncluster {
|
||||
break
|
||||
@@ -49,56 +81,79 @@ func (printer *Printer) ActionPrint() {
|
||||
}
|
||||
}
|
||||
|
||||
func (printer *Printer) SummerySetup(postureReport *opapolicy.PostureReport) {
|
||||
func (printer *Printer) SummarySetup(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)
|
||||
workloadsSummary := listResultSummary(cr.RuleReports)
|
||||
mapResources := groupByNamespace(workloadsSummary)
|
||||
|
||||
printer.summery[cr.Name] = ControlSummery{
|
||||
printer.summary[cr.Name] = ControlSummary{
|
||||
TotalResources: cr.GetNumberOfResources(),
|
||||
TotalFailed: len(workloadsSummery),
|
||||
WorkloadSummery: mapResources,
|
||||
Description: strings.ReplaceAll(cr.Description, ". ", fmt.Sprintf(".\n%s%s", INDENT, INDENT)),
|
||||
TotalFailed: len(workloadsSummary),
|
||||
WorkloadSummary: mapResources,
|
||||
Description: cr.Description,
|
||||
Remediation: cr.Remediation,
|
||||
ListInputKinds: cr.ListControlsInputKinds(),
|
||||
}
|
||||
}
|
||||
}
|
||||
printer.sortedControlNames = printer.getSortedControlsNames()
|
||||
|
||||
}
|
||||
|
||||
func (printer *Printer) PrintResults() {
|
||||
for control, controlSummery := range printer.summery {
|
||||
printer.printTitle(control, &controlSummery)
|
||||
printer.printResult(control, &controlSummery)
|
||||
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) 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)
|
||||
func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
|
||||
cautils.SimpleDisplay(printer.writer, "Summary - ")
|
||||
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed)
|
||||
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.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
|
||||
} else {
|
||||
cautils.FailureDisplay(os.Stdout, "failed %v\n", emoji.SadButRelievedFace)
|
||||
cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
|
||||
}
|
||||
|
||||
cautils.SimpleDisplay(os.Stdout, "%sDescription: %s\n", INDENT, controlSummery.Description)
|
||||
cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
|
||||
|
||||
}
|
||||
func (printer *Printer) printResult(controlName string, controlSummery *ControlSummery) {
|
||||
func (printer *Printer) printResult(controlName string, controlSummary *ControlSummary) {
|
||||
|
||||
indent := INDENT
|
||||
for ns, rsc := range controlSummery.WorkloadSummery {
|
||||
for ns, rsc := range controlSummary.WorkloadSummary {
|
||||
preIndent := indent
|
||||
indent += indent
|
||||
cautils.SimpleDisplay(os.Stdout, "%sNamespace %s\n", indent, ns)
|
||||
if ns != "" {
|
||||
cautils.SimpleDisplay(printer.writer, "%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))
|
||||
cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
|
||||
indent = preIndent2
|
||||
}
|
||||
indent = preIndent
|
||||
@@ -106,10 +161,14 @@ func (printer *Printer) printResult(controlName string, controlSummery *ControlS
|
||||
|
||||
}
|
||||
|
||||
func generateRow(control string, cs ControlSummery) []string {
|
||||
func generateRow(control string, cs ControlSummary) []string {
|
||||
row := []string{control}
|
||||
row = append(row, cs.ToSlice()...)
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
|
||||
if cs.TotalResources != 0 {
|
||||
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
|
||||
} else {
|
||||
row = append(row, EmptyPercentage)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
@@ -132,12 +191,15 @@ func generateFooter(numControlers, sumFailed, sumTotal int) []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), "%"))
|
||||
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(os.Stdout)
|
||||
summaryTable := tablewriter.NewWriter(printer.writer)
|
||||
summaryTable.SetAutoWrapText(false)
|
||||
summaryTable.SetHeader(generateHeader())
|
||||
summaryTable.SetHeaderLine(true)
|
||||
@@ -145,11 +207,35 @@ func (printer *Printer) PrintSummaryTable() {
|
||||
sumTotal := 0
|
||||
sumFailed := 0
|
||||
|
||||
for k, v := range printer.summery {
|
||||
summaryTable.Append(generateRow(k, v))
|
||||
sumFailed += v.TotalFailed
|
||||
sumTotal += v.TotalResources
|
||||
for i := 0; i < len(printer.sortedControlNames); i++ {
|
||||
controlSummary := printer.summary[printer.sortedControlNames[i]]
|
||||
summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
|
||||
sumFailed += controlSummary.TotalFailed
|
||||
sumTotal += controlSummary.TotalResources
|
||||
}
|
||||
summaryTable.SetFooter(generateFooter(len(printer.summery), sumFailed, sumTotal))
|
||||
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, 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
|
||||
|
||||
}
|
||||
|
||||
38
printer/summary.go
Normal file
38
printer/summary.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Summary map[string]ControlSummary
|
||||
|
||||
func NewSummary() Summary {
|
||||
return make(map[string]ControlSummary)
|
||||
}
|
||||
|
||||
type ControlSummary struct {
|
||||
TotalResources int
|
||||
TotalFailed int
|
||||
Description string
|
||||
Remediation string
|
||||
ListInputKinds []string
|
||||
WorkloadSummary map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
|
||||
}
|
||||
|
||||
type WorkloadSummary struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string
|
||||
Group string
|
||||
}
|
||||
|
||||
func (controlSummary *ControlSummary) ToSlice() []string {
|
||||
s := []string{}
|
||||
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -36,18 +36,18 @@ func listResultSummery(ruleReports []opapolicy.RuleReport) []WorkloadSummery {
|
||||
for i := range resource {
|
||||
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
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func ruleResultSummery(obj opapolicy.AlertObject) ([]WorkloadSummery, error) {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user