Compare commits

..

161 Commits

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

* update screenshot

* update summary
2021-09-05 14:44:55 +03:00
Benyamin Hirschberg
23090fbb9f Merge pull request #53 from armosec/dev
Supporting failure threshold
2021-09-02 00:00:17 +03:00
Benyamin Hirschberg
fcb6255f75 Merge pull request #52 from BenHirschbergCa/dev
Support failure threshold
2021-09-01 23:59:16 +03:00
Ben Hirschberg
abbc9c3e2e Support failure threshold 2021-09-01 23:41:12 +03:00
David Wertenteil
75f76fcecd Update README.md 2021-09-01 17:23:52 +03:00
dwertent
a00bba5fe4 support downloaded files 2021-09-01 17:18:26 +03:00
Daniel Grunberger
96473188ed Merge pull request #51 from Daniel-GrunbergerCA/master
scan with local file
2021-09-01 17:11:55 +03:00
Daniel-GrunbergerCA
56264df047 update scan with local file 2021-09-01 17:02:41 +03:00
Daniel-GrunbergerCA
c3008981e4 download from be 2021-09-01 16:51:43 +03:00
Daniel Grunberger
572130d797 Merge pull request #49 from Daniel-GrunbergerCA/master
support store download
2021-09-01 16:35:39 +03:00
Daniel-GrunbergerCA
7d8cf37532 support store download 2021-09-01 16:32:33 +03:00
Daniel-GrunbergerCA
ebd9661255 Merge remote-tracking branch 'upstream/dev' 2021-09-01 15:51:35 +03:00
Daniel-GrunbergerCA
05e108b47b add download json option 2021-09-01 15:51:24 +03:00
dwertent
8cb6824f3c ignore k8s config when running local yamls 2021-09-01 13:29:44 +03:00
dwertent
304017fc41 Merge branch 'dev' of ssh://github.com/armosec/kubescape into dev 2021-09-01 11:04:49 +03:00
dwertent
b82673f694 Merge branch 'master' of ssh://github.com/armosec/kubescape 2021-09-01 11:03:27 +03:00
dwertent
ac7e5219f1 interface support of framework 2021-09-01 11:03:20 +03:00
Daniel Grunberger
a4c4f9c6ed Update README.md 2021-08-31 17:22:27 +03:00
David Wertenteil
571a15bee8 Update README.md 2021-08-31 16:31:06 +03:00
dwertent
8a00a5c54b Add input table to readme 2021-08-31 16:29:11 +03:00
Daniel Grunberger
8f8aaf70d9 Update README.md 2021-08-31 13:00:17 +03:00
David Wertenteil
d3f4af0f9c Update module to github (#47)
* update module
2021-08-31 12:00:42 +03:00
dwertent
f069955231 Merge branch 'master' of ssh://github.com/armosec/kubescape 2021-08-31 11:07:16 +03:00
dwertent
d5290a6671 support new controls 2021-08-31 11:05:59 +03:00
dwertent
a54dac51af remove beta from version 2021-08-31 10:42:05 +03:00
Daniel Grunberger
823455846f Update README.md 2021-08-31 10:38:32 +03:00
Daniel Grunberger
39c7bfeed9 Merge pull request #46 from Daniel-GrunbergerCA/master
Add anonymous http req
2021-08-31 10:27:11 +03:00
danielgrunbergerarmo
67fc9832f8 Merge remote-tracking branch 'upstream/dev' 2021-08-31 10:14:41 +03:00
danielgrunbergerarmo
559c4f3f15 add anonymous http req 2021-08-31 09:55:23 +03:00
dwertent
187f517f58 update help print 2021-08-31 09:27:29 +03:00
dwertent
c963b19364 Merge branch 'master' of ssh://github.com/armosec/kubescape into dev 2021-08-31 09:14:59 +03:00
David Wertenteil
2d7c5fd1ce update output functionality (#45)
* update output f
2021-08-31 09:13:12 +03:00
Benyamin Hirschberg
e728b3ae37 Merge pull request #44 from BenHirschbergCa/dev
some fixes around command texts
2021-08-30 22:08:36 +03:00
Benyamin Hirschberg
01bc5345ab Merge branch 'dev' into dev 2021-08-30 22:08:13 +03:00
Ben Hirschberg
fc2374a690 some fixes around command textx 2021-08-30 22:03:03 +03:00
David Wertenteil
00314be32a Update format and output flags (#43)
* support stdin input

* support output to file
2021-08-30 18:49:17 +03:00
David Wertenteil
c8068a8d90 Support stdin input (#42)
* support stdin input
2021-08-30 14:55:50 +03:00
David Wertenteil
44803ab915 Support glob file loading (#41)
* recursive glob

* adding helm support to readme

* update glob function
2021-08-30 11:06:13 +03:00
David Wertenteil
b50a665920 Update README.md 2021-08-29 10:21:32 +03:00
David Wertenteil
2d5ed19d6d support url input and update readme (#40)
* split to functions

* update package name to kubescape

* support url input, update readme
2021-08-29 10:20:12 +03:00
Benyamin Hirschberg
81e5bc3991 Merge pull request #39 from BenHirschbergCa/dev
Adding explanation about YAML support and output integration
2021-08-28 22:22:05 +03:00
Ben Hirschberg
d6a9e50626 Adding exaplanation about YAML support and output integration 2021-08-28 22:19:18 +03:00
David Wertenteil
7500438991 Support yaml input and silent mode print
Support yaml input and silent mode print
2021-08-26 18:37:42 +03:00
dwertent
328265559c after merge 2021-08-26 18:13:46 +03:00
dwertent
1fae5755fe example sh 2021-08-26 17:42:02 +03:00
dwertent
96148ac6fd support yaml input 2021-08-26 17:41:11 +03:00
danielgrunbergerarmo
b2fcf295ce add silent flag 2021-08-26 13:50:48 +03:00
Daniel Grunberger
89ad5c1df0 Merge pull request #30 from Daniel-GrunbergerCA/master
Support json and junit output
2021-08-26 13:48:16 +03:00
Benyamin Hirschberg
798994850d Adding link to download badge 2021-08-26 13:11:07 +03:00
dwertent
f9ab72d595 after merge 2021-08-26 12:15:58 +03:00
dwertent
8f4ac5dd87 adding yaml input 2021-08-26 12:11:04 +03:00
danielgrunbergerarmo
5a5f2b408c support json and junit output 2021-08-26 11:30:51 +03:00
danielgrunbergerarmo
615c1af63e Merge remote-tracking branch 'upstream/dev' 2021-08-26 11:17:37 +03:00
danielgrunbergerarmo
c67e584cfb Support yamls 2021-08-26 11:03:51 +03:00
dwertent
6414be3c6f Merge remote-tracking branch 'upstream/dev' 2021-08-26 10:37:21 +03:00
Benyamin Hirschberg
3d9f98e866 Merge pull request #28 from BenHirschbergCa/dev
added JUnit formatter
2021-08-26 07:44:26 +03:00
Ben Hirschberg
aba67cc596 asigning class name 2021-08-26 07:42:57 +03:00
Ben Hirschberg
0d43fec008 Junit XML support 2021-08-25 22:32:10 +03:00
Benyamin Hirschberg
2574f1954d Merge pull request #26 from BenHirschbergCa/dev
Adding JSON print option and fixing typos
2021-08-25 17:47:12 +03:00
Ben Hirschberg
b97f7d6e36 adding option for JSON output 2021-08-25 17:37:49 +03:00
Ben Hirschberg
ce908aa748 fixing typo 2021-08-25 17:09:04 +03:00
Daniel Grunberger
707a2c78fe Merge pull request #25 from amiralis/patch-1
Fix typo
2021-08-25 16:36:05 +03:00
Ben Hirschberg
5f1bb0ce0a State supported Go version 2021-08-25 11:35:24 +03:00
Amirali Sanatinia
4683b56ce0 Fix typo
"Kubernetes Hardening Guidance by to NSA and CISA" -> "Kubernetes Hardening Guidance by NSA and CISA"
2021-08-24 15:46:03 -04:00
danielgrunbergerarmo
a7e69c8096 start using cobra pkg for cli flags 2021-08-24 17:35:57 +03:00
Daniel Grunberger
3735f4fcc2 Merge pull request #23 from Daniel-GrunbergerCA/master
Download framework from staging
2021-08-24 10:30:04 +03:00
danielgrunbergerarmo
2606be8ecd Merge remote-tracking branch 'upstream/master' 2021-08-24 10:25:36 +03:00
danielgrunbergerarmo
c51ea6bafd Download framework from staging 2021-08-24 10:25:15 +03:00
dwertent
6588507e6f Support kubeconfig from KUBECONFIG env and --kubeconfig flag. issue #15 2021-08-24 09:33:59 +03:00
dwertent
b9da2380fa Support kubeconfig from KUBECONFIG env and --kubeconfig flag. issue #15 2021-08-24 09:04:09 +03:00
Mayo
e162d5e8b2 feat: support kubeconfig env 2021-08-24 12:05:30 +08:00
Bezbran
5bec5b0075 fix release semvar 2021-08-23 17:35:48 +03:00
Bezbran
747940d66c add some whitespaces 2021-08-23 17:33:43 +03:00
Benyamin Hirschberg
789902f534 Update README.md 2021-08-23 13:30:24 +03:00
Benyamin Hirschberg
140ef8ac91 Update README.md 2021-08-23 13:27:06 +03:00
Daniel Grunberger
4b4b05caba Update README.md 2021-08-22 21:12:38 +03:00
David Wertenteil
abbb14c571 Support os
Support os
2021-08-22 12:08:29 +03:00
dwertent
93a5f133c3 support os types 2021-08-22 12:03:28 +03:00
dwertent
50f99f4719 run once 2021-08-22 08:46:06 +03:00
dwertent
ba45dd9f1c rm from path 2021-08-22 08:26:06 +03:00
dwertent
7669f791d5 remove path from setup 2021-08-22 08:23:48 +03:00
dwertent
c592728f62 add os to path 2021-08-22 08:20:56 +03:00
dwertent
ea0d48bf6d update tag name 2021-08-22 08:09:55 +03:00
dwertent
b8a9dd9359 update tag name 2021-08-22 08:03:54 +03:00
dwertent
dba4520f27 adding os name 2021-08-22 07:55:24 +03:00
dwertent
111d04dabe remove os from name 2021-08-21 23:15:45 +03:00
dwertent
5f5215ff8e update install 2021-08-21 23:13:27 +03:00
dwertent
12a6229193 support os release 2021-08-21 23:10:48 +03:00
dwertent
0992e6edd7 sort summary table 2021-08-20 00:07:29 +03:00
dwertent
12294ad23c download latest release 2021-08-19 23:23:07 +03:00
David Wertenteil
b2d14a778a Sort controls, update gif
Sort control output
2021-08-19 22:45:01 +03:00
dwertent
b92a44dd7c sort control output 2021-08-19 22:41:35 +03:00
Daniel Grunberger
35ea718080 Add new tests 2021-08-19 17:50:06 +03:00
Daniel Grunberger
47ed057e66 Update tag 2021-08-19 17:45:59 +03:00
David Wertenteil
5e0e5c6231 do not run flow on push 2021-08-19 17:43:48 +03:00
David Wertenteil
4f9a4a6c61 update release 2021-08-19 17:37:22 +03:00
David Wertenteil
22b3544243 Update error handling 2021-08-19 17:36:21 +03:00
danielgrunbergerarmo
297a4fc42b fix error handling 2021-08-19 13:42:23 +03:00
Daniel Grunberger
405cd837a1 Update README.md 2021-08-18 18:45:46 +03:00
dwertent
e6b2688462 update install version 2021-08-18 14:56:48 +03:00
Daniel Grunberger
e5b35fcb55 Merge pull request #6 from Daniel-GrunbergerCA/master
Update output format
2021-08-18 14:54:59 +03:00
Daniel Grunberger
35449e3d4e Merge branch 'master' into master 2021-08-18 14:54:33 +03:00
danielgrunbergerarmo
9509c69d87 update install version 2021-08-18 14:51:05 +03:00
danielgrunbergerarmo
34170faae9 update readme.md 2021-08-18 14:45:49 +03:00
danielgrunbergerarmo
d5d0da8ac3 fix non-namespacd resources 2021-08-18 14:33:14 +03:00
danielgrunbergerarmo
8b7a4b1e48 fix output format 2021-08-18 14:21:44 +03:00
danielgrunbergerarmo
d5383fe218 fix field selector for non-namespaced resources 2021-08-18 14:04:00 +03:00
dwertent
46b9cf35ac update version 2021-08-18 12:05:01 +03:00
danielgrunbergerarmo
329d341fbf delete build.yaml 2021-08-18 11:58:23 +03:00
David Wertenteil
6be692c66f Merge pull request #4 from Daniel-GrunbergerCA/master
add exclude-namespaces flag
2021-08-18 11:56:43 +03:00
danielgrunbergerarmo
3c062238ad add cli flag 2021-08-18 11:48:10 +03:00
Daniel Grunberger
954224e9f6 Update README.md 2021-08-17 17:39:50 +03:00
David Wertenteil
a5f99e0a8d Merge pull request #3 from dwertent/master
Update description display
2021-08-17 16:27:53 +03:00
dwertent
d484aeb62c update description 2021-08-17 16:24:23 +03:00
Daniel Grunberger
8c3eeab7ed Update README.md 2021-08-17 13:37:24 +03:00
Benyamin Hirschberg
cea8266734 Update README.md 2021-08-15 22:35:29 +03:00
Benyamin Hirschberg
eefaf7b23c adding progress bar 2021-08-15 21:53:26 +03:00
Benyamin Hirschberg
bc61755f67 Update install.sh 2021-08-15 21:46:37 +03:00
Benyamin Hirschberg
c462d1ec2f Update build.yaml 2021-08-15 21:43:11 +03:00
Benyamin Hirschberg
203d43347e optimize release 2021-08-15 21:40:39 +03:00
Benyamin Hirschberg
d102789a35 Update README.md 2021-08-15 21:34:01 +03:00
Benyamin Hirschberg
28b431c623 Update README.md 2021-08-15 21:33:15 +03:00
Benyamin Hirschberg
2fb1fef6d5 Merge pull request #2 from BenHirschbergCa/dev
updating image + gif v
2021-08-15 21:21:45 +03:00
Ben Hirschberg
091a811fa1 updating image + gif v 2021-08-15 21:19:56 +03:00
96 changed files with 7896 additions and 1098 deletions

View File

@@ -5,49 +5,50 @@ on:
branches: [ master ]
pull_request:
branches: [ master ]
types: [ closed ]
jobs:
build:
once:
name: Create release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- uses: actions/checkout@v2
- name: Create a release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v1.0.${{ github.run_number }}
release_name: Release v1.0.${{ github.run_number }}
draft: false
prerelease: false
build:
name: Create cross-platform release build, tag and upload binaries
needs: once
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v1
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Build
run: mkdir build && go mod tidy && go build -o build/kubescape
- name: Chmod
run: chmod +x build/kubescape
- name: List
run: ls -la
- name: Build
run: mkdir -p build/${{ matrix.os }} && go mod tidy && go build -ldflags "-w -s" -o build/${{ matrix.os }}/kubescape # && md5sum build/${{ matrix.os }}/kubescape > build/${{ matrix.os }}/kubescape.md5
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v0.0.${{ github.run_number }}
release_name: Release v0.0.${{ github.run_number }}
body: |
Changes in this Release
- First Change
- Second Change
draft: false
prerelease: false
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: build/kubescape
asset_name: kubescape
asset_content_type: application/octet-stream
- name: Upload Release binaries
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.once.outputs.upload_url }}
asset_path: build/${{ matrix.os }}/kubescape
asset_name: kubescape-${{ matrix.os }}
asset_content_type: application/octet-stream

4
.gitignore vendored
View File

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

4
.gitmodules vendored
View File

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

View File

@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

171
README.md
View File

@@ -1,39 +1,176 @@
<img src="docs/kubescape.png" width="300" alt="logo" align="center">
kubescape is a tool for testing Kubernetes clusters against industry accepted security standards and recomendations like:
* NSA hardening for Kubernetes operators [see here](https://media.defense.gov/2021/Aug/03/2002820425/-1/-1/1/CTR_KUBERNETES%20HARDENING%20GUIDANCE.PDF)
* MITRE threat matrix for Kubernetes [see here](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/)
[![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://goreportcard.com/report/github.com/armosec/kubescape)
Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
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
[![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml)
### Flags
| flag | default | description | options |
| --- | --- | --- | --- |
| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces |
| `-s`/`--silent` | Display progress messages | Silent progress messages |
| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` |
| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` |
| `-o`/`--output` | print to stdout | Save scan result in file |
| `--use-from` | | Load local framework object from specified path. If not used will download latest |
| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` |
| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json) |
## Usage & Examples
### Examples
* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
* Scan local `yaml`/`json` files before deploying
```
kubescape scan framework nsa *.yaml
```
* Scan `yaml`/`json` files from url
```
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
```
* Output in `json` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
```
* Output in `junit xml` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
```
* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` <img src="docs/new-feature.svg">
```
kubescape scan framework nsa --exceptions examples/exceptions.json
```
### Helm Support
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
```
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
```
for example:
```
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
```
### Offline Support <img src="docs/new-feature.svg">
It is possible to run Kubescape offline!
First download the framework and then scan with `--use-from` flag
* Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
```
kubescape download framework nsa --output nsa.json
```
* Scan using the downloaded framework
```
kubescape scan framework nsa --use-from nsa.json
```
# How to build
`go mod tidy && go build -o kubescape` :zany_face:
Note: development (and the release process) is done with Go `1.16`
1. Clone Project
```
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
```
2. Build
```
go mod tidy && go build -o kubescape .
```
3. Run
```
./kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
4. Enjoy :zany_face:
# How to build in Docker
1. Clone Project
```
git clone git@github.com:armosec/kubescape.git kubescape && cd "$_"
```
2. Build
```
docker build -t kubescape -f build/Dockerfile .
```
# Under the hood
## Tests
Defining the tests here...
Kubescape is running the following tests according to what is defined by [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
* Non-root containers
* Immutable container filesystem
* Privileged containers
* hostPID, hostIPC privileges
* hostNetwork access
* allowedHostPaths field
* Protecting pod service account tokens
* Resource policies
* Control plane hardening
* Exposed dashboard
* Allow privilege escalation
* Applications credentials in configuration files
* Cluster-admin binding
* Exec into container
* Dangerous capabilities
* Insecure capabilities
* Linux hardening
* Ingress and Egress blocked
* Container hostPort
* Network policies
## Technology
Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls.
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by (ARMO)[https://www.armosec.io/].
The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/).
The results by default printed in a pretty "console friendly" manner, but they can be retrieved in JSON format for further processing.
Kubescape is an open source project, we welcome your feedback and ideas for improvement. Were also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.

13
build/Dockerfile Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,28 +10,62 @@ import (
"github.com/mattn/go-isatty"
)
var silent = false
func SetSilentMode(s bool) {
silent = s
}
func IsSilent() bool {
return silent
}
var FailureDisplay = color.New(color.Bold, color.FgHiRed).FprintfFunc()
var WarningDisplay = color.New(color.Bold, color.FgCyan).FprintfFunc()
var FailureTextDisplay = color.New(color.Faint, color.FgHiRed).FprintfFunc()
var InfoDisplay = color.New(color.Bold, color.FgHiYellow).FprintfFunc()
var InfoTextDisplay = color.New(color.Faint, color.FgHiYellow).FprintfFunc()
var SimpleDisplay = color.New(color.Bold, color.FgHiWhite).FprintfFunc()
var SuccessDisplay = color.New(color.Bold, color.FgHiGreen).FprintfFunc()
var DescriptionDisplay = color.New(color.Faint, color.FgWhite).FprintfFunc()
var Spinner *spinner.Spinner
func ScanStartDisplay() {
if IsSilent() {
return
}
InfoDisplay(os.Stdout, "ARMO security scanner starting\n")
}
func SuccessTextDisplay(str string) {
if IsSilent() {
return
}
SuccessDisplay(os.Stdout, "[success] ")
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
}
func ErrorDisplay(str string) {
if IsSilent() {
return
}
SuccessDisplay(os.Stdout, "[Error] ")
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
}
func ProgressTextDisplay(str string) {
if IsSilent() {
return
}
InfoDisplay(os.Stdout, "[progress] ")
SimpleDisplay(os.Stdout, fmt.Sprintf("%s\n", str))
}
func StartSpinner() {
if isatty.IsTerminal(os.Stdout.Fd()) {
if !IsSilent() && isatty.IsTerminal(os.Stdout.Fd()) {
Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) // Build our new spinner
Spinner.Start()
}

6
cautils/downloadinfo.go Normal file
View File

@@ -0,0 +1,6 @@
package cautils
type DownloadInfo struct {
Path string
FrameworkName string
}

View File

@@ -1,9 +1,5 @@
package cautils
import (
"os"
)
// CA environment vars
var (
CustomerGUID = ""
@@ -13,12 +9,3 @@ var (
DashboardBackendURL = ""
RestAPIPort = "4001"
)
func SetupDefaultEnvs() {
if os.Getenv("CA_DASHBOARD_BACKEND") == "" {
os.Setenv("CA_DASHBOARD_BACKEND", "https://dashbe.eudev3.cyberarmorsoft.com") // use prod
}
if os.Getenv("CA_CUSTOMER_GUID") == "" {
os.Setenv("CA_CUSTOMER_GUID", "11111111-1111-1111-1111-111111111111")
}
}

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

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

View File

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

View File

@@ -0,0 +1,12 @@
package getter
import (
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type IPolicyGetter interface {
GetFramework(name string) (*opapolicy.Framework, error)
GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error)
// GetScores(scope, customerName, namespace string) ([]armotypes.PostureExceptionPolicy, error)
}

View File

@@ -0,0 +1,114 @@
package getter
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils/opapolicy"
)
func GetDefaultPath(frameworkName string) string {
defaultfilePath := filepath.Join(DefaultLocalStore, frameworkName+".json")
if homeDir, err := os.UserHomeDir(); err == nil {
defaultfilePath = filepath.Join(homeDir, defaultfilePath)
}
return defaultfilePath
}
func SaveFrameworkInFile(framework *opapolicy.Framework, path string) error {
encodedData, err := json.Marshal(framework)
if err != nil {
return err
}
err = os.WriteFile(path, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
if err != nil {
return err
}
return nil
}
// JSONDecoder returns JSON decoder for given string
func JSONDecoder(origin string) *json.Decoder {
dec := json.NewDecoder(strings.NewReader(origin))
dec.UseNumber()
return dec
}
func HttpGetter(httpClient *http.Client, fullURL string) (string, error) {
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return "", err
}
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
respStr, err := httpRespToString(resp)
if err != nil {
return "", err
}
return respStr, nil
}
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
func httpRespToString(resp *http.Response) (string, error) {
if resp == nil || resp.Body == nil {
return "", nil
}
strBuilder := strings.Builder{}
defer resp.Body.Close()
if resp.ContentLength > 0 {
strBuilder.Grow(int(resp.ContentLength))
}
bytesNum, err := io.Copy(&strBuilder, resp.Body)
respStr := strBuilder.String()
if err != nil {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
return respStr, err
}
// URLEncoder encode url
func urlEncoder(oldURL string) string {
fullURL := strings.Split(oldURL, "?")
baseURL, err := url.Parse(fullURL[0])
if err != nil {
return ""
}
// Prepare Query Parameters
if len(fullURL) > 1 {
params := url.Values{}
queryParams := strings.Split(fullURL[1], "&")
for _, i := range queryParams {
queryParam := strings.Split(i, "=")
val := ""
if len(queryParam) > 1 {
val = queryParam[1]
}
params.Add(queryParam[0], val)
}
baseURL.RawQuery = params.Encode()
}
return baseURL.String()
}

View File

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

23
cautils/jsonutils.go Normal file
View File

@@ -0,0 +1,23 @@
package cautils
import (
"bytes"
"encoding/json"
)
const (
empty = ""
tab = " "
)
func PrettyJson(data interface{}) ([]byte, error) {
buffer := new(bytes.Buffer)
encoder := json.NewEncoder(buffer)
encoder.SetIndent(empty, tab)
err := encoder.Encode(data)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}

View File

@@ -4,15 +4,15 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
// DO NOT REMOVE - load cloud providers auth
_ "k8s.io/client-go/plugin/pkg/client/auth"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
// K8SConfig pointer to k8s config
@@ -27,13 +27,16 @@ type KubernetesApi struct {
// NewKubernetesApi -
func NewKubernetesApi() *KubernetesApi {
kubernetesClient, err := kubernetes.NewForConfig(GetK8sConfig())
if err != nil {
panic(fmt.Sprintf("kubernetes.NewForConfig - Failed to load config file, reason: %s", err.Error()))
fmt.Printf("Failed to load config file, reason: %s", err.Error())
os.Exit(1)
}
dynamicClient, err := dynamic.NewForConfig(GetK8sConfig())
dynamicClient, err := dynamic.NewForConfig(K8SConfig)
if err != nil {
panic(fmt.Sprintf("dynamic.NewForConfig - Failed to load config file, reason: %s", err.Error()))
fmt.Printf("Failed to load config file, reason: %s", err.Error())
os.Exit(1)
}
return &KubernetesApi{
@@ -43,30 +46,29 @@ func NewKubernetesApi() *KubernetesApi {
}
}
var ConfigPath = filepath.Join(os.Getenv("HOME"), ".kube", "config")
// RunningIncluster whether running in cluster
var RunningIncluster bool
// LoadK8sConfig load config from local file or from cluster
func LoadK8sConfig() error {
kubeconfig, err := clientcmd.BuildConfigFromFlags("", ConfigPath)
kubeconfig, err := config.GetConfig()
if err != nil {
kubeconfig, err = restclient.InClusterConfig()
if err != nil {
return fmt.Errorf("Failed to load kubernetes config from file: '%s', err: %v", ConfigPath, err)
}
return fmt.Errorf("failed to load kubernetes config: %s\n", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
}
if _, err := restclient.InClusterConfig(); err == nil {
RunningIncluster = true
} else {
RunningIncluster = false
}
K8SConfig = kubeconfig
return nil
}
// GetK8sConfig get config. load if not loaded yer
// GetK8sConfig get config. load if not loaded yet
func GetK8sConfig() *restclient.Config {
if K8SConfig == nil {
if err := LoadK8sConfig(); err != nil {
return nil
// print error
fmt.Printf("%s", err.Error())
os.Exit(1)
}
}
return K8SConfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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:]...)
// // }
// }

View File

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

View File

@@ -3,7 +3,7 @@ package opapolicy
import (
"time"
armotypes "kube-escape/cautils/armotypes"
armotypes "github.com/armosec/kubescape/cautils/armotypes"
)
// Mock A
@@ -33,7 +33,8 @@ func MockFrameworkReportA() *FrameworkReport {
Name: AMockFrameworkName,
ControlReports: []ControlReport{
{
Name: AMockControlName,
ControlID: "testctrl",
Name: AMockControlName,
RuleReports: []RuleReport{
{
Name: AMockRuleName,

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"kube-escape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/golang/glog"
"github.com/open-policy-agent/opa/storage"
@@ -42,9 +42,12 @@ func NewRegoDependenciesDataMock() *RegoDependenciesData {
func NewRegoDependenciesData(k8sConfig *rest.Config) *RegoDependenciesData {
regoDependenciesData := RegoDependenciesData{
K8sConfig: *NewRegoK8sConfig(k8sConfig),
regoDependenciesData := RegoDependenciesData{}
if k8sConfig != nil {
regoDependenciesData.K8sConfig = *NewRegoK8sConfig(k8sConfig)
}
return &regoDependenciesData
}
func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
@@ -61,19 +64,9 @@ func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
token = fmt.Sprintf("Bearer %s", k8sConfig.BearerToken)
}
// crtFile := os.Getenv("KUBERNETES_CRT_PATH")
// if crtFile == "" {
// crtFile = k8sConfig.CAFile
// // crtFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
// }
// glog.Infof("===========================================================================")
// glog.Infof(fmt.Sprintf("%v", k8sConfig.String()))
// glog.Infof("===========================================================================")
regoK8sConfig := RegoK8sConfig{
Token: token,
Host: k8sConfig.Host,
Host: host,
CrtFile: k8sConfig.CAFile,
ClientCrtFile: k8sConfig.CertFile,
ClientKeyFile: k8sConfig.KeyFile,

82
cautils/scaninfo.go Normal file
View File

@@ -0,0 +1,82 @@
package cautils
import (
"path/filepath"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type ScanInfo struct {
Getters
PolicyIdentifier opapolicy.PolicyIdentifier
UseExceptions string
UseFrom string
UseDefault bool
Format string
Output string
ExcludedNamespaces string
InputPatterns []string
Silent bool
FailThreshold uint16
}
type Getters struct {
ExceptionsGetter getter.IPolicyGetter
PolicyGetter getter.IPolicyGetter
}
func (scanInfo *ScanInfo) Init() {
scanInfo.setUseFrom()
scanInfo.setUseExceptions()
scanInfo.setOutputFile()
scanInfo.setGetter()
}
func (scanInfo *ScanInfo) setUseExceptions() {
if scanInfo.UseExceptions != "" {
// load exceptions from file
scanInfo.ExceptionsGetter = getter.NewLoadPolicy(scanInfo.UseExceptions)
} else {
scanInfo.ExceptionsGetter = getter.NewArmoAPI()
}
}
func (scanInfo *ScanInfo) setUseFrom() {
if scanInfo.UseFrom != "" {
return
}
if scanInfo.UseDefault {
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name)
}
}
func (scanInfo *ScanInfo) setGetter() {
if scanInfo.UseFrom != "" {
// load from file
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
} else {
scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
}
}
func (scanInfo *ScanInfo) setOutputFile() {
if scanInfo.Output == "" {
return
}
if scanInfo.Format == "json" {
if filepath.Ext(scanInfo.Output) != "json" {
scanInfo.Output += ".json"
}
}
if scanInfo.Format == "junit" {
if filepath.Ext(scanInfo.Output) != "xml" {
scanInfo.Output += ".xml"
}
}
}
func (scanInfo *ScanInfo) ScanRunningCluster() bool {
return len(scanInfo.InputPatterns) == 0
}

45
cmd/download.go Normal file
View File

@@ -0,0 +1,45 @@
package cmd
import (
"fmt"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/spf13/cobra"
)
var downloadInfo cautils.DownloadInfo
var downloadCmd = &cobra.Command{
Use: "download framework <framework-name>",
Short: "Download framework controls",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("requires two arguments : framework <framework-name>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
downloadInfo.FrameworkName = args[1]
g := getter.NewDownloadReleasedPolicy()
if downloadInfo.Path == "" {
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName)
}
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
if err != nil {
return err
}
err = getter.SaveFrameworkInFile(frameworks, downloadInfo.Path)
if err != nil {
return err
}
return nil
},
}
func init() {
rootCmd.AddCommand(downloadCmd)
downloadInfo = cautils.DownloadInfo{}
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If specified, will store save to `~/.kubescape/<framework name>.json`")
}

169
cmd/framework.go Normal file
View File

@@ -0,0 +1,169 @@
package cmd
import (
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/opaprocessor"
"github.com/armosec/kubescape/policyhandler"
"github.com/armosec/kubescape/resultshandling"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/reporter"
"github.com/spf13/cobra"
)
var scanInfo cautils.ScanInfo
var supportedFrameworks = []string{"nsa"}
type CLIHandler struct {
policyHandler *policyhandler.PolicyHandler
scanInfo *cautils.ScanInfo
}
var frameworkCmd = &cobra.Command{
Use: "framework <framework name> [`<glob patter>`/`-`] [flags]",
Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(supportedFrameworks, ", ")),
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
ValidArgs: supportedFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
return fmt.Errorf("requires at least one argument")
} else if len(args) > 0 {
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
scanInfo.PolicyIdentifier = opapolicy.PolicyIdentifier{}
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
if !(cmd.Flags().Lookup("use-from").Changed) {
scanInfo.PolicyIdentifier.Name = args[0]
}
if len(args) > 0 {
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
} else { // store stout to file
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
return err
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
}
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
err := CliSetup()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return nil
},
}
func isValidFramework(framework string) bool {
return cautils.StringInSlice(supportedFrameworks, framework) != cautils.ValueNotFound
}
func init() {
scanCmd.AddCommand(frameworkCmd)
scanInfo = cautils.ScanInfo{}
frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Path to load framework from")
frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load framework from default path")
frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to file containing list of exceptions")
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout")
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages")
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1")
}
func CliSetup() error {
flag.Parse()
if 100 < scanInfo.FailThreshold {
fmt.Println("bad argument: out of range threshold")
os.Exit(1)
}
var k8s *k8sinterface.KubernetesApi
if scanInfo.ScanRunningCluster() {
k8s = k8sinterface.NewKubernetesApi()
}
processNotification := make(chan *cautils.OPASessionObj)
reportResults := make(chan *cautils.OPASessionObj)
// policy handler setup
policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
// cli handler setup
cli := NewCLIHandler(policyHandler)
if err := cli.Scan(); err != nil {
panic(err)
}
// processor setup - rego run
go func() {
opaprocessorObj := opaprocessor.NewOPAProcessorHandler(&processNotification, &reportResults)
opaprocessorObj.ProcessRulesListenner()
}()
resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
score := resultsHandling.HandleResults()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {
return fmt.Errorf("Scan score is bellow threshold")
}
return nil
}
func NewCLIHandler(policyHandler *policyhandler.PolicyHandler) *CLIHandler {
return &CLIHandler{
scanInfo: &scanInfo,
policyHandler: policyHandler,
}
}
func (clihandler *CLIHandler) Scan() error {
cautils.ScanStartDisplay()
policyNotification := &opapolicy.PolicyNotification{
NotificationType: opapolicy.TypeExecPostureScan,
Rules: []opapolicy.PolicyIdentifier{
clihandler.scanInfo.PolicyIdentifier,
},
Designators: armotypes.PortalDesignator{},
}
switch policyNotification.NotificationType {
case opapolicy.TypeExecPostureScan:
go func() {
if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
fmt.Printf("%v\n", err)
os.Exit(0)
}
}()
default:
return fmt.Errorf("notification type '%s' Unknown", policyNotification.NotificationType)
}
return nil
}

25
cmd/root.go Normal file
View File

@@ -0,0 +1,25 @@
package cmd
import (
"github.com/spf13/cobra"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "kubescape",
Short: "Kubescape is a tool for testing Kubernetes security posture",
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA specifications.`,
}
func Execute() {
rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
}

18
cmd/scan.go Normal file
View 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

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
View 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="&#39;DejaVu Sans&#39;,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="&#39;DejaVu Sans&#39;,Verdana,Geneva,sans-serif" font-size="11">
<path d="M0 0h46.939C48.629 0 50 1.343 50 3v14c0 1.657-1.37 3-3.061 3H0V0z" id="state-bg" fill="url(#state-fill)" fill-rule="nonzero"></path>
<text fill="#010101" fill-opacity=".3">
<tspan x="4" y="15">feature</tspan>
</text>
<text fill="#FFFFFF">
<tspan x="4" y="14">feature</tspan>
</text>
</g>
<path fill="#959DA5" d="M11 3c-3.868 0-7 3.132-7 7a6.996 6.996 0 0 0 4.786 6.641c.35.062.482-.148.482-.332 0-.166-.01-.718-.01-1.304-1.758.324-2.213-.429-2.353-.822-.079-.202-.42-.823-.717-.99-.245-.13-.595-.454-.01-.463.552-.009.946.508 1.077.718.63 1.058 1.636.76 2.039.577.061-.455.245-.761.446-.936-1.557-.175-3.185-.779-3.185-3.456 0-.762.271-1.392.718-1.882-.07-.175-.315-.892.07-1.855 0 0 .586-.183 1.925.718a6.5 6.5 0 0 1 1.75-.236 6.5 6.5 0 0 1 1.75.236c1.338-.91 1.925-.718 1.925-.718.385.963.14 1.68.07 1.855.446.49.717 1.112.717 1.882 0 2.686-1.636 3.28-3.194 3.456.254.219.473.639.473 1.295 0 .936-.009 1.689-.009 1.925 0 .184.131.402.481.332A7.011 7.011 0 0 0 18 10c0-3.867-3.133-7-7-7z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

91
docs/release.md Normal file
View File

@@ -0,0 +1,91 @@
# Kubescape Release
## Input
### Scan a running Kubernetes cluster
* Scan your Kubernetes cluster. Ignore `kube-system` and `kube-public` namespaces
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
* Scan your Kubernetes cluster
```
kubescape scan framework nsa
```
### Scan a local Kubernetes manifest
* Scan single Kubernetes manifest file <img src="new-feature.svg">
```
kubescape scan framework nsa <my-workload.yaml>
```
* Scan many Kubernetes manifest files <img src="new-feature.svg">
```
kubescape scan framework nsa <my-workload-1.yaml> <my-workload-2.yaml>
```
* Scan all Kubernetes manifest files in directory <img src="new-feature.svg">
```
kubescape scan framework nsa *.yaml
```
* Scan Kubernetes manifest from stdout <img src="new-feature.svg">
```
cat <my-workload.yaml> | kubescape scan framework nsa -
```
* Scan Kubernetes manifest url <img src="new-feature.svg">
```
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
```
### Scan HELM chart
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout <img src="new-feature.svg">
```
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
```
### Scan on-prem (offline)
* Scan using a framework from the local file system
```
kubescape scan framework --use-from <path>
```
* Scan using the framework from the default location in file system
```
kubescape scan framework --use-default
```
## Output formats
By default, the output is user friendly.
For the sake of automation, it is possible to receive the result in a `json` or `junit xml` format.
* Output in `json` format <img src="new-feature.svg">
```
kubescape scan framework nsa --format json --output results.json
```
* Output in `junit xml` format <img src="new-feature.svg">
```
kubescape scan framework nsa --format junit --output results.xml
```
## Download
* Download and save in file <img src="new-feature.svg">
```
kubescape download framework nsa --output nsa.json
```
* Download and save in default file (`~/.kubescape/<framework name>.json`)
```
kubescape download framework nsa
```

BIN
docs/summary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/using-mov.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

5
examples/example.md Normal file
View File

@@ -0,0 +1,5 @@
#! /bin/bash
echo "Testing Online Boutique yamls (https://github.com/GoogleCloudPlatform/microservices-demo)"
kubescape scan framework nsa online-boutique/*

34
examples/exceptions.json Normal file
View File

@@ -0,0 +1,34 @@
[
{
"name": "ignore-kube-namespaces",
"policyType": "postureExceptionPolicy",
"actions": [
"alertOnly"
],
"resources": [
{
"designatorType": "Attributes",
"attributes": {
"namespace": "kube-system"
}
},
{
"designatorType": "Attributes",
"attributes": {
"namespace": "kube-public"
}
},
{
"designatorType": "Attributes",
"attributes": {
"namespace": "kube-node-lease"
}
}
],
"posturePolicies": [
{
"frameworkName": "NSA"
}
]
}
]

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

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

View File

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

View File

@@ -1 +0,0 @@
package clihandler

View File

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

View File

@@ -6,23 +6,39 @@ echo
BASE_DIR=~/.kubescape
KUBESCAPE_EXEC=kubescape
RELEASE=v0.0.12
DOWNLOAD_URL="https://github.com/armosec/kubescape/releases/download/$RELEASE/kubescape"
osName=$(uname -s)
if [[ $osName == *"MINGW"* ]]; then
osName=windows-latest
elif [[ $osName == *"Darwin"* ]]; then
osName=macos-latest
else
osName=ubuntu-latest
fi
GITHUB_OWNER=armosec
DOWNLOAD_URL=$(curl --silent "https://api.github.com/repos/$GITHUB_OWNER/kubescape/releases/latest" | grep -o "browser_download_url.*${osName}.*")
DOWNLOAD_URL=${DOWNLOAD_URL//\"}
DOWNLOAD_URL=${DOWNLOAD_URL/browser_download_url: /}
mkdir -p $BASE_DIR
OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
curl -sL $DOWNLOAD_URL -o $OUTPUT
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
echo -e "\033[32m[V] Downloaded Kubescape"
sudo chmod +x $OUTPUT
sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
sudo cp $OUTPUT /usr/local/bin
rm -rf $BASE_DIR
# Ping download counter
curl --silent https://us-central1-elated-pottery-310110.cloudfunctions.net/kubescape-download-counter -o /dev/null
chmod +x $OUTPUT || sudo chmod +x $OUTPUT
rm -f /usr/local/bin/$KUBESCAPE_EXEC || sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
cp $OUTPUT /usr/local/bin || sudo cp $OUTPUT /usr/local/bin
rm -rf $OUTPUT
echo -e "[V] Finished Installation"
echo
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa"
echo -e "\033[35m Usage: $ $KUBESCAPE_EXEC scan framework nsa --exclude-namespaces kube-system,kube-public"
echo

47
main.go
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -0,0 +1,270 @@
package policyhandler
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"gopkg.in/yaml.v2"
)
var (
YAML_PREFIX = []string{".yaml", ".yml"}
JSON_PREFIX = []string{".json"}
)
type FileFormat string
const (
YAML_FILE_FORMAT FileFormat = "yaml"
JSON_FILE_FORMAT FileFormat = "json"
)
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
workloads := []k8sinterface.IWorkload{}
// load resource from local file system
w, err := loadResourcesFromFiles(scanInfo.InputPatterns)
if err != nil {
return nil, err
}
if w != nil {
workloads = append(workloads, w...)
}
// load resources from url
w, err = loadResourcesFromUrl(scanInfo.InputPatterns)
if err != nil {
return nil, err
}
if w != nil {
workloads = append(workloads, w...)
}
if len(workloads) == 0 {
return nil, fmt.Errorf("empty list of workloads - no workloads found")
}
// map all resources: map["/group/version/kind"][]<k8s workloads>
allResources := mapResources(workloads)
// build resources map
// map resources based on framework required resources: map["/group/version/kind"][]<k8s workloads>
k8sResources := setResourceMap(frameworks)
// save only relevant resources
for i := range allResources {
if _, ok := (*k8sResources)[i]; ok {
(*k8sResources)[i] = allResources[i]
}
}
return k8sResources, nil
}
func loadResourcesFromFiles(inputPatterns []string) ([]k8sinterface.IWorkload, error) {
files, errs := listFiles(inputPatterns)
if len(errs) > 0 {
cautils.ErrorDisplay(fmt.Sprintf("%v", errs)) // TODO - print error
}
if len(files) == 0 {
return nil, nil
}
workloads, errs := loadFiles(files)
if len(errs) > 0 {
cautils.ErrorDisplay(fmt.Sprintf("%v", errs)) // TODO - print error
}
return workloads, nil
}
// build resources map
func mapResources(workloads []k8sinterface.IWorkload) map[string][]map[string]interface{} {
allResources := map[string][]map[string]interface{}{}
for i := range workloads {
groupVersionResource, err := k8sinterface.GetGroupVersionResource(workloads[i].GetKind())
if err != nil {
// TODO - print warning
continue
}
if groupVersionResource.Group != workloads[i].GetGroup() || groupVersionResource.Version != workloads[i].GetVersion() {
// TODO - print warning
continue
}
resourceTriplets := k8sinterface.JoinResourceTriplets(groupVersionResource.Group, groupVersionResource.Version, groupVersionResource.Resource)
if r, ok := allResources[resourceTriplets]; ok {
r = append(r, workloads[i].GetWorkload())
allResources[resourceTriplets] = r
} else {
allResources[resourceTriplets] = []map[string]interface{}{workloads[i].GetWorkload()}
}
}
return allResources
}
func loadFiles(filePaths []string) ([]k8sinterface.IWorkload, []error) {
workloads := []k8sinterface.IWorkload{}
errs := []error{}
for i := range filePaths {
f, err := loadFile(filePaths[i])
if err != nil {
errs = append(errs, err)
continue
}
w, e := readFile(f, getFileFormat(filePaths[i]))
errs = append(errs, e...)
if w != nil {
workloads = append(workloads, w...)
}
}
return workloads, errs
}
func loadFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
}
func readFile(fileContent []byte, fileFromat FileFormat) ([]k8sinterface.IWorkload, []error) {
switch fileFromat {
case YAML_FILE_FORMAT:
return readYamlFile(fileContent)
case JSON_FILE_FORMAT:
return readJsonFile(fileContent)
default:
return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)}
}
}
func listFiles(patterns []string) ([]string, []error) {
files := []string{}
errs := []error{}
for i := range patterns {
if strings.HasPrefix(patterns[i], "http") {
continue
}
if !filepath.IsAbs(patterns[i]) {
o, _ := os.Getwd()
patterns[i] = filepath.Join(o, patterns[i])
}
f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i])
if err != nil {
errs = append(errs, err)
} else {
files = append(files, f...)
}
}
return files, errs
}
func readYamlFile(yamlFile []byte) ([]k8sinterface.IWorkload, []error) {
errs := []error{}
r := bytes.NewReader(yamlFile)
dec := yaml.NewDecoder(r)
yamlObjs := []k8sinterface.IWorkload{}
var t interface{}
for dec.Decode(&t) == nil {
j := convertYamlToJson(t)
if j == nil {
continue
}
if obj, ok := j.(map[string]interface{}); ok {
yamlObjs = append(yamlObjs, k8sinterface.NewWorkloadObj(obj))
} else {
errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j))
}
}
return yamlObjs, errs
}
func readJsonFile(jsonFile []byte) ([]k8sinterface.IWorkload, []error) {
workloads := []k8sinterface.IWorkload{}
var jsonObj interface{}
if err := json.Unmarshal(jsonFile, &jsonObj); err != nil {
return workloads, []error{err}
}
convertJsonToWorkload(jsonObj, &workloads)
return workloads, nil
}
func convertJsonToWorkload(jsonObj interface{}, workloads *[]k8sinterface.IWorkload) {
switch x := jsonObj.(type) {
case map[string]interface{}:
(*workloads) = append(*workloads, k8sinterface.NewWorkloadObj(x))
case []interface{}:
for i := range x {
convertJsonToWorkload(x[i], workloads)
}
}
}
func convertYamlToJson(i interface{}) interface{} {
switch x := i.(type) {
case map[interface{}]interface{}:
m2 := map[string]interface{}{}
for k, v := range x {
if s, ok := k.(string); ok {
m2[s] = convertYamlToJson(v)
}
}
return m2
case []interface{}:
for i, v := range x {
x[i] = convertYamlToJson(v)
}
}
return i
}
func glob(root, pattern string) ([]string, error) {
var matches []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil {
return err
} else if matched {
matches = append(matches, path)
}
return nil
})
if err != nil {
return nil, err
}
return matches, nil
}
func isYaml(filePath string) bool {
return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
}
func isJson(filePath string) bool {
return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound
}
func getFileFormat(filePath string) FileFormat {
if isYaml(filePath) {
return YAML_FILE_FORMAT
} else if isJson(filePath) {
return JSON_FILE_FORMAT
} else {
return FileFormat(filePath)
}
}

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

View File

@@ -2,13 +2,13 @@ package policyhandler
import (
"fmt"
"kube-escape/cautils"
"kube-escape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils"
"kube-escape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/golang/glog"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// PolicyHandler -
@@ -16,6 +16,7 @@ type PolicyHandler struct {
k8s *k8sinterface.KubernetesApi
// we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func
processPolicy *chan *cautils.OPASessionObj
getters *cautils.Getters
}
// CreatePolicyHandler Create ws-handler obj
@@ -26,37 +27,29 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
}
}
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification) error {
glog.Infof("Processing notification. reportID: %s", notification.ReportID)
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *cautils.ScanInfo) error {
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
// validate notification
// TODO
policyHandler.getters = &scanInfo.Getters
// get policies
glog.Infof(fmt.Sprintf("Getting %d policies from backend. reportID: %s", len(notification.Rules), notification.ReportID))
cautils.ProgressTextDisplay("Downloading framework definitions")
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
frameworks, exceptions, err := policyHandler.getPolicies(notification)
if err != nil {
return err
}
if len(frameworks) == 0 {
err := fmt.Errorf("Could not download any policies, please check previous logs")
return err
return fmt.Errorf("empty list of frameworks")
}
opaSessionObj.Frameworks = frameworks
cautils.SuccessTextDisplay("Downloaded framework")
// store policies as configmaps
// TODO
opaSessionObj.Exceptions = exceptions
// get k8s resources
cautils.ProgressTextDisplay("Accessing Kubernetes objects")
glog.Infof(fmt.Sprintf("Getting kubernetes objects. reportID: %s", notification.ReportID))
k8sResources, err := policyHandler.getK8sResources(frameworks, &notification.Designators)
if err != nil || len(*k8sResources) == 0 {
glog.Error(err)
} else {
cautils.SuccessTextDisplay("Accessed successfully to Kubernetes objects, lets start!!!")
k8sResources, err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
if err != nil {
return err
}
if k8sResources == nil || len(*k8sResources) == 0 {
return fmt.Errorf("empty list of resources")
}
opaSessionObj.K8SResources = k8sResources
@@ -64,3 +57,33 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
*policyHandler.processPolicy <- opaSessionObj
return nil
}
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
frameworks, exceptions, err := policyHandler.GetPoliciesFromBackend(notification)
if err != nil {
return frameworks, exceptions, err
}
if len(frameworks) == 0 {
err := fmt.Errorf("could not download any policies, please check previous logs")
return frameworks, exceptions, err
}
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
return frameworks, exceptions, nil
}
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
var k8sResources *cautils.K8SResources
var err error
if scanInfo.ScanRunningCluster() {
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, &notification.Designators, scanInfo.ExcludedNamespaces)
} else {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
}
return k8sResources, err
}

View File

@@ -1,162 +1,50 @@
package policyhandler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"kube-escape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// URLEncoder encode url
func URLEncoder(oldURL string) string {
fullURL := strings.Split(oldURL, "?")
baseURL, err := url.Parse(fullURL[0])
if err != nil {
return ""
}
// Prepare Query Parameters
if len(fullURL) > 1 {
params := url.Values{}
queryParams := strings.Split(fullURL[1], "&")
for _, i := range queryParams {
queryParam := strings.Split(i, "=")
val := ""
if len(queryParam) > 1 {
val = queryParam[1]
}
params.Add(queryParam[0], val)
}
baseURL.RawQuery = params.Encode()
}
return baseURL.String()
}
type IArmoAPI interface {
OPAFRAMEWORKGet(string) ([]opapolicy.Framework, error)
}
type ArmoAPI struct {
httpClient *http.Client
hostURL string
}
func NewArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{},
hostURL: "https://dashbe.eudev3.cyberarmorsoft.com",
}
}
func (db *ArmoAPI) GetServerAddress() string {
return db.hostURL
}
func (db *ArmoAPI) GetHttpClient() *http.Client {
return db.httpClient
}
func (db *ArmoAPI) OPAFRAMEWORKGet(name string) ([]opapolicy.Framework, error) {
requestURI := "v1/armoFrameworks"
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
requestURI += fmt.Sprintf("&frameworkName=%s", name)
requestURI += "&getRules=true"
fullURL := URLEncoder(fmt.Sprintf("%s/%s", db.GetServerAddress(), requestURI))
frameworkList := []opapolicy.Framework{}
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return frameworkList, err
}
c := http.Client{}
resp, err := c.Do(req)
if err != nil {
return frameworkList, err
}
respStr, err := HTTPRespToString(resp)
if err != nil {
return frameworkList, err
}
if name != "" {
frameworkSingle := opapolicy.Framework{}
err = JSONDecoder(respStr).Decode(&frameworkSingle)
frameworkList = append(frameworkList, frameworkSingle)
} else {
err = JSONDecoder(respStr).Decode(&frameworkList)
}
return frameworkList, err
}
// JSONDecoder returns JSON decoder for given string
func JSONDecoder(origin string) *json.Decoder {
dec := json.NewDecoder(strings.NewReader(origin))
dec.UseNumber()
return dec
}
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
func HTTPRespToString(resp *http.Response) (string, error) {
if resp == nil || resp.Body == nil {
return "", nil
}
strBuilder := strings.Builder{}
defer resp.Body.Close()
if resp.ContentLength > 0 {
strBuilder.Grow(int(resp.ContentLength))
}
bytesNum, err := io.Copy(&strBuilder, resp.Body)
respStr := strBuilder.String()
if err != nil {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
return respStr, err
}
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
var errs error
d := NewArmoAPI()
frameworks := []opapolicy.Framework{}
exceptionPolicies := []armotypes.PostureExceptionPolicy{}
// Get - cacli opa get
for _, rule := range notification.Rules {
switch rule.Kind {
case opapolicy.KindFramework:
// backend
receivedFrameworks, err := d.OPAFRAMEWORKGet(rule.Name)
receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
if err != nil {
errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
}
frameworks = append(frameworks, receivedFrameworks...)
case opapolicy.KindControl:
receivedControls := []opapolicy.Control{} //, err := policyHandler.cacli.OPAFRAMEWORKGet(rule.Name, !k8sinterface.RunningIncluster)
// receivedControls, err := policyHandler.cacli.OPACONTROLGet(rule.Name)
// if err != nil {
// errs = fmt.Errorf("%v\nKind: %v, Name: %s, error: %s", errs, rule.Kind, rule.Name, err.Error())
// }
framework := opapolicy.Framework{ // TODO - wrap control by framework properly
Controls: receivedControls,
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
if recExceptionPolicies != nil {
exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
}
}
frameworks = append(frameworks, framework)
default:
err := fmt.Errorf("missing rule kind, expected: %s", opapolicy.KindFramework)
errs = fmt.Errorf("%v\nerror: %s", errs, err.Error())
errs = fmt.Errorf("%s", err.Error())
}
}
return frameworks, errs
return frameworks, exceptionPolicies, errs
}
func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*opapolicy.Framework, []armotypes.PostureExceptionPolicy, error) {
receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(policyName)
if err != nil {
return nil, nil, err
}
receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions("", "")
if err != nil {
return receivedFramework, nil, err
}
return receivedFramework, receivedException, nil
}

View File

@@ -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, lets 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 {

View File

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

View File

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

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

View File

@@ -1,155 +0,0 @@
package printer
import (
"fmt"
"kube-escape/cautils"
"os"
"strings"
"kube-escape/cautils/k8sinterface"
"kube-escape/cautils/opapolicy"
"github.com/enescakir/emoji"
"github.com/golang/glog"
"github.com/olekukonko/tablewriter"
)
var INDENT = " "
type Printer struct {
opaSessionObj *chan *cautils.OPASessionObj
summery Summery
}
func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj) *Printer {
return &Printer{
opaSessionObj: opaSessionObj,
summery: NewSummery(),
}
}
func (printer *Printer) ActionPrint() {
// recover
defer func() {
if err := recover(); err != nil {
glog.Errorf("RECOVER in ActionSendReportListenner, reason: %v", err)
}
}()
for {
opaSessionObj := <-*printer.opaSessionObj
printer.SummerySetup(opaSessionObj.PostureReport)
printer.PrintResults()
printer.PrintSummaryTable()
if !k8sinterface.RunningIncluster {
break
}
}
}
func (printer *Printer) SummerySetup(postureReport *opapolicy.PostureReport) {
for _, fr := range postureReport.FrameworkReports {
for _, cr := range fr.ControlReports {
if len(cr.RuleReports) == 0 {
continue
}
workloadsSummery := listResultSummery(cr.RuleReports)
mapResources := groupByNamespace(workloadsSummery)
printer.summery[cr.Name] = ControlSummery{
TotalResources: cr.GetNumberOfResources(),
TotalFailed: len(workloadsSummery),
WorkloadSummery: mapResources,
Description: strings.ReplaceAll(cr.Description, ". ", fmt.Sprintf(".\n%s%s", INDENT, INDENT)),
}
}
}
}
func (printer *Printer) PrintResults() {
for control, controlSummery := range printer.summery {
printer.printTitle(control, &controlSummery)
printer.printResult(control, &controlSummery)
}
}
func (printer *Printer) printTitle(controlName string, controlSummery *ControlSummery) {
cautils.InfoDisplay(os.Stdout, "[control: %s] ", controlName)
if controlSummery.TotalResources == 0 {
cautils.InfoDisplay(os.Stdout, "resources not found %v\n", emoji.ConfusedFace)
} else if controlSummery.TotalFailed == 0 {
cautils.SuccessDisplay(os.Stdout, "passed %v\n", emoji.ThumbsUp)
} else {
cautils.FailureDisplay(os.Stdout, "failed %v\n", emoji.SadButRelievedFace)
}
cautils.SimpleDisplay(os.Stdout, "%sDescription: %s\n", INDENT, controlSummery.Description)
}
func (printer *Printer) printResult(controlName string, controlSummery *ControlSummery) {
indent := INDENT
for ns, rsc := range controlSummery.WorkloadSummery {
preIndent := indent
indent += indent
cautils.SimpleDisplay(os.Stdout, "%sNamespace %s\n", indent, ns)
preIndent2 := indent
for r := range rsc {
indent += indent
cautils.SimpleDisplay(os.Stdout, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
indent = preIndent2
}
indent = preIndent
}
}
func generateRow(control string, cs ControlSummery) []string {
row := []string{control}
row = append(row, cs.ToSlice()...)
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
return row
}
func generateHeader() []string {
return []string{"Control Name", "Failed Resources", "All Resources", "% success"}
}
func percentage(big, small int) int {
if big == 0 {
if small == 0 {
return 100
}
return 0
}
return int(float64(float64(big-small)/float64(big)) * 100)
}
func generateFooter(numControlers, sumFailed, sumTotal int) []string {
// Control name | # failed resources | all resources | % success
row := []string{}
row = append(row, fmt.Sprintf("%d", numControlers))
row = append(row, fmt.Sprintf("%d", sumFailed))
row = append(row, fmt.Sprintf("%d", sumTotal))
row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
return row
}
func (printer *Printer) PrintSummaryTable() {
summaryTable := tablewriter.NewWriter(os.Stdout)
summaryTable.SetAutoWrapText(false)
summaryTable.SetHeader(generateHeader())
summaryTable.SetHeaderLine(true)
summaryTable.SetAlignment(tablewriter.ALIGN_LEFT)
sumTotal := 0
sumFailed := 0
for k, v := range printer.summery {
summaryTable.Append(generateRow(k, v))
sumFailed += v.TotalFailed
sumTotal += v.TotalResources
}
summaryTable.SetFooter(generateFooter(len(printer.summery), sumFailed, sumTotal))
summaryTable.Render()
}

View File

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

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

View File

@@ -0,0 +1,261 @@
package printer
import (
"encoding/json"
"encoding/xml"
"fmt"
"os"
"sort"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/enescakir/emoji"
"github.com/olekukonko/tablewriter"
)
var INDENT = " "
const EmptyPercentage = "NaN"
const (
PrettyPrinter string = "pretty-printer"
JsonPrinter string = "json"
JunitResultPrinter string = "junit"
)
type Printer struct {
writer *os.File
summary Summary
sortedControlNames []string
printerType string
}
func NewPrinter(printerType, outputFile string) *Printer {
return &Printer{
summary: NewSummary(),
writer: getWriter(outputFile),
printerType: printerType,
}
}
func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
totalResources := 0
totalFailed := 0
for _, frameworkReport := range postureReport.FrameworkReports {
for _, controlReport := range frameworkReport.ControlReports {
for _, ruleReport := range controlReport.RuleReports {
for _, ruleResponses := range ruleReport.RuleResponses {
totalFailed += len(ruleResponses.AlertObject.K8SApiObjects)
totalFailed += len(ruleResponses.AlertObject.ExternalObjects)
}
}
totalResources += controlReport.GetNumberOfResources()
}
}
if totalResources == 0 {
return float32(0)
}
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
}
func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 {
var score float32
if printer.printerType == PrettyPrinter {
printer.SummarySetup(opaSessionObj.PostureReport)
printer.PrintResults()
printer.PrintSummaryTable()
} else if printer.printerType == JsonPrinter {
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
printer.writer.Write(postureReportStr)
} else if printer.printerType == JunitResultPrinter {
junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
fmt.Println("Failed to convert posture report object!")
os.Exit(1)
}
printer.writer.Write(postureReportStr)
} else if !cautils.IsSilent() {
fmt.Println("unknown output printer")
os.Exit(1)
}
score = calculatePostureScore(opaSessionObj.PostureReport)
return score
}
func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
for _, fr := range postureReport.FrameworkReports {
for _, cr := range fr.ControlReports {
if len(cr.RuleReports) == 0 {
continue
}
workloadsSummary := listResultSummary(cr.RuleReports)
mapResources := groupByNamespace(workloadsSummary)
printer.summary[cr.Name] = ControlSummary{
TotalResources: cr.GetNumberOfResources(),
TotalFailed: cr.GetNumberOfFailedResources(),
TotalWarnign: cr.GetNumberOfWarningResources(),
WorkloadSummary: mapResources,
Description: cr.Description,
Remediation: cr.Remediation,
ListInputKinds: cr.ListControlsInputKinds(),
}
}
}
printer.sortedControlNames = printer.getSortedControlsNames()
}
func (printer *Printer) PrintResults() {
for i := 0; i < len(printer.sortedControlNames); i++ {
controlSummary := printer.summary[printer.sortedControlNames[i]]
printer.printTitle(printer.sortedControlNames[i], &controlSummary)
printer.printResult(printer.sortedControlNames[i], &controlSummary)
if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 {
printer.printSummary(printer.sortedControlNames[i], &controlSummary)
}
}
}
func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
cautils.SimpleDisplay(printer.writer, "Summary - ")
cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed)
cautils.WarningDisplay(printer.writer, "Warning:%v ", controlSummary.TotalWarnign)
cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
if controlSummary.TotalFailed > 0 {
cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation)
}
cautils.DescriptionDisplay(printer.writer, "\n")
}
func (printer *Printer) printTitle(controlName string, controlSummary *ControlSummary) {
cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName)
if controlSummary.TotalResources == 0 && len(controlSummary.ListInputKinds) > 0 {
cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace)
} else if controlSummary.TotalFailed != 0 {
cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
} else if controlSummary.TotalWarnign != 0 {
cautils.WarningDisplay(printer.writer, "warning %v\n", emoji.NeutralFace)
} else {
cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
}
cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
}
func (printer *Printer) printResult(controlName string, controlSummary *ControlSummary) {
indent := INDENT
for ns, rsc := range controlSummary.WorkloadSummary {
preIndent := indent
if ns != "" {
cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns)
}
preIndent2 := indent
for r := range rsc {
indent += indent
cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
indent = preIndent2
}
indent = preIndent
}
}
func generateRow(control string, cs ControlSummary) []string {
row := []string{control}
row = append(row, cs.ToSlice()...)
if cs.TotalResources != 0 {
row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
} else {
row = append(row, EmptyPercentage)
}
return row
}
func generateHeader() []string {
return []string{"Control Name", "Failed Resources", "Warning Resources", "All Resources", "% success"}
}
func percentage(big, small int) int {
if big == 0 {
if small == 0 {
return 100
}
return 0
}
return int(float64(float64(big-small)/float64(big)) * 100)
}
func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string {
// Control name | # failed resources | all resources | % success
row := []string{}
row = append(row, fmt.Sprintf("%d", numControlers))
row = append(row, fmt.Sprintf("%d", sumFailed))
row = append(row, fmt.Sprintf("%d", sumWarning))
row = append(row, fmt.Sprintf("%d", sumTotal))
if sumTotal != 0 {
row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
} else {
row = append(row, EmptyPercentage)
}
return row
}
func (printer *Printer) PrintSummaryTable() {
summaryTable := tablewriter.NewWriter(printer.writer)
summaryTable.SetAutoWrapText(false)
summaryTable.SetHeader(generateHeader())
summaryTable.SetHeaderLine(true)
summaryTable.SetAlignment(tablewriter.ALIGN_LEFT)
sumTotal := 0
sumFailed := 0
sumWarning := 0
for i := 0; i < len(printer.sortedControlNames); i++ {
controlSummary := printer.summary[printer.sortedControlNames[i]]
summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
sumFailed += controlSummary.TotalFailed
sumWarning += controlSummary.TotalWarnign
sumTotal += controlSummary.TotalResources
}
summaryTable.SetFooter(generateFooter(len(printer.summary), sumFailed, sumWarning, sumTotal))
summaryTable.Render()
}
func (printer *Printer) getSortedControlsNames() []string {
controlNames := make([]string, 0, len(printer.summary))
for k := range printer.summary {
controlNames = append(controlNames, k)
}
sort.Strings(controlNames)
return controlNames
}
func getWriter(outputFile string) *os.File {
if outputFile != "" {
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Println("Error opening file")
return os.Stdout
}
return f
}
return os.Stdout
}

View File

@@ -0,0 +1,43 @@
package printer
import (
"fmt"
"github.com/armosec/kubescape/cautils/armotypes"
)
type Summary map[string]ControlSummary
func NewSummary() Summary {
return make(map[string]ControlSummary)
}
type ControlSummary struct {
TotalResources int
TotalFailed int
TotalWarnign int
Description string
Remediation string
ListInputKinds []string
WorkloadSummary map[string][]WorkloadSummary // <namespace>:[<WorkloadSummary>]
}
type WorkloadSummary struct {
Kind string
Name string
Namespace string
Group string
Exception *armotypes.PostureExceptionPolicy
}
func (controlSummary *ControlSummary) ToSlice() []string {
s := []string{}
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarnign))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
return s
}
func (workloadSummary *WorkloadSummary) ToString() string {
return fmt.Sprintf("/%s/%s/%s/%s", workloadSummary.Group, workloadSummary.Namespace, workloadSummary.Kind, workloadSummary.Name)
}

View File

@@ -3,30 +3,30 @@ package printer
import (
"fmt"
"kube-escape/cautils/k8sinterface"
"kube-escape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// Group workloads by namespace - return {"namespace": <[]WorkloadSummery>}
func groupByNamespace(resources []WorkloadSummery) map[string][]WorkloadSummery {
mapResources := make(map[string][]WorkloadSummery)
// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>}
func groupByNamespace(resources []WorkloadSummary) map[string][]WorkloadSummary {
mapResources := make(map[string][]WorkloadSummary)
for i := range resources {
if r, ok := mapResources[resources[i].Namespace]; ok {
r = append(r, resources[i])
mapResources[resources[i].Namespace] = r
} else {
mapResources[resources[i].Namespace] = []WorkloadSummery{resources[i]}
mapResources[resources[i].Namespace] = []WorkloadSummary{resources[i]}
}
}
return mapResources
}
func listResultSummery(ruleReports []opapolicy.RuleReport) []WorkloadSummery {
workloadsSummery := []WorkloadSummery{}
func listResultSummary(ruleReports []opapolicy.RuleReport) []WorkloadSummary {
workloadsSummary := []WorkloadSummary{}
track := map[string]bool{}
for c := range ruleReports {
for _, ruleReport := range ruleReports[c].RuleResponses {
resource, err := ruleResultSummery(ruleReport.AlertObject)
resource, err := ruleResultSummary(ruleReport.AlertObject)
if err != nil {
fmt.Println(err.Error())
continue
@@ -34,31 +34,33 @@ func listResultSummery(ruleReports []opapolicy.RuleReport) []WorkloadSummery {
// add resource only once
for i := range resource {
resource[i].Exception = ruleReport.Exception
if ok := track[resource[i].ToString()]; !ok {
track[resource[i].ToString()] = true
workloadsSummery = append(workloadsSummery, resource[i])
workloadsSummary = append(workloadsSummary, resource[i])
}
}
}
}
return workloadsSummery
return workloadsSummary
}
func ruleResultSummery(obj opapolicy.AlertObject) ([]WorkloadSummery, error) {
resource := []WorkloadSummery{}
func ruleResultSummary(obj opapolicy.AlertObject) ([]WorkloadSummary, error) {
resource := []WorkloadSummary{}
for i := range obj.K8SApiObjects {
r, err := newWorkloadSummery(obj.K8SApiObjects[i])
r, err := newWorkloadSummary(obj.K8SApiObjects[i])
if err != nil {
return resource, err
}
resource = append(resource, *r)
}
return resource, nil
}
func newWorkloadSummery(obj map[string]interface{}) (*WorkloadSummery, error) {
r := &WorkloadSummery{}
func newWorkloadSummary(obj map[string]interface{}) (*WorkloadSummary, error) {
r := &WorkloadSummary{}
workload := k8sinterface.NewWorkloadObj(obj)
if workload == nil {

View File

@@ -0,0 +1,56 @@
package reporter
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type ReportEventReceiver struct {
httpClient http.Client
host url.URL
}
func NewReportEventReceiver() *ReportEventReceiver {
hostURL := initEventReceiverURL()
return &ReportEventReceiver{
httpClient: http.Client{},
host: *hostURL,
}
}
func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *cautils.OPASessionObj) {
if cautils.CustomerGUID == "" {
return
}
if err := report.Send(opaSessionObj.PostureReport); err != nil {
fmt.Println(err)
}
}
func (report *ReportEventReceiver) Send(postureReport *opapolicy.PostureReport) error {
reqBody, err := json.Marshal(*postureReport)
if err != nil {
return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err)
}
host := hostToString(&report.host, postureReport.ReportID)
req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody))
if err != nil {
return fmt.Errorf("in 'Send', http.NewRequest failed, host: %s, reason: %v", host, err)
}
res, err := report.httpClient.Do(req)
if err != nil {
return fmt.Errorf("httpClient.Do failed: %v", err)
}
msg, err := httpRespToString(res)
if err != nil {
return fmt.Errorf("%s, %v:%s", host, err, msg)
}
return err
}

View File

@@ -0,0 +1,57 @@
package reporter
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/gofrs/uuid"
)
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
func httpRespToString(resp *http.Response) (string, error) {
if resp == nil || resp.Body == nil {
return "", nil
}
strBuilder := strings.Builder{}
defer resp.Body.Close()
if resp.ContentLength > 0 {
strBuilder.Grow(int(resp.ContentLength))
}
_, err := io.Copy(&strBuilder, resp.Body)
if err != nil {
return strBuilder.String(), err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
err = fmt.Errorf("response status: %d. Content: %s", resp.StatusCode, strBuilder.String())
}
return strBuilder.String(), err
}
func initEventReceiverURL() *url.URL {
urlObj := url.URL{}
urlObj.Scheme = "https"
urlObj.Host = "report.euprod1.cyberarmorsoft.com"
urlObj.Path = "/k8s/postureReport"
q := urlObj.Query()
q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
q.Add("clusterName", cautils.ClusterName)
urlObj.RawQuery = q.Encode()
return &urlObj
}
func hostToString(host *url.URL, reportID string) string {
q := host.Query()
if reportID != "" {
q.Add("reportID", reportID) // TODO - do we add the reportID?
}
host.RawQuery = q.Encode()
return host.String()
}

View File

@@ -0,0 +1,20 @@
package reporter
import (
"net/url"
"testing"
)
func TestHostToString(t *testing.T) {
host := url.URL{
Scheme: "https",
Host: "report.eudev3.cyberarmorsoft.com",
Path: "k8srestapi/v1/postureReport",
RawQuery: "cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af",
}
expectedHost := "https://report.eudev3.cyberarmorsoft.com/k8srestapi/v1/postureReport?cluster=openrasty_seal-7fvz&customerGUID=5d817063-096f-4d91-b39b-8665240080af&reportID=ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41"
receivedHost := hostToString(&host, "ffdd2a00-4dc8-4bf3-b97a-a6d4fd198a41")
if receivedHost != expectedHost {
t.Errorf("%s != %s", receivedHost, expectedHost)
}
}

View File

@@ -0,0 +1,32 @@
package resultshandling
import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/reporter"
)
type ResultsHandler struct {
opaSessionObj *chan *cautils.OPASessionObj
reporterObj *reporter.ReportEventReceiver
printerObj *printer.Printer
}
func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *reporter.ReportEventReceiver, printerObj *printer.Printer) *ResultsHandler {
return &ResultsHandler{
opaSessionObj: opaSessionObj,
reporterObj: reporterObj,
printerObj: printerObj,
}
}
func (resultsHandler *ResultsHandler) HandleResults() float32 {
opaSessionObj := <-*resultsHandler.opaSessionObj
resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
return score
}

View File

@@ -0,0 +1,136 @@
package exceptions
import (
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
"k8s.io/apimachinery/pkg/labels"
)
func ListRuleExceptions(exceptionPolicies []armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) []armotypes.PostureExceptionPolicy {
ruleExceptions := []armotypes.PostureExceptionPolicy{}
for i := range exceptionPolicies {
if ruleHasExceptions(&exceptionPolicies[i], frameworkName, controlName, ruleName) {
ruleExceptions = append(ruleExceptions, exceptionPolicies[i])
}
}
return ruleExceptions
}
func ruleHasExceptions(exceptionPolicy *armotypes.PostureExceptionPolicy, frameworkName, controlName, ruleName string) bool {
for _, posturePolicy := range exceptionPolicy.PosturePolicies {
if posturePolicy.FrameworkName == "" && posturePolicy.ControlName == "" && posturePolicy.RuleName == "" {
continue // empty policy -> ignore
}
if posturePolicy.FrameworkName != "" && posturePolicy.FrameworkName != frameworkName {
continue // policy does not match
}
if posturePolicy.ControlName != "" && posturePolicy.ControlName != controlName {
continue // policy does not match
}
if posturePolicy.RuleName != "" && posturePolicy.RuleName != ruleName {
continue // policy does not match
}
return true // policies match
}
return false
}
func AddExceptionsToRuleResponses(results []opapolicy.RuleResponse, ruleExceptions []armotypes.PostureExceptionPolicy) {
if len(ruleExceptions) == 0 {
return
}
for i := range results {
workloads := alertObjectToWorkloads(&results[i].AlertObject)
if len(workloads) == 0 {
continue
}
for w := range workloads {
if exception := getException(ruleExceptions, workloads[w]); exception != nil {
results[i].Exception = exception
}
}
results[i].RuleStatus = results[i].GetSingleResultStatus()
}
}
func alertObjectToWorkloads(obj *opapolicy.AlertObject) []k8sinterface.IWorkload {
resource := []k8sinterface.IWorkload{}
for i := range obj.K8SApiObjects {
r := k8sinterface.NewWorkloadObj(obj.K8SApiObjects[i])
if r == nil {
continue
}
resource = append(resource, r)
}
return resource
}
func getException(ruleExceptions []armotypes.PostureExceptionPolicy, workload k8sinterface.IWorkload) *armotypes.PostureExceptionPolicy {
for e := range ruleExceptions {
for _, resource := range ruleExceptions[e].Resources {
if hasException(&resource, workload) {
return &ruleExceptions[e] // TODO - return disable exception out of all exceptions
}
}
}
return nil
}
// compareMetadata - compare namespace and kind
func hasException(designator *armotypes.PortalDesignator, workload k8sinterface.IWorkload) bool {
cluster, namespace, kind, name, labels := designator.DigestPortalDesignator()
if cluster == "" && namespace == "" && kind == "" && name == "" && len(labels) == 0 {
return false // if designators are empty
}
// if cluster != "" && cluster != ClusterName { // TODO - where do we receive cluster name from?
// return false // cluster name does not match
// }
if namespace != "" && !compareNamespace(workload, namespace) {
return false // namespaces do not match
}
if kind != "" && !compareKind(workload, kind) {
return false // kinds do not match
}
if name != "" && !compareName(workload, name) {
return false // names do not match
}
if len(labels) > 0 && !compareLabels(workload, labels) {
return false // labels do not match
}
return true // no mismatch found -> the workload has an exception
}
func compareNamespace(workload k8sinterface.IWorkload, namespace string) bool {
if workload.GetKind() == "Namespace" {
return namespace == workload.GetName()
}
return namespace == workload.GetNamespace()
}
func compareKind(workload k8sinterface.IWorkload, kind string) bool {
return kind == workload.GetKind()
}
func compareName(workload k8sinterface.IWorkload, name string) bool {
return name == workload.GetName()
}
func compareLabels(workload k8sinterface.IWorkload, attributes map[string]string) bool {
workloadLabels := labels.Set(workload.GetLabels())
designators := labels.Set(attributes).AsSelector()
return designators.Matches(workloadLabels)
}

View File

@@ -0,0 +1,59 @@
package exceptions
import (
"testing"
"github.com/armosec/kubescape/cautils/armotypes"
)
func PostureExceptionPolicyDisableMock() *armotypes.PostureExceptionPolicy {
return &armotypes.PostureExceptionPolicy{}
}
func PostureExceptionPolicyAlertOnlyMock() *armotypes.PostureExceptionPolicy {
return &armotypes.PostureExceptionPolicy{
PortalBase: armotypes.PortalBase{
Name: "postureExceptionPolicyAlertOnlyMock",
},
PolicyType: "postureExceptionPolicy",
Actions: []armotypes.PostureExceptionPolicyActions{armotypes.AlertOnly},
Resources: []armotypes.PortalDesignator{
{
DesignatorType: armotypes.DesignatorAttributes,
Attributes: map[string]string{
armotypes.AttributeNamespace: "default",
armotypes.AttributeCluster: "unittest",
},
},
},
PosturePolicies: []armotypes.PosturePolicy{
{
FrameworkName: "MITRE",
},
},
}
}
func TestListRuleExceptions(t *testing.T) {
exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
if len(res1) != 1 {
t.Errorf("expecting 1 exception")
}
res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
if len(res2) != 0 {
t.Errorf("expecting 0 exception")
}
}
// func TestGetException(t *testing.T) {
// exceptionPolicies := []armotypes.PostureExceptionPolicy{*PostureExceptionPolicyAlertOnlyMock()}
// res1 := ListRuleExceptions(exceptionPolicies, "MITRE", "", "")
// if len(res1) != 1 {
// t.Errorf("expecting 1 exception")
// }
// res2 := ListRuleExceptions(exceptionPolicies, "", "hostPath mount", "")
// if len(res2) != 0 {
// t.Errorf("expecting 0 exception")
// }
// }

View File

@@ -0,0 +1,232 @@
{
"developer_framework": {
"Writable hostPath mount": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Compromised images in registry": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Exposed dashboard": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Network mapping": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access container service account": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access Kubelet API": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Cluster-admin binding": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Kubernetes CronJob": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"SSH server running inside container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Pod / container name similarity": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Cluster internal networking": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access Kubernetes dashboard": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Privileged container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"hostPath mount": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Instance Metadata API": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Applications credentials in configuration files": {
"baseScore": 1.0,
"improvementRatio": 1.0
}
},
"MITRE": {
"Writable hostPath mount": {
"baseScore": 8.0,
"improvementRatio": 0.5
},
"Sidecar injection": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Compromised images in registry": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access tiller endpoint": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Data Destruction": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Resource Hijacking": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access the Kubernetes API server": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Exposed dashboard": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Backdoor container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Network mapping": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Images from private registry": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Mount service principal": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access container service account": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Malicious admission controller (validating)": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access Kubelet API": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Vulnerable application": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Application exploit (RCE)": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Cluster-admin binding": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Kubernetes CronJob": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"SSH server running inside container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"List Kubernetes secrets": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Pod / container name similarity": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Cluster internal networking": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Exposed sensitive interfaces": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Bash/cmd inside container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Clear container logs": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Access Kubernetes dashboard": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"New container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Privileged container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"CoreDNS poisoning": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"hostPath mount": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Instance Metadata API": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Malicious admission controller (mutating)": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Exec into container": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Delete Kubernetes events": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Applications credentials in configuration files": {
"baseScore": 1.0,
"improvementRatio": 1.0
}
},
"NSA": {
"Control plane hardening": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Immutable container filesystem": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Non-root containers": {
"baseScore": 1.0,
"improvementRatio": 1.0
},
"Host PID/IPC privileges": {
"baseScore": 1.0,
"improvementRatio": 1.0
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
{
"pod": 1.0,
"service": 1.0,
"daemonset": 1.0,
"deployment": 1.0,
"replicaset": 1.1,
"statefulset": 1.0,
"job": 1.0,
"secret": 1.0,
"cronjob": 1.0,
"clusterrolebinding": 1.0,
"clusterrole": 1.0,
"rolebinding": 1.0,
"role": 1.0,
"networkpolicy": 1.0,
"controllerrevision": 1.0,
"namespace": 1.0,
"serviceaccount": 1.0,
"configmap": 1.0,
"node": 1.0
}

201
scapepkg/score/score.go Normal file
View File

@@ -0,0 +1,201 @@
package score
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
appsv1 "k8s.io/api/apps/v1"
// corev1 "k8s.io/api/core/v1"
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type ControlScoreWeights struct {
BaseScore float32 `json:"baseScore"`
RuntimeImprovementMultiplier float32 `json:"improvementRatio"`
}
type ScoreUtil struct {
ResourceTypeScores map[string]float32
FrameworksScore map[string]map[string]ControlScoreWeights
K8SApoObj *k8sinterface.KubernetesApi
configPath string
}
var postureScore *ScoreUtil
func (su *ScoreUtil) Calculate(frameworksReports []opapolicy.FrameworkReport) error {
for i := range frameworksReports {
su.CalculateFrameworkScore(&frameworksReports[i])
}
return nil
}
func (su *ScoreUtil) CalculateFrameworkScore(framework *opapolicy.FrameworkReport) error {
for i := range framework.ControlReports {
framework.WCSScore += su.ControlScore(&framework.ControlReports[i], framework.Name)
framework.Score += framework.ControlReports[i].Score
framework.ARMOImprovement += framework.ControlReports[i].ARMOImprovement
}
if framework.WCSScore > 0 {
framework.Score = (framework.Score * 100) / framework.WCSScore
framework.ARMOImprovement = (framework.ARMOImprovement * 100) / framework.WCSScore
}
return fmt.Errorf("unable to calculate score for framework %s due to bad wcs score", framework.Name)
}
/*
daemonset: daemonsetscore*#nodes
workloads: if replicas:
replicascore*workloadkindscore*#replicas
else:
regular
*/
func (su *ScoreUtil) resourceRules(resources []map[string]interface{}) float32 {
var weight float32 = 0
for _, v := range resources {
var score float32 = 0
wl := k8sinterface.NewWorkloadObj(v)
kind := ""
if wl != nil {
kind = strings.ToLower(wl.GetKind())
replicas := wl.GetReplicas()
score = su.ResourceTypeScores[kind]
if replicas > 1 {
score *= su.ResourceTypeScores["replicaset"] * float32(replicas)
}
} else {
epsilon := float32(0.00001)
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
kind = keys[0]
score = su.ResourceTypeScores[kind]
if score == 0.0 || (score > -1*epsilon && score < epsilon) {
score = 1
}
}
if kind == "daemonset" {
b, err := json.Marshal(v)
if err == nil {
dmnset := appsv1.DaemonSet{}
json.Unmarshal(b, &dmnset)
score *= float32(dmnset.Status.DesiredNumberScheduled)
}
}
weight += score
}
return weight
}
func (su *ScoreUtil) externalResourceConverter(rscs map[string]interface{}) []map[string]interface{} {
resources := make([]map[string]interface{}, 0)
for atype, v := range rscs {
resources = append(resources, map[string]interface{}{atype: v})
}
return resources
}
/*
ControlScore:
@input:
ctrlReport - opapolicy.ControlReport object, must contain down the line the Input resources and the output resources
frameworkName - calculate this control according to a given framework weights
ctrl.score = baseScore * SUM_resource (resourceWeight*min(#replicas*replicaweight,1)(nodes if daemonset)
returns control score ***for the input resources***
*/
func (su *ScoreUtil) ControlScore(ctrlReport *opapolicy.ControlReport, frameworkName string) float32 {
aggregatedInputs := make([]map[string]interface{}, 0)
aggregatedResponses := make([]map[string]interface{}, 0)
for _, ruleReport := range ctrlReport.RuleReports {
status, _, _ := ruleReport.GetRuleStatus()
if status != "warning" {
for _, ruleResponse := range ruleReport.RuleResponses {
aggregatedResponses = append(aggregatedResponses, ruleResponse.AlertObject.K8SApiObjects...)
aggregatedResponses = append(aggregatedResponses, su.externalResourceConverter(ruleResponse.AlertObject.ExternalObjects)...)
}
}
aggregatedInputs = append(aggregatedInputs, ruleReport.ListInputResources...)
}
improvementRatio := float32(1)
if ctrls, isOk := su.FrameworksScore[frameworkName]; isOk {
if scoreobj, isOk2 := ctrls[ctrlReport.Name]; isOk2 {
ctrlReport.BaseScore = scoreobj.BaseScore
improvementRatio -= scoreobj.RuntimeImprovementMultiplier
}
} else {
ctrlReport.BaseScore = 1.0
}
ctrlReport.Score = ctrlReport.BaseScore * su.resourceRules(aggregatedResponses)
ctrlReport.ARMOImprovement = ctrlReport.Score * improvementRatio
return ctrlReport.BaseScore * su.resourceRules(aggregatedInputs)
}
func getPostureFrameworksScores(weightPath string) map[string]map[string]ControlScoreWeights {
if len(weightPath) != 0 {
weightPath = weightPath + "/"
}
frameworksScoreMap := make(map[string]map[string]ControlScoreWeights)
dat, err := ioutil.ReadFile(weightPath + "frameworkdict.json")
if err != nil {
return nil
}
if err := json.Unmarshal(dat, &frameworksScoreMap); err != nil {
return nil
}
return frameworksScoreMap
}
func getPostureResourceScores(weightPath string) map[string]float32 {
if len(weightPath) != 0 {
weightPath = weightPath + "/"
}
resourceScoreMap := make(map[string]float32)
dat, err := ioutil.ReadFile(weightPath + "resourcesdict.json")
if err != nil {
return nil
}
if err := json.Unmarshal(dat, &resourceScoreMap); err != nil {
return nil
}
return resourceScoreMap
}
func NewScore(k8sapiobj *k8sinterface.KubernetesApi, configPath string) *ScoreUtil {
if postureScore == nil {
postureScore = &ScoreUtil{
ResourceTypeScores: getPostureResourceScores(configPath),
FrameworksScore: getPostureFrameworksScores(configPath),
configPath: configPath,
}
}
return postureScore
}

View File

@@ -0,0 +1,77 @@
package score
import (
"encoding/json"
"io/ioutil"
"strings"
k8sinterface "github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
)
func loadResourcesMock() []map[string]interface{} {
resources := make([]map[string]interface{}, 0)
dat, err := ioutil.ReadFile("resourcemocks.json")
if err != nil {
return resources
}
if err := json.Unmarshal(dat, &resources); err != nil {
return resources
}
return resources
}
func getResouceByType(desiredType string) map[string]interface{} {
rsrcs := loadResourcesMock()
if rsrcs == nil {
return nil
}
for _, v := range rsrcs {
wl := k8sinterface.NewWorkloadObj(v)
if wl != nil {
if strings.ToLower(wl.GetKind()) == desiredType {
return v
}
continue
} else {
for k := range v {
if k == desiredType {
return v
}
}
}
}
return nil
}
func loadFrameworkMock() *opapolicy.FrameworkReport {
report := &opapolicy.FrameworkReport{}
dat, err := ioutil.ReadFile("frameworkmock.json")
if err != nil {
return report
}
if err := json.Unmarshal(dat, &report); err != nil {
return report
}
return report
}
func getMITREFrameworkResultMock() []opapolicy.FrameworkReport {
l := make([]opapolicy.FrameworkReport, 0)
report := loadFrameworkMock()
resources := loadResourcesMock()
if report != nil && resources != nil {
report.ControlReports[0].RuleReports[0].ListInputResources = resources
l = append(l, *report)
}
return l
}

View File

@@ -0,0 +1,65 @@
package score
import (
"testing"
)
func TestFrameworkMock(t *testing.T) {
r := getMITREFrameworkResultMock()
su := NewScore(nil, "")
var epsilon float32 = 0.001
su.Calculate(r)
var sumweights float32 = 0.0
for _, v := range su.ResourceTypeScores {
sumweights += v
}
for _, framework := range r {
if framework.Score < 1 {
t.Errorf("framework %s invalid calculation1: %v", framework.Name, framework)
}
if framework.Score > framework.WCSScore+epsilon {
t.Errorf("framework %s invalid calculation2: %v", framework.Name, framework)
}
if framework.ARMOImprovement > framework.Score+epsilon {
t.Errorf("framework %s invalid calculation3: %v", framework.Name, framework)
}
if framework.ControlReports[0].Score*sumweights <= 0+epsilon {
t.Errorf("framework %s invalid calculation4: %v", framework.Name, framework)
}
}
//
}
func TestDaemonsetRule(t *testing.T) {
desiredType := "daemonset"
r := getResouceByType(desiredType)
if r == nil {
t.Errorf("no %v was found in the mock, should be 1", desiredType)
}
su := NewScore(nil, "")
resources := []map[string]interface{}{r}
weights := su.resourceRules(resources)
expecting := 13 * su.ResourceTypeScores[desiredType]
if weights != expecting {
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
}
}
func TestMultipleReplicasRule(t *testing.T) {
desiredType := "deployment"
r := getResouceByType(desiredType)
if r == nil {
t.Errorf("no %v was found in the mock, should be 1", desiredType)
}
su := NewScore(nil, "")
resources := []map[string]interface{}{r}
weights := su.resourceRules(resources)
expecting := 3 * su.ResourceTypeScores[desiredType] * su.ResourceTypeScores["replicaset"]
if weights != expecting {
t.Errorf("no %v unexpected weights were calculated expecting: %v got %v", desiredType, expecting, weights)
}
}

View File

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